@fps-games/editor 0.1.2-beta.2 → 0.1.3-beta.2

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 (83) hide show
  1. package/dist/local-editor-harness.d.ts +39 -7
  2. package/dist/local-editor-harness.d.ts.map +1 -1
  3. package/dist/local-editor-harness.js +379 -119
  4. package/dist/local-editor-harness.js.map +1 -1
  5. package/node_modules/@fps-games/babylon-renderer/dist/index.d.ts +5 -0
  6. package/node_modules/@fps-games/babylon-renderer/dist/index.d.ts.map +1 -0
  7. package/node_modules/@fps-games/babylon-renderer/dist/index.js +5 -0
  8. package/node_modules/@fps-games/babylon-renderer/dist/index.js.map +1 -0
  9. package/node_modules/@fps-games/babylon-renderer/dist/lighting/directional-light-angles.d.ts +17 -0
  10. package/node_modules/@fps-games/babylon-renderer/dist/lighting/directional-light-angles.d.ts.map +1 -0
  11. package/node_modules/@fps-games/babylon-renderer/dist/lighting/directional-light-angles.js +64 -0
  12. package/node_modules/@fps-games/babylon-renderer/dist/lighting/directional-light-angles.js.map +1 -0
  13. package/node_modules/@fps-games/babylon-renderer/dist/shadows/planar-shadow-shaders.d.ts +7 -0
  14. package/node_modules/@fps-games/babylon-renderer/dist/shadows/planar-shadow-shaders.d.ts.map +1 -0
  15. package/node_modules/@fps-games/babylon-renderer/dist/shadows/planar-shadow-shaders.js +176 -0
  16. package/node_modules/@fps-games/babylon-renderer/dist/shadows/planar-shadow-shaders.js.map +1 -0
  17. package/node_modules/@fps-games/babylon-renderer/dist/shadows/planar-shadow-system.d.ts +5 -0
  18. package/node_modules/@fps-games/babylon-renderer/dist/shadows/planar-shadow-system.d.ts.map +1 -0
  19. package/node_modules/@fps-games/babylon-renderer/dist/shadows/planar-shadow-system.js +837 -0
  20. package/node_modules/@fps-games/babylon-renderer/dist/shadows/planar-shadow-system.js.map +1 -0
  21. package/node_modules/@fps-games/babylon-renderer/dist/shadows/types.d.ts +96 -0
  22. package/node_modules/@fps-games/babylon-renderer/dist/shadows/types.d.ts.map +1 -0
  23. package/node_modules/@fps-games/babylon-renderer/dist/shadows/types.js +45 -0
  24. package/node_modules/@fps-games/babylon-renderer/dist/shadows/types.js.map +1 -0
  25. package/node_modules/@fps-games/babylon-renderer/package.json +20 -0
  26. package/node_modules/@fps-games/editor-babylon/dist/editor-sky.d.ts +2 -2
  27. package/node_modules/@fps-games/editor-babylon/dist/editor-sky.d.ts.map +1 -1
  28. package/node_modules/@fps-games/editor-babylon/dist/editor-sky.js +31 -15
  29. package/node_modules/@fps-games/editor-babylon/dist/editor-sky.js.map +1 -1
  30. package/node_modules/@fps-games/editor-babylon/dist/editor-world.d.ts +10 -0
  31. package/node_modules/@fps-games/editor-babylon/dist/editor-world.d.ts.map +1 -1
  32. package/node_modules/@fps-games/editor-babylon/dist/editor-world.js +31 -9
  33. package/node_modules/@fps-games/editor-babylon/dist/editor-world.js.map +1 -1
  34. package/node_modules/@fps-games/editor-babylon/dist/index.d.ts +1 -0
  35. package/node_modules/@fps-games/editor-babylon/dist/index.d.ts.map +1 -1
  36. package/node_modules/@fps-games/editor-babylon/dist/index.js +1 -0
  37. package/node_modules/@fps-games/editor-babylon/dist/index.js.map +1 -1
  38. package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.d.ts +23 -0
  39. package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.d.ts.map +1 -1
  40. package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.js +34 -4
  41. package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.js.map +1 -1
  42. package/node_modules/@fps-games/editor-babylon/dist/material-property-adapter.d.ts +3 -1
  43. package/node_modules/@fps-games/editor-babylon/dist/material-property-adapter.d.ts.map +1 -1
  44. package/node_modules/@fps-games/editor-babylon/dist/material-property-adapter.js +166 -0
  45. package/node_modules/@fps-games/editor-babylon/dist/material-property-adapter.js.map +1 -1
  46. package/node_modules/@fps-games/editor-babylon/dist/projection.d.ts +12 -1
  47. package/node_modules/@fps-games/editor-babylon/dist/projection.d.ts.map +1 -1
  48. package/node_modules/@fps-games/editor-babylon/dist/projection.js +231 -32
  49. package/node_modules/@fps-games/editor-babylon/dist/projection.js.map +1 -1
  50. package/node_modules/@fps-games/editor-babylon/dist/shadow-preview-controller.d.ts +22 -0
  51. package/node_modules/@fps-games/editor-babylon/dist/shadow-preview-controller.d.ts.map +1 -0
  52. package/node_modules/@fps-games/editor-babylon/dist/shadow-preview-controller.js +71 -0
  53. package/node_modules/@fps-games/editor-babylon/dist/shadow-preview-controller.js.map +1 -0
  54. package/node_modules/@fps-games/editor-babylon/dist/types.d.ts +2 -1
  55. package/node_modules/@fps-games/editor-babylon/dist/types.d.ts.map +1 -1
  56. package/node_modules/@fps-games/editor-babylon/package.json +5 -4
  57. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panel-registry.d.ts +3 -1
  58. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panel-registry.d.ts.map +1 -1
  59. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panel-registry.js +7 -0
  60. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panel-registry.js.map +1 -1
  61. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.d.ts +8 -3
  62. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.d.ts.map +1 -1
  63. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.js +645 -20
  64. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.js.map +1 -1
  65. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts +147 -3
  66. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts.map +1 -1
  67. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.d.ts +6 -0
  68. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.d.ts.map +1 -1
  69. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.js +1 -0
  70. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.js.map +1 -1
  71. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.d.ts +2 -4
  72. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.d.ts.map +1 -1
  73. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js +1060 -86
  74. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js.map +1 -1
  75. package/node_modules/@fps-games/editor-browser/package.json +2 -2
  76. package/node_modules/@fps-games/editor-core/package.json +2 -2
  77. package/node_modules/@fps-games/editor-forge-play/package.json +2 -2
  78. package/node_modules/@fps-games/editor-protocol/dist/index.d.ts +61 -0
  79. package/node_modules/@fps-games/editor-protocol/dist/index.d.ts.map +1 -1
  80. package/node_modules/@fps-games/editor-protocol/dist/index.js +157 -0
  81. package/node_modules/@fps-games/editor-protocol/dist/index.js.map +1 -1
  82. package/node_modules/@fps-games/editor-protocol/package.json +1 -1
  83. package/package.json +8 -6
@@ -1,6 +1,6 @@
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
6
  import { createLocalEditorSceneViewInteractionRuntime, } from './local-editor-scene-view-interaction-runtime';
@@ -13,21 +13,28 @@ export function createLocalEditorHarness(options) {
13
13
  source: null,
14
14
  assets: [],
15
15
  assetFilter: '',
16
+ selectedAssetId: null,
16
17
  babylon: null,
17
18
  engine: null,
18
19
  world: null,
19
20
  grid: null,
20
21
  gridVisible: true,
21
22
  projection: null,
23
+ shadowPreview: null,
22
24
  gizmo: null,
23
- mainCameraPreview: null,
24
- mainCameraPreviewEnabled: false,
25
+ sceneCameraPreview: null,
26
+ sceneCameraPreviewEnabled: false,
25
27
  sceneViewInput: null,
26
28
  sceneViewCamera: null,
27
29
  sceneViewInteraction: null,
28
30
  sceneViewMeasurement: null,
29
31
  sceneViewSpatialOverlay: null,
30
32
  selectionController: null,
33
+ sceneRenderScheduler: null,
34
+ viewportRenderCoordinator: null,
35
+ sceneFrameStats: null,
36
+ worldAppearanceKey: '',
37
+ worldRenderingKey: '',
31
38
  boxSelection: null,
32
39
  transformTool: 'select',
33
40
  transformSpace: 'world',
@@ -38,9 +45,6 @@ export function createLocalEditorHarness(options) {
38
45
  viewportSpatialOverlay: createEmptyEditorViewportSpatialOverlayState(),
39
46
  duplicateDrag: null,
40
47
  armedPlacement: null,
41
- sceneRenderScheduler: null,
42
- viewportRenderCoordinator: null,
43
- sceneFrameStats: null,
44
48
  resizeHandler: null,
45
49
  status: 'Game running',
46
50
  statusTone: 'default',
@@ -85,6 +89,14 @@ export function createLocalEditorHarness(options) {
85
89
  if (createAssetFromBrowserIntent(state, options, assetId))
86
90
  harness.render();
87
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
+ },
88
100
  onSelectHierarchyItem: (input) => {
89
101
  if (selectItem(state, options, input))
90
102
  harness.render();
@@ -146,25 +158,43 @@ export function createLocalEditorHarness(options) {
146
158
  if (patched || state.status !== previousStatus)
147
159
  harness.render();
148
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,
149
179
  onTransformToolChange: (tool) => {
150
180
  state.transformTool = tool;
151
181
  state.transformConstraint = normalizeTransformConstraint(tool, state.transformConstraint);
152
182
  state.gizmo?.setTool(tool);
153
183
  state.gizmo?.setConstraint(state.transformConstraint);
154
- requestEditorSceneFrame(state, 'transform-tool-change');
155
184
  harness.render();
185
+ requestEditorSceneFrame(state, 'transform-tool-change');
156
186
  },
157
187
  onTransformSpaceChange: (space) => {
158
188
  state.transformSpace = space;
159
189
  state.gizmo?.setSpace(space);
160
- requestEditorSceneFrame(state, 'transform-space-change');
161
190
  harness.render();
191
+ requestEditorSceneFrame(state, 'transform-space-change');
162
192
  },
163
193
  onTransformConstraintChange: (constraint) => {
164
194
  state.transformConstraint = normalizeTransformConstraint(state.transformTool, constraint);
165
195
  state.gizmo?.setConstraint(state.transformConstraint);
166
- requestEditorSceneFrame(state, 'transform-constraint-change');
167
196
  harness.render();
197
+ requestEditorSceneFrame(state, 'transform-constraint-change');
168
198
  },
169
199
  onTransformSnapEnabledChange: (enabled) => {
170
200
  state.transformOperationSettings = updateTransformOperationSettings(state.transformOperationSettings, {
@@ -175,8 +205,8 @@ export function createLocalEditorHarness(options) {
175
205
  });
176
206
  state.gizmo?.setOperationSettings(state.transformOperationSettings);
177
207
  state.status = enabled ? 'Transform snap enabled' : 'Transform snap disabled';
178
- requestEditorSceneFrame(state, 'transform-operation-settings-change');
179
208
  harness.render();
209
+ requestEditorSceneFrame(state, 'transform-operation-settings-change');
180
210
  },
181
211
  onTransformSnapStepChange: (input) => {
182
212
  const value = normalizePositiveStep(input.value);
@@ -192,8 +222,8 @@ export function createLocalEditorHarness(options) {
192
222
  state.transformOperationSettings = updateTransformOperationSettings(state.transformOperationSettings, { snap });
193
223
  state.gizmo?.setOperationSettings(state.transformOperationSettings);
194
224
  state.status = `Transform snap ${input.kind} step ${value}`;
195
- requestEditorSceneFrame(state, 'transform-operation-settings-change');
196
225
  harness.render();
226
+ requestEditorSceneFrame(state, 'transform-operation-settings-change');
197
227
  },
198
228
  onPlacementModeChange: (mode) => {
199
229
  state.transformOperationSettings = updateTransformOperationSettings(state.transformOperationSettings, {
@@ -203,8 +233,8 @@ export function createLocalEditorHarness(options) {
203
233
  if (state.transformOperationSettings.placementMode === 'off')
204
234
  clearArmedPlacement(state);
205
235
  state.status = `Placement mode: ${state.transformOperationSettings.placementMode}`;
206
- requestEditorSceneFrame(state, 'placement-mode-change');
207
236
  harness.render();
237
+ requestEditorSceneFrame(state, 'placement-mode-change');
208
238
  },
209
239
  onTransformAction: (action) => {
210
240
  if (executeTransformAction(state, options, action))
@@ -230,8 +260,8 @@ export function createLocalEditorHarness(options) {
230
260
  if (clearViewportMeasurement(state))
231
261
  harness.render();
232
262
  },
233
- onMainCameraPreviewToggle: (enabled) => {
234
- if (setMainCameraPreviewEnabled(state, options, enabled))
263
+ onSceneCameraPreviewToggle: (enabled) => {
264
+ if (setSceneCameraPreviewEnabled(state, options, enabled))
235
265
  harness.render();
236
266
  },
237
267
  onGridVisibleChange: (visible) => {
@@ -248,20 +278,17 @@ export function createLocalEditorHarness(options) {
248
278
  },
249
279
  onCancelActiveOperation: () => {
250
280
  cancelActiveOperation(state);
251
- requestEditorSceneFrame(state, 'cancel-active-operation');
252
281
  harness.render();
253
282
  },
254
283
  },
255
284
  });
256
- const renderUi = () => {
257
- syncViewportCameraState(state);
258
- syncViewportMeasurementState(state);
259
- syncViewportSpatialOverlay(state);
260
- ui.update(createUiState(state, options));
261
- };
262
285
  harness = {
263
286
  render() {
264
- renderUi();
287
+ syncSceneCameraPreview(state, options);
288
+ syncViewportCameraState(state);
289
+ syncViewportMeasurementState(state);
290
+ syncViewportSpatialOverlay(state);
291
+ ui.update(createUiState(state, options));
265
292
  },
266
293
  notifyViewportRevealed(reason = 'viewport-revealed') {
267
294
  state.viewportRenderCoordinator?.requestRevealFrame(reason);
@@ -512,11 +539,11 @@ function cloneViewportMeasurement(measurement) {
512
539
  };
513
540
  }
514
541
  function setViewportViewPreset(state, preset) {
515
- if (state.mainCameraPreviewEnabled) {
516
- state.status = 'Viewport view unavailable during Main Camera preview';
542
+ if (state.sceneCameraPreviewEnabled) {
543
+ state.status = 'Viewport view unavailable during Scene Camera preview';
517
544
  state.statusTone = 'warning';
518
545
  state.statusToneStatus = state.status;
519
- state.statusDetails = 'Disable Main Camera preview before switching editor viewport views.';
546
+ state.statusDetails = 'Disable Scene Camera preview before switching editor viewport views.';
520
547
  return true;
521
548
  }
522
549
  const nextPreset = normalizeViewportViewPreset(preset);
@@ -535,15 +562,15 @@ function setViewportViewPreset(state, preset) {
535
562
  state.statusTone = 'default';
536
563
  state.statusToneStatus = state.status;
537
564
  state.statusDetails = '';
538
- requestEditorSceneFrame(state, 'viewport-view-preset-change');
565
+ requestEditorSceneFrame(state, 'viewport-view-preset');
539
566
  return true;
540
567
  }
541
568
  function setViewportProjectionMode(state, mode) {
542
- if (state.mainCameraPreviewEnabled) {
543
- state.status = 'Viewport projection unavailable during Main Camera preview';
569
+ if (state.sceneCameraPreviewEnabled) {
570
+ state.status = 'Viewport projection unavailable during Scene Camera preview';
544
571
  state.statusTone = 'warning';
545
572
  state.statusToneStatus = state.status;
546
- state.statusDetails = 'Disable Main Camera preview before switching editor viewport projection.';
573
+ state.statusDetails = 'Disable Scene Camera preview before switching editor viewport projection.';
547
574
  return true;
548
575
  }
549
576
  const nextMode = normalizeViewportProjectionMode(mode);
@@ -559,7 +586,7 @@ function setViewportProjectionMode(state, mode) {
559
586
  state.statusTone = 'default';
560
587
  state.statusToneStatus = state.status;
561
588
  state.statusDetails = '';
562
- requestEditorSceneFrame(state, 'viewport-projection-mode-change');
589
+ requestEditorSceneFrame(state, 'viewport-projection-mode');
563
590
  return true;
564
591
  }
565
592
  function setViewportOverlaySettings(state, settings) {
@@ -608,6 +635,7 @@ function setViewportUtilityTool(state, tool) {
608
635
  state.statusTone = 'default';
609
636
  state.statusToneStatus = state.status;
610
637
  state.statusDetails = '';
638
+ requestEditorSceneFrame(state, 'viewport-utility-tool');
611
639
  return true;
612
640
  }
613
641
  function clearViewportMeasurement(state) {
@@ -620,6 +648,7 @@ function clearViewportMeasurement(state) {
620
648
  state.statusTone = 'default';
621
649
  state.statusToneStatus = state.status;
622
650
  state.statusDetails = '';
651
+ requestEditorSceneFrame(state, 'viewport-measurement-clear');
623
652
  return true;
624
653
  }
625
654
  export function applyLocalEditorHarnessViewportMeasurementPointerStart(state, event) {
@@ -713,19 +742,28 @@ function validateTransformActionSelection(state, action) {
713
742
  }
714
743
  async function createEditorWorld(state, options, renderUi, updateSceneFrameStats) {
715
744
  disposeEditorWorld(state);
745
+ const render = (reason = 'editor-world-update') => {
746
+ renderUi();
747
+ requestEditorSceneFrame(state, reason);
748
+ };
716
749
  const canvas = options.worldAdapter.getCanvas();
717
750
  if (!canvas)
718
751
  throw new Error('Editor canvas not found');
719
752
  const babylon = await options.worldAdapter.loadBabylon();
720
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);
721
759
  const world = createBabylonEditorWorld({
722
760
  engine,
723
761
  canvas,
724
762
  babylon,
725
763
  cameraTarget: options.world?.cameraTarget,
726
764
  cameraRadius: options.world?.cameraRadius,
727
- clearColor: options.world?.clearColor,
728
- sky: options.world?.sky,
765
+ clearColor: initialAppearance.clearColor,
766
+ sky: initialAppearance.sky,
729
767
  useRightHandedSystem: options.world?.useRightHandedSystem,
730
768
  enableDefaultCameraControls: false,
731
769
  });
@@ -752,6 +790,8 @@ async function createEditorWorld(state, options, renderUi, updateSceneFrameStats
752
790
  getCamera: () => world.scene?.activeCamera ?? world.camera ?? null,
753
791
  getEditorCamera: () => world.camera ?? null,
754
792
  }) ?? null;
793
+ if (initialAppearance.grid)
794
+ grid?.setColors(initialAppearance.grid);
755
795
  grid?.setVisible(state.gridVisible);
756
796
  const projection = createBabylonEditorProjection({
757
797
  babylon,
@@ -775,10 +815,10 @@ async function createEditorWorld(state, options, renderUi, updateSceneFrameStats
775
815
  state.status = event.targetIds.length > 1
776
816
  ? `Dragging ${event.duplicate ? 'duplicate ' : ''}${event.tool} ${event.targetIds.length} objects`
777
817
  : `Dragging ${event.duplicate ? 'duplicate ' : ''}${event.tool} ${event.nodeId ?? event.activeId ?? 'selection'}`;
778
- renderUi();
818
+ render();
779
819
  },
780
820
  onDragUpdate() {
781
- renderUi();
821
+ render();
782
822
  },
783
823
  onDragEnd(event) {
784
824
  sceneViewInteraction.endGizmoDrag();
@@ -790,14 +830,13 @@ async function createEditorWorld(state, options, renderUi, updateSceneFrameStats
790
830
  onDragCancel(event) {
791
831
  sceneViewInteraction.endGizmoDrag();
792
832
  if (event.duplicate && cancelDuplicateDrag(state, options)) {
793
- renderUi();
833
+ render();
794
834
  return;
795
835
  }
796
- requestEditorSceneFrame(state, 'gizmo-drag-cancel');
797
836
  state.status = event.targetIds.length > 1
798
837
  ? `Canceled ${event.tool} ${event.targetIds.length} objects`
799
838
  : `Canceled ${event.tool} ${event.nodeId ?? event.activeId ?? 'selection'}`;
800
- renderUi();
839
+ render();
801
840
  },
802
841
  onDragBlocked(event) {
803
842
  sceneViewInteraction.endGizmoDrag();
@@ -826,15 +865,15 @@ async function createEditorWorld(state, options, renderUi, updateSceneFrameStats
826
865
  isOperationBlocked: () => state.gizmo?.getState().dragPhase === 'dragging',
827
866
  onSelectionCommand(command) {
828
867
  if (dispatchSelectionCommand(state, options, command))
829
- renderUi();
868
+ render();
830
869
  },
831
870
  onFocusIntent(nodeId) {
832
871
  if (focusProjectionNode(state, nodeId))
833
- renderUi();
872
+ render();
834
873
  },
835
874
  onBoxSelectionChange(box) {
836
875
  state.boxSelection = box;
837
- renderUi();
876
+ render();
838
877
  },
839
878
  });
840
879
  const sceneViewMeasurement = createBabylonSceneViewMeasurementController({
@@ -853,7 +892,7 @@ async function createEditorWorld(state, options, renderUi, updateSceneFrameStats
853
892
  sceneViewInteraction.beginPointerIntent(event.state.intent);
854
893
  if (event.state.intent === 'measurement') {
855
894
  if (handleViewportMeasurementStart(state, event.originalEvent))
856
- renderUi();
895
+ render();
857
896
  return;
858
897
  }
859
898
  if (event.state.intent === 'gizmo-drag') {
@@ -862,100 +901,93 @@ async function createEditorWorld(state, options, renderUi, updateSceneFrameStats
862
901
  }
863
902
  if (event.state.intent === 'placement') {
864
903
  if (previewArmedPlacement(state, event.originalEvent))
865
- renderUi();
904
+ render();
866
905
  return;
867
906
  }
868
907
  if (event.state.intent === 'view-plane-move') {
869
908
  if (gizmo.beginViewPlaneMove(event.originalEvent))
870
- renderUi();
909
+ render();
871
910
  return;
872
911
  }
873
912
  if (event.state.intent === 'selection-click' || event.state.intent === 'box-select') {
874
913
  selectionController.beginPointerSelection(event.originalEvent);
875
- requestEditorSceneFrame(state, `pointer-${event.state.intent}-start`);
914
+ requestEditorSceneFrame(state, 'viewport-pointer-selection-start');
876
915
  }
877
916
  },
878
917
  onPointerIntentMove(event) {
879
918
  if (event.state.intent === 'measurement') {
880
919
  if (handleViewportMeasurementMove(state, event.originalEvent))
881
- renderUi();
920
+ render();
882
921
  return;
883
922
  }
884
923
  if (event.state.intent === 'placement') {
885
924
  if (previewArmedPlacement(state, event.originalEvent))
886
- renderUi();
925
+ render();
887
926
  return;
888
927
  }
889
928
  if (event.state.intent === 'view-plane-move') {
890
929
  if (gizmo.updateViewPlaneMove(event.originalEvent))
891
- renderUi();
930
+ render();
892
931
  return;
893
932
  }
894
- if (!state.mainCameraPreviewEnabled && state.sceneViewCamera?.handlePointerIntentMove(event)) {
895
- renderUi();
933
+ if (!state.sceneCameraPreviewEnabled && state.sceneViewCamera?.handlePointerIntentMove(event)) {
934
+ render();
896
935
  return;
897
936
  }
898
937
  if (event.state.intent === 'selection-click' || event.state.intent === 'box-select') {
899
938
  selectionController.updatePointerSelection(event.originalEvent, event.state.intent);
939
+ requestEditorSceneFrame(state, 'viewport-pointer-selection-update');
900
940
  }
901
- requestEditorSceneFrame(state, `pointer-${event.state.intent}`);
902
941
  },
903
942
  onPointerIntentEnd(event) {
904
943
  sceneViewInteraction.endPointerIntent(event.state.intent);
905
944
  if (event.state.intent === 'measurement') {
906
945
  if (handleViewportMeasurementEnd(state))
907
- renderUi();
946
+ render();
908
947
  return;
909
948
  }
910
949
  if (event.state.intent === 'placement') {
911
950
  if (commitArmedPlacement(state, options, event.originalEvent))
912
- renderUi();
951
+ render();
913
952
  return;
914
953
  }
915
954
  if (event.state.intent === 'view-plane-move') {
916
- if (gizmo.endViewPlaneMove(event.originalEvent)) {
917
- requestEditorSceneFrame(state, 'view-plane-move-end');
918
- renderUi();
919
- }
955
+ if (gizmo.endViewPlaneMove(event.originalEvent))
956
+ render();
920
957
  return;
921
958
  }
922
959
  if (event.state.intent === 'selection-click' || event.state.intent === 'box-select') {
923
960
  selectionController.endPointerSelection(event.originalEvent, event.state.intent);
961
+ requestEditorSceneFrame(state, 'viewport-pointer-selection-end');
924
962
  }
925
- requestEditorSceneFrame(state, `pointer-${event.state.intent}-end`);
926
963
  },
927
964
  onPointerIntentCancel(event) {
928
965
  sceneViewInteraction.cancelPointerIntent(event.state.intent);
929
966
  if (event.state.intent === 'measurement') {
930
- renderUi();
967
+ render();
931
968
  return;
932
969
  }
933
970
  if (event.state.intent === 'placement') {
934
971
  state.gizmo?.setPlacementMarker(null);
935
- requestEditorSceneFrame(state, 'placement-marker-clear');
936
- renderUi();
972
+ render();
937
973
  return;
938
974
  }
939
975
  if (event.state.intent === 'view-plane-move') {
940
976
  gizmo.cancelDrag();
941
- requestEditorSceneFrame(state, 'view-plane-move-cancel');
942
- renderUi();
977
+ render();
943
978
  return;
944
979
  }
945
980
  if (event.state.intent === 'selection-click' || event.state.intent === 'box-select') {
946
981
  selectionController.cancelBoxSelection();
947
- renderUi();
982
+ render();
948
983
  }
949
- requestEditorSceneFrame(state, `pointer-${event.state.intent}-cancel`);
950
984
  },
951
985
  onDoubleClick(event) {
952
986
  selectionController.handleDoubleClick(event);
953
987
  },
954
988
  onWheel(event) {
955
- if (!state.mainCameraPreviewEnabled && state.sceneViewCamera?.handleWheel(event)) {
956
- requestEditorSceneFrame(state, 'camera-wheel');
957
- renderUi();
958
- }
989
+ if (!state.sceneCameraPreviewEnabled && state.sceneViewCamera?.handleWheel(event))
990
+ render();
959
991
  },
960
992
  onMovementKeysChange(event) {
961
993
  sceneViewInteraction.updateMovementKeys(event.pressedMovementKeys);
@@ -968,7 +1000,7 @@ async function createEditorWorld(state, options, renderUi, updateSceneFrameStats
968
1000
  camera: world.camera,
969
1001
  input: sceneViewInput,
970
1002
  });
971
- const mainCameraPreview = createBabylonMainCameraPreviewController({
1003
+ const sceneCameraPreview = createBabylonSceneCameraPreviewController({
972
1004
  babylon,
973
1005
  scene: world.scene,
974
1006
  editorCamera: world.camera,
@@ -977,13 +1009,17 @@ async function createEditorWorld(state, options, renderUi, updateSceneFrameStats
977
1009
  babylon,
978
1010
  scene: world.scene,
979
1011
  });
980
- const document = state.session?.getState().workingDocument;
981
1012
  if (document) {
982
1013
  projection.projectNodes(options.documentAdapter.getProjectionNodes(document));
983
1014
  const selection = state.session?.getState().selection ?? { selectedIds: [], activeId: null };
984
1015
  projection.syncSelection(selection);
985
1016
  gizmo.setSelection(selection);
986
1017
  }
1018
+ const shadowPreview = createBabylonEditorShadowPreviewController({
1019
+ scene: world.scene,
1020
+ projection,
1021
+ options: initialRendering.shadowPreview,
1022
+ });
987
1023
  const resize = () => {
988
1024
  engine.resize?.();
989
1025
  viewportRenderCoordinator.requestFrame('resize');
@@ -994,8 +1030,9 @@ async function createEditorWorld(state, options, renderUi, updateSceneFrameStats
994
1030
  state.world = world;
995
1031
  state.grid = grid;
996
1032
  state.projection = projection;
1033
+ state.shadowPreview = shadowPreview;
997
1034
  state.gizmo = gizmo;
998
- state.mainCameraPreview = mainCameraPreview;
1035
+ state.sceneCameraPreview = sceneCameraPreview;
999
1036
  state.sceneViewInput = sceneViewInput;
1000
1037
  state.sceneViewCamera = sceneViewCamera;
1001
1038
  state.sceneViewMeasurement = sceneViewMeasurement;
@@ -1004,24 +1041,21 @@ async function createEditorWorld(state, options, renderUi, updateSceneFrameStats
1004
1041
  state.sceneRenderScheduler = sceneRenderScheduler;
1005
1042
  state.viewportRenderCoordinator = viewportRenderCoordinator;
1006
1043
  state.sceneFrameStats = sceneRenderScheduler.getStats();
1044
+ state.worldAppearanceKey = initialAppearanceKey;
1045
+ state.worldRenderingKey = initialRenderingKey;
1007
1046
  state.resizeHandler = resize;
1008
- await sceneRenderScheduler.waitForNextFrame('editor-world-created');
1047
+ viewportRenderCoordinator.requestFrame('editor-world-created');
1009
1048
  }
1010
1049
  function disposeEditorWorld(state) {
1011
1050
  state.sceneViewInteraction?.dispose();
1012
1051
  state.sceneViewInteraction = null;
1013
- state.viewportRenderCoordinator?.dispose();
1014
- state.viewportRenderCoordinator = null;
1015
- state.sceneRenderScheduler?.dispose();
1016
- state.sceneRenderScheduler = null;
1017
- state.sceneFrameStats = null;
1018
1052
  if (state.resizeHandler) {
1019
1053
  window.removeEventListener('resize', state.resizeHandler);
1020
1054
  state.resizeHandler = null;
1021
1055
  }
1022
- state.mainCameraPreview?.dispose();
1023
- state.mainCameraPreview = null;
1024
- state.mainCameraPreviewEnabled = false;
1056
+ state.sceneCameraPreview?.dispose();
1057
+ state.sceneCameraPreview = null;
1058
+ state.sceneCameraPreviewEnabled = false;
1025
1059
  state.sceneViewCamera?.dispose();
1026
1060
  state.sceneViewCamera = null;
1027
1061
  state.sceneViewMeasurement?.dispose();
@@ -1032,6 +1066,11 @@ function disposeEditorWorld(state) {
1032
1066
  state.sceneViewInput = null;
1033
1067
  state.selectionController?.dispose();
1034
1068
  state.selectionController = null;
1069
+ state.viewportRenderCoordinator?.dispose();
1070
+ state.viewportRenderCoordinator = null;
1071
+ state.sceneRenderScheduler?.dispose();
1072
+ state.sceneRenderScheduler = null;
1073
+ state.sceneFrameStats = null;
1035
1074
  state.boxSelection = null;
1036
1075
  state.viewportSpatialOverlay = createEmptyEditorViewportSpatialOverlayState();
1037
1076
  state.viewportMeasurement = createEmptyViewportMeasurement();
@@ -1041,10 +1080,13 @@ function disposeEditorWorld(state) {
1041
1080
  };
1042
1081
  state.gizmo?.dispose();
1043
1082
  state.gizmo = null;
1083
+ state.shadowPreview?.dispose();
1084
+ state.shadowPreview = null;
1044
1085
  state.projection?.dispose();
1045
1086
  state.projection = null;
1046
1087
  state.grid?.dispose();
1047
1088
  state.grid = null;
1089
+ state.engine?.stopRenderLoop?.();
1048
1090
  state.world?.dispose();
1049
1091
  state.engine?.dispose?.();
1050
1092
  state.babylon = null;
@@ -1054,11 +1096,11 @@ function disposeEditorWorld(state) {
1054
1096
  function requestEditorSceneFrame(state, reason) {
1055
1097
  state.viewportRenderCoordinator?.requestFrame(reason);
1056
1098
  }
1057
- async function runExclusive(state, renderUi, action) {
1099
+ async function runExclusive(state, render, action) {
1058
1100
  if (state.busy)
1059
1101
  return;
1060
1102
  state.busy = true;
1061
- renderUi();
1103
+ render();
1062
1104
  try {
1063
1105
  await action();
1064
1106
  }
@@ -1071,7 +1113,7 @@ async function runExclusive(state, renderUi, action) {
1071
1113
  }
1072
1114
  finally {
1073
1115
  state.busy = false;
1074
- renderUi();
1116
+ render();
1075
1117
  }
1076
1118
  }
1077
1119
  function selectItem(state, options, input) {
@@ -1591,11 +1633,69 @@ function syncCurrentSelectionToSceneArtifacts(state) {
1591
1633
  syncSelectionToProjection(state, state.session?.getState().selection ?? { selectedIds: [], activeId: null });
1592
1634
  }
1593
1635
  function invalidateEditorScene(state, reason, options) {
1594
- if (options && state.mainCameraPreviewEnabled) {
1595
- syncMainCameraPreview(state, options);
1636
+ if (options && state.sceneCameraPreviewEnabled) {
1637
+ syncSceneCameraPreview(state, options);
1596
1638
  }
1597
1639
  state.viewportRenderCoordinator?.invalidateScene(reason);
1598
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
+ }
1599
1699
  function executeTransformAction(state, options, action) {
1600
1700
  if (state.mode !== 'editor' || !state.session)
1601
1701
  return false;
@@ -1693,6 +1793,66 @@ function createAssetFromBrowserIntent(state, options, assetId) {
1693
1793
  }
1694
1794
  return armAssetPlacement(state, options, assetId);
1695
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
+ }
1696
1856
  function armAssetPlacement(state, options, assetId) {
1697
1857
  if (state.mode !== 'editor')
1698
1858
  return false;
@@ -1702,7 +1862,6 @@ function armAssetPlacement(state, options, assetId) {
1702
1862
  return false;
1703
1863
  state.armedPlacement = { assetId, asset };
1704
1864
  state.gizmo?.setPlacementMarker(null);
1705
- requestEditorSceneFrame(state, 'placement-arm-change');
1706
1865
  state.status = `Placement armed: ${formatAssetLabel(asset, assetId)} (${state.transformOperationSettings.placementMode})`;
1707
1866
  return true;
1708
1867
  }
@@ -1732,7 +1891,6 @@ function commitArmedPlacement(state, options, event) {
1732
1891
  if (!armed || !hit) {
1733
1892
  state.gizmo?.setPlacementMarker(null);
1734
1893
  state.status = 'Placement rejected: no hit';
1735
- requestEditorSceneFrame(state, 'placement-marker-clear');
1736
1894
  return true;
1737
1895
  }
1738
1896
  const patch = options.documentAdapter.createPlacedAssetPatch?.({
@@ -1782,11 +1940,8 @@ function pickArmedPlacementHit(state, event) {
1782
1940
  : state.gizmo?.pickPlacementHit(event.clientX, event.clientY, mode) ?? null;
1783
1941
  }
1784
1942
  function clearArmedPlacement(state) {
1785
- const hadArmedPlacement = !!state.armedPlacement;
1786
1943
  state.armedPlacement = null;
1787
1944
  state.gizmo?.setPlacementMarker(null);
1788
- if (hadArmedPlacement)
1789
- requestEditorSceneFrame(state, 'placement-clear');
1790
1945
  }
1791
1946
  function addAssetToDocument(state, options, assetId, createOptions = {}) {
1792
1947
  if (state.mode !== 'editor') {
@@ -1837,7 +1992,7 @@ function addAssetToDocument(state, options, assetId, createOptions = {}) {
1837
1992
  state.projection?.projectNode(projectedNode);
1838
1993
  if (selectionResult)
1839
1994
  syncSelectionToProjection(state, selectionResult.selection);
1840
- invalidateEditorScene(state, 'projection-project-node', options);
1995
+ invalidateEditorScene(state, 'projection-project-node');
1841
1996
  }
1842
1997
  }
1843
1998
  return {
@@ -1924,6 +2079,93 @@ function patchSerializedProperty(state, options, input) {
1924
2079
  state.status = patch.label ?? `Patched ${payload.path}`;
1925
2080
  return true;
1926
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
+ }
1927
2169
  function createInspectorEditTransaction(state, options, document, input, targetIds) {
1928
2170
  const property = findInspectorPropertyForEdit(state, options, document, input, targetIds);
1929
2171
  if (!property) {
@@ -2241,7 +2483,9 @@ function rebuildProjectionFromDocument(state, options, document, selection) {
2241
2483
  state.projection?.rebuild(options.documentAdapter.getProjectionNodes(document));
2242
2484
  const sanitized = sanitizeSelection(state, options, document, selection);
2243
2485
  syncSelectionToProjection(state, sanitized ?? selection);
2244
- 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');
2245
2489
  }
2246
2490
  function syncProjectionForDispatchResult(state, options, result, changedId) {
2247
2491
  if (result.documentChanged && changedId && result.workingDocument) {
@@ -2257,8 +2501,12 @@ function syncProjectionForDispatchResult(state, options, result, changedId) {
2257
2501
  const sanitized = result.workingDocument ? sanitizeSelection(state, options, result.workingDocument, selection) : null;
2258
2502
  syncSelectionToProjection(state, sanitized ?? selection);
2259
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
+ }
2260
2508
  if (result.documentChanged || result.selectionChanged) {
2261
- invalidateEditorScene(state, 'projection-dispatch-result', options);
2509
+ invalidateEditorScene(state, 'projection-dispatch-result');
2262
2510
  }
2263
2511
  }
2264
2512
  function syncProjectionForChangedIds(state, options, document, changedIds) {
@@ -2269,7 +2517,9 @@ function syncProjectionForChangedIds(state, options, document, changedIds) {
2269
2517
  }
2270
2518
  const selection = state.session?.getState().selection ?? { selectedIds: [], activeId: null };
2271
2519
  syncSelectionToProjection(state, selection);
2272
- 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');
2273
2523
  }
2274
2524
  function reprojectProjectionForChangedIds(state, options, document, changedIds) {
2275
2525
  for (const changedId of changedIds) {
@@ -2278,28 +2528,29 @@ function reprojectProjectionForChangedIds(state, options, document, changedIds)
2278
2528
  state.projection?.projectNode(projectedNode);
2279
2529
  }
2280
2530
  syncCurrentSelectionToSceneArtifacts(state);
2531
+ syncEditorWorldAppearanceFromDocument(state, options, document, 'world-appearance-reproject-changed-ids');
2532
+ syncEditorWorldRenderingFromDocument(state, options, document, 'world-rendering-reproject-changed-ids', true);
2281
2533
  invalidateEditorScene(state, 'projection-reproject-changed-ids', options);
2282
2534
  }
2283
- function setMainCameraPreviewEnabled(state, options, enabled) {
2535
+ function setSceneCameraPreviewEnabled(state, options, enabled) {
2284
2536
  if (!enabled) {
2285
- state.mainCameraPreviewEnabled = false;
2286
- state.mainCameraPreview?.setActive(false);
2287
- state.status = 'Main Camera preview disabled';
2288
- 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');
2289
2541
  return true;
2290
2542
  }
2291
- state.mainCameraPreviewEnabled = true;
2292
- if (!syncMainCameraPreview(state, options)) {
2293
- state.mainCameraPreviewEnabled = false;
2294
- 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';
2295
2547
  state.statusTone = 'warning';
2296
2548
  state.statusToneStatus = state.status;
2297
2549
  state.statusDetails = 'The current document did not provide a Main Camera preview rig.';
2298
- requestEditorSceneFrame(state, 'main-camera-preview-toggle');
2299
2550
  return true;
2300
2551
  }
2301
- state.status = 'Main Camera preview enabled';
2302
- requestEditorSceneFrame(state, 'main-camera-preview-toggle');
2552
+ state.status = 'Scene Camera preview enabled';
2553
+ requestEditorSceneFrame(state, 'scene-camera-preview-toggle');
2303
2554
  return true;
2304
2555
  }
2305
2556
  function setGridVisible(state, visible) {
@@ -2315,16 +2566,16 @@ function setGridVisible(state, visible) {
2315
2566
  requestEditorSceneFrame(state, 'grid-visible-change');
2316
2567
  return true;
2317
2568
  }
2318
- function syncMainCameraPreview(state, options) {
2319
- const controller = state.mainCameraPreview;
2569
+ function syncSceneCameraPreview(state, options) {
2570
+ const controller = state.sceneCameraPreview;
2320
2571
  if (!controller)
2321
2572
  return false;
2322
- if (!state.mainCameraPreviewEnabled) {
2573
+ if (!state.sceneCameraPreviewEnabled) {
2323
2574
  controller.setActive(false);
2324
2575
  return false;
2325
2576
  }
2326
2577
  const document = state.session?.getState().workingDocument ?? null;
2327
- const rig = document ? options.documentAdapter.getMainCameraPreviewRig?.(document) ?? null : null;
2578
+ const rig = document ? options.documentAdapter.getSceneCameraPreviewRig?.(document) ?? null : null;
2328
2579
  if (!rig) {
2329
2580
  controller.setActive(false);
2330
2581
  return false;
@@ -2371,9 +2622,9 @@ function syncViewportMeasurementState(state) {
2371
2622
  if (measurement)
2372
2623
  state.viewportMeasurement = measurement;
2373
2624
  }
2374
- function hasMainCameraPreviewRig(state, options) {
2625
+ function hasSceneCameraPreviewRig(state, options) {
2375
2626
  const document = state.session?.getState().workingDocument ?? null;
2376
- return !!document && !!options.documentAdapter.getMainCameraPreviewRig?.(document);
2627
+ return !!document && !!options.documentAdapter.getSceneCameraPreviewRig?.(document);
2377
2628
  }
2378
2629
  function createUiState(state, options) {
2379
2630
  const sessionState = state.session?.getState();
@@ -2398,8 +2649,13 @@ function createUiState(state, options) {
2398
2649
  const inspectorMultiObject = document && inspectorMultiObjectBase
2399
2650
  ? withRuntimeInspectorSections(state, options, document, inspectorMultiObjectBase)
2400
2651
  : inspectorMultiObjectBase;
2401
- const assets = dedupeLocalEditorBrowserAssetItems(state.assets
2402
- .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
+ ]);
2403
2659
  return {
2404
2660
  mode: state.mode,
2405
2661
  busy: state.busy,
@@ -2409,6 +2665,7 @@ function createUiState(state, options) {
2409
2665
  summary: state.summary,
2410
2666
  assetFilter: state.assetFilter,
2411
2667
  assets,
2668
+ selectedAssetId: state.selectedAssetId,
2412
2669
  assetCountLabel: `${assets.length} assets`,
2413
2670
  hierarchy: document ? options.documentAdapter.getHierarchyItems(document) : [],
2414
2671
  selectedIds,
@@ -2421,6 +2678,7 @@ function createUiState(state, options) {
2421
2678
  serializedMultiObject,
2422
2679
  inspectorObject,
2423
2680
  inspectorMultiObject,
2681
+ renderingPanel: document ? options.documentAdapter.getRenderingPanelState?.(document) ?? null : null,
2424
2682
  boxSelection: state.boxSelection,
2425
2683
  coordinateAxes: options.world?.coordinateAxes === false
2426
2684
  ? null
@@ -2445,9 +2703,9 @@ function createUiState(state, options) {
2445
2703
  sceneFrameStats: state.sceneFrameStats
2446
2704
  ? { ...state.sceneFrameStats, activeReasons: [...state.sceneFrameStats.activeReasons] }
2447
2705
  : null,
2448
- mainCameraPreview: {
2449
- enabled: state.mainCameraPreviewEnabled,
2450
- available: hasMainCameraPreviewRig(state, options),
2706
+ sceneCameraPreview: {
2707
+ enabled: state.sceneCameraPreviewEnabled,
2708
+ available: hasSceneCameraPreviewRig(state, options),
2451
2709
  },
2452
2710
  grid: {
2453
2711
  visible: state.gridVisible,
@@ -2479,7 +2737,7 @@ function createSceneViewCoordinateAxesState(state) {
2479
2737
  return null;
2480
2738
  return {
2481
2739
  projectionMode: readSceneViewFreeCameraProjectionMode(state, camera),
2482
- projectionToggleDisabled: state.mainCameraPreviewEnabled,
2740
+ projectionToggleDisabled: state.sceneCameraPreviewEnabled,
2483
2741
  axes: [
2484
2742
  createSceneViewCoordinateAxis('x', 'X', '#ff4b70', { x: 1, y: 0, z: 0 }, right, up, forward),
2485
2743
  createSceneViewCoordinateAxis('y', 'Y', '#75ff42', { x: 0, y: 1, z: 0 }, right, up, forward),
@@ -2761,7 +3019,7 @@ function findAssetByResolvedId(state, options, assetId) {
2761
3019
  selected = { asset, item };
2762
3020
  }
2763
3021
  }
2764
- return selected?.asset;
3022
+ return selected?.asset ?? null;
2765
3023
  }
2766
3024
  function toBrowserAssetItem(options, asset) {
2767
3025
  return options.worldAdapter.toBrowserAssetItem?.(asset)
@@ -2775,6 +3033,8 @@ function toBrowserAssetItem(options, asset) {
2775
3033
  origin: asset.origin,
2776
3034
  dedupeKey: asset.dedupeKey,
2777
3035
  placeable: asset.placeable,
3036
+ preview: asset.preview,
3037
+ material: asset.material,
2778
3038
  meta: asset.meta,
2779
3039
  disabled: asset.disabled ?? asset.placeable === false,
2780
3040
  };