@fps-games/editor 0.1.2-beta.2 → 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 (62) hide show
  1. package/dist/local-editor-harness.d.ts +39 -7
  2. package/dist/local-editor-harness.d.ts.map +1 -1
  3. package/dist/local-editor-harness.js +379 -119
  4. package/dist/local-editor-harness.js.map +1 -1
  5. package/node_modules/@fps-games/editor-babylon/dist/editor-sky.d.ts +2 -2
  6. package/node_modules/@fps-games/editor-babylon/dist/editor-sky.d.ts.map +1 -1
  7. package/node_modules/@fps-games/editor-babylon/dist/editor-sky.js +31 -15
  8. package/node_modules/@fps-games/editor-babylon/dist/editor-sky.js.map +1 -1
  9. package/node_modules/@fps-games/editor-babylon/dist/editor-world.d.ts +10 -0
  10. package/node_modules/@fps-games/editor-babylon/dist/editor-world.d.ts.map +1 -1
  11. package/node_modules/@fps-games/editor-babylon/dist/editor-world.js +31 -9
  12. package/node_modules/@fps-games/editor-babylon/dist/editor-world.js.map +1 -1
  13. package/node_modules/@fps-games/editor-babylon/dist/index.d.ts +1 -0
  14. package/node_modules/@fps-games/editor-babylon/dist/index.d.ts.map +1 -1
  15. package/node_modules/@fps-games/editor-babylon/dist/index.js +1 -0
  16. package/node_modules/@fps-games/editor-babylon/dist/index.js.map +1 -1
  17. package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.d.ts +23 -0
  18. package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.d.ts.map +1 -1
  19. package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.js +34 -4
  20. package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.js.map +1 -1
  21. package/node_modules/@fps-games/editor-babylon/dist/material-property-adapter.d.ts +3 -1
  22. package/node_modules/@fps-games/editor-babylon/dist/material-property-adapter.d.ts.map +1 -1
  23. package/node_modules/@fps-games/editor-babylon/dist/material-property-adapter.js +166 -0
  24. package/node_modules/@fps-games/editor-babylon/dist/material-property-adapter.js.map +1 -1
  25. package/node_modules/@fps-games/editor-babylon/dist/projection.d.ts +12 -1
  26. package/node_modules/@fps-games/editor-babylon/dist/projection.d.ts.map +1 -1
  27. package/node_modules/@fps-games/editor-babylon/dist/projection.js +231 -32
  28. package/node_modules/@fps-games/editor-babylon/dist/projection.js.map +1 -1
  29. package/node_modules/@fps-games/editor-babylon/dist/shadow-preview-controller.d.ts +22 -0
  30. package/node_modules/@fps-games/editor-babylon/dist/shadow-preview-controller.d.ts.map +1 -0
  31. package/node_modules/@fps-games/editor-babylon/dist/shadow-preview-controller.js +71 -0
  32. package/node_modules/@fps-games/editor-babylon/dist/shadow-preview-controller.js.map +1 -0
  33. package/node_modules/@fps-games/editor-babylon/dist/types.d.ts +2 -1
  34. package/node_modules/@fps-games/editor-babylon/dist/types.d.ts.map +1 -1
  35. package/node_modules/@fps-games/editor-babylon/package.json +5 -4
  36. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panel-registry.d.ts +3 -1
  37. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panel-registry.d.ts.map +1 -1
  38. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panel-registry.js +7 -0
  39. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panel-registry.js.map +1 -1
  40. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.d.ts +8 -3
  41. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.d.ts.map +1 -1
  42. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.js +645 -20
  43. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.js.map +1 -1
  44. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts +147 -3
  45. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts.map +1 -1
  46. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.d.ts +6 -0
  47. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.d.ts.map +1 -1
  48. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.js +1 -0
  49. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.js.map +1 -1
  50. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.d.ts +2 -4
  51. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.d.ts.map +1 -1
  52. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js +1060 -86
  53. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js.map +1 -1
  54. package/node_modules/@fps-games/editor-browser/package.json +2 -2
  55. package/node_modules/@fps-games/editor-core/package.json +2 -2
  56. package/node_modules/@fps-games/editor-forge-play/package.json +2 -2
  57. package/node_modules/@fps-games/editor-protocol/dist/index.d.ts +61 -0
  58. package/node_modules/@fps-games/editor-protocol/dist/index.d.ts.map +1 -1
  59. package/node_modules/@fps-games/editor-protocol/dist/index.js +157 -0
  60. package/node_modules/@fps-games/editor-protocol/dist/index.js.map +1 -1
  61. package/node_modules/@fps-games/editor-protocol/package.json +1 -1
  62. package/package.json +8 -6
@@ -16,33 +16,15 @@ import { createShortcutHelpPanel as createLocalEditorShortcutHelpPanel } from '.
16
16
  import { applyLocalEditorTheme, ensureLocalEditorTheme, LOCAL_EDITOR_THEME_CLASS, normalizeLocalEditorThemeName, } from './local-editor-ui-theme';
17
17
  import { createDefaultLocalEditorWorkbenchLayout, createLocalEditorWorkbenchLayoutController, createLocalEditorWorkbench, createSceneHeaderToolbar, createWorkbenchPanelContent, } from './local-editor-ui-workbench';
18
18
  import { createLocalEditorIcon } from './local-editor-ui-icons';
19
- 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';
20
20
  export { DEFAULT_LOCAL_EDITOR_THEME, LOCAL_EDITOR_THEME_CLASS, applyLocalEditorTheme, normalizeLocalEditorThemeName, } from './local-editor-ui-theme';
21
21
  export { LOCAL_EDITOR_ICON_NAMES, createLocalEditorIcon, isLocalEditorIconName, resolveLocalEditorIconName, } from './local-editor-ui-icons';
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
+ const PENDING_NUMERIC_PROPERTY_INPUT = Symbol('pending-numeric-property-input');
23
+ function readInspectorInputValue(input, source) {
42
24
  const control = input.dataset.serializedControl;
43
25
  const valueType = input.dataset.serializedValueType;
44
26
  if ((control === 'vec2' || control === 'vec3') && input instanceof HTMLInputElement) {
45
- return readInspectorVectorInputValue(input, mode);
27
+ return readInspectorVectorInputValue(input, source) ?? PENDING_NUMERIC_PROPERTY_INPUT;
46
28
  }
47
29
  if (control === 'enum' && input instanceof HTMLSelectElement) {
48
30
  const option = input.selectedOptions.item(0);
@@ -61,7 +43,9 @@ function readInspectorInputValue(input, mode = 'final') {
61
43
  if (control === 'boolean' || valueType === 'boolean')
62
44
  return input.value === 'true';
63
45
  if (control === 'number' || valueType === 'number') {
64
- 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;
65
49
  }
66
50
  if (control === 'color' && input instanceof HTMLInputElement && input.type === 'color') {
67
51
  const value = input.value.replace('#', '');
@@ -169,9 +153,10 @@ function setToolbarButtonIcon(doc, button, icon) {
169
153
  else
170
154
  button.prepend(nextIcon);
171
155
  }
172
- function readInspectorVectorInputValue(input, mode) {
156
+ function readInspectorVectorInputValue(input, source) {
173
157
  const wrapper = input.closest('[data-inspector-vector-control]');
174
158
  const values = {};
159
+ const mode = source === 'input' && readInspectorCommitMode(input) === 'live' ? 'live' : 'final';
175
160
  const fields = wrapper
176
161
  ? Array.from(wrapper.querySelectorAll('input[data-serialized-vector-axis]'))
177
162
  : [input];
@@ -179,21 +164,21 @@ function readInspectorVectorInputValue(input, mode) {
179
164
  const axis = field.dataset.serializedVectorAxis;
180
165
  if (!axis)
181
166
  continue;
182
- const numeric = parseLocalEditorBrowserInspectorNumberValue(field.value, mode);
167
+ const numeric = LocalEditorPanels.parseLocalEditorBrowserInspectorNumberValue(field.value, mode);
183
168
  if (numeric == null)
184
- return INSPECTOR_INPUT_PENDING;
169
+ return null;
185
170
  values[axis] = numeric;
186
171
  }
187
172
  return values;
188
173
  }
189
- function createInspectorPropertyInput(input, source, numberParseMode = 'final') {
174
+ function createInspectorPropertyInput(input, source) {
190
175
  if (!input.dataset.serializedPath || !input.dataset.serializedTargetId)
191
176
  return null;
192
177
  const targetIds = input.dataset.serializedTargetIds
193
178
  ? input.dataset.serializedTargetIds.split(',').filter(Boolean)
194
179
  : undefined;
195
- const value = readInspectorInputValue(input, numberParseMode);
196
- if (value === INSPECTOR_INPUT_PENDING)
180
+ const value = readInspectorInputValue(input, source);
181
+ if (value === PENDING_NUMERIC_PROPERTY_INPUT)
197
182
  return null;
198
183
  return {
199
184
  targetId: input.dataset.serializedTargetId,
@@ -207,16 +192,63 @@ function createInspectorPropertyInput(input, source, numberParseMode = 'final')
207
192
  source: input.dataset.serializedEditSource ?? source,
208
193
  };
209
194
  }
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;
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
+ };
216
245
  }
217
246
  function readInspectorCommitMode(input) {
218
247
  return input.dataset.serializedCommitMode ?? 'live';
219
248
  }
249
+ function readRenderingCommitMode(input) {
250
+ return input.dataset.renderingCommitMode ?? 'live';
251
+ }
220
252
  function readInspectorImmediateSource(input) {
221
253
  if (input instanceof HTMLSelectElement)
222
254
  return 'select';
@@ -226,6 +258,15 @@ function readInspectorImmediateSource(input) {
226
258
  return 'color';
227
259
  return 'input';
228
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
+ }
229
270
  function createSceneFrameRateOverlay(doc) {
230
271
  const root = doc.createElement('div');
231
272
  root.classList.add(LOCAL_EDITOR_THEME_CLASS);
@@ -726,6 +767,170 @@ function renderMeasurementOverlay(overlay, state) {
726
767
  function formatSvgNumber(value) {
727
768
  return Number.isFinite(value) ? value.toFixed(2) : '0';
728
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
+ }
729
934
  export function createLocalEditorBrowserUi(options = {}) {
730
935
  const doc = options.document ?? document;
731
936
  const root = options.root ?? doc.body;
@@ -745,6 +950,7 @@ export function createLocalEditorBrowserUi(options = {}) {
745
950
  });
746
951
  let currentState = null;
747
952
  let inspectorFilter = '';
953
+ let assetBrowserTab = 'all';
748
954
  const workbenchLayout = createDefaultLocalEditorWorkbenchLayout();
749
955
  const panelRegistry = createLocalEditorPanelRegistry(workbenchLayout);
750
956
  const hostChrome = doc.createElement('div');
@@ -836,15 +1042,26 @@ export function createLocalEditorBrowserUi(options = {}) {
836
1042
  const undoButton = createToolbarIconButton(doc, '撤销', 'undo', '撤销');
837
1043
  const redoButton = createToolbarIconButton(doc, '重做', 'redo', '重做');
838
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;
839
1052
  const editorStatusButton = createToolbarIconButton(doc, '编辑器状态', 'status', '编辑器状态');
840
1053
  editorStatusButton.dataset.editorStatusButton = 'true';
841
1054
  editorStatusButton.style.cursor = 'help';
842
1055
  const workbench = createLocalEditorWorkbench(doc);
843
1056
  const hierarchyPanel = createWorkbenchPanelContent(doc);
844
1057
  const assetPanel = workbench.bottomDock;
1058
+ const rightDockTabs = doc.createElement('div');
845
1059
  const inspectorPanel = createWorkbenchPanelContent(doc);
1060
+ const renderingPanel = createWorkbenchPanelContent(doc);
846
1061
  workbench.leftDock.appendChild(hierarchyPanel);
1062
+ workbench.rightDock.appendChild(rightDockTabs);
847
1063
  workbench.rightDock.appendChild(inspectorPanel);
1064
+ workbench.rightDock.appendChild(renderingPanel);
848
1065
  root.appendChild(workbench.root);
849
1066
  tooltipSurfaces.add(workbench.root);
850
1067
  const workbenchLayoutController = createLocalEditorWorkbenchLayoutController(doc, workbench);
@@ -1123,17 +1340,17 @@ export function createLocalEditorBrowserUi(options = {}) {
1123
1340
  sceneUtilityActions.appendChild(gridToggleButton);
1124
1341
  sceneUtilityActions.appendChild(themeToggleButton);
1125
1342
  sceneUtilityActions.appendChild(sceneHelpButton);
1126
- const mainCameraPreviewGroup = doc.createElement('div');
1127
- mainCameraPreviewGroup.style.cssText = [
1343
+ const cameraPreviewGroup = doc.createElement('div');
1344
+ cameraPreviewGroup.style.cssText = [
1128
1345
  'display:flex',
1129
1346
  'align-items:center',
1130
1347
  'gap:4px',
1131
1348
  'padding-left:8px',
1132
1349
  'border-left:1px solid var(--fps-editor-divider)',
1133
1350
  ].join(';');
1134
- const mainCameraPreviewButton = createToolbarIconButton(doc, '从 Main Camera 查看场景', 'camera');
1135
- mainCameraPreviewButton.dataset.mainCameraPreviewToggle = 'true';
1136
- mainCameraPreviewGroup.appendChild(mainCameraPreviewButton);
1351
+ const sceneCameraButton = createToolbarIconButton(doc, '从 Main Camera 查看场景', 'camera');
1352
+ sceneCameraButton.dataset.sceneCameraPreviewToggle = 'true';
1353
+ cameraPreviewGroup.appendChild(sceneCameraButton);
1137
1354
  const viewportToolsGroup = doc.createElement('div');
1138
1355
  viewportToolsGroup.style.cssText = [
1139
1356
  'display:flex',
@@ -1238,7 +1455,7 @@ export function createLocalEditorBrowserUi(options = {}) {
1238
1455
  sceneToolOverlay.appendChild(actionGroup);
1239
1456
  sceneToolOverlay.appendChild(viewportToolsGroup);
1240
1457
  sceneToolOverlay.appendChild(sceneUtilityActions);
1241
- sceneToolOverlay.appendChild(mainCameraPreviewGroup);
1458
+ sceneToolOverlay.appendChild(cameraPreviewGroup);
1242
1459
  sceneToolOverlay.appendChild(editorStatusButton);
1243
1460
  sceneToolOverlay.appendChild(toolbarOverflowButton);
1244
1461
  workbench.sceneHeader.appendChild(sceneToolOverlay);
@@ -1275,6 +1492,164 @@ export function createLocalEditorBrowserUi(options = {}) {
1275
1492
  shortcutHelpPanel.classList.add(LOCAL_EDITOR_THEME_CLASS);
1276
1493
  root.appendChild(shortcutHelpPanel);
1277
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);
1278
1653
  function applyThemeToSurfaces() {
1279
1654
  applyLocalEditorTheme(hostChrome, activeTheme);
1280
1655
  applyLocalEditorTheme(workbench.root, activeTheme);
@@ -1282,6 +1657,8 @@ export function createLocalEditorBrowserUi(options = {}) {
1282
1657
  applyLocalEditorTheme(measurementOverlay.root, activeTheme);
1283
1658
  applyLocalEditorTheme(sceneFrameRateOverlay.root, activeTheme);
1284
1659
  applyLocalEditorTheme(shortcutHelpPanel, activeTheme);
1660
+ applyLocalEditorTheme(assetPreviewModal, activeTheme);
1661
+ applyLocalEditorTheme(assetPickerModal, activeTheme);
1285
1662
  applyLocalEditorTheme(boxSelectionOverlay, activeTheme);
1286
1663
  applyLocalEditorTheme(localTestMenu, activeTheme);
1287
1664
  applyLocalEditorTheme(toolbarOverflowMenu, activeTheme);
@@ -1493,14 +1870,14 @@ export function createLocalEditorBrowserUi(options = {}) {
1493
1870
  actionGroup,
1494
1871
  viewportToolsGroup,
1495
1872
  sceneUtilityActions,
1496
- mainCameraPreviewGroup,
1873
+ cameraPreviewGroup,
1497
1874
  editorStatusButton,
1498
1875
  toolbarOverflowButton,
1499
1876
  ];
1500
1877
  const toolbarOverflowItems = [
1501
1878
  { id: 'editor-status', element: editorStatusButton, kind: 'group' },
1502
1879
  { id: 'scene-utilities', element: sceneUtilityActions, kind: 'group' },
1503
- { id: 'main-camera-preview', element: mainCameraPreviewGroup, kind: 'group' },
1880
+ { id: 'camera-preview', element: cameraPreviewGroup, kind: 'group' },
1504
1881
  { id: 'viewport-tools', element: viewportToolsGroup, kind: 'group' },
1505
1882
  { id: 'transform-actions', element: actionGroup, kind: 'group' },
1506
1883
  { id: 'placement', element: placementGroup, kind: 'group' },
@@ -1705,9 +2082,9 @@ export function createLocalEditorBrowserUi(options = {}) {
1705
2082
  const visible = currentState?.grid?.visible ?? true;
1706
2083
  callbacks.onGridVisibleChange?.(!visible);
1707
2084
  });
1708
- mainCameraPreviewButton.addEventListener('click', () => {
1709
- const enabled = currentState?.mainCameraPreview?.enabled ?? false;
1710
- callbacks.onMainCameraPreviewToggle?.(!enabled);
2085
+ sceneCameraButton.addEventListener('click', () => {
2086
+ const enabled = currentState?.sceneCameraPreview?.enabled ?? false;
2087
+ callbacks.onSceneCameraPreviewToggle?.(!enabled);
1711
2088
  });
1712
2089
  const toggleCoordinateAxesProjection = (event) => {
1713
2090
  event.preventDefault();
@@ -1766,10 +2143,414 @@ export function createLocalEditorBrowserUi(options = {}) {
1766
2143
  if (open)
1767
2144
  contextMenu.close();
1768
2145
  helpOpen = open;
1769
- inputRouter.setModalOpen(open);
2146
+ syncInputModalOpen();
1770
2147
  shortcutHelpPanel.style.display = helpOpen ? '' : 'none';
1771
2148
  LocalEditorShared.applyButtonActiveState(sceneHelpButton, helpOpen);
1772
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
+ }
1773
2554
  sceneHelpButton.addEventListener('click', () => {
1774
2555
  setShortcutHelpOpen(!helpOpen);
1775
2556
  });
@@ -1782,6 +2563,44 @@ export function createLocalEditorBrowserUi(options = {}) {
1782
2563
  return;
1783
2564
  setShortcutHelpOpen(false);
1784
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
+ });
1785
2604
  toolGroup.addEventListener('click', (event) => {
1786
2605
  const target = event.target instanceof HTMLElement ? event.target : null;
1787
2606
  const tool = target?.closest('[data-transform-tool]')?.dataset.transformTool;
@@ -1871,11 +2690,70 @@ export function createLocalEditorBrowserUi(options = {}) {
1871
2690
  render(currentState);
1872
2691
  return;
1873
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
+ }
1874
2701
  const assetButton = target?.closest('[data-editor-asset-id]');
1875
2702
  if (assetButton?.dataset.editorAssetId) {
1876
- 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);
1877
2748
  return;
1878
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
+ }
1879
2757
  });
1880
2758
  const hierarchyController = createLocalEditorHierarchyController({
1881
2759
  doc,
@@ -1892,6 +2770,8 @@ export function createLocalEditorBrowserUi(options = {}) {
1892
2770
  onBeforeOpenContextMenu: () => {
1893
2771
  if (helpOpen)
1894
2772
  setShortcutHelpOpen(false);
2773
+ if (assetPreviewOpen)
2774
+ closeAssetPreviewModal();
1895
2775
  },
1896
2776
  });
1897
2777
  assetPanel.addEventListener('input', (event) => {
@@ -1914,10 +2794,38 @@ export function createLocalEditorBrowserUi(options = {}) {
1914
2794
  return;
1915
2795
  if (readInspectorCommitMode(input) !== 'live')
1916
2796
  return;
1917
- const propertyInput = createInspectorPropertyInput(input, 'input', 'live');
2797
+ const propertyInput = createInspectorPropertyInput(input, 'input');
1918
2798
  if (propertyInput)
1919
2799
  callbacks.onPropertyInput?.(propertyInput);
1920
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
+ });
1921
2829
  inspectorPanel.addEventListener('change', (event) => {
1922
2830
  const input = event.target instanceof HTMLInputElement || event.target instanceof HTMLSelectElement
1923
2831
  ? event.target
@@ -1928,7 +2836,7 @@ export function createLocalEditorBrowserUi(options = {}) {
1928
2836
  if (readInspectorCommitMode(input) !== 'change')
1929
2837
  return;
1930
2838
  }
1931
- const propertyInput = createInspectorPropertyInput(input, readInspectorImmediateSource(input), 'final');
2839
+ const propertyInput = createInspectorPropertyInput(input, readInspectorImmediateSource(input));
1932
2840
  if (propertyInput)
1933
2841
  callbacks.onPropertyInput?.(propertyInput);
1934
2842
  });
@@ -1936,27 +2844,65 @@ export function createLocalEditorBrowserUi(options = {}) {
1936
2844
  const input = event.target instanceof HTMLInputElement ? event.target : null;
1937
2845
  if (!input?.dataset.serializedPath || !input.dataset.serializedTargetId)
1938
2846
  return;
1939
- if (hasPendingInspectorNumberValue(input, 'final')) {
1940
- if (currentState)
1941
- render(currentState);
2847
+ if (readInspectorCommitMode(input) !== 'blur')
1942
2848
  return;
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);
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'))
1951
2860
  return;
2861
+ if (readRenderingCommitMode(input) !== 'live')
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;
1952
2876
  }
1953
- 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)
2886
+ return;
2887
+ if (readRenderingCommitMode(input) !== 'blur')
1954
2888
  return;
1955
- const propertyInput = createInspectorPropertyInput(input, 'input', 'final');
2889
+ const propertyInput = createRenderingPropertyInput(input, input instanceof HTMLTextAreaElement ? 'list' : 'input');
1956
2890
  if (propertyInput)
1957
- callbacks.onPropertyInput?.(propertyInput);
1958
- else if (currentState)
1959
- 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 });
1960
2906
  });
1961
2907
  const onKeyDown = (event) => {
1962
2908
  if (event.defaultPrevented)
@@ -1964,6 +2910,16 @@ export function createLocalEditorBrowserUi(options = {}) {
1964
2910
  if (currentState?.mode !== 'editor' || currentState.busy)
1965
2911
  return;
1966
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
+ }
1967
2923
  if (key === 'escape' && snapSettingsOpen) {
1968
2924
  event.preventDefault();
1969
2925
  closeSnapSettingsPopover();
@@ -2053,7 +3009,6 @@ export function createLocalEditorBrowserUi(options = {}) {
2053
3009
  `[data-serialized-target-id="${cssEscape(active.dataset.serializedTargetId)}"]`,
2054
3010
  ].join(''),
2055
3011
  value: active.value,
2056
- preserveValue: hasPendingInspectorNumberValue(active),
2057
3012
  };
2058
3013
  }
2059
3014
  if (active.dataset.editorInspectorSearch != null) {
@@ -2074,8 +3029,6 @@ export function createLocalEditorBrowserUi(options = {}) {
2074
3029
  if (!input)
2075
3030
  return;
2076
3031
  input.focus({ preventScroll: true });
2077
- if (snapshot.value != null && snapshot.preserveValue)
2078
- input.value = snapshot.value;
2079
3032
  if (snapshot.value != null && input.value === snapshot.value) {
2080
3033
  const position = input.value.length;
2081
3034
  try {
@@ -2113,7 +3066,7 @@ export function createLocalEditorBrowserUi(options = {}) {
2113
3066
  contextMenu.close();
2114
3067
  hostChrome.style.display = !inEditor && localTestActionsEnabled ? 'flex' : 'none';
2115
3068
  enterEditorButton.disabled = disabled;
2116
- 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]) {
2117
3070
  button.style.display = 'inline-flex';
2118
3071
  button.disabled = disabled;
2119
3072
  }
@@ -2123,11 +3076,17 @@ export function createLocalEditorBrowserUi(options = {}) {
2123
3076
  closeLocalTestMenu();
2124
3077
  if (!inEditor)
2125
3078
  helpOpen = false;
2126
- 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();
2127
3088
  const transformTool = state.transformTool ?? null;
2128
3089
  workbench.root.style.display = inEditor ? '' : 'none';
2129
- if (inEditor)
2130
- workbenchLayoutController.refresh();
2131
3090
  sceneToolOverlay.style.display = inEditor ? 'flex' : 'none';
2132
3091
  if (!inEditor)
2133
3092
  closeSnapSettingsPopover();
@@ -2137,20 +3096,20 @@ export function createLocalEditorBrowserUi(options = {}) {
2137
3096
  closeTransformActionPopover();
2138
3097
  if (!inEditor)
2139
3098
  closeOverlaySettingsPopover();
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
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
2144
3103
  ? '从 Main Camera 查看当前场景'
2145
3104
  : '当前场景没有可预览的 Main Camera';
2146
- setToolbarButtonTooltip(mainCameraPreviewButton, mainCameraPreviewTooltip);
2147
- LocalEditorShared.applyButtonActiveState(mainCameraPreviewButton, mainCameraPreview.enabled);
3105
+ setToolbarButtonTooltip(sceneCameraButton, sceneCameraTooltip);
3106
+ LocalEditorShared.applyButtonActiveState(sceneCameraButton, sceneCameraPreview.enabled);
2148
3107
  const viewportTools = state.viewportTools ?? null;
2149
- const viewportControlsDisabled = disabled || mainCameraPreview.enabled;
2150
- const viewportControlsTooltip = mainCameraPreview.enabled
3108
+ const viewportControlsDisabled = disabled || sceneCameraPreview.enabled;
3109
+ const viewportControlsTooltip = sceneCameraPreview.enabled
2151
3110
  ? '当前为 Main Camera 预览,请关闭后再使用视口工具'
2152
3111
  : '';
2153
- const viewportProjectionTooltip = mainCameraPreview.enabled
3112
+ const viewportProjectionTooltip = sceneCameraPreview.enabled
2154
3113
  ? '当前为 Main Camera 预览,投影由 Main Camera 决定;关闭预览后可切换编辑视口投影'
2155
3114
  : '';
2156
3115
  viewportToolsGroup.style.display = inEditor ? 'flex' : 'none';
@@ -2247,6 +3206,8 @@ export function createLocalEditorBrowserUi(options = {}) {
2247
3206
  redoButton.disabled = disabled || !state.session?.canRedo;
2248
3207
  LocalEditorShared.applyButtonActiveState(sceneHelpButton, inEditor && helpOpen);
2249
3208
  shortcutHelpPanel.style.display = inEditor && helpOpen ? '' : 'none';
3209
+ renderAssetPreviewModal();
3210
+ renderAssetPickerModal();
2250
3211
  dirtyBadge.style.display = inEditor && state.session?.dirty ? 'inline-flex' : 'none';
2251
3212
  const editorStatusText = [
2252
3213
  state.status,
@@ -2293,12 +3254,22 @@ export function createLocalEditorBrowserUi(options = {}) {
2293
3254
  if (!inEditor)
2294
3255
  return;
2295
3256
  const hierarchyDescriptor = panelRegistry.getActivePanel('left');
2296
- const inspectorDescriptor = panelRegistry.getActivePanel('right');
3257
+ const rightDescriptor = panelRegistry.getActivePanel('right');
3258
+ const activeRightTab = panelRegistry.getRightDockTab();
2297
3259
  hierarchyPanel.dataset.editorPanelId = hierarchyDescriptor?.id ?? 'hierarchy';
2298
- 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';
2299
3264
  hierarchyController.render(state);
2300
- LocalEditorPanels.renderWorkbenchBottomDockPanel(doc, assetPanel, state, panelRegistry.getBottomDockTab(), panelRegistry.getPanels('bottom'));
2301
- 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
+ }
2302
3273
  restoreEditableFocus(doc, focusSnapshot);
2303
3274
  };
2304
3275
  return {
@@ -2334,8 +3305,11 @@ export function createLocalEditorBrowserUi(options = {}) {
2334
3305
  overlaySettingsPopover.remove();
2335
3306
  spatialOverlay.root.remove();
2336
3307
  measurementOverlay.root.remove();
3308
+ sceneFrameRateOverlay.root.remove();
2337
3309
  boxSelectionOverlay.remove();
2338
3310
  shortcutHelpPanel.remove();
3311
+ assetPreviewModal.remove();
3312
+ assetPickerModal.remove();
2339
3313
  hierarchyController.dispose();
2340
3314
  workbenchLayoutController.dispose();
2341
3315
  tooltipController.dispose();