@fps-games/editor 0.1.1-beta.5 → 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 (35) hide show
  1. package/dist/local-editor-harness.d.ts +1 -0
  2. package/dist/local-editor-harness.d.ts.map +1 -1
  3. package/dist/local-editor-harness.js +73 -0
  4. package/dist/local-editor-harness.js.map +1 -1
  5. package/node_modules/@fps-games/editor-babylon/package.json +4 -4
  6. package/node_modules/@fps-games/editor-browser/dist/index.d.ts +2 -0
  7. package/node_modules/@fps-games/editor-browser/dist/index.d.ts.map +1 -1
  8. package/node_modules/@fps-games/editor-browser/dist/index.js +1 -0
  9. package/node_modules/@fps-games/editor-browser/dist/index.js.map +1 -1
  10. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.d.ts +10 -0
  11. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.d.ts.map +1 -1
  12. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.js +142 -3
  13. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.js.map +1 -1
  14. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.d.ts +1 -0
  15. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.d.ts.map +1 -1
  16. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.js +82 -14
  17. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.js.map +1 -1
  18. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.d.ts.map +1 -1
  19. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.js +3 -0
  20. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.js.map +1 -1
  21. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts +16 -0
  22. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts.map +1 -1
  23. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.d.ts +35 -0
  24. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.d.ts.map +1 -1
  25. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.js +375 -1
  26. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.js.map +1 -1
  27. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.d.ts +2 -2
  28. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.d.ts.map +1 -1
  29. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js +340 -15
  30. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js.map +1 -1
  31. package/node_modules/@fps-games/editor-browser/package.json +2 -2
  32. package/node_modules/@fps-games/editor-core/package.json +2 -2
  33. package/node_modules/@fps-games/editor-forge-play/package.json +2 -2
  34. package/node_modules/@fps-games/editor-protocol/package.json +1 -1
  35. 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';
@@ -11,7 +11,7 @@ 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
13
  import { applyLocalEditorTheme, ensureLocalEditorTheme, LOCAL_EDITOR_THEME_CLASS, normalizeLocalEditorThemeName, } from './local-editor-ui-theme';
14
- import { createDefaultLocalEditorWorkbenchLayout, createLocalEditorWorkbench, createSceneHeaderToolbar, createWorkbenchPanelContent, } from './local-editor-ui-workbench';
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
16
  export { DEFAULT_LOCAL_EDITOR_THEME, LOCAL_EDITOR_THEME_CLASS, applyLocalEditorTheme, normalizeLocalEditorThemeName, } from './local-editor-ui-theme';
17
17
  export { LOCAL_EDITOR_ICON_NAMES, createLocalEditorIcon, isLocalEditorIconName, resolveLocalEditorIconName, } from './local-editor-ui-icons';
@@ -116,6 +116,7 @@ export function createLocalEditorBrowserUi(options = {}) {
116
116
  const doc = options.document ?? document;
117
117
  const root = options.root ?? doc.body;
118
118
  const callbacks = options.callbacks ?? {};
119
+ const localTestActionsEnabled = options.localTestActions === true;
119
120
  ensureLocalEditorTheme(doc);
120
121
  let activeTheme = normalizeLocalEditorThemeName(options.theme);
121
122
  const inputRouter = createLocalEditorWorkbenchInputRouter(doc);
@@ -177,6 +178,38 @@ export function createLocalEditorBrowserUi(options = {}) {
177
178
  const saveButton = LocalEditorShared.createButton(doc, '保存场景', { icon: 'save' });
178
179
  const saveAndRunButton = LocalEditorShared.createButton(doc, '保存并运行', { icon: 'execute' });
179
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);
180
213
  const undoButton = LocalEditorShared.createButton(doc, '撤销', { icon: 'undo' });
181
214
  const redoButton = LocalEditorShared.createButton(doc, '重做', { icon: 'redo' });
182
215
  let helpOpen = false;
@@ -198,6 +231,7 @@ export function createLocalEditorBrowserUi(options = {}) {
198
231
  workbench.leftDock.appendChild(hierarchyPanel);
199
232
  workbench.rightDock.appendChild(inspectorPanel);
200
233
  root.appendChild(workbench.root);
234
+ const workbenchLayoutController = createLocalEditorWorkbenchLayoutController(doc, workbench);
201
235
  const sceneToolOverlay = createSceneHeaderToolbar(doc);
202
236
  const sceneTitle = doc.createElement('div');
203
237
  sceneTitle.textContent = 'Preview';
@@ -215,6 +249,14 @@ export function createLocalEditorBrowserUi(options = {}) {
215
249
  ].join(';');
216
250
  const sceneQuickActions = doc.createElement('div');
217
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(';');
218
260
  const toolGroup = doc.createElement('div');
219
261
  toolGroup.style.cssText = [
220
262
  'display:flex',
@@ -393,14 +435,12 @@ export function createLocalEditorBrowserUi(options = {}) {
393
435
  themeToggleButton.style.padding = '5px 7px';
394
436
  const sceneHelpButton = LocalEditorShared.createButton(doc, '快捷键', { icon: 'help' });
395
437
  sceneHelpButton.style.padding = '5px 7px';
396
- sceneQuickActions.appendChild(saveButton);
397
- sceneQuickActions.appendChild(saveAndRunButton);
398
- sceneQuickActions.appendChild(discardRunButton);
438
+ sceneQuickActions.appendChild(localTestGroup);
399
439
  sceneQuickActions.appendChild(undoButton);
400
440
  sceneQuickActions.appendChild(redoButton);
401
441
  sceneQuickActions.appendChild(dirtyBadge);
402
- sceneQuickActions.appendChild(themeToggleButton);
403
- sceneQuickActions.appendChild(sceneHelpButton);
442
+ sceneUtilityActions.appendChild(themeToggleButton);
443
+ sceneUtilityActions.appendChild(sceneHelpButton);
404
444
  const cameraPreviewGroup = doc.createElement('div');
405
445
  cameraPreviewGroup.style.cssText = [
406
446
  'display:flex',
@@ -413,8 +453,33 @@ export function createLocalEditorBrowserUi(options = {}) {
413
453
  sceneCameraButton.dataset.sceneCameraPreviewToggle = 'true';
414
454
  sceneCameraButton.title = '从 Main Camera 查看当前场景';
415
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(';');
416
480
  sceneToolOverlay.appendChild(sceneTitle);
417
481
  sceneToolOverlay.appendChild(sceneQuickActions);
482
+ sceneToolOverlay.appendChild(sceneUtilityActions);
418
483
  sceneToolOverlay.appendChild(cameraPreviewGroup);
419
484
  sceneToolOverlay.appendChild(toolGroup);
420
485
  sceneToolOverlay.appendChild(spaceGroup);
@@ -425,7 +490,10 @@ export function createLocalEditorBrowserUi(options = {}) {
425
490
  sceneToolOverlay.appendChild(sceneToolStatus);
426
491
  sceneToolOverlay.appendChild(sceneMouseHint);
427
492
  sceneToolOverlay.appendChild(status);
493
+ sceneToolOverlay.appendChild(toolbarOverflowButton);
428
494
  workbench.sceneHeader.appendChild(sceneToolOverlay);
495
+ root.appendChild(localTestMenu);
496
+ root.appendChild(toolbarOverflowMenu);
429
497
  const boxSelectionOverlay = doc.createElement('div');
430
498
  boxSelectionOverlay.classList.add(LOCAL_EDITOR_THEME_CLASS);
431
499
  boxSelectionOverlay.style.cssText = [
@@ -446,6 +514,8 @@ export function createLocalEditorBrowserUi(options = {}) {
446
514
  applyLocalEditorTheme(workbench.root, activeTheme);
447
515
  applyLocalEditorTheme(shortcutHelpPanel, activeTheme);
448
516
  applyLocalEditorTheme(boxSelectionOverlay, activeTheme);
517
+ applyLocalEditorTheme(localTestMenu, activeTheme);
518
+ applyLocalEditorTheme(toolbarOverflowMenu, activeTheme);
449
519
  contextMenu.setTheme?.(activeTheme);
450
520
  }
451
521
  function updateThemeToggleButton() {
@@ -459,12 +529,251 @@ export function createLocalEditorBrowserUi(options = {}) {
459
529
  applyThemeToSurfaces();
460
530
  updateThemeToggleButton();
461
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);
462
759
  applyThemeToSurfaces();
463
760
  updateThemeToggleButton();
464
761
  enterEditorButton.addEventListener('click', () => callbacks.onEnterEditor?.());
465
- saveButton.addEventListener('click', () => callbacks.onSaveScene?.());
466
- saveAndRunButton.addEventListener('click', () => callbacks.onSaveAndRunGame?.());
467
- 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
+ });
468
777
  undoButton.addEventListener('click', () => callbacks.onUndo?.());
469
778
  redoButton.addEventListener('click', () => callbacks.onRedo?.());
470
779
  sceneCameraButton.addEventListener('click', () => {
@@ -624,7 +933,7 @@ export function createLocalEditorBrowserUi(options = {}) {
624
933
  const primaryModifier = event.metaKey || event.ctrlKey;
625
934
  const handleDocumentShortcut = inputRouter.shouldHandleDocumentShortcut(event);
626
935
  const handleGlobalShortcut = inputRouter.shouldHandleGlobalShortcut(event);
627
- if (handleDocumentShortcut && primaryModifier && key === 's') {
936
+ if (handleDocumentShortcut && localTestActionsEnabled && primaryModifier && key === 's') {
628
937
  event.preventDefault();
629
938
  callbacks.onSaveScene?.();
630
939
  return;
@@ -642,8 +951,7 @@ export function createLocalEditorBrowserUi(options = {}) {
642
951
  callbacks.onRedo?.();
643
952
  return;
644
953
  }
645
- if (handleGlobalShortcut && (key === 'delete' || key === 'backspace')) {
646
- hierarchyController.handleDeleteShortcut(event);
954
+ if (handleGlobalShortcut && hierarchyController.handleEditShortcut(event)) {
647
955
  return;
648
956
  }
649
957
  if (!handleGlobalShortcut || event.metaKey || event.ctrlKey || event.altKey)
@@ -767,12 +1075,16 @@ export function createLocalEditorBrowserUi(options = {}) {
767
1075
  const disabled = state.busy;
768
1076
  if (!inEditor || disabled)
769
1077
  contextMenu.close();
770
- hostChrome.style.display = inEditor ? 'none' : 'flex';
1078
+ hostChrome.style.display = !inEditor && localTestActionsEnabled ? 'flex' : 'none';
771
1079
  enterEditorButton.disabled = disabled;
772
1080
  for (const button of [saveButton, saveAndRunButton, discardRunButton, undoButton, redoButton, sceneHelpButton, sceneCameraButton]) {
773
- button.style.display = '';
1081
+ button.style.display = 'inline-flex';
774
1082
  button.disabled = disabled;
775
1083
  }
1084
+ localTestGroup.style.display = inEditor && localTestActionsEnabled ? 'flex' : 'none';
1085
+ localTestButton.disabled = disabled;
1086
+ if (!inEditor || !localTestActionsEnabled)
1087
+ closeLocalTestMenu();
776
1088
  if (!inEditor)
777
1089
  helpOpen = false;
778
1090
  inputRouter.setModalOpen(inEditor && helpOpen);
@@ -864,6 +1176,7 @@ export function createLocalEditorBrowserUi(options = {}) {
864
1176
  else {
865
1177
  boxSelectionOverlay.style.display = 'none';
866
1178
  }
1179
+ scheduleToolbarOverflowLayout();
867
1180
  if (!inEditor)
868
1181
  return;
869
1182
  const hierarchyDescriptor = panelRegistry.getActivePanel('left');
@@ -886,11 +1199,23 @@ export function createLocalEditorBrowserUi(options = {}) {
886
1199
  return activeTheme;
887
1200
  },
888
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);
889
1211
  hostChrome.remove();
890
1212
  workbench.root.remove();
1213
+ localTestMenu.remove();
1214
+ toolbarOverflowMenu.remove();
891
1215
  boxSelectionOverlay.remove();
892
1216
  shortcutHelpPanel.remove();
893
1217
  hierarchyController.dispose();
1218
+ workbenchLayoutController.dispose();
894
1219
  contextMenu.dispose();
895
1220
  inputRouter.dispose();
896
1221
  doc.removeEventListener('keydown', onKeyDown);