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