@fps-games/editor 0.1.1-beta.6 → 0.1.1-beta.8

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 (115) hide show
  1. package/dist/local-editor-harness.d.ts +26 -3
  2. package/dist/local-editor-harness.d.ts.map +1 -1
  3. package/dist/local-editor-harness.js +534 -36
  4. package/dist/local-editor-harness.js.map +1 -1
  5. package/node_modules/@fps-games/editor-babylon/dist/editor-sky.d.ts +33 -0
  6. package/node_modules/@fps-games/editor-babylon/dist/editor-sky.d.ts.map +1 -0
  7. package/node_modules/@fps-games/editor-babylon/dist/editor-sky.js +231 -0
  8. package/node_modules/@fps-games/editor-babylon/dist/editor-sky.js.map +1 -0
  9. package/node_modules/@fps-games/editor-babylon/dist/editor-world.d.ts +3 -0
  10. package/node_modules/@fps-games/editor-babylon/dist/editor-world.d.ts.map +1 -1
  11. package/node_modules/@fps-games/editor-babylon/dist/editor-world.js +8 -0
  12. package/node_modules/@fps-games/editor-babylon/dist/editor-world.js.map +1 -1
  13. package/node_modules/@fps-games/editor-babylon/dist/index.d.ts +4 -0
  14. package/node_modules/@fps-games/editor-babylon/dist/index.d.ts.map +1 -1
  15. package/node_modules/@fps-games/editor-babylon/dist/index.js +4 -0
  16. package/node_modules/@fps-games/editor-babylon/dist/index.js.map +1 -1
  17. package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.d.ts +40 -0
  18. package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.d.ts.map +1 -0
  19. package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.js +237 -0
  20. package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.js.map +1 -0
  21. package/node_modules/@fps-games/editor-babylon/dist/projection-selection-controller.d.ts +1 -1
  22. package/node_modules/@fps-games/editor-babylon/dist/projection-selection-controller.d.ts.map +1 -1
  23. package/node_modules/@fps-games/editor-babylon/dist/projection-selection-controller.js +15 -50
  24. package/node_modules/@fps-games/editor-babylon/dist/projection-selection-controller.js.map +1 -1
  25. package/node_modules/@fps-games/editor-babylon/dist/projection.d.ts +5 -0
  26. package/node_modules/@fps-games/editor-babylon/dist/projection.d.ts.map +1 -1
  27. package/node_modules/@fps-games/editor-babylon/dist/projection.js +61 -8
  28. package/node_modules/@fps-games/editor-babylon/dist/projection.js.map +1 -1
  29. package/node_modules/@fps-games/editor-babylon/dist/scene-view-camera-controller.d.ts +18 -0
  30. package/node_modules/@fps-games/editor-babylon/dist/scene-view-camera-controller.d.ts.map +1 -1
  31. package/node_modules/@fps-games/editor-babylon/dist/scene-view-camera-controller.js +142 -0
  32. package/node_modules/@fps-games/editor-babylon/dist/scene-view-camera-controller.js.map +1 -1
  33. package/node_modules/@fps-games/editor-babylon/dist/scene-view-input-controller.d.ts +1 -0
  34. package/node_modules/@fps-games/editor-babylon/dist/scene-view-input-controller.d.ts.map +1 -1
  35. package/node_modules/@fps-games/editor-babylon/dist/scene-view-input-controller.js +3 -1
  36. package/node_modules/@fps-games/editor-babylon/dist/scene-view-input-controller.js.map +1 -1
  37. package/node_modules/@fps-games/editor-babylon/dist/scene-view-measurement-controller.d.ts +20 -0
  38. package/node_modules/@fps-games/editor-babylon/dist/scene-view-measurement-controller.d.ts.map +1 -0
  39. package/node_modules/@fps-games/editor-babylon/dist/scene-view-measurement-controller.js +187 -0
  40. package/node_modules/@fps-games/editor-babylon/dist/scene-view-measurement-controller.js.map +1 -0
  41. package/node_modules/@fps-games/editor-babylon/dist/scene-view-spatial-overlay-controller.d.ts +20 -0
  42. package/node_modules/@fps-games/editor-babylon/dist/scene-view-spatial-overlay-controller.d.ts.map +1 -0
  43. package/node_modules/@fps-games/editor-babylon/dist/scene-view-spatial-overlay-controller.js +188 -0
  44. package/node_modules/@fps-games/editor-babylon/dist/scene-view-spatial-overlay-controller.js.map +1 -0
  45. package/node_modules/@fps-games/editor-babylon/dist/types.d.ts +11 -0
  46. package/node_modules/@fps-games/editor-babylon/dist/types.d.ts.map +1 -1
  47. package/node_modules/@fps-games/editor-babylon/package.json +4 -4
  48. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-context-menu.js +97 -46
  49. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-context-menu.js.map +1 -1
  50. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.d.ts +5 -0
  51. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.d.ts.map +1 -1
  52. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.js +57 -7
  53. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.js.map +1 -1
  54. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.d.ts.map +1 -1
  55. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.js +61 -5
  56. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.js.map +1 -1
  57. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-tree.js +0 -2
  58. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-tree.js.map +1 -1
  59. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-view.d.ts.map +1 -1
  60. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-view.js +6 -2
  61. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-view.js.map +1 -1
  62. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-icons.d.ts +1 -1
  63. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-icons.d.ts.map +1 -1
  64. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-icons.js +58 -0
  65. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-icons.js.map +1 -1
  66. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.d.ts +8 -1
  67. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.d.ts.map +1 -1
  68. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.js +358 -53
  69. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.js.map +1 -1
  70. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.d.ts +5 -0
  71. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.d.ts.map +1 -1
  72. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.js +28 -8
  73. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.js.map +1 -1
  74. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.d.ts.map +1 -1
  75. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.js +4 -1
  76. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.js.map +1 -1
  77. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-theme.d.ts.map +1 -1
  78. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-theme.js +14 -0
  79. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-theme.js.map +1 -1
  80. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-tooltip.d.ts +11 -0
  81. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-tooltip.d.ts.map +1 -0
  82. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-tooltip.js +148 -0
  83. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-tooltip.js.map +1 -0
  84. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts +57 -1
  85. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts.map +1 -1
  86. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.d.ts +5 -4
  87. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.d.ts.map +1 -1
  88. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js +1108 -205
  89. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js.map +1 -1
  90. package/node_modules/@fps-games/editor-browser/package.json +2 -2
  91. package/node_modules/@fps-games/editor-core/dist/index.d.ts +2 -0
  92. package/node_modules/@fps-games/editor-core/dist/index.d.ts.map +1 -1
  93. package/node_modules/@fps-games/editor-core/dist/index.js +2 -0
  94. package/node_modules/@fps-games/editor-core/dist/index.js.map +1 -1
  95. package/node_modules/@fps-games/editor-core/dist/inspector.d.ts +6 -1
  96. package/node_modules/@fps-games/editor-core/dist/inspector.d.ts.map +1 -1
  97. package/node_modules/@fps-games/editor-core/dist/inspector.js +6 -1
  98. package/node_modules/@fps-games/editor-core/dist/inspector.js.map +1 -1
  99. package/node_modules/@fps-games/editor-core/dist/scene-graph.d.ts +7 -0
  100. package/node_modules/@fps-games/editor-core/dist/scene-graph.d.ts.map +1 -1
  101. package/node_modules/@fps-games/editor-core/dist/scene-graph.js.map +1 -1
  102. package/node_modules/@fps-games/editor-core/dist/scene-view-input.d.ts +1 -1
  103. package/node_modules/@fps-games/editor-core/dist/scene-view-input.d.ts.map +1 -1
  104. package/node_modules/@fps-games/editor-core/dist/selection-strategy.d.ts +12 -0
  105. package/node_modules/@fps-games/editor-core/dist/selection-strategy.d.ts.map +1 -0
  106. package/node_modules/@fps-games/editor-core/dist/selection-strategy.js +52 -0
  107. package/node_modules/@fps-games/editor-core/dist/selection-strategy.js.map +1 -0
  108. package/node_modules/@fps-games/editor-core/dist/viewport-tools.d.ts +94 -0
  109. package/node_modules/@fps-games/editor-core/dist/viewport-tools.d.ts.map +1 -0
  110. package/node_modules/@fps-games/editor-core/dist/viewport-tools.js +58 -0
  111. package/node_modules/@fps-games/editor-core/dist/viewport-tools.js.map +1 -0
  112. package/node_modules/@fps-games/editor-core/package.json +2 -2
  113. package/node_modules/@fps-games/editor-forge-play/package.json +2 -2
  114. package/node_modules/@fps-games/editor-protocol/package.json +1 -1
  115. package/package.json +6 -6
@@ -1,6 +1,6 @@
1
- import { createEditorSession, createInspectorRegistry, DEFAULT_EDITOR_TRANSFORM_OPERATION_SETTINGS, createInspectorEditPayload, computeEditorTransformActionTargets, mergeInspectorSections, normalizeEditorTransformConstraint, serializedMultiObjectToInspectorObject, serializedObjectToInspectorObject, validateSceneGraphDelete, validateSceneGraphDrop, validateSceneGraphGroupSelection, validateSceneGraphMove, validateSceneGraphRename, } from '@fps-games/editor-core';
1
+ import { createEditorSession, createInspectorRegistry, DEFAULT_EDITOR_TRANSFORM_OPERATION_SETTINGS, DEFAULT_EDITOR_VIEWPORT_TOOL_STATE, createEmptyEditorViewportSpatialOverlayState, cloneEditorViewportToolState, cloneEditorViewportSpatialOverlayState, createInspectorEditPayload, computeEditorTransformActionTargets, mergeInspectorSections, normalizeEditorTransformConstraint, resolveEditorSelectionCommand, serializedMultiObjectToInspectorObject, serializedObjectToInspectorObject, validateSceneGraphDelete, validateSceneGraphDrop, validateSceneGraphGroupSelection, validateSceneGraphMove, validateSceneGraphRename, } from '@fps-games/editor-core';
2
2
  import { createLocalEditorBrowserUi, } from '@fps-games/editor-browser';
3
- import { createBabylonEditorProjection, createBabylonEditorWorld, createBabylonProjectionSelectionController, createBabylonSceneCameraPreviewController, createBabylonSceneViewCameraController, createBabylonSceneViewInputController, createBabylonTransformGizmoController, focusEditorViewportSelection, } from '@fps-games/editor-babylon';
3
+ import { createBabylonEditorProjection, createBabylonEditorWorld, createBabylonProjectionSelectionController, createBabylonSceneCameraPreviewController, createBabylonSceneViewCameraController, createBabylonSceneViewInputController, createBabylonSceneViewMeasurementController, createBabylonSceneViewSpatialOverlayController, createBabylonTransformGizmoController, focusEditorViewportSelection, } from '@fps-games/editor-babylon';
4
4
  export function createLocalEditorHarness(options) {
5
5
  const root = options.root ?? document.body;
6
6
  const state = {
@@ -13,18 +13,25 @@ export function createLocalEditorHarness(options) {
13
13
  babylon: null,
14
14
  engine: null,
15
15
  world: null,
16
+ grid: null,
17
+ gridVisible: true,
16
18
  projection: null,
17
19
  gizmo: null,
18
20
  sceneCameraPreview: null,
19
21
  sceneCameraPreviewEnabled: false,
20
22
  sceneViewInput: null,
21
23
  sceneViewCamera: null,
24
+ sceneViewMeasurement: null,
25
+ sceneViewSpatialOverlay: null,
22
26
  selectionController: null,
23
27
  boxSelection: null,
24
28
  transformTool: 'select',
25
29
  transformSpace: 'world',
26
30
  transformConstraint: 'axis',
27
31
  transformOperationSettings: cloneTransformOperationSettings(DEFAULT_EDITOR_TRANSFORM_OPERATION_SETTINGS),
32
+ viewportTools: cloneEditorViewportToolState(DEFAULT_EDITOR_VIEWPORT_TOOL_STATE),
33
+ viewportMeasurement: createEmptyViewportMeasurement(),
34
+ viewportSpatialOverlay: createEmptyEditorViewportSpatialOverlayState(),
28
35
  duplicateDrag: null,
29
36
  armedPlacement: null,
30
37
  resizeHandler: null,
@@ -69,6 +76,10 @@ export function createLocalEditorHarness(options) {
69
76
  if (selectItem(state, options, input))
70
77
  harness.render();
71
78
  },
79
+ onSelectionCommand: (command) => {
80
+ if (dispatchSelectionCommand(state, options, command))
81
+ harness.render();
82
+ },
72
83
  onSceneGraphRename: (intent) => {
73
84
  if (renameSceneGraphNode(state, options, intent))
74
85
  harness.render();
@@ -77,6 +88,12 @@ export function createLocalEditorHarness(options) {
77
88
  if (createSceneGraphGroup(state, options, intent))
78
89
  harness.render();
79
90
  },
91
+ onSceneGraphCreatePrimitive: options.documentAdapter.createSceneGraphCreatePrimitivePatch
92
+ ? (intent) => {
93
+ if (createSceneGraphPrimitive(state, options, intent))
94
+ harness.render();
95
+ }
96
+ : undefined,
80
97
  onSceneGraphDelete: (intent) => {
81
98
  if (deleteSceneGraphNodes(state, options, intent))
82
99
  harness.render();
@@ -174,14 +191,42 @@ export function createLocalEditorHarness(options) {
174
191
  if (executeTransformAction(state, options, action))
175
192
  harness.render();
176
193
  },
194
+ onViewportViewPresetChange: (preset) => {
195
+ if (setViewportViewPreset(state, preset))
196
+ harness.render();
197
+ },
198
+ onViewportProjectionModeChange: (mode) => {
199
+ if (setViewportProjectionMode(state, mode))
200
+ harness.render();
201
+ },
202
+ onViewportOverlaySettingsChange: (settings) => {
203
+ if (setViewportOverlaySettings(state, settings))
204
+ harness.render();
205
+ },
206
+ onViewportUtilityToolChange: (tool) => {
207
+ if (setViewportUtilityTool(state, tool))
208
+ harness.render();
209
+ },
210
+ onViewportMeasurementClear: () => {
211
+ if (clearViewportMeasurement(state))
212
+ harness.render();
213
+ },
177
214
  onSceneCameraPreviewToggle: (enabled) => {
178
215
  if (setSceneCameraPreviewEnabled(state, options, enabled))
179
216
  harness.render();
180
217
  },
218
+ onGridVisibleChange: (visible) => {
219
+ if (setGridVisible(state, visible))
220
+ harness.render();
221
+ },
181
222
  onFocusSelection: () => {
182
223
  if (focusSelectedProjection(state))
183
224
  harness.render();
184
225
  },
226
+ onCancelEditorIntent: () => {
227
+ if (cancelEditorIntent(state, options))
228
+ harness.render();
229
+ },
185
230
  onCancelActiveOperation: () => {
186
231
  cancelActiveOperation(state);
187
232
  harness.render();
@@ -191,6 +236,9 @@ export function createLocalEditorHarness(options) {
191
236
  harness = {
192
237
  render() {
193
238
  syncSceneCameraPreview(state, options);
239
+ syncViewportCameraState(state);
240
+ syncViewportMeasurementState(state);
241
+ syncViewportSpatialOverlay(state);
194
242
  ui.update(createUiState(state, options));
195
243
  },
196
244
  setTheme(theme) {
@@ -404,6 +452,224 @@ function normalizePositiveStep(value) {
404
452
  function normalizePlacementMode(mode) {
405
453
  return mode === 'ground' || mode === 'surface' ? mode : 'off';
406
454
  }
455
+ function normalizeViewportViewPreset(preset) {
456
+ return preset === 'top' || preset === 'front' || preset === 'right' ? preset : 'perspective';
457
+ }
458
+ function normalizeViewportProjectionMode(mode) {
459
+ return mode === 'orthographic' ? 'orthographic' : 'perspective';
460
+ }
461
+ function createEmptyViewportMeasurement() {
462
+ return {
463
+ active: false,
464
+ start: null,
465
+ end: null,
466
+ preview: null,
467
+ distance: null,
468
+ screenStart: null,
469
+ screenEnd: null,
470
+ screenPreview: null,
471
+ label: null,
472
+ };
473
+ }
474
+ function cloneViewportMeasurement(measurement) {
475
+ return {
476
+ active: measurement.active,
477
+ start: measurement.start ? { ...measurement.start } : null,
478
+ end: measurement.end ? { ...measurement.end } : null,
479
+ preview: measurement.preview ? { ...measurement.preview } : null,
480
+ distance: measurement.distance,
481
+ screenStart: measurement.screenStart ? { ...measurement.screenStart } : null,
482
+ screenEnd: measurement.screenEnd ? { ...measurement.screenEnd } : null,
483
+ screenPreview: measurement.screenPreview ? { ...measurement.screenPreview } : null,
484
+ label: measurement.label ? { ...measurement.label } : null,
485
+ };
486
+ }
487
+ function setViewportViewPreset(state, preset) {
488
+ if (state.sceneCameraPreviewEnabled) {
489
+ state.status = 'Viewport view unavailable during Scene Camera preview';
490
+ state.statusTone = 'warning';
491
+ state.statusToneStatus = state.status;
492
+ state.statusDetails = 'Disable Scene Camera preview before switching editor viewport views.';
493
+ return true;
494
+ }
495
+ const nextPreset = normalizeViewportViewPreset(preset);
496
+ const target = resolveViewportPresetTarget(state);
497
+ const radius = resolveViewportPresetRadius(state);
498
+ const changed = state.sceneViewCamera?.setViewPreset(nextPreset, { target, radius }) ?? false;
499
+ if (!changed)
500
+ return false;
501
+ const cameraState = state.sceneViewCamera?.getState();
502
+ state.viewportTools = {
503
+ ...state.viewportTools,
504
+ viewPreset: cameraState?.viewPreset ?? nextPreset,
505
+ projectionMode: cameraState?.projectionMode ?? (nextPreset === 'perspective' ? 'perspective' : 'orthographic'),
506
+ };
507
+ state.status = `Viewport view: ${state.viewportTools.viewPreset}`;
508
+ state.statusTone = 'default';
509
+ state.statusToneStatus = state.status;
510
+ state.statusDetails = '';
511
+ return true;
512
+ }
513
+ function setViewportProjectionMode(state, mode) {
514
+ if (state.sceneCameraPreviewEnabled) {
515
+ state.status = 'Viewport projection unavailable during Scene Camera preview';
516
+ state.statusTone = 'warning';
517
+ state.statusToneStatus = state.status;
518
+ state.statusDetails = 'Disable Scene Camera preview before switching editor viewport projection.';
519
+ return true;
520
+ }
521
+ const nextMode = normalizeViewportProjectionMode(mode);
522
+ const changed = state.sceneViewCamera?.setProjectionMode(nextMode) ?? false;
523
+ if (!changed)
524
+ return false;
525
+ const cameraState = state.sceneViewCamera?.getState();
526
+ state.viewportTools = {
527
+ ...state.viewportTools,
528
+ projectionMode: cameraState?.projectionMode ?? nextMode,
529
+ };
530
+ state.status = `Viewport projection: ${state.viewportTools.projectionMode}`;
531
+ state.statusTone = 'default';
532
+ state.statusToneStatus = state.status;
533
+ state.statusDetails = '';
534
+ return true;
535
+ }
536
+ function setViewportOverlaySettings(state, settings) {
537
+ state.viewportTools = {
538
+ ...state.viewportTools,
539
+ overlay: {
540
+ ...state.viewportTools.overlay,
541
+ bounds: settings.bounds ?? state.viewportTools.overlay.bounds,
542
+ dimensions: settings.dimensions ?? state.viewportTools.overlay.dimensions,
543
+ edgeLengths: settings.edgeLengths ?? state.viewportTools.overlay.edgeLengths,
544
+ anchor: settings.anchor ?? state.viewportTools.overlay.anchor,
545
+ },
546
+ };
547
+ state.status = 'Viewport overlay settings updated';
548
+ state.statusTone = 'default';
549
+ state.statusToneStatus = state.status;
550
+ state.statusDetails = '';
551
+ return true;
552
+ }
553
+ function setViewportUtilityTool(state, tool) {
554
+ const nextTool = tool === 'measure-distance' ? 'measure-distance' : 'none';
555
+ state.viewportTools = {
556
+ ...state.viewportTools,
557
+ activeUtilityTool: nextTool,
558
+ };
559
+ if (nextTool === 'measure-distance') {
560
+ state.viewportMeasurement = {
561
+ ...cloneViewportMeasurement(state.viewportMeasurement),
562
+ active: !!state.viewportMeasurement.start && !state.viewportMeasurement.end,
563
+ };
564
+ state.status = 'Measure distance: pick first XZ ground point';
565
+ }
566
+ else {
567
+ const unfinishedMeasurement = state.viewportMeasurement.active
568
+ || (!!state.viewportMeasurement.start && !state.viewportMeasurement.end);
569
+ state.viewportMeasurement = unfinishedMeasurement
570
+ ? state.sceneViewMeasurement?.clear() ?? createEmptyViewportMeasurement()
571
+ : {
572
+ ...cloneViewportMeasurement(state.viewportMeasurement),
573
+ active: false,
574
+ preview: null,
575
+ screenPreview: null,
576
+ };
577
+ state.status = 'Viewport utility: none';
578
+ }
579
+ state.statusTone = 'default';
580
+ state.statusToneStatus = state.status;
581
+ state.statusDetails = '';
582
+ return true;
583
+ }
584
+ function clearViewportMeasurement(state) {
585
+ state.viewportMeasurement = state.sceneViewMeasurement?.clear() ?? createEmptyViewportMeasurement();
586
+ state.viewportTools = {
587
+ ...state.viewportTools,
588
+ activeUtilityTool: 'none',
589
+ };
590
+ state.status = 'Measurement cleared';
591
+ state.statusTone = 'default';
592
+ state.statusToneStatus = state.status;
593
+ state.statusDetails = '';
594
+ return true;
595
+ }
596
+ export function applyLocalEditorHarnessViewportMeasurementPointerStart(state, event) {
597
+ if (state.viewportTools.activeUtilityTool !== 'measure-distance')
598
+ return false;
599
+ if (!state.viewportMeasurement.start || state.viewportMeasurement.end) {
600
+ const next = state.sceneViewMeasurement?.beginAt(event.clientX, event.clientY) ?? null;
601
+ if (!next)
602
+ return false;
603
+ state.viewportMeasurement = next;
604
+ state.status = 'Measure distance: pick second XZ ground point';
605
+ state.statusTone = 'default';
606
+ state.statusToneStatus = state.status;
607
+ state.statusDetails = '';
608
+ return true;
609
+ }
610
+ const next = state.sceneViewMeasurement?.completeAt(event.clientX, event.clientY) ?? null;
611
+ if (!next)
612
+ return false;
613
+ state.viewportMeasurement = next;
614
+ state.viewportTools = {
615
+ ...state.viewportTools,
616
+ activeUtilityTool: 'none',
617
+ };
618
+ state.status = `Measured distance: ${formatViewportMeasurementDistance(next.distance)} units`;
619
+ state.statusTone = 'default';
620
+ state.statusToneStatus = state.status;
621
+ state.statusDetails = next.start && next.end
622
+ ? `A (${formatViewportMeasurementDistance(next.start.x)}, ${formatViewportMeasurementDistance(next.start.z)}) · B (${formatViewportMeasurementDistance(next.end.x)}, ${formatViewportMeasurementDistance(next.end.z)})`
623
+ : '';
624
+ return true;
625
+ }
626
+ export function applyLocalEditorHarnessViewportMeasurementPointerMove(state, event) {
627
+ if (state.viewportTools.activeUtilityTool !== 'measure-distance')
628
+ return false;
629
+ const next = state.sceneViewMeasurement?.previewAt(event.clientX, event.clientY) ?? null;
630
+ if (!next)
631
+ return false;
632
+ state.viewportMeasurement = next;
633
+ return true;
634
+ }
635
+ export function applyLocalEditorHarnessViewportMeasurementPointerEnd(_state) {
636
+ return false;
637
+ }
638
+ function handleViewportMeasurementStart(state, event) {
639
+ return applyLocalEditorHarnessViewportMeasurementPointerStart(state, event);
640
+ }
641
+ function handleViewportMeasurementMove(state, event) {
642
+ return applyLocalEditorHarnessViewportMeasurementPointerMove(state, event);
643
+ }
644
+ function handleViewportMeasurementEnd(state) {
645
+ return applyLocalEditorHarnessViewportMeasurementPointerEnd(state);
646
+ }
647
+ function formatViewportMeasurementDistance(value) {
648
+ if (!Number.isFinite(value))
649
+ return '0';
650
+ const rounded = Math.abs(value) < 0.005 ? 0 : value;
651
+ return rounded.toFixed(2).replace(/\.?0+$/, '');
652
+ }
653
+ function resolveViewportPresetTarget(state) {
654
+ const selection = state.session?.getState().selection;
655
+ const activeId = selection?.activeId ?? null;
656
+ if (!activeId || selection?.selectedIds.length !== 1)
657
+ return null;
658
+ const bounds = state.projection?.getSelectionBounds([activeId]) ?? null;
659
+ return bounds?.center ?? null;
660
+ }
661
+ function resolveViewportPresetRadius(state) {
662
+ const selection = state.session?.getState().selection;
663
+ const activeId = selection?.activeId ?? null;
664
+ if (!activeId || selection?.selectedIds.length !== 1)
665
+ return undefined;
666
+ const bounds = state.projection?.getSelectionBounds([activeId]) ?? null;
667
+ if (!bounds)
668
+ return undefined;
669
+ const size = bounds.size;
670
+ const radius = Math.hypot(size.x, size.y, size.z);
671
+ return Number.isFinite(radius) && radius > 0 ? Math.max(radius * 1.5, 2) : undefined;
672
+ }
407
673
  function validateTransformActionSelection(state, action) {
408
674
  const selection = state.session?.getState().selection;
409
675
  const selectedCount = selection?.selectedIds.length ?? 0;
@@ -430,10 +696,12 @@ async function createEditorWorld(state, options, render) {
430
696
  cameraTarget: options.world?.cameraTarget,
431
697
  cameraRadius: options.world?.cameraRadius,
432
698
  clearColor: options.world?.clearColor,
699
+ sky: options.world?.sky,
433
700
  useRightHandedSystem: options.world?.useRightHandedSystem,
434
701
  enableDefaultCameraControls: false,
435
702
  });
436
- options.createGrid?.(babylon, world.scene);
703
+ const grid = options.createGrid?.(babylon, world.scene) ?? null;
704
+ grid?.setVisible(state.gridVisible);
437
705
  const projection = createBabylonEditorProjection({
438
706
  babylon,
439
707
  scene: world.scene,
@@ -498,6 +766,10 @@ async function createEditorWorld(state, options, render) {
498
766
  render();
499
767
  },
500
768
  });
769
+ const sceneViewMeasurement = createBabylonSceneViewMeasurementController({
770
+ babylon,
771
+ scene: world.scene,
772
+ });
501
773
  const sceneViewInput = createBabylonSceneViewInputController({
502
774
  canvas,
503
775
  isEnabled: () => state.mode === 'editor',
@@ -505,7 +777,13 @@ async function createEditorWorld(state, options, render) {
505
777
  isBoxSelectCandidate: (event) => selectionController.isBoxSelectionCandidate(event),
506
778
  isViewPlaneMoveCandidate: (event) => gizmo.isViewPlaneMoveCandidate(event),
507
779
  isPlacementCandidate: () => isPlacementArmed(state),
780
+ isMeasurementCandidate: () => state.viewportTools.activeUtilityTool === 'measure-distance',
508
781
  onPointerIntentStart(event) {
782
+ if (event.state.intent === 'measurement') {
783
+ if (handleViewportMeasurementStart(state, event.originalEvent))
784
+ render();
785
+ return;
786
+ }
509
787
  if (event.state.intent === 'gizmo-drag') {
510
788
  gizmo.preparePointerDrag(event.originalEvent);
511
789
  return;
@@ -525,6 +803,11 @@ async function createEditorWorld(state, options, render) {
525
803
  }
526
804
  },
527
805
  onPointerIntentMove(event) {
806
+ if (event.state.intent === 'measurement') {
807
+ if (handleViewportMeasurementMove(state, event.originalEvent))
808
+ render();
809
+ return;
810
+ }
528
811
  if (event.state.intent === 'placement') {
529
812
  if (previewArmedPlacement(state, event.originalEvent))
530
813
  render();
@@ -544,6 +827,11 @@ async function createEditorWorld(state, options, render) {
544
827
  }
545
828
  },
546
829
  onPointerIntentEnd(event) {
830
+ if (event.state.intent === 'measurement') {
831
+ if (handleViewportMeasurementEnd(state))
832
+ render();
833
+ return;
834
+ }
547
835
  if (event.state.intent === 'placement') {
548
836
  if (commitArmedPlacement(state, options, event.originalEvent))
549
837
  render();
@@ -559,6 +847,10 @@ async function createEditorWorld(state, options, render) {
559
847
  }
560
848
  },
561
849
  onPointerIntentCancel(event) {
850
+ if (event.state.intent === 'measurement') {
851
+ render();
852
+ return;
853
+ }
562
854
  if (event.state.intent === 'placement') {
563
855
  state.gizmo?.setPlacementMarker(null);
564
856
  render();
@@ -593,6 +885,10 @@ async function createEditorWorld(state, options, render) {
593
885
  scene: world.scene,
594
886
  editorCamera: world.camera,
595
887
  });
888
+ const sceneViewSpatialOverlay = createBabylonSceneViewSpatialOverlayController({
889
+ babylon,
890
+ scene: world.scene,
891
+ });
596
892
  const document = state.session?.getState().workingDocument;
597
893
  if (document) {
598
894
  projection.projectNodes(options.documentAdapter.getProjectionNodes(document));
@@ -606,11 +902,14 @@ async function createEditorWorld(state, options, render) {
606
902
  state.babylon = babylon;
607
903
  state.engine = engine;
608
904
  state.world = world;
905
+ state.grid = grid;
609
906
  state.projection = projection;
610
907
  state.gizmo = gizmo;
611
908
  state.sceneCameraPreview = sceneCameraPreview;
612
909
  state.sceneViewInput = sceneViewInput;
613
910
  state.sceneViewCamera = sceneViewCamera;
911
+ state.sceneViewMeasurement = sceneViewMeasurement;
912
+ state.sceneViewSpatialOverlay = sceneViewSpatialOverlay;
614
913
  state.selectionController = selectionController;
615
914
  state.resizeHandler = resize;
616
915
  }
@@ -624,15 +923,27 @@ function disposeEditorWorld(state) {
624
923
  state.sceneCameraPreviewEnabled = false;
625
924
  state.sceneViewCamera?.dispose();
626
925
  state.sceneViewCamera = null;
926
+ state.sceneViewMeasurement?.dispose();
927
+ state.sceneViewMeasurement = null;
928
+ state.sceneViewSpatialOverlay?.dispose();
929
+ state.sceneViewSpatialOverlay = null;
627
930
  state.sceneViewInput?.dispose();
628
931
  state.sceneViewInput = null;
629
932
  state.selectionController?.dispose();
630
933
  state.selectionController = null;
631
934
  state.boxSelection = null;
935
+ state.viewportSpatialOverlay = createEmptyEditorViewportSpatialOverlayState();
936
+ state.viewportMeasurement = createEmptyViewportMeasurement();
937
+ state.viewportTools = {
938
+ ...state.viewportTools,
939
+ activeUtilityTool: 'none',
940
+ };
632
941
  state.gizmo?.dispose();
633
942
  state.gizmo = null;
634
943
  state.projection?.dispose();
635
944
  state.projection = null;
945
+ state.grid?.dispose();
946
+ state.grid = null;
636
947
  state.engine?.stopRenderLoop?.();
637
948
  state.world?.dispose();
638
949
  state.engine?.dispose?.();
@@ -665,26 +976,16 @@ function selectItem(state, options, input) {
665
976
  return false;
666
977
  if (!isDocumentNodeSelectable(state, options, input.id))
667
978
  return false;
668
- const command = input.toggle
669
- ? {
670
- type: 'selection.toggle',
671
- selectedIds: [input.id],
672
- activeId: input.id,
673
- label: 'Toggle Selection',
674
- }
675
- : input.additive
676
- ? {
677
- type: 'selection.add',
678
- selectedIds: [input.id],
679
- activeId: input.id,
680
- label: 'Add Selection',
681
- }
682
- : {
683
- type: 'selection.replace',
684
- selectedIds: [input.id],
685
- activeId: input.id,
686
- label: 'Select Item',
687
- };
979
+ const selection = state.session?.getState().selection ?? { selectedIds: [], activeId: null };
980
+ const command = resolveEditorSelectionCommand({
981
+ selection,
982
+ targetIds: [input.id],
983
+ activeId: input.id,
984
+ gesture: 'click',
985
+ modifier: input.toggle ? 'toggle' : input.additive ? 'additive' : 'replace',
986
+ });
987
+ if (!command)
988
+ return false;
688
989
  return dispatchSelectionCommand(state, options, command);
689
990
  }
690
991
  function handleContextAction(state, options, action) {
@@ -708,7 +1009,15 @@ function handleContextAction(state, options, action) {
708
1009
  return createSceneGraphGroup(state, options, {
709
1010
  parentId: action.parentId ?? null,
710
1011
  activeId: action.activeId ?? null,
711
- name: 'Group',
1012
+ name: 'Empty',
1013
+ });
1014
+ }
1015
+ if (action.action === 'create-primitive') {
1016
+ return createSceneGraphPrimitive(state, options, {
1017
+ parentId: action.parentId ?? null,
1018
+ activeId: action.activeId ?? null,
1019
+ shape: action.shape,
1020
+ name: action.name,
712
1021
  });
713
1022
  }
714
1023
  if (action.action === 'delete') {
@@ -781,17 +1090,17 @@ function createSceneGraphGroup(state, options, intent) {
781
1090
  cancelActiveOperation(state);
782
1091
  const patch = options.documentAdapter.createSceneGraphCreateGroupPatch?.(document, intent);
783
1092
  if (!patch) {
784
- state.status = 'Create group rejected';
1093
+ state.status = 'Create empty rejected';
785
1094
  return true;
786
1095
  }
787
1096
  const result = state.session.dispatch({
788
1097
  type: 'document.patch',
789
- label: patch.label ?? 'Create Empty Group',
1098
+ label: patch.label ?? 'Create Empty',
790
1099
  patch: patch.patch,
791
1100
  targetId: patch.createdId ?? undefined,
792
1101
  });
793
1102
  if (!result.documentChanged) {
794
- state.status = 'Create group unchanged';
1103
+ state.status = 'Create empty unchanged';
795
1104
  return true;
796
1105
  }
797
1106
  const createdId = patch.createdId ?? null;
@@ -801,12 +1110,47 @@ function createSceneGraphGroup(state, options, intent) {
801
1110
  type: 'selection.replace',
802
1111
  selectedIds: [createdId],
803
1112
  activeId: createdId,
804
- label: 'Select Created Group',
1113
+ label: 'Select Created Empty',
805
1114
  }).selection;
806
1115
  }
807
1116
  rebuildProjectionFromDocument(state, options, result.workingDocument, selection);
808
1117
  state.summary = summarizeDocument(options, result.workingDocument, state.session.getSource());
809
- state.status = patch.label ?? (createdId ? `Created group ${createdId}` : 'Created group');
1118
+ state.status = patch.label ?? (createdId ? `Created empty ${createdId}` : 'Created empty');
1119
+ return true;
1120
+ }
1121
+ function createSceneGraphPrimitive(state, options, intent) {
1122
+ const document = state.session?.getState().workingDocument;
1123
+ if (state.mode !== 'editor' || !state.session || !document)
1124
+ return false;
1125
+ cancelActiveOperation(state);
1126
+ const patch = options.documentAdapter.createSceneGraphCreatePrimitivePatch?.(document, intent);
1127
+ if (!patch) {
1128
+ state.status = `Create ${intent.shape} rejected`;
1129
+ return true;
1130
+ }
1131
+ const result = state.session.dispatch({
1132
+ type: 'document.patch',
1133
+ label: patch.label ?? `Create ${intent.shape}`,
1134
+ patch: patch.patch,
1135
+ targetId: patch.createdId ?? undefined,
1136
+ });
1137
+ if (!result.documentChanged) {
1138
+ state.status = `Create ${intent.shape} unchanged`;
1139
+ return true;
1140
+ }
1141
+ const createdId = patch.createdId ?? null;
1142
+ let selection = result.selection;
1143
+ if (createdId && isNodeSelectableInDocument(options, result.workingDocument, createdId)) {
1144
+ selection = state.session.dispatch({
1145
+ type: 'selection.replace',
1146
+ selectedIds: [createdId],
1147
+ activeId: createdId,
1148
+ label: 'Select Created Primitive',
1149
+ }).selection;
1150
+ }
1151
+ rebuildProjectionFromDocument(state, options, result.workingDocument, selection);
1152
+ state.summary = summarizeDocument(options, result.workingDocument, state.session.getSource());
1153
+ state.status = patch.label ?? (createdId ? `Created ${intent.shape} ${createdId}` : `Created ${intent.shape}`);
810
1154
  return true;
811
1155
  }
812
1156
  function deleteSceneGraphNodes(state, options, intent) {
@@ -996,27 +1340,27 @@ function groupSceneGraphSelection(state, options, intent) {
996
1340
  const hierarchy = options.documentAdapter.getHierarchyItems(document);
997
1341
  const coreValidation = validateSceneGraphGroupSelection(hierarchy, intent);
998
1342
  if (!coreValidation.ok) {
999
- state.status = `Group selection rejected: ${coreValidation.reason ?? 'invalid scene graph group selection'}`;
1343
+ state.status = `Parent selection rejected: ${coreValidation.reason ?? 'invalid scene graph parent selection'}`;
1000
1344
  return true;
1001
1345
  }
1002
1346
  const projectValidation = options.documentAdapter.validateSceneGraphGroupSelection?.(document, intent);
1003
1347
  if (projectValidation && !projectValidation.ok) {
1004
- state.status = `Group selection rejected: ${projectValidation.reason ?? 'project validation failed'}`;
1348
+ state.status = `Parent selection rejected: ${projectValidation.reason ?? 'project validation failed'}`;
1005
1349
  return true;
1006
1350
  }
1007
1351
  const patch = options.documentAdapter.createSceneGraphGroupSelectionPatch?.(document, intent);
1008
1352
  if (!patch) {
1009
- state.status = 'Group selection rejected';
1353
+ state.status = 'Parent selection rejected';
1010
1354
  return true;
1011
1355
  }
1012
1356
  const result = state.session.dispatch({
1013
1357
  type: 'document.patch',
1014
- label: patch.label ?? 'Group Selection',
1358
+ label: patch.label ?? 'Parent Selection',
1015
1359
  patch: patch.patch,
1016
1360
  targetId: patch.createdId,
1017
1361
  });
1018
1362
  if (!result.documentChanged) {
1019
- state.status = 'Group selection unchanged';
1363
+ state.status = 'Parent selection unchanged';
1020
1364
  return true;
1021
1365
  }
1022
1366
  let selection = result.selection;
@@ -1025,7 +1369,7 @@ function groupSceneGraphSelection(state, options, intent) {
1025
1369
  type: 'selection.replace',
1026
1370
  selectedIds: [patch.createdId],
1027
1371
  activeId: patch.createdId,
1028
- label: 'Select Created Group',
1372
+ label: 'Select Created Parent',
1029
1373
  }).selection;
1030
1374
  }
1031
1375
  else {
@@ -1033,7 +1377,7 @@ function groupSceneGraphSelection(state, options, intent) {
1033
1377
  }
1034
1378
  rebuildProjectionFromDocument(state, options, result.workingDocument, selection);
1035
1379
  state.summary = summarizeDocument(options, result.workingDocument, state.session.getSource());
1036
- state.status = patch.label ?? `Grouped ${patch.changedIds?.length ?? intent.ids.length} node(s)`;
1380
+ state.status = patch.label ?? `Parented ${patch.changedIds?.length ?? intent.ids.length} node(s)`;
1037
1381
  return true;
1038
1382
  }
1039
1383
  function sanitizeSelection(state, options, document, selection) {
@@ -1593,6 +1937,21 @@ function cancelActiveOperation(state) {
1593
1937
  cancelActiveGizmoDrag(state);
1594
1938
  clearArmedPlacement(state);
1595
1939
  }
1940
+ function cancelEditorIntent(state, options) {
1941
+ if (state.mode !== 'editor')
1942
+ return false;
1943
+ if (state.viewportTools.activeUtilityTool === 'measure-distance'
1944
+ || state.viewportMeasurement.start
1945
+ || state.viewportMeasurement.end) {
1946
+ return clearViewportMeasurement(state);
1947
+ }
1948
+ const selection = state.session?.getSelection();
1949
+ if (selection && selection.selectedIds.length > 0) {
1950
+ return dispatchSelectionCommand(state, options, { type: 'selection.clear', label: 'Clear Selection' });
1951
+ }
1952
+ cancelActiveOperation(state);
1953
+ return true;
1954
+ }
1596
1955
  function focusSelectedProjection(state) {
1597
1956
  if (state.mode !== 'editor')
1598
1957
  return false;
@@ -1716,6 +2075,18 @@ function setSceneCameraPreviewEnabled(state, options, enabled) {
1716
2075
  state.status = 'Scene Camera preview enabled';
1717
2076
  return true;
1718
2077
  }
2078
+ function setGridVisible(state, visible) {
2079
+ const nextVisible = visible === true;
2080
+ if (state.gridVisible === nextVisible)
2081
+ return false;
2082
+ state.gridVisible = nextVisible;
2083
+ state.grid?.setVisible(nextVisible);
2084
+ state.status = nextVisible ? 'Grid visible' : 'Grid hidden';
2085
+ state.statusTone = 'default';
2086
+ state.statusToneStatus = state.status;
2087
+ state.statusDetails = '';
2088
+ return true;
2089
+ }
1719
2090
  function syncSceneCameraPreview(state, options) {
1720
2091
  const controller = state.sceneCameraPreview;
1721
2092
  if (!controller)
@@ -1733,6 +2104,40 @@ function syncSceneCameraPreview(state, options) {
1733
2104
  controller.setActive(true, rig);
1734
2105
  return controller.isActive();
1735
2106
  }
2107
+ function syncViewportCameraState(state) {
2108
+ const cameraState = state.sceneViewCamera?.getState();
2109
+ if (!cameraState)
2110
+ return;
2111
+ state.viewportTools = {
2112
+ ...state.viewportTools,
2113
+ viewPreset: cameraState.viewPreset,
2114
+ projectionMode: cameraState.projectionMode,
2115
+ };
2116
+ }
2117
+ function syncViewportSpatialOverlay(state) {
2118
+ const overlay = state.sceneViewSpatialOverlay;
2119
+ const selection = state.session?.getState().selection;
2120
+ const activeId = selection?.activeId ?? null;
2121
+ if (state.mode !== 'editor' || !overlay || !activeId || selection?.selectedIds.length !== 1) {
2122
+ state.viewportSpatialOverlay = createEmptyEditorViewportSpatialOverlayState();
2123
+ return;
2124
+ }
2125
+ const bounds = state.projection?.getSelectionBounds([activeId]) ?? null;
2126
+ const anchor = state.projection?.readNodeTransform(activeId)?.position ?? bounds?.center ?? null;
2127
+ state.viewportSpatialOverlay = overlay.compute({
2128
+ nodeId: activeId,
2129
+ bounds,
2130
+ anchor,
2131
+ settings: state.viewportTools.overlay,
2132
+ });
2133
+ }
2134
+ function syncViewportMeasurementState(state) {
2135
+ if (state.mode !== 'editor')
2136
+ return;
2137
+ const measurement = state.sceneViewMeasurement?.getState();
2138
+ if (measurement)
2139
+ state.viewportMeasurement = measurement;
2140
+ }
1736
2141
  function hasSceneCameraPreviewRig(state, options) {
1737
2142
  const document = state.session?.getState().workingDocument ?? null;
1738
2143
  return !!document && !!options.documentAdapter.getSceneCameraPreviewRig?.(document);
@@ -1784,6 +2189,9 @@ function createUiState(state, options) {
1784
2189
  inspectorObject,
1785
2190
  inspectorMultiObject,
1786
2191
  boxSelection: state.boxSelection,
2192
+ coordinateAxes: options.world?.coordinateAxes === false
2193
+ ? null
2194
+ : createSceneViewCoordinateAxesState(state),
1787
2195
  transformTool: {
1788
2196
  activeTool: state.gizmo?.getState().tool ?? state.transformTool,
1789
2197
  activeSpace: state.gizmo?.getState().space ?? state.transformSpace,
@@ -1798,10 +2206,17 @@ function createUiState(state, options) {
1798
2206
  canAlign: selectedIds.length >= 2 && activeId != null,
1799
2207
  canDistribute: selectedIds.length >= 3,
1800
2208
  },
2209
+ viewportTools: cloneEditorViewportToolState(state.viewportTools),
2210
+ viewportMeasurement: cloneViewportMeasurement(state.viewportMeasurement),
2211
+ viewportSpatialOverlay: cloneEditorViewportSpatialOverlayState(state.viewportSpatialOverlay),
1801
2212
  sceneCameraPreview: {
1802
2213
  enabled: state.sceneCameraPreviewEnabled,
1803
2214
  available: hasSceneCameraPreviewRig(state, options),
1804
2215
  },
2216
+ grid: {
2217
+ visible: state.gridVisible,
2218
+ available: !!state.grid,
2219
+ },
1805
2220
  session: sessionState
1806
2221
  ? {
1807
2222
  source: sessionState.source,
@@ -1813,6 +2228,89 @@ function createUiState(state, options) {
1813
2228
  : null,
1814
2229
  };
1815
2230
  }
2231
+ function createSceneViewCoordinateAxesState(state) {
2232
+ if (state.mode !== 'editor')
2233
+ return null;
2234
+ const scene = state.world?.scene ?? null;
2235
+ const camera = scene?.activeCamera ?? state.world?.camera ?? null;
2236
+ const Vector3 = state.babylon?.Vector3;
2237
+ if (!scene || !camera || !Vector3)
2238
+ return null;
2239
+ const right = readSceneViewCameraDirection(camera, Vector3, { x: 1, y: 0, z: 0 });
2240
+ const up = readSceneViewCameraDirection(camera, Vector3, { x: 0, y: 1, z: 0 });
2241
+ const forward = readSceneViewCameraForward(scene, camera, Vector3);
2242
+ if (!right || !up || !forward)
2243
+ return null;
2244
+ return {
2245
+ axes: [
2246
+ createSceneViewCoordinateAxis('x', 'X', '#ff4b70', { x: 1, y: 0, z: 0 }, right, up, forward),
2247
+ createSceneViewCoordinateAxis('y', 'Y', '#75ff42', { x: 0, y: 1, z: 0 }, right, up, forward),
2248
+ createSceneViewCoordinateAxis('z', 'Z', '#4f86ff', { x: 0, y: 0, z: 1 }, right, up, forward),
2249
+ ],
2250
+ };
2251
+ }
2252
+ function createSceneViewCoordinateAxis(id, label, color, worldAxis, cameraRight, cameraUp, cameraForward) {
2253
+ const screenX = dotSceneViewAxisVec3(worldAxis, cameraRight);
2254
+ const screenY = -dotSceneViewAxisVec3(worldAxis, cameraUp);
2255
+ const projectedLength = Math.hypot(screenX, screenY);
2256
+ const fallback = getSceneViewAxisFallbackDirection(id);
2257
+ const direction = projectedLength > 0.0001
2258
+ ? { x: screenX / projectedLength, y: screenY / projectedLength }
2259
+ : fallback;
2260
+ return {
2261
+ id,
2262
+ label,
2263
+ color,
2264
+ x: direction.x,
2265
+ y: direction.y,
2266
+ depth: dotSceneViewAxisVec3(worldAxis, cameraForward),
2267
+ scale: clampNumber(projectedLength, 0.36, 1),
2268
+ };
2269
+ }
2270
+ function readSceneViewCameraDirection(camera, Vector3, local) {
2271
+ if (!camera?.getDirection)
2272
+ return null;
2273
+ try {
2274
+ return normalizeSceneViewAxisVec3(camera.getDirection(new Vector3(local.x, local.y, local.z)));
2275
+ }
2276
+ catch {
2277
+ return null;
2278
+ }
2279
+ }
2280
+ function readSceneViewCameraForward(scene, camera, Vector3) {
2281
+ const rayDirection = normalizeSceneViewAxisVec3(camera?.getForwardRay?.()?.direction);
2282
+ if (rayDirection)
2283
+ return rayDirection;
2284
+ return readSceneViewCameraDirection(camera, Vector3, {
2285
+ x: 0,
2286
+ y: 0,
2287
+ z: scene?.useRightHandedSystem ? -1 : 1,
2288
+ });
2289
+ }
2290
+ function normalizeSceneViewAxisVec3(value) {
2291
+ const x = Number(value?.x) || 0;
2292
+ const y = Number(value?.y) || 0;
2293
+ const z = Number(value?.z) || 0;
2294
+ const length = Math.hypot(x, y, z);
2295
+ if (!Number.isFinite(length) || length <= 0.000001)
2296
+ return null;
2297
+ return { x: x / length, y: y / length, z: z / length };
2298
+ }
2299
+ function dotSceneViewAxisVec3(left, right) {
2300
+ return left.x * right.x + left.y * right.y + left.z * right.z;
2301
+ }
2302
+ function getSceneViewAxisFallbackDirection(id) {
2303
+ if (id === 'x')
2304
+ return { x: 1, y: 0 };
2305
+ if (id === 'y')
2306
+ return { x: 0, y: -1 };
2307
+ return { x: -1, y: 0 };
2308
+ }
2309
+ function clampNumber(value, min, max) {
2310
+ if (!Number.isFinite(value))
2311
+ return min;
2312
+ return Math.min(max, Math.max(min, value));
2313
+ }
1816
2314
  function withRuntimeInspectorSections(state, options, document, inspectorObject) {
1817
2315
  const baseInspectorObject = withInspectorComponentSections(state, options, document, inspectorObject);
1818
2316
  const activeId = inspectorObject.activeId;