@fps-games/editor 0.1.2-beta.1 → 0.1.3-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 (110) hide show
  1. package/dist/local-editor-harness.d.ts +44 -8
  2. package/dist/local-editor-harness.d.ts.map +1 -1
  3. package/dist/local-editor-harness.js +436 -170
  4. package/dist/local-editor-harness.js.map +1 -1
  5. package/dist/local-editor-scene-render-scheduler.d.ts +8 -1
  6. package/dist/local-editor-scene-render-scheduler.d.ts.map +1 -1
  7. package/dist/local-editor-scene-render-scheduler.js +23 -2
  8. package/dist/local-editor-scene-render-scheduler.js.map +1 -1
  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/node_modules/@fps-games/editor-babylon/dist/editor-sky.d.ts +2 -2
  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 +31 -15
  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 +10 -0
  18. package/node_modules/@fps-games/editor-babylon/dist/editor-world.d.ts.map +1 -1
  19. package/node_modules/@fps-games/editor-babylon/dist/editor-world.js +31 -9
  20. package/node_modules/@fps-games/editor-babylon/dist/editor-world.js.map +1 -1
  21. package/node_modules/@fps-games/editor-babylon/dist/index.d.ts +1 -0
  22. package/node_modules/@fps-games/editor-babylon/dist/index.d.ts.map +1 -1
  23. package/node_modules/@fps-games/editor-babylon/dist/index.js +1 -0
  24. package/node_modules/@fps-games/editor-babylon/dist/index.js.map +1 -1
  25. package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.d.ts +24 -0
  26. package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.d.ts.map +1 -1
  27. package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.js +241 -48
  28. package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.js.map +1 -1
  29. package/node_modules/@fps-games/editor-babylon/dist/material-property-adapter.d.ts +3 -1
  30. package/node_modules/@fps-games/editor-babylon/dist/material-property-adapter.d.ts.map +1 -1
  31. package/node_modules/@fps-games/editor-babylon/dist/material-property-adapter.js +166 -0
  32. package/node_modules/@fps-games/editor-babylon/dist/material-property-adapter.js.map +1 -1
  33. package/node_modules/@fps-games/editor-babylon/dist/projection.d.ts +12 -1
  34. package/node_modules/@fps-games/editor-babylon/dist/projection.d.ts.map +1 -1
  35. package/node_modules/@fps-games/editor-babylon/dist/projection.js +231 -32
  36. package/node_modules/@fps-games/editor-babylon/dist/projection.js.map +1 -1
  37. package/node_modules/@fps-games/editor-babylon/dist/scene-view-camera-controller.d.ts +4 -0
  38. package/node_modules/@fps-games/editor-babylon/dist/scene-view-camera-controller.d.ts.map +1 -1
  39. package/node_modules/@fps-games/editor-babylon/dist/scene-view-camera-controller.js +37 -23
  40. package/node_modules/@fps-games/editor-babylon/dist/scene-view-camera-controller.js.map +1 -1
  41. package/node_modules/@fps-games/editor-babylon/dist/scene-view-input-controller.d.ts +1 -0
  42. package/node_modules/@fps-games/editor-babylon/dist/scene-view-input-controller.d.ts.map +1 -1
  43. package/node_modules/@fps-games/editor-babylon/dist/scene-view-input-controller.js +100 -8
  44. package/node_modules/@fps-games/editor-babylon/dist/scene-view-input-controller.js.map +1 -1
  45. package/node_modules/@fps-games/editor-babylon/dist/shadow-preview-controller.d.ts +22 -0
  46. package/node_modules/@fps-games/editor-babylon/dist/shadow-preview-controller.d.ts.map +1 -0
  47. package/node_modules/@fps-games/editor-babylon/dist/shadow-preview-controller.js +71 -0
  48. package/node_modules/@fps-games/editor-babylon/dist/shadow-preview-controller.js.map +1 -0
  49. package/node_modules/@fps-games/editor-babylon/dist/transform-gizmo-controller.d.ts +6 -1
  50. package/node_modules/@fps-games/editor-babylon/dist/transform-gizmo-controller.d.ts.map +1 -1
  51. package/node_modules/@fps-games/editor-babylon/dist/transform-gizmo-controller.js +107 -79
  52. package/node_modules/@fps-games/editor-babylon/dist/transform-gizmo-controller.js.map +1 -1
  53. package/node_modules/@fps-games/editor-babylon/dist/transform-solver.d.ts +30 -0
  54. package/node_modules/@fps-games/editor-babylon/dist/transform-solver.d.ts.map +1 -0
  55. package/node_modules/@fps-games/editor-babylon/dist/transform-solver.js +120 -0
  56. package/node_modules/@fps-games/editor-babylon/dist/transform-solver.js.map +1 -0
  57. package/node_modules/@fps-games/editor-babylon/dist/types.d.ts +3 -1
  58. package/node_modules/@fps-games/editor-babylon/dist/types.d.ts.map +1 -1
  59. package/node_modules/@fps-games/editor-babylon/package.json +5 -4
  60. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.d.ts.map +1 -1
  61. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.js +28 -0
  62. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.js.map +1 -1
  63. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.d.ts.map +1 -1
  64. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.js +103 -1
  65. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.js.map +1 -1
  66. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-view.d.ts +3 -1
  67. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-view.d.ts.map +1 -1
  68. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-view.js +49 -4
  69. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-view.js.map +1 -1
  70. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-input-router.d.ts +4 -1
  71. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-input-router.d.ts.map +1 -1
  72. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-input-router.js +12 -3
  73. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-input-router.js.map +1 -1
  74. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panel-registry.d.ts +3 -1
  75. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panel-registry.d.ts.map +1 -1
  76. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panel-registry.js +7 -0
  77. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panel-registry.js.map +1 -1
  78. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.d.ts +8 -3
  79. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.d.ts.map +1 -1
  80. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.js +645 -20
  81. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.js.map +1 -1
  82. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.js +1 -1
  83. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.js.map +1 -1
  84. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.d.ts.map +1 -1
  85. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.js +7 -6
  86. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.js.map +1 -1
  87. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts +150 -3
  88. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts.map +1 -1
  89. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.d.ts +6 -0
  90. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.d.ts.map +1 -1
  91. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.js +1 -0
  92. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.js.map +1 -1
  93. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.d.ts +3 -4
  94. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.d.ts.map +1 -1
  95. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js +1067 -87
  96. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js.map +1 -1
  97. package/node_modules/@fps-games/editor-browser/package.json +2 -2
  98. package/node_modules/@fps-games/editor-core/dist/scene-view-input.d.ts +7 -0
  99. package/node_modules/@fps-games/editor-core/dist/scene-view-input.d.ts.map +1 -1
  100. package/node_modules/@fps-games/editor-core/dist/transform-gizmo.d.ts +22 -0
  101. package/node_modules/@fps-games/editor-core/dist/transform-gizmo.d.ts.map +1 -1
  102. package/node_modules/@fps-games/editor-core/dist/transform-gizmo.js.map +1 -1
  103. package/node_modules/@fps-games/editor-core/package.json +2 -2
  104. package/node_modules/@fps-games/editor-forge-play/package.json +2 -2
  105. package/node_modules/@fps-games/editor-protocol/dist/index.d.ts +61 -0
  106. package/node_modules/@fps-games/editor-protocol/dist/index.d.ts.map +1 -1
  107. package/node_modules/@fps-games/editor-protocol/dist/index.js +157 -0
  108. package/node_modules/@fps-games/editor-protocol/dist/index.js.map +1 -1
  109. package/node_modules/@fps-games/editor-protocol/package.json +1 -1
  110. package/package.json +8 -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,33 +16,15 @@ 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, formatLocalEditorBrowserInspectorNumberValue, formatLocalEditorBrowserInspectorValue, resolveLocalEditorBrowserInspectorSectionStatus, resolveLocalEditorBrowserInspectorControlRegistration, } from './local-editor-ui-panels';
19
+ export { applyLocalEditorBrowserInspectorControlBinding, createLocalEditorBrowserInspectorControlRegistry, formatLocalEditorBrowserInspectorNumberValue, formatLocalEditorBrowserInspectorValue, parseLocalEditorBrowserInspectorNumberValue, 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
- 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
+ const PENDING_NUMERIC_PROPERTY_INPUT = Symbol('pending-numeric-property-input');
23
+ function readInspectorInputValue(input, source) {
41
24
  const control = input.dataset.serializedControl;
42
25
  const valueType = input.dataset.serializedValueType;
43
26
  if ((control === 'vec2' || control === 'vec3') && input instanceof HTMLInputElement) {
44
- return readInspectorVectorInputValue(input, mode);
27
+ return readInspectorVectorInputValue(input, source) ?? PENDING_NUMERIC_PROPERTY_INPUT;
45
28
  }
46
29
  if (control === 'enum' && input instanceof HTMLSelectElement) {
47
30
  const option = input.selectedOptions.item(0);
@@ -60,7 +43,9 @@ function readInspectorInputValue(input, mode = 'final') {
60
43
  if (control === 'boolean' || valueType === 'boolean')
61
44
  return input.value === 'true';
62
45
  if (control === 'number' || valueType === 'number') {
63
- return parseLocalEditorBrowserInspectorNumberValue(input.value, mode) ?? INSPECTOR_INPUT_PENDING;
46
+ const mode = source === 'input' && readInspectorCommitMode(input) === 'live' ? 'live' : 'final';
47
+ return LocalEditorPanels.parseLocalEditorBrowserInspectorNumberValue(input.value, mode)
48
+ ?? PENDING_NUMERIC_PROPERTY_INPUT;
64
49
  }
65
50
  if (control === 'color' && input instanceof HTMLInputElement && input.type === 'color') {
66
51
  const value = input.value.replace('#', '');
@@ -168,9 +153,10 @@ function setToolbarButtonIcon(doc, button, icon) {
168
153
  else
169
154
  button.prepend(nextIcon);
170
155
  }
171
- function readInspectorVectorInputValue(input, mode) {
156
+ function readInspectorVectorInputValue(input, source) {
172
157
  const wrapper = input.closest('[data-inspector-vector-control]');
173
158
  const values = {};
159
+ const mode = source === 'input' && readInspectorCommitMode(input) === 'live' ? 'live' : 'final';
174
160
  const fields = wrapper
175
161
  ? Array.from(wrapper.querySelectorAll('input[data-serialized-vector-axis]'))
176
162
  : [input];
@@ -178,21 +164,21 @@ function readInspectorVectorInputValue(input, mode) {
178
164
  const axis = field.dataset.serializedVectorAxis;
179
165
  if (!axis)
180
166
  continue;
181
- const numeric = parseLocalEditorBrowserInspectorNumberValue(field.value, mode);
167
+ const numeric = LocalEditorPanels.parseLocalEditorBrowserInspectorNumberValue(field.value, mode);
182
168
  if (numeric == null)
183
- return INSPECTOR_INPUT_PENDING;
169
+ return null;
184
170
  values[axis] = numeric;
185
171
  }
186
172
  return values;
187
173
  }
188
- function createInspectorPropertyInput(input, source, numberParseMode = 'final') {
174
+ function createInspectorPropertyInput(input, source) {
189
175
  if (!input.dataset.serializedPath || !input.dataset.serializedTargetId)
190
176
  return null;
191
177
  const targetIds = input.dataset.serializedTargetIds
192
178
  ? input.dataset.serializedTargetIds.split(',').filter(Boolean)
193
179
  : undefined;
194
- const value = readInspectorInputValue(input, numberParseMode);
195
- if (value === INSPECTOR_INPUT_PENDING)
180
+ const value = readInspectorInputValue(input, source);
181
+ if (value === PENDING_NUMERIC_PROPERTY_INPUT)
196
182
  return null;
197
183
  return {
198
184
  targetId: input.dataset.serializedTargetId,
@@ -206,16 +192,63 @@ function createInspectorPropertyInput(input, source, numberParseMode = 'final')
206
192
  source: input.dataset.serializedEditSource ?? source,
207
193
  };
208
194
  }
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;
195
+ function readRenderingInputValue(input, source) {
196
+ const control = input.dataset.renderingControl;
197
+ const valueType = input.dataset.renderingValueType;
198
+ if (input instanceof HTMLInputElement && input.type === 'checkbox')
199
+ return input.checked;
200
+ if (input instanceof HTMLInputElement && input.type === 'color') {
201
+ const value = input.value.replace('#', '');
202
+ const numeric = Number.parseInt(value, 16);
203
+ if (!Number.isFinite(numeric))
204
+ return null;
205
+ return {
206
+ r: ((numeric >> 16) & 255) / 255,
207
+ g: ((numeric >> 8) & 255) / 255,
208
+ b: (numeric & 255) / 255,
209
+ };
210
+ }
211
+ if (control === 'number' || valueType === 'number') {
212
+ const mode = source === 'input' && readRenderingCommitMode(input) === 'live' ? 'live' : 'final';
213
+ return LocalEditorPanels.parseLocalEditorBrowserInspectorNumberValue(input.value, mode)
214
+ ?? PENDING_NUMERIC_PROPERTY_INPUT;
215
+ }
216
+ if (control === 'string-list' || valueType === 'string-list') {
217
+ return input.value
218
+ .split(/\r?\n/)
219
+ .map(value => value.trim())
220
+ .filter(Boolean);
221
+ }
222
+ if (control === 'boolean' || valueType === 'boolean')
223
+ return input.value === 'true';
224
+ return input.value;
225
+ }
226
+ function createRenderingPropertyInput(input, source) {
227
+ const sectionId = input.dataset.renderingSectionId;
228
+ const systemId = input.dataset.renderingSystemId;
229
+ const path = input.dataset.renderingPath;
230
+ if (!sectionId || !systemId || !path)
231
+ return null;
232
+ const value = readRenderingInputValue(input, source);
233
+ if (value === PENDING_NUMERIC_PROPERTY_INPUT)
234
+ return null;
235
+ return {
236
+ sectionId,
237
+ systemId,
238
+ path,
239
+ value,
240
+ control: input.dataset.renderingControl,
241
+ valueType: input.dataset.renderingValueType,
242
+ commitMode: input.dataset.renderingCommitMode,
243
+ source,
244
+ };
215
245
  }
216
246
  function readInspectorCommitMode(input) {
217
247
  return input.dataset.serializedCommitMode ?? 'live';
218
248
  }
249
+ function readRenderingCommitMode(input) {
250
+ return input.dataset.renderingCommitMode ?? 'live';
251
+ }
219
252
  function readInspectorImmediateSource(input) {
220
253
  if (input instanceof HTMLSelectElement)
221
254
  return 'select';
@@ -225,6 +258,15 @@ function readInspectorImmediateSource(input) {
225
258
  return 'color';
226
259
  return 'input';
227
260
  }
261
+ function readRenderingImmediateSource(input) {
262
+ if (input instanceof HTMLInputElement && input.type === 'checkbox')
263
+ return 'toggle';
264
+ if (input instanceof HTMLInputElement && input.type === 'color')
265
+ return 'color';
266
+ if (input instanceof HTMLTextAreaElement)
267
+ return 'list';
268
+ return 'input';
269
+ }
228
270
  function createSceneFrameRateOverlay(doc) {
229
271
  const root = doc.createElement('div');
230
272
  root.classList.add(LOCAL_EDITOR_THEME_CLASS);
@@ -725,6 +767,170 @@ function renderMeasurementOverlay(overlay, state) {
725
767
  function formatSvgNumber(value) {
726
768
  return Number.isFinite(value) ? value.toFixed(2) : '0';
727
769
  }
770
+ function isLocalEditorAssetBrowserTab(value) {
771
+ return value === 'all' || value === 'models' || value === 'materials' || value === 'textures';
772
+ }
773
+ function createAssetBrowserMenuItems(state, asset) {
774
+ const assetKind = resolveBrowserAssetKind(asset);
775
+ const placeable = asset.placeable !== false && asset.disabled !== true;
776
+ const activeId = state.activeId;
777
+ const items = [];
778
+ if (assetKind === 'material') {
779
+ items.push({
780
+ id: 'asset.edit-material',
781
+ label: '编辑材质球',
782
+ disabled: !asset.material,
783
+ disabledReason: '该材质资产缺少可编辑参数。',
784
+ });
785
+ }
786
+ else if (assetKind === 'texture' && asset.preview) {
787
+ items.push({
788
+ id: 'asset.preview',
789
+ label: '查看贴图预览',
790
+ });
791
+ }
792
+ if (assetKind === 'material') {
793
+ items.push({
794
+ id: 'asset.apply-material',
795
+ label: '应用到当前 GameObject',
796
+ separatorBefore: true,
797
+ disabled: !activeId,
798
+ disabledReason: '请先在 Hierarchy 或 Scene View 中选择一个 GameObject。',
799
+ });
800
+ }
801
+ else if (assetKind === 'texture') {
802
+ items.push({
803
+ id: 'asset.add-to-scene',
804
+ label: '作为地贴添加到场景',
805
+ separatorBefore: asset.preview ? true : undefined,
806
+ disabled: !placeable,
807
+ disabledReason: '该贴图当前不可放置。',
808
+ });
809
+ items.push({
810
+ id: 'asset.place',
811
+ label: '进入地面放置模式',
812
+ disabled: !placeable,
813
+ disabledReason: '该贴图当前不可放置。',
814
+ });
815
+ items.push({
816
+ id: 'asset.use-base-texture',
817
+ label: '用作当前材质 Base 图',
818
+ disabled: true,
819
+ disabledReason: 'Base 图材质字段将在下一步接入。',
820
+ separatorBefore: true,
821
+ });
822
+ items.push({
823
+ id: 'asset.use-emission-mask',
824
+ label: '用作当前材质自发光遮罩',
825
+ disabled: true,
826
+ disabledReason: '自发光遮罩绑定将在下一步接入。',
827
+ });
828
+ }
829
+ else {
830
+ items.push({
831
+ id: 'asset.add-to-scene',
832
+ label: '添加到场景',
833
+ disabled: !placeable,
834
+ disabledReason: '该模型当前不可放置。',
835
+ });
836
+ items.push({
837
+ id: 'asset.place',
838
+ label: '进入放置模式',
839
+ disabled: !placeable,
840
+ disabledReason: '该模型当前不可放置。',
841
+ });
842
+ items.push({
843
+ id: 'asset.add-as-child',
844
+ label: '添加为当前节点子对象',
845
+ disabled: true,
846
+ disabledReason: '子对象添加动作将在资产 patch 中接入。',
847
+ separatorBefore: true,
848
+ });
849
+ items.push({
850
+ id: 'asset.replace-selected-model',
851
+ label: '替换当前模型',
852
+ disabled: true,
853
+ disabledReason: '模型替换动作将在资产 patch 中接入。',
854
+ });
855
+ }
856
+ items.push({
857
+ id: 'asset.copy-id',
858
+ label: '复制 Asset ID',
859
+ separatorBefore: true,
860
+ });
861
+ return items;
862
+ }
863
+ function resolveBrowserAssetKind(asset) {
864
+ const kind = `${asset.kind ?? ''}`.trim().toLowerCase();
865
+ const id = `${asset.id ?? ''}`.trim().toLowerCase();
866
+ const assetId = `${asset.assetId ?? ''}`.trim().toLowerCase();
867
+ if (kind === 'material' || id.startsWith('material:') || assetId.startsWith('mat_'))
868
+ return 'material';
869
+ if (kind === 'texture' || kind === 'image' || id.startsWith('texture:') || assetId.startsWith('texture_'))
870
+ return 'texture';
871
+ return 'model';
872
+ }
873
+ function createAssetBrowserActionInput(actionId, asset) {
874
+ return {
875
+ actionId,
876
+ assetId: asset.assetId ?? asset.id,
877
+ browserAssetId: asset.id,
878
+ assetKind: asset.kind,
879
+ };
880
+ }
881
+ function copyTextToClipboard(win, text) {
882
+ void win?.navigator?.clipboard?.writeText?.(text);
883
+ }
884
+ function readInspectorAssetPickerOptions(property) {
885
+ const title = readControlOptionString(property, 'pickerTitle') || property.label;
886
+ const rawCandidates = property.controlOptions?.candidates;
887
+ const candidates = Array.isArray(rawCandidates)
888
+ ? rawCandidates.flatMap(readInspectorAssetPickerOption)
889
+ : [];
890
+ return { title, candidates };
891
+ }
892
+ function readInspectorAssetPickerOption(value) {
893
+ if (!value || typeof value !== 'object' || Array.isArray(value))
894
+ return [];
895
+ const record = value;
896
+ const label = typeof record.label === 'string' ? record.label : '';
897
+ const optionValue = typeof record.value === 'string' ? record.value : '';
898
+ if (!label && !optionValue)
899
+ return [];
900
+ return [{
901
+ label: label || optionValue,
902
+ value: optionValue,
903
+ ...(typeof record.meta === 'string' ? { meta: record.meta } : {}),
904
+ ...(typeof record.kind === 'string' ? { kind: record.kind } : {}),
905
+ ...(isAssetPreview(record.preview) ? { preview: record.preview } : {}),
906
+ }];
907
+ }
908
+ function readControlOptionString(property, key) {
909
+ const value = property.controlOptions?.[key];
910
+ return typeof value === 'string' ? value : '';
911
+ }
912
+ function isAssetPreview(value) {
913
+ if (!value || typeof value !== 'object' || Array.isArray(value))
914
+ return false;
915
+ const record = value;
916
+ if (record.kind === 'image')
917
+ return typeof record.url === 'string' && record.url.length > 0;
918
+ return record.kind === 'material-sphere';
919
+ }
920
+ function findInspectorPropertyForInput(state, input) {
921
+ const path = input.dataset.serializedPath;
922
+ if (!path)
923
+ return null;
924
+ const objects = [state.inspectorObject, state.inspectorMultiObject].filter(Boolean);
925
+ for (const object of objects) {
926
+ for (const section of object.sections) {
927
+ const property = section.properties.find(candidate => candidate.path === path);
928
+ if (property)
929
+ return property;
930
+ }
931
+ }
932
+ return null;
933
+ }
728
934
  export function createLocalEditorBrowserUi(options = {}) {
729
935
  const doc = options.document ?? document;
730
936
  const root = options.root ?? doc.body;
@@ -732,7 +938,9 @@ export function createLocalEditorBrowserUi(options = {}) {
732
938
  const localTestActionsEnabled = options.localTestActions === true;
733
939
  ensureLocalEditorTheme(doc);
734
940
  let activeTheme = normalizeLocalEditorThemeName(options.theme);
735
- const inputRouter = createLocalEditorWorkbenchInputRouter(doc);
941
+ const inputRouter = createLocalEditorWorkbenchInputRouter(doc, {
942
+ isShortcutReserved: options.input?.isShortcutReserved,
943
+ });
736
944
  const contextMenu = createLocalEditorContextMenuController(doc, (open) => {
737
945
  inputRouter.setContextMenuOpen(open);
738
946
  }, activeTheme);
@@ -742,6 +950,7 @@ export function createLocalEditorBrowserUi(options = {}) {
742
950
  });
743
951
  let currentState = null;
744
952
  let inspectorFilter = '';
953
+ let assetBrowserTab = 'all';
745
954
  const workbenchLayout = createDefaultLocalEditorWorkbenchLayout();
746
955
  const panelRegistry = createLocalEditorPanelRegistry(workbenchLayout);
747
956
  const hostChrome = doc.createElement('div');
@@ -833,15 +1042,26 @@ export function createLocalEditorBrowserUi(options = {}) {
833
1042
  const undoButton = createToolbarIconButton(doc, '撤销', 'undo', '撤销');
834
1043
  const redoButton = createToolbarIconButton(doc, '重做', 'redo', '重做');
835
1044
  let helpOpen = false;
1045
+ let assetPreviewOpen = false;
1046
+ let previewedAsset = null;
1047
+ let assetPreviewMode = 'preview';
1048
+ let assetPickerOpen = false;
1049
+ let assetPickerFilter = '';
1050
+ let assetPickerInput = null;
1051
+ let assetPickerProperty = null;
836
1052
  const editorStatusButton = createToolbarIconButton(doc, '编辑器状态', 'status', '编辑器状态');
837
1053
  editorStatusButton.dataset.editorStatusButton = 'true';
838
1054
  editorStatusButton.style.cursor = 'help';
839
1055
  const workbench = createLocalEditorWorkbench(doc);
840
1056
  const hierarchyPanel = createWorkbenchPanelContent(doc);
841
1057
  const assetPanel = workbench.bottomDock;
1058
+ const rightDockTabs = doc.createElement('div');
842
1059
  const inspectorPanel = createWorkbenchPanelContent(doc);
1060
+ const renderingPanel = createWorkbenchPanelContent(doc);
843
1061
  workbench.leftDock.appendChild(hierarchyPanel);
1062
+ workbench.rightDock.appendChild(rightDockTabs);
844
1063
  workbench.rightDock.appendChild(inspectorPanel);
1064
+ workbench.rightDock.appendChild(renderingPanel);
845
1065
  root.appendChild(workbench.root);
846
1066
  tooltipSurfaces.add(workbench.root);
847
1067
  const workbenchLayoutController = createLocalEditorWorkbenchLayoutController(doc, workbench);
@@ -1120,17 +1340,17 @@ export function createLocalEditorBrowserUi(options = {}) {
1120
1340
  sceneUtilityActions.appendChild(gridToggleButton);
1121
1341
  sceneUtilityActions.appendChild(themeToggleButton);
1122
1342
  sceneUtilityActions.appendChild(sceneHelpButton);
1123
- const mainCameraPreviewGroup = doc.createElement('div');
1124
- mainCameraPreviewGroup.style.cssText = [
1343
+ const cameraPreviewGroup = doc.createElement('div');
1344
+ cameraPreviewGroup.style.cssText = [
1125
1345
  'display:flex',
1126
1346
  'align-items:center',
1127
1347
  'gap:4px',
1128
1348
  'padding-left:8px',
1129
1349
  'border-left:1px solid var(--fps-editor-divider)',
1130
1350
  ].join(';');
1131
- const mainCameraPreviewButton = createToolbarIconButton(doc, '从 Main Camera 查看场景', 'camera');
1132
- mainCameraPreviewButton.dataset.mainCameraPreviewToggle = 'true';
1133
- mainCameraPreviewGroup.appendChild(mainCameraPreviewButton);
1351
+ const sceneCameraButton = createToolbarIconButton(doc, '从 Main Camera 查看场景', 'camera');
1352
+ sceneCameraButton.dataset.sceneCameraPreviewToggle = 'true';
1353
+ cameraPreviewGroup.appendChild(sceneCameraButton);
1134
1354
  const viewportToolsGroup = doc.createElement('div');
1135
1355
  viewportToolsGroup.style.cssText = [
1136
1356
  'display:flex',
@@ -1235,7 +1455,7 @@ export function createLocalEditorBrowserUi(options = {}) {
1235
1455
  sceneToolOverlay.appendChild(actionGroup);
1236
1456
  sceneToolOverlay.appendChild(viewportToolsGroup);
1237
1457
  sceneToolOverlay.appendChild(sceneUtilityActions);
1238
- sceneToolOverlay.appendChild(mainCameraPreviewGroup);
1458
+ sceneToolOverlay.appendChild(cameraPreviewGroup);
1239
1459
  sceneToolOverlay.appendChild(editorStatusButton);
1240
1460
  sceneToolOverlay.appendChild(toolbarOverflowButton);
1241
1461
  workbench.sceneHeader.appendChild(sceneToolOverlay);
@@ -1272,6 +1492,164 @@ export function createLocalEditorBrowserUi(options = {}) {
1272
1492
  shortcutHelpPanel.classList.add(LOCAL_EDITOR_THEME_CLASS);
1273
1493
  root.appendChild(shortcutHelpPanel);
1274
1494
  tooltipSurfaces.add(shortcutHelpPanel);
1495
+ const assetPreviewModal = doc.createElement('div');
1496
+ assetPreviewModal.classList.add(LOCAL_EDITOR_THEME_CLASS);
1497
+ assetPreviewModal.dataset.editorAssetPreviewModal = 'true';
1498
+ assetPreviewModal.dataset.editorWorkbenchRegion = 'modal';
1499
+ assetPreviewModal.style.cssText = [
1500
+ 'position:fixed',
1501
+ 'inset:0',
1502
+ 'z-index:2147483642',
1503
+ 'display:none',
1504
+ 'align-items:center',
1505
+ 'justify-content:center',
1506
+ 'padding:32px',
1507
+ 'background:rgba(0,0,0,0.45)',
1508
+ 'font-family:var(--fps-editor-font)',
1509
+ 'color:var(--fps-editor-text)',
1510
+ 'pointer-events:auto',
1511
+ ].join(';');
1512
+ const assetPreviewDialog = doc.createElement('div');
1513
+ assetPreviewDialog.setAttribute('role', 'dialog');
1514
+ assetPreviewDialog.setAttribute('aria-modal', 'true');
1515
+ assetPreviewDialog.setAttribute('aria-label', '资产预览');
1516
+ assetPreviewDialog.style.cssText = [
1517
+ 'width:min(420px,calc(100vw - 64px))',
1518
+ 'max-height:calc(100vh - 64px)',
1519
+ 'display:flex',
1520
+ 'flex-direction:column',
1521
+ 'gap:12px',
1522
+ 'padding:14px',
1523
+ 'border:1px solid var(--fps-editor-border)',
1524
+ 'border-radius:4px',
1525
+ 'background:var(--fps-editor-panel)',
1526
+ 'box-shadow:var(--fps-editor-shadow-modal)',
1527
+ 'overflow:auto',
1528
+ ].join(';');
1529
+ const assetPreviewHeader = doc.createElement('div');
1530
+ assetPreviewHeader.style.cssText = 'display:flex;align-items:center;justify-content:space-between;gap:12px';
1531
+ const assetPreviewTitle = doc.createElement('h2');
1532
+ assetPreviewTitle.style.cssText = [
1533
+ 'min-width:0',
1534
+ 'margin:0',
1535
+ 'overflow:hidden',
1536
+ 'text-overflow:ellipsis',
1537
+ 'white-space:nowrap',
1538
+ 'font-size:13px',
1539
+ 'font-weight:900',
1540
+ 'color:var(--fps-editor-text-strong)',
1541
+ ].join(';');
1542
+ const assetPreviewCloseButton = LocalEditorShared.createButton(doc, '关闭');
1543
+ assetPreviewCloseButton.dataset.editorAssetPreviewClose = 'true';
1544
+ assetPreviewHeader.appendChild(assetPreviewTitle);
1545
+ assetPreviewHeader.appendChild(assetPreviewCloseButton);
1546
+ const assetPreviewBody = doc.createElement('div');
1547
+ assetPreviewBody.style.cssText = [
1548
+ 'display:flex',
1549
+ 'align-items:center',
1550
+ 'justify-content:center',
1551
+ 'min-height:240px',
1552
+ 'border:1px solid var(--fps-editor-divider)',
1553
+ 'border-radius:4px',
1554
+ 'background:var(--fps-editor-panel-soft)',
1555
+ ].join(';');
1556
+ const assetPreviewMeta = doc.createElement('div');
1557
+ assetPreviewMeta.style.cssText = [
1558
+ 'min-height:16px',
1559
+ 'overflow:hidden',
1560
+ 'text-overflow:ellipsis',
1561
+ 'white-space:nowrap',
1562
+ 'color:var(--fps-editor-muted)',
1563
+ 'font-size:11px',
1564
+ 'font-weight:800',
1565
+ ].join(';');
1566
+ assetPreviewDialog.appendChild(assetPreviewHeader);
1567
+ assetPreviewDialog.appendChild(assetPreviewBody);
1568
+ assetPreviewDialog.appendChild(assetPreviewMeta);
1569
+ assetPreviewModal.appendChild(assetPreviewDialog);
1570
+ root.appendChild(assetPreviewModal);
1571
+ tooltipSurfaces.add(assetPreviewModal);
1572
+ const assetPickerModal = doc.createElement('div');
1573
+ assetPickerModal.classList.add(LOCAL_EDITOR_THEME_CLASS);
1574
+ assetPickerModal.dataset.editorAssetPickerModal = 'true';
1575
+ assetPickerModal.dataset.editorWorkbenchRegion = 'modal';
1576
+ assetPickerModal.style.cssText = [
1577
+ 'position:fixed',
1578
+ 'inset:0',
1579
+ 'z-index:2147483642',
1580
+ 'display:none',
1581
+ 'align-items:center',
1582
+ 'justify-content:center',
1583
+ 'padding:32px',
1584
+ 'background:rgba(0,0,0,0.45)',
1585
+ 'font-family:var(--fps-editor-font)',
1586
+ 'color:var(--fps-editor-text)',
1587
+ 'pointer-events:auto',
1588
+ ].join(';');
1589
+ const assetPickerDialog = doc.createElement('div');
1590
+ assetPickerDialog.setAttribute('role', 'dialog');
1591
+ assetPickerDialog.setAttribute('aria-modal', 'true');
1592
+ assetPickerDialog.setAttribute('aria-label', '资产选择');
1593
+ assetPickerDialog.style.cssText = [
1594
+ 'width:min(620px,calc(100vw - 64px))',
1595
+ 'max-height:calc(100vh - 64px)',
1596
+ 'display:flex',
1597
+ 'flex-direction:column',
1598
+ 'gap:10px',
1599
+ 'padding:14px',
1600
+ 'border:1px solid var(--fps-editor-border)',
1601
+ 'border-radius:4px',
1602
+ 'background:var(--fps-editor-panel)',
1603
+ 'box-shadow:var(--fps-editor-shadow-modal)',
1604
+ 'overflow:hidden',
1605
+ ].join(';');
1606
+ const assetPickerHeader = doc.createElement('div');
1607
+ assetPickerHeader.style.cssText = 'display:flex;align-items:center;justify-content:space-between;gap:12px';
1608
+ const assetPickerTitle = doc.createElement('h2');
1609
+ assetPickerTitle.style.cssText = [
1610
+ 'min-width:0',
1611
+ 'margin:0',
1612
+ 'overflow:hidden',
1613
+ 'text-overflow:ellipsis',
1614
+ 'white-space:nowrap',
1615
+ 'font-size:13px',
1616
+ 'font-weight:900',
1617
+ 'color:var(--fps-editor-text-strong)',
1618
+ ].join(';');
1619
+ const assetPickerCloseButton = LocalEditorShared.createButton(doc, '关闭');
1620
+ assetPickerCloseButton.dataset.editorAssetPickerClose = 'true';
1621
+ assetPickerHeader.appendChild(assetPickerTitle);
1622
+ assetPickerHeader.appendChild(assetPickerCloseButton);
1623
+ const assetPickerSearch = doc.createElement('input');
1624
+ assetPickerSearch.type = 'search';
1625
+ assetPickerSearch.dataset.editorAssetPickerSearch = 'true';
1626
+ assetPickerSearch.placeholder = '搜索资产';
1627
+ assetPickerSearch.style.cssText = [
1628
+ 'width:100%',
1629
+ 'height:30px',
1630
+ 'border:1px solid var(--fps-editor-border)',
1631
+ 'border-radius:3px',
1632
+ 'background:var(--fps-editor-field)',
1633
+ 'color:var(--fps-editor-text)',
1634
+ 'font-size:12px',
1635
+ 'padding:0 8px',
1636
+ ].join(';');
1637
+ const assetPickerList = doc.createElement('div');
1638
+ assetPickerList.style.cssText = [
1639
+ 'min-height:180px',
1640
+ 'max-height:min(430px,calc(100vh - 196px))',
1641
+ 'overflow:auto',
1642
+ 'display:grid',
1643
+ 'grid-template-columns:repeat(auto-fill,minmax(176px,1fr))',
1644
+ 'gap:7px',
1645
+ 'padding-right:2px',
1646
+ ].join(';');
1647
+ assetPickerDialog.appendChild(assetPickerHeader);
1648
+ assetPickerDialog.appendChild(assetPickerSearch);
1649
+ assetPickerDialog.appendChild(assetPickerList);
1650
+ assetPickerModal.appendChild(assetPickerDialog);
1651
+ root.appendChild(assetPickerModal);
1652
+ tooltipSurfaces.add(assetPickerModal);
1275
1653
  function applyThemeToSurfaces() {
1276
1654
  applyLocalEditorTheme(hostChrome, activeTheme);
1277
1655
  applyLocalEditorTheme(workbench.root, activeTheme);
@@ -1279,6 +1657,8 @@ export function createLocalEditorBrowserUi(options = {}) {
1279
1657
  applyLocalEditorTheme(measurementOverlay.root, activeTheme);
1280
1658
  applyLocalEditorTheme(sceneFrameRateOverlay.root, activeTheme);
1281
1659
  applyLocalEditorTheme(shortcutHelpPanel, activeTheme);
1660
+ applyLocalEditorTheme(assetPreviewModal, activeTheme);
1661
+ applyLocalEditorTheme(assetPickerModal, activeTheme);
1282
1662
  applyLocalEditorTheme(boxSelectionOverlay, activeTheme);
1283
1663
  applyLocalEditorTheme(localTestMenu, activeTheme);
1284
1664
  applyLocalEditorTheme(toolbarOverflowMenu, activeTheme);
@@ -1490,14 +1870,14 @@ export function createLocalEditorBrowserUi(options = {}) {
1490
1870
  actionGroup,
1491
1871
  viewportToolsGroup,
1492
1872
  sceneUtilityActions,
1493
- mainCameraPreviewGroup,
1873
+ cameraPreviewGroup,
1494
1874
  editorStatusButton,
1495
1875
  toolbarOverflowButton,
1496
1876
  ];
1497
1877
  const toolbarOverflowItems = [
1498
1878
  { id: 'editor-status', element: editorStatusButton, kind: 'group' },
1499
1879
  { id: 'scene-utilities', element: sceneUtilityActions, kind: 'group' },
1500
- { id: 'main-camera-preview', element: mainCameraPreviewGroup, kind: 'group' },
1880
+ { id: 'camera-preview', element: cameraPreviewGroup, kind: 'group' },
1501
1881
  { id: 'viewport-tools', element: viewportToolsGroup, kind: 'group' },
1502
1882
  { id: 'transform-actions', element: actionGroup, kind: 'group' },
1503
1883
  { id: 'placement', element: placementGroup, kind: 'group' },
@@ -1702,9 +2082,9 @@ export function createLocalEditorBrowserUi(options = {}) {
1702
2082
  const visible = currentState?.grid?.visible ?? true;
1703
2083
  callbacks.onGridVisibleChange?.(!visible);
1704
2084
  });
1705
- mainCameraPreviewButton.addEventListener('click', () => {
1706
- const enabled = currentState?.mainCameraPreview?.enabled ?? false;
1707
- callbacks.onMainCameraPreviewToggle?.(!enabled);
2085
+ sceneCameraButton.addEventListener('click', () => {
2086
+ const enabled = currentState?.sceneCameraPreview?.enabled ?? false;
2087
+ callbacks.onSceneCameraPreviewToggle?.(!enabled);
1708
2088
  });
1709
2089
  const toggleCoordinateAxesProjection = (event) => {
1710
2090
  event.preventDefault();
@@ -1763,10 +2143,414 @@ export function createLocalEditorBrowserUi(options = {}) {
1763
2143
  if (open)
1764
2144
  contextMenu.close();
1765
2145
  helpOpen = open;
1766
- inputRouter.setModalOpen(open);
2146
+ syncInputModalOpen();
1767
2147
  shortcutHelpPanel.style.display = helpOpen ? '' : 'none';
1768
2148
  LocalEditorShared.applyButtonActiveState(sceneHelpButton, helpOpen);
1769
2149
  };
2150
+ const openAssetPreviewModal = (asset) => {
2151
+ if (!asset.preview)
2152
+ return;
2153
+ contextMenu.close();
2154
+ previewedAsset = asset;
2155
+ assetPreviewMode = 'preview';
2156
+ assetPreviewDialog.setAttribute('aria-label', '资产预览');
2157
+ assetPreviewOpen = true;
2158
+ renderAssetPreviewModal();
2159
+ syncInputModalOpen();
2160
+ assetPreviewCloseButton.focus({ preventScroll: true });
2161
+ };
2162
+ const openAssetMaterialEditorModal = (asset) => {
2163
+ if (!asset.material)
2164
+ return;
2165
+ contextMenu.close();
2166
+ previewedAsset = asset;
2167
+ assetPreviewMode = 'material-editor';
2168
+ assetPreviewDialog.setAttribute('aria-label', '编辑材质球');
2169
+ assetPreviewOpen = true;
2170
+ renderAssetPreviewModal();
2171
+ syncInputModalOpen();
2172
+ assetPreviewCloseButton.focus({ preventScroll: true });
2173
+ };
2174
+ const closeAssetPreviewModal = () => {
2175
+ if (!assetPreviewOpen)
2176
+ return;
2177
+ assetPreviewOpen = false;
2178
+ previewedAsset = null;
2179
+ assetPreviewMode = 'preview';
2180
+ renderAssetPreviewModal();
2181
+ syncInputModalOpen();
2182
+ };
2183
+ const openInspectorAssetPicker = (input, property) => {
2184
+ contextMenu.close();
2185
+ if (helpOpen)
2186
+ setShortcutHelpOpen(false);
2187
+ assetPickerInput = input;
2188
+ assetPickerProperty = property;
2189
+ assetPickerFilter = '';
2190
+ assetPickerOpen = true;
2191
+ closeAssetPreviewModal();
2192
+ renderAssetPickerModal();
2193
+ syncInputModalOpen();
2194
+ assetPickerSearch.focus({ preventScroll: true });
2195
+ assetPickerSearch.select();
2196
+ };
2197
+ const closeInspectorAssetPicker = () => {
2198
+ if (!assetPickerOpen)
2199
+ return;
2200
+ assetPickerOpen = false;
2201
+ assetPickerInput = null;
2202
+ assetPickerProperty = null;
2203
+ assetPickerFilter = '';
2204
+ renderAssetPickerModal();
2205
+ syncInputModalOpen();
2206
+ };
2207
+ function syncInputModalOpen() {
2208
+ inputRouter.setModalOpen(currentState?.mode === 'editor' && (helpOpen || assetPreviewOpen || assetPickerOpen));
2209
+ }
2210
+ function resolvePreviewedAssetFromState() {
2211
+ if (!previewedAsset)
2212
+ return null;
2213
+ const refreshed = currentState?.assets.find(asset => asset.id === previewedAsset?.id)
2214
+ ?? currentState?.assets.find(asset => (asset.assetId ?? asset.id) === (previewedAsset?.assetId ?? previewedAsset?.id))
2215
+ ?? null;
2216
+ if (refreshed)
2217
+ previewedAsset = refreshed;
2218
+ return previewedAsset;
2219
+ }
2220
+ function renderAssetPreviewModal() {
2221
+ const asset = resolvePreviewedAssetFromState();
2222
+ if (!assetPreviewOpen || !asset || (assetPreviewMode === 'preview' && !asset.preview) || (assetPreviewMode === 'material-editor' && !asset.material)) {
2223
+ assetPreviewModal.style.display = 'none';
2224
+ assetPreviewTitle.textContent = '';
2225
+ assetPreviewMeta.textContent = '';
2226
+ while (assetPreviewBody.firstChild)
2227
+ assetPreviewBody.removeChild(assetPreviewBody.firstChild);
2228
+ return;
2229
+ }
2230
+ if (assetPreviewMode === 'material-editor' && asset.material) {
2231
+ renderAssetMaterialEditorModal(asset);
2232
+ assetPreviewModal.style.display = 'flex';
2233
+ return;
2234
+ }
2235
+ assetPreviewDialog.style.width = 'min(420px,calc(100vw - 64px))';
2236
+ assetPreviewBody.style.cssText = [
2237
+ 'display:flex',
2238
+ 'align-items:center',
2239
+ 'justify-content:center',
2240
+ 'min-height:240px',
2241
+ 'border:1px solid var(--fps-editor-divider)',
2242
+ 'border-radius:4px',
2243
+ 'background:var(--fps-editor-panel-soft)',
2244
+ ].join(';');
2245
+ assetPreviewTitle.textContent = asset.label;
2246
+ assetPreviewMeta.textContent = [
2247
+ asset.kind ? `Kind: ${asset.kind}` : '',
2248
+ asset.meta ?? asset.assetId ?? asset.id,
2249
+ ].filter(Boolean).join(' · ');
2250
+ while (assetPreviewBody.firstChild)
2251
+ assetPreviewBody.removeChild(assetPreviewBody.firstChild);
2252
+ assetPreviewBody.appendChild(LocalEditorPanels.createAssetPreviewElement(doc, asset, 'modal'));
2253
+ assetPreviewModal.style.display = 'flex';
2254
+ }
2255
+ function renderAssetMaterialEditorModal(asset) {
2256
+ const material = asset.material;
2257
+ const profile = material.profile ?? {};
2258
+ const baseColor = profile.baseColor ?? {};
2259
+ const emission = profile.emission ?? {};
2260
+ const readonly = material.readonly === true;
2261
+ const materialKind = material.materialKind === 'standard' ? 'standard' : 'pbr';
2262
+ assetPreviewDialog.style.width = 'min(780px,calc(100vw - 64px))';
2263
+ assetPreviewTitle.textContent = '编辑材质球';
2264
+ assetPreviewMeta.textContent = [
2265
+ material.name || asset.label,
2266
+ materialKind === 'standard' ? 'Standard' : 'PBR',
2267
+ readonly ? '只读系统材质' : '共享资产参数',
2268
+ material.id,
2269
+ ].filter(Boolean).join(' · ');
2270
+ while (assetPreviewBody.firstChild)
2271
+ assetPreviewBody.removeChild(assetPreviewBody.firstChild);
2272
+ assetPreviewBody.style.cssText = [
2273
+ 'display:grid',
2274
+ 'grid-template-columns:minmax(180px,250px) minmax(0,1fr)',
2275
+ 'gap:14px',
2276
+ 'align-items:start',
2277
+ 'min-height:360px',
2278
+ 'border:1px solid var(--fps-editor-divider)',
2279
+ 'border-radius:4px',
2280
+ 'background:var(--fps-editor-panel-soft)',
2281
+ 'padding:12px',
2282
+ 'box-sizing:border-box',
2283
+ ].join(';');
2284
+ const previewColumn = doc.createElement('div');
2285
+ previewColumn.style.cssText = 'display:flex;flex-direction:column;gap:10px;min-width:0';
2286
+ previewColumn.appendChild(LocalEditorPanels.createAssetPreviewElement(doc, asset, 'modal'));
2287
+ const previewMeta = doc.createElement('div');
2288
+ previewMeta.textContent = readonly
2289
+ ? '系统默认材质不可编辑。'
2290
+ : '修改会影响所有使用该材质球的物体。';
2291
+ previewMeta.style.cssText = 'color:var(--fps-editor-muted);font-size:11px;font-weight:800;line-height:1.45';
2292
+ previewColumn.appendChild(previewMeta);
2293
+ const form = doc.createElement('div');
2294
+ form.style.cssText = 'display:grid;grid-template-columns:minmax(96px,140px) minmax(0,1fr);gap:7px 10px;align-items:center;min-width:0';
2295
+ appendMaterialEditorReadonlyRow(form, '名称', material.name || asset.label);
2296
+ appendMaterialEditorReadonlyRow(form, '类型', materialKind === 'standard' ? 'Standard 标准材质球' : 'PBR 标准材质球');
2297
+ appendMaterialEditorColorField(form, '基础颜色', 'profile.baseColor.color', baseColor.color ?? { r: 1, g: 1, b: 1 }, readonly);
2298
+ appendMaterialEditorTextureField(form, '基础贴图', 'profile.baseColor.texture.url', baseColor.texture?.url ?? '', readonly);
2299
+ appendMaterialEditorNumberField(form, '亮度', 'profile.baseColor.brightness', baseColor.brightness ?? 1, readonly, 0, 2, 0.05);
2300
+ appendMaterialEditorNumberField(form, '饱和度', 'profile.baseColor.saturation', baseColor.saturation ?? 1, readonly, 0, 2, 0.05);
2301
+ appendMaterialEditorNumberField(form, '对比度', 'profile.baseColor.contrast', baseColor.contrast ?? 1, readonly, 0, 2, 0.05);
2302
+ appendMaterialEditorNumberField(form, '色相', 'profile.baseColor.hue', baseColor.hue ?? 0, readonly, -180, 180, 1);
2303
+ if (materialKind === 'pbr') {
2304
+ appendMaterialEditorNumberField(form, '金属度', 'profile.metallic', profile.metallic ?? 0, readonly, 0, 1, 0.05);
2305
+ appendMaterialEditorNumberField(form, '粗糙度', 'profile.roughness', profile.roughness ?? 1, readonly, 0, 1, 0.05);
2306
+ }
2307
+ appendMaterialEditorColorField(form, '自发光颜色', 'profile.emission.color', emission.color ?? { r: 0, g: 0, b: 0 }, readonly);
2308
+ appendMaterialEditorNumberField(form, '自发光强度', 'profile.emission.intensity', emission.intensity ?? 0, readonly, 0, undefined, 0.05);
2309
+ appendMaterialEditorTextureField(form, '遮罩贴图', 'profile.emission.maskTexture.url', emission.maskTexture?.url ?? '', readonly);
2310
+ assetPreviewBody.appendChild(previewColumn);
2311
+ assetPreviewBody.appendChild(form);
2312
+ }
2313
+ function appendMaterialEditorReadonlyRow(container, label, value) {
2314
+ appendMaterialEditorLabel(container, label);
2315
+ const output = doc.createElement('div');
2316
+ output.textContent = value;
2317
+ output.style.cssText = [
2318
+ 'min-height:30px',
2319
+ 'display:flex',
2320
+ 'align-items:center',
2321
+ 'min-width:0',
2322
+ 'padding:0 8px',
2323
+ 'border:1px solid var(--fps-editor-border)',
2324
+ 'border-radius:3px',
2325
+ 'background:var(--fps-editor-field)',
2326
+ 'color:var(--fps-editor-muted-strong)',
2327
+ 'font-size:12px',
2328
+ 'font-weight:900',
2329
+ 'overflow:hidden',
2330
+ 'text-overflow:ellipsis',
2331
+ 'white-space:nowrap',
2332
+ ].join(';');
2333
+ container.appendChild(output);
2334
+ }
2335
+ function appendMaterialEditorColorField(container, label, fieldPath, color, readonly) {
2336
+ appendMaterialEditorLabel(container, label);
2337
+ const input = doc.createElement('input');
2338
+ input.type = 'color';
2339
+ input.value = materialEditorColorToHex(color);
2340
+ input.disabled = readonly;
2341
+ input.dataset.editorAssetMaterialField = fieldPath;
2342
+ input.dataset.editorAssetMaterialFieldKind = 'color';
2343
+ input.style.cssText = [
2344
+ 'width:48px',
2345
+ 'height:32px',
2346
+ 'padding:2px',
2347
+ 'border:1px solid var(--fps-editor-accent)',
2348
+ 'border-radius:3px',
2349
+ 'background:var(--fps-editor-field)',
2350
+ readonly ? 'opacity:0.62' : 'cursor:pointer',
2351
+ ].join(';');
2352
+ container.appendChild(input);
2353
+ }
2354
+ function appendMaterialEditorNumberField(container, label, fieldPath, value, readonly, min, max, step = 0.05) {
2355
+ appendMaterialEditorLabel(container, label);
2356
+ const input = doc.createElement('input');
2357
+ input.type = 'number';
2358
+ input.value = formatMaterialEditorNumber(value);
2359
+ if (min != null)
2360
+ input.min = String(min);
2361
+ if (max != null)
2362
+ input.max = String(max);
2363
+ input.step = String(step);
2364
+ input.disabled = readonly;
2365
+ input.dataset.editorAssetMaterialField = fieldPath;
2366
+ input.dataset.editorAssetMaterialFieldKind = 'number';
2367
+ input.style.cssText = createMaterialEditorFieldCss(readonly);
2368
+ container.appendChild(input);
2369
+ }
2370
+ function appendMaterialEditorTextureField(container, label, fieldPath, value, readonly) {
2371
+ appendMaterialEditorLabel(container, label);
2372
+ const select = doc.createElement('select');
2373
+ select.disabled = readonly;
2374
+ select.dataset.editorAssetMaterialField = fieldPath;
2375
+ select.dataset.editorAssetMaterialFieldKind = 'texture';
2376
+ select.style.cssText = createMaterialEditorFieldCss(readonly);
2377
+ for (const option of createMaterialEditorTextureOptions(value)) {
2378
+ const optionElement = doc.createElement('option');
2379
+ optionElement.value = option.value;
2380
+ optionElement.textContent = option.label;
2381
+ optionElement.title = option.meta ?? option.value;
2382
+ select.appendChild(optionElement);
2383
+ }
2384
+ select.value = value;
2385
+ container.appendChild(select);
2386
+ }
2387
+ function appendMaterialEditorLabel(container, text) {
2388
+ const label = doc.createElement('div');
2389
+ label.textContent = text;
2390
+ label.style.cssText = 'color:var(--fps-editor-muted-strong);font-size:12px;font-weight:900;line-height:1.2';
2391
+ container.appendChild(label);
2392
+ }
2393
+ function createMaterialEditorTextureOptions(currentValue) {
2394
+ const options = [{ label: '继承 / 无', value: '', meta: '清空贴图引用' }];
2395
+ const seen = new Set(['']);
2396
+ for (const asset of currentState?.assets ?? []) {
2397
+ if (resolveBrowserAssetKind(asset) !== 'texture')
2398
+ continue;
2399
+ const value = asset.preview?.kind === 'image'
2400
+ ? asset.preview.url
2401
+ : asset.external?.assetUrl ?? asset.external?.assetPath ?? '';
2402
+ if (!value || seen.has(value))
2403
+ continue;
2404
+ seen.add(value);
2405
+ options.push({ label: asset.label, value, meta: asset.meta ?? asset.assetId ?? asset.id });
2406
+ }
2407
+ if (currentValue && !seen.has(currentValue)) {
2408
+ options.push({ label: currentValue, value: currentValue, meta: '当前贴图 URL' });
2409
+ }
2410
+ return options;
2411
+ }
2412
+ function createMaterialEditorFieldCss(readonly) {
2413
+ return [
2414
+ 'width:100%',
2415
+ 'min-width:0',
2416
+ 'height:32px',
2417
+ 'padding:0 8px',
2418
+ 'border:1px solid var(--fps-editor-accent)',
2419
+ 'border-radius:3px',
2420
+ 'background:var(--fps-editor-field)',
2421
+ 'color:var(--fps-editor-text-strong)',
2422
+ 'font-size:12px',
2423
+ 'font-weight:800',
2424
+ 'box-sizing:border-box',
2425
+ readonly ? 'opacity:0.62' : '',
2426
+ ].join(';');
2427
+ }
2428
+ function formatMaterialEditorNumber(value) {
2429
+ if (!Number.isFinite(value))
2430
+ return '0';
2431
+ return Number.isInteger(value) ? String(value) : value.toFixed(4).replace(/0+$/, '').replace(/\.$/, '');
2432
+ }
2433
+ function materialEditorColorToHex(color) {
2434
+ const toHex = (value) => {
2435
+ const normalized = Number.isFinite(value) ? value : 0;
2436
+ const byte = Math.round(Math.min(255, Math.max(0, normalized <= 1 ? normalized * 255 : normalized)));
2437
+ return byte.toString(16).padStart(2, '0');
2438
+ };
2439
+ return `#${toHex(color.r)}${toHex(color.g)}${toHex(color.b)}`;
2440
+ }
2441
+ function materialEditorHexToColor(value) {
2442
+ const match = value.trim().match(/^#?([0-9a-f]{6})$/i);
2443
+ if (!match)
2444
+ return null;
2445
+ const hex = match[1];
2446
+ return {
2447
+ r: Number.parseInt(hex.slice(0, 2), 16) / 255,
2448
+ g: Number.parseInt(hex.slice(2, 4), 16) / 255,
2449
+ b: Number.parseInt(hex.slice(4, 6), 16) / 255,
2450
+ };
2451
+ }
2452
+ function dispatchMaterialEditorFieldChange(input) {
2453
+ const asset = resolvePreviewedAssetFromState();
2454
+ const material = asset?.material;
2455
+ const fieldPath = input.dataset.editorAssetMaterialField;
2456
+ if (!asset || !material || !fieldPath || material.readonly === true)
2457
+ return;
2458
+ const kind = input.dataset.editorAssetMaterialFieldKind;
2459
+ let value;
2460
+ if (kind === 'color' && input instanceof HTMLInputElement) {
2461
+ value = materialEditorHexToColor(input.value);
2462
+ if (!value)
2463
+ return;
2464
+ }
2465
+ else if (kind === 'number' && input instanceof HTMLInputElement) {
2466
+ value = Number(input.value);
2467
+ if (!Number.isFinite(value))
2468
+ return;
2469
+ }
2470
+ else if (kind === 'texture') {
2471
+ value = input.value.trim() || null;
2472
+ }
2473
+ else {
2474
+ value = input.value;
2475
+ }
2476
+ callbacks.onAssetAction?.({
2477
+ actionId: 'asset.edit-material-field',
2478
+ assetId: material.id || asset.assetId || asset.id,
2479
+ browserAssetId: asset.id,
2480
+ assetKind: asset.kind,
2481
+ fieldPath,
2482
+ value,
2483
+ });
2484
+ }
2485
+ function renderAssetPickerModal() {
2486
+ if (!assetPickerOpen || !assetPickerProperty || !assetPickerInput) {
2487
+ assetPickerModal.style.display = 'none';
2488
+ assetPickerTitle.textContent = '';
2489
+ assetPickerSearch.value = '';
2490
+ while (assetPickerList.firstChild)
2491
+ assetPickerList.removeChild(assetPickerList.firstChild);
2492
+ return;
2493
+ }
2494
+ const options = readInspectorAssetPickerOptions(assetPickerProperty);
2495
+ assetPickerTitle.textContent = options.title;
2496
+ assetPickerSearch.value = assetPickerFilter;
2497
+ while (assetPickerList.firstChild)
2498
+ assetPickerList.removeChild(assetPickerList.firstChild);
2499
+ const normalizedFilter = assetPickerFilter.trim().toLowerCase();
2500
+ const candidates = options.candidates.filter(candidate => {
2501
+ if (!normalizedFilter)
2502
+ return true;
2503
+ return [candidate.label, candidate.value, candidate.meta ?? '', candidate.kind ?? '']
2504
+ .some(value => value.toLowerCase().includes(normalizedFilter));
2505
+ });
2506
+ if (candidates.length === 0) {
2507
+ const empty = doc.createElement('div');
2508
+ empty.textContent = '没有匹配的资产。';
2509
+ empty.style.cssText = 'color:var(--fps-editor-muted);font-size:12px;font-weight:800;padding:10px';
2510
+ assetPickerList.appendChild(empty);
2511
+ }
2512
+ for (const candidate of candidates) {
2513
+ assetPickerList.appendChild(createInspectorAssetPickerCandidateButton(candidate, assetPickerInput.value));
2514
+ }
2515
+ assetPickerModal.style.display = 'flex';
2516
+ }
2517
+ function createInspectorAssetPickerCandidateButton(candidate, currentValue) {
2518
+ const button = doc.createElement('button');
2519
+ button.type = 'button';
2520
+ button.dataset.editorAssetPickerCandidate = candidate.value;
2521
+ const selected = candidate.value === currentValue;
2522
+ button.style.cssText = [
2523
+ 'min-width:0',
2524
+ 'min-height:56px',
2525
+ 'display:flex',
2526
+ 'align-items:center',
2527
+ 'gap:8px',
2528
+ 'padding:7px 8px',
2529
+ `border:1px solid ${selected ? 'var(--fps-editor-accent-strong)' : 'var(--fps-editor-border)'}`,
2530
+ 'border-radius:3px',
2531
+ `background:${selected ? 'var(--fps-editor-accent-soft)' : 'var(--fps-editor-asset-card-bg)'}`,
2532
+ 'color:var(--fps-editor-text)',
2533
+ 'text-align:left',
2534
+ 'cursor:pointer',
2535
+ ].join(';');
2536
+ button.appendChild(LocalEditorPanels.createAssetPreviewElement(doc, {
2537
+ id: candidate.value || '__empty__',
2538
+ label: candidate.label,
2539
+ preview: candidate.preview,
2540
+ }, 'grid'));
2541
+ const body = doc.createElement('div');
2542
+ body.style.cssText = 'min-width:0;flex:1';
2543
+ const label = doc.createElement('div');
2544
+ label.textContent = candidate.label;
2545
+ label.style.cssText = 'font-weight:900;overflow:hidden;text-overflow:ellipsis;white-space:nowrap';
2546
+ const meta = doc.createElement('div');
2547
+ meta.textContent = candidate.meta ?? candidate.value;
2548
+ meta.style.cssText = 'color:var(--fps-editor-muted);font-size:11px;font-weight:800;overflow:hidden;text-overflow:ellipsis;white-space:nowrap';
2549
+ body.appendChild(label);
2550
+ body.appendChild(meta);
2551
+ button.appendChild(body);
2552
+ return button;
2553
+ }
1770
2554
  sceneHelpButton.addEventListener('click', () => {
1771
2555
  setShortcutHelpOpen(!helpOpen);
1772
2556
  });
@@ -1779,6 +2563,44 @@ export function createLocalEditorBrowserUi(options = {}) {
1779
2563
  return;
1780
2564
  setShortcutHelpOpen(false);
1781
2565
  });
2566
+ assetPreviewModal.addEventListener('click', (event) => {
2567
+ const target = event.target instanceof HTMLElement ? event.target : null;
2568
+ if (target?.closest('[data-editor-asset-preview-close]')) {
2569
+ closeAssetPreviewModal();
2570
+ return;
2571
+ }
2572
+ if (target === assetPreviewModal)
2573
+ closeAssetPreviewModal();
2574
+ });
2575
+ assetPreviewBody.addEventListener('change', (event) => {
2576
+ if (assetPreviewMode !== 'material-editor')
2577
+ return;
2578
+ const target = event.target instanceof HTMLElement ? event.target : null;
2579
+ const input = target?.closest('[data-editor-asset-material-field]');
2580
+ if (!input)
2581
+ return;
2582
+ dispatchMaterialEditorFieldChange(input);
2583
+ });
2584
+ assetPickerModal.addEventListener('click', (event) => {
2585
+ const target = event.target instanceof HTMLElement ? event.target : null;
2586
+ if (target?.closest('[data-editor-asset-picker-close]')) {
2587
+ closeInspectorAssetPicker();
2588
+ return;
2589
+ }
2590
+ const candidateButton = target?.closest('[data-editor-asset-picker-candidate]');
2591
+ if (candidateButton && assetPickerInput) {
2592
+ assetPickerInput.value = candidateButton.dataset.editorAssetPickerCandidate ?? '';
2593
+ assetPickerInput.dispatchEvent(new Event('change', { bubbles: true }));
2594
+ closeInspectorAssetPicker();
2595
+ return;
2596
+ }
2597
+ if (target === assetPickerModal)
2598
+ closeInspectorAssetPicker();
2599
+ });
2600
+ assetPickerSearch.addEventListener('input', () => {
2601
+ assetPickerFilter = assetPickerSearch.value;
2602
+ renderAssetPickerModal();
2603
+ });
1782
2604
  toolGroup.addEventListener('click', (event) => {
1783
2605
  const target = event.target instanceof HTMLElement ? event.target : null;
1784
2606
  const tool = target?.closest('[data-transform-tool]')?.dataset.transformTool;
@@ -1868,11 +2690,70 @@ export function createLocalEditorBrowserUi(options = {}) {
1868
2690
  render(currentState);
1869
2691
  return;
1870
2692
  }
2693
+ const assetBrowserTabButton = target?.closest('[data-editor-asset-browser-tab]');
2694
+ const nextAssetBrowserTab = assetBrowserTabButton?.dataset.editorAssetBrowserTab;
2695
+ if (isLocalEditorAssetBrowserTab(nextAssetBrowserTab)) {
2696
+ assetBrowserTab = nextAssetBrowserTab;
2697
+ if (currentState)
2698
+ render(currentState);
2699
+ return;
2700
+ }
1871
2701
  const assetButton = target?.closest('[data-editor-asset-id]');
1872
2702
  if (assetButton?.dataset.editorAssetId) {
1873
- callbacks.onCreateFromAsset?.(assetButton.dataset.editorAssetId);
2703
+ callbacks.onSelectAsset?.(assetButton.dataset.editorAssetId);
2704
+ return;
2705
+ }
2706
+ });
2707
+ assetPanel.addEventListener('contextmenu', (event) => {
2708
+ const target = event.target instanceof HTMLElement ? event.target : null;
2709
+ const assetButton = target?.closest('[data-editor-asset-id]');
2710
+ const browserAssetId = assetButton?.dataset.editorAssetId;
2711
+ if (!browserAssetId || !currentState)
2712
+ return;
2713
+ const asset = currentState.assets.find(candidate => candidate.id === browserAssetId);
2714
+ if (!asset)
2715
+ return;
2716
+ event.preventDefault();
2717
+ event.stopPropagation();
2718
+ callbacks.onSelectAsset?.(browserAssetId);
2719
+ contextMenu.open({
2720
+ x: event.clientX,
2721
+ y: event.clientY,
2722
+ items: createAssetBrowserMenuItems(currentState, asset),
2723
+ onAction(item) {
2724
+ if (item.id === 'asset.preview') {
2725
+ openAssetPreviewModal(asset);
2726
+ return;
2727
+ }
2728
+ if (item.id === 'asset.edit-material') {
2729
+ openAssetMaterialEditorModal(asset);
2730
+ return;
2731
+ }
2732
+ if (item.id === 'asset.copy-id') {
2733
+ copyTextToClipboard(doc.defaultView, asset.assetId ?? asset.id);
2734
+ return;
2735
+ }
2736
+ callbacks.onAssetAction?.(createAssetBrowserActionInput(item.id, asset));
2737
+ },
2738
+ });
2739
+ });
2740
+ workbench.rightDock.addEventListener('click', (event) => {
2741
+ const target = event.target instanceof HTMLElement ? event.target : null;
2742
+ const openTabButton = target?.closest('[data-editor-open-right-dock-tab]');
2743
+ const openTab = openTabButton?.dataset.editorOpenRightDockTab;
2744
+ if (openTab === 'inspector' || openTab === 'rendering') {
2745
+ panelRegistry.setRightDockTab(openTab);
2746
+ if (currentState)
2747
+ render(currentState);
1874
2748
  return;
1875
2749
  }
2750
+ const tabButton = target?.closest('[data-editor-right-dock-tab]');
2751
+ const tab = tabButton?.dataset.editorRightDockTab;
2752
+ if (tab === 'inspector' || tab === 'rendering') {
2753
+ panelRegistry.setRightDockTab(tab);
2754
+ if (currentState)
2755
+ render(currentState);
2756
+ }
1876
2757
  });
1877
2758
  const hierarchyController = createLocalEditorHierarchyController({
1878
2759
  doc,
@@ -1889,6 +2770,8 @@ export function createLocalEditorBrowserUi(options = {}) {
1889
2770
  onBeforeOpenContextMenu: () => {
1890
2771
  if (helpOpen)
1891
2772
  setShortcutHelpOpen(false);
2773
+ if (assetPreviewOpen)
2774
+ closeAssetPreviewModal();
1892
2775
  },
1893
2776
  });
1894
2777
  assetPanel.addEventListener('input', (event) => {
@@ -1911,10 +2794,38 @@ export function createLocalEditorBrowserUi(options = {}) {
1911
2794
  return;
1912
2795
  if (readInspectorCommitMode(input) !== 'live')
1913
2796
  return;
1914
- const propertyInput = createInspectorPropertyInput(input, 'input', 'live');
2797
+ const propertyInput = createInspectorPropertyInput(input, 'input');
1915
2798
  if (propertyInput)
1916
2799
  callbacks.onPropertyInput?.(propertyInput);
1917
2800
  });
2801
+ inspectorPanel.addEventListener('click', (event) => {
2802
+ const target = event.target instanceof HTMLElement ? event.target : null;
2803
+ const copyButton = target?.closest('[data-editor-inspector-asset-copy]');
2804
+ if (copyButton) {
2805
+ const control = copyButton.closest('[data-editor-inspector-asset-picker-control]');
2806
+ const input = control?.querySelector('input[data-serialized-path]');
2807
+ const value = copyButton.dataset.editorInspectorAssetCopyValue;
2808
+ if (!input || !value)
2809
+ return;
2810
+ event.preventDefault();
2811
+ event.stopPropagation();
2812
+ input.value = value;
2813
+ input.dispatchEvent(new Event('change', { bubbles: true }));
2814
+ return;
2815
+ }
2816
+ const pickerButton = target?.closest('[data-editor-inspector-asset-picker]');
2817
+ if (!pickerButton)
2818
+ return;
2819
+ const control = pickerButton.closest('[data-editor-inspector-asset-picker-control]');
2820
+ const input = control?.querySelector('input[data-serialized-path]');
2821
+ if (!input || !currentState)
2822
+ return;
2823
+ const property = findInspectorPropertyForInput(currentState, input);
2824
+ if (!property)
2825
+ return;
2826
+ event.preventDefault();
2827
+ openInspectorAssetPicker(input, property);
2828
+ });
1918
2829
  inspectorPanel.addEventListener('change', (event) => {
1919
2830
  const input = event.target instanceof HTMLInputElement || event.target instanceof HTMLSelectElement
1920
2831
  ? event.target
@@ -1925,7 +2836,7 @@ export function createLocalEditorBrowserUi(options = {}) {
1925
2836
  if (readInspectorCommitMode(input) !== 'change')
1926
2837
  return;
1927
2838
  }
1928
- const propertyInput = createInspectorPropertyInput(input, readInspectorImmediateSource(input), 'final');
2839
+ const propertyInput = createInspectorPropertyInput(input, readInspectorImmediateSource(input));
1929
2840
  if (propertyInput)
1930
2841
  callbacks.onPropertyInput?.(propertyInput);
1931
2842
  });
@@ -1933,27 +2844,65 @@ export function createLocalEditorBrowserUi(options = {}) {
1933
2844
  const input = event.target instanceof HTMLInputElement ? event.target : null;
1934
2845
  if (!input?.dataset.serializedPath || !input.dataset.serializedTargetId)
1935
2846
  return;
1936
- if (hasPendingInspectorNumberValue(input, 'final')) {
1937
- if (currentState)
1938
- render(currentState);
2847
+ if (readInspectorCommitMode(input) !== 'blur')
1939
2848
  return;
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);
2849
+ const propertyInput = createInspectorPropertyInput(input, 'input');
2850
+ if (propertyInput)
2851
+ callbacks.onPropertyInput?.(propertyInput);
2852
+ });
2853
+ renderingPanel.addEventListener('input', (event) => {
2854
+ const input = event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement
2855
+ ? event.target
2856
+ : null;
2857
+ if (!input?.dataset.editorRenderingProperty)
2858
+ return;
2859
+ if (input instanceof HTMLInputElement && (input.type === 'checkbox' || input.type === 'color'))
2860
+ return;
2861
+ if (readRenderingCommitMode(input) !== 'live')
1948
2862
  return;
2863
+ const propertyInput = createRenderingPropertyInput(input, input instanceof HTMLTextAreaElement ? 'list' : 'input');
2864
+ if (propertyInput)
2865
+ callbacks.onRenderingPropertyChange?.(propertyInput);
2866
+ });
2867
+ renderingPanel.addEventListener('change', (event) => {
2868
+ const input = event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement
2869
+ ? event.target
2870
+ : null;
2871
+ if (!input?.dataset.editorRenderingProperty)
2872
+ return;
2873
+ if ((input instanceof HTMLInputElement && (input.type === 'text' || input.type === 'number')) || input instanceof HTMLTextAreaElement) {
2874
+ if (readRenderingCommitMode(input) !== 'change')
2875
+ return;
1949
2876
  }
1950
- if (commitMode !== 'blur')
2877
+ const propertyInput = createRenderingPropertyInput(input, readRenderingImmediateSource(input));
2878
+ if (propertyInput)
2879
+ callbacks.onRenderingPropertyChange?.(propertyInput);
2880
+ });
2881
+ renderingPanel.addEventListener('focusout', (event) => {
2882
+ const input = event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement
2883
+ ? event.target
2884
+ : null;
2885
+ if (!input?.dataset.editorRenderingProperty)
1951
2886
  return;
1952
- const propertyInput = createInspectorPropertyInput(input, 'input', 'final');
2887
+ if (readRenderingCommitMode(input) !== 'blur')
2888
+ return;
2889
+ const propertyInput = createRenderingPropertyInput(input, input instanceof HTMLTextAreaElement ? 'list' : 'input');
1953
2890
  if (propertyInput)
1954
- callbacks.onPropertyInput?.(propertyInput);
1955
- else if (currentState)
1956
- render(currentState);
2891
+ callbacks.onRenderingPropertyChange?.(propertyInput);
2892
+ });
2893
+ renderingPanel.addEventListener('click', (event) => {
2894
+ const target = event.target instanceof Element
2895
+ ? event.target.closest('[data-editor-rendering-action]')
2896
+ : null;
2897
+ if (!(target instanceof HTMLButtonElement))
2898
+ return;
2899
+ if (target.disabled)
2900
+ return;
2901
+ const actionId = target.dataset.editorRenderingAction;
2902
+ if (!actionId)
2903
+ return;
2904
+ event.preventDefault();
2905
+ callbacks.onRenderingAction?.({ actionId });
1957
2906
  });
1958
2907
  const onKeyDown = (event) => {
1959
2908
  if (event.defaultPrevented)
@@ -1961,6 +2910,16 @@ export function createLocalEditorBrowserUi(options = {}) {
1961
2910
  if (currentState?.mode !== 'editor' || currentState.busy)
1962
2911
  return;
1963
2912
  const key = event.key.toLowerCase();
2913
+ if (key === 'escape' && assetPreviewOpen) {
2914
+ event.preventDefault();
2915
+ closeAssetPreviewModal();
2916
+ return;
2917
+ }
2918
+ if (key === 'escape' && assetPickerOpen) {
2919
+ event.preventDefault();
2920
+ closeInspectorAssetPicker();
2921
+ return;
2922
+ }
1964
2923
  if (key === 'escape' && snapSettingsOpen) {
1965
2924
  event.preventDefault();
1966
2925
  closeSnapSettingsPopover();
@@ -2050,12 +3009,14 @@ export function createLocalEditorBrowserUi(options = {}) {
2050
3009
  `[data-serialized-target-id="${cssEscape(active.dataset.serializedTargetId)}"]`,
2051
3010
  ].join(''),
2052
3011
  value: active.value,
2053
- preserveValue: hasPendingInspectorNumberValue(active),
2054
3012
  };
2055
3013
  }
2056
3014
  if (active.dataset.editorInspectorSearch != null) {
2057
3015
  return { selector: 'input[data-editor-inspector-search]', value: active.value };
2058
3016
  }
3017
+ if (active.dataset.editorHierarchySearch != null) {
3018
+ return { selector: 'input[data-editor-hierarchy-search]', value: active.value };
3019
+ }
2059
3020
  if (active.dataset.editorAssetFilter != null) {
2060
3021
  return { selector: 'input[data-editor-asset-filter]', value: active.value };
2061
3022
  }
@@ -2068,8 +3029,6 @@ export function createLocalEditorBrowserUi(options = {}) {
2068
3029
  if (!input)
2069
3030
  return;
2070
3031
  input.focus({ preventScroll: true });
2071
- if (snapshot.value != null && snapshot.preserveValue)
2072
- input.value = snapshot.value;
2073
3032
  if (snapshot.value != null && input.value === snapshot.value) {
2074
3033
  const position = input.value.length;
2075
3034
  try {
@@ -2107,7 +3066,7 @@ export function createLocalEditorBrowserUi(options = {}) {
2107
3066
  contextMenu.close();
2108
3067
  hostChrome.style.display = !inEditor && localTestActionsEnabled ? 'flex' : 'none';
2109
3068
  enterEditorButton.disabled = disabled;
2110
- for (const button of [saveButton, saveAndRunButton, discardRunButton, undoButton, redoButton, sceneHelpButton, mainCameraPreviewButton, gridToggleButton]) {
3069
+ for (const button of [saveButton, saveAndRunButton, discardRunButton, undoButton, redoButton, sceneHelpButton, sceneCameraButton, gridToggleButton]) {
2111
3070
  button.style.display = 'inline-flex';
2112
3071
  button.disabled = disabled;
2113
3072
  }
@@ -2117,11 +3076,17 @@ export function createLocalEditorBrowserUi(options = {}) {
2117
3076
  closeLocalTestMenu();
2118
3077
  if (!inEditor)
2119
3078
  helpOpen = false;
2120
- inputRouter.setModalOpen(inEditor && helpOpen);
3079
+ if (!inEditor || disabled) {
3080
+ assetPreviewOpen = false;
3081
+ previewedAsset = null;
3082
+ assetPreviewMode = 'preview';
3083
+ assetPickerOpen = false;
3084
+ assetPickerInput = null;
3085
+ assetPickerProperty = null;
3086
+ }
3087
+ syncInputModalOpen();
2121
3088
  const transformTool = state.transformTool ?? null;
2122
3089
  workbench.root.style.display = inEditor ? '' : 'none';
2123
- if (inEditor)
2124
- workbenchLayoutController.refresh();
2125
3090
  sceneToolOverlay.style.display = inEditor ? 'flex' : 'none';
2126
3091
  if (!inEditor)
2127
3092
  closeSnapSettingsPopover();
@@ -2131,20 +3096,20 @@ export function createLocalEditorBrowserUi(options = {}) {
2131
3096
  closeTransformActionPopover();
2132
3097
  if (!inEditor)
2133
3098
  closeOverlaySettingsPopover();
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
3099
+ const sceneCameraPreview = state.sceneCameraPreview ?? { enabled: false, available: false };
3100
+ cameraPreviewGroup.style.display = inEditor ? 'flex' : 'none';
3101
+ sceneCameraButton.disabled = disabled || !sceneCameraPreview.available;
3102
+ const sceneCameraTooltip = sceneCameraPreview.available
2138
3103
  ? '从 Main Camera 查看当前场景'
2139
3104
  : '当前场景没有可预览的 Main Camera';
2140
- setToolbarButtonTooltip(mainCameraPreviewButton, mainCameraPreviewTooltip);
2141
- LocalEditorShared.applyButtonActiveState(mainCameraPreviewButton, mainCameraPreview.enabled);
3105
+ setToolbarButtonTooltip(sceneCameraButton, sceneCameraTooltip);
3106
+ LocalEditorShared.applyButtonActiveState(sceneCameraButton, sceneCameraPreview.enabled);
2142
3107
  const viewportTools = state.viewportTools ?? null;
2143
- const viewportControlsDisabled = disabled || mainCameraPreview.enabled;
2144
- const viewportControlsTooltip = mainCameraPreview.enabled
3108
+ const viewportControlsDisabled = disabled || sceneCameraPreview.enabled;
3109
+ const viewportControlsTooltip = sceneCameraPreview.enabled
2145
3110
  ? '当前为 Main Camera 预览,请关闭后再使用视口工具'
2146
3111
  : '';
2147
- const viewportProjectionTooltip = mainCameraPreview.enabled
3112
+ const viewportProjectionTooltip = sceneCameraPreview.enabled
2148
3113
  ? '当前为 Main Camera 预览,投影由 Main Camera 决定;关闭预览后可切换编辑视口投影'
2149
3114
  : '';
2150
3115
  viewportToolsGroup.style.display = inEditor ? 'flex' : 'none';
@@ -2241,6 +3206,8 @@ export function createLocalEditorBrowserUi(options = {}) {
2241
3206
  redoButton.disabled = disabled || !state.session?.canRedo;
2242
3207
  LocalEditorShared.applyButtonActiveState(sceneHelpButton, inEditor && helpOpen);
2243
3208
  shortcutHelpPanel.style.display = inEditor && helpOpen ? '' : 'none';
3209
+ renderAssetPreviewModal();
3210
+ renderAssetPickerModal();
2244
3211
  dirtyBadge.style.display = inEditor && state.session?.dirty ? 'inline-flex' : 'none';
2245
3212
  const editorStatusText = [
2246
3213
  state.status,
@@ -2287,12 +3254,22 @@ export function createLocalEditorBrowserUi(options = {}) {
2287
3254
  if (!inEditor)
2288
3255
  return;
2289
3256
  const hierarchyDescriptor = panelRegistry.getActivePanel('left');
2290
- const inspectorDescriptor = panelRegistry.getActivePanel('right');
3257
+ const rightDescriptor = panelRegistry.getActivePanel('right');
3258
+ const activeRightTab = panelRegistry.getRightDockTab();
2291
3259
  hierarchyPanel.dataset.editorPanelId = hierarchyDescriptor?.id ?? 'hierarchy';
2292
- inspectorPanel.dataset.editorPanelId = inspectorDescriptor?.id ?? 'inspector';
3260
+ inspectorPanel.dataset.editorPanelId = activeRightTab === 'inspector' ? rightDescriptor?.id ?? 'inspector' : 'inspector';
3261
+ renderingPanel.dataset.editorPanelId = activeRightTab === 'rendering' ? rightDescriptor?.id ?? 'rendering' : 'rendering';
3262
+ inspectorPanel.style.display = activeRightTab === 'inspector' ? '' : 'none';
3263
+ renderingPanel.style.display = activeRightTab === 'rendering' ? '' : 'none';
2293
3264
  hierarchyController.render(state);
2294
- LocalEditorPanels.renderWorkbenchBottomDockPanel(doc, assetPanel, state, panelRegistry.getBottomDockTab(), panelRegistry.getPanels('bottom'));
2295
- LocalEditorPanels.renderInspectorPanel(doc, inspectorPanel, state, inspectorFilter, options.inspector);
3265
+ LocalEditorPanels.renderWorkbenchBottomDockPanel(doc, assetPanel, state, panelRegistry.getBottomDockTab(), panelRegistry.getPanels('bottom'), assetBrowserTab);
3266
+ LocalEditorPanels.renderWorkbenchRightDockTabs(doc, rightDockTabs, activeRightTab, panelRegistry.getPanels('right'));
3267
+ if (activeRightTab === 'inspector') {
3268
+ LocalEditorPanels.renderInspectorPanel(doc, inspectorPanel, state, inspectorFilter, options.inspector);
3269
+ }
3270
+ else {
3271
+ LocalEditorPanels.renderRenderingPanel(doc, renderingPanel, state);
3272
+ }
2296
3273
  restoreEditableFocus(doc, focusSnapshot);
2297
3274
  };
2298
3275
  return {
@@ -2328,8 +3305,11 @@ export function createLocalEditorBrowserUi(options = {}) {
2328
3305
  overlaySettingsPopover.remove();
2329
3306
  spatialOverlay.root.remove();
2330
3307
  measurementOverlay.root.remove();
3308
+ sceneFrameRateOverlay.root.remove();
2331
3309
  boxSelectionOverlay.remove();
2332
3310
  shortcutHelpPanel.remove();
3311
+ assetPreviewModal.remove();
3312
+ assetPickerModal.remove();
2333
3313
  hierarchyController.dispose();
2334
3314
  workbenchLayoutController.dispose();
2335
3315
  tooltipController.dispose();