@fps-games/editor 0.1.1-beta.1 → 0.1.1-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/local-editor-harness.d.ts +113 -6
  5. package/dist/local-editor-harness.d.ts.map +1 -1
  6. package/dist/local-editor-harness.js +1253 -55
  7. package/dist/local-editor-harness.js.map +1 -1
  8. package/node_modules/@fps-games/editor-babylon/dist/editor-sky.d.ts +33 -0
  9. package/node_modules/@fps-games/editor-babylon/dist/editor-sky.d.ts.map +1 -0
  10. package/node_modules/@fps-games/editor-babylon/dist/editor-sky.js +231 -0
  11. package/node_modules/@fps-games/editor-babylon/dist/editor-sky.js.map +1 -0
  12. package/node_modules/@fps-games/editor-babylon/dist/editor-world.d.ts +3 -0
  13. package/node_modules/@fps-games/editor-babylon/dist/editor-world.d.ts.map +1 -1
  14. package/node_modules/@fps-games/editor-babylon/dist/editor-world.js +8 -0
  15. package/node_modules/@fps-games/editor-babylon/dist/editor-world.js.map +1 -1
  16. package/node_modules/@fps-games/editor-babylon/dist/index.d.ts +5 -0
  17. package/node_modules/@fps-games/editor-babylon/dist/index.d.ts.map +1 -1
  18. package/node_modules/@fps-games/editor-babylon/dist/index.js +5 -0
  19. package/node_modules/@fps-games/editor-babylon/dist/index.js.map +1 -1
  20. package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.d.ts +40 -0
  21. package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.d.ts.map +1 -0
  22. package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.js +237 -0
  23. package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.js.map +1 -0
  24. package/node_modules/@fps-games/editor-babylon/dist/projection-selection-controller.d.ts +1 -1
  25. package/node_modules/@fps-games/editor-babylon/dist/projection-selection-controller.d.ts.map +1 -1
  26. package/node_modules/@fps-games/editor-babylon/dist/projection-selection-controller.js +15 -50
  27. package/node_modules/@fps-games/editor-babylon/dist/projection-selection-controller.js.map +1 -1
  28. package/node_modules/@fps-games/editor-babylon/dist/projection.d.ts +27 -0
  29. package/node_modules/@fps-games/editor-babylon/dist/projection.d.ts.map +1 -1
  30. package/node_modules/@fps-games/editor-babylon/dist/projection.js +368 -14
  31. package/node_modules/@fps-games/editor-babylon/dist/projection.js.map +1 -1
  32. package/node_modules/@fps-games/editor-babylon/dist/scene-camera-preview.d.ts +25 -0
  33. package/node_modules/@fps-games/editor-babylon/dist/scene-camera-preview.d.ts.map +1 -0
  34. package/node_modules/@fps-games/editor-babylon/dist/scene-camera-preview.js +176 -0
  35. package/node_modules/@fps-games/editor-babylon/dist/scene-camera-preview.js.map +1 -0
  36. package/node_modules/@fps-games/editor-babylon/dist/scene-view-camera-controller.d.ts +18 -0
  37. package/node_modules/@fps-games/editor-babylon/dist/scene-view-camera-controller.d.ts.map +1 -1
  38. package/node_modules/@fps-games/editor-babylon/dist/scene-view-camera-controller.js +142 -0
  39. package/node_modules/@fps-games/editor-babylon/dist/scene-view-camera-controller.js.map +1 -1
  40. package/node_modules/@fps-games/editor-babylon/dist/scene-view-input-controller.d.ts +2 -0
  41. package/node_modules/@fps-games/editor-babylon/dist/scene-view-input-controller.d.ts.map +1 -1
  42. package/node_modules/@fps-games/editor-babylon/dist/scene-view-input-controller.js +6 -0
  43. package/node_modules/@fps-games/editor-babylon/dist/scene-view-input-controller.js.map +1 -1
  44. package/node_modules/@fps-games/editor-babylon/dist/scene-view-measurement-controller.d.ts +20 -0
  45. package/node_modules/@fps-games/editor-babylon/dist/scene-view-measurement-controller.d.ts.map +1 -0
  46. package/node_modules/@fps-games/editor-babylon/dist/scene-view-measurement-controller.js +187 -0
  47. package/node_modules/@fps-games/editor-babylon/dist/scene-view-measurement-controller.js.map +1 -0
  48. package/node_modules/@fps-games/editor-babylon/dist/scene-view-spatial-overlay-controller.d.ts +20 -0
  49. package/node_modules/@fps-games/editor-babylon/dist/scene-view-spatial-overlay-controller.d.ts.map +1 -0
  50. package/node_modules/@fps-games/editor-babylon/dist/scene-view-spatial-overlay-controller.js +188 -0
  51. package/node_modules/@fps-games/editor-babylon/dist/scene-view-spatial-overlay-controller.js.map +1 -0
  52. package/node_modules/@fps-games/editor-babylon/dist/transform-gizmo-controller.d.ts +22 -2
  53. package/node_modules/@fps-games/editor-babylon/dist/transform-gizmo-controller.d.ts.map +1 -1
  54. package/node_modules/@fps-games/editor-babylon/dist/transform-gizmo-controller.js +469 -90
  55. package/node_modules/@fps-games/editor-babylon/dist/transform-gizmo-controller.js.map +1 -1
  56. package/node_modules/@fps-games/editor-babylon/dist/types.d.ts +20 -0
  57. package/node_modules/@fps-games/editor-babylon/dist/types.d.ts.map +1 -1
  58. package/node_modules/@fps-games/editor-babylon/package.json +4 -4
  59. package/node_modules/@fps-games/editor-browser/dist/index.d.ts +2 -0
  60. package/node_modules/@fps-games/editor-browser/dist/index.d.ts.map +1 -1
  61. package/node_modules/@fps-games/editor-browser/dist/index.js +1 -0
  62. package/node_modules/@fps-games/editor-browser/dist/index.js.map +1 -1
  63. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-context-menu.d.ts +3 -1
  64. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-context-menu.d.ts.map +1 -1
  65. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-context-menu.js +103 -48
  66. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-context-menu.js.map +1 -1
  67. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.d.ts +17 -1
  68. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.d.ts.map +1 -1
  69. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.js +327 -33
  70. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.js.map +1 -1
  71. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.d.ts +3 -1
  72. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.d.ts.map +1 -1
  73. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.js +143 -17
  74. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.js.map +1 -1
  75. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-tree.js +0 -2
  76. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-tree.js.map +1 -1
  77. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-view.d.ts.map +1 -1
  78. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-view.js +27 -20
  79. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-view.js.map +1 -1
  80. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-icons.d.ts +10 -0
  81. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-icons.d.ts.map +1 -0
  82. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-icons.js +253 -0
  83. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-icons.js.map +1 -0
  84. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.d.ts +8 -1
  85. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.d.ts.map +1 -1
  86. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.js +403 -110
  87. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.js.map +1 -1
  88. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-primitives.d.ts +13 -3
  89. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-primitives.d.ts.map +1 -1
  90. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-primitives.js +107 -14
  91. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-primitives.js.map +1 -1
  92. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.d.ts +20 -3
  93. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.d.ts.map +1 -1
  94. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.js +127 -16
  95. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.js.map +1 -1
  96. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.d.ts.map +1 -1
  97. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.js +13 -4
  98. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.js.map +1 -1
  99. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-theme.d.ts +4 -0
  100. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-theme.d.ts.map +1 -1
  101. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-theme.js +114 -9
  102. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-theme.js.map +1 -1
  103. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-tooltip.d.ts +11 -0
  104. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-tooltip.d.ts.map +1 -0
  105. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-tooltip.js +148 -0
  106. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-tooltip.js.map +1 -0
  107. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts +141 -3
  108. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts.map +1 -1
  109. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.d.ts +35 -0
  110. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.d.ts.map +1 -1
  111. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.js +376 -2
  112. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.js.map +1 -1
  113. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.d.ts +11 -6
  114. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.d.ts.map +1 -1
  115. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js +1617 -110
  116. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js.map +1 -1
  117. package/node_modules/@fps-games/editor-browser/package.json +4 -1
  118. package/node_modules/@fps-games/editor-core/dist/editor-session.d.ts +4 -1
  119. package/node_modules/@fps-games/editor-core/dist/editor-session.d.ts.map +1 -1
  120. package/node_modules/@fps-games/editor-core/dist/editor-session.js +13 -3
  121. package/node_modules/@fps-games/editor-core/dist/editor-session.js.map +1 -1
  122. package/node_modules/@fps-games/editor-core/dist/index.d.ts +3 -0
  123. package/node_modules/@fps-games/editor-core/dist/index.d.ts.map +1 -1
  124. package/node_modules/@fps-games/editor-core/dist/index.js +3 -0
  125. package/node_modules/@fps-games/editor-core/dist/index.js.map +1 -1
  126. package/node_modules/@fps-games/editor-core/dist/inspector.d.ts +6 -1
  127. package/node_modules/@fps-games/editor-core/dist/inspector.d.ts.map +1 -1
  128. package/node_modules/@fps-games/editor-core/dist/inspector.js +6 -1
  129. package/node_modules/@fps-games/editor-core/dist/inspector.js.map +1 -1
  130. package/node_modules/@fps-games/editor-core/dist/scene-graph.d.ts +7 -0
  131. package/node_modules/@fps-games/editor-core/dist/scene-graph.d.ts.map +1 -1
  132. package/node_modules/@fps-games/editor-core/dist/scene-graph.js.map +1 -1
  133. package/node_modules/@fps-games/editor-core/dist/scene-view-input.d.ts +1 -1
  134. package/node_modules/@fps-games/editor-core/dist/scene-view-input.d.ts.map +1 -1
  135. package/node_modules/@fps-games/editor-core/dist/selection-strategy.d.ts +12 -0
  136. package/node_modules/@fps-games/editor-core/dist/selection-strategy.d.ts.map +1 -0
  137. package/node_modules/@fps-games/editor-core/dist/selection-strategy.js +52 -0
  138. package/node_modules/@fps-games/editor-core/dist/selection-strategy.js.map +1 -0
  139. package/node_modules/@fps-games/editor-core/dist/transform-gizmo.d.ts +78 -2
  140. package/node_modules/@fps-games/editor-core/dist/transform-gizmo.d.ts.map +1 -1
  141. package/node_modules/@fps-games/editor-core/dist/transform-gizmo.js +81 -1
  142. package/node_modules/@fps-games/editor-core/dist/transform-gizmo.js.map +1 -1
  143. package/node_modules/@fps-games/editor-core/dist/transform-operations.d.ts +32 -0
  144. package/node_modules/@fps-games/editor-core/dist/transform-operations.d.ts.map +1 -0
  145. package/node_modules/@fps-games/editor-core/dist/transform-operations.js +125 -0
  146. package/node_modules/@fps-games/editor-core/dist/transform-operations.js.map +1 -0
  147. package/node_modules/@fps-games/editor-core/dist/viewport-tools.d.ts +94 -0
  148. package/node_modules/@fps-games/editor-core/dist/viewport-tools.d.ts.map +1 -0
  149. package/node_modules/@fps-games/editor-core/dist/viewport-tools.js +58 -0
  150. package/node_modules/@fps-games/editor-core/dist/viewport-tools.js.map +1 -0
  151. package/node_modules/@fps-games/editor-core/package.json +2 -2
  152. package/node_modules/@fps-games/editor-forge-play/dist/index.d.ts +1 -1
  153. package/node_modules/@fps-games/editor-forge-play/dist/index.d.ts.map +1 -1
  154. package/node_modules/@fps-games/editor-forge-play/dist/index.js +5 -1
  155. package/node_modules/@fps-games/editor-forge-play/dist/index.js.map +1 -1
  156. package/node_modules/@fps-games/editor-forge-play/package.json +2 -2
  157. package/node_modules/@fps-games/editor-protocol/dist/index.d.ts +4 -0
  158. package/node_modules/@fps-games/editor-protocol/dist/index.d.ts.map +1 -1
  159. package/node_modules/@fps-games/editor-protocol/dist/index.js +4 -0
  160. package/node_modules/@fps-games/editor-protocol/dist/index.js.map +1 -1
  161. package/node_modules/@fps-games/editor-protocol/package.json +1 -1
  162. package/package.json +6 -6
@@ -1,17 +1,23 @@
1
1
  import * as LocalEditorPanels from './local-editor-ui-panels';
2
2
  import { createLocalEditorHierarchyController } from './local-editor-ui-hierarchy-controller';
3
3
  import { createLocalEditorContextMenuController } from './local-editor-ui-context-menu';
4
- export { createLocalEditorHierarchyBlankMenu, createLocalEditorHierarchyDeleteShortcutAction, createLocalEditorHierarchyNodeMenu, } from './local-editor-ui-hierarchy-actions';
4
+ import { createLocalEditorTooltipController } from './local-editor-ui-tooltip';
5
+ import { DEFAULT_EDITOR_TRANSFORM_OPERATION_SETTINGS, DEFAULT_EDITOR_TRANSFORM_TOOL_DESCRIPTORS, } from '@fps-games/editor-core';
6
+ export { createLocalEditorHierarchyBlankMenu, createLocalEditorHierarchyCopyShortcutAction, createLocalEditorHierarchyDeleteShortcutAction, createLocalEditorHierarchyDuplicateShortcutAction, createLocalEditorHierarchyNodeMenu, createLocalEditorHierarchyPasteShortcutAction, createLocalEditorHierarchySelectAllShortcutAction, } from './local-editor-ui-hierarchy-actions';
5
7
  export { createLocalEditorHierarchyController, } from './local-editor-ui-hierarchy-controller';
6
8
  export { canLocalEditorHierarchyNodeHaveChildren, createLocalEditorHierarchyTreeModel, isLocalEditorHierarchyNodeMovable, } from './local-editor-ui-hierarchy-tree';
7
9
  export { createLocalEditorContextMenuController, } from './local-editor-ui-context-menu';
10
+ export { createLocalEditorTooltipController, } from './local-editor-ui-tooltip';
8
11
  import { createLocalEditorWorkbenchInputRouter } from './local-editor-ui-input-router';
9
12
  import { createLocalEditorPanelRegistry } from './local-editor-ui-panel-registry';
10
13
  import * as LocalEditorShared from './local-editor-ui-shared';
11
14
  import { createShortcutHelpPanel as createLocalEditorShortcutHelpPanel } from './local-editor-ui-shortcuts';
12
- import { ensureLocalEditorTheme, LOCAL_EDITOR_THEME_CLASS } from './local-editor-ui-theme';
13
- import { createDefaultLocalEditorWorkbenchLayout, createLocalEditorWorkbench, createSceneHeaderToolbar, createWorkbenchPanelContent, } from './local-editor-ui-workbench';
14
- export { applyLocalEditorBrowserInspectorControlBinding, createLocalEditorBrowserInspectorControlRegistry, formatLocalEditorBrowserInspectorValue, resolveLocalEditorBrowserInspectorControlRegistration, } from './local-editor-ui-panels';
15
+ import { applyLocalEditorTheme, ensureLocalEditorTheme, LOCAL_EDITOR_THEME_CLASS, normalizeLocalEditorThemeName, } from './local-editor-ui-theme';
16
+ import { createDefaultLocalEditorWorkbenchLayout, createLocalEditorWorkbenchLayoutController, createLocalEditorWorkbench, createSceneHeaderToolbar, createWorkbenchPanelContent, } from './local-editor-ui-workbench';
17
+ import { createLocalEditorIcon } from './local-editor-ui-icons';
18
+ export { applyLocalEditorBrowserInspectorControlBinding, createLocalEditorBrowserInspectorControlRegistry, formatLocalEditorBrowserInspectorValue, resolveLocalEditorBrowserInspectorSectionStatus, resolveLocalEditorBrowserInspectorControlRegistration, } from './local-editor-ui-panels';
19
+ export { DEFAULT_LOCAL_EDITOR_THEME, LOCAL_EDITOR_THEME_CLASS, applyLocalEditorTheme, normalizeLocalEditorThemeName, } from './local-editor-ui-theme';
20
+ export { LOCAL_EDITOR_ICON_NAMES, createLocalEditorIcon, isLocalEditorIconName, resolveLocalEditorIconName, } from './local-editor-ui-icons';
15
21
  function readInspectorInputValue(input) {
16
22
  const control = input.dataset.serializedControl;
17
23
  const valueType = input.dataset.serializedValueType;
@@ -49,6 +55,99 @@ function readInspectorInputValue(input) {
49
55
  }
50
56
  return input.value;
51
57
  }
58
+ const TRANSFORM_TOOL_ICONS = {
59
+ select: 'select',
60
+ move: 'move',
61
+ rotate: 'rotate',
62
+ scale: 'scale',
63
+ };
64
+ const TRANSFORM_SPACE_ICONS = {
65
+ world: 'world',
66
+ local: 'local',
67
+ };
68
+ const PLACEMENT_MODE_ICONS = {
69
+ off: 'place-off',
70
+ ground: 'place-ground',
71
+ surface: 'place-surface',
72
+ };
73
+ const PLACEMENT_MODE_DESCRIPTIONS = {
74
+ off: '关闭放置模式',
75
+ ground: '放置到 XZ 地面',
76
+ surface: '放置到场景表面',
77
+ };
78
+ const VIEW_PRESET_ICONS = {
79
+ perspective: 'view-perspective',
80
+ top: 'view-top',
81
+ front: 'view-front',
82
+ right: 'view-right',
83
+ };
84
+ const VIEW_PRESET_TOOLTIPS = {
85
+ perspective: '透视视图',
86
+ top: '顶视图 · 自动切换正交',
87
+ front: '前视图 · 自动切换正交',
88
+ right: '右视图 · 自动切换正交',
89
+ };
90
+ const VIEWPORT_OVERLAY_TOGGLE_LABELS = {
91
+ bounds: '包围盒',
92
+ dimensions: '尺寸摘要',
93
+ edgeLengths: '边长标注',
94
+ anchor: '锚点坐标',
95
+ };
96
+ const VIEWPORT_PROJECTION_LABELS = {
97
+ perspective: '透视',
98
+ orthographic: '正交',
99
+ };
100
+ const TRANSFORM_ACTION_GROUPS = [
101
+ {
102
+ title: '对齐到 Active Object',
103
+ hint: '需要至少 2 个选中对象',
104
+ actions: ['align-x', 'align-y', 'align-z', 'align-all'],
105
+ },
106
+ {
107
+ title: '均匀分布',
108
+ hint: '需要至少 3 个选中对象',
109
+ actions: ['distribute-x', 'distribute-y', 'distribute-z'],
110
+ },
111
+ ];
112
+ const TRANSFORM_ACTION_LABELS = {
113
+ 'align-x': '对齐 X',
114
+ 'align-y': '对齐 Y',
115
+ 'align-z': '对齐 Z',
116
+ 'align-all': '对齐全',
117
+ 'distribute-x': '分布 X',
118
+ 'distribute-y': '分布 Y',
119
+ 'distribute-z': '分布 Z',
120
+ };
121
+ function createTransformToolTooltip(descriptor) {
122
+ const toolLabel = descriptor.shortcut
123
+ ? `${descriptor.label}工具 (${descriptor.shortcut})`
124
+ : `${descriptor.label}工具`;
125
+ return `${toolLabel} · ${LocalEditorShared.toTransformMouseHint(descriptor.tool)}`;
126
+ }
127
+ function createToolbarIconButton(doc, label, icon, tooltip = label, toolbarRole = 'command') {
128
+ return LocalEditorShared.createButton(doc, label, {
129
+ icon,
130
+ labelMode: 'icon-only',
131
+ variant: 'toolbar-icon',
132
+ tooltip,
133
+ ariaLabel: tooltip,
134
+ toolbarRole,
135
+ });
136
+ }
137
+ function setToolbarButtonTooltip(button, tooltip) {
138
+ button.dataset.editorTooltip = tooltip;
139
+ button.setAttribute('aria-label', tooltip);
140
+ }
141
+ function setToolbarButtonIcon(doc, button, icon) {
142
+ const currentIcon = button.querySelector('[data-editor-icon]');
143
+ if (currentIcon?.dataset.editorIcon === icon)
144
+ return;
145
+ const nextIcon = createLocalEditorIcon(doc, icon);
146
+ if (currentIcon)
147
+ currentIcon.replaceWith(nextIcon);
148
+ else
149
+ button.prepend(nextIcon);
150
+ }
52
151
  function readInspectorVectorInputValue(input) {
53
152
  const wrapper = input.closest('[data-inspector-vector-control]');
54
153
  const values = {};
@@ -94,14 +193,391 @@ function readInspectorImmediateSource(input) {
94
193
  return 'color';
95
194
  return 'input';
96
195
  }
196
+ function createCoordinateAxesOverlay(doc) {
197
+ const root = doc.createElement('div');
198
+ root.dataset.editorCoordinateAxesOverlay = 'true';
199
+ root.style.cssText = [
200
+ 'position:absolute',
201
+ 'right:14px',
202
+ 'bottom:14px',
203
+ 'width:112px',
204
+ 'height:112px',
205
+ 'display:none',
206
+ 'z-index:2',
207
+ 'box-sizing:border-box',
208
+ 'border:1px solid color-mix(in srgb, var(--fps-editor-border) 78%, transparent)',
209
+ 'border-radius:8px',
210
+ 'background:color-mix(in srgb, var(--fps-editor-chrome) 58%, transparent)',
211
+ 'box-shadow:var(--fps-editor-shadow-panel)',
212
+ 'pointer-events:none',
213
+ 'overflow:hidden',
214
+ ].join(';');
215
+ const svg = doc.createElementNS('http://www.w3.org/2000/svg', 'svg');
216
+ svg.setAttribute('viewBox', '0 0 112 112');
217
+ svg.setAttribute('aria-hidden', 'true');
218
+ svg.style.cssText = 'display:block;width:100%;height:100%';
219
+ root.appendChild(svg);
220
+ const lineLayer = doc.createElementNS('http://www.w3.org/2000/svg', 'g');
221
+ const labelLayer = doc.createElementNS('http://www.w3.org/2000/svg', 'g');
222
+ svg.appendChild(lineLayer);
223
+ svg.appendChild(labelLayer);
224
+ const axisGroups = new Map();
225
+ const axisLines = new Map();
226
+ const axisLabels = new Map();
227
+ const axisDiscs = new Map();
228
+ for (const axis of ['x', 'y', 'z']) {
229
+ const group = doc.createElementNS('http://www.w3.org/2000/svg', 'g');
230
+ const line = doc.createElementNS('http://www.w3.org/2000/svg', 'line');
231
+ line.setAttribute('stroke-width', '3.2');
232
+ line.setAttribute('stroke-linecap', 'round');
233
+ line.setAttribute('x1', '56');
234
+ line.setAttribute('y1', '56');
235
+ const disc = doc.createElementNS('http://www.w3.org/2000/svg', 'circle');
236
+ disc.setAttribute('r', '7');
237
+ const label = doc.createElementNS('http://www.w3.org/2000/svg', 'text');
238
+ label.setAttribute('text-anchor', 'middle');
239
+ label.setAttribute('dominant-baseline', 'middle');
240
+ label.setAttribute('dy', '0.04em');
241
+ label.style.cssText = [
242
+ 'fill:#172033',
243
+ 'font-size:10px',
244
+ 'font-weight:900',
245
+ 'font-family:Arial, Helvetica, sans-serif',
246
+ 'letter-spacing:0',
247
+ ].join(';');
248
+ group.appendChild(disc);
249
+ group.appendChild(label);
250
+ lineLayer.appendChild(line);
251
+ labelLayer.appendChild(group);
252
+ axisGroups.set(axis, group);
253
+ axisLines.set(axis, line);
254
+ axisLabels.set(axis, label);
255
+ axisDiscs.set(axis, disc);
256
+ }
257
+ const centerDot = doc.createElementNS('http://www.w3.org/2000/svg', 'circle');
258
+ centerDot.setAttribute('cx', '56');
259
+ centerDot.setAttribute('cy', '56');
260
+ centerDot.setAttribute('r', '3.2');
261
+ centerDot.setAttribute('fill', 'color-mix(in srgb, var(--fps-editor-text) 82%, transparent)');
262
+ labelLayer.appendChild(centerDot);
263
+ return {
264
+ root,
265
+ lineLayer,
266
+ labelLayer,
267
+ axisGroups,
268
+ axisLines,
269
+ axisLabels,
270
+ axisDiscs,
271
+ centerDot,
272
+ };
273
+ }
274
+ function renderCoordinateAxesOverlay(overlay, state) {
275
+ if (!state?.axes?.length) {
276
+ overlay.root.style.display = 'none';
277
+ return;
278
+ }
279
+ overlay.root.style.display = '';
280
+ const center = 56;
281
+ const lineLength = 38;
282
+ const sortedAxes = [...state.axes].sort((left, right) => right.depth - left.depth);
283
+ for (const axis of sortedAxes) {
284
+ const group = overlay.axisGroups.get(axis.id);
285
+ const line = overlay.axisLines.get(axis.id);
286
+ const disc = overlay.axisDiscs.get(axis.id);
287
+ const label = overlay.axisLabels.get(axis.id);
288
+ if (!group || !line || !disc || !label)
289
+ continue;
290
+ const length = lineLength * axis.scale;
291
+ const endX = center + axis.x * length;
292
+ const endY = center + axis.y * length;
293
+ line.setAttribute('x2', formatSvgNumber(endX));
294
+ line.setAttribute('y2', formatSvgNumber(endY));
295
+ line.setAttribute('stroke', axis.color);
296
+ disc.setAttribute('cx', formatSvgNumber(endX));
297
+ disc.setAttribute('cy', formatSvgNumber(endY));
298
+ disc.setAttribute('fill', axis.color);
299
+ label.setAttribute('x', formatSvgNumber(endX));
300
+ label.setAttribute('y', formatSvgNumber(endY));
301
+ label.textContent = axis.label;
302
+ group.style.opacity = String(0.72 + axis.scale * 0.28);
303
+ overlay.lineLayer.appendChild(line);
304
+ overlay.labelLayer.appendChild(group);
305
+ }
306
+ overlay.labelLayer.appendChild(overlay.centerDot);
307
+ }
308
+ function createSpatialOverlay(doc) {
309
+ const root = doc.createElement('div');
310
+ root.classList.add(LOCAL_EDITOR_THEME_CLASS);
311
+ root.dataset.editorSpatialOverlay = 'true';
312
+ root.style.cssText = [
313
+ 'position:absolute',
314
+ 'top:38px',
315
+ 'left:0',
316
+ 'right:0',
317
+ 'bottom:0',
318
+ 'z-index:1',
319
+ 'display:none',
320
+ 'pointer-events:none',
321
+ 'font-family:var(--fps-editor-font)',
322
+ 'color:var(--fps-editor-text)',
323
+ 'overflow:hidden',
324
+ ].join(';');
325
+ const svg = doc.createElementNS('http://www.w3.org/2000/svg', 'svg');
326
+ svg.setAttribute('aria-hidden', 'true');
327
+ svg.style.cssText = [
328
+ 'position:absolute',
329
+ 'inset:0',
330
+ 'width:100%',
331
+ 'height:100%',
332
+ 'overflow:hidden',
333
+ ].join(';');
334
+ const lineLayer = doc.createElementNS('http://www.w3.org/2000/svg', 'g');
335
+ const markerLayer = doc.createElementNS('http://www.w3.org/2000/svg', 'g');
336
+ svg.appendChild(lineLayer);
337
+ svg.appendChild(markerLayer);
338
+ root.appendChild(svg);
339
+ const labelLayer = doc.createElement('div');
340
+ labelLayer.style.cssText = 'position:absolute;inset:0;pointer-events:none';
341
+ root.appendChild(labelLayer);
342
+ return {
343
+ root,
344
+ svg,
345
+ lineLayer,
346
+ markerLayer,
347
+ labelLayer,
348
+ lines: new Map(),
349
+ markers: new Map(),
350
+ labels: new Map(),
351
+ };
352
+ }
353
+ function renderSpatialOverlay(overlay, state) {
354
+ if (!state?.active) {
355
+ overlay.root.style.display = 'none';
356
+ clearSpatialOverlayElements(overlay);
357
+ return;
358
+ }
359
+ overlay.root.style.display = '';
360
+ const rect = overlay.root.getBoundingClientRect();
361
+ overlay.svg.setAttribute('width', formatSvgNumber(rect.width));
362
+ overlay.svg.setAttribute('height', formatSvgNumber(rect.height));
363
+ overlay.svg.setAttribute('viewBox', `0 0 ${formatSvgNumber(rect.width)} ${formatSvgNumber(rect.height)}`);
364
+ const visibleLineIds = new Set();
365
+ for (const lineState of state.lines) {
366
+ const line = ensureSpatialOverlayLine(overlay, lineState.id);
367
+ line.setAttribute('x1', formatSvgNumber(lineState.start.x - rect.left));
368
+ line.setAttribute('y1', formatSvgNumber(lineState.start.y - rect.top));
369
+ line.setAttribute('x2', formatSvgNumber(lineState.end.x - rect.left));
370
+ line.setAttribute('y2', formatSvgNumber(lineState.end.y - rect.top));
371
+ visibleLineIds.add(lineState.id);
372
+ }
373
+ for (const [id, line] of overlay.lines) {
374
+ if (!visibleLineIds.has(id)) {
375
+ line.remove();
376
+ overlay.lines.delete(id);
377
+ }
378
+ }
379
+ const visibleMarkerIds = new Set();
380
+ for (const markerState of state.markers) {
381
+ const marker = ensureSpatialOverlayMarker(overlay, markerState.id);
382
+ marker.setAttribute('cx', formatSvgNumber(markerState.position.x - rect.left));
383
+ marker.setAttribute('cy', formatSvgNumber(markerState.position.y - rect.top));
384
+ visibleMarkerIds.add(markerState.id);
385
+ }
386
+ for (const [id, marker] of overlay.markers) {
387
+ if (!visibleMarkerIds.has(id)) {
388
+ marker.remove();
389
+ overlay.markers.delete(id);
390
+ }
391
+ }
392
+ const visibleLabelIds = new Set();
393
+ for (const labelState of state.labels) {
394
+ const label = ensureSpatialOverlayLabel(overlay, labelState.id, labelState.kind);
395
+ label.textContent = labelState.text;
396
+ label.style.left = `${labelState.x - rect.left}px`;
397
+ label.style.top = `${labelState.y - rect.top}px`;
398
+ visibleLabelIds.add(labelState.id);
399
+ }
400
+ for (const [id, label] of overlay.labels) {
401
+ if (!visibleLabelIds.has(id)) {
402
+ label.remove();
403
+ overlay.labels.delete(id);
404
+ }
405
+ }
406
+ }
407
+ function clearSpatialOverlayElements(overlay) {
408
+ for (const line of overlay.lines.values())
409
+ line.remove();
410
+ for (const marker of overlay.markers.values())
411
+ marker.remove();
412
+ for (const label of overlay.labels.values())
413
+ label.remove();
414
+ overlay.lines.clear();
415
+ overlay.markers.clear();
416
+ overlay.labels.clear();
417
+ }
418
+ function ensureSpatialOverlayLine(overlay, id) {
419
+ const existing = overlay.lines.get(id);
420
+ if (existing)
421
+ return existing;
422
+ const line = overlay.root.ownerDocument.createElementNS('http://www.w3.org/2000/svg', 'line');
423
+ line.setAttribute('stroke', 'color-mix(in srgb, var(--fps-editor-drop-target) 82%, white)');
424
+ line.setAttribute('stroke-width', '1.4');
425
+ line.setAttribute('stroke-linecap', 'round');
426
+ line.setAttribute('vector-effect', 'non-scaling-stroke');
427
+ line.style.filter = 'drop-shadow(0 1px 2px rgba(0,0,0,0.45))';
428
+ overlay.lineLayer.appendChild(line);
429
+ overlay.lines.set(id, line);
430
+ return line;
431
+ }
432
+ function ensureSpatialOverlayMarker(overlay, id) {
433
+ const existing = overlay.markers.get(id);
434
+ if (existing)
435
+ return existing;
436
+ const marker = overlay.root.ownerDocument.createElementNS('http://www.w3.org/2000/svg', 'circle');
437
+ marker.setAttribute('r', '4.5');
438
+ marker.setAttribute('fill', 'var(--fps-editor-drop-target)');
439
+ marker.setAttribute('stroke', 'var(--fps-editor-panel)');
440
+ marker.setAttribute('stroke-width', '2');
441
+ marker.style.filter = 'drop-shadow(0 1px 2px rgba(0,0,0,0.5))';
442
+ overlay.markerLayer.appendChild(marker);
443
+ overlay.markers.set(id, marker);
444
+ return marker;
445
+ }
446
+ function ensureSpatialOverlayLabel(overlay, id, kind) {
447
+ const existing = overlay.labels.get(id);
448
+ if (existing)
449
+ return existing;
450
+ const label = overlay.root.ownerDocument.createElement('div');
451
+ label.dataset.editorSpatialOverlayLabel = kind;
452
+ label.style.cssText = [
453
+ 'position:absolute',
454
+ 'transform:translate(-50%, -50%)',
455
+ 'max-width:220px',
456
+ 'padding:2px 5px',
457
+ 'border:1px solid color-mix(in srgb, var(--fps-editor-border) 82%, transparent)',
458
+ 'border-radius:3px',
459
+ 'background:color-mix(in srgb, var(--fps-editor-panel) 88%, transparent)',
460
+ 'box-shadow:0 1px 4px rgba(0,0,0,0.22)',
461
+ 'color:var(--fps-editor-text-strong)',
462
+ 'font-size:10px',
463
+ 'font-weight:900',
464
+ 'line-height:1.25',
465
+ 'letter-spacing:0',
466
+ 'white-space:nowrap',
467
+ ].join(';');
468
+ if (kind === 'anchor')
469
+ label.style.color = 'var(--fps-editor-drop-target)';
470
+ overlay.labelLayer.appendChild(label);
471
+ overlay.labels.set(id, label);
472
+ return label;
473
+ }
474
+ function createMeasurementOverlay(doc) {
475
+ const root = doc.createElement('div');
476
+ root.classList.add(LOCAL_EDITOR_THEME_CLASS);
477
+ root.dataset.editorMeasurementOverlay = 'true';
478
+ root.style.cssText = [
479
+ 'position:absolute',
480
+ 'top:38px',
481
+ 'left:0',
482
+ 'right:0',
483
+ 'bottom:0',
484
+ 'z-index:2',
485
+ 'display:none',
486
+ 'pointer-events:none',
487
+ 'overflow:hidden',
488
+ 'font-family:var(--fps-editor-font)',
489
+ ].join(';');
490
+ const svg = doc.createElementNS('http://www.w3.org/2000/svg', 'svg');
491
+ svg.setAttribute('aria-hidden', 'true');
492
+ svg.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;overflow:hidden';
493
+ const line = doc.createElementNS('http://www.w3.org/2000/svg', 'line');
494
+ line.setAttribute('stroke', 'var(--fps-editor-drop-target)');
495
+ line.setAttribute('stroke-width', '2');
496
+ line.setAttribute('stroke-linecap', 'round');
497
+ line.setAttribute('vector-effect', 'non-scaling-stroke');
498
+ line.style.filter = 'drop-shadow(0 1px 2px rgba(0,0,0,0.5))';
499
+ const startMarker = createMeasurementMarker(doc);
500
+ const endMarker = createMeasurementMarker(doc);
501
+ svg.appendChild(line);
502
+ svg.appendChild(startMarker);
503
+ svg.appendChild(endMarker);
504
+ root.appendChild(svg);
505
+ const label = doc.createElement('div');
506
+ label.dataset.editorMeasurementLabel = 'true';
507
+ label.style.cssText = [
508
+ 'position:absolute',
509
+ 'transform:translate(-50%, -50%)',
510
+ 'padding:3px 6px',
511
+ 'border:1px solid color-mix(in srgb, var(--fps-editor-drop-target) 72%, var(--fps-editor-border))',
512
+ 'border-radius:3px',
513
+ 'background:color-mix(in srgb, var(--fps-editor-panel) 90%, transparent)',
514
+ 'box-shadow:0 1px 4px rgba(0,0,0,0.24)',
515
+ 'color:var(--fps-editor-text-strong)',
516
+ 'font-size:10px',
517
+ 'font-weight:900',
518
+ 'line-height:1.25',
519
+ 'letter-spacing:0',
520
+ 'white-space:nowrap',
521
+ ].join(';');
522
+ root.appendChild(label);
523
+ return { root, svg, line, startMarker, endMarker, label };
524
+ }
525
+ function createMeasurementMarker(doc) {
526
+ const marker = doc.createElementNS('http://www.w3.org/2000/svg', 'circle');
527
+ marker.setAttribute('r', '4');
528
+ marker.setAttribute('fill', 'var(--fps-editor-drop-target)');
529
+ marker.setAttribute('stroke', 'var(--fps-editor-panel)');
530
+ marker.setAttribute('stroke-width', '2');
531
+ return marker;
532
+ }
533
+ function renderMeasurementOverlay(overlay, state) {
534
+ const start = state?.screenStart ?? null;
535
+ const end = state?.screenEnd ?? state?.screenPreview ?? null;
536
+ if (!start || !end) {
537
+ overlay.root.style.display = 'none';
538
+ return;
539
+ }
540
+ overlay.root.style.display = '';
541
+ const rect = overlay.root.getBoundingClientRect();
542
+ overlay.svg.setAttribute('width', formatSvgNumber(rect.width));
543
+ overlay.svg.setAttribute('height', formatSvgNumber(rect.height));
544
+ overlay.svg.setAttribute('viewBox', `0 0 ${formatSvgNumber(rect.width)} ${formatSvgNumber(rect.height)}`);
545
+ const startX = start.x - rect.left;
546
+ const startY = start.y - rect.top;
547
+ const endX = end.x - rect.left;
548
+ const endY = end.y - rect.top;
549
+ overlay.line.setAttribute('x1', formatSvgNumber(startX));
550
+ overlay.line.setAttribute('y1', formatSvgNumber(startY));
551
+ overlay.line.setAttribute('x2', formatSvgNumber(endX));
552
+ overlay.line.setAttribute('y2', formatSvgNumber(endY));
553
+ overlay.startMarker.setAttribute('cx', formatSvgNumber(startX));
554
+ overlay.startMarker.setAttribute('cy', formatSvgNumber(startY));
555
+ overlay.endMarker.setAttribute('cx', formatSvgNumber(endX));
556
+ overlay.endMarker.setAttribute('cy', formatSvgNumber(endY));
557
+ overlay.label.textContent = state?.label?.text ?? '';
558
+ overlay.label.style.display = state?.label ? '' : 'none';
559
+ if (state?.label) {
560
+ overlay.label.style.left = `${state.label.x - rect.left}px`;
561
+ overlay.label.style.top = `${state.label.y - rect.top}px`;
562
+ }
563
+ }
564
+ function formatSvgNumber(value) {
565
+ return Number.isFinite(value) ? value.toFixed(2) : '0';
566
+ }
97
567
  export function createLocalEditorBrowserUi(options = {}) {
98
568
  const doc = options.document ?? document;
99
569
  const root = options.root ?? doc.body;
100
570
  const callbacks = options.callbacks ?? {};
571
+ const localTestActionsEnabled = options.localTestActions === true;
101
572
  ensureLocalEditorTheme(doc);
573
+ let activeTheme = normalizeLocalEditorThemeName(options.theme);
102
574
  const inputRouter = createLocalEditorWorkbenchInputRouter(doc);
103
575
  const contextMenu = createLocalEditorContextMenuController(doc, (open) => {
104
576
  inputRouter.setContextMenuOpen(open);
577
+ }, activeTheme);
578
+ const tooltipSurfaces = new Set();
579
+ const tooltipController = createLocalEditorTooltipController(doc, root, activeTheme, {
580
+ scope: () => tooltipSurfaces,
105
581
  });
106
582
  let currentState = null;
107
583
  let inspectorFilter = '';
@@ -124,7 +600,7 @@ export function createLocalEditorBrowserUi(options = {}) {
124
600
  'border:1px solid var(--fps-editor-border)',
125
601
  'border-radius:3px',
126
602
  'background:var(--fps-editor-chrome)',
127
- 'box-shadow:0 10px 28px rgba(0,0,0,0.24)',
603
+ 'box-shadow:var(--fps-editor-shadow-panel)',
128
604
  'font-family:var(--fps-editor-font)',
129
605
  'color:var(--fps-editor-text)',
130
606
  'font-size:12px',
@@ -137,41 +613,68 @@ export function createLocalEditorBrowserUi(options = {}) {
137
613
  'font-weight:800',
138
614
  'white-space:nowrap',
139
615
  ].join(';');
140
- const enterEditorButton = LocalEditorShared.createButton(doc, '进入编辑场景');
616
+ const enterEditorButton = LocalEditorShared.createButton(doc, '进入编辑场景', { icon: 'execute' });
141
617
  hostChrome.appendChild(hostModeLabel);
142
618
  hostChrome.appendChild(enterEditorButton);
143
619
  root.appendChild(hostChrome);
620
+ tooltipSurfaces.add(hostChrome);
144
621
  const dirtyBadge = doc.createElement('span');
622
+ dirtyBadge.dataset.editorDirtyBadge = 'true';
623
+ dirtyBadge.dataset.editorTooltip = '有未保存更改';
624
+ dirtyBadge.setAttribute('role', 'status');
625
+ dirtyBadge.setAttribute('aria-label', '有未保存更改');
145
626
  dirtyBadge.style.cssText = [
146
627
  'display:none',
147
- 'height:20px',
628
+ 'width:8px',
629
+ 'height:8px',
148
630
  'align-items:center',
149
- 'padding:0 7px',
631
+ 'justify-content:center',
632
+ 'padding:0',
150
633
  'border-radius:999px',
151
- 'background:rgba(248,81,73,0.16)',
152
- 'border:1px solid rgba(248,81,73,0.45)',
153
- 'color:#ffd7d5',
154
- 'font-size:11px',
155
- 'font-weight:800',
634
+ 'background:var(--fps-editor-danger-strong)',
635
+ 'border:1px solid var(--fps-editor-danger-border)',
636
+ 'box-shadow:0 0 0 3px var(--fps-editor-danger-soft)',
156
637
  ].join(';');
157
- dirtyBadge.textContent = '未保存';
158
- const saveButton = LocalEditorShared.createButton(doc, '保存场景');
159
- const saveAndRunButton = LocalEditorShared.createButton(doc, '保存并运行');
160
- const discardRunButton = LocalEditorShared.createButton(doc, '放弃并运行');
161
- const undoButton = LocalEditorShared.createButton(doc, '撤销');
162
- const redoButton = LocalEditorShared.createButton(doc, '重做');
163
- let helpOpen = false;
164
- const status = doc.createElement('span');
165
- status.style.cssText = [
166
- 'flex:0 1 300px',
167
- 'min-width:120px',
168
- 'max-width:320px',
169
- 'overflow:hidden',
170
- 'text-overflow:ellipsis',
171
- 'white-space:nowrap',
172
- 'color:var(--fps-editor-muted)',
173
- 'font-size:11px',
638
+ const saveButton = LocalEditorShared.createButton(doc, '保存场景', { icon: 'save' });
639
+ const saveAndRunButton = LocalEditorShared.createButton(doc, '保存并运行', { icon: 'execute' });
640
+ const discardRunButton = LocalEditorShared.createButton(doc, '放弃并运行', { icon: 'discard' });
641
+ const localTestButton = createToolbarIconButton(doc, '本地测试', 'execute', '打开本地测试操作');
642
+ localTestButton.dataset.editorLocalTestToggle = 'true';
643
+ const localTestMenu = doc.createElement('div');
644
+ localTestMenu.classList.add(LOCAL_EDITOR_THEME_CLASS);
645
+ localTestMenu.dataset.editorLocalTestMenu = 'true';
646
+ localTestMenu.style.cssText = [
647
+ 'position:fixed',
648
+ 'z-index:2147483641',
649
+ 'display:none',
650
+ 'flex-direction:column',
651
+ 'gap:6px',
652
+ 'min-width:150px',
653
+ 'padding:8px',
654
+ 'border:1px solid var(--fps-editor-border)',
655
+ 'border-radius:3px',
656
+ 'background:var(--fps-editor-panel)',
657
+ 'box-shadow:var(--fps-editor-shadow-popover)',
658
+ 'pointer-events:auto',
174
659
  ].join(';');
660
+ for (const button of [saveButton, saveAndRunButton, discardRunButton]) {
661
+ button.style.justifyContent = 'flex-start';
662
+ button.style.width = '100%';
663
+ localTestMenu.appendChild(button);
664
+ }
665
+ const localTestGroup = doc.createElement('div');
666
+ localTestGroup.style.cssText = [
667
+ `display:${localTestActionsEnabled ? 'flex' : 'none'}`,
668
+ 'align-items:center',
669
+ 'gap:4px',
670
+ ].join(';');
671
+ localTestGroup.appendChild(localTestButton);
672
+ const undoButton = createToolbarIconButton(doc, '撤销', 'undo', '撤销');
673
+ const redoButton = createToolbarIconButton(doc, '重做', 'redo', '重做');
674
+ let helpOpen = false;
675
+ const editorStatusButton = createToolbarIconButton(doc, '编辑器状态', 'status', '编辑器状态');
676
+ editorStatusButton.dataset.editorStatusButton = 'true';
677
+ editorStatusButton.style.cursor = 'help';
175
678
  const workbench = createLocalEditorWorkbench(doc);
176
679
  const hierarchyPanel = createWorkbenchPanelContent(doc);
177
680
  const assetPanel = workbench.bottomDock;
@@ -179,6 +682,8 @@ export function createLocalEditorBrowserUi(options = {}) {
179
682
  workbench.leftDock.appendChild(hierarchyPanel);
180
683
  workbench.rightDock.appendChild(inspectorPanel);
181
684
  root.appendChild(workbench.root);
685
+ tooltipSurfaces.add(workbench.root);
686
+ const workbenchLayoutController = createLocalEditorWorkbenchLayoutController(doc, workbench);
182
687
  const sceneToolOverlay = createSceneHeaderToolbar(doc);
183
688
  const sceneTitle = doc.createElement('div');
184
689
  sceneTitle.textContent = 'Preview';
@@ -196,6 +701,14 @@ export function createLocalEditorBrowserUi(options = {}) {
196
701
  ].join(';');
197
702
  const sceneQuickActions = doc.createElement('div');
198
703
  sceneQuickActions.style.cssText = 'display:flex;align-items:center;gap:4px';
704
+ const sceneUtilityActions = doc.createElement('div');
705
+ sceneUtilityActions.style.cssText = [
706
+ 'display:flex',
707
+ 'align-items:center',
708
+ 'gap:4px',
709
+ 'padding-left:8px',
710
+ 'border-left:1px solid var(--fps-editor-divider)',
711
+ ].join(';');
199
712
  const toolGroup = doc.createElement('div');
200
713
  toolGroup.style.cssText = [
201
714
  'display:flex',
@@ -204,14 +717,19 @@ export function createLocalEditorBrowserUi(options = {}) {
204
717
  'padding-left:8px',
205
718
  'border-left:1px solid var(--fps-editor-divider)',
206
719
  ].join(';');
207
- const toolButtons = {
208
- select: LocalEditorShared.createButton(doc, 'Q 选择'),
209
- move: LocalEditorShared.createButton(doc, 'W 移动'),
210
- rotate: LocalEditorShared.createButton(doc, 'E 旋转'),
211
- scale: LocalEditorShared.createButton(doc, 'R 缩放'),
212
- };
213
- for (const [tool, button] of Object.entries(toolButtons)) {
214
- button.dataset.transformTool = tool;
720
+ const toolButtons = new Map();
721
+ const transformToolDescriptors = Object.values(DEFAULT_EDITOR_TRANSFORM_TOOL_DESCRIPTORS);
722
+ for (const descriptor of transformToolDescriptors) {
723
+ const tooltip = createTransformToolTooltip(descriptor);
724
+ const button = LocalEditorShared.createButton(doc, descriptor.label, {
725
+ icon: TRANSFORM_TOOL_ICONS[descriptor.tool],
726
+ labelMode: 'icon-only',
727
+ variant: 'toolbar-icon',
728
+ tooltip,
729
+ ariaLabel: tooltip,
730
+ });
731
+ button.dataset.transformTool = descriptor.tool;
732
+ toolButtons.set(descriptor.tool, button);
215
733
  toolGroup.appendChild(button);
216
734
  }
217
735
  const spaceGroup = doc.createElement('div');
@@ -223,83 +741,839 @@ export function createLocalEditorBrowserUi(options = {}) {
223
741
  'border-left:1px solid var(--fps-editor-divider)',
224
742
  ].join(';');
225
743
  const spaceButtons = {
226
- world: LocalEditorShared.createButton(doc, '世界'),
227
- local: LocalEditorShared.createButton(doc, '本地'),
744
+ world: createToolbarIconButton(doc, '世界', TRANSFORM_SPACE_ICONS.world, '世界坐标空间'),
745
+ local: createToolbarIconButton(doc, '本地', TRANSFORM_SPACE_ICONS.local, '本地坐标空间'),
228
746
  };
229
747
  for (const [space, button] of Object.entries(spaceButtons)) {
230
748
  button.dataset.transformSpace = space;
231
749
  spaceGroup.appendChild(button);
232
750
  }
233
- const constraintGroup = doc.createElement('div');
234
- constraintGroup.style.cssText = [
751
+ const snapGroup = doc.createElement('div');
752
+ snapGroup.style.cssText = [
235
753
  'display:flex',
236
754
  'align-items:center',
237
755
  'gap:4px',
238
756
  'padding-left:8px',
239
757
  'border-left:1px solid var(--fps-editor-divider)',
240
758
  ].join(';');
241
- const viewPlaneButton = LocalEditorShared.createButton(doc, '视图平面');
242
- viewPlaneButton.dataset.transformConstraint = 'view-plane';
243
- constraintGroup.appendChild(viewPlaneButton);
244
- const sceneToolStatus = doc.createElement('span');
245
- sceneToolStatus.style.cssText = [
246
- 'max-width:220px',
247
- 'overflow:hidden',
248
- 'text-overflow:ellipsis',
249
- 'white-space:nowrap',
250
- 'color:var(--fps-editor-muted-strong)',
251
- 'font-size:11px',
759
+ const snapButton = createToolbarIconButton(doc, '吸附设置', 'snap', '吸附设置', 'settings');
760
+ snapButton.dataset.transformSnapToggle = 'true';
761
+ const snapSettingsPopover = doc.createElement('div');
762
+ snapSettingsPopover.classList.add(LOCAL_EDITOR_THEME_CLASS);
763
+ snapSettingsPopover.dataset.transformSnapSettingsPopover = 'true';
764
+ snapSettingsPopover.setAttribute('role', 'dialog');
765
+ snapSettingsPopover.setAttribute('aria-label', '吸附设置');
766
+ snapSettingsPopover.style.cssText = [
767
+ 'position:fixed',
768
+ 'z-index:2147483641',
769
+ 'display:none',
770
+ 'flex-direction:column',
771
+ 'gap:8px',
772
+ 'width:220px',
773
+ 'padding:10px',
774
+ 'border:1px solid var(--fps-editor-border)',
775
+ 'border-radius:3px',
776
+ 'background:var(--fps-editor-panel)',
777
+ 'box-shadow:var(--fps-editor-shadow-popover)',
778
+ 'pointer-events:auto',
252
779
  ].join(';');
253
- const sceneMouseHint = doc.createElement('span');
254
- sceneMouseHint.style.cssText = [
780
+ const snapSettingsHeader = doc.createElement('label');
781
+ snapSettingsHeader.style.cssText = [
782
+ 'display:flex',
783
+ 'align-items:center',
784
+ 'justify-content:space-between',
785
+ 'gap:8px',
786
+ 'color:var(--fps-editor-text)',
787
+ 'font-size:12px',
788
+ 'font-weight:900',
789
+ ].join(';');
790
+ const snapSettingsHeaderText = doc.createElement('span');
791
+ snapSettingsHeaderText.textContent = '启用吸附';
792
+ const snapEnabledInput = doc.createElement('input');
793
+ snapEnabledInput.type = 'checkbox';
794
+ snapEnabledInput.dataset.transformSnapEnabled = 'true';
795
+ snapSettingsHeader.appendChild(snapSettingsHeaderText);
796
+ snapSettingsHeader.appendChild(snapEnabledInput);
797
+ snapSettingsPopover.appendChild(snapSettingsHeader);
798
+ const snapStepInputs = new Map();
799
+ for (const [kind, label, unit] of [
800
+ ['move', '位移', '单位'],
801
+ ['rotate', '旋转', '度'],
802
+ ['scale', '缩放', '倍数'],
803
+ ]) {
804
+ const row = doc.createElement('label');
805
+ row.style.cssText = [
806
+ 'display:grid',
807
+ 'grid-template-columns:54px 1fr 34px',
808
+ 'align-items:center',
809
+ 'gap:8px',
810
+ 'color:var(--fps-editor-muted-strong)',
811
+ 'font-size:11px',
812
+ 'font-weight:800',
813
+ ].join(';');
814
+ const rowLabel = doc.createElement('span');
815
+ rowLabel.textContent = label;
816
+ const input = doc.createElement('input');
817
+ input.type = 'number';
818
+ input.min = '0.0001';
819
+ input.step = kind === 'rotate' ? '1' : '0.1';
820
+ input.dataset.transformSnapStep = kind;
821
+ input.style.cssText = [
822
+ 'height:26px',
823
+ 'min-width:0',
824
+ 'box-sizing:border-box',
825
+ 'border:1px solid var(--fps-editor-border)',
826
+ 'border-radius:3px',
827
+ 'background:var(--fps-editor-field)',
828
+ 'color:var(--fps-editor-text)',
829
+ 'font-size:11px',
830
+ 'font-weight:800',
831
+ 'padding:0 4px',
832
+ ].join(';');
833
+ const rowUnit = doc.createElement('span');
834
+ rowUnit.textContent = unit;
835
+ rowUnit.style.cssText = 'color:var(--fps-editor-muted);font-size:10px;font-weight:800';
836
+ row.appendChild(rowLabel);
837
+ row.appendChild(input);
838
+ row.appendChild(rowUnit);
839
+ snapSettingsPopover.appendChild(row);
840
+ snapStepInputs.set(kind, input);
841
+ }
842
+ snapGroup.appendChild(snapButton);
843
+ const placementGroup = doc.createElement('div');
844
+ placementGroup.style.cssText = [
845
+ 'display:flex',
846
+ 'align-items:center',
847
+ 'gap:4px',
848
+ 'padding-left:8px',
849
+ 'border-left:1px solid var(--fps-editor-divider)',
850
+ ].join(';');
851
+ const placementButton = createToolbarIconButton(doc, '放置模式', PLACEMENT_MODE_ICONS.off, '放置模式', 'settings');
852
+ placementButton.dataset.placementModeToggle = 'true';
853
+ const placementSettingsPopover = doc.createElement('div');
854
+ placementSettingsPopover.classList.add(LOCAL_EDITOR_THEME_CLASS);
855
+ placementSettingsPopover.dataset.placementSettingsPopover = 'true';
856
+ placementSettingsPopover.setAttribute('role', 'dialog');
857
+ placementSettingsPopover.setAttribute('aria-label', '放置模式');
858
+ placementSettingsPopover.style.cssText = [
859
+ 'position:fixed',
860
+ 'z-index:2147483641',
861
+ 'display:none',
862
+ 'flex-direction:column',
863
+ 'gap:5px',
864
+ 'width:180px',
865
+ 'padding:8px',
866
+ 'border:1px solid var(--fps-editor-border)',
867
+ 'border-radius:3px',
868
+ 'background:var(--fps-editor-panel)',
869
+ 'box-shadow:var(--fps-editor-shadow-popover)',
870
+ 'pointer-events:auto',
871
+ ].join(';');
872
+ const placementButtons = Object.fromEntries(Object.keys(PLACEMENT_MODE_ICONS).map((mode) => {
873
+ const button = LocalEditorShared.createButton(doc, PLACEMENT_MODE_DESCRIPTIONS[mode], {
874
+ icon: PLACEMENT_MODE_ICONS[mode],
875
+ tooltip: PLACEMENT_MODE_DESCRIPTIONS[mode],
876
+ ariaLabel: PLACEMENT_MODE_DESCRIPTIONS[mode],
877
+ });
878
+ button.dataset.placementMode = mode;
879
+ button.style.justifyContent = 'flex-start';
880
+ button.style.width = '100%';
881
+ placementSettingsPopover.appendChild(button);
882
+ return [mode, button];
883
+ }));
884
+ placementGroup.appendChild(placementButton);
885
+ const actionGroup = doc.createElement('div');
886
+ actionGroup.style.cssText = [
887
+ 'display:flex',
888
+ 'align-items:center',
889
+ 'gap:4px',
255
890
  'padding-left:8px',
256
891
  'border-left:1px solid var(--fps-editor-divider)',
257
- 'max-width:360px',
258
- 'overflow:hidden',
259
- 'text-overflow:ellipsis',
260
- 'white-space:nowrap',
261
- 'color:var(--fps-editor-muted)',
262
- 'font-size:11px',
263
892
  ].join(';');
264
- sceneMouseHint.textContent = '左键选择 · 空白拖拽框选 · 中键平移 · Alt+左键环绕 · 右键飞行 · 滚轮缩放';
265
- const sceneHelpButton = LocalEditorShared.createButton(doc, '快捷键');
266
- sceneHelpButton.style.padding = '5px 7px';
267
- sceneQuickActions.appendChild(saveButton);
268
- sceneQuickActions.appendChild(saveAndRunButton);
269
- sceneQuickActions.appendChild(discardRunButton);
893
+ const transformActionButton = createToolbarIconButton(doc, '对齐与分布', 'arrange', '对齐与分布', 'settings');
894
+ transformActionButton.dataset.transformActionToggle = 'true';
895
+ const transformActionPopover = doc.createElement('div');
896
+ transformActionPopover.classList.add(LOCAL_EDITOR_THEME_CLASS);
897
+ transformActionPopover.dataset.transformActionPopover = 'true';
898
+ transformActionPopover.setAttribute('role', 'dialog');
899
+ transformActionPopover.setAttribute('aria-label', '对齐与分布');
900
+ transformActionPopover.style.cssText = [
901
+ 'position:fixed',
902
+ 'z-index:2147483641',
903
+ 'display:none',
904
+ 'flex-direction:column',
905
+ 'gap:9px',
906
+ 'width:236px',
907
+ 'padding:10px',
908
+ 'border:1px solid var(--fps-editor-border)',
909
+ 'border-radius:3px',
910
+ 'background:var(--fps-editor-panel)',
911
+ 'box-shadow:var(--fps-editor-shadow-popover)',
912
+ 'pointer-events:auto',
913
+ ].join(';');
914
+ const transformActionButtons = new Map();
915
+ for (const group of TRANSFORM_ACTION_GROUPS) {
916
+ const section = doc.createElement('section');
917
+ section.style.cssText = 'display:flex;flex-direction:column;gap:5px';
918
+ const heading = doc.createElement('div');
919
+ heading.textContent = group.title;
920
+ heading.style.cssText = [
921
+ 'color:var(--fps-editor-text-strong)',
922
+ 'font-size:11px',
923
+ 'font-weight:900',
924
+ ].join(';');
925
+ const hint = doc.createElement('div');
926
+ hint.textContent = group.hint;
927
+ hint.style.cssText = [
928
+ 'color:var(--fps-editor-muted)',
929
+ 'font-size:10px',
930
+ 'font-weight:700',
931
+ 'margin-top:-3px',
932
+ ].join(';');
933
+ section.appendChild(heading);
934
+ section.appendChild(hint);
935
+ for (const action of group.actions) {
936
+ const button = LocalEditorShared.createButton(doc, TRANSFORM_ACTION_LABELS[action], {
937
+ icon: 'arrange',
938
+ tooltip: TRANSFORM_ACTION_LABELS[action],
939
+ ariaLabel: TRANSFORM_ACTION_LABELS[action],
940
+ });
941
+ button.dataset.transformAction = action;
942
+ button.style.justifyContent = 'flex-start';
943
+ button.style.width = '100%';
944
+ transformActionButtons.set(action, button);
945
+ section.appendChild(button);
946
+ }
947
+ transformActionPopover.appendChild(section);
948
+ }
949
+ actionGroup.appendChild(transformActionButton);
950
+ const themeToggleButton = createToolbarIconButton(doc, '主题', 'theme', '切换主题');
951
+ themeToggleButton.dataset.editorThemeToggle = 'true';
952
+ const gridToggleButton = createToolbarIconButton(doc, '网格', 'grid', '显示 / 隐藏 Scene View 网格');
953
+ gridToggleButton.dataset.editorGridToggle = 'true';
954
+ const sceneHelpButton = createToolbarIconButton(doc, '快捷键', 'help', '快捷键与操作说明');
955
+ sceneQuickActions.appendChild(localTestGroup);
270
956
  sceneQuickActions.appendChild(undoButton);
271
957
  sceneQuickActions.appendChild(redoButton);
272
958
  sceneQuickActions.appendChild(dirtyBadge);
273
- sceneQuickActions.appendChild(sceneHelpButton);
959
+ sceneUtilityActions.appendChild(gridToggleButton);
960
+ sceneUtilityActions.appendChild(themeToggleButton);
961
+ sceneUtilityActions.appendChild(sceneHelpButton);
962
+ const cameraPreviewGroup = doc.createElement('div');
963
+ cameraPreviewGroup.style.cssText = [
964
+ 'display:flex',
965
+ 'align-items:center',
966
+ 'gap:4px',
967
+ 'padding-left:8px',
968
+ 'border-left:1px solid var(--fps-editor-divider)',
969
+ ].join(';');
970
+ const sceneCameraButton = createToolbarIconButton(doc, '从 Main Camera 查看场景', 'camera');
971
+ sceneCameraButton.dataset.sceneCameraPreviewToggle = 'true';
972
+ cameraPreviewGroup.appendChild(sceneCameraButton);
973
+ const viewportToolsGroup = doc.createElement('div');
974
+ viewportToolsGroup.style.cssText = [
975
+ 'display:flex',
976
+ 'align-items:center',
977
+ 'gap:4px',
978
+ 'padding-left:8px',
979
+ 'border-left:1px solid var(--fps-editor-divider)',
980
+ ].join(';');
981
+ const viewportPresetButtons = new Map();
982
+ for (const preset of ['perspective', 'top', 'front', 'right']) {
983
+ const button = createToolbarIconButton(doc, VIEW_PRESET_TOOLTIPS[preset], VIEW_PRESET_ICONS[preset], VIEW_PRESET_TOOLTIPS[preset]);
984
+ button.dataset.viewportViewPreset = preset;
985
+ viewportPresetButtons.set(preset, button);
986
+ viewportToolsGroup.appendChild(button);
987
+ }
988
+ const projectionToggleButton = createToolbarIconButton(doc, '投影模式', 'projection-perspective', '切换投影模式');
989
+ projectionToggleButton.dataset.viewportProjectionToggle = 'true';
990
+ viewportToolsGroup.appendChild(projectionToggleButton);
991
+ const overlaySettingsButton = createToolbarIconButton(doc, '视口信息层设置', 'view-overlay', '视口信息层设置', 'settings');
992
+ overlaySettingsButton.dataset.viewportOverlaySettingsToggle = 'true';
993
+ viewportToolsGroup.appendChild(overlaySettingsButton);
994
+ const measureButton = createToolbarIconButton(doc, '测量距离', 'measure', '测量 XZ 地面距离', 'toggle');
995
+ measureButton.dataset.viewportMeasureToggle = 'true';
996
+ viewportToolsGroup.appendChild(measureButton);
997
+ const clearMeasurementButton = createToolbarIconButton(doc, '清除测量结果', 'discard', '清除测量结果');
998
+ clearMeasurementButton.dataset.viewportMeasurementClear = 'true';
999
+ viewportToolsGroup.appendChild(clearMeasurementButton);
1000
+ const overlaySettingsPopover = doc.createElement('div');
1001
+ overlaySettingsPopover.classList.add(LOCAL_EDITOR_THEME_CLASS);
1002
+ overlaySettingsPopover.dataset.viewportOverlaySettingsPopover = 'true';
1003
+ overlaySettingsPopover.setAttribute('role', 'dialog');
1004
+ overlaySettingsPopover.setAttribute('aria-label', '视口信息层设置');
1005
+ overlaySettingsPopover.style.cssText = [
1006
+ 'position:fixed',
1007
+ 'z-index:2147483641',
1008
+ 'display:none',
1009
+ 'flex-direction:column',
1010
+ 'gap:7px',
1011
+ 'width:190px',
1012
+ 'padding:8px',
1013
+ 'border:1px solid var(--fps-editor-border)',
1014
+ 'border-radius:3px',
1015
+ 'background:var(--fps-editor-panel)',
1016
+ 'box-shadow:var(--fps-editor-shadow-popover)',
1017
+ 'pointer-events:auto',
1018
+ ].join(';');
1019
+ const overlayInputs = new Map();
1020
+ for (const key of Object.keys(VIEWPORT_OVERLAY_TOGGLE_LABELS)) {
1021
+ const row = doc.createElement('label');
1022
+ row.style.cssText = [
1023
+ 'display:flex',
1024
+ 'align-items:center',
1025
+ 'justify-content:space-between',
1026
+ 'gap:10px',
1027
+ 'color:var(--fps-editor-text)',
1028
+ 'font-size:11px',
1029
+ 'font-weight:800',
1030
+ ].join(';');
1031
+ const text = doc.createElement('span');
1032
+ text.textContent = VIEWPORT_OVERLAY_TOGGLE_LABELS[key];
1033
+ const input = doc.createElement('input');
1034
+ input.type = 'checkbox';
1035
+ input.dataset.viewportOverlaySetting = key;
1036
+ row.appendChild(text);
1037
+ row.appendChild(input);
1038
+ overlaySettingsPopover.appendChild(row);
1039
+ overlayInputs.set(key, input);
1040
+ }
1041
+ const toolbarOverflowButton = createToolbarIconButton(doc, '更多', 'chevron-down', '显示隐藏的工具栏命令');
1042
+ toolbarOverflowButton.dataset.editorToolbarOverflowToggle = 'true';
1043
+ toolbarOverflowButton.style.display = 'none';
1044
+ const toolbarOverflowMenu = doc.createElement('div');
1045
+ toolbarOverflowMenu.classList.add(LOCAL_EDITOR_THEME_CLASS);
1046
+ toolbarOverflowMenu.dataset.editorToolbarOverflowMenu = 'true';
1047
+ toolbarOverflowMenu.style.cssText = [
1048
+ 'position:fixed',
1049
+ 'z-index:2147483641',
1050
+ 'display:none',
1051
+ 'flex-direction:column',
1052
+ 'gap:6px',
1053
+ 'min-width:220px',
1054
+ 'max-width:min(420px, calc(100vw - 16px))',
1055
+ 'max-height:calc(100vh - 72px)',
1056
+ 'overflow:auto',
1057
+ 'padding:8px',
1058
+ 'border:1px solid var(--fps-editor-border)',
1059
+ 'border-radius:3px',
1060
+ 'background:var(--fps-editor-panel)',
1061
+ 'box-shadow:var(--fps-editor-shadow-popover)',
1062
+ 'pointer-events:auto',
1063
+ ].join(';');
1064
+ const coordinateAxesOverlay = createCoordinateAxesOverlay(doc);
1065
+ const spatialOverlay = createSpatialOverlay(doc);
1066
+ const measurementOverlay = createMeasurementOverlay(doc);
274
1067
  sceneToolOverlay.appendChild(sceneTitle);
275
1068
  sceneToolOverlay.appendChild(sceneQuickActions);
276
1069
  sceneToolOverlay.appendChild(toolGroup);
277
1070
  sceneToolOverlay.appendChild(spaceGroup);
278
- sceneToolOverlay.appendChild(constraintGroup);
279
- sceneToolOverlay.appendChild(sceneToolStatus);
280
- sceneToolOverlay.appendChild(sceneMouseHint);
281
- sceneToolOverlay.appendChild(status);
1071
+ sceneToolOverlay.appendChild(snapGroup);
1072
+ sceneToolOverlay.appendChild(placementGroup);
1073
+ sceneToolOverlay.appendChild(actionGroup);
1074
+ sceneToolOverlay.appendChild(viewportToolsGroup);
1075
+ sceneToolOverlay.appendChild(sceneUtilityActions);
1076
+ sceneToolOverlay.appendChild(cameraPreviewGroup);
1077
+ sceneToolOverlay.appendChild(editorStatusButton);
1078
+ sceneToolOverlay.appendChild(toolbarOverflowButton);
282
1079
  workbench.sceneHeader.appendChild(sceneToolOverlay);
1080
+ workbench.sceneFrame.appendChild(spatialOverlay.root);
1081
+ workbench.sceneFrame.appendChild(measurementOverlay.root);
1082
+ workbench.sceneFrame.appendChild(coordinateAxesOverlay.root);
1083
+ root.appendChild(localTestMenu);
1084
+ root.appendChild(toolbarOverflowMenu);
1085
+ root.appendChild(snapSettingsPopover);
1086
+ root.appendChild(placementSettingsPopover);
1087
+ root.appendChild(transformActionPopover);
1088
+ root.appendChild(overlaySettingsPopover);
1089
+ tooltipSurfaces.add(localTestMenu);
1090
+ tooltipSurfaces.add(toolbarOverflowMenu);
1091
+ tooltipSurfaces.add(snapSettingsPopover);
1092
+ tooltipSurfaces.add(placementSettingsPopover);
1093
+ tooltipSurfaces.add(transformActionPopover);
1094
+ tooltipSurfaces.add(overlaySettingsPopover);
283
1095
  const boxSelectionOverlay = doc.createElement('div');
1096
+ boxSelectionOverlay.classList.add(LOCAL_EDITOR_THEME_CLASS);
284
1097
  boxSelectionOverlay.style.cssText = [
285
1098
  'position:fixed',
286
1099
  'z-index:2147483637',
287
1100
  'display:none',
288
- 'border:1px solid rgba(88,166,255,0.95)',
289
- 'background:rgba(56,139,253,0.18)',
290
- 'box-shadow:0 0 0 1px rgba(10,15,23,0.55) inset',
1101
+ 'border:1px solid var(--fps-editor-drop-target)',
1102
+ 'background:var(--fps-editor-box-select-bg)',
1103
+ 'box-shadow:var(--fps-editor-box-select-shadow)',
291
1104
  'pointer-events:none',
292
1105
  ].join(';');
293
1106
  root.appendChild(boxSelectionOverlay);
1107
+ tooltipSurfaces.add(boxSelectionOverlay);
294
1108
  const shortcutHelpPanel = createLocalEditorShortcutHelpPanel(doc);
295
1109
  shortcutHelpPanel.classList.add(LOCAL_EDITOR_THEME_CLASS);
296
1110
  root.appendChild(shortcutHelpPanel);
1111
+ tooltipSurfaces.add(shortcutHelpPanel);
1112
+ function applyThemeToSurfaces() {
1113
+ applyLocalEditorTheme(hostChrome, activeTheme);
1114
+ applyLocalEditorTheme(workbench.root, activeTheme);
1115
+ applyLocalEditorTheme(spatialOverlay.root, activeTheme);
1116
+ applyLocalEditorTheme(measurementOverlay.root, activeTheme);
1117
+ applyLocalEditorTheme(shortcutHelpPanel, activeTheme);
1118
+ applyLocalEditorTheme(boxSelectionOverlay, activeTheme);
1119
+ applyLocalEditorTheme(localTestMenu, activeTheme);
1120
+ applyLocalEditorTheme(toolbarOverflowMenu, activeTheme);
1121
+ applyLocalEditorTheme(snapSettingsPopover, activeTheme);
1122
+ applyLocalEditorTheme(placementSettingsPopover, activeTheme);
1123
+ applyLocalEditorTheme(transformActionPopover, activeTheme);
1124
+ applyLocalEditorTheme(overlaySettingsPopover, activeTheme);
1125
+ tooltipController.setTheme(activeTheme);
1126
+ contextMenu.setTheme?.(activeTheme);
1127
+ }
1128
+ function updateThemeToggleButton() {
1129
+ const nextTheme = activeTheme === 'dark' ? 'light' : 'dark';
1130
+ themeToggleButton.dataset.editorTheme = activeTheme;
1131
+ themeToggleButton.setAttribute('aria-pressed', activeTheme === 'light' ? 'true' : 'false');
1132
+ setToolbarButtonTooltip(themeToggleButton, nextTheme === 'light' ? '切换为浅色主题' : '切换为深色主题');
1133
+ }
1134
+ function setActiveTheme(theme) {
1135
+ activeTheme = normalizeLocalEditorThemeName(theme);
1136
+ applyThemeToSurfaces();
1137
+ updateThemeToggleButton();
1138
+ }
1139
+ let localTestMenuOpen = false;
1140
+ let snapSettingsOpen = false;
1141
+ let placementSettingsOpen = false;
1142
+ let transformActionOpen = false;
1143
+ let overlaySettingsOpen = false;
1144
+ function closeLocalTestMenu() {
1145
+ localTestMenuOpen = false;
1146
+ localTestMenu.style.display = 'none';
1147
+ LocalEditorShared.applyButtonActiveState(localTestButton, false);
1148
+ }
1149
+ function closeSnapSettingsPopover() {
1150
+ snapSettingsOpen = false;
1151
+ snapSettingsPopover.style.display = 'none';
1152
+ snapButton.setAttribute('aria-expanded', 'false');
1153
+ const enabled = currentState?.transformOperations?.settings.snap.enabled
1154
+ ?? DEFAULT_EDITOR_TRANSFORM_OPERATION_SETTINGS.snap.enabled;
1155
+ LocalEditorShared.applyButtonActiveState(snapButton, enabled);
1156
+ }
1157
+ function closePlacementSettingsPopover() {
1158
+ placementSettingsOpen = false;
1159
+ placementSettingsPopover.style.display = 'none';
1160
+ placementButton.setAttribute('aria-expanded', 'false');
1161
+ const mode = currentState?.transformOperations?.settings.placementMode
1162
+ ?? DEFAULT_EDITOR_TRANSFORM_OPERATION_SETTINGS.placementMode;
1163
+ LocalEditorShared.applyButtonActiveState(placementButton, mode !== 'off');
1164
+ }
1165
+ function closeTransformActionPopover() {
1166
+ transformActionOpen = false;
1167
+ transformActionPopover.style.display = 'none';
1168
+ transformActionButton.setAttribute('aria-expanded', 'false');
1169
+ LocalEditorShared.applyButtonActiveState(transformActionButton, false);
1170
+ }
1171
+ function closeOverlaySettingsPopover() {
1172
+ overlaySettingsOpen = false;
1173
+ overlaySettingsPopover.style.display = 'none';
1174
+ overlaySettingsButton.setAttribute('aria-expanded', 'false');
1175
+ LocalEditorShared.applyButtonActiveState(overlaySettingsButton, false);
1176
+ }
1177
+ function openLocalTestMenu() {
1178
+ if (!localTestActionsEnabled || localTestButton.style.display === 'none')
1179
+ return;
1180
+ closeToolbarOverflowMenu();
1181
+ closeSnapSettingsPopover();
1182
+ closePlacementSettingsPopover();
1183
+ closeTransformActionPopover();
1184
+ closeOverlaySettingsPopover();
1185
+ closeOverlaySettingsPopover();
1186
+ const win = doc.defaultView;
1187
+ const rect = localTestButton.getBoundingClientRect();
1188
+ const viewportWidth = win?.innerWidth ?? doc.documentElement.clientWidth;
1189
+ localTestMenu.style.top = `${Math.max(8, rect.bottom + 5)}px`;
1190
+ localTestMenu.style.right = `${Math.max(8, viewportWidth - rect.right)}px`;
1191
+ localTestMenu.style.display = 'flex';
1192
+ localTestMenuOpen = true;
1193
+ LocalEditorShared.applyButtonActiveState(localTestButton, true);
1194
+ }
1195
+ function openSnapSettingsPopover() {
1196
+ if (snapButton.disabled || snapGroup.style.display === 'none')
1197
+ return;
1198
+ closeToolbarOverflowMenu();
1199
+ closeLocalTestMenu();
1200
+ closePlacementSettingsPopover();
1201
+ closeTransformActionPopover();
1202
+ closeOverlaySettingsPopover();
1203
+ const win = doc.defaultView;
1204
+ const rect = snapButton.getBoundingClientRect();
1205
+ const viewportWidth = win?.innerWidth ?? doc.documentElement.clientWidth;
1206
+ snapSettingsPopover.style.display = 'flex';
1207
+ const popoverWidth = snapSettingsPopover.offsetWidth || 220;
1208
+ snapSettingsPopover.style.top = `${Math.max(8, rect.bottom + 5)}px`;
1209
+ snapSettingsPopover.style.left = `${Math.max(8, Math.min(rect.left, viewportWidth - popoverWidth - 8))}px`;
1210
+ snapSettingsOpen = true;
1211
+ snapButton.setAttribute('aria-expanded', 'true');
1212
+ LocalEditorShared.applyButtonActiveState(snapButton, true);
1213
+ }
1214
+ function toggleSnapSettingsPopover() {
1215
+ if (snapSettingsOpen)
1216
+ closeSnapSettingsPopover();
1217
+ else
1218
+ openSnapSettingsPopover();
1219
+ }
1220
+ function openPlacementSettingsPopover() {
1221
+ if (placementButton.disabled || placementGroup.style.display === 'none')
1222
+ return;
1223
+ closeToolbarOverflowMenu();
1224
+ closeLocalTestMenu();
1225
+ closeSnapSettingsPopover();
1226
+ closeTransformActionPopover();
1227
+ closeOverlaySettingsPopover();
1228
+ const win = doc.defaultView;
1229
+ const rect = placementButton.getBoundingClientRect();
1230
+ const viewportWidth = win?.innerWidth ?? doc.documentElement.clientWidth;
1231
+ placementSettingsPopover.style.display = 'flex';
1232
+ const popoverWidth = placementSettingsPopover.offsetWidth || 180;
1233
+ placementSettingsPopover.style.top = `${Math.max(8, rect.bottom + 5)}px`;
1234
+ placementSettingsPopover.style.left = `${Math.max(8, Math.min(rect.left, viewportWidth - popoverWidth - 8))}px`;
1235
+ placementSettingsOpen = true;
1236
+ placementButton.setAttribute('aria-expanded', 'true');
1237
+ LocalEditorShared.applyButtonActiveState(placementButton, true);
1238
+ }
1239
+ function togglePlacementSettingsPopover() {
1240
+ if (placementSettingsOpen)
1241
+ closePlacementSettingsPopover();
1242
+ else
1243
+ openPlacementSettingsPopover();
1244
+ }
1245
+ function openTransformActionPopover() {
1246
+ if (transformActionButton.disabled || actionGroup.style.display === 'none')
1247
+ return;
1248
+ closeToolbarOverflowMenu();
1249
+ closeLocalTestMenu();
1250
+ closeSnapSettingsPopover();
1251
+ closePlacementSettingsPopover();
1252
+ closeOverlaySettingsPopover();
1253
+ const win = doc.defaultView;
1254
+ const rect = transformActionButton.getBoundingClientRect();
1255
+ const viewportWidth = win?.innerWidth ?? doc.documentElement.clientWidth;
1256
+ transformActionPopover.style.display = 'flex';
1257
+ const popoverWidth = transformActionPopover.offsetWidth || 236;
1258
+ transformActionPopover.style.top = `${Math.max(8, rect.bottom + 5)}px`;
1259
+ transformActionPopover.style.left = `${Math.max(8, Math.min(rect.left, viewportWidth - popoverWidth - 8))}px`;
1260
+ transformActionOpen = true;
1261
+ transformActionButton.setAttribute('aria-expanded', 'true');
1262
+ LocalEditorShared.applyButtonActiveState(transformActionButton, true);
1263
+ }
1264
+ function toggleTransformActionPopover() {
1265
+ if (transformActionOpen)
1266
+ closeTransformActionPopover();
1267
+ else
1268
+ openTransformActionPopover();
1269
+ }
1270
+ function openOverlaySettingsPopover() {
1271
+ if (overlaySettingsButton.disabled || viewportToolsGroup.style.display === 'none')
1272
+ return;
1273
+ const win = doc.defaultView;
1274
+ const rect = overlaySettingsButton.getBoundingClientRect();
1275
+ const viewportWidth = win?.innerWidth ?? doc.documentElement.clientWidth;
1276
+ closeLocalTestMenu();
1277
+ closeSnapSettingsPopover();
1278
+ closePlacementSettingsPopover();
1279
+ closeTransformActionPopover();
1280
+ closeToolbarOverflowMenu();
1281
+ overlaySettingsPopover.style.display = 'flex';
1282
+ const popoverWidth = overlaySettingsPopover.offsetWidth || 190;
1283
+ overlaySettingsPopover.style.top = `${Math.max(8, rect.bottom + 5)}px`;
1284
+ overlaySettingsPopover.style.left = `${Math.max(8, Math.min(rect.left, viewportWidth - popoverWidth - 8))}px`;
1285
+ overlaySettingsOpen = true;
1286
+ overlaySettingsButton.setAttribute('aria-expanded', 'true');
1287
+ LocalEditorShared.applyButtonActiveState(overlaySettingsButton, true);
1288
+ }
1289
+ function toggleOverlaySettingsPopover() {
1290
+ if (overlaySettingsOpen)
1291
+ closeOverlaySettingsPopover();
1292
+ else
1293
+ openOverlaySettingsPopover();
1294
+ }
1295
+ const onLocalTestButtonClick = (event) => {
1296
+ event.stopPropagation();
1297
+ if (localTestMenuOpen)
1298
+ closeLocalTestMenu();
1299
+ else
1300
+ openLocalTestMenu();
1301
+ };
1302
+ const toolbarOverflowStyleProperties = [
1303
+ 'alignSelf',
1304
+ 'borderLeft',
1305
+ 'display',
1306
+ 'flex',
1307
+ 'height',
1308
+ 'maxWidth',
1309
+ 'minHeight',
1310
+ 'minWidth',
1311
+ 'overflow',
1312
+ 'paddingLeft',
1313
+ 'textOverflow',
1314
+ 'whiteSpace',
1315
+ 'width',
1316
+ ];
1317
+ const toolbarButtonOriginalPadding = new WeakMap();
1318
+ const toolbarOverflowOriginalStyles = new WeakMap();
1319
+ const toolbarOrder = [
1320
+ sceneTitle,
1321
+ sceneQuickActions,
1322
+ toolGroup,
1323
+ spaceGroup,
1324
+ snapGroup,
1325
+ placementGroup,
1326
+ actionGroup,
1327
+ viewportToolsGroup,
1328
+ sceneUtilityActions,
1329
+ cameraPreviewGroup,
1330
+ editorStatusButton,
1331
+ toolbarOverflowButton,
1332
+ ];
1333
+ const toolbarOverflowItems = [
1334
+ { id: 'editor-status', element: editorStatusButton, kind: 'group' },
1335
+ { id: 'scene-utilities', element: sceneUtilityActions, kind: 'group' },
1336
+ { id: 'camera-preview', element: cameraPreviewGroup, kind: 'group' },
1337
+ { id: 'viewport-tools', element: viewportToolsGroup, kind: 'group' },
1338
+ { id: 'transform-actions', element: actionGroup, kind: 'group' },
1339
+ { id: 'placement', element: placementGroup, kind: 'group' },
1340
+ { id: 'snap', element: snapGroup, kind: 'group' },
1341
+ { id: 'space', element: spaceGroup, kind: 'group' },
1342
+ ];
1343
+ let toolbarOverflowOpen = false;
1344
+ let toolbarOverflowRaf = null;
1345
+ function restoreToolbarOverflowItemStyle(item) {
1346
+ const original = toolbarOverflowOriginalStyles.get(item.element);
1347
+ if (!original)
1348
+ return;
1349
+ for (const property of toolbarOverflowStyleProperties) {
1350
+ item.element.style[property] = original[property] ?? '';
1351
+ }
1352
+ toolbarOverflowOriginalStyles.delete(item.element);
1353
+ item.element.dataset.editorToolbarOverflowPlacement = 'toolbar';
1354
+ }
1355
+ function applyToolbarOverflowMenuStyle(item) {
1356
+ if (!toolbarOverflowOriginalStyles.has(item.element)) {
1357
+ const original = {};
1358
+ for (const property of toolbarOverflowStyleProperties) {
1359
+ original[property] = item.element.style[property];
1360
+ }
1361
+ toolbarOverflowOriginalStyles.set(item.element, original);
1362
+ }
1363
+ item.element.dataset.editorToolbarOverflowPlacement = 'menu';
1364
+ item.element.style.alignSelf = 'stretch';
1365
+ item.element.style.borderLeft = '0';
1366
+ item.element.style.display = item.kind === 'group' ? 'flex' : 'block';
1367
+ item.element.style.flex = '0 0 auto';
1368
+ item.element.style.height = '';
1369
+ item.element.style.maxWidth = 'none';
1370
+ item.element.style.minHeight = '0';
1371
+ item.element.style.minWidth = '0';
1372
+ item.element.style.overflow = item.kind === 'group' ? 'visible' : 'hidden';
1373
+ item.element.style.paddingLeft = '0';
1374
+ item.element.style.textOverflow = item.kind === 'text' ? 'ellipsis' : '';
1375
+ item.element.style.whiteSpace = item.kind === 'text' ? 'nowrap' : '';
1376
+ item.element.style.width = '100%';
1377
+ }
1378
+ function setToolbarButtonCompact(button, compact) {
1379
+ if (button.dataset.editorButtonLabelMode === 'icon-only')
1380
+ return;
1381
+ const label = button.querySelector('[data-editor-button-label]');
1382
+ if (!label)
1383
+ return;
1384
+ if (!toolbarButtonOriginalPadding.has(button)) {
1385
+ toolbarButtonOriginalPadding.set(button, button.style.padding);
1386
+ }
1387
+ const accessibleLabel = button.dataset.editorToolbarLabel || label.textContent || button.title;
1388
+ button.dataset.editorToolbarLabel = accessibleLabel;
1389
+ button.setAttribute('aria-label', accessibleLabel);
1390
+ if (!button.title)
1391
+ button.title = accessibleLabel;
1392
+ button.style.padding = compact ? '4px 6px' : toolbarButtonOriginalPadding.get(button) ?? '';
1393
+ label.style.display = compact ? 'inline-block' : '';
1394
+ label.style.width = compact ? '0' : '';
1395
+ label.style.maxWidth = compact ? '0' : '';
1396
+ label.style.opacity = compact ? '0' : '';
1397
+ label.style.marginLeft = compact ? '-5px' : '';
1398
+ }
1399
+ function setToolbarCompact(compact) {
1400
+ for (const button of Array.from(sceneToolOverlay.querySelectorAll('button'))) {
1401
+ setToolbarButtonCompact(button, compact);
1402
+ }
1403
+ }
1404
+ function closeToolbarOverflowMenu() {
1405
+ toolbarOverflowOpen = false;
1406
+ toolbarOverflowMenu.style.display = 'none';
1407
+ LocalEditorShared.applyButtonActiveState(toolbarOverflowButton, false);
1408
+ }
1409
+ function openToolbarOverflowMenu() {
1410
+ if (toolbarOverflowMenu.childElementCount === 0)
1411
+ return;
1412
+ closeLocalTestMenu();
1413
+ closeSnapSettingsPopover();
1414
+ closePlacementSettingsPopover();
1415
+ closeTransformActionPopover();
1416
+ closeOverlaySettingsPopover();
1417
+ const win = doc.defaultView;
1418
+ const rect = toolbarOverflowButton.getBoundingClientRect();
1419
+ const viewportWidth = win?.innerWidth ?? doc.documentElement.clientWidth;
1420
+ toolbarOverflowMenu.style.top = `${Math.max(8, rect.bottom + 5)}px`;
1421
+ toolbarOverflowMenu.style.right = `${Math.max(8, viewportWidth - rect.right)}px`;
1422
+ toolbarOverflowMenu.style.display = 'flex';
1423
+ toolbarOverflowOpen = true;
1424
+ LocalEditorShared.applyButtonActiveState(toolbarOverflowButton, true);
1425
+ }
1426
+ function resetToolbarOverflowLayout() {
1427
+ setToolbarCompact(false);
1428
+ for (const item of toolbarOverflowItems)
1429
+ restoreToolbarOverflowItemStyle(item);
1430
+ for (const item of toolbarOrder) {
1431
+ item.dataset.editorToolbarOverflowed = 'false';
1432
+ sceneToolOverlay.appendChild(item);
1433
+ }
1434
+ LocalEditorShared.clearElement(toolbarOverflowMenu);
1435
+ toolbarOverflowButton.style.display = 'none';
1436
+ sceneToolOverlay.style.overflowX = 'hidden';
1437
+ closeToolbarOverflowMenu();
1438
+ }
1439
+ function layoutToolbarOverflow() {
1440
+ toolbarOverflowRaf = null;
1441
+ resetToolbarOverflowLayout();
1442
+ if (sceneToolOverlay.style.display === 'none')
1443
+ return;
1444
+ const availableWidth = sceneToolOverlay.clientWidth;
1445
+ if (availableWidth <= 0)
1446
+ return;
1447
+ const isOverflowing = () => sceneToolOverlay.scrollWidth > availableWidth + 1;
1448
+ if (!isOverflowing())
1449
+ return;
1450
+ toolbarOverflowButton.style.display = 'inline-flex';
1451
+ for (const item of toolbarOverflowItems) {
1452
+ if (!isOverflowing())
1453
+ break;
1454
+ if (item.element.style.display === 'none')
1455
+ continue;
1456
+ item.element.dataset.editorToolbarOverflowed = 'true';
1457
+ applyToolbarOverflowMenuStyle(item);
1458
+ toolbarOverflowMenu.appendChild(item.element);
1459
+ }
1460
+ if (toolbarOverflowMenu.childElementCount === 0) {
1461
+ toolbarOverflowButton.style.display = 'none';
1462
+ }
1463
+ if (isOverflowing()) {
1464
+ setToolbarCompact(true);
1465
+ }
1466
+ sceneToolOverlay.style.overflowX = isOverflowing() ? 'auto' : 'hidden';
1467
+ }
1468
+ function scheduleToolbarOverflowLayout() {
1469
+ const win = doc.defaultView;
1470
+ if (!win) {
1471
+ layoutToolbarOverflow();
1472
+ return;
1473
+ }
1474
+ if (toolbarOverflowRaf != null)
1475
+ win.cancelAnimationFrame(toolbarOverflowRaf);
1476
+ toolbarOverflowRaf = win.requestAnimationFrame(layoutToolbarOverflow);
1477
+ }
1478
+ const onToolbarOverflowButtonClick = (event) => {
1479
+ event.stopPropagation();
1480
+ if (toolbarOverflowOpen)
1481
+ closeToolbarOverflowMenu();
1482
+ else
1483
+ openToolbarOverflowMenu();
1484
+ };
1485
+ const onToolbarOverflowPointerDown = (event) => {
1486
+ const target = event.target instanceof HTMLElement ? event.target : null;
1487
+ if (!target)
1488
+ return;
1489
+ if (target.closest('[data-editor-local-test-menu]') || target.closest('[data-editor-local-test-toggle]'))
1490
+ return;
1491
+ if (target.closest('[data-editor-toolbar-overflow-menu]') || target.closest('[data-editor-toolbar-overflow-toggle]'))
1492
+ return;
1493
+ if (target.closest('[data-transform-snap-settings-popover]') || target.closest('[data-transform-snap-toggle]'))
1494
+ return;
1495
+ if (target.closest('[data-placement-settings-popover]') || target.closest('[data-placement-mode-toggle]'))
1496
+ return;
1497
+ if (target.closest('[data-transform-action-popover]') || target.closest('[data-transform-action-toggle]'))
1498
+ return;
1499
+ if (target.closest('[data-viewport-overlay-settings-popover]') || target.closest('[data-viewport-overlay-settings-toggle]'))
1500
+ return;
1501
+ closeLocalTestMenu();
1502
+ closeToolbarOverflowMenu();
1503
+ closeSnapSettingsPopover();
1504
+ closePlacementSettingsPopover();
1505
+ closeTransformActionPopover();
1506
+ closeOverlaySettingsPopover();
1507
+ };
1508
+ const ResizeObserverCtor = doc.defaultView?.ResizeObserver;
1509
+ const toolbarResizeObserver = ResizeObserverCtor
1510
+ ? new ResizeObserverCtor(() => scheduleToolbarOverflowLayout())
1511
+ : null;
1512
+ toolbarOverflowButton.addEventListener('click', onToolbarOverflowButtonClick);
1513
+ localTestButton.addEventListener('click', onLocalTestButtonClick);
1514
+ doc.addEventListener('pointerdown', onToolbarOverflowPointerDown);
1515
+ toolbarResizeObserver?.observe(sceneToolOverlay);
1516
+ doc.defaultView?.addEventListener('resize', scheduleToolbarOverflowLayout);
1517
+ applyThemeToSurfaces();
1518
+ updateThemeToggleButton();
297
1519
  enterEditorButton.addEventListener('click', () => callbacks.onEnterEditor?.());
298
- saveButton.addEventListener('click', () => callbacks.onSaveScene?.());
299
- saveAndRunButton.addEventListener('click', () => callbacks.onSaveAndRunGame?.());
300
- discardRunButton.addEventListener('click', () => callbacks.onDiscardAndRunGame?.());
1520
+ saveButton.addEventListener('click', () => {
1521
+ closeLocalTestMenu();
1522
+ if (localTestActionsEnabled)
1523
+ callbacks.onSaveScene?.();
1524
+ });
1525
+ saveAndRunButton.addEventListener('click', () => {
1526
+ closeLocalTestMenu();
1527
+ if (localTestActionsEnabled)
1528
+ callbacks.onSaveAndRunGame?.();
1529
+ });
1530
+ discardRunButton.addEventListener('click', () => {
1531
+ closeLocalTestMenu();
1532
+ if (localTestActionsEnabled)
1533
+ callbacks.onDiscardAndRunGame?.();
1534
+ });
301
1535
  undoButton.addEventListener('click', () => callbacks.onUndo?.());
302
1536
  redoButton.addEventListener('click', () => callbacks.onRedo?.());
1537
+ gridToggleButton.addEventListener('click', () => {
1538
+ const visible = currentState?.grid?.visible ?? true;
1539
+ callbacks.onGridVisibleChange?.(!visible);
1540
+ });
1541
+ sceneCameraButton.addEventListener('click', () => {
1542
+ const enabled = currentState?.sceneCameraPreview?.enabled ?? false;
1543
+ callbacks.onSceneCameraPreviewToggle?.(!enabled);
1544
+ });
1545
+ viewportToolsGroup.addEventListener('click', (event) => {
1546
+ const target = event.target instanceof HTMLElement ? event.target : null;
1547
+ const preset = target?.closest('[data-viewport-view-preset]')?.dataset.viewportViewPreset;
1548
+ if (preset) {
1549
+ callbacks.onViewportViewPresetChange?.(preset);
1550
+ return;
1551
+ }
1552
+ const projectionToggle = target?.closest('[data-viewport-projection-toggle]');
1553
+ if (projectionToggle) {
1554
+ const currentMode = currentState?.viewportTools?.projectionMode ?? 'perspective';
1555
+ const nextMode = currentMode === 'orthographic' ? 'perspective' : 'orthographic';
1556
+ callbacks.onViewportProjectionModeChange?.(nextMode);
1557
+ return;
1558
+ }
1559
+ const overlayToggle = target?.closest('[data-viewport-overlay-settings-toggle]');
1560
+ if (overlayToggle) {
1561
+ event.stopPropagation();
1562
+ toggleOverlaySettingsPopover();
1563
+ return;
1564
+ }
1565
+ const measureToggle = target?.closest('[data-viewport-measure-toggle]');
1566
+ if (measureToggle) {
1567
+ const currentTool = currentState?.viewportTools?.activeUtilityTool ?? 'none';
1568
+ const nextTool = currentTool === 'measure-distance' ? 'none' : 'measure-distance';
1569
+ callbacks.onViewportUtilityToolChange?.(nextTool);
1570
+ return;
1571
+ }
1572
+ const clearMeasurement = target?.closest('[data-viewport-measurement-clear]');
1573
+ if (clearMeasurement) {
1574
+ callbacks.onViewportMeasurementClear?.();
1575
+ }
1576
+ });
303
1577
  const setShortcutHelpOpen = (open) => {
304
1578
  if (open)
305
1579
  contextMenu.close();
@@ -311,6 +1585,9 @@ export function createLocalEditorBrowserUi(options = {}) {
311
1585
  sceneHelpButton.addEventListener('click', () => {
312
1586
  setShortcutHelpOpen(!helpOpen);
313
1587
  });
1588
+ themeToggleButton.addEventListener('click', () => {
1589
+ setActiveTheme(activeTheme === 'dark' ? 'light' : 'dark');
1590
+ });
314
1591
  shortcutHelpPanel.addEventListener('click', (event) => {
315
1592
  const target = event.target instanceof HTMLElement ? event.target : null;
316
1593
  if (!target?.closest('[data-editor-shortcut-help-close]'))
@@ -329,13 +1606,72 @@ export function createLocalEditorBrowserUi(options = {}) {
329
1606
  if (space)
330
1607
  callbacks.onTransformSpaceChange?.(space);
331
1608
  });
332
- constraintGroup.addEventListener('click', (event) => {
1609
+ snapButton.addEventListener('click', (event) => {
1610
+ event.stopPropagation();
1611
+ toggleSnapSettingsPopover();
1612
+ });
1613
+ snapSettingsPopover.addEventListener('change', (event) => {
1614
+ const enabledInput = event.target instanceof HTMLInputElement
1615
+ ? event.target.closest('[data-transform-snap-enabled]')
1616
+ : null;
1617
+ if (enabledInput) {
1618
+ callbacks.onTransformSnapEnabledChange?.(enabledInput.checked);
1619
+ return;
1620
+ }
1621
+ const input = event.target instanceof HTMLInputElement ? event.target : null;
1622
+ const kind = input?.dataset.transformSnapStep;
1623
+ if (!input || !kind)
1624
+ return;
1625
+ const value = Number(input.value);
1626
+ if (!Number.isFinite(value) || value <= 0) {
1627
+ const settings = currentState?.transformOperations?.settings ?? DEFAULT_EDITOR_TRANSFORM_OPERATION_SETTINGS;
1628
+ input.value = formatSnapStepInputValue(settings.snap, kind);
1629
+ return;
1630
+ }
1631
+ callbacks.onTransformSnapStepChange?.({ kind, value });
1632
+ });
1633
+ placementGroup.addEventListener('click', (event) => {
1634
+ event.stopPropagation();
1635
+ togglePlacementSettingsPopover();
1636
+ });
1637
+ placementSettingsPopover.addEventListener('click', (event) => {
1638
+ const target = event.target instanceof HTMLElement ? event.target : null;
1639
+ const mode = target?.closest('[data-placement-mode]')?.dataset.placementMode;
1640
+ if (!mode)
1641
+ return;
1642
+ callbacks.onPlacementModeChange?.(mode);
1643
+ closePlacementSettingsPopover();
1644
+ });
1645
+ actionGroup.addEventListener('click', (event) => {
1646
+ event.stopPropagation();
1647
+ toggleTransformActionPopover();
1648
+ });
1649
+ transformActionPopover.addEventListener('click', (event) => {
333
1650
  const target = event.target instanceof HTMLElement ? event.target : null;
334
- const constraint = target?.closest('[data-transform-constraint]')?.dataset.transformConstraint;
335
- if (!constraint)
1651
+ const action = target?.closest('[data-transform-action]')?.dataset.transformAction;
1652
+ if (!action)
1653
+ return;
1654
+ const transformOperations = currentState?.transformOperations;
1655
+ if (!transformOperations || !isTransformActionEnabled(action, transformOperations))
1656
+ return;
1657
+ callbacks.onTransformAction?.(action);
1658
+ closeTransformActionPopover();
1659
+ });
1660
+ overlaySettingsPopover.addEventListener('change', (event) => {
1661
+ const input = event.target instanceof HTMLInputElement
1662
+ ? event.target.closest('[data-viewport-overlay-setting]')
1663
+ : null;
1664
+ const key = input?.dataset.viewportOverlaySetting;
1665
+ if (!input || !key)
336
1666
  return;
337
- const currentConstraint = currentState?.transformTool?.activeConstraint ?? 'axis';
338
- callbacks.onTransformConstraintChange?.(currentConstraint === constraint ? 'axis' : constraint);
1667
+ const currentOverlay = currentState?.viewportTools?.overlay;
1668
+ callbacks.onViewportOverlaySettingsChange?.({
1669
+ bounds: currentOverlay?.bounds ?? true,
1670
+ dimensions: currentOverlay?.dimensions ?? true,
1671
+ edgeLengths: currentOverlay?.edgeLengths ?? false,
1672
+ anchor: currentOverlay?.anchor ?? true,
1673
+ [key]: input.checked,
1674
+ });
339
1675
  });
340
1676
  assetPanel.addEventListener('click', (event) => {
341
1677
  const target = event.target instanceof HTMLElement ? event.target : null;
@@ -360,6 +1696,7 @@ export function createLocalEditorBrowserUi(options = {}) {
360
1696
  inputRouter,
361
1697
  contextMenu,
362
1698
  getState: () => currentState,
1699
+ contextActions: options.hierarchy?.contextActions,
363
1700
  requestRender: () => {
364
1701
  if (currentState)
365
1702
  render(currentState);
@@ -423,10 +1760,30 @@ export function createLocalEditorBrowserUi(options = {}) {
423
1760
  if (currentState?.mode !== 'editor' || currentState.busy)
424
1761
  return;
425
1762
  const key = event.key.toLowerCase();
1763
+ if (key === 'escape' && snapSettingsOpen) {
1764
+ event.preventDefault();
1765
+ closeSnapSettingsPopover();
1766
+ return;
1767
+ }
1768
+ if (key === 'escape' && placementSettingsOpen) {
1769
+ event.preventDefault();
1770
+ closePlacementSettingsPopover();
1771
+ return;
1772
+ }
1773
+ if (key === 'escape' && transformActionOpen) {
1774
+ event.preventDefault();
1775
+ closeTransformActionPopover();
1776
+ return;
1777
+ }
1778
+ if (key === 'escape' && overlaySettingsOpen) {
1779
+ event.preventDefault();
1780
+ closeOverlaySettingsPopover();
1781
+ return;
1782
+ }
426
1783
  const primaryModifier = event.metaKey || event.ctrlKey;
427
1784
  const handleDocumentShortcut = inputRouter.shouldHandleDocumentShortcut(event);
428
1785
  const handleGlobalShortcut = inputRouter.shouldHandleGlobalShortcut(event);
429
- if (handleDocumentShortcut && primaryModifier && key === 's') {
1786
+ if (handleDocumentShortcut && localTestActionsEnabled && primaryModifier && key === 's') {
430
1787
  event.preventDefault();
431
1788
  callbacks.onSaveScene?.();
432
1789
  return;
@@ -444,8 +1801,7 @@ export function createLocalEditorBrowserUi(options = {}) {
444
1801
  callbacks.onRedo?.();
445
1802
  return;
446
1803
  }
447
- if (handleGlobalShortcut && (key === 'delete' || key === 'backspace')) {
448
- hierarchyController.handleDeleteShortcut(event);
1804
+ if (handleGlobalShortcut && hierarchyController.handleEditShortcut(event)) {
449
1805
  return;
450
1806
  }
451
1807
  if (!handleGlobalShortcut || event.metaKey || event.ctrlKey || event.altKey)
@@ -469,7 +1825,10 @@ export function createLocalEditorBrowserUi(options = {}) {
469
1825
  }
470
1826
  if (key === 'escape') {
471
1827
  event.preventDefault();
472
- callbacks.onCancelActiveOperation?.();
1828
+ if (callbacks.onCancelEditorIntent)
1829
+ callbacks.onCancelEditorIntent();
1830
+ else
1831
+ callbacks.onCancelActiveOperation?.();
473
1832
  }
474
1833
  };
475
1834
  doc.addEventListener('keydown', onKeyDown);
@@ -522,6 +1881,19 @@ export function createLocalEditorBrowserUi(options = {}) {
522
1881
  ? CSS.escape(value)
523
1882
  : value.replace(/["\\]/g, '\\$&');
524
1883
  }
1884
+ function formatSnapStepInputValue(snap, kind) {
1885
+ const value = kind === 'move'
1886
+ ? snap.moveStep
1887
+ : kind === 'rotate'
1888
+ ? snap.rotateStepDegrees
1889
+ : snap.scaleStep;
1890
+ return Number.isFinite(value) ? String(value) : '';
1891
+ }
1892
+ function isTransformActionEnabled(action, operationState) {
1893
+ return action.startsWith('align-')
1894
+ ? operationState.canAlign
1895
+ : operationState.canDistribute;
1896
+ }
525
1897
  const render = (state) => {
526
1898
  currentState = state;
527
1899
  const focusSnapshot = captureEditableFocus(doc);
@@ -529,19 +1901,80 @@ export function createLocalEditorBrowserUi(options = {}) {
529
1901
  const disabled = state.busy;
530
1902
  if (!inEditor || disabled)
531
1903
  contextMenu.close();
532
- hostChrome.style.display = inEditor ? 'none' : 'flex';
1904
+ hostChrome.style.display = !inEditor && localTestActionsEnabled ? 'flex' : 'none';
533
1905
  enterEditorButton.disabled = disabled;
534
- for (const button of [saveButton, saveAndRunButton, discardRunButton, undoButton, redoButton, sceneHelpButton]) {
535
- button.style.display = '';
1906
+ for (const button of [saveButton, saveAndRunButton, discardRunButton, undoButton, redoButton, sceneHelpButton, sceneCameraButton, gridToggleButton]) {
1907
+ button.style.display = 'inline-flex';
536
1908
  button.disabled = disabled;
537
1909
  }
1910
+ localTestGroup.style.display = inEditor && localTestActionsEnabled ? 'flex' : 'none';
1911
+ localTestButton.disabled = disabled;
1912
+ if (!inEditor || !localTestActionsEnabled)
1913
+ closeLocalTestMenu();
538
1914
  if (!inEditor)
539
1915
  helpOpen = false;
540
1916
  inputRouter.setModalOpen(inEditor && helpOpen);
541
1917
  const transformTool = state.transformTool ?? null;
542
1918
  workbench.root.style.display = inEditor ? '' : 'none';
543
1919
  sceneToolOverlay.style.display = inEditor ? 'flex' : 'none';
544
- for (const [tool, button] of Object.entries(toolButtons)) {
1920
+ if (!inEditor)
1921
+ closeSnapSettingsPopover();
1922
+ if (!inEditor)
1923
+ closePlacementSettingsPopover();
1924
+ if (!inEditor)
1925
+ closeTransformActionPopover();
1926
+ if (!inEditor)
1927
+ closeOverlaySettingsPopover();
1928
+ const sceneCameraPreview = state.sceneCameraPreview ?? { enabled: false, available: false };
1929
+ cameraPreviewGroup.style.display = inEditor ? 'flex' : 'none';
1930
+ sceneCameraButton.disabled = disabled || !sceneCameraPreview.available;
1931
+ const sceneCameraTooltip = sceneCameraPreview.available
1932
+ ? '从 Main Camera 查看当前场景'
1933
+ : '当前场景没有可预览的 Main Camera';
1934
+ setToolbarButtonTooltip(sceneCameraButton, sceneCameraTooltip);
1935
+ LocalEditorShared.applyButtonActiveState(sceneCameraButton, sceneCameraPreview.enabled);
1936
+ const viewportTools = state.viewportTools ?? null;
1937
+ const viewportControlsDisabled = disabled || sceneCameraPreview.enabled;
1938
+ const viewportControlsTooltip = sceneCameraPreview.enabled
1939
+ ? '请先关闭 Scene Camera 预览,再使用视口工具'
1940
+ : '';
1941
+ viewportToolsGroup.style.display = inEditor ? 'flex' : 'none';
1942
+ for (const [preset, button] of viewportPresetButtons) {
1943
+ button.disabled = viewportControlsDisabled;
1944
+ setToolbarButtonTooltip(button, viewportControlsTooltip || VIEW_PRESET_TOOLTIPS[preset]);
1945
+ LocalEditorShared.applyButtonActiveState(button, viewportTools?.viewPreset === preset);
1946
+ }
1947
+ projectionToggleButton.disabled = viewportControlsDisabled;
1948
+ const projectionMode = viewportTools?.projectionMode ?? 'perspective';
1949
+ const nextProjectionMode = projectionMode === 'orthographic' ? 'perspective' : 'orthographic';
1950
+ setToolbarButtonIcon(doc, projectionToggleButton, projectionMode === 'orthographic' ? 'projection-ortho' : 'projection-perspective');
1951
+ setToolbarButtonTooltip(projectionToggleButton, viewportControlsTooltip || `投影模式:${VIEWPORT_PROJECTION_LABELS[projectionMode]}。切换为${VIEWPORT_PROJECTION_LABELS[nextProjectionMode]}`);
1952
+ LocalEditorShared.applyButtonActiveState(projectionToggleButton, projectionMode === 'orthographic');
1953
+ overlaySettingsButton.disabled = disabled;
1954
+ setToolbarButtonTooltip(overlaySettingsButton, '视口信息层设置');
1955
+ LocalEditorShared.applyButtonActiveState(overlaySettingsButton, overlaySettingsOpen);
1956
+ const overlaySettings = viewportTools?.overlay;
1957
+ for (const [key, input] of overlayInputs) {
1958
+ input.disabled = disabled;
1959
+ input.checked = overlaySettings?.[key] ?? false;
1960
+ }
1961
+ measureButton.disabled = viewportControlsDisabled;
1962
+ setToolbarButtonTooltip(measureButton, viewportControlsTooltip || '测量 XZ 地面距离');
1963
+ LocalEditorShared.applyButtonActiveState(measureButton, viewportTools?.activeUtilityTool === 'measure-distance');
1964
+ const hasMeasurement = !!(state.viewportMeasurement?.start
1965
+ || state.viewportMeasurement?.end
1966
+ || state.viewportMeasurement?.preview);
1967
+ clearMeasurementButton.style.display = hasMeasurement ? 'inline-flex' : 'none';
1968
+ clearMeasurementButton.disabled = disabled || !hasMeasurement;
1969
+ setToolbarButtonTooltip(clearMeasurementButton, '清除测量结果');
1970
+ LocalEditorShared.applyButtonActiveState(clearMeasurementButton, false);
1971
+ if (!inEditor || disabled || viewportToolsGroup.style.display === 'none')
1972
+ closeOverlaySettingsPopover();
1973
+ const gridState = state.grid ?? { visible: false, available: false };
1974
+ gridToggleButton.disabled = disabled || !gridState.available;
1975
+ setToolbarButtonTooltip(gridToggleButton, gridState.visible ? '隐藏 Scene View 网格' : '显示 Scene View 网格');
1976
+ LocalEditorShared.applyButtonActiveState(gridToggleButton, gridState.visible);
1977
+ for (const [tool, button] of toolButtons) {
545
1978
  button.disabled = disabled;
546
1979
  LocalEditorShared.applyButtonActiveState(button, transformTool?.activeTool === tool);
547
1980
  }
@@ -550,37 +1983,85 @@ export function createLocalEditorBrowserUi(options = {}) {
550
1983
  LocalEditorShared.applyButtonActiveState(button, transformTool?.activeSpace === space);
551
1984
  }
552
1985
  sceneHelpButton.disabled = disabled;
553
- viewPlaneButton.disabled = disabled || transformTool?.activeTool !== 'move';
554
- LocalEditorShared.applyButtonActiveState(viewPlaneButton, transformTool?.activeTool === 'move' && transformTool?.activeConstraint === 'view-plane');
1986
+ const transformOperations = state.transformOperations ?? {
1987
+ settings: DEFAULT_EDITOR_TRANSFORM_OPERATION_SETTINGS,
1988
+ selectedCount: state.selectedIds.length,
1989
+ activeId: state.activeId,
1990
+ canAlign: false,
1991
+ canDistribute: false,
1992
+ };
1993
+ const operationSettings = transformOperations.settings;
1994
+ snapGroup.style.display = inEditor ? 'flex' : 'none';
1995
+ snapButton.disabled = disabled;
1996
+ snapEnabledInput.disabled = disabled;
1997
+ snapEnabledInput.checked = operationSettings.snap.enabled;
1998
+ LocalEditorShared.applyButtonActiveState(snapButton, operationSettings.snap.enabled || snapSettingsOpen);
1999
+ setToolbarButtonTooltip(snapButton, `吸附设置 · 位移 ${formatSnapStepInputValue(operationSettings.snap, 'move')} · 旋转 ${formatSnapStepInputValue(operationSettings.snap, 'rotate')}° · 缩放 ${formatSnapStepInputValue(operationSettings.snap, 'scale')}`);
2000
+ for (const [kind, input] of snapStepInputs) {
2001
+ input.disabled = disabled;
2002
+ if (doc.activeElement !== input)
2003
+ input.value = formatSnapStepInputValue(operationSettings.snap, kind);
2004
+ }
2005
+ if (!inEditor || disabled || snapGroup.style.display === 'none')
2006
+ closeSnapSettingsPopover();
2007
+ placementGroup.style.display = inEditor ? 'flex' : 'none';
2008
+ placementButton.disabled = disabled;
2009
+ setToolbarButtonIcon(doc, placementButton, PLACEMENT_MODE_ICONS[operationSettings.placementMode]);
2010
+ setToolbarButtonTooltip(placementButton, `放置模式 · ${PLACEMENT_MODE_DESCRIPTIONS[operationSettings.placementMode]}`);
2011
+ LocalEditorShared.applyButtonActiveState(placementButton, operationSettings.placementMode !== 'off' || placementSettingsOpen);
2012
+ for (const [mode, button] of Object.entries(placementButtons)) {
2013
+ button.disabled = disabled;
2014
+ LocalEditorShared.applyButtonActiveState(button, operationSettings.placementMode === mode);
2015
+ }
2016
+ if (!inEditor || disabled || placementGroup.style.display === 'none')
2017
+ closePlacementSettingsPopover();
2018
+ actionGroup.style.display = inEditor ? 'flex' : 'none';
2019
+ const hasTransformAction = transformOperations.canAlign || transformOperations.canDistribute;
2020
+ transformActionButton.disabled = disabled || !hasTransformAction;
2021
+ setToolbarButtonTooltip(transformActionButton, hasTransformAction ? '对齐与分布' : '选择至少 2 个对象后可用');
2022
+ LocalEditorShared.applyButtonActiveState(transformActionButton, transformActionOpen);
2023
+ for (const [action, button] of transformActionButtons) {
2024
+ const enabled = isTransformActionEnabled(action, transformOperations);
2025
+ button.disabled = disabled || !enabled;
2026
+ button.style.opacity = button.disabled ? '0.45' : '1';
2027
+ button.style.cursor = button.disabled ? 'not-allowed' : 'pointer';
2028
+ }
2029
+ if (!inEditor || disabled || !hasTransformAction || actionGroup.style.display === 'none')
2030
+ closeTransformActionPopover();
555
2031
  undoButton.disabled = disabled || !state.session?.canUndo;
556
2032
  redoButton.disabled = disabled || !state.session?.canRedo;
557
2033
  LocalEditorShared.applyButtonActiveState(sceneHelpButton, inEditor && helpOpen);
558
2034
  shortcutHelpPanel.style.display = inEditor && helpOpen ? '' : 'none';
559
2035
  dirtyBadge.style.display = inEditor && state.session?.dirty ? 'inline-flex' : 'none';
560
- const dragSuffix = transformTool?.dragPhase === 'dragging'
561
- ? ` | dragging ${transformTool.draggingNodeId ?? 'selection'}`
562
- : '';
563
- status.textContent = [
2036
+ const editorStatusText = [
564
2037
  state.status,
565
- state.session?.dirty ? '*' : '',
2038
+ state.session?.dirty ? '有未保存更改' : '',
566
2039
  LocalEditorShared.formatAuthoringSourceLabel(state.session?.source),
567
2040
  state.summary,
568
2041
  ].filter(Boolean).join(' | ');
569
- status.textContent += dragSuffix;
570
- status.title = state.statusDetails || status.textContent;
571
- status.style.color = state.statusTone === 'error'
572
- ? '#ff8f87'
2042
+ const transformStatusText = transformTool?.dragPhase === 'dragging'
2043
+ ? `正在拖拽 ${transformTool.draggingNodeId ?? '选择对象'}`
2044
+ : LocalEditorShared.toTransformToolStatusLabel(transformTool?.activeTool ?? 'select', transformTool?.activeSpace ?? 'world');
2045
+ const statusToneColor = state.statusTone === 'error'
2046
+ ? 'var(--fps-editor-danger-strong)'
573
2047
  : state.statusTone === 'warning'
574
- ? '#d29922'
2048
+ ? 'var(--fps-editor-warn)'
575
2049
  : state.statusTone === 'success'
576
- ? '#7ee787'
2050
+ ? 'var(--fps-editor-success)'
577
2051
  : 'var(--fps-editor-muted)';
578
- sceneToolStatus.textContent = transformTool?.dragPhase === 'dragging'
579
- ? `正在拖拽 ${transformTool.draggingNodeId ?? '选择对象'}`
580
- : `${LocalEditorShared.toTransformToolLabel(transformTool?.activeTool ?? 'select')} · ${LocalEditorShared.toTransformSpaceLabel(transformTool?.activeSpace ?? 'world')}${transformTool?.activeConstraint === 'view-plane' ? ' · 视图平面' : ''}`;
581
- sceneMouseHint.textContent = transformTool?.activeConstraint === 'view-plane'
582
- ? '视图平面移动 · 左键拖拽选中对象 · Esc 取消'
583
- : '左键选择 · 空白拖拽框选 · 中键平移 · Alt+左键环绕 · 右键飞行 · 滚轮缩放';
2052
+ setToolbarButtonTooltip(editorStatusButton, [
2053
+ `编辑器状态:${editorStatusText}`,
2054
+ state.statusDetails && state.statusDetails !== state.status ? state.statusDetails : '',
2055
+ transformStatusText,
2056
+ LocalEditorShared.toTransformOperationStatusLabel(operationSettings),
2057
+ ].filter(Boolean).join(' · '));
2058
+ editorStatusButton.style.color = statusToneColor;
2059
+ editorStatusButton.style.borderColor = state.statusTone == null || state.statusTone === 'default'
2060
+ ? 'var(--fps-editor-border)'
2061
+ : statusToneColor;
2062
+ renderCoordinateAxesOverlay(coordinateAxesOverlay, inEditor ? state.coordinateAxes ?? null : null);
2063
+ renderSpatialOverlay(spatialOverlay, inEditor ? state.viewportSpatialOverlay ?? null : null);
2064
+ renderMeasurementOverlay(measurementOverlay, inEditor ? state.viewportMeasurement ?? null : null);
584
2065
  const boxSelection = state.boxSelection;
585
2066
  if (inEditor && boxSelection?.active) {
586
2067
  boxSelectionOverlay.style.display = '';
@@ -592,6 +2073,7 @@ export function createLocalEditorBrowserUi(options = {}) {
592
2073
  else {
593
2074
  boxSelectionOverlay.style.display = 'none';
594
2075
  }
2076
+ scheduleToolbarOverflowLayout();
595
2077
  if (!inEditor)
596
2078
  return;
597
2079
  const hierarchyDescriptor = panelRegistry.getActivePanel('left');
@@ -607,12 +2089,37 @@ export function createLocalEditorBrowserUi(options = {}) {
607
2089
  update(state) {
608
2090
  render(state);
609
2091
  },
2092
+ setTheme(theme) {
2093
+ setActiveTheme(theme);
2094
+ },
2095
+ getTheme() {
2096
+ return activeTheme;
2097
+ },
610
2098
  dispose() {
2099
+ if (toolbarOverflowRaf != null) {
2100
+ doc.defaultView?.cancelAnimationFrame(toolbarOverflowRaf);
2101
+ toolbarOverflowRaf = null;
2102
+ }
2103
+ toolbarResizeObserver?.disconnect();
2104
+ toolbarOverflowButton.removeEventListener('click', onToolbarOverflowButtonClick);
2105
+ localTestButton.removeEventListener('click', onLocalTestButtonClick);
2106
+ doc.removeEventListener('pointerdown', onToolbarOverflowPointerDown);
2107
+ doc.defaultView?.removeEventListener('resize', scheduleToolbarOverflowLayout);
611
2108
  hostChrome.remove();
612
2109
  workbench.root.remove();
2110
+ localTestMenu.remove();
2111
+ toolbarOverflowMenu.remove();
2112
+ snapSettingsPopover.remove();
2113
+ placementSettingsPopover.remove();
2114
+ transformActionPopover.remove();
2115
+ overlaySettingsPopover.remove();
2116
+ spatialOverlay.root.remove();
2117
+ measurementOverlay.root.remove();
613
2118
  boxSelectionOverlay.remove();
614
2119
  shortcutHelpPanel.remove();
615
2120
  hierarchyController.dispose();
2121
+ workbenchLayoutController.dispose();
2122
+ tooltipController.dispose();
616
2123
  contextMenu.dispose();
617
2124
  inputRouter.dispose();
618
2125
  doc.removeEventListener('keydown', onKeyDown);