@fps-games/editor 0.1.1-beta.8 → 0.1.2-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/local-editor-harness.d.ts +48 -4
  2. package/dist/local-editor-harness.d.ts.map +1 -1
  3. package/dist/local-editor-harness.js +428 -88
  4. package/dist/local-editor-harness.js.map +1 -1
  5. package/dist/local-editor-scene-render-scheduler.d.ts +26 -0
  6. package/dist/local-editor-scene-render-scheduler.d.ts.map +1 -0
  7. package/dist/local-editor-scene-render-scheduler.js +139 -0
  8. package/dist/local-editor-scene-render-scheduler.js.map +1 -0
  9. package/dist/local-editor-viewport-render-coordinator.d.ts +17 -0
  10. package/dist/local-editor-viewport-render-coordinator.d.ts.map +1 -0
  11. package/dist/local-editor-viewport-render-coordinator.js +102 -0
  12. package/dist/local-editor-viewport-render-coordinator.js.map +1 -0
  13. package/node_modules/@fps-games/editor-babylon/dist/editor-sky.d.ts +2 -0
  14. package/node_modules/@fps-games/editor-babylon/dist/editor-sky.d.ts.map +1 -1
  15. package/node_modules/@fps-games/editor-babylon/dist/editor-sky.js +9 -3
  16. package/node_modules/@fps-games/editor-babylon/dist/editor-sky.js.map +1 -1
  17. package/node_modules/@fps-games/editor-babylon/dist/editor-world.d.ts.map +1 -1
  18. package/node_modules/@fps-games/editor-babylon/dist/editor-world.js +2 -0
  19. package/node_modules/@fps-games/editor-babylon/dist/editor-world.js.map +1 -1
  20. package/node_modules/@fps-games/editor-babylon/dist/index.d.ts +1 -0
  21. package/node_modules/@fps-games/editor-babylon/dist/index.d.ts.map +1 -1
  22. package/node_modules/@fps-games/editor-babylon/dist/index.js +1 -0
  23. package/node_modules/@fps-games/editor-babylon/dist/index.js.map +1 -1
  24. package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.d.ts.map +1 -1
  25. package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.js +167 -27
  26. package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.js.map +1 -1
  27. package/node_modules/@fps-games/editor-babylon/dist/main-camera-preview.d.ts +25 -0
  28. package/node_modules/@fps-games/editor-babylon/dist/main-camera-preview.d.ts.map +1 -0
  29. package/node_modules/@fps-games/editor-babylon/dist/main-camera-preview.js +162 -0
  30. package/node_modules/@fps-games/editor-babylon/dist/main-camera-preview.js.map +1 -0
  31. package/node_modules/@fps-games/editor-babylon/dist/projection.d.ts +29 -1
  32. package/node_modules/@fps-games/editor-babylon/dist/projection.d.ts.map +1 -1
  33. package/node_modules/@fps-games/editor-babylon/dist/projection.js +280 -17
  34. package/node_modules/@fps-games/editor-babylon/dist/projection.js.map +1 -1
  35. package/node_modules/@fps-games/editor-babylon/dist/scene-camera-preview.d.ts +2 -22
  36. package/node_modules/@fps-games/editor-babylon/dist/scene-camera-preview.d.ts.map +1 -1
  37. package/node_modules/@fps-games/editor-babylon/dist/scene-camera-preview.js +1 -121
  38. package/node_modules/@fps-games/editor-babylon/dist/scene-camera-preview.js.map +1 -1
  39. package/node_modules/@fps-games/editor-babylon/dist/transform-gizmo-controller.d.ts.map +1 -1
  40. package/node_modules/@fps-games/editor-babylon/dist/transform-gizmo-controller.js +68 -30
  41. package/node_modules/@fps-games/editor-babylon/dist/transform-gizmo-controller.js.map +1 -1
  42. package/node_modules/@fps-games/editor-babylon/dist/types.d.ts +6 -0
  43. package/node_modules/@fps-games/editor-babylon/dist/types.d.ts.map +1 -1
  44. package/node_modules/@fps-games/editor-babylon/package.json +4 -4
  45. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.d.ts +2 -1
  46. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.d.ts.map +1 -1
  47. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.js +139 -34
  48. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.js.map +1 -1
  49. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.d.ts +2 -1
  50. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.d.ts.map +1 -1
  51. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.js +2 -0
  52. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.js.map +1 -1
  53. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.d.ts +1 -0
  54. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.d.ts.map +1 -1
  55. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.js +96 -14
  56. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.js.map +1 -1
  57. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-tooltip.js +1 -1
  58. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts +56 -2
  59. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts.map +1 -1
  60. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.d.ts +1 -0
  61. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.d.ts.map +1 -1
  62. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.js +3 -0
  63. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.js.map +1 -1
  64. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.d.ts +4 -2
  65. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.d.ts.map +1 -1
  66. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js +253 -39
  67. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js.map +1 -1
  68. package/node_modules/@fps-games/editor-browser/package.json +2 -2
  69. package/node_modules/@fps-games/editor-core/dist/viewport-tools.d.ts +6 -6
  70. package/node_modules/@fps-games/editor-core/dist/viewport-tools.js +3 -3
  71. package/node_modules/@fps-games/editor-core/dist/viewport-tools.js.map +1 -1
  72. package/node_modules/@fps-games/editor-core/package.json +2 -2
  73. package/node_modules/@fps-games/editor-forge-play/package.json +2 -2
  74. package/node_modules/@fps-games/editor-protocol/package.json +1 -1
  75. package/package.json +6 -6
@@ -15,14 +15,33 @@ import { createShortcutHelpPanel as createLocalEditorShortcutHelpPanel } from '.
15
15
  import { applyLocalEditorTheme, ensureLocalEditorTheme, LOCAL_EDITOR_THEME_CLASS, normalizeLocalEditorThemeName, } from './local-editor-ui-theme';
16
16
  import { createDefaultLocalEditorWorkbenchLayout, createLocalEditorWorkbenchLayoutController, createLocalEditorWorkbench, createSceneHeaderToolbar, createWorkbenchPanelContent, } from './local-editor-ui-workbench';
17
17
  import { createLocalEditorIcon } from './local-editor-ui-icons';
18
- export { applyLocalEditorBrowserInspectorControlBinding, createLocalEditorBrowserInspectorControlRegistry, formatLocalEditorBrowserInspectorValue, resolveLocalEditorBrowserInspectorSectionStatus, resolveLocalEditorBrowserInspectorControlRegistration, } from './local-editor-ui-panels';
18
+ export { applyLocalEditorBrowserInspectorControlBinding, createLocalEditorBrowserInspectorControlRegistry, formatLocalEditorBrowserInspectorNumberValue, formatLocalEditorBrowserInspectorValue, resolveLocalEditorBrowserInspectorSectionStatus, resolveLocalEditorBrowserInspectorControlRegistration, } from './local-editor-ui-panels';
19
19
  export { DEFAULT_LOCAL_EDITOR_THEME, LOCAL_EDITOR_THEME_CLASS, applyLocalEditorTheme, normalizeLocalEditorThemeName, } from './local-editor-ui-theme';
20
20
  export { LOCAL_EDITOR_ICON_NAMES, createLocalEditorIcon, isLocalEditorIconName, resolveLocalEditorIconName, } from './local-editor-ui-icons';
21
- function readInspectorInputValue(input) {
21
+ const INSPECTOR_INPUT_PENDING = Symbol('inspector-input-pending');
22
+ export function parseLocalEditorBrowserInspectorNumberValue(value, mode = 'final') {
23
+ const normalized = value.trim();
24
+ if (normalized === '')
25
+ return mode === 'final' ? 0 : null;
26
+ if (normalized === '-'
27
+ || normalized === '+'
28
+ || normalized === '.'
29
+ || normalized === '-.'
30
+ || normalized === '+.') {
31
+ return null;
32
+ }
33
+ if (mode === 'live' && (/[.]$/.test(normalized) || /e[+-]?$/i.test(normalized)))
34
+ return null;
35
+ if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i.test(normalized))
36
+ return null;
37
+ const numeric = Number(normalized);
38
+ return Number.isFinite(numeric) ? numeric : null;
39
+ }
40
+ function readInspectorInputValue(input, mode = 'final') {
22
41
  const control = input.dataset.serializedControl;
23
42
  const valueType = input.dataset.serializedValueType;
24
43
  if ((control === 'vec2' || control === 'vec3') && input instanceof HTMLInputElement) {
25
- return readInspectorVectorInputValue(input);
44
+ return readInspectorVectorInputValue(input, mode);
26
45
  }
27
46
  if (control === 'enum' && input instanceof HTMLSelectElement) {
28
47
  const option = input.selectedOptions.item(0);
@@ -40,8 +59,9 @@ function readInspectorInputValue(input) {
40
59
  return input.checked;
41
60
  if (control === 'boolean' || valueType === 'boolean')
42
61
  return input.value === 'true';
43
- if (control === 'number' || valueType === 'number')
44
- return Number(input.value);
62
+ if (control === 'number' || valueType === 'number') {
63
+ return parseLocalEditorBrowserInspectorNumberValue(input.value, mode) ?? INSPECTOR_INPUT_PENDING;
64
+ }
45
65
  if (control === 'color' && input instanceof HTMLInputElement && input.type === 'color') {
46
66
  const value = input.value.replace('#', '');
47
67
  const numeric = Number.parseInt(value, 16);
@@ -148,7 +168,7 @@ function setToolbarButtonIcon(doc, button, icon) {
148
168
  else
149
169
  button.prepend(nextIcon);
150
170
  }
151
- function readInspectorVectorInputValue(input) {
171
+ function readInspectorVectorInputValue(input, mode) {
152
172
  const wrapper = input.closest('[data-inspector-vector-control]');
153
173
  const values = {};
154
174
  const fields = wrapper
@@ -158,22 +178,27 @@ function readInspectorVectorInputValue(input) {
158
178
  const axis = field.dataset.serializedVectorAxis;
159
179
  if (!axis)
160
180
  continue;
161
- const numeric = Number(field.value);
162
- values[axis] = Number.isFinite(numeric) ? numeric : 0;
181
+ const numeric = parseLocalEditorBrowserInspectorNumberValue(field.value, mode);
182
+ if (numeric == null)
183
+ return INSPECTOR_INPUT_PENDING;
184
+ values[axis] = numeric;
163
185
  }
164
186
  return values;
165
187
  }
166
- function createInspectorPropertyInput(input, source) {
188
+ function createInspectorPropertyInput(input, source, numberParseMode = 'final') {
167
189
  if (!input.dataset.serializedPath || !input.dataset.serializedTargetId)
168
190
  return null;
169
191
  const targetIds = input.dataset.serializedTargetIds
170
192
  ? input.dataset.serializedTargetIds.split(',').filter(Boolean)
171
193
  : undefined;
194
+ const value = readInspectorInputValue(input, numberParseMode);
195
+ if (value === INSPECTOR_INPUT_PENDING)
196
+ return null;
172
197
  return {
173
198
  targetId: input.dataset.serializedTargetId,
174
199
  targetIds,
175
200
  path: input.dataset.serializedPath,
176
- value: readInspectorInputValue(input),
201
+ value,
177
202
  control: input.dataset.serializedControl,
178
203
  valueType: input.dataset.serializedValueType,
179
204
  commitMode: input.dataset.serializedCommitMode,
@@ -181,6 +206,13 @@ function createInspectorPropertyInput(input, source) {
181
206
  source: input.dataset.serializedEditSource ?? source,
182
207
  };
183
208
  }
209
+ function hasPendingInspectorNumberValue(input, mode = 'live') {
210
+ const control = input.dataset.serializedControl;
211
+ const valueType = input.dataset.serializedValueType;
212
+ if (control !== 'number' && valueType !== 'number' && control !== 'vec2' && control !== 'vec3')
213
+ return false;
214
+ return readInspectorInputValue(input, mode) === INSPECTOR_INPUT_PENDING;
215
+ }
184
216
  function readInspectorCommitMode(input) {
185
217
  return input.dataset.serializedCommitMode ?? 'live';
186
218
  }
@@ -193,6 +225,96 @@ function readInspectorImmediateSource(input) {
193
225
  return 'color';
194
226
  return 'input';
195
227
  }
228
+ function createSceneFrameRateOverlay(doc) {
229
+ const root = doc.createElement('div');
230
+ root.classList.add(LOCAL_EDITOR_THEME_CLASS);
231
+ root.dataset.editorSceneFrameRateOverlay = 'true';
232
+ root.style.cssText = [
233
+ 'position:absolute',
234
+ 'right:14px',
235
+ 'bottom:134px',
236
+ 'z-index:3',
237
+ 'display:none',
238
+ 'align-items:center',
239
+ 'gap:6px',
240
+ 'width:112px',
241
+ 'height:30px',
242
+ 'padding:0 8px',
243
+ 'box-sizing:border-box',
244
+ 'border:1px solid color-mix(in srgb, var(--fps-editor-border) 78%, transparent)',
245
+ 'border-radius:6px',
246
+ 'background:color-mix(in srgb, var(--fps-editor-chrome) 76%, transparent)',
247
+ 'box-shadow:var(--fps-editor-shadow-panel)',
248
+ 'font-family:var(--fps-editor-font)',
249
+ 'font-size:11px',
250
+ 'font-weight:900',
251
+ 'letter-spacing:0',
252
+ 'color:var(--fps-editor-text-strong)',
253
+ 'pointer-events:none',
254
+ 'backdrop-filter:blur(5px)',
255
+ 'overflow:hidden',
256
+ ].join(';');
257
+ const label = doc.createElement('span');
258
+ label.textContent = 'FPS';
259
+ label.style.cssText = [
260
+ 'color:var(--fps-editor-muted-strong)',
261
+ 'font-size:10px',
262
+ 'line-height:1',
263
+ ].join(';');
264
+ const fpsValue = doc.createElement('span');
265
+ fpsValue.textContent = '--';
266
+ fpsValue.style.cssText = [
267
+ 'width:26px',
268
+ 'flex:0 0 26px',
269
+ 'font-variant-numeric:tabular-nums',
270
+ 'font-size:13px',
271
+ 'line-height:1',
272
+ ].join(';');
273
+ const modeLabel = doc.createElement('span');
274
+ modeLabel.textContent = 'IDLE';
275
+ modeLabel.style.cssText = [
276
+ 'margin-left:auto',
277
+ 'padding:2px 4px',
278
+ 'flex:0 0 auto',
279
+ 'border:1px solid var(--fps-editor-border-soft)',
280
+ 'border-radius:4px',
281
+ 'background:color-mix(in srgb, var(--fps-editor-field) 72%, transparent)',
282
+ 'color:var(--fps-editor-muted)',
283
+ 'font-size:9px',
284
+ 'line-height:1',
285
+ ].join(';');
286
+ root.appendChild(label);
287
+ root.appendChild(fpsValue);
288
+ root.appendChild(modeLabel);
289
+ return { root, fpsValue, modeLabel };
290
+ }
291
+ function renderSceneFrameRateOverlay(overlay, stats) {
292
+ if (!stats) {
293
+ overlay.root.style.display = 'none';
294
+ return;
295
+ }
296
+ overlay.root.style.display = 'flex';
297
+ const fps = Number.isFinite(stats.fps) ? Math.round(stats.fps) : null;
298
+ const displayFps = stats.mode === 'idle' ? 0 : fps;
299
+ overlay.fpsValue.textContent = displayFps == null ? '--' : String(displayFps);
300
+ overlay.modeLabel.textContent = stats.mode === 'continuous' ? 'LIVE' : 'IDLE';
301
+ overlay.modeLabel.style.color = stats.mode === 'continuous'
302
+ ? 'var(--fps-editor-success)'
303
+ : 'var(--fps-editor-muted)';
304
+ overlay.fpsValue.style.color = displayFps == null || stats.mode === 'idle'
305
+ ? 'var(--fps-editor-muted)'
306
+ : displayFps >= 50
307
+ ? 'var(--fps-editor-success)'
308
+ : displayFps >= 30
309
+ ? 'var(--fps-editor-warn)'
310
+ : 'var(--fps-editor-danger-strong)';
311
+ overlay.root.title = [
312
+ `Scene FPS: ${displayFps == null ? '--' : displayFps}`,
313
+ `mode: ${stats.mode}`,
314
+ stats.lastFrameMs == null ? '' : `frame: ${stats.lastFrameMs.toFixed(1)}ms`,
315
+ stats.activeReasons.length ? `active: ${stats.activeReasons.join(', ')}` : '',
316
+ ].filter(Boolean).join(' | ');
317
+ }
196
318
  function createCoordinateAxesOverlay(doc) {
197
319
  const root = doc.createElement('div');
198
320
  root.dataset.editorCoordinateAxesOverlay = 'true';
@@ -260,6 +382,30 @@ function createCoordinateAxesOverlay(doc) {
260
382
  centerDot.setAttribute('r', '3.2');
261
383
  centerDot.setAttribute('fill', 'color-mix(in srgb, var(--fps-editor-text) 82%, transparent)');
262
384
  labelLayer.appendChild(centerDot);
385
+ const projectionIcon = doc.createElementNS('http://www.w3.org/2000/svg', 'g');
386
+ projectionIcon.setAttribute('transform', 'translate(8 8)');
387
+ projectionIcon.setAttribute('role', 'button');
388
+ projectionIcon.setAttribute('tabindex', '0');
389
+ projectionIcon.dataset.coordinateAxesProjectionToggle = 'true';
390
+ projectionIcon.style.cssText = 'opacity:0.92;pointer-events:auto;cursor:pointer;outline:none';
391
+ const projectionIconFrame = doc.createElementNS('http://www.w3.org/2000/svg', 'rect');
392
+ projectionIconFrame.setAttribute('x', '0');
393
+ projectionIconFrame.setAttribute('y', '0');
394
+ projectionIconFrame.setAttribute('width', '18');
395
+ projectionIconFrame.setAttribute('height', '18');
396
+ projectionIconFrame.setAttribute('rx', '4');
397
+ projectionIconFrame.setAttribute('fill', 'color-mix(in srgb, var(--fps-editor-chrome) 78%, transparent)');
398
+ projectionIconFrame.setAttribute('stroke', 'color-mix(in srgb, var(--fps-editor-border) 82%, transparent)');
399
+ projectionIconFrame.setAttribute('stroke-width', '1');
400
+ const projectionIconShape = doc.createElementNS('http://www.w3.org/2000/svg', 'path');
401
+ projectionIconShape.setAttribute('fill', 'none');
402
+ projectionIconShape.setAttribute('stroke', 'color-mix(in srgb, var(--fps-editor-accent) 82%, #ffffff)');
403
+ projectionIconShape.setAttribute('stroke-width', '1.6');
404
+ projectionIconShape.setAttribute('stroke-linejoin', 'round');
405
+ projectionIconShape.setAttribute('stroke-linecap', 'round');
406
+ projectionIcon.appendChild(projectionIconFrame);
407
+ projectionIcon.appendChild(projectionIconShape);
408
+ svg.appendChild(projectionIcon);
263
409
  return {
264
410
  root,
265
411
  lineLayer,
@@ -269,6 +415,8 @@ function createCoordinateAxesOverlay(doc) {
269
415
  axisLabels,
270
416
  axisDiscs,
271
417
  centerDot,
418
+ projectionIcon,
419
+ projectionIconShape,
272
420
  };
273
421
  }
274
422
  function renderCoordinateAxesOverlay(overlay, state) {
@@ -277,6 +425,7 @@ function renderCoordinateAxesOverlay(overlay, state) {
277
425
  return;
278
426
  }
279
427
  overlay.root.style.display = '';
428
+ renderCoordinateAxesProjectionIcon(overlay, state.projectionMode, state.projectionToggleDisabled === true);
280
429
  const center = 56;
281
430
  const lineLength = 38;
282
431
  const sortedAxes = [...state.axes].sort((left, right) => right.depth - left.depth);
@@ -305,6 +454,18 @@ function renderCoordinateAxesOverlay(overlay, state) {
305
454
  }
306
455
  overlay.labelLayer.appendChild(overlay.centerDot);
307
456
  }
457
+ function renderCoordinateAxesProjectionIcon(overlay, mode, disabled) {
458
+ overlay.projectionIcon.dataset.viewportProjectionMode = mode;
459
+ overlay.projectionIcon.setAttribute('aria-label', disabled
460
+ ? `编辑视口投影:${VIEWPORT_PROJECTION_LABELS[mode]},Main Camera 预览中不可切换`
461
+ : `编辑视口投影:${VIEWPORT_PROJECTION_LABELS[mode]},点击切换`);
462
+ overlay.projectionIcon.setAttribute('aria-disabled', disabled ? 'true' : 'false');
463
+ overlay.projectionIcon.style.opacity = disabled ? '0.46' : '0.92';
464
+ overlay.projectionIcon.style.cursor = disabled ? 'not-allowed' : 'pointer';
465
+ overlay.projectionIconShape.setAttribute('d', mode === 'orthographic'
466
+ ? 'M5 6.5H12.5V14H5ZM7.5 4H15V11.5'
467
+ : 'M4.5 14H13.5L10.5 4H7.5ZM7 10.5H11');
468
+ }
308
469
  function createSpatialOverlay(doc) {
309
470
  const root = doc.createElement('div');
310
471
  root.classList.add(LOCAL_EDITOR_THEME_CLASS);
@@ -959,17 +1120,17 @@ export function createLocalEditorBrowserUi(options = {}) {
959
1120
  sceneUtilityActions.appendChild(gridToggleButton);
960
1121
  sceneUtilityActions.appendChild(themeToggleButton);
961
1122
  sceneUtilityActions.appendChild(sceneHelpButton);
962
- const cameraPreviewGroup = doc.createElement('div');
963
- cameraPreviewGroup.style.cssText = [
1123
+ const mainCameraPreviewGroup = doc.createElement('div');
1124
+ mainCameraPreviewGroup.style.cssText = [
964
1125
  'display:flex',
965
1126
  'align-items:center',
966
1127
  'gap:4px',
967
1128
  'padding-left:8px',
968
1129
  'border-left:1px solid var(--fps-editor-divider)',
969
1130
  ].join(';');
970
- const sceneCameraButton = createToolbarIconButton(doc, '从 Main Camera 查看场景', 'camera');
971
- sceneCameraButton.dataset.sceneCameraPreviewToggle = 'true';
972
- cameraPreviewGroup.appendChild(sceneCameraButton);
1131
+ const mainCameraPreviewButton = createToolbarIconButton(doc, '从 Main Camera 查看场景', 'camera');
1132
+ mainCameraPreviewButton.dataset.mainCameraPreviewToggle = 'true';
1133
+ mainCameraPreviewGroup.appendChild(mainCameraPreviewButton);
973
1134
  const viewportToolsGroup = doc.createElement('div');
974
1135
  viewportToolsGroup.style.cssText = [
975
1136
  'display:flex',
@@ -1062,6 +1223,7 @@ export function createLocalEditorBrowserUi(options = {}) {
1062
1223
  'pointer-events:auto',
1063
1224
  ].join(';');
1064
1225
  const coordinateAxesOverlay = createCoordinateAxesOverlay(doc);
1226
+ const sceneFrameRateOverlay = createSceneFrameRateOverlay(doc);
1065
1227
  const spatialOverlay = createSpatialOverlay(doc);
1066
1228
  const measurementOverlay = createMeasurementOverlay(doc);
1067
1229
  sceneToolOverlay.appendChild(sceneTitle);
@@ -1073,12 +1235,13 @@ export function createLocalEditorBrowserUi(options = {}) {
1073
1235
  sceneToolOverlay.appendChild(actionGroup);
1074
1236
  sceneToolOverlay.appendChild(viewportToolsGroup);
1075
1237
  sceneToolOverlay.appendChild(sceneUtilityActions);
1076
- sceneToolOverlay.appendChild(cameraPreviewGroup);
1238
+ sceneToolOverlay.appendChild(mainCameraPreviewGroup);
1077
1239
  sceneToolOverlay.appendChild(editorStatusButton);
1078
1240
  sceneToolOverlay.appendChild(toolbarOverflowButton);
1079
1241
  workbench.sceneHeader.appendChild(sceneToolOverlay);
1080
1242
  workbench.sceneFrame.appendChild(spatialOverlay.root);
1081
1243
  workbench.sceneFrame.appendChild(measurementOverlay.root);
1244
+ workbench.sceneFrame.appendChild(sceneFrameRateOverlay.root);
1082
1245
  workbench.sceneFrame.appendChild(coordinateAxesOverlay.root);
1083
1246
  root.appendChild(localTestMenu);
1084
1247
  root.appendChild(toolbarOverflowMenu);
@@ -1114,6 +1277,7 @@ export function createLocalEditorBrowserUi(options = {}) {
1114
1277
  applyLocalEditorTheme(workbench.root, activeTheme);
1115
1278
  applyLocalEditorTheme(spatialOverlay.root, activeTheme);
1116
1279
  applyLocalEditorTheme(measurementOverlay.root, activeTheme);
1280
+ applyLocalEditorTheme(sceneFrameRateOverlay.root, activeTheme);
1117
1281
  applyLocalEditorTheme(shortcutHelpPanel, activeTheme);
1118
1282
  applyLocalEditorTheme(boxSelectionOverlay, activeTheme);
1119
1283
  applyLocalEditorTheme(localTestMenu, activeTheme);
@@ -1326,14 +1490,14 @@ export function createLocalEditorBrowserUi(options = {}) {
1326
1490
  actionGroup,
1327
1491
  viewportToolsGroup,
1328
1492
  sceneUtilityActions,
1329
- cameraPreviewGroup,
1493
+ mainCameraPreviewGroup,
1330
1494
  editorStatusButton,
1331
1495
  toolbarOverflowButton,
1332
1496
  ];
1333
1497
  const toolbarOverflowItems = [
1334
1498
  { id: 'editor-status', element: editorStatusButton, kind: 'group' },
1335
1499
  { id: 'scene-utilities', element: sceneUtilityActions, kind: 'group' },
1336
- { id: 'camera-preview', element: cameraPreviewGroup, kind: 'group' },
1500
+ { id: 'main-camera-preview', element: mainCameraPreviewGroup, kind: 'group' },
1337
1501
  { id: 'viewport-tools', element: viewportToolsGroup, kind: 'group' },
1338
1502
  { id: 'transform-actions', element: actionGroup, kind: 'group' },
1339
1503
  { id: 'placement', element: placementGroup, kind: 'group' },
@@ -1538,9 +1702,30 @@ export function createLocalEditorBrowserUi(options = {}) {
1538
1702
  const visible = currentState?.grid?.visible ?? true;
1539
1703
  callbacks.onGridVisibleChange?.(!visible);
1540
1704
  });
1541
- sceneCameraButton.addEventListener('click', () => {
1542
- const enabled = currentState?.sceneCameraPreview?.enabled ?? false;
1543
- callbacks.onSceneCameraPreviewToggle?.(!enabled);
1705
+ mainCameraPreviewButton.addEventListener('click', () => {
1706
+ const enabled = currentState?.mainCameraPreview?.enabled ?? false;
1707
+ callbacks.onMainCameraPreviewToggle?.(!enabled);
1708
+ });
1709
+ const toggleCoordinateAxesProjection = (event) => {
1710
+ event.preventDefault();
1711
+ event.stopPropagation();
1712
+ const axesState = currentState?.coordinateAxes ?? null;
1713
+ if (!axesState || axesState.projectionToggleDisabled || currentState?.busy)
1714
+ return;
1715
+ const currentMode = axesState.projectionMode;
1716
+ const nextMode = currentMode === 'orthographic' ? 'perspective' : 'orthographic';
1717
+ callbacks.onViewportProjectionModeChange?.(nextMode);
1718
+ if (event.type === 'click')
1719
+ coordinateAxesOverlay.projectionIcon.blur();
1720
+ };
1721
+ coordinateAxesOverlay.projectionIcon.addEventListener('pointerdown', (event) => {
1722
+ event.preventDefault();
1723
+ });
1724
+ coordinateAxesOverlay.projectionIcon.addEventListener('click', toggleCoordinateAxesProjection);
1725
+ coordinateAxesOverlay.projectionIcon.addEventListener('keydown', (event) => {
1726
+ if (event.key !== 'Enter' && event.key !== ' ')
1727
+ return;
1728
+ toggleCoordinateAxesProjection(event);
1544
1729
  });
1545
1730
  viewportToolsGroup.addEventListener('click', (event) => {
1546
1731
  const target = event.target instanceof HTMLElement ? event.target : null;
@@ -1666,10 +1851,10 @@ export function createLocalEditorBrowserUi(options = {}) {
1666
1851
  return;
1667
1852
  const currentOverlay = currentState?.viewportTools?.overlay;
1668
1853
  callbacks.onViewportOverlaySettingsChange?.({
1669
- bounds: currentOverlay?.bounds ?? true,
1670
- dimensions: currentOverlay?.dimensions ?? true,
1854
+ bounds: currentOverlay?.bounds ?? false,
1855
+ dimensions: currentOverlay?.dimensions ?? false,
1671
1856
  edgeLengths: currentOverlay?.edgeLengths ?? false,
1672
- anchor: currentOverlay?.anchor ?? true,
1857
+ anchor: currentOverlay?.anchor ?? false,
1673
1858
  [key]: input.checked,
1674
1859
  });
1675
1860
  });
@@ -1696,6 +1881,7 @@ export function createLocalEditorBrowserUi(options = {}) {
1696
1881
  inputRouter,
1697
1882
  contextMenu,
1698
1883
  getState: () => currentState,
1884
+ contextActions: options.hierarchy?.contextActions,
1699
1885
  requestRender: () => {
1700
1886
  if (currentState)
1701
1887
  render(currentState);
@@ -1725,7 +1911,7 @@ export function createLocalEditorBrowserUi(options = {}) {
1725
1911
  return;
1726
1912
  if (readInspectorCommitMode(input) !== 'live')
1727
1913
  return;
1728
- const propertyInput = createInspectorPropertyInput(input, 'input');
1914
+ const propertyInput = createInspectorPropertyInput(input, 'input', 'live');
1729
1915
  if (propertyInput)
1730
1916
  callbacks.onPropertyInput?.(propertyInput);
1731
1917
  });
@@ -1739,7 +1925,7 @@ export function createLocalEditorBrowserUi(options = {}) {
1739
1925
  if (readInspectorCommitMode(input) !== 'change')
1740
1926
  return;
1741
1927
  }
1742
- const propertyInput = createInspectorPropertyInput(input, readInspectorImmediateSource(input));
1928
+ const propertyInput = createInspectorPropertyInput(input, readInspectorImmediateSource(input), 'final');
1743
1929
  if (propertyInput)
1744
1930
  callbacks.onPropertyInput?.(propertyInput);
1745
1931
  });
@@ -1747,11 +1933,27 @@ export function createLocalEditorBrowserUi(options = {}) {
1747
1933
  const input = event.target instanceof HTMLInputElement ? event.target : null;
1748
1934
  if (!input?.dataset.serializedPath || !input.dataset.serializedTargetId)
1749
1935
  return;
1750
- if (readInspectorCommitMode(input) !== 'blur')
1936
+ if (hasPendingInspectorNumberValue(input, 'final')) {
1937
+ if (currentState)
1938
+ render(currentState);
1751
1939
  return;
1752
- const propertyInput = createInspectorPropertyInput(input, 'input');
1940
+ }
1941
+ const commitMode = readInspectorCommitMode(input);
1942
+ if (commitMode === 'live' && hasPendingInspectorNumberValue(input, 'live')) {
1943
+ const propertyInput = createInspectorPropertyInput(input, 'input', 'final');
1944
+ if (propertyInput)
1945
+ callbacks.onPropertyInput?.(propertyInput);
1946
+ else if (currentState)
1947
+ render(currentState);
1948
+ return;
1949
+ }
1950
+ if (commitMode !== 'blur')
1951
+ return;
1952
+ const propertyInput = createInspectorPropertyInput(input, 'input', 'final');
1753
1953
  if (propertyInput)
1754
1954
  callbacks.onPropertyInput?.(propertyInput);
1955
+ else if (currentState)
1956
+ render(currentState);
1755
1957
  });
1756
1958
  const onKeyDown = (event) => {
1757
1959
  if (event.defaultPrevented)
@@ -1848,6 +2050,7 @@ export function createLocalEditorBrowserUi(options = {}) {
1848
2050
  `[data-serialized-target-id="${cssEscape(active.dataset.serializedTargetId)}"]`,
1849
2051
  ].join(''),
1850
2052
  value: active.value,
2053
+ preserveValue: hasPendingInspectorNumberValue(active),
1851
2054
  };
1852
2055
  }
1853
2056
  if (active.dataset.editorInspectorSearch != null) {
@@ -1865,6 +2068,8 @@ export function createLocalEditorBrowserUi(options = {}) {
1865
2068
  if (!input)
1866
2069
  return;
1867
2070
  input.focus({ preventScroll: true });
2071
+ if (snapshot.value != null && snapshot.preserveValue)
2072
+ input.value = snapshot.value;
1868
2073
  if (snapshot.value != null && input.value === snapshot.value) {
1869
2074
  const position = input.value.length;
1870
2075
  try {
@@ -1902,7 +2107,7 @@ export function createLocalEditorBrowserUi(options = {}) {
1902
2107
  contextMenu.close();
1903
2108
  hostChrome.style.display = !inEditor && localTestActionsEnabled ? 'flex' : 'none';
1904
2109
  enterEditorButton.disabled = disabled;
1905
- for (const button of [saveButton, saveAndRunButton, discardRunButton, undoButton, redoButton, sceneHelpButton, sceneCameraButton, gridToggleButton]) {
2110
+ for (const button of [saveButton, saveAndRunButton, discardRunButton, undoButton, redoButton, sceneHelpButton, mainCameraPreviewButton, gridToggleButton]) {
1906
2111
  button.style.display = 'inline-flex';
1907
2112
  button.disabled = disabled;
1908
2113
  }
@@ -1915,6 +2120,8 @@ export function createLocalEditorBrowserUi(options = {}) {
1915
2120
  inputRouter.setModalOpen(inEditor && helpOpen);
1916
2121
  const transformTool = state.transformTool ?? null;
1917
2122
  workbench.root.style.display = inEditor ? '' : 'none';
2123
+ if (inEditor)
2124
+ workbenchLayoutController.refresh();
1918
2125
  sceneToolOverlay.style.display = inEditor ? 'flex' : 'none';
1919
2126
  if (!inEditor)
1920
2127
  closeSnapSettingsPopover();
@@ -1924,18 +2131,21 @@ export function createLocalEditorBrowserUi(options = {}) {
1924
2131
  closeTransformActionPopover();
1925
2132
  if (!inEditor)
1926
2133
  closeOverlaySettingsPopover();
1927
- const sceneCameraPreview = state.sceneCameraPreview ?? { enabled: false, available: false };
1928
- cameraPreviewGroup.style.display = inEditor ? 'flex' : 'none';
1929
- sceneCameraButton.disabled = disabled || !sceneCameraPreview.available;
1930
- const sceneCameraTooltip = sceneCameraPreview.available
2134
+ const mainCameraPreview = state.mainCameraPreview ?? { enabled: false, available: false };
2135
+ mainCameraPreviewGroup.style.display = inEditor ? 'flex' : 'none';
2136
+ mainCameraPreviewButton.disabled = disabled || !mainCameraPreview.available;
2137
+ const mainCameraPreviewTooltip = mainCameraPreview.available
1931
2138
  ? '从 Main Camera 查看当前场景'
1932
2139
  : '当前场景没有可预览的 Main Camera';
1933
- setToolbarButtonTooltip(sceneCameraButton, sceneCameraTooltip);
1934
- LocalEditorShared.applyButtonActiveState(sceneCameraButton, sceneCameraPreview.enabled);
2140
+ setToolbarButtonTooltip(mainCameraPreviewButton, mainCameraPreviewTooltip);
2141
+ LocalEditorShared.applyButtonActiveState(mainCameraPreviewButton, mainCameraPreview.enabled);
1935
2142
  const viewportTools = state.viewportTools ?? null;
1936
- const viewportControlsDisabled = disabled || sceneCameraPreview.enabled;
1937
- const viewportControlsTooltip = sceneCameraPreview.enabled
1938
- ? '请先关闭 Scene Camera 预览,再使用视口工具'
2143
+ const viewportControlsDisabled = disabled || mainCameraPreview.enabled;
2144
+ const viewportControlsTooltip = mainCameraPreview.enabled
2145
+ ? '当前为 Main Camera 预览,请关闭后再使用视口工具'
2146
+ : '';
2147
+ const viewportProjectionTooltip = mainCameraPreview.enabled
2148
+ ? '当前为 Main Camera 预览,投影由 Main Camera 决定;关闭预览后可切换编辑视口投影'
1939
2149
  : '';
1940
2150
  viewportToolsGroup.style.display = inEditor ? 'flex' : 'none';
1941
2151
  for (const [preset, button] of viewportPresetButtons) {
@@ -1947,7 +2157,7 @@ export function createLocalEditorBrowserUi(options = {}) {
1947
2157
  const projectionMode = viewportTools?.projectionMode ?? 'perspective';
1948
2158
  const nextProjectionMode = projectionMode === 'orthographic' ? 'perspective' : 'orthographic';
1949
2159
  setToolbarButtonIcon(doc, projectionToggleButton, projectionMode === 'orthographic' ? 'projection-ortho' : 'projection-perspective');
1950
- setToolbarButtonTooltip(projectionToggleButton, viewportControlsTooltip || `投影模式:${VIEWPORT_PROJECTION_LABELS[projectionMode]}。切换为${VIEWPORT_PROJECTION_LABELS[nextProjectionMode]}`);
2160
+ setToolbarButtonTooltip(projectionToggleButton, viewportProjectionTooltip || `投影模式:${VIEWPORT_PROJECTION_LABELS[projectionMode]}。切换为${VIEWPORT_PROJECTION_LABELS[nextProjectionMode]}`);
1951
2161
  LocalEditorShared.applyButtonActiveState(projectionToggleButton, projectionMode === 'orthographic');
1952
2162
  overlaySettingsButton.disabled = disabled;
1953
2163
  setToolbarButtonTooltip(overlaySettingsButton, '视口信息层设置');
@@ -2059,6 +2269,7 @@ export function createLocalEditorBrowserUi(options = {}) {
2059
2269
  ? 'var(--fps-editor-border)'
2060
2270
  : statusToneColor;
2061
2271
  renderCoordinateAxesOverlay(coordinateAxesOverlay, inEditor ? state.coordinateAxes ?? null : null);
2272
+ renderSceneFrameRateOverlay(sceneFrameRateOverlay, inEditor ? state.sceneFrameStats ?? null : null);
2062
2273
  renderSpatialOverlay(spatialOverlay, inEditor ? state.viewportSpatialOverlay ?? null : null);
2063
2274
  renderMeasurementOverlay(measurementOverlay, inEditor ? state.viewportMeasurement ?? null : null);
2064
2275
  const boxSelection = state.boxSelection;
@@ -2088,6 +2299,9 @@ export function createLocalEditorBrowserUi(options = {}) {
2088
2299
  update(state) {
2089
2300
  render(state);
2090
2301
  },
2302
+ updateSceneFrameStats(stats) {
2303
+ renderSceneFrameRateOverlay(sceneFrameRateOverlay, currentState?.mode === 'editor' ? stats : null);
2304
+ },
2091
2305
  setTheme(theme) {
2092
2306
  setActiveTheme(theme);
2093
2307
  },