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

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 (93) hide show
  1. package/dist/local-editor-harness.d.ts +14 -3
  2. package/dist/local-editor-harness.d.ts.map +1 -1
  3. package/dist/local-editor-harness.js +205 -35
  4. package/dist/local-editor-harness.js.map +1 -1
  5. package/node_modules/@fps-games/editor-babylon/dist/editor-sky.d.ts +33 -0
  6. package/node_modules/@fps-games/editor-babylon/dist/editor-sky.d.ts.map +1 -0
  7. package/node_modules/@fps-games/editor-babylon/dist/editor-sky.js +231 -0
  8. package/node_modules/@fps-games/editor-babylon/dist/editor-sky.js.map +1 -0
  9. package/node_modules/@fps-games/editor-babylon/dist/editor-world.d.ts +3 -0
  10. package/node_modules/@fps-games/editor-babylon/dist/editor-world.d.ts.map +1 -1
  11. package/node_modules/@fps-games/editor-babylon/dist/editor-world.js +8 -0
  12. package/node_modules/@fps-games/editor-babylon/dist/editor-world.js.map +1 -1
  13. package/node_modules/@fps-games/editor-babylon/dist/index.d.ts +2 -0
  14. package/node_modules/@fps-games/editor-babylon/dist/index.d.ts.map +1 -1
  15. package/node_modules/@fps-games/editor-babylon/dist/index.js +2 -0
  16. package/node_modules/@fps-games/editor-babylon/dist/index.js.map +1 -1
  17. package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.d.ts +40 -0
  18. package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.d.ts.map +1 -0
  19. package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.js +237 -0
  20. package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.js.map +1 -0
  21. package/node_modules/@fps-games/editor-babylon/dist/projection-selection-controller.d.ts +1 -1
  22. package/node_modules/@fps-games/editor-babylon/dist/projection-selection-controller.d.ts.map +1 -1
  23. package/node_modules/@fps-games/editor-babylon/dist/projection-selection-controller.js +15 -50
  24. package/node_modules/@fps-games/editor-babylon/dist/projection-selection-controller.js.map +1 -1
  25. package/node_modules/@fps-games/editor-babylon/dist/projection.d.ts +5 -0
  26. package/node_modules/@fps-games/editor-babylon/dist/projection.d.ts.map +1 -1
  27. package/node_modules/@fps-games/editor-babylon/dist/projection.js +61 -8
  28. package/node_modules/@fps-games/editor-babylon/dist/projection.js.map +1 -1
  29. package/node_modules/@fps-games/editor-babylon/dist/types.d.ts +7 -0
  30. package/node_modules/@fps-games/editor-babylon/dist/types.d.ts.map +1 -1
  31. package/node_modules/@fps-games/editor-babylon/package.json +4 -4
  32. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-context-menu.js +97 -46
  33. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-context-menu.js.map +1 -1
  34. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.d.ts +5 -0
  35. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.d.ts.map +1 -1
  36. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.js +57 -7
  37. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.js.map +1 -1
  38. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.d.ts.map +1 -1
  39. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.js +61 -5
  40. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.js.map +1 -1
  41. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-tree.js +0 -2
  42. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-tree.js.map +1 -1
  43. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-view.d.ts.map +1 -1
  44. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-view.js +6 -2
  45. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-view.js.map +1 -1
  46. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-icons.d.ts +1 -1
  47. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-icons.d.ts.map +1 -1
  48. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-icons.js +16 -0
  49. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-icons.js.map +1 -1
  50. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.d.ts +8 -1
  51. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.d.ts.map +1 -1
  52. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.js +358 -53
  53. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.js.map +1 -1
  54. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.d.ts +5 -0
  55. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.d.ts.map +1 -1
  56. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.js +28 -8
  57. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.js.map +1 -1
  58. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.d.ts.map +1 -1
  59. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.js +4 -1
  60. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.js.map +1 -1
  61. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-theme.d.ts.map +1 -1
  62. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-theme.js +14 -0
  63. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-theme.js.map +1 -1
  64. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-tooltip.d.ts +11 -0
  65. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-tooltip.d.ts.map +1 -0
  66. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-tooltip.js +148 -0
  67. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-tooltip.js.map +1 -0
  68. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts +42 -1
  69. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts.map +1 -1
  70. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.d.ts +5 -4
  71. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.d.ts.map +1 -1
  72. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js +606 -199
  73. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js.map +1 -1
  74. package/node_modules/@fps-games/editor-browser/package.json +2 -2
  75. package/node_modules/@fps-games/editor-core/dist/index.d.ts +1 -0
  76. package/node_modules/@fps-games/editor-core/dist/index.d.ts.map +1 -1
  77. package/node_modules/@fps-games/editor-core/dist/index.js +1 -0
  78. package/node_modules/@fps-games/editor-core/dist/index.js.map +1 -1
  79. package/node_modules/@fps-games/editor-core/dist/inspector.d.ts +6 -1
  80. package/node_modules/@fps-games/editor-core/dist/inspector.d.ts.map +1 -1
  81. package/node_modules/@fps-games/editor-core/dist/inspector.js +6 -1
  82. package/node_modules/@fps-games/editor-core/dist/inspector.js.map +1 -1
  83. package/node_modules/@fps-games/editor-core/dist/scene-graph.d.ts +7 -0
  84. package/node_modules/@fps-games/editor-core/dist/scene-graph.d.ts.map +1 -1
  85. package/node_modules/@fps-games/editor-core/dist/scene-graph.js.map +1 -1
  86. package/node_modules/@fps-games/editor-core/dist/selection-strategy.d.ts +12 -0
  87. package/node_modules/@fps-games/editor-core/dist/selection-strategy.d.ts.map +1 -0
  88. package/node_modules/@fps-games/editor-core/dist/selection-strategy.js +52 -0
  89. package/node_modules/@fps-games/editor-core/dist/selection-strategy.js.map +1 -0
  90. package/node_modules/@fps-games/editor-core/package.json +2 -2
  91. package/node_modules/@fps-games/editor-forge-play/package.json +2 -2
  92. package/node_modules/@fps-games/editor-protocol/package.json +1 -1
  93. package/package.json +6 -6
@@ -1,18 +1,21 @@
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
+ import { createLocalEditorTooltipController } from './local-editor-ui-tooltip';
4
5
  import { DEFAULT_EDITOR_TRANSFORM_OPERATION_SETTINGS, DEFAULT_EDITOR_TRANSFORM_TOOL_DESCRIPTORS, } from '@fps-games/editor-core';
5
- export { createLocalEditorHierarchyBlankMenu, createLocalEditorHierarchyCopyShortcutAction, createLocalEditorHierarchyDeleteShortcutAction, createLocalEditorHierarchyDuplicateShortcutAction, createLocalEditorHierarchyNodeMenu, createLocalEditorHierarchyPasteShortcutAction, } from './local-editor-ui-hierarchy-actions';
6
+ export { createLocalEditorHierarchyBlankMenu, createLocalEditorHierarchyCopyShortcutAction, createLocalEditorHierarchyDeleteShortcutAction, createLocalEditorHierarchyDuplicateShortcutAction, createLocalEditorHierarchyNodeMenu, createLocalEditorHierarchyPasteShortcutAction, createLocalEditorHierarchySelectAllShortcutAction, } from './local-editor-ui-hierarchy-actions';
6
7
  export { createLocalEditorHierarchyController, } from './local-editor-ui-hierarchy-controller';
7
8
  export { canLocalEditorHierarchyNodeHaveChildren, createLocalEditorHierarchyTreeModel, isLocalEditorHierarchyNodeMovable, } from './local-editor-ui-hierarchy-tree';
8
9
  export { createLocalEditorContextMenuController, } from './local-editor-ui-context-menu';
10
+ export { createLocalEditorTooltipController, } from './local-editor-ui-tooltip';
9
11
  import { createLocalEditorWorkbenchInputRouter } from './local-editor-ui-input-router';
10
12
  import { createLocalEditorPanelRegistry } from './local-editor-ui-panel-registry';
11
13
  import * as LocalEditorShared from './local-editor-ui-shared';
12
14
  import { createShortcutHelpPanel as createLocalEditorShortcutHelpPanel } from './local-editor-ui-shortcuts';
13
15
  import { applyLocalEditorTheme, ensureLocalEditorTheme, LOCAL_EDITOR_THEME_CLASS, normalizeLocalEditorThemeName, } from './local-editor-ui-theme';
14
16
  import { createDefaultLocalEditorWorkbenchLayout, createLocalEditorWorkbenchLayoutController, createLocalEditorWorkbench, createSceneHeaderToolbar, createWorkbenchPanelContent, } from './local-editor-ui-workbench';
15
- export { applyLocalEditorBrowserInspectorControlBinding, createLocalEditorBrowserInspectorControlRegistry, formatLocalEditorBrowserInspectorValue, resolveLocalEditorBrowserInspectorControlRegistration, } from './local-editor-ui-panels';
17
+ import { createLocalEditorIcon } from './local-editor-ui-icons';
18
+ export { applyLocalEditorBrowserInspectorControlBinding, createLocalEditorBrowserInspectorControlRegistry, formatLocalEditorBrowserInspectorValue, resolveLocalEditorBrowserInspectorSectionStatus, resolveLocalEditorBrowserInspectorControlRegistration, } from './local-editor-ui-panels';
16
19
  export { DEFAULT_LOCAL_EDITOR_THEME, LOCAL_EDITOR_THEME_CLASS, applyLocalEditorTheme, normalizeLocalEditorThemeName, } from './local-editor-ui-theme';
17
20
  export { LOCAL_EDITOR_ICON_NAMES, createLocalEditorIcon, isLocalEditorIconName, resolveLocalEditorIconName, } from './local-editor-ui-icons';
18
21
  function readInspectorInputValue(input) {
@@ -67,6 +70,62 @@ const PLACEMENT_MODE_ICONS = {
67
70
  ground: 'place-ground',
68
71
  surface: 'place-surface',
69
72
  };
73
+ const PLACEMENT_MODE_DESCRIPTIONS = {
74
+ off: '关闭放置模式',
75
+ ground: '放置到 XZ 地面',
76
+ surface: '放置到场景表面',
77
+ };
78
+ const TRANSFORM_ACTION_GROUPS = [
79
+ {
80
+ title: '对齐到 Active Object',
81
+ hint: '需要至少 2 个选中对象',
82
+ actions: ['align-x', 'align-y', 'align-z', 'align-all'],
83
+ },
84
+ {
85
+ title: '均匀分布',
86
+ hint: '需要至少 3 个选中对象',
87
+ actions: ['distribute-x', 'distribute-y', 'distribute-z'],
88
+ },
89
+ ];
90
+ const TRANSFORM_ACTION_LABELS = {
91
+ 'align-x': '对齐 X',
92
+ 'align-y': '对齐 Y',
93
+ 'align-z': '对齐 Z',
94
+ 'align-all': '对齐全',
95
+ 'distribute-x': '分布 X',
96
+ 'distribute-y': '分布 Y',
97
+ 'distribute-z': '分布 Z',
98
+ };
99
+ function createTransformToolTooltip(descriptor) {
100
+ const toolLabel = descriptor.shortcut
101
+ ? `${descriptor.label}工具 (${descriptor.shortcut})`
102
+ : `${descriptor.label}工具`;
103
+ return `${toolLabel} · ${LocalEditorShared.toTransformMouseHint(descriptor.tool)}`;
104
+ }
105
+ function createToolbarIconButton(doc, label, icon, tooltip = label, toolbarRole = 'command') {
106
+ return LocalEditorShared.createButton(doc, label, {
107
+ icon,
108
+ labelMode: 'icon-only',
109
+ variant: 'toolbar-icon',
110
+ tooltip,
111
+ ariaLabel: tooltip,
112
+ toolbarRole,
113
+ });
114
+ }
115
+ function setToolbarButtonTooltip(button, tooltip) {
116
+ button.dataset.editorTooltip = tooltip;
117
+ button.setAttribute('aria-label', tooltip);
118
+ }
119
+ function setToolbarButtonIcon(doc, button, icon) {
120
+ const currentIcon = button.querySelector('[data-editor-icon]');
121
+ if (currentIcon?.dataset.editorIcon === icon)
122
+ return;
123
+ const nextIcon = createLocalEditorIcon(doc, icon);
124
+ if (currentIcon)
125
+ currentIcon.replaceWith(nextIcon);
126
+ else
127
+ button.prepend(nextIcon);
128
+ }
70
129
  function readInspectorVectorInputValue(input) {
71
130
  const wrapper = input.closest('[data-inspector-vector-control]');
72
131
  const values = {};
@@ -112,6 +171,121 @@ function readInspectorImmediateSource(input) {
112
171
  return 'color';
113
172
  return 'input';
114
173
  }
174
+ function createCoordinateAxesOverlay(doc) {
175
+ const root = doc.createElement('div');
176
+ root.dataset.editorCoordinateAxesOverlay = 'true';
177
+ root.style.cssText = [
178
+ 'position:absolute',
179
+ 'right:14px',
180
+ 'bottom:14px',
181
+ 'width:112px',
182
+ 'height:112px',
183
+ 'display:none',
184
+ 'z-index:2',
185
+ 'box-sizing:border-box',
186
+ 'border:1px solid color-mix(in srgb, var(--fps-editor-border) 78%, transparent)',
187
+ 'border-radius:8px',
188
+ 'background:color-mix(in srgb, var(--fps-editor-chrome) 58%, transparent)',
189
+ 'box-shadow:var(--fps-editor-shadow-panel)',
190
+ 'pointer-events:none',
191
+ 'overflow:hidden',
192
+ ].join(';');
193
+ const svg = doc.createElementNS('http://www.w3.org/2000/svg', 'svg');
194
+ svg.setAttribute('viewBox', '0 0 112 112');
195
+ svg.setAttribute('aria-hidden', 'true');
196
+ svg.style.cssText = 'display:block;width:100%;height:100%';
197
+ root.appendChild(svg);
198
+ const lineLayer = doc.createElementNS('http://www.w3.org/2000/svg', 'g');
199
+ const labelLayer = doc.createElementNS('http://www.w3.org/2000/svg', 'g');
200
+ svg.appendChild(lineLayer);
201
+ svg.appendChild(labelLayer);
202
+ const axisGroups = new Map();
203
+ const axisLines = new Map();
204
+ const axisLabels = new Map();
205
+ const axisDiscs = new Map();
206
+ for (const axis of ['x', 'y', 'z']) {
207
+ const group = doc.createElementNS('http://www.w3.org/2000/svg', 'g');
208
+ const line = doc.createElementNS('http://www.w3.org/2000/svg', 'line');
209
+ line.setAttribute('stroke-width', '3.2');
210
+ line.setAttribute('stroke-linecap', 'round');
211
+ line.setAttribute('x1', '56');
212
+ line.setAttribute('y1', '56');
213
+ const disc = doc.createElementNS('http://www.w3.org/2000/svg', 'circle');
214
+ disc.setAttribute('r', '7');
215
+ const label = doc.createElementNS('http://www.w3.org/2000/svg', 'text');
216
+ label.setAttribute('text-anchor', 'middle');
217
+ label.setAttribute('dominant-baseline', 'middle');
218
+ label.setAttribute('dy', '0.04em');
219
+ label.style.cssText = [
220
+ 'fill:#172033',
221
+ 'font-size:10px',
222
+ 'font-weight:900',
223
+ 'font-family:Arial, Helvetica, sans-serif',
224
+ 'letter-spacing:0',
225
+ ].join(';');
226
+ group.appendChild(disc);
227
+ group.appendChild(label);
228
+ lineLayer.appendChild(line);
229
+ labelLayer.appendChild(group);
230
+ axisGroups.set(axis, group);
231
+ axisLines.set(axis, line);
232
+ axisLabels.set(axis, label);
233
+ axisDiscs.set(axis, disc);
234
+ }
235
+ const centerDot = doc.createElementNS('http://www.w3.org/2000/svg', 'circle');
236
+ centerDot.setAttribute('cx', '56');
237
+ centerDot.setAttribute('cy', '56');
238
+ centerDot.setAttribute('r', '3.2');
239
+ centerDot.setAttribute('fill', 'color-mix(in srgb, var(--fps-editor-text) 82%, transparent)');
240
+ labelLayer.appendChild(centerDot);
241
+ return {
242
+ root,
243
+ lineLayer,
244
+ labelLayer,
245
+ axisGroups,
246
+ axisLines,
247
+ axisLabels,
248
+ axisDiscs,
249
+ centerDot,
250
+ };
251
+ }
252
+ function renderCoordinateAxesOverlay(overlay, state) {
253
+ if (!state?.axes?.length) {
254
+ overlay.root.style.display = 'none';
255
+ return;
256
+ }
257
+ overlay.root.style.display = '';
258
+ const center = 56;
259
+ const lineLength = 38;
260
+ const sortedAxes = [...state.axes].sort((left, right) => right.depth - left.depth);
261
+ for (const axis of sortedAxes) {
262
+ const group = overlay.axisGroups.get(axis.id);
263
+ const line = overlay.axisLines.get(axis.id);
264
+ const disc = overlay.axisDiscs.get(axis.id);
265
+ const label = overlay.axisLabels.get(axis.id);
266
+ if (!group || !line || !disc || !label)
267
+ continue;
268
+ const length = lineLength * axis.scale;
269
+ const endX = center + axis.x * length;
270
+ const endY = center + axis.y * length;
271
+ line.setAttribute('x2', formatSvgNumber(endX));
272
+ line.setAttribute('y2', formatSvgNumber(endY));
273
+ line.setAttribute('stroke', axis.color);
274
+ disc.setAttribute('cx', formatSvgNumber(endX));
275
+ disc.setAttribute('cy', formatSvgNumber(endY));
276
+ disc.setAttribute('fill', axis.color);
277
+ label.setAttribute('x', formatSvgNumber(endX));
278
+ label.setAttribute('y', formatSvgNumber(endY));
279
+ label.textContent = axis.label;
280
+ group.style.opacity = String(0.72 + axis.scale * 0.28);
281
+ overlay.lineLayer.appendChild(line);
282
+ overlay.labelLayer.appendChild(group);
283
+ }
284
+ overlay.labelLayer.appendChild(overlay.centerDot);
285
+ }
286
+ function formatSvgNumber(value) {
287
+ return Number.isFinite(value) ? value.toFixed(2) : '0';
288
+ }
115
289
  export function createLocalEditorBrowserUi(options = {}) {
116
290
  const doc = options.document ?? document;
117
291
  const root = options.root ?? doc.body;
@@ -123,6 +297,10 @@ export function createLocalEditorBrowserUi(options = {}) {
123
297
  const contextMenu = createLocalEditorContextMenuController(doc, (open) => {
124
298
  inputRouter.setContextMenuOpen(open);
125
299
  }, activeTheme);
300
+ const tooltipSurfaces = new Set();
301
+ const tooltipController = createLocalEditorTooltipController(doc, root, activeTheme, {
302
+ scope: () => tooltipSurfaces,
303
+ });
126
304
  let currentState = null;
127
305
  let inspectorFilter = '';
128
306
  const workbenchLayout = createDefaultLocalEditorWorkbenchLayout();
@@ -161,26 +339,29 @@ export function createLocalEditorBrowserUi(options = {}) {
161
339
  hostChrome.appendChild(hostModeLabel);
162
340
  hostChrome.appendChild(enterEditorButton);
163
341
  root.appendChild(hostChrome);
342
+ tooltipSurfaces.add(hostChrome);
164
343
  const dirtyBadge = doc.createElement('span');
344
+ dirtyBadge.dataset.editorDirtyBadge = 'true';
345
+ dirtyBadge.dataset.editorTooltip = '有未保存更改';
346
+ dirtyBadge.setAttribute('role', 'status');
347
+ dirtyBadge.setAttribute('aria-label', '有未保存更改');
165
348
  dirtyBadge.style.cssText = [
166
349
  'display:none',
167
- 'height:20px',
350
+ 'width:8px',
351
+ 'height:8px',
168
352
  'align-items:center',
169
- 'padding:0 7px',
353
+ 'justify-content:center',
354
+ 'padding:0',
170
355
  'border-radius:999px',
171
- 'background:var(--fps-editor-danger-soft)',
356
+ 'background:var(--fps-editor-danger-strong)',
172
357
  'border:1px solid var(--fps-editor-danger-border)',
173
- 'color:var(--fps-editor-danger-text)',
174
- 'font-size:11px',
175
- 'font-weight:800',
358
+ 'box-shadow:0 0 0 3px var(--fps-editor-danger-soft)',
176
359
  ].join(';');
177
- dirtyBadge.textContent = '未保存';
178
360
  const saveButton = LocalEditorShared.createButton(doc, '保存场景', { icon: 'save' });
179
361
  const saveAndRunButton = LocalEditorShared.createButton(doc, '保存并运行', { icon: 'execute' });
180
362
  const discardRunButton = LocalEditorShared.createButton(doc, '放弃并运行', { icon: 'discard' });
181
- const localTestButton = LocalEditorShared.createButton(doc, '本地测试', { icon: 'execute' });
363
+ const localTestButton = createToolbarIconButton(doc, '本地测试', 'execute', '打开本地测试操作');
182
364
  localTestButton.dataset.editorLocalTestToggle = 'true';
183
- localTestButton.title = '打开本地测试操作';
184
365
  const localTestMenu = doc.createElement('div');
185
366
  localTestMenu.classList.add(LOCAL_EDITOR_THEME_CLASS);
186
367
  localTestMenu.dataset.editorLocalTestMenu = 'true';
@@ -210,20 +391,12 @@ export function createLocalEditorBrowserUi(options = {}) {
210
391
  'gap:4px',
211
392
  ].join(';');
212
393
  localTestGroup.appendChild(localTestButton);
213
- const undoButton = LocalEditorShared.createButton(doc, '撤销', { icon: 'undo' });
214
- const redoButton = LocalEditorShared.createButton(doc, '重做', { icon: 'redo' });
394
+ const undoButton = createToolbarIconButton(doc, '撤销', 'undo', '撤销');
395
+ const redoButton = createToolbarIconButton(doc, '重做', 'redo', '重做');
215
396
  let helpOpen = false;
216
- const status = doc.createElement('span');
217
- status.style.cssText = [
218
- 'flex:0 1 300px',
219
- 'min-width:120px',
220
- 'max-width:320px',
221
- 'overflow:hidden',
222
- 'text-overflow:ellipsis',
223
- 'white-space:nowrap',
224
- 'color:var(--fps-editor-muted)',
225
- 'font-size:11px',
226
- ].join(';');
397
+ const editorStatusButton = createToolbarIconButton(doc, '编辑器状态', 'status', '编辑器状态');
398
+ editorStatusButton.dataset.editorStatusButton = 'true';
399
+ editorStatusButton.style.cursor = 'help';
227
400
  const workbench = createLocalEditorWorkbench(doc);
228
401
  const hierarchyPanel = createWorkbenchPanelContent(doc);
229
402
  const assetPanel = workbench.bottomDock;
@@ -231,6 +404,7 @@ export function createLocalEditorBrowserUi(options = {}) {
231
404
  workbench.leftDock.appendChild(hierarchyPanel);
232
405
  workbench.rightDock.appendChild(inspectorPanel);
233
406
  root.appendChild(workbench.root);
407
+ tooltipSurfaces.add(workbench.root);
234
408
  const workbenchLayoutController = createLocalEditorWorkbenchLayoutController(doc, workbench);
235
409
  const sceneToolOverlay = createSceneHeaderToolbar(doc);
236
410
  const sceneTitle = doc.createElement('div');
@@ -268,14 +442,15 @@ export function createLocalEditorBrowserUi(options = {}) {
268
442
  const toolButtons = new Map();
269
443
  const transformToolDescriptors = Object.values(DEFAULT_EDITOR_TRANSFORM_TOOL_DESCRIPTORS);
270
444
  for (const descriptor of transformToolDescriptors) {
271
- const shortcut = descriptor.shortcut ? `${descriptor.shortcut} ` : '';
272
- const button = LocalEditorShared.createButton(doc, `${shortcut}${descriptor.label}`, {
445
+ const tooltip = createTransformToolTooltip(descriptor);
446
+ const button = LocalEditorShared.createButton(doc, descriptor.label, {
273
447
  icon: TRANSFORM_TOOL_ICONS[descriptor.tool],
448
+ labelMode: 'icon-only',
449
+ variant: 'toolbar-icon',
450
+ tooltip,
451
+ ariaLabel: tooltip,
274
452
  });
275
453
  button.dataset.transformTool = descriptor.tool;
276
- button.title = descriptor.handles.length > 0
277
- ? `${descriptor.label} · ${descriptor.handles.map(handle => handle.label).join(' / ')}`
278
- : descriptor.label;
279
454
  toolButtons.set(descriptor.tool, button);
280
455
  toolGroup.appendChild(button);
281
456
  }
@@ -288,21 +463,13 @@ export function createLocalEditorBrowserUi(options = {}) {
288
463
  'border-left:1px solid var(--fps-editor-divider)',
289
464
  ].join(';');
290
465
  const spaceButtons = {
291
- world: LocalEditorShared.createButton(doc, '世界', { icon: TRANSFORM_SPACE_ICONS.world }),
292
- local: LocalEditorShared.createButton(doc, '本地', { icon: TRANSFORM_SPACE_ICONS.local }),
466
+ world: createToolbarIconButton(doc, '世界', TRANSFORM_SPACE_ICONS.world, '世界坐标空间'),
467
+ local: createToolbarIconButton(doc, '本地', TRANSFORM_SPACE_ICONS.local, '本地坐标空间'),
293
468
  };
294
469
  for (const [space, button] of Object.entries(spaceButtons)) {
295
470
  button.dataset.transformSpace = space;
296
471
  spaceGroup.appendChild(button);
297
472
  }
298
- const handleGroup = doc.createElement('div');
299
- handleGroup.style.cssText = [
300
- 'display:flex',
301
- 'align-items:center',
302
- 'gap:4px',
303
- 'padding-left:8px',
304
- 'border-left:1px solid var(--fps-editor-divider)',
305
- ].join(';');
306
473
  const snapGroup = doc.createElement('div');
307
474
  snapGroup.style.cssText = [
308
475
  'display:flex',
@@ -311,24 +478,71 @@ export function createLocalEditorBrowserUi(options = {}) {
311
478
  'padding-left:8px',
312
479
  'border-left:1px solid var(--fps-editor-divider)',
313
480
  ].join(';');
314
- const snapButton = LocalEditorShared.createButton(doc, '吸附', { icon: 'snap' });
481
+ const snapButton = createToolbarIconButton(doc, '吸附设置', 'snap', '吸附设置', 'settings');
315
482
  snapButton.dataset.transformSnapToggle = 'true';
316
- snapButton.title = '移动 / 旋转 / 缩放吸附';
483
+ const snapSettingsPopover = doc.createElement('div');
484
+ snapSettingsPopover.classList.add(LOCAL_EDITOR_THEME_CLASS);
485
+ snapSettingsPopover.dataset.transformSnapSettingsPopover = 'true';
486
+ snapSettingsPopover.setAttribute('role', 'dialog');
487
+ snapSettingsPopover.setAttribute('aria-label', '吸附设置');
488
+ snapSettingsPopover.style.cssText = [
489
+ 'position:fixed',
490
+ 'z-index:2147483641',
491
+ 'display:none',
492
+ 'flex-direction:column',
493
+ 'gap:8px',
494
+ 'width:220px',
495
+ 'padding:10px',
496
+ 'border:1px solid var(--fps-editor-border)',
497
+ 'border-radius:3px',
498
+ 'background:var(--fps-editor-panel)',
499
+ 'box-shadow:var(--fps-editor-shadow-popover)',
500
+ 'pointer-events:auto',
501
+ ].join(';');
502
+ const snapSettingsHeader = doc.createElement('label');
503
+ snapSettingsHeader.style.cssText = [
504
+ 'display:flex',
505
+ 'align-items:center',
506
+ 'justify-content:space-between',
507
+ 'gap:8px',
508
+ 'color:var(--fps-editor-text)',
509
+ 'font-size:12px',
510
+ 'font-weight:900',
511
+ ].join(';');
512
+ const snapSettingsHeaderText = doc.createElement('span');
513
+ snapSettingsHeaderText.textContent = '启用吸附';
514
+ const snapEnabledInput = doc.createElement('input');
515
+ snapEnabledInput.type = 'checkbox';
516
+ snapEnabledInput.dataset.transformSnapEnabled = 'true';
517
+ snapSettingsHeader.appendChild(snapSettingsHeaderText);
518
+ snapSettingsHeader.appendChild(snapEnabledInput);
519
+ snapSettingsPopover.appendChild(snapSettingsHeader);
317
520
  const snapStepInputs = new Map();
318
- for (const [kind, label, title] of [
319
- ['move', 'M', '移动吸附步长'],
320
- ['rotate', 'R', '旋转吸附角度'],
321
- ['scale', 'S', '缩放吸附步长'],
521
+ for (const [kind, label, unit] of [
522
+ ['move', '位移', '单位'],
523
+ ['rotate', '旋转', ''],
524
+ ['scale', '缩放', '倍数'],
322
525
  ]) {
526
+ const row = doc.createElement('label');
527
+ row.style.cssText = [
528
+ 'display:grid',
529
+ 'grid-template-columns:54px 1fr 34px',
530
+ 'align-items:center',
531
+ 'gap:8px',
532
+ 'color:var(--fps-editor-muted-strong)',
533
+ 'font-size:11px',
534
+ 'font-weight:800',
535
+ ].join(';');
536
+ const rowLabel = doc.createElement('span');
537
+ rowLabel.textContent = label;
323
538
  const input = doc.createElement('input');
324
539
  input.type = 'number';
325
540
  input.min = '0.0001';
326
541
  input.step = kind === 'rotate' ? '1' : '0.1';
327
542
  input.dataset.transformSnapStep = kind;
328
- input.title = `${label} · ${title}`;
329
543
  input.style.cssText = [
330
- 'width:46px',
331
544
  'height:26px',
545
+ 'min-width:0',
332
546
  'box-sizing:border-box',
333
547
  'border:1px solid var(--fps-editor-border)',
334
548
  'border-radius:3px',
@@ -338,11 +552,16 @@ export function createLocalEditorBrowserUi(options = {}) {
338
552
  'font-weight:800',
339
553
  'padding:0 4px',
340
554
  ].join(';');
555
+ const rowUnit = doc.createElement('span');
556
+ rowUnit.textContent = unit;
557
+ rowUnit.style.cssText = 'color:var(--fps-editor-muted);font-size:10px;font-weight:800';
558
+ row.appendChild(rowLabel);
559
+ row.appendChild(input);
560
+ row.appendChild(rowUnit);
561
+ snapSettingsPopover.appendChild(row);
341
562
  snapStepInputs.set(kind, input);
342
563
  }
343
564
  snapGroup.appendChild(snapButton);
344
- for (const input of snapStepInputs.values())
345
- snapGroup.appendChild(input);
346
565
  const placementGroup = doc.createElement('div');
347
566
  placementGroup.style.cssText = [
348
567
  'display:flex',
@@ -351,22 +570,40 @@ export function createLocalEditorBrowserUi(options = {}) {
351
570
  'padding-left:8px',
352
571
  'border-left:1px solid var(--fps-editor-divider)',
353
572
  ].join(';');
354
- const placementLabel = doc.createElement('span');
355
- placementLabel.textContent = '放置';
356
- placementLabel.style.cssText = 'color:var(--fps-editor-muted);font-size:11px;font-weight:900;white-space:nowrap';
357
- const placementButtons = {
358
- off: LocalEditorShared.createButton(doc, '', { icon: PLACEMENT_MODE_ICONS.off }),
359
- ground: LocalEditorShared.createButton(doc, '', { icon: PLACEMENT_MODE_ICONS.ground }),
360
- surface: LocalEditorShared.createButton(doc, '', { icon: PLACEMENT_MODE_ICONS.surface }),
361
- };
362
- placementButtons.off.title = '关闭放置模式';
363
- placementButtons.ground.title = '放置到 XZ 地面';
364
- placementButtons.surface.title = '放置到场景表面';
365
- placementGroup.appendChild(placementLabel);
366
- for (const [mode, button] of Object.entries(placementButtons)) {
573
+ const placementButton = createToolbarIconButton(doc, '放置模式', PLACEMENT_MODE_ICONS.off, '放置模式', 'settings');
574
+ placementButton.dataset.placementModeToggle = 'true';
575
+ const placementSettingsPopover = doc.createElement('div');
576
+ placementSettingsPopover.classList.add(LOCAL_EDITOR_THEME_CLASS);
577
+ placementSettingsPopover.dataset.placementSettingsPopover = 'true';
578
+ placementSettingsPopover.setAttribute('role', 'dialog');
579
+ placementSettingsPopover.setAttribute('aria-label', '放置模式');
580
+ placementSettingsPopover.style.cssText = [
581
+ 'position:fixed',
582
+ 'z-index:2147483641',
583
+ 'display:none',
584
+ 'flex-direction:column',
585
+ 'gap:5px',
586
+ 'width:180px',
587
+ 'padding:8px',
588
+ 'border:1px solid var(--fps-editor-border)',
589
+ 'border-radius:3px',
590
+ 'background:var(--fps-editor-panel)',
591
+ 'box-shadow:var(--fps-editor-shadow-popover)',
592
+ 'pointer-events:auto',
593
+ ].join(';');
594
+ const placementButtons = Object.fromEntries(Object.keys(PLACEMENT_MODE_ICONS).map((mode) => {
595
+ const button = LocalEditorShared.createButton(doc, PLACEMENT_MODE_DESCRIPTIONS[mode], {
596
+ icon: PLACEMENT_MODE_ICONS[mode],
597
+ tooltip: PLACEMENT_MODE_DESCRIPTIONS[mode],
598
+ ariaLabel: PLACEMENT_MODE_DESCRIPTIONS[mode],
599
+ });
367
600
  button.dataset.placementMode = mode;
368
- placementGroup.appendChild(button);
369
- }
601
+ button.style.justifyContent = 'flex-start';
602
+ button.style.width = '100%';
603
+ placementSettingsPopover.appendChild(button);
604
+ return [mode, button];
605
+ }));
606
+ placementGroup.appendChild(placementButton);
370
607
  const actionGroup = doc.createElement('div');
371
608
  actionGroup.style.cssText = [
372
609
  'display:flex',
@@ -375,70 +612,73 @@ export function createLocalEditorBrowserUi(options = {}) {
375
612
  'padding-left:8px',
376
613
  'border-left:1px solid var(--fps-editor-divider)',
377
614
  ].join(';');
378
- const transformActionSelect = doc.createElement('select');
379
- transformActionSelect.dataset.transformActionSelect = 'true';
380
- transformActionSelect.title = '多选对齐 / 分布';
381
- transformActionSelect.style.cssText = [
382
- 'height:26px',
615
+ const transformActionButton = createToolbarIconButton(doc, '对齐与分布', 'arrange', '对齐与分布', 'settings');
616
+ transformActionButton.dataset.transformActionToggle = 'true';
617
+ const transformActionPopover = doc.createElement('div');
618
+ transformActionPopover.classList.add(LOCAL_EDITOR_THEME_CLASS);
619
+ transformActionPopover.dataset.transformActionPopover = 'true';
620
+ transformActionPopover.setAttribute('role', 'dialog');
621
+ transformActionPopover.setAttribute('aria-label', '对齐与分布');
622
+ transformActionPopover.style.cssText = [
623
+ 'position:fixed',
624
+ 'z-index:2147483641',
625
+ 'display:none',
626
+ 'flex-direction:column',
627
+ 'gap:9px',
628
+ 'width:236px',
629
+ 'padding:10px',
383
630
  'border:1px solid var(--fps-editor-border)',
384
631
  'border-radius:3px',
385
- 'background:var(--fps-editor-field)',
386
- 'color:var(--fps-editor-text)',
387
- 'font-size:11px',
388
- 'font-weight:800',
389
- 'padding:0 4px',
632
+ 'background:var(--fps-editor-panel)',
633
+ 'box-shadow:var(--fps-editor-shadow-popover)',
634
+ 'pointer-events:auto',
390
635
  ].join(';');
391
- let selectedTransformAction = 'align-all';
392
- for (const action of [
393
- 'align-x',
394
- 'align-y',
395
- 'align-z',
396
- 'align-all',
397
- 'distribute-x',
398
- 'distribute-y',
399
- 'distribute-z',
400
- ]) {
401
- const option = doc.createElement('option');
402
- option.value = action;
403
- option.textContent = LocalEditorShared.toTransformActionLabel(action);
404
- transformActionSelect.appendChild(option);
636
+ const transformActionButtons = new Map();
637
+ for (const group of TRANSFORM_ACTION_GROUPS) {
638
+ const section = doc.createElement('section');
639
+ section.style.cssText = 'display:flex;flex-direction:column;gap:5px';
640
+ const heading = doc.createElement('div');
641
+ heading.textContent = group.title;
642
+ heading.style.cssText = [
643
+ 'color:var(--fps-editor-text-strong)',
644
+ 'font-size:11px',
645
+ 'font-weight:900',
646
+ ].join(';');
647
+ const hint = doc.createElement('div');
648
+ hint.textContent = group.hint;
649
+ hint.style.cssText = [
650
+ 'color:var(--fps-editor-muted)',
651
+ 'font-size:10px',
652
+ 'font-weight:700',
653
+ 'margin-top:-3px',
654
+ ].join(';');
655
+ section.appendChild(heading);
656
+ section.appendChild(hint);
657
+ for (const action of group.actions) {
658
+ const button = LocalEditorShared.createButton(doc, TRANSFORM_ACTION_LABELS[action], {
659
+ icon: 'arrange',
660
+ tooltip: TRANSFORM_ACTION_LABELS[action],
661
+ ariaLabel: TRANSFORM_ACTION_LABELS[action],
662
+ });
663
+ button.dataset.transformAction = action;
664
+ button.style.justifyContent = 'flex-start';
665
+ button.style.width = '100%';
666
+ transformActionButtons.set(action, button);
667
+ section.appendChild(button);
668
+ }
669
+ transformActionPopover.appendChild(section);
405
670
  }
406
- transformActionSelect.value = selectedTransformAction;
407
- const transformActionButton = LocalEditorShared.createButton(doc, '执行', { icon: 'execute' });
408
- transformActionButton.dataset.transformActionRun = 'true';
409
- transformActionButton.title = '对当前多选执行 Transform 操作';
410
- actionGroup.appendChild(transformActionSelect);
411
671
  actionGroup.appendChild(transformActionButton);
412
- const sceneToolStatus = doc.createElement('span');
413
- sceneToolStatus.style.cssText = [
414
- 'max-width:220px',
415
- 'overflow:hidden',
416
- 'text-overflow:ellipsis',
417
- 'white-space:nowrap',
418
- 'color:var(--fps-editor-muted-strong)',
419
- 'font-size:11px',
420
- ].join(';');
421
- const sceneMouseHint = doc.createElement('span');
422
- sceneMouseHint.style.cssText = [
423
- 'padding-left:8px',
424
- 'border-left:1px solid var(--fps-editor-divider)',
425
- 'max-width:360px',
426
- 'overflow:hidden',
427
- 'text-overflow:ellipsis',
428
- 'white-space:nowrap',
429
- 'color:var(--fps-editor-muted)',
430
- 'font-size:11px',
431
- ].join(';');
432
- sceneMouseHint.textContent = '左键选择 · 空白拖拽框选 · 中键平移 · Alt+左键环绕 · 右键飞行 · 滚轮缩放';
433
- const themeToggleButton = LocalEditorShared.createButton(doc, '主题', { icon: 'theme' });
672
+ const themeToggleButton = createToolbarIconButton(doc, '主题', 'theme', '切换主题');
434
673
  themeToggleButton.dataset.editorThemeToggle = 'true';
435
- themeToggleButton.style.padding = '5px 7px';
436
- const sceneHelpButton = LocalEditorShared.createButton(doc, '快捷键', { icon: 'help' });
437
- sceneHelpButton.style.padding = '5px 7px';
674
+ const gridToggleButton = createToolbarIconButton(doc, '网格', 'grid', '显示 / 隐藏 Scene View 网格');
675
+ gridToggleButton.dataset.editorGridToggle = 'true';
676
+ const sceneHelpButton = createToolbarIconButton(doc, '快捷键', 'help', '快捷键与操作说明');
438
677
  sceneQuickActions.appendChild(localTestGroup);
439
678
  sceneQuickActions.appendChild(undoButton);
440
679
  sceneQuickActions.appendChild(redoButton);
441
680
  sceneQuickActions.appendChild(dirtyBadge);
681
+ sceneUtilityActions.appendChild(gridToggleButton);
442
682
  sceneUtilityActions.appendChild(themeToggleButton);
443
683
  sceneUtilityActions.appendChild(sceneHelpButton);
444
684
  const cameraPreviewGroup = doc.createElement('div');
@@ -449,13 +689,11 @@ export function createLocalEditorBrowserUi(options = {}) {
449
689
  'padding-left:8px',
450
690
  'border-left:1px solid var(--fps-editor-divider)',
451
691
  ].join(';');
452
- const sceneCameraButton = LocalEditorShared.createButton(doc, 'Scene Camera', { icon: 'camera' });
692
+ const sceneCameraButton = createToolbarIconButton(doc, ' Main Camera 查看场景', 'camera');
453
693
  sceneCameraButton.dataset.sceneCameraPreviewToggle = 'true';
454
- sceneCameraButton.title = '从 Main Camera 查看当前场景';
455
694
  cameraPreviewGroup.appendChild(sceneCameraButton);
456
- const toolbarOverflowButton = LocalEditorShared.createButton(doc, '更多', { icon: 'chevron-down' });
695
+ const toolbarOverflowButton = createToolbarIconButton(doc, '更多', 'chevron-down', '显示隐藏的工具栏命令');
457
696
  toolbarOverflowButton.dataset.editorToolbarOverflowToggle = 'true';
458
- toolbarOverflowButton.title = '显示隐藏的工具栏命令';
459
697
  toolbarOverflowButton.style.display = 'none';
460
698
  const toolbarOverflowMenu = doc.createElement('div');
461
699
  toolbarOverflowMenu.classList.add(LOCAL_EDITOR_THEME_CLASS);
@@ -477,23 +715,30 @@ export function createLocalEditorBrowserUi(options = {}) {
477
715
  'box-shadow:var(--fps-editor-shadow-popover)',
478
716
  'pointer-events:auto',
479
717
  ].join(';');
718
+ const coordinateAxesOverlay = createCoordinateAxesOverlay(doc);
480
719
  sceneToolOverlay.appendChild(sceneTitle);
481
720
  sceneToolOverlay.appendChild(sceneQuickActions);
482
721
  sceneToolOverlay.appendChild(sceneUtilityActions);
483
722
  sceneToolOverlay.appendChild(cameraPreviewGroup);
484
723
  sceneToolOverlay.appendChild(toolGroup);
485
724
  sceneToolOverlay.appendChild(spaceGroup);
486
- sceneToolOverlay.appendChild(handleGroup);
487
725
  sceneToolOverlay.appendChild(snapGroup);
488
726
  sceneToolOverlay.appendChild(placementGroup);
489
727
  sceneToolOverlay.appendChild(actionGroup);
490
- sceneToolOverlay.appendChild(sceneToolStatus);
491
- sceneToolOverlay.appendChild(sceneMouseHint);
492
- sceneToolOverlay.appendChild(status);
728
+ sceneToolOverlay.appendChild(editorStatusButton);
493
729
  sceneToolOverlay.appendChild(toolbarOverflowButton);
494
730
  workbench.sceneHeader.appendChild(sceneToolOverlay);
731
+ workbench.sceneFrame.appendChild(coordinateAxesOverlay.root);
495
732
  root.appendChild(localTestMenu);
496
733
  root.appendChild(toolbarOverflowMenu);
734
+ root.appendChild(snapSettingsPopover);
735
+ root.appendChild(placementSettingsPopover);
736
+ root.appendChild(transformActionPopover);
737
+ tooltipSurfaces.add(localTestMenu);
738
+ tooltipSurfaces.add(toolbarOverflowMenu);
739
+ tooltipSurfaces.add(snapSettingsPopover);
740
+ tooltipSurfaces.add(placementSettingsPopover);
741
+ tooltipSurfaces.add(transformActionPopover);
497
742
  const boxSelectionOverlay = doc.createElement('div');
498
743
  boxSelectionOverlay.classList.add(LOCAL_EDITOR_THEME_CLASS);
499
744
  boxSelectionOverlay.style.cssText = [
@@ -506,9 +751,11 @@ export function createLocalEditorBrowserUi(options = {}) {
506
751
  'pointer-events:none',
507
752
  ].join(';');
508
753
  root.appendChild(boxSelectionOverlay);
754
+ tooltipSurfaces.add(boxSelectionOverlay);
509
755
  const shortcutHelpPanel = createLocalEditorShortcutHelpPanel(doc);
510
756
  shortcutHelpPanel.classList.add(LOCAL_EDITOR_THEME_CLASS);
511
757
  root.appendChild(shortcutHelpPanel);
758
+ tooltipSurfaces.add(shortcutHelpPanel);
512
759
  function applyThemeToSurfaces() {
513
760
  applyLocalEditorTheme(hostChrome, activeTheme);
514
761
  applyLocalEditorTheme(workbench.root, activeTheme);
@@ -516,13 +763,17 @@ export function createLocalEditorBrowserUi(options = {}) {
516
763
  applyLocalEditorTheme(boxSelectionOverlay, activeTheme);
517
764
  applyLocalEditorTheme(localTestMenu, activeTheme);
518
765
  applyLocalEditorTheme(toolbarOverflowMenu, activeTheme);
766
+ applyLocalEditorTheme(snapSettingsPopover, activeTheme);
767
+ applyLocalEditorTheme(placementSettingsPopover, activeTheme);
768
+ applyLocalEditorTheme(transformActionPopover, activeTheme);
769
+ tooltipController.setTheme(activeTheme);
519
770
  contextMenu.setTheme?.(activeTheme);
520
771
  }
521
772
  function updateThemeToggleButton() {
522
773
  const nextTheme = activeTheme === 'dark' ? 'light' : 'dark';
523
774
  themeToggleButton.dataset.editorTheme = activeTheme;
524
775
  themeToggleButton.setAttribute('aria-pressed', activeTheme === 'light' ? 'true' : 'false');
525
- themeToggleButton.title = nextTheme === 'light' ? '切换为浅色主题' : '切换为深色主题';
776
+ setToolbarButtonTooltip(themeToggleButton, nextTheme === 'light' ? '切换为浅色主题' : '切换为深色主题');
526
777
  }
527
778
  function setActiveTheme(theme) {
528
779
  activeTheme = normalizeLocalEditorThemeName(theme);
@@ -530,15 +781,43 @@ export function createLocalEditorBrowserUi(options = {}) {
530
781
  updateThemeToggleButton();
531
782
  }
532
783
  let localTestMenuOpen = false;
784
+ let snapSettingsOpen = false;
785
+ let placementSettingsOpen = false;
786
+ let transformActionOpen = false;
533
787
  function closeLocalTestMenu() {
534
788
  localTestMenuOpen = false;
535
789
  localTestMenu.style.display = 'none';
536
790
  LocalEditorShared.applyButtonActiveState(localTestButton, false);
537
791
  }
792
+ function closeSnapSettingsPopover() {
793
+ snapSettingsOpen = false;
794
+ snapSettingsPopover.style.display = 'none';
795
+ snapButton.setAttribute('aria-expanded', 'false');
796
+ const enabled = currentState?.transformOperations?.settings.snap.enabled
797
+ ?? DEFAULT_EDITOR_TRANSFORM_OPERATION_SETTINGS.snap.enabled;
798
+ LocalEditorShared.applyButtonActiveState(snapButton, enabled);
799
+ }
800
+ function closePlacementSettingsPopover() {
801
+ placementSettingsOpen = false;
802
+ placementSettingsPopover.style.display = 'none';
803
+ placementButton.setAttribute('aria-expanded', 'false');
804
+ const mode = currentState?.transformOperations?.settings.placementMode
805
+ ?? DEFAULT_EDITOR_TRANSFORM_OPERATION_SETTINGS.placementMode;
806
+ LocalEditorShared.applyButtonActiveState(placementButton, mode !== 'off');
807
+ }
808
+ function closeTransformActionPopover() {
809
+ transformActionOpen = false;
810
+ transformActionPopover.style.display = 'none';
811
+ transformActionButton.setAttribute('aria-expanded', 'false');
812
+ LocalEditorShared.applyButtonActiveState(transformActionButton, false);
813
+ }
538
814
  function openLocalTestMenu() {
539
815
  if (!localTestActionsEnabled || localTestButton.style.display === 'none')
540
816
  return;
541
817
  closeToolbarOverflowMenu();
818
+ closeSnapSettingsPopover();
819
+ closePlacementSettingsPopover();
820
+ closeTransformActionPopover();
542
821
  const win = doc.defaultView;
543
822
  const rect = localTestButton.getBoundingClientRect();
544
823
  const viewportWidth = win?.innerWidth ?? doc.documentElement.clientWidth;
@@ -548,6 +827,78 @@ export function createLocalEditorBrowserUi(options = {}) {
548
827
  localTestMenuOpen = true;
549
828
  LocalEditorShared.applyButtonActiveState(localTestButton, true);
550
829
  }
830
+ function openSnapSettingsPopover() {
831
+ if (snapButton.disabled || snapGroup.style.display === 'none')
832
+ return;
833
+ closeToolbarOverflowMenu();
834
+ closeLocalTestMenu();
835
+ closePlacementSettingsPopover();
836
+ closeTransformActionPopover();
837
+ const win = doc.defaultView;
838
+ const rect = snapButton.getBoundingClientRect();
839
+ const viewportWidth = win?.innerWidth ?? doc.documentElement.clientWidth;
840
+ snapSettingsPopover.style.display = 'flex';
841
+ const popoverWidth = snapSettingsPopover.offsetWidth || 220;
842
+ snapSettingsPopover.style.top = `${Math.max(8, rect.bottom + 5)}px`;
843
+ snapSettingsPopover.style.left = `${Math.max(8, Math.min(rect.left, viewportWidth - popoverWidth - 8))}px`;
844
+ snapSettingsOpen = true;
845
+ snapButton.setAttribute('aria-expanded', 'true');
846
+ LocalEditorShared.applyButtonActiveState(snapButton, true);
847
+ }
848
+ function toggleSnapSettingsPopover() {
849
+ if (snapSettingsOpen)
850
+ closeSnapSettingsPopover();
851
+ else
852
+ openSnapSettingsPopover();
853
+ }
854
+ function openPlacementSettingsPopover() {
855
+ if (placementButton.disabled || placementGroup.style.display === 'none')
856
+ return;
857
+ closeToolbarOverflowMenu();
858
+ closeLocalTestMenu();
859
+ closeSnapSettingsPopover();
860
+ closeTransformActionPopover();
861
+ const win = doc.defaultView;
862
+ const rect = placementButton.getBoundingClientRect();
863
+ const viewportWidth = win?.innerWidth ?? doc.documentElement.clientWidth;
864
+ placementSettingsPopover.style.display = 'flex';
865
+ const popoverWidth = placementSettingsPopover.offsetWidth || 180;
866
+ placementSettingsPopover.style.top = `${Math.max(8, rect.bottom + 5)}px`;
867
+ placementSettingsPopover.style.left = `${Math.max(8, Math.min(rect.left, viewportWidth - popoverWidth - 8))}px`;
868
+ placementSettingsOpen = true;
869
+ placementButton.setAttribute('aria-expanded', 'true');
870
+ LocalEditorShared.applyButtonActiveState(placementButton, true);
871
+ }
872
+ function togglePlacementSettingsPopover() {
873
+ if (placementSettingsOpen)
874
+ closePlacementSettingsPopover();
875
+ else
876
+ openPlacementSettingsPopover();
877
+ }
878
+ function openTransformActionPopover() {
879
+ if (transformActionButton.disabled || actionGroup.style.display === 'none')
880
+ return;
881
+ closeToolbarOverflowMenu();
882
+ closeLocalTestMenu();
883
+ closeSnapSettingsPopover();
884
+ closePlacementSettingsPopover();
885
+ const win = doc.defaultView;
886
+ const rect = transformActionButton.getBoundingClientRect();
887
+ const viewportWidth = win?.innerWidth ?? doc.documentElement.clientWidth;
888
+ transformActionPopover.style.display = 'flex';
889
+ const popoverWidth = transformActionPopover.offsetWidth || 236;
890
+ transformActionPopover.style.top = `${Math.max(8, rect.bottom + 5)}px`;
891
+ transformActionPopover.style.left = `${Math.max(8, Math.min(rect.left, viewportWidth - popoverWidth - 8))}px`;
892
+ transformActionOpen = true;
893
+ transformActionButton.setAttribute('aria-expanded', 'true');
894
+ LocalEditorShared.applyButtonActiveState(transformActionButton, true);
895
+ }
896
+ function toggleTransformActionPopover() {
897
+ if (transformActionOpen)
898
+ closeTransformActionPopover();
899
+ else
900
+ openTransformActionPopover();
901
+ }
551
902
  const onLocalTestButtonClick = (event) => {
552
903
  event.stopPropagation();
553
904
  if (localTestMenuOpen)
@@ -579,23 +930,17 @@ export function createLocalEditorBrowserUi(options = {}) {
579
930
  cameraPreviewGroup,
580
931
  toolGroup,
581
932
  spaceGroup,
582
- handleGroup,
583
933
  snapGroup,
584
934
  placementGroup,
585
935
  actionGroup,
586
- sceneToolStatus,
587
- sceneMouseHint,
588
- status,
936
+ editorStatusButton,
589
937
  toolbarOverflowButton,
590
938
  ];
591
939
  const toolbarOverflowItems = [
592
- { id: 'status', element: status, kind: 'text' },
593
- { id: 'mouse-hint', element: sceneMouseHint, kind: 'text' },
594
- { id: 'tool-status', element: sceneToolStatus, kind: 'text' },
940
+ { id: 'editor-status', element: editorStatusButton, kind: 'group' },
595
941
  { id: 'transform-actions', element: actionGroup, kind: 'group' },
596
942
  { id: 'placement', element: placementGroup, kind: 'group' },
597
943
  { id: 'snap', element: snapGroup, kind: 'group' },
598
- { id: 'handles', element: handleGroup, kind: 'group' },
599
944
  { id: 'space', element: spaceGroup, kind: 'group' },
600
945
  { id: 'camera-preview', element: cameraPreviewGroup, kind: 'group' },
601
946
  { id: 'scene-utilities', element: sceneUtilityActions, kind: 'group' },
@@ -636,6 +981,8 @@ export function createLocalEditorBrowserUi(options = {}) {
636
981
  item.element.style.width = '100%';
637
982
  }
638
983
  function setToolbarButtonCompact(button, compact) {
984
+ if (button.dataset.editorButtonLabelMode === 'icon-only')
985
+ return;
639
986
  const label = button.querySelector('[data-editor-button-label]');
640
987
  if (!label)
641
988
  return;
@@ -668,6 +1015,9 @@ export function createLocalEditorBrowserUi(options = {}) {
668
1015
  if (toolbarOverflowMenu.childElementCount === 0)
669
1016
  return;
670
1017
  closeLocalTestMenu();
1018
+ closeSnapSettingsPopover();
1019
+ closePlacementSettingsPopover();
1020
+ closeTransformActionPopover();
671
1021
  const win = doc.defaultView;
672
1022
  const rect = toolbarOverflowButton.getBoundingClientRect();
673
1023
  const viewportWidth = win?.innerWidth ?? doc.documentElement.clientWidth;
@@ -744,8 +1094,17 @@ export function createLocalEditorBrowserUi(options = {}) {
744
1094
  return;
745
1095
  if (target.closest('[data-editor-toolbar-overflow-menu]') || target.closest('[data-editor-toolbar-overflow-toggle]'))
746
1096
  return;
1097
+ if (target.closest('[data-transform-snap-settings-popover]') || target.closest('[data-transform-snap-toggle]'))
1098
+ return;
1099
+ if (target.closest('[data-placement-settings-popover]') || target.closest('[data-placement-mode-toggle]'))
1100
+ return;
1101
+ if (target.closest('[data-transform-action-popover]') || target.closest('[data-transform-action-toggle]'))
1102
+ return;
747
1103
  closeLocalTestMenu();
748
1104
  closeToolbarOverflowMenu();
1105
+ closeSnapSettingsPopover();
1106
+ closePlacementSettingsPopover();
1107
+ closeTransformActionPopover();
749
1108
  };
750
1109
  const ResizeObserverCtor = doc.defaultView?.ResizeObserver;
751
1110
  const toolbarResizeObserver = ResizeObserverCtor
@@ -776,6 +1135,10 @@ export function createLocalEditorBrowserUi(options = {}) {
776
1135
  });
777
1136
  undoButton.addEventListener('click', () => callbacks.onUndo?.());
778
1137
  redoButton.addEventListener('click', () => callbacks.onRedo?.());
1138
+ gridToggleButton.addEventListener('click', () => {
1139
+ const visible = currentState?.grid?.visible ?? true;
1140
+ callbacks.onGridVisibleChange?.(!visible);
1141
+ });
779
1142
  sceneCameraButton.addEventListener('click', () => {
780
1143
  const enabled = currentState?.sceneCameraPreview?.enabled ?? false;
781
1144
  callbacks.onSceneCameraPreviewToggle?.(!enabled);
@@ -812,12 +1175,18 @@ export function createLocalEditorBrowserUi(options = {}) {
812
1175
  if (space)
813
1176
  callbacks.onTransformSpaceChange?.(space);
814
1177
  });
815
- snapButton.addEventListener('click', () => {
816
- const enabled = currentState?.transformOperations?.settings.snap.enabled
817
- ?? DEFAULT_EDITOR_TRANSFORM_OPERATION_SETTINGS.snap.enabled;
818
- callbacks.onTransformSnapEnabledChange?.(!enabled);
1178
+ snapButton.addEventListener('click', (event) => {
1179
+ event.stopPropagation();
1180
+ toggleSnapSettingsPopover();
819
1181
  });
820
- snapGroup.addEventListener('change', (event) => {
1182
+ snapSettingsPopover.addEventListener('change', (event) => {
1183
+ const enabledInput = event.target instanceof HTMLInputElement
1184
+ ? event.target.closest('[data-transform-snap-enabled]')
1185
+ : null;
1186
+ if (enabledInput) {
1187
+ callbacks.onTransformSnapEnabledChange?.(enabledInput.checked);
1188
+ return;
1189
+ }
821
1190
  const input = event.target instanceof HTMLInputElement ? event.target : null;
822
1191
  const kind = input?.dataset.transformSnapStep;
823
1192
  if (!input || !kind)
@@ -831,18 +1200,31 @@ export function createLocalEditorBrowserUi(options = {}) {
831
1200
  callbacks.onTransformSnapStepChange?.({ kind, value });
832
1201
  });
833
1202
  placementGroup.addEventListener('click', (event) => {
1203
+ event.stopPropagation();
1204
+ togglePlacementSettingsPopover();
1205
+ });
1206
+ placementSettingsPopover.addEventListener('click', (event) => {
834
1207
  const target = event.target instanceof HTMLElement ? event.target : null;
835
1208
  const mode = target?.closest('[data-placement-mode]')?.dataset.placementMode;
836
- if (mode)
837
- callbacks.onPlacementModeChange?.(mode);
1209
+ if (!mode)
1210
+ return;
1211
+ callbacks.onPlacementModeChange?.(mode);
1212
+ closePlacementSettingsPopover();
838
1213
  });
839
- transformActionSelect.addEventListener('change', () => {
840
- selectedTransformAction = transformActionSelect.value;
841
- if (currentState)
842
- render(currentState);
1214
+ actionGroup.addEventListener('click', (event) => {
1215
+ event.stopPropagation();
1216
+ toggleTransformActionPopover();
843
1217
  });
844
- transformActionButton.addEventListener('click', () => {
845
- callbacks.onTransformAction?.(selectedTransformAction);
1218
+ transformActionPopover.addEventListener('click', (event) => {
1219
+ const target = event.target instanceof HTMLElement ? event.target : null;
1220
+ const action = target?.closest('[data-transform-action]')?.dataset.transformAction;
1221
+ if (!action)
1222
+ return;
1223
+ const transformOperations = currentState?.transformOperations;
1224
+ if (!transformOperations || !isTransformActionEnabled(action, transformOperations))
1225
+ return;
1226
+ callbacks.onTransformAction?.(action);
1227
+ closeTransformActionPopover();
846
1228
  });
847
1229
  assetPanel.addEventListener('click', (event) => {
848
1230
  const target = event.target instanceof HTMLElement ? event.target : null;
@@ -930,6 +1312,21 @@ export function createLocalEditorBrowserUi(options = {}) {
930
1312
  if (currentState?.mode !== 'editor' || currentState.busy)
931
1313
  return;
932
1314
  const key = event.key.toLowerCase();
1315
+ if (key === 'escape' && snapSettingsOpen) {
1316
+ event.preventDefault();
1317
+ closeSnapSettingsPopover();
1318
+ return;
1319
+ }
1320
+ if (key === 'escape' && placementSettingsOpen) {
1321
+ event.preventDefault();
1322
+ closePlacementSettingsPopover();
1323
+ return;
1324
+ }
1325
+ if (key === 'escape' && transformActionOpen) {
1326
+ event.preventDefault();
1327
+ closeTransformActionPopover();
1328
+ return;
1329
+ }
933
1330
  const primaryModifier = event.metaKey || event.ctrlKey;
934
1331
  const handleDocumentShortcut = inputRouter.shouldHandleDocumentShortcut(event);
935
1332
  const handleGlobalShortcut = inputRouter.shouldHandleGlobalShortcut(event);
@@ -975,7 +1372,10 @@ export function createLocalEditorBrowserUi(options = {}) {
975
1372
  }
976
1373
  if (key === 'escape') {
977
1374
  event.preventDefault();
978
- callbacks.onCancelActiveOperation?.();
1375
+ if (callbacks.onCancelEditorIntent)
1376
+ callbacks.onCancelEditorIntent();
1377
+ else
1378
+ callbacks.onCancelActiveOperation?.();
979
1379
  }
980
1380
  };
981
1381
  doc.addEventListener('keydown', onKeyDown);
@@ -1028,33 +1428,6 @@ export function createLocalEditorBrowserUi(options = {}) {
1028
1428
  ? CSS.escape(value)
1029
1429
  : value.replace(/["\\]/g, '\\$&');
1030
1430
  }
1031
- function renderTransformHandleBadges(transformTool) {
1032
- LocalEditorShared.clearElement(handleGroup);
1033
- const tool = transformTool?.activeTool ?? 'select';
1034
- const descriptor = DEFAULT_EDITOR_TRANSFORM_TOOL_DESCRIPTORS[tool];
1035
- handleGroup.style.display = descriptor.handles.length > 0 ? 'flex' : 'none';
1036
- for (const handle of descriptor.handles) {
1037
- const badge = doc.createElement('span');
1038
- badge.dataset.transformHandle = handle.constraint;
1039
- badge.title = handle.description;
1040
- badge.textContent = handle.label;
1041
- badge.style.cssText = [
1042
- 'height:22px',
1043
- 'display:inline-flex',
1044
- 'align-items:center',
1045
- 'padding:0 7px',
1046
- 'border:1px solid var(--fps-editor-border)',
1047
- 'border-radius:3px',
1048
- 'background:var(--fps-editor-field)',
1049
- 'color:var(--fps-editor-muted-strong)',
1050
- 'font-size:11px',
1051
- 'font-weight:800',
1052
- 'white-space:nowrap',
1053
- 'pointer-events:none',
1054
- ].join(';');
1055
- handleGroup.appendChild(badge);
1056
- }
1057
- }
1058
1431
  function formatSnapStepInputValue(snap, kind) {
1059
1432
  const value = kind === 'move'
1060
1433
  ? snap.moveStep
@@ -1077,7 +1450,7 @@ export function createLocalEditorBrowserUi(options = {}) {
1077
1450
  contextMenu.close();
1078
1451
  hostChrome.style.display = !inEditor && localTestActionsEnabled ? 'flex' : 'none';
1079
1452
  enterEditorButton.disabled = disabled;
1080
- for (const button of [saveButton, saveAndRunButton, discardRunButton, undoButton, redoButton, sceneHelpButton, sceneCameraButton]) {
1453
+ for (const button of [saveButton, saveAndRunButton, discardRunButton, undoButton, redoButton, sceneHelpButton, sceneCameraButton, gridToggleButton]) {
1081
1454
  button.style.display = 'inline-flex';
1082
1455
  button.disabled = disabled;
1083
1456
  }
@@ -1091,13 +1464,24 @@ export function createLocalEditorBrowserUi(options = {}) {
1091
1464
  const transformTool = state.transformTool ?? null;
1092
1465
  workbench.root.style.display = inEditor ? '' : 'none';
1093
1466
  sceneToolOverlay.style.display = inEditor ? 'flex' : 'none';
1467
+ if (!inEditor)
1468
+ closeSnapSettingsPopover();
1469
+ if (!inEditor)
1470
+ closePlacementSettingsPopover();
1471
+ if (!inEditor)
1472
+ closeTransformActionPopover();
1094
1473
  const sceneCameraPreview = state.sceneCameraPreview ?? { enabled: false, available: false };
1095
1474
  cameraPreviewGroup.style.display = inEditor ? 'flex' : 'none';
1096
1475
  sceneCameraButton.disabled = disabled || !sceneCameraPreview.available;
1097
- sceneCameraButton.title = sceneCameraPreview.available
1476
+ const sceneCameraTooltip = sceneCameraPreview.available
1098
1477
  ? '从 Main Camera 查看当前场景'
1099
1478
  : '当前场景没有可预览的 Main Camera';
1479
+ setToolbarButtonTooltip(sceneCameraButton, sceneCameraTooltip);
1100
1480
  LocalEditorShared.applyButtonActiveState(sceneCameraButton, sceneCameraPreview.enabled);
1481
+ const gridState = state.grid ?? { visible: false, available: false };
1482
+ gridToggleButton.disabled = disabled || !gridState.available;
1483
+ setToolbarButtonTooltip(gridToggleButton, gridState.visible ? '隐藏 Scene View 网格' : '显示 Scene View 网格');
1484
+ LocalEditorShared.applyButtonActiveState(gridToggleButton, gridState.visible);
1101
1485
  for (const [tool, button] of toolButtons) {
1102
1486
  button.disabled = disabled;
1103
1487
  LocalEditorShared.applyButtonActiveState(button, transformTool?.activeTool === tool);
@@ -1107,7 +1491,6 @@ export function createLocalEditorBrowserUi(options = {}) {
1107
1491
  LocalEditorShared.applyButtonActiveState(button, transformTool?.activeSpace === space);
1108
1492
  }
1109
1493
  sceneHelpButton.disabled = disabled;
1110
- renderTransformHandleBadges(transformTool);
1111
1494
  const transformOperations = state.transformOperations ?? {
1112
1495
  settings: DEFAULT_EDITOR_TRANSFORM_OPERATION_SETTINGS,
1113
1496
  selectedCount: state.selectedIds.length,
@@ -1118,53 +1501,73 @@ export function createLocalEditorBrowserUi(options = {}) {
1118
1501
  const operationSettings = transformOperations.settings;
1119
1502
  snapGroup.style.display = inEditor ? 'flex' : 'none';
1120
1503
  snapButton.disabled = disabled;
1121
- LocalEditorShared.applyButtonActiveState(snapButton, operationSettings.snap.enabled);
1504
+ snapEnabledInput.disabled = disabled;
1505
+ snapEnabledInput.checked = operationSettings.snap.enabled;
1506
+ LocalEditorShared.applyButtonActiveState(snapButton, operationSettings.snap.enabled || snapSettingsOpen);
1507
+ setToolbarButtonTooltip(snapButton, `吸附设置 · 位移 ${formatSnapStepInputValue(operationSettings.snap, 'move')} · 旋转 ${formatSnapStepInputValue(operationSettings.snap, 'rotate')}° · 缩放 ${formatSnapStepInputValue(operationSettings.snap, 'scale')}`);
1122
1508
  for (const [kind, input] of snapStepInputs) {
1123
1509
  input.disabled = disabled;
1124
1510
  if (doc.activeElement !== input)
1125
1511
  input.value = formatSnapStepInputValue(operationSettings.snap, kind);
1126
1512
  }
1513
+ if (!inEditor || disabled || snapGroup.style.display === 'none')
1514
+ closeSnapSettingsPopover();
1127
1515
  placementGroup.style.display = inEditor ? 'flex' : 'none';
1516
+ placementButton.disabled = disabled;
1517
+ setToolbarButtonIcon(doc, placementButton, PLACEMENT_MODE_ICONS[operationSettings.placementMode]);
1518
+ setToolbarButtonTooltip(placementButton, `放置模式 · ${PLACEMENT_MODE_DESCRIPTIONS[operationSettings.placementMode]}`);
1519
+ LocalEditorShared.applyButtonActiveState(placementButton, operationSettings.placementMode !== 'off' || placementSettingsOpen);
1128
1520
  for (const [mode, button] of Object.entries(placementButtons)) {
1129
1521
  button.disabled = disabled;
1130
1522
  LocalEditorShared.applyButtonActiveState(button, operationSettings.placementMode === mode);
1131
1523
  }
1524
+ if (!inEditor || disabled || placementGroup.style.display === 'none')
1525
+ closePlacementSettingsPopover();
1132
1526
  actionGroup.style.display = inEditor ? 'flex' : 'none';
1133
- transformActionSelect.disabled = disabled;
1134
- transformActionButton.disabled = disabled || !isTransformActionEnabled(selectedTransformAction, transformOperations);
1135
- transformActionButton.title = selectedTransformAction.startsWith('align-')
1136
- ? '需要至少 2 个选中对象,并以 active object 为目标'
1137
- : '需要至少 3 个选中对象';
1527
+ const hasTransformAction = transformOperations.canAlign || transformOperations.canDistribute;
1528
+ transformActionButton.disabled = disabled || !hasTransformAction;
1529
+ setToolbarButtonTooltip(transformActionButton, hasTransformAction ? '对齐与分布' : '选择至少 2 个对象后可用');
1530
+ LocalEditorShared.applyButtonActiveState(transformActionButton, transformActionOpen);
1531
+ for (const [action, button] of transformActionButtons) {
1532
+ const enabled = isTransformActionEnabled(action, transformOperations);
1533
+ button.disabled = disabled || !enabled;
1534
+ button.style.opacity = button.disabled ? '0.45' : '1';
1535
+ button.style.cursor = button.disabled ? 'not-allowed' : 'pointer';
1536
+ }
1537
+ if (!inEditor || disabled || !hasTransformAction || actionGroup.style.display === 'none')
1538
+ closeTransformActionPopover();
1138
1539
  undoButton.disabled = disabled || !state.session?.canUndo;
1139
1540
  redoButton.disabled = disabled || !state.session?.canRedo;
1140
1541
  LocalEditorShared.applyButtonActiveState(sceneHelpButton, inEditor && helpOpen);
1141
1542
  shortcutHelpPanel.style.display = inEditor && helpOpen ? '' : 'none';
1142
1543
  dirtyBadge.style.display = inEditor && state.session?.dirty ? 'inline-flex' : 'none';
1143
- const dragSuffix = transformTool?.dragPhase === 'dragging'
1144
- ? ` | dragging ${transformTool.draggingNodeId ?? 'selection'}`
1145
- : '';
1146
- status.textContent = [
1544
+ const editorStatusText = [
1147
1545
  state.status,
1148
- state.session?.dirty ? '*' : '',
1546
+ state.session?.dirty ? '有未保存更改' : '',
1149
1547
  LocalEditorShared.formatAuthoringSourceLabel(state.session?.source),
1150
1548
  state.summary,
1151
1549
  ].filter(Boolean).join(' | ');
1152
- status.textContent += dragSuffix;
1153
- status.title = state.statusDetails || status.textContent;
1154
- status.style.color = state.statusTone === 'error'
1550
+ const transformStatusText = transformTool?.dragPhase === 'dragging'
1551
+ ? `正在拖拽 ${transformTool.draggingNodeId ?? '选择对象'}`
1552
+ : LocalEditorShared.toTransformToolStatusLabel(transformTool?.activeTool ?? 'select', transformTool?.activeSpace ?? 'world');
1553
+ const statusToneColor = state.statusTone === 'error'
1155
1554
  ? 'var(--fps-editor-danger-strong)'
1156
1555
  : state.statusTone === 'warning'
1157
1556
  ? 'var(--fps-editor-warn)'
1158
1557
  : state.statusTone === 'success'
1159
1558
  ? 'var(--fps-editor-success)'
1160
1559
  : 'var(--fps-editor-muted)';
1161
- sceneToolStatus.textContent = transformTool?.dragPhase === 'dragging'
1162
- ? `正在拖拽 ${transformTool.draggingNodeId ?? '选择对象'}`
1163
- : LocalEditorShared.toTransformToolStatusLabel(transformTool?.activeTool ?? 'select', transformTool?.activeSpace ?? 'world');
1164
- sceneMouseHint.textContent = [
1165
- LocalEditorShared.toTransformMouseHint(transformTool?.activeTool ?? 'select'),
1560
+ setToolbarButtonTooltip(editorStatusButton, [
1561
+ `编辑器状态:${editorStatusText}`,
1562
+ state.statusDetails && state.statusDetails !== state.status ? state.statusDetails : '',
1563
+ transformStatusText,
1166
1564
  LocalEditorShared.toTransformOperationStatusLabel(operationSettings),
1167
- ].join(' · ');
1565
+ ].filter(Boolean).join(' · '));
1566
+ editorStatusButton.style.color = statusToneColor;
1567
+ editorStatusButton.style.borderColor = state.statusTone == null || state.statusTone === 'default'
1568
+ ? 'var(--fps-editor-border)'
1569
+ : statusToneColor;
1570
+ renderCoordinateAxesOverlay(coordinateAxesOverlay, inEditor ? state.coordinateAxes ?? null : null);
1168
1571
  const boxSelection = state.boxSelection;
1169
1572
  if (inEditor && boxSelection?.active) {
1170
1573
  boxSelectionOverlay.style.display = '';
@@ -1212,10 +1615,14 @@ export function createLocalEditorBrowserUi(options = {}) {
1212
1615
  workbench.root.remove();
1213
1616
  localTestMenu.remove();
1214
1617
  toolbarOverflowMenu.remove();
1618
+ snapSettingsPopover.remove();
1619
+ placementSettingsPopover.remove();
1620
+ transformActionPopover.remove();
1215
1621
  boxSelectionOverlay.remove();
1216
1622
  shortcutHelpPanel.remove();
1217
1623
  hierarchyController.dispose();
1218
1624
  workbenchLayoutController.dispose();
1625
+ tooltipController.dispose();
1219
1626
  contextMenu.dispose();
1220
1627
  inputRouter.dispose();
1221
1628
  doc.removeEventListener('keydown', onKeyDown);