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

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 (64) 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 +6 -2
  5. package/dist/local-editor-harness.d.ts.map +1 -1
  6. package/dist/local-editor-harness.js +80 -0
  7. package/dist/local-editor-harness.js.map +1 -1
  8. package/node_modules/@fps-games/editor-babylon/package.json +4 -4
  9. package/node_modules/@fps-games/editor-browser/dist/index.d.ts +2 -0
  10. package/node_modules/@fps-games/editor-browser/dist/index.d.ts.map +1 -1
  11. package/node_modules/@fps-games/editor-browser/dist/index.js +1 -0
  12. package/node_modules/@fps-games/editor-browser/dist/index.js.map +1 -1
  13. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-context-menu.d.ts +3 -1
  14. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-context-menu.d.ts.map +1 -1
  15. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-context-menu.js +7 -3
  16. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-context-menu.js.map +1 -1
  17. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.d.ts +10 -0
  18. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.d.ts.map +1 -1
  19. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.js +142 -3
  20. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.js.map +1 -1
  21. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.d.ts +1 -0
  22. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.d.ts.map +1 -1
  23. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.js +82 -14
  24. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.js.map +1 -1
  25. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-view.d.ts.map +1 -1
  26. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-view.js +23 -20
  27. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-view.js.map +1 -1
  28. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-icons.d.ts +10 -0
  29. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-icons.d.ts.map +1 -0
  30. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-icons.js +195 -0
  31. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-icons.js.map +1 -0
  32. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.d.ts.map +1 -1
  33. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.js +52 -64
  34. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.js.map +1 -1
  35. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-primitives.d.ts +13 -3
  36. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-primitives.d.ts.map +1 -1
  37. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-primitives.js +107 -14
  38. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-primitives.js.map +1 -1
  39. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.d.ts +6 -2
  40. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.d.ts.map +1 -1
  41. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.js +28 -7
  42. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.js.map +1 -1
  43. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.d.ts.map +1 -1
  44. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.js +5 -2
  45. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.js.map +1 -1
  46. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-theme.d.ts +4 -0
  47. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-theme.d.ts.map +1 -1
  48. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-theme.js +100 -9
  49. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-theme.js.map +1 -1
  50. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts +24 -0
  51. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts.map +1 -1
  52. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.d.ts +35 -0
  53. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.d.ts.map +1 -1
  54. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.js +376 -2
  55. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.js.map +1 -1
  56. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.d.ts +8 -4
  57. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.d.ts.map +1 -1
  58. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js +421 -42
  59. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js.map +1 -1
  60. package/node_modules/@fps-games/editor-browser/package.json +2 -2
  61. package/node_modules/@fps-games/editor-core/package.json +2 -2
  62. package/node_modules/@fps-games/editor-forge-play/package.json +2 -2
  63. package/node_modules/@fps-games/editor-protocol/package.json +1 -1
  64. package/package.json +6 -6
@@ -2,7 +2,7 @@ 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
4
  import { DEFAULT_EDITOR_TRANSFORM_OPERATION_SETTINGS, DEFAULT_EDITOR_TRANSFORM_TOOL_DESCRIPTORS, } from '@fps-games/editor-core';
5
- export { createLocalEditorHierarchyBlankMenu, createLocalEditorHierarchyDeleteShortcutAction, createLocalEditorHierarchyNodeMenu, } from './local-editor-ui-hierarchy-actions';
5
+ export { createLocalEditorHierarchyBlankMenu, createLocalEditorHierarchyCopyShortcutAction, createLocalEditorHierarchyDeleteShortcutAction, createLocalEditorHierarchyDuplicateShortcutAction, createLocalEditorHierarchyNodeMenu, createLocalEditorHierarchyPasteShortcutAction, } from './local-editor-ui-hierarchy-actions';
6
6
  export { createLocalEditorHierarchyController, } from './local-editor-ui-hierarchy-controller';
7
7
  export { canLocalEditorHierarchyNodeHaveChildren, createLocalEditorHierarchyTreeModel, isLocalEditorHierarchyNodeMovable, } from './local-editor-ui-hierarchy-tree';
8
8
  export { createLocalEditorContextMenuController, } from './local-editor-ui-context-menu';
@@ -10,9 +10,11 @@ import { createLocalEditorWorkbenchInputRouter } from './local-editor-ui-input-r
10
10
  import { createLocalEditorPanelRegistry } from './local-editor-ui-panel-registry';
11
11
  import * as LocalEditorShared from './local-editor-ui-shared';
12
12
  import { createShortcutHelpPanel as createLocalEditorShortcutHelpPanel } from './local-editor-ui-shortcuts';
13
- import { ensureLocalEditorTheme, LOCAL_EDITOR_THEME_CLASS } from './local-editor-ui-theme';
14
- import { createDefaultLocalEditorWorkbenchLayout, createLocalEditorWorkbench, createSceneHeaderToolbar, createWorkbenchPanelContent, } from './local-editor-ui-workbench';
13
+ import { applyLocalEditorTheme, ensureLocalEditorTheme, LOCAL_EDITOR_THEME_CLASS, normalizeLocalEditorThemeName, } from './local-editor-ui-theme';
14
+ import { createDefaultLocalEditorWorkbenchLayout, createLocalEditorWorkbenchLayoutController, createLocalEditorWorkbench, createSceneHeaderToolbar, createWorkbenchPanelContent, } from './local-editor-ui-workbench';
15
15
  export { applyLocalEditorBrowserInspectorControlBinding, createLocalEditorBrowserInspectorControlRegistry, formatLocalEditorBrowserInspectorValue, resolveLocalEditorBrowserInspectorControlRegistration, } from './local-editor-ui-panels';
16
+ export { DEFAULT_LOCAL_EDITOR_THEME, LOCAL_EDITOR_THEME_CLASS, applyLocalEditorTheme, normalizeLocalEditorThemeName, } from './local-editor-ui-theme';
17
+ export { LOCAL_EDITOR_ICON_NAMES, createLocalEditorIcon, isLocalEditorIconName, resolveLocalEditorIconName, } from './local-editor-ui-icons';
16
18
  function readInspectorInputValue(input) {
17
19
  const control = input.dataset.serializedControl;
18
20
  const valueType = input.dataset.serializedValueType;
@@ -50,6 +52,21 @@ function readInspectorInputValue(input) {
50
52
  }
51
53
  return input.value;
52
54
  }
55
+ const TRANSFORM_TOOL_ICONS = {
56
+ select: 'select',
57
+ move: 'move',
58
+ rotate: 'rotate',
59
+ scale: 'scale',
60
+ };
61
+ const TRANSFORM_SPACE_ICONS = {
62
+ world: 'world',
63
+ local: 'local',
64
+ };
65
+ const PLACEMENT_MODE_ICONS = {
66
+ off: 'place-off',
67
+ ground: 'place-ground',
68
+ surface: 'place-surface',
69
+ };
53
70
  function readInspectorVectorInputValue(input) {
54
71
  const wrapper = input.closest('[data-inspector-vector-control]');
55
72
  const values = {};
@@ -99,11 +116,13 @@ export function createLocalEditorBrowserUi(options = {}) {
99
116
  const doc = options.document ?? document;
100
117
  const root = options.root ?? doc.body;
101
118
  const callbacks = options.callbacks ?? {};
119
+ const localTestActionsEnabled = options.localTestActions === true;
102
120
  ensureLocalEditorTheme(doc);
121
+ let activeTheme = normalizeLocalEditorThemeName(options.theme);
103
122
  const inputRouter = createLocalEditorWorkbenchInputRouter(doc);
104
123
  const contextMenu = createLocalEditorContextMenuController(doc, (open) => {
105
124
  inputRouter.setContextMenuOpen(open);
106
- });
125
+ }, activeTheme);
107
126
  let currentState = null;
108
127
  let inspectorFilter = '';
109
128
  const workbenchLayout = createDefaultLocalEditorWorkbenchLayout();
@@ -125,7 +144,7 @@ export function createLocalEditorBrowserUi(options = {}) {
125
144
  'border:1px solid var(--fps-editor-border)',
126
145
  'border-radius:3px',
127
146
  'background:var(--fps-editor-chrome)',
128
- 'box-shadow:0 10px 28px rgba(0,0,0,0.24)',
147
+ 'box-shadow:var(--fps-editor-shadow-panel)',
129
148
  'font-family:var(--fps-editor-font)',
130
149
  'color:var(--fps-editor-text)',
131
150
  'font-size:12px',
@@ -138,7 +157,7 @@ export function createLocalEditorBrowserUi(options = {}) {
138
157
  'font-weight:800',
139
158
  'white-space:nowrap',
140
159
  ].join(';');
141
- const enterEditorButton = LocalEditorShared.createButton(doc, '进入编辑场景');
160
+ const enterEditorButton = LocalEditorShared.createButton(doc, '进入编辑场景', { icon: 'execute' });
142
161
  hostChrome.appendChild(hostModeLabel);
143
162
  hostChrome.appendChild(enterEditorButton);
144
163
  root.appendChild(hostChrome);
@@ -149,18 +168,50 @@ export function createLocalEditorBrowserUi(options = {}) {
149
168
  'align-items:center',
150
169
  'padding:0 7px',
151
170
  'border-radius:999px',
152
- 'background:rgba(248,81,73,0.16)',
153
- 'border:1px solid rgba(248,81,73,0.45)',
154
- 'color:#ffd7d5',
171
+ 'background:var(--fps-editor-danger-soft)',
172
+ 'border:1px solid var(--fps-editor-danger-border)',
173
+ 'color:var(--fps-editor-danger-text)',
155
174
  'font-size:11px',
156
175
  'font-weight:800',
157
176
  ].join(';');
158
177
  dirtyBadge.textContent = '未保存';
159
- const saveButton = LocalEditorShared.createButton(doc, '保存场景');
160
- const saveAndRunButton = LocalEditorShared.createButton(doc, '保存并运行');
161
- const discardRunButton = LocalEditorShared.createButton(doc, '放弃并运行');
162
- const undoButton = LocalEditorShared.createButton(doc, '撤销');
163
- const redoButton = LocalEditorShared.createButton(doc, '重做');
178
+ const saveButton = LocalEditorShared.createButton(doc, '保存场景', { icon: 'save' });
179
+ const saveAndRunButton = LocalEditorShared.createButton(doc, '保存并运行', { icon: 'execute' });
180
+ const discardRunButton = LocalEditorShared.createButton(doc, '放弃并运行', { icon: 'discard' });
181
+ const localTestButton = LocalEditorShared.createButton(doc, '本地测试', { icon: 'execute' });
182
+ localTestButton.dataset.editorLocalTestToggle = 'true';
183
+ localTestButton.title = '打开本地测试操作';
184
+ const localTestMenu = doc.createElement('div');
185
+ localTestMenu.classList.add(LOCAL_EDITOR_THEME_CLASS);
186
+ localTestMenu.dataset.editorLocalTestMenu = 'true';
187
+ localTestMenu.style.cssText = [
188
+ 'position:fixed',
189
+ 'z-index:2147483641',
190
+ 'display:none',
191
+ 'flex-direction:column',
192
+ 'gap:6px',
193
+ 'min-width:150px',
194
+ 'padding:8px',
195
+ 'border:1px solid var(--fps-editor-border)',
196
+ 'border-radius:3px',
197
+ 'background:var(--fps-editor-panel)',
198
+ 'box-shadow:var(--fps-editor-shadow-popover)',
199
+ 'pointer-events:auto',
200
+ ].join(';');
201
+ for (const button of [saveButton, saveAndRunButton, discardRunButton]) {
202
+ button.style.justifyContent = 'flex-start';
203
+ button.style.width = '100%';
204
+ localTestMenu.appendChild(button);
205
+ }
206
+ const localTestGroup = doc.createElement('div');
207
+ localTestGroup.style.cssText = [
208
+ `display:${localTestActionsEnabled ? 'flex' : 'none'}`,
209
+ 'align-items:center',
210
+ 'gap:4px',
211
+ ].join(';');
212
+ localTestGroup.appendChild(localTestButton);
213
+ const undoButton = LocalEditorShared.createButton(doc, '撤销', { icon: 'undo' });
214
+ const redoButton = LocalEditorShared.createButton(doc, '重做', { icon: 'redo' });
164
215
  let helpOpen = false;
165
216
  const status = doc.createElement('span');
166
217
  status.style.cssText = [
@@ -180,6 +231,7 @@ export function createLocalEditorBrowserUi(options = {}) {
180
231
  workbench.leftDock.appendChild(hierarchyPanel);
181
232
  workbench.rightDock.appendChild(inspectorPanel);
182
233
  root.appendChild(workbench.root);
234
+ const workbenchLayoutController = createLocalEditorWorkbenchLayoutController(doc, workbench);
183
235
  const sceneToolOverlay = createSceneHeaderToolbar(doc);
184
236
  const sceneTitle = doc.createElement('div');
185
237
  sceneTitle.textContent = 'Preview';
@@ -197,6 +249,14 @@ export function createLocalEditorBrowserUi(options = {}) {
197
249
  ].join(';');
198
250
  const sceneQuickActions = doc.createElement('div');
199
251
  sceneQuickActions.style.cssText = 'display:flex;align-items:center;gap:4px';
252
+ const sceneUtilityActions = doc.createElement('div');
253
+ sceneUtilityActions.style.cssText = [
254
+ 'display:flex',
255
+ 'align-items:center',
256
+ 'gap:4px',
257
+ 'padding-left:8px',
258
+ 'border-left:1px solid var(--fps-editor-divider)',
259
+ ].join(';');
200
260
  const toolGroup = doc.createElement('div');
201
261
  toolGroup.style.cssText = [
202
262
  'display:flex',
@@ -209,7 +269,9 @@ export function createLocalEditorBrowserUi(options = {}) {
209
269
  const transformToolDescriptors = Object.values(DEFAULT_EDITOR_TRANSFORM_TOOL_DESCRIPTORS);
210
270
  for (const descriptor of transformToolDescriptors) {
211
271
  const shortcut = descriptor.shortcut ? `${descriptor.shortcut} ` : '';
212
- const button = LocalEditorShared.createButton(doc, `${shortcut}${descriptor.label}`);
272
+ const button = LocalEditorShared.createButton(doc, `${shortcut}${descriptor.label}`, {
273
+ icon: TRANSFORM_TOOL_ICONS[descriptor.tool],
274
+ });
213
275
  button.dataset.transformTool = descriptor.tool;
214
276
  button.title = descriptor.handles.length > 0
215
277
  ? `${descriptor.label} · ${descriptor.handles.map(handle => handle.label).join(' / ')}`
@@ -226,8 +288,8 @@ export function createLocalEditorBrowserUi(options = {}) {
226
288
  'border-left:1px solid var(--fps-editor-divider)',
227
289
  ].join(';');
228
290
  const spaceButtons = {
229
- world: LocalEditorShared.createButton(doc, '世界'),
230
- local: LocalEditorShared.createButton(doc, '本地'),
291
+ world: LocalEditorShared.createButton(doc, '世界', { icon: TRANSFORM_SPACE_ICONS.world }),
292
+ local: LocalEditorShared.createButton(doc, '本地', { icon: TRANSFORM_SPACE_ICONS.local }),
231
293
  };
232
294
  for (const [space, button] of Object.entries(spaceButtons)) {
233
295
  button.dataset.transformSpace = space;
@@ -249,7 +311,7 @@ export function createLocalEditorBrowserUi(options = {}) {
249
311
  'padding-left:8px',
250
312
  'border-left:1px solid var(--fps-editor-divider)',
251
313
  ].join(';');
252
- const snapButton = LocalEditorShared.createButton(doc, '吸附');
314
+ const snapButton = LocalEditorShared.createButton(doc, '吸附', { icon: 'snap' });
253
315
  snapButton.dataset.transformSnapToggle = 'true';
254
316
  snapButton.title = '移动 / 旋转 / 缩放吸附';
255
317
  const snapStepInputs = new Map();
@@ -293,9 +355,9 @@ export function createLocalEditorBrowserUi(options = {}) {
293
355
  placementLabel.textContent = '放置';
294
356
  placementLabel.style.cssText = 'color:var(--fps-editor-muted);font-size:11px;font-weight:900;white-space:nowrap';
295
357
  const placementButtons = {
296
- off: LocalEditorShared.createButton(doc, '关'),
297
- ground: LocalEditorShared.createButton(doc, '地'),
298
- surface: LocalEditorShared.createButton(doc, '表'),
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 }),
299
361
  };
300
362
  placementButtons.off.title = '关闭放置模式';
301
363
  placementButtons.ground.title = '放置到 XZ 地面';
@@ -342,7 +404,7 @@ export function createLocalEditorBrowserUi(options = {}) {
342
404
  transformActionSelect.appendChild(option);
343
405
  }
344
406
  transformActionSelect.value = selectedTransformAction;
345
- const transformActionButton = LocalEditorShared.createButton(doc, '执行');
407
+ const transformActionButton = LocalEditorShared.createButton(doc, '执行', { icon: 'execute' });
346
408
  transformActionButton.dataset.transformActionRun = 'true';
347
409
  transformActionButton.title = '对当前多选执行 Transform 操作';
348
410
  actionGroup.appendChild(transformActionSelect);
@@ -368,15 +430,17 @@ export function createLocalEditorBrowserUi(options = {}) {
368
430
  'font-size:11px',
369
431
  ].join(';');
370
432
  sceneMouseHint.textContent = '左键选择 · 空白拖拽框选 · 中键平移 · Alt+左键环绕 · 右键飞行 · 滚轮缩放';
371
- const sceneHelpButton = LocalEditorShared.createButton(doc, '快捷键');
433
+ const themeToggleButton = LocalEditorShared.createButton(doc, '主题', { icon: 'theme' });
434
+ themeToggleButton.dataset.editorThemeToggle = 'true';
435
+ themeToggleButton.style.padding = '5px 7px';
436
+ const sceneHelpButton = LocalEditorShared.createButton(doc, '快捷键', { icon: 'help' });
372
437
  sceneHelpButton.style.padding = '5px 7px';
373
- sceneQuickActions.appendChild(saveButton);
374
- sceneQuickActions.appendChild(saveAndRunButton);
375
- sceneQuickActions.appendChild(discardRunButton);
438
+ sceneQuickActions.appendChild(localTestGroup);
376
439
  sceneQuickActions.appendChild(undoButton);
377
440
  sceneQuickActions.appendChild(redoButton);
378
441
  sceneQuickActions.appendChild(dirtyBadge);
379
- sceneQuickActions.appendChild(sceneHelpButton);
442
+ sceneUtilityActions.appendChild(themeToggleButton);
443
+ sceneUtilityActions.appendChild(sceneHelpButton);
380
444
  const cameraPreviewGroup = doc.createElement('div');
381
445
  cameraPreviewGroup.style.cssText = [
382
446
  'display:flex',
@@ -385,12 +449,37 @@ export function createLocalEditorBrowserUi(options = {}) {
385
449
  'padding-left:8px',
386
450
  'border-left:1px solid var(--fps-editor-divider)',
387
451
  ].join(';');
388
- const sceneCameraButton = LocalEditorShared.createButton(doc, 'Scene Camera');
452
+ const sceneCameraButton = LocalEditorShared.createButton(doc, 'Scene Camera', { icon: 'camera' });
389
453
  sceneCameraButton.dataset.sceneCameraPreviewToggle = 'true';
390
454
  sceneCameraButton.title = '从 Main Camera 查看当前场景';
391
455
  cameraPreviewGroup.appendChild(sceneCameraButton);
456
+ const toolbarOverflowButton = LocalEditorShared.createButton(doc, '更多', { icon: 'chevron-down' });
457
+ toolbarOverflowButton.dataset.editorToolbarOverflowToggle = 'true';
458
+ toolbarOverflowButton.title = '显示隐藏的工具栏命令';
459
+ toolbarOverflowButton.style.display = 'none';
460
+ const toolbarOverflowMenu = doc.createElement('div');
461
+ toolbarOverflowMenu.classList.add(LOCAL_EDITOR_THEME_CLASS);
462
+ toolbarOverflowMenu.dataset.editorToolbarOverflowMenu = 'true';
463
+ toolbarOverflowMenu.style.cssText = [
464
+ 'position:fixed',
465
+ 'z-index:2147483641',
466
+ 'display:none',
467
+ 'flex-direction:column',
468
+ 'gap:6px',
469
+ 'min-width:220px',
470
+ 'max-width:min(420px, calc(100vw - 16px))',
471
+ 'max-height:calc(100vh - 72px)',
472
+ 'overflow:auto',
473
+ 'padding:8px',
474
+ 'border:1px solid var(--fps-editor-border)',
475
+ 'border-radius:3px',
476
+ 'background:var(--fps-editor-panel)',
477
+ 'box-shadow:var(--fps-editor-shadow-popover)',
478
+ 'pointer-events:auto',
479
+ ].join(';');
392
480
  sceneToolOverlay.appendChild(sceneTitle);
393
481
  sceneToolOverlay.appendChild(sceneQuickActions);
482
+ sceneToolOverlay.appendChild(sceneUtilityActions);
394
483
  sceneToolOverlay.appendChild(cameraPreviewGroup);
395
484
  sceneToolOverlay.appendChild(toolGroup);
396
485
  sceneToolOverlay.appendChild(spaceGroup);
@@ -401,25 +490,290 @@ export function createLocalEditorBrowserUi(options = {}) {
401
490
  sceneToolOverlay.appendChild(sceneToolStatus);
402
491
  sceneToolOverlay.appendChild(sceneMouseHint);
403
492
  sceneToolOverlay.appendChild(status);
493
+ sceneToolOverlay.appendChild(toolbarOverflowButton);
404
494
  workbench.sceneHeader.appendChild(sceneToolOverlay);
495
+ root.appendChild(localTestMenu);
496
+ root.appendChild(toolbarOverflowMenu);
405
497
  const boxSelectionOverlay = doc.createElement('div');
498
+ boxSelectionOverlay.classList.add(LOCAL_EDITOR_THEME_CLASS);
406
499
  boxSelectionOverlay.style.cssText = [
407
500
  'position:fixed',
408
501
  'z-index:2147483637',
409
502
  'display:none',
410
- 'border:1px solid rgba(88,166,255,0.95)',
411
- 'background:rgba(56,139,253,0.18)',
412
- 'box-shadow:0 0 0 1px rgba(10,15,23,0.55) inset',
503
+ 'border:1px solid var(--fps-editor-drop-target)',
504
+ 'background:var(--fps-editor-box-select-bg)',
505
+ 'box-shadow:var(--fps-editor-box-select-shadow)',
413
506
  'pointer-events:none',
414
507
  ].join(';');
415
508
  root.appendChild(boxSelectionOverlay);
416
509
  const shortcutHelpPanel = createLocalEditorShortcutHelpPanel(doc);
417
510
  shortcutHelpPanel.classList.add(LOCAL_EDITOR_THEME_CLASS);
418
511
  root.appendChild(shortcutHelpPanel);
512
+ function applyThemeToSurfaces() {
513
+ applyLocalEditorTheme(hostChrome, activeTheme);
514
+ applyLocalEditorTheme(workbench.root, activeTheme);
515
+ applyLocalEditorTheme(shortcutHelpPanel, activeTheme);
516
+ applyLocalEditorTheme(boxSelectionOverlay, activeTheme);
517
+ applyLocalEditorTheme(localTestMenu, activeTheme);
518
+ applyLocalEditorTheme(toolbarOverflowMenu, activeTheme);
519
+ contextMenu.setTheme?.(activeTheme);
520
+ }
521
+ function updateThemeToggleButton() {
522
+ const nextTheme = activeTheme === 'dark' ? 'light' : 'dark';
523
+ themeToggleButton.dataset.editorTheme = activeTheme;
524
+ themeToggleButton.setAttribute('aria-pressed', activeTheme === 'light' ? 'true' : 'false');
525
+ themeToggleButton.title = nextTheme === 'light' ? '切换为浅色主题' : '切换为深色主题';
526
+ }
527
+ function setActiveTheme(theme) {
528
+ activeTheme = normalizeLocalEditorThemeName(theme);
529
+ applyThemeToSurfaces();
530
+ updateThemeToggleButton();
531
+ }
532
+ let localTestMenuOpen = false;
533
+ function closeLocalTestMenu() {
534
+ localTestMenuOpen = false;
535
+ localTestMenu.style.display = 'none';
536
+ LocalEditorShared.applyButtonActiveState(localTestButton, false);
537
+ }
538
+ function openLocalTestMenu() {
539
+ if (!localTestActionsEnabled || localTestButton.style.display === 'none')
540
+ return;
541
+ closeToolbarOverflowMenu();
542
+ const win = doc.defaultView;
543
+ const rect = localTestButton.getBoundingClientRect();
544
+ const viewportWidth = win?.innerWidth ?? doc.documentElement.clientWidth;
545
+ localTestMenu.style.top = `${Math.max(8, rect.bottom + 5)}px`;
546
+ localTestMenu.style.right = `${Math.max(8, viewportWidth - rect.right)}px`;
547
+ localTestMenu.style.display = 'flex';
548
+ localTestMenuOpen = true;
549
+ LocalEditorShared.applyButtonActiveState(localTestButton, true);
550
+ }
551
+ const onLocalTestButtonClick = (event) => {
552
+ event.stopPropagation();
553
+ if (localTestMenuOpen)
554
+ closeLocalTestMenu();
555
+ else
556
+ openLocalTestMenu();
557
+ };
558
+ const toolbarOverflowStyleProperties = [
559
+ 'alignSelf',
560
+ 'borderLeft',
561
+ 'display',
562
+ 'flex',
563
+ 'height',
564
+ 'maxWidth',
565
+ 'minHeight',
566
+ 'minWidth',
567
+ 'overflow',
568
+ 'paddingLeft',
569
+ 'textOverflow',
570
+ 'whiteSpace',
571
+ 'width',
572
+ ];
573
+ const toolbarButtonOriginalPadding = new WeakMap();
574
+ const toolbarOverflowOriginalStyles = new WeakMap();
575
+ const toolbarOrder = [
576
+ sceneTitle,
577
+ sceneQuickActions,
578
+ sceneUtilityActions,
579
+ cameraPreviewGroup,
580
+ toolGroup,
581
+ spaceGroup,
582
+ handleGroup,
583
+ snapGroup,
584
+ placementGroup,
585
+ actionGroup,
586
+ sceneToolStatus,
587
+ sceneMouseHint,
588
+ status,
589
+ toolbarOverflowButton,
590
+ ];
591
+ 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' },
595
+ { id: 'transform-actions', element: actionGroup, kind: 'group' },
596
+ { id: 'placement', element: placementGroup, kind: 'group' },
597
+ { id: 'snap', element: snapGroup, kind: 'group' },
598
+ { id: 'handles', element: handleGroup, kind: 'group' },
599
+ { id: 'space', element: spaceGroup, kind: 'group' },
600
+ { id: 'camera-preview', element: cameraPreviewGroup, kind: 'group' },
601
+ { id: 'scene-utilities', element: sceneUtilityActions, kind: 'group' },
602
+ ];
603
+ let toolbarOverflowOpen = false;
604
+ let toolbarOverflowRaf = null;
605
+ function restoreToolbarOverflowItemStyle(item) {
606
+ const original = toolbarOverflowOriginalStyles.get(item.element);
607
+ if (!original)
608
+ return;
609
+ for (const property of toolbarOverflowStyleProperties) {
610
+ item.element.style[property] = original[property] ?? '';
611
+ }
612
+ toolbarOverflowOriginalStyles.delete(item.element);
613
+ item.element.dataset.editorToolbarOverflowPlacement = 'toolbar';
614
+ }
615
+ function applyToolbarOverflowMenuStyle(item) {
616
+ if (!toolbarOverflowOriginalStyles.has(item.element)) {
617
+ const original = {};
618
+ for (const property of toolbarOverflowStyleProperties) {
619
+ original[property] = item.element.style[property];
620
+ }
621
+ toolbarOverflowOriginalStyles.set(item.element, original);
622
+ }
623
+ item.element.dataset.editorToolbarOverflowPlacement = 'menu';
624
+ item.element.style.alignSelf = 'stretch';
625
+ item.element.style.borderLeft = '0';
626
+ item.element.style.display = item.kind === 'group' ? 'flex' : 'block';
627
+ item.element.style.flex = '0 0 auto';
628
+ item.element.style.height = '';
629
+ item.element.style.maxWidth = 'none';
630
+ item.element.style.minHeight = '0';
631
+ item.element.style.minWidth = '0';
632
+ item.element.style.overflow = item.kind === 'group' ? 'visible' : 'hidden';
633
+ item.element.style.paddingLeft = '0';
634
+ item.element.style.textOverflow = item.kind === 'text' ? 'ellipsis' : '';
635
+ item.element.style.whiteSpace = item.kind === 'text' ? 'nowrap' : '';
636
+ item.element.style.width = '100%';
637
+ }
638
+ function setToolbarButtonCompact(button, compact) {
639
+ const label = button.querySelector('[data-editor-button-label]');
640
+ if (!label)
641
+ return;
642
+ if (!toolbarButtonOriginalPadding.has(button)) {
643
+ toolbarButtonOriginalPadding.set(button, button.style.padding);
644
+ }
645
+ const accessibleLabel = button.dataset.editorToolbarLabel || label.textContent || button.title;
646
+ button.dataset.editorToolbarLabel = accessibleLabel;
647
+ button.setAttribute('aria-label', accessibleLabel);
648
+ if (!button.title)
649
+ button.title = accessibleLabel;
650
+ button.style.padding = compact ? '4px 6px' : toolbarButtonOriginalPadding.get(button) ?? '';
651
+ label.style.display = compact ? 'inline-block' : '';
652
+ label.style.width = compact ? '0' : '';
653
+ label.style.maxWidth = compact ? '0' : '';
654
+ label.style.opacity = compact ? '0' : '';
655
+ label.style.marginLeft = compact ? '-5px' : '';
656
+ }
657
+ function setToolbarCompact(compact) {
658
+ for (const button of Array.from(sceneToolOverlay.querySelectorAll('button'))) {
659
+ setToolbarButtonCompact(button, compact);
660
+ }
661
+ }
662
+ function closeToolbarOverflowMenu() {
663
+ toolbarOverflowOpen = false;
664
+ toolbarOverflowMenu.style.display = 'none';
665
+ LocalEditorShared.applyButtonActiveState(toolbarOverflowButton, false);
666
+ }
667
+ function openToolbarOverflowMenu() {
668
+ if (toolbarOverflowMenu.childElementCount === 0)
669
+ return;
670
+ closeLocalTestMenu();
671
+ const win = doc.defaultView;
672
+ const rect = toolbarOverflowButton.getBoundingClientRect();
673
+ const viewportWidth = win?.innerWidth ?? doc.documentElement.clientWidth;
674
+ toolbarOverflowMenu.style.top = `${Math.max(8, rect.bottom + 5)}px`;
675
+ toolbarOverflowMenu.style.right = `${Math.max(8, viewportWidth - rect.right)}px`;
676
+ toolbarOverflowMenu.style.display = 'flex';
677
+ toolbarOverflowOpen = true;
678
+ LocalEditorShared.applyButtonActiveState(toolbarOverflowButton, true);
679
+ }
680
+ function resetToolbarOverflowLayout() {
681
+ setToolbarCompact(false);
682
+ for (const item of toolbarOverflowItems)
683
+ restoreToolbarOverflowItemStyle(item);
684
+ for (const item of toolbarOrder) {
685
+ item.dataset.editorToolbarOverflowed = 'false';
686
+ sceneToolOverlay.appendChild(item);
687
+ }
688
+ LocalEditorShared.clearElement(toolbarOverflowMenu);
689
+ toolbarOverflowButton.style.display = 'none';
690
+ sceneToolOverlay.style.overflowX = 'hidden';
691
+ closeToolbarOverflowMenu();
692
+ }
693
+ function layoutToolbarOverflow() {
694
+ toolbarOverflowRaf = null;
695
+ resetToolbarOverflowLayout();
696
+ if (sceneToolOverlay.style.display === 'none')
697
+ return;
698
+ const availableWidth = sceneToolOverlay.clientWidth;
699
+ if (availableWidth <= 0)
700
+ return;
701
+ const isOverflowing = () => sceneToolOverlay.scrollWidth > availableWidth + 1;
702
+ if (!isOverflowing())
703
+ return;
704
+ toolbarOverflowButton.style.display = 'inline-flex';
705
+ for (const item of toolbarOverflowItems) {
706
+ if (!isOverflowing())
707
+ break;
708
+ if (item.element.style.display === 'none')
709
+ continue;
710
+ item.element.dataset.editorToolbarOverflowed = 'true';
711
+ applyToolbarOverflowMenuStyle(item);
712
+ toolbarOverflowMenu.appendChild(item.element);
713
+ }
714
+ if (toolbarOverflowMenu.childElementCount === 0) {
715
+ toolbarOverflowButton.style.display = 'none';
716
+ }
717
+ if (isOverflowing()) {
718
+ setToolbarCompact(true);
719
+ }
720
+ sceneToolOverlay.style.overflowX = isOverflowing() ? 'auto' : 'hidden';
721
+ }
722
+ function scheduleToolbarOverflowLayout() {
723
+ const win = doc.defaultView;
724
+ if (!win) {
725
+ layoutToolbarOverflow();
726
+ return;
727
+ }
728
+ if (toolbarOverflowRaf != null)
729
+ win.cancelAnimationFrame(toolbarOverflowRaf);
730
+ toolbarOverflowRaf = win.requestAnimationFrame(layoutToolbarOverflow);
731
+ }
732
+ const onToolbarOverflowButtonClick = (event) => {
733
+ event.stopPropagation();
734
+ if (toolbarOverflowOpen)
735
+ closeToolbarOverflowMenu();
736
+ else
737
+ openToolbarOverflowMenu();
738
+ };
739
+ const onToolbarOverflowPointerDown = (event) => {
740
+ const target = event.target instanceof HTMLElement ? event.target : null;
741
+ if (!target)
742
+ return;
743
+ if (target.closest('[data-editor-local-test-menu]') || target.closest('[data-editor-local-test-toggle]'))
744
+ return;
745
+ if (target.closest('[data-editor-toolbar-overflow-menu]') || target.closest('[data-editor-toolbar-overflow-toggle]'))
746
+ return;
747
+ closeLocalTestMenu();
748
+ closeToolbarOverflowMenu();
749
+ };
750
+ const ResizeObserverCtor = doc.defaultView?.ResizeObserver;
751
+ const toolbarResizeObserver = ResizeObserverCtor
752
+ ? new ResizeObserverCtor(() => scheduleToolbarOverflowLayout())
753
+ : null;
754
+ toolbarOverflowButton.addEventListener('click', onToolbarOverflowButtonClick);
755
+ localTestButton.addEventListener('click', onLocalTestButtonClick);
756
+ doc.addEventListener('pointerdown', onToolbarOverflowPointerDown);
757
+ toolbarResizeObserver?.observe(sceneToolOverlay);
758
+ doc.defaultView?.addEventListener('resize', scheduleToolbarOverflowLayout);
759
+ applyThemeToSurfaces();
760
+ updateThemeToggleButton();
419
761
  enterEditorButton.addEventListener('click', () => callbacks.onEnterEditor?.());
420
- saveButton.addEventListener('click', () => callbacks.onSaveScene?.());
421
- saveAndRunButton.addEventListener('click', () => callbacks.onSaveAndRunGame?.());
422
- discardRunButton.addEventListener('click', () => callbacks.onDiscardAndRunGame?.());
762
+ saveButton.addEventListener('click', () => {
763
+ closeLocalTestMenu();
764
+ if (localTestActionsEnabled)
765
+ callbacks.onSaveScene?.();
766
+ });
767
+ saveAndRunButton.addEventListener('click', () => {
768
+ closeLocalTestMenu();
769
+ if (localTestActionsEnabled)
770
+ callbacks.onSaveAndRunGame?.();
771
+ });
772
+ discardRunButton.addEventListener('click', () => {
773
+ closeLocalTestMenu();
774
+ if (localTestActionsEnabled)
775
+ callbacks.onDiscardAndRunGame?.();
776
+ });
423
777
  undoButton.addEventListener('click', () => callbacks.onUndo?.());
424
778
  redoButton.addEventListener('click', () => callbacks.onRedo?.());
425
779
  sceneCameraButton.addEventListener('click', () => {
@@ -437,6 +791,9 @@ export function createLocalEditorBrowserUi(options = {}) {
437
791
  sceneHelpButton.addEventListener('click', () => {
438
792
  setShortcutHelpOpen(!helpOpen);
439
793
  });
794
+ themeToggleButton.addEventListener('click', () => {
795
+ setActiveTheme(activeTheme === 'dark' ? 'light' : 'dark');
796
+ });
440
797
  shortcutHelpPanel.addEventListener('click', (event) => {
441
798
  const target = event.target instanceof HTMLElement ? event.target : null;
442
799
  if (!target?.closest('[data-editor-shortcut-help-close]'))
@@ -576,7 +933,7 @@ export function createLocalEditorBrowserUi(options = {}) {
576
933
  const primaryModifier = event.metaKey || event.ctrlKey;
577
934
  const handleDocumentShortcut = inputRouter.shouldHandleDocumentShortcut(event);
578
935
  const handleGlobalShortcut = inputRouter.shouldHandleGlobalShortcut(event);
579
- if (handleDocumentShortcut && primaryModifier && key === 's') {
936
+ if (handleDocumentShortcut && localTestActionsEnabled && primaryModifier && key === 's') {
580
937
  event.preventDefault();
581
938
  callbacks.onSaveScene?.();
582
939
  return;
@@ -594,8 +951,7 @@ export function createLocalEditorBrowserUi(options = {}) {
594
951
  callbacks.onRedo?.();
595
952
  return;
596
953
  }
597
- if (handleGlobalShortcut && (key === 'delete' || key === 'backspace')) {
598
- hierarchyController.handleDeleteShortcut(event);
954
+ if (handleGlobalShortcut && hierarchyController.handleEditShortcut(event)) {
599
955
  return;
600
956
  }
601
957
  if (!handleGlobalShortcut || event.metaKey || event.ctrlKey || event.altKey)
@@ -719,12 +1075,16 @@ export function createLocalEditorBrowserUi(options = {}) {
719
1075
  const disabled = state.busy;
720
1076
  if (!inEditor || disabled)
721
1077
  contextMenu.close();
722
- hostChrome.style.display = inEditor ? 'none' : 'flex';
1078
+ hostChrome.style.display = !inEditor && localTestActionsEnabled ? 'flex' : 'none';
723
1079
  enterEditorButton.disabled = disabled;
724
1080
  for (const button of [saveButton, saveAndRunButton, discardRunButton, undoButton, redoButton, sceneHelpButton, sceneCameraButton]) {
725
- button.style.display = '';
1081
+ button.style.display = 'inline-flex';
726
1082
  button.disabled = disabled;
727
1083
  }
1084
+ localTestGroup.style.display = inEditor && localTestActionsEnabled ? 'flex' : 'none';
1085
+ localTestButton.disabled = disabled;
1086
+ if (!inEditor || !localTestActionsEnabled)
1087
+ closeLocalTestMenu();
728
1088
  if (!inEditor)
729
1089
  helpOpen = false;
730
1090
  inputRouter.setModalOpen(inEditor && helpOpen);
@@ -792,11 +1152,11 @@ export function createLocalEditorBrowserUi(options = {}) {
792
1152
  status.textContent += dragSuffix;
793
1153
  status.title = state.statusDetails || status.textContent;
794
1154
  status.style.color = state.statusTone === 'error'
795
- ? '#ff8f87'
1155
+ ? 'var(--fps-editor-danger-strong)'
796
1156
  : state.statusTone === 'warning'
797
- ? '#d29922'
1157
+ ? 'var(--fps-editor-warn)'
798
1158
  : state.statusTone === 'success'
799
- ? '#7ee787'
1159
+ ? 'var(--fps-editor-success)'
800
1160
  : 'var(--fps-editor-muted)';
801
1161
  sceneToolStatus.textContent = transformTool?.dragPhase === 'dragging'
802
1162
  ? `正在拖拽 ${transformTool.draggingNodeId ?? '选择对象'}`
@@ -816,6 +1176,7 @@ export function createLocalEditorBrowserUi(options = {}) {
816
1176
  else {
817
1177
  boxSelectionOverlay.style.display = 'none';
818
1178
  }
1179
+ scheduleToolbarOverflowLayout();
819
1180
  if (!inEditor)
820
1181
  return;
821
1182
  const hierarchyDescriptor = panelRegistry.getActivePanel('left');
@@ -831,12 +1192,30 @@ export function createLocalEditorBrowserUi(options = {}) {
831
1192
  update(state) {
832
1193
  render(state);
833
1194
  },
1195
+ setTheme(theme) {
1196
+ setActiveTheme(theme);
1197
+ },
1198
+ getTheme() {
1199
+ return activeTheme;
1200
+ },
834
1201
  dispose() {
1202
+ if (toolbarOverflowRaf != null) {
1203
+ doc.defaultView?.cancelAnimationFrame(toolbarOverflowRaf);
1204
+ toolbarOverflowRaf = null;
1205
+ }
1206
+ toolbarResizeObserver?.disconnect();
1207
+ toolbarOverflowButton.removeEventListener('click', onToolbarOverflowButtonClick);
1208
+ localTestButton.removeEventListener('click', onLocalTestButtonClick);
1209
+ doc.removeEventListener('pointerdown', onToolbarOverflowPointerDown);
1210
+ doc.defaultView?.removeEventListener('resize', scheduleToolbarOverflowLayout);
835
1211
  hostChrome.remove();
836
1212
  workbench.root.remove();
1213
+ localTestMenu.remove();
1214
+ toolbarOverflowMenu.remove();
837
1215
  boxSelectionOverlay.remove();
838
1216
  shortcutHelpPanel.remove();
839
1217
  hierarchyController.dispose();
1218
+ workbenchLayoutController.dispose();
840
1219
  contextMenu.dispose();
841
1220
  inputRouter.dispose();
842
1221
  doc.removeEventListener('keydown', onKeyDown);