@fps-games/editor 0.1.1-beta.9 → 0.1.2-beta.2

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