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