@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
@@ -1,10 +1,20 @@
1
- import { createLocalEditorIcon } from './local-editor-ui-icons';
1
+ import { createLocalEditorIcon, isLocalEditorIconName, } from './local-editor-ui-icons';
2
2
  import { createBadge, createAssetList, createDockTab, createEditorInputStyle, createEmptyState, createListItemBlock, createPanelHeader, createPropertyRow, createToolbarButton, createTreeView, createTreeViewItem, } from './local-editor-ui-primitives';
3
3
  import { clearElement, toTitle } from './local-editor-ui-shared';
4
- const DOCK_TAB_ICONS = {
4
+ const BOTTOM_DOCK_TAB_ICONS = {
5
5
  assets: 'asset',
6
6
  history: 'history',
7
7
  };
8
+ const RIGHT_DOCK_TAB_ICONS = {
9
+ inspector: 'inspector',
10
+ rendering: 'world',
11
+ };
12
+ const ASSET_BROWSER_TABS = [
13
+ { id: 'all', label: '全部', title: '显示全部项目资产' },
14
+ { id: 'models', label: '模型', title: '显示模型资产' },
15
+ { id: 'materials', label: '材质', title: '显示项目材质资产' },
16
+ { id: 'textures', label: '贴图', title: '显示贴图资产' },
17
+ ];
8
18
  export function renderHierarchyPanel(doc, panel, state, rename, drop) {
9
19
  clearElement(panel);
10
20
  const createGroupButton = createToolbarButton(doc, '+ Empty', 'object');
@@ -59,15 +69,15 @@ export function renderHierarchyPanel(doc, panel, state, rename, drop) {
59
69
  }
60
70
  panel.appendChild(list);
61
71
  }
62
- export function renderBottomDockPanel(doc, panel, state, activeTab) {
72
+ export function renderBottomDockPanel(doc, panel, state, activeTab, activeAssetBrowserTab = 'all') {
63
73
  clearElement(panel);
64
74
  appendDockTabs(doc, panel, activeTab);
65
75
  if (activeTab === 'history')
66
76
  renderHistoryPanel(doc, panel, state.session?.history ?? null);
67
77
  else
68
- renderAssetBrowserContent(doc, panel, state);
78
+ renderAssetBrowserContent(doc, panel, state, 'list', activeAssetBrowserTab);
69
79
  }
70
- export function renderWorkbenchBottomDockPanel(doc, panel, state, activeTab, panels = []) {
80
+ export function renderWorkbenchBottomDockPanel(doc, panel, state, activeTab, panels = [], activeAssetBrowserTab = 'all') {
71
81
  clearElement(panel);
72
82
  const tabHeader = doc.createElement('div');
73
83
  tabHeader.style.cssText = [
@@ -85,7 +95,7 @@ export function renderWorkbenchBottomDockPanel(doc, panel, state, activeTab, pan
85
95
  { id: 'history', title: '历史', area: 'bottom' },
86
96
  ];
87
97
  for (const tabPanel of dockTabs) {
88
- const button = createDockTab(doc, tabPanel.title, activeTab === tabPanel.id, DOCK_TAB_ICONS[tabPanel.id]);
98
+ const button = createDockTab(doc, tabPanel.title, activeTab === tabPanel.id, BOTTOM_DOCK_TAB_ICONS[tabPanel.id]);
89
99
  button.dataset.editorDockTab = tabPanel.id;
90
100
  tabHeader.appendChild(button);
91
101
  }
@@ -96,7 +106,7 @@ export function renderWorkbenchBottomDockPanel(doc, panel, state, activeTab, pan
96
106
  if (activeTab === 'history')
97
107
  renderHistoryPanel(doc, content, state.session?.history ?? null);
98
108
  else
99
- renderAssetBrowserContent(doc, content, state, 'grid');
109
+ renderAssetBrowserContent(doc, content, state, 'grid', activeAssetBrowserTab);
100
110
  }
101
111
  function appendDockTabs(doc, panel, activeTab) {
102
112
  const tabs = doc.createElement('div');
@@ -113,7 +123,7 @@ function appendDockTabs(doc, panel, activeTab) {
113
123
  };
114
124
  for (const tab of ['assets', 'history']) {
115
125
  const active = activeTab === tab;
116
- const button = createDockTab(doc, labels[tab], active, DOCK_TAB_ICONS[tab]);
126
+ const button = createDockTab(doc, labels[tab], active, BOTTOM_DOCK_TAB_ICONS[tab]);
117
127
  button.dataset.editorDockTab = tab;
118
128
  button.style.border = '0';
119
129
  button.style.borderBottom = `2px solid ${active ? 'var(--fps-editor-accent-strong)' : 'transparent'}`;
@@ -124,18 +134,299 @@ function appendDockTabs(doc, panel, activeTab) {
124
134
  }
125
135
  panel.appendChild(tabs);
126
136
  }
137
+ export function renderWorkbenchRightDockTabs(doc, panel, activeTab, panels = []) {
138
+ clearElement(panel);
139
+ panel.style.cssText = [
140
+ 'height:35px',
141
+ 'display:flex',
142
+ 'align-items:stretch',
143
+ 'border-bottom:1px solid var(--fps-editor-divider)',
144
+ 'background:var(--fps-editor-chrome-dark)',
145
+ 'flex:0 0 auto',
146
+ ].join(';');
147
+ const dockTabs = panels.length > 0
148
+ ? panels.filter((panelDescriptor) => (panelDescriptor.id === 'inspector' || panelDescriptor.id === 'rendering'))
149
+ : [
150
+ { id: 'inspector', title: '检查器', area: 'right' },
151
+ { id: 'rendering', title: '渲染', area: 'right' },
152
+ ];
153
+ for (const tabPanel of dockTabs) {
154
+ const button = createDockTab(doc, tabPanel.title, activeTab === tabPanel.id, RIGHT_DOCK_TAB_ICONS[tabPanel.id]);
155
+ button.dataset.editorRightDockTab = tabPanel.id;
156
+ tabHeaderButtonStyle(button);
157
+ panel.appendChild(button);
158
+ }
159
+ }
160
+ export function renderRenderingPanel(doc, panel, state) {
161
+ clearElement(panel);
162
+ const rendering = state.renderingPanel ?? null;
163
+ panel.appendChild(createPanelHeader(doc, rendering?.title ?? 'Rendering', createRenderingPanelActions(doc, rendering), 'world'));
164
+ if (!rendering || rendering.sections.length === 0) {
165
+ panel.appendChild(createEmptyState(doc, '当前项目没有提供渲染设置。'));
166
+ return;
167
+ }
168
+ if (rendering.dirty || rendering.status) {
169
+ const status = doc.createElement('div');
170
+ status.style.cssText = [
171
+ 'display:flex',
172
+ 'align-items:center',
173
+ 'gap:6px',
174
+ 'margin:0 0 8px',
175
+ 'min-height:20px',
176
+ ].join(';');
177
+ if (rendering.dirty)
178
+ status.appendChild(createBadge(doc, 'UNSAVED', { compact: true, tone: 'warning' }));
179
+ if (rendering.status) {
180
+ const message = doc.createElement('span');
181
+ message.textContent = rendering.status;
182
+ message.style.cssText = [
183
+ `color:${rendering.statusTone === 'error' ? 'var(--fps-editor-danger-text)' : rendering.statusTone === 'warning' ? 'var(--fps-editor-warn)' : 'var(--fps-editor-muted)'}`,
184
+ 'font-size:11px',
185
+ 'font-weight:800',
186
+ 'min-width:0',
187
+ 'overflow:hidden',
188
+ 'text-overflow:ellipsis',
189
+ 'white-space:nowrap',
190
+ ].join(';');
191
+ status.appendChild(message);
192
+ }
193
+ panel.appendChild(status);
194
+ }
195
+ if (rendering.summary) {
196
+ const summary = doc.createElement('div');
197
+ summary.textContent = rendering.summary;
198
+ summary.style.cssText = [
199
+ 'margin:0 0 8px',
200
+ 'color:var(--fps-editor-muted)',
201
+ 'font-size:11px',
202
+ 'line-height:1.45',
203
+ ].join(';');
204
+ panel.appendChild(summary);
205
+ }
206
+ for (const section of rendering.sections) {
207
+ panel.appendChild(createRenderingSectionBlock(doc, section));
208
+ }
209
+ }
210
+ function createRenderingPanelActions(doc, rendering) {
211
+ return (rendering?.actions ?? []).map(action => {
212
+ const icon = action.icon && isLocalEditorIconName(action.icon) ? action.icon : 'world';
213
+ const button = createToolbarButton(doc, action.label, icon);
214
+ button.type = 'button';
215
+ button.dataset.editorRenderingAction = action.id;
216
+ button.disabled = action.disabled === true;
217
+ button.title = action.tooltip ?? action.label;
218
+ button.dataset.editorTooltip = button.title;
219
+ button.style.padding = '3px 7px';
220
+ button.style.fontSize = '11px';
221
+ return button;
222
+ });
223
+ }
224
+ function tabHeaderButtonStyle(button) {
225
+ button.style.borderTop = '0';
226
+ button.style.borderLeft = '0';
227
+ button.style.borderRadius = '0';
228
+ button.style.minWidth = '0';
229
+ }
230
+ function createRenderingSectionBlock(doc, section) {
231
+ const block = doc.createElement('section');
232
+ block.dataset.editorRenderingSection = section.id;
233
+ block.style.cssText = [
234
+ 'margin:0 0 10px',
235
+ 'padding:8px',
236
+ 'border:1px solid var(--fps-editor-border)',
237
+ 'border-radius:3px',
238
+ 'background:var(--fps-editor-panel-soft)',
239
+ ].join(';');
240
+ const header = doc.createElement('div');
241
+ header.style.cssText = 'display:flex;align-items:flex-start;justify-content:space-between;gap:8px;margin:0 0 8px';
242
+ const title = doc.createElement('div');
243
+ title.style.cssText = 'min-width:0';
244
+ const heading = doc.createElement('h3');
245
+ heading.textContent = section.title;
246
+ heading.style.cssText = 'margin:0;color:var(--fps-editor-text-strong);font-size:12px;font-weight:900';
247
+ title.appendChild(heading);
248
+ if (section.summary) {
249
+ const summary = doc.createElement('div');
250
+ summary.textContent = section.summary;
251
+ summary.style.cssText = 'margin-top:3px;color:var(--fps-editor-muted);font-size:11px;line-height:1.35';
252
+ title.appendChild(summary);
253
+ }
254
+ header.appendChild(title);
255
+ header.appendChild(createBadge(doc, `${section.systems.length}`, { compact: true }));
256
+ block.appendChild(header);
257
+ const list = doc.createElement('div');
258
+ list.style.cssText = 'display:flex;flex-direction:column;gap:8px';
259
+ for (const system of section.systems) {
260
+ list.appendChild(createRenderingSystemBlock(doc, section.id, system));
261
+ }
262
+ block.appendChild(list);
263
+ return block;
264
+ }
265
+ function createRenderingSystemBlock(doc, sectionId, system) {
266
+ const block = createListItemBlock(doc);
267
+ block.dataset.editorRenderingSystem = system.id;
268
+ block.dataset.editorRenderingSystemKind = system.kind;
269
+ const header = doc.createElement('div');
270
+ header.style.cssText = 'display:flex;align-items:flex-start;justify-content:space-between;gap:8px;margin:0 0 8px';
271
+ const title = doc.createElement('div');
272
+ title.style.cssText = 'min-width:0';
273
+ const label = doc.createElement('div');
274
+ label.textContent = system.label;
275
+ label.style.cssText = 'color:var(--fps-editor-text-strong);font-size:12px;font-weight:900;line-height:1.25';
276
+ title.appendChild(label);
277
+ if (system.summary) {
278
+ const summary = doc.createElement('div');
279
+ summary.textContent = system.summary;
280
+ summary.style.cssText = 'margin-top:3px;color:var(--fps-editor-muted);font-size:11px;line-height:1.35';
281
+ title.appendChild(summary);
282
+ }
283
+ header.appendChild(title);
284
+ const badges = doc.createElement('div');
285
+ badges.style.cssText = 'display:flex;align-items:center;gap:4px;flex:0 0 auto';
286
+ if (system.active != null)
287
+ badges.appendChild(createBadge(doc, system.active ? 'ON' : 'OFF', { compact: true, tone: system.active ? 'success' : 'default' }));
288
+ if (system.readOnly)
289
+ badges.appendChild(createBadge(doc, 'READONLY', { compact: true }));
290
+ if (system.status)
291
+ badges.appendChild(createBadge(doc, system.status, { compact: true, tone: system.active === false ? 'warning' : 'default' }));
292
+ header.appendChild(badges);
293
+ block.appendChild(header);
294
+ const propertyGrid = doc.createElement('div');
295
+ propertyGrid.style.cssText = 'display:flex;flex-direction:column;gap:6px';
296
+ for (const property of system.properties) {
297
+ propertyGrid.appendChild(createRenderingPropertyRow(doc, sectionId, system.id, property, system.readOnly === true));
298
+ }
299
+ block.appendChild(propertyGrid);
300
+ return block;
301
+ }
302
+ function createRenderingPropertyRow(doc, sectionId, systemId, property, systemReadOnly) {
303
+ const inspectorProperty = createInspectorPropertyFromRenderingProperty(property, systemReadOnly);
304
+ const control = createRenderingPropertyControl(doc, sectionId, systemId, property, inspectorProperty);
305
+ const access = getInspectorPropertyAccess(inspectorProperty);
306
+ const effect = getInspectorPropertyEffect(inspectorProperty);
307
+ const row = createPropertyRow(doc, inspectorProperty.label, control);
308
+ row.dataset.editorInspectorAccess = access;
309
+ row.dataset.editorInspectorEffect = effect;
310
+ row.title = createInspectorStatusTitle(access, effect, inspectorProperty.tooltip ?? inspectorProperty.disabledReason);
311
+ row.dataset.editorTooltip = row.title;
312
+ const label = row.firstElementChild;
313
+ if (label)
314
+ label.style.cssText = createInspectorPropertyLabelStyle(access, effect);
315
+ return row;
316
+ }
317
+ function createRenderingPropertyControl(doc, sectionId, systemId, property, inspectorProperty) {
318
+ const target = createRenderingInspectorTarget(systemId);
319
+ if (isInspectorPropertyEditable(inspectorProperty) && property.control === 'string-list') {
320
+ return createRenderingStringListControl(doc, sectionId, systemId, property, inspectorProperty);
321
+ }
322
+ let control = renderInspectorControl(doc, builtinInspectorControlRegistrations, target, inspectorProperty);
323
+ bindRenderedInspectorControlsToRendering(control, sectionId, systemId, property);
324
+ if (property.unit && isInspectorPropertyEditable(inspectorProperty) && property.control === 'number') {
325
+ const wrapper = doc.createElement('div');
326
+ wrapper.style.cssText = 'display:flex;align-items:center;gap:6px;min-width:0';
327
+ control.style.minWidth = '0';
328
+ control.style.flex = '1 1 auto';
329
+ wrapper.appendChild(control);
330
+ wrapper.appendChild(createPropertyUnitLabel(doc, property.unit));
331
+ return wrapper;
332
+ }
333
+ return control;
334
+ }
335
+ function bindRenderingControl(element, sectionId, systemId, property, control) {
336
+ element.dataset.editorRenderingProperty = 'true';
337
+ element.dataset.renderingSectionId = sectionId;
338
+ element.dataset.renderingSystemId = systemId;
339
+ element.dataset.renderingPath = property.path;
340
+ element.dataset.renderingControl = control;
341
+ element.dataset.renderingValueType = property.valueType;
342
+ element.dataset.renderingCommitMode = property.commitMode ?? (control === 'string-list' ? 'blur' : 'live');
343
+ if (property.disabled) {
344
+ element.disabled = true;
345
+ element.title = property.disabledReason ?? '';
346
+ }
347
+ }
348
+ function createInspectorPropertyFromRenderingProperty(property, systemReadOnly) {
349
+ const readOnly = systemReadOnly || property.readOnly === true || property.control === 'readonly' || property.disabled === true;
350
+ return {
351
+ path: property.path,
352
+ label: property.label,
353
+ valueType: property.valueType === 'string-list' ? 'string' : property.valueType,
354
+ control: property.control === 'string-list'
355
+ ? readOnly ? 'readonly' : 'string'
356
+ : property.control,
357
+ value: property.value,
358
+ readOnly,
359
+ persistence: readOnly ? 'readonly' : 'document',
360
+ commitMode: property.commitMode ?? (property.control === 'string-list' ? 'blur' : 'live'),
361
+ tooltip: property.tooltip,
362
+ effect: property.disabled ? 'unsupported' : undefined,
363
+ disabledReason: property.disabledReason,
364
+ placeholder: property.placeholder,
365
+ min: property.min,
366
+ max: property.max,
367
+ step: property.step,
368
+ tags: property.tags,
369
+ };
370
+ }
371
+ function createRenderingInspectorTarget(systemId) {
372
+ return {
373
+ targetIds: [systemId],
374
+ activeId: systemId,
375
+ selection: {
376
+ targetIds: [systemId],
377
+ activeId: systemId,
378
+ targetKind: 'rendering-system',
379
+ },
380
+ sections: [],
381
+ };
382
+ }
383
+ function bindRenderedInspectorControlsToRendering(control, sectionId, systemId, property) {
384
+ const fields = control.matches('input, textarea, select')
385
+ ? [control]
386
+ : Array.from(control.querySelectorAll('input, textarea, select'));
387
+ for (const field of fields) {
388
+ if (!(field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement))
389
+ continue;
390
+ bindRenderingControl(field, sectionId, systemId, property, property.control);
391
+ }
392
+ }
393
+ function createRenderingStringListControl(doc, sectionId, systemId, property, inspectorProperty) {
394
+ const input = doc.createElement('textarea');
395
+ input.value = Array.isArray(property.value) ? property.value.map(String).join('\n') : String(property.value ?? '');
396
+ input.placeholder = property.placeholder ?? 'One pattern per line';
397
+ input.rows = 3;
398
+ input.style.cssText = [
399
+ createInspectorInputStyle(),
400
+ 'height:auto',
401
+ 'min-height:66px',
402
+ 'resize:vertical',
403
+ 'padding:6px 8px',
404
+ 'line-height:1.35',
405
+ ].join(';');
406
+ input.title = inspectorProperty.tooltip ?? inspectorProperty.label;
407
+ input.dataset.editorTooltip = input.title;
408
+ bindRenderingControl(input, sectionId, systemId, property, 'string-list');
409
+ return input;
410
+ }
411
+ function createPropertyUnitLabel(doc, unit) {
412
+ const label = doc.createElement('span');
413
+ label.textContent = unit;
414
+ label.style.cssText = 'flex:0 0 auto;color:var(--fps-editor-muted);font-size:10px;font-weight:800';
415
+ return label;
416
+ }
127
417
  function createHeadingLabel(doc, text) {
128
418
  const label = doc.createElement('span');
129
419
  label.textContent = text;
130
420
  label.style.cssText = 'min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap';
131
421
  return label;
132
422
  }
133
- function renderAssetBrowserContent(doc, panel, state, variant = 'list') {
423
+ function renderAssetBrowserContent(doc, panel, state, variant = 'list', activeTab = 'all') {
134
424
  const title = doc.createElement('h2');
135
425
  title.style.cssText = 'display:flex;align-items:center;gap:6px;font-size:13px;margin:0 0 8px;font-weight:800;color:var(--fps-editor-text-strong)';
136
426
  title.appendChild(createLocalEditorIcon(doc, 'asset'));
137
427
  title.appendChild(createHeadingLabel(doc, '资产浏览器'));
138
428
  panel.appendChild(title);
429
+ appendAssetBrowserTabs(doc, panel, state, activeTab);
139
430
  const filter = doc.createElement('input');
140
431
  filter.type = 'search';
141
432
  filter.placeholder = '筛选资产';
@@ -153,8 +444,9 @@ function renderAssetBrowserContent(doc, panel, state, variant = 'list') {
153
444
  'margin:0 0 8px',
154
445
  ].join(';');
155
446
  panel.appendChild(filter);
447
+ const tabAssets = filterAssetsByBrowserTab(state.assets, activeTab);
156
448
  const normalizedFilter = state.assetFilter.trim().toLowerCase();
157
- const filteredAssets = state.assets
449
+ const filteredAssets = tabAssets
158
450
  .filter((asset) => {
159
451
  if (!normalizedFilter)
160
452
  return true;
@@ -173,33 +465,38 @@ function renderAssetBrowserContent(doc, panel, state, variant = 'list') {
173
465
  })
174
466
  .slice(0, 80);
175
467
  const count = doc.createElement('div');
176
- count.textContent = state.assetCountLabel
468
+ const activeTabLabel = ASSET_BROWSER_TABS.find(tab => tab.id === activeTab)?.label ?? '资产';
469
+ count.textContent = activeTab === 'all' && state.assetCountLabel
177
470
  ? `${filteredAssets.length} / ${state.assetCountLabel}`
178
- : `${filteredAssets.length} / ${state.assets.length} 个资产`;
471
+ : `${filteredAssets.length} / ${tabAssets.length} 个${activeTabLabel}资产`;
179
472
  count.style.cssText = 'color:var(--fps-editor-muted);font-size:11px;margin-bottom:6px';
180
473
  panel.appendChild(count);
181
474
  const list = createAssetList(doc, variant === 'grid');
182
475
  for (const asset of filteredAssets) {
476
+ const selected = state.selectedAssetId === asset.id;
477
+ const hasPreview = !!asset.preview;
183
478
  const button = doc.createElement('button');
184
479
  button.type = 'button';
185
480
  button.dataset.editorAssetId = asset.id;
186
481
  button.disabled = asset.disabled ?? false;
482
+ button.setAttribute('aria-selected', selected ? 'true' : 'false');
187
483
  button.style.cssText = [
188
484
  'width:100%',
189
- `min-height:${variant === 'grid' ? '48px' : '32px'}`,
485
+ `min-height:${variant === 'grid' ? (hasPreview ? '66px' : '48px') : (hasPreview ? '38px' : '32px')}`,
190
486
  'text-align:left',
191
- 'padding:6px 8px',
192
- 'border:1px solid var(--fps-editor-border)',
487
+ `padding:${hasPreview ? '7px 8px' : '6px 8px'}`,
488
+ `border:1px solid ${selected ? 'var(--fps-editor-accent-strong)' : 'var(--fps-editor-border)'}`,
193
489
  'border-radius:3px',
194
- 'background:var(--fps-editor-asset-card-bg)',
490
+ `background:${selected ? 'var(--fps-editor-accent-soft)' : 'var(--fps-editor-asset-card-bg)'}`,
195
491
  'color:var(--fps-editor-text)',
196
492
  'display:flex',
197
493
  'align-items:center',
198
494
  'gap:8px',
199
495
  'font-size:12px',
200
- 'cursor:pointer',
496
+ asset.disabled ? 'cursor:not-allowed' : 'cursor:pointer',
497
+ asset.disabled ? 'opacity:0.72' : '',
201
498
  ].join(';');
202
- const icon = createLocalEditorIcon(doc, 'asset', { size: 16 });
499
+ const preview = createAssetPreviewElement(doc, asset, variant);
203
500
  const body = doc.createElement('div');
204
501
  body.style.cssText = 'min-width:0;flex:1';
205
502
  const label = doc.createElement('div');
@@ -210,12 +507,178 @@ function renderAssetBrowserContent(doc, panel, state, variant = 'list') {
210
507
  meta.style.cssText = 'color:var(--fps-editor-muted);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap';
211
508
  body.appendChild(label);
212
509
  body.appendChild(meta);
213
- button.appendChild(icon);
510
+ button.appendChild(preview);
214
511
  button.appendChild(body);
215
512
  list.appendChild(button);
216
513
  }
514
+ if (filteredAssets.length === 0) {
515
+ list.appendChild(createEmptyState(doc, normalizedFilter ? '没有匹配的资产。' : '暂无该类型资产。'));
516
+ }
217
517
  panel.appendChild(list);
218
518
  }
519
+ export function createAssetPreviewElement(doc, asset, variant) {
520
+ const size = variant === 'modal' ? 220 : variant === 'grid' ? 46 : 24;
521
+ const preview = asset.preview;
522
+ if (!preview)
523
+ return createLocalEditorIcon(doc, 'asset', { size: variant === 'grid' ? 18 : 16 });
524
+ const frame = doc.createElement('div');
525
+ frame.style.cssText = [
526
+ `width:${size}px`,
527
+ `height:${size}px`,
528
+ `flex:0 0 ${size}px`,
529
+ 'display:flex',
530
+ 'align-items:center',
531
+ 'justify-content:center',
532
+ 'overflow:hidden',
533
+ 'border:1px solid var(--fps-editor-border)',
534
+ `border-radius:${variant === 'modal' ? '6px' : '3px'}`,
535
+ 'background:var(--fps-editor-field)',
536
+ 'box-sizing:border-box',
537
+ ].join(';');
538
+ if (preview.kind === 'image') {
539
+ const image = doc.createElement('img');
540
+ image.src = preview.url;
541
+ image.alt = preview.alt ?? asset.label;
542
+ image.loading = 'lazy';
543
+ image.style.cssText = [
544
+ 'width:100%',
545
+ 'height:100%',
546
+ `object-fit:${preview.fit ?? 'cover'}`,
547
+ 'display:block',
548
+ ].join(';');
549
+ frame.appendChild(image);
550
+ return frame;
551
+ }
552
+ const sphere = doc.createElement('div');
553
+ const metallic = clamp01(preview.metallic ?? 0);
554
+ const roughness = clamp01(preview.roughness ?? 0.55);
555
+ const emissionIntensity = clampNumber(preview.emissionIntensity ?? 0, 0, 8);
556
+ const baseColor = formatAssetPreviewColor(preview.baseColor, '#8f9aa8');
557
+ const emissionColor = formatAssetPreviewColor(preview.emissionColor, '#ffffff');
558
+ sphere.style.cssText = [
559
+ 'position:relative',
560
+ 'width:82%',
561
+ 'height:82%',
562
+ 'overflow:hidden',
563
+ 'border-radius:999px',
564
+ `background:${baseColor}`,
565
+ 'border:1px solid rgba(255,255,255,0.22)',
566
+ 'box-shadow:' + [
567
+ `inset -5px -7px 12px rgba(0,0,0,${(0.26 + roughness * 0.14).toFixed(2)})`,
568
+ `inset 5px 5px 10px rgba(255,255,255,${(0.12 + metallic * 0.2).toFixed(2)})`,
569
+ emissionIntensity > 0 ? `0 0 ${Math.round(6 + emissionIntensity * 5)}px ${emissionColor}` : '0 1px 2px rgba(0,0,0,0.4)',
570
+ ].join(','),
571
+ ].join(';');
572
+ if (preview.textureUrl) {
573
+ const image = doc.createElement('img');
574
+ image.src = preview.textureUrl;
575
+ image.alt = '';
576
+ image.style.cssText = [
577
+ 'position:absolute',
578
+ 'inset:0',
579
+ 'width:100%',
580
+ 'height:100%',
581
+ 'object-fit:cover',
582
+ 'opacity:0.62',
583
+ 'mix-blend-mode:multiply',
584
+ ].join(';');
585
+ sphere.appendChild(image);
586
+ }
587
+ const shading = doc.createElement('div');
588
+ const highlightAlpha = clampNumber(0.86 - roughness * 0.48 + metallic * 0.12, 0.25, 0.92);
589
+ const highlightEnd = Math.round(9 + roughness * 12);
590
+ const shadowAlpha = clampNumber(0.28 + roughness * 0.14 + metallic * 0.16, 0.2, 0.62);
591
+ const rimAlpha = clampNumber(0.12 + metallic * 0.36, 0.08, 0.52);
592
+ shading.style.cssText = [
593
+ 'position:absolute',
594
+ 'inset:0',
595
+ 'border-radius:inherit',
596
+ 'pointer-events:none',
597
+ 'background:' + [
598
+ `radial-gradient(circle at 31% 23%, rgba(255,255,255,${highlightAlpha.toFixed(2)}) 0 ${highlightEnd}%, rgba(255,255,255,0) ${highlightEnd + 17}%)`,
599
+ `radial-gradient(circle at 70% 82%, rgba(0,0,0,${shadowAlpha.toFixed(2)}) 0 26%, rgba(0,0,0,0) 58%)`,
600
+ `radial-gradient(circle at 82% 22%, rgba(255,255,255,${rimAlpha.toFixed(2)}) 0 9%, rgba(255,255,255,0) 36%)`,
601
+ `linear-gradient(135deg, rgba(255,255,255,${(0.08 + metallic * 0.16).toFixed(2)}), rgba(0,0,0,${(0.12 + roughness * 0.1).toFixed(2)}))`,
602
+ ].join(','),
603
+ ].join(';');
604
+ sphere.appendChild(shading);
605
+ frame.appendChild(sphere);
606
+ return frame;
607
+ }
608
+ function formatAssetPreviewColor(value, fallback) {
609
+ if (!value)
610
+ return fallback;
611
+ if (typeof value === 'string')
612
+ return value;
613
+ const r = normalizeAssetPreviewColorChannel(value.r);
614
+ const g = normalizeAssetPreviewColorChannel(value.g);
615
+ const b = normalizeAssetPreviewColorChannel(value.b);
616
+ if (value.a === undefined)
617
+ return `rgb(${r}, ${g}, ${b})`;
618
+ return `rgba(${r}, ${g}, ${b}, ${clampNumber(value.a, 0, 1)})`;
619
+ }
620
+ function normalizeAssetPreviewColorChannel(value) {
621
+ if (!Number.isFinite(value))
622
+ return 0;
623
+ return Math.round(clampNumber(value >= 0 && value <= 1 ? value * 255 : value, 0, 255));
624
+ }
625
+ function clamp01(value) {
626
+ return clampNumber(value, 0, 1);
627
+ }
628
+ function clampNumber(value, min, max) {
629
+ if (!Number.isFinite(value))
630
+ return min;
631
+ return Math.min(max, Math.max(min, value));
632
+ }
633
+ function appendAssetBrowserTabs(doc, panel, state, activeTab) {
634
+ const tabs = doc.createElement('div');
635
+ tabs.style.cssText = [
636
+ 'display:flex',
637
+ 'align-items:center',
638
+ 'gap:4px',
639
+ 'margin:0 0 8px',
640
+ 'min-height:26px',
641
+ ].join(';');
642
+ for (const tab of ASSET_BROWSER_TABS) {
643
+ const button = doc.createElement('button');
644
+ button.type = 'button';
645
+ button.dataset.editorAssetBrowserTab = tab.id;
646
+ button.title = tab.title;
647
+ const active = activeTab === tab.id;
648
+ const count = filterAssetsByBrowserTab(state.assets, tab.id).length;
649
+ button.textContent = `${tab.label} ${count}`;
650
+ button.style.cssText = [
651
+ 'height:24px',
652
+ 'padding:0 8px',
653
+ 'border:1px solid var(--fps-editor-border)',
654
+ 'border-radius:3px',
655
+ `background:${active ? 'var(--fps-editor-accent)' : 'var(--fps-editor-button-bg)'}`,
656
+ `color:${active ? 'var(--fps-editor-text-strong)' : 'var(--fps-editor-muted)'}`,
657
+ 'font-size:11px',
658
+ 'font-weight:800',
659
+ 'cursor:pointer',
660
+ ].join(';');
661
+ tabs.appendChild(button);
662
+ }
663
+ panel.appendChild(tabs);
664
+ }
665
+ function filterAssetsByBrowserTab(assets, tab) {
666
+ if (tab === 'all')
667
+ return [...assets];
668
+ return assets.filter(asset => resolveAssetBrowserTab(asset) === tab);
669
+ }
670
+ function resolveAssetBrowserTab(asset) {
671
+ const kind = `${asset.kind ?? ''}`.trim().toLowerCase();
672
+ const id = `${asset.id ?? ''}`.trim().toLowerCase();
673
+ const assetId = `${asset.assetId ?? ''}`.trim().toLowerCase();
674
+ if (kind === 'material' || kind === 'materials' || id.startsWith('material:') || assetId.startsWith('material:')) {
675
+ return 'materials';
676
+ }
677
+ if (kind === 'texture' || kind === 'image' || kind === 'textures' || id.startsWith('texture:') || assetId.startsWith('texture:')) {
678
+ return 'textures';
679
+ }
680
+ return 'models';
681
+ }
219
682
  function renderHistoryPanel(doc, panel, history) {
220
683
  const title = doc.createElement('h2');
221
684
  const entries = history?.entries ?? [];
@@ -624,6 +1087,18 @@ function readInspectorControlOptionNumber(property, key) {
624
1087
  const value = property.controlOptions?.[key];
625
1088
  return typeof value === 'number' && Number.isFinite(value) ? value : 0;
626
1089
  }
1090
+ function readInspectorAssetPickerPreview(property, key) {
1091
+ const value = property.controlOptions?.[key];
1092
+ return isLocalEditorBrowserAssetPreview(value) ? value : undefined;
1093
+ }
1094
+ function isLocalEditorBrowserAssetPreview(value) {
1095
+ if (!value || typeof value !== 'object' || Array.isArray(value))
1096
+ return false;
1097
+ const preview = value;
1098
+ if (preview.kind === 'image')
1099
+ return typeof preview.url === 'string' && preview.url.length > 0;
1100
+ return preview.kind === 'material-sphere';
1101
+ }
627
1102
  function resolveInspectorVectorGroupLabel(properties, fallback) {
628
1103
  const labels = properties.map(property => {
629
1104
  const match = property.label.match(/^(.*)\.(x|y|z|r|g|b)$/);
@@ -683,6 +1158,20 @@ const builtinInspectorControlRegistrations = [
683
1158
  control: 'color',
684
1159
  render: ({ doc, target, property }) => createInspectorColorControl(doc, target, property),
685
1160
  },
1161
+ {
1162
+ id: 'builtin.open-right-dock-tab',
1163
+ order: 90,
1164
+ control: 'custom',
1165
+ customControl: 'open-right-dock-tab',
1166
+ render: ({ doc, property }) => createInspectorOpenRightDockTabControl(doc, property),
1167
+ },
1168
+ {
1169
+ id: 'builtin.asset-picker-card',
1170
+ order: 95,
1171
+ control: 'custom',
1172
+ customControl: 'asset-picker-card',
1173
+ render: ({ doc, target, property, bindInput }) => createInspectorAssetPickerCardControl(doc, target, property, bindInput),
1174
+ },
686
1175
  {
687
1176
  id: 'builtin.asset',
688
1177
  order: 100,
@@ -743,7 +1232,8 @@ function compareInspectorControlRegistrations(left, right) {
743
1232
  return (left.order ?? 0) - (right.order ?? 0) || left.id.localeCompare(right.id);
744
1233
  }
745
1234
  function renderInspectorControl(doc, registrations, target, property) {
746
- if (!isInspectorPropertyEditable(property))
1235
+ const canRenderCustomAction = property.control === 'custom' && !!property.customControl;
1236
+ if (!isInspectorPropertyEditable(property) && !canRenderCustomAction)
747
1237
  return createInspectorReadonlyControl(doc, target, property);
748
1238
  const context = {
749
1239
  doc,
@@ -756,6 +1246,8 @@ function renderInspectorControl(doc, registrations, target, property) {
756
1246
  const registration = resolveLocalEditorBrowserInspectorControlRegistration(registrations, context);
757
1247
  if (registration)
758
1248
  return registration.render(context);
1249
+ if (!isInspectorPropertyEditable(property))
1250
+ return createInspectorReadonlyControl(doc, target, property);
759
1251
  return createInspectorReadonlyControl(doc, target, property);
760
1252
  }
761
1253
  export function resolveLocalEditorBrowserInspectorControlRegistration(registrations, context) {
@@ -1024,6 +1516,127 @@ function createInspectorColorControl(doc, target, property) {
1024
1516
  input.style.cssText = 'width:34px;height:26px;border:1px solid var(--fps-editor-editable-border);border-radius:3px;background:var(--fps-editor-editable-bg);padding:0;box-shadow:var(--fps-editor-editable-shadow)';
1025
1517
  return input;
1026
1518
  }
1519
+ function createInspectorOpenRightDockTabControl(doc, property) {
1520
+ const targetTabOption = readInspectorControlOptionString(property, 'targetTab');
1521
+ const targetTab = targetTabOption === 'inspector' ? 'inspector' : 'rendering';
1522
+ const label = readInspectorControlOptionString(property, 'label')
1523
+ || (typeof property.value === 'string' ? property.value : property.label);
1524
+ const button = createToolbarButton(doc, label, RIGHT_DOCK_TAB_ICONS[targetTab]);
1525
+ button.type = 'button';
1526
+ button.dataset.editorOpenRightDockTab = targetTab;
1527
+ button.title = property.tooltip ?? label;
1528
+ button.dataset.editorTooltip = button.title;
1529
+ button.style.cssText = [
1530
+ button.style.cssText,
1531
+ 'width:100%',
1532
+ 'justify-content:center',
1533
+ 'height:28px',
1534
+ ].filter(Boolean).join(';');
1535
+ return button;
1536
+ }
1537
+ function createInspectorAssetPickerCardControl(doc, _target, property, bindInput) {
1538
+ const wrapper = doc.createElement('div');
1539
+ wrapper.dataset.editorInspectorAssetPickerControl = 'true';
1540
+ wrapper.style.cssText = [
1541
+ 'min-width:0',
1542
+ 'display:grid',
1543
+ readInspectorControlOptionString(property, 'copyActionValue') ? 'grid-template-columns:1fr auto auto' : 'grid-template-columns:1fr auto',
1544
+ 'align-items:stretch',
1545
+ 'gap:6px',
1546
+ ].join(';');
1547
+ const input = doc.createElement('input');
1548
+ input.type = 'hidden';
1549
+ input.value = property.mixed ? '' : String(property.value ?? '');
1550
+ bindInput(input, { source: 'asset' });
1551
+ wrapper.appendChild(input);
1552
+ const card = doc.createElement('button');
1553
+ card.type = 'button';
1554
+ card.dataset.editorInspectorAssetPicker = 'true';
1555
+ card.disabled = !isInspectorPropertyEditable(property);
1556
+ card.style.cssText = [
1557
+ 'min-width:0',
1558
+ 'min-height:42px',
1559
+ 'display:flex',
1560
+ 'align-items:center',
1561
+ 'gap:8px',
1562
+ 'padding:6px 7px',
1563
+ 'border:1px solid var(--fps-editor-editable-border)',
1564
+ 'border-radius:3px',
1565
+ 'background:var(--fps-editor-field)',
1566
+ 'color:var(--fps-editor-text)',
1567
+ 'text-align:left',
1568
+ card.disabled ? 'cursor:not-allowed' : 'cursor:pointer',
1569
+ card.disabled ? 'opacity:0.72' : '',
1570
+ ].filter(Boolean).join(';');
1571
+ const label = readInspectorControlOptionString(property, 'currentLabel')
1572
+ || readInspectorControlOptionString(property, 'placeholder')
1573
+ || (property.mixed ? '--' : String(property.value || 'None'));
1574
+ const meta = readInspectorControlOptionString(property, 'currentMeta')
1575
+ || readInspectorControlOptionString(property, 'pickerKind')
1576
+ || property.label;
1577
+ const preview = readInspectorAssetPickerPreview(property, 'currentPreview');
1578
+ card.appendChild(createAssetPreviewElement(doc, {
1579
+ id: `${property.path}:preview`,
1580
+ label,
1581
+ preview,
1582
+ }, 'list'));
1583
+ const body = doc.createElement('div');
1584
+ body.style.cssText = 'min-width:0;flex:1';
1585
+ const title = doc.createElement('div');
1586
+ title.textContent = label;
1587
+ title.style.cssText = 'font-weight:900;overflow:hidden;text-overflow:ellipsis;white-space:nowrap';
1588
+ const subtitle = doc.createElement('div');
1589
+ subtitle.textContent = meta;
1590
+ subtitle.style.cssText = 'color:var(--fps-editor-muted);font-size:11px;font-weight:800;overflow:hidden;text-overflow:ellipsis;white-space:nowrap';
1591
+ body.appendChild(title);
1592
+ body.appendChild(subtitle);
1593
+ card.appendChild(body);
1594
+ wrapper.appendChild(card);
1595
+ const action = doc.createElement('button');
1596
+ action.type = 'button';
1597
+ action.dataset.editorInspectorAssetPicker = 'true';
1598
+ action.disabled = card.disabled;
1599
+ action.textContent = readInspectorControlOptionString(property, 'actionLabel') || '替换';
1600
+ action.style.cssText = [
1601
+ 'width:54px',
1602
+ 'min-height:42px',
1603
+ 'padding:0 8px',
1604
+ 'border:1px solid var(--fps-editor-editable-border)',
1605
+ 'border-radius:3px',
1606
+ 'background:var(--fps-editor-button-bg)',
1607
+ 'color:var(--fps-editor-text)',
1608
+ 'font-size:11px',
1609
+ 'font-weight:900',
1610
+ action.disabled ? 'cursor:not-allowed' : 'cursor:pointer',
1611
+ action.disabled ? 'opacity:0.72' : '',
1612
+ ].filter(Boolean).join(';');
1613
+ wrapper.appendChild(action);
1614
+ const copyValue = readInspectorControlOptionString(property, 'copyActionValue');
1615
+ if (copyValue) {
1616
+ const copyAction = doc.createElement('button');
1617
+ copyAction.type = 'button';
1618
+ copyAction.dataset.editorInspectorAssetCopy = 'true';
1619
+ copyAction.dataset.editorInspectorAssetCopyValue = copyValue;
1620
+ copyAction.disabled = card.disabled;
1621
+ copyAction.textContent = readInspectorControlOptionString(property, 'copyActionLabel') || '复制';
1622
+ copyAction.title = readInspectorControlOptionString(property, 'copyActionTooltip');
1623
+ copyAction.style.cssText = [
1624
+ 'width:48px',
1625
+ 'min-height:42px',
1626
+ 'padding:0 8px',
1627
+ 'border:1px solid var(--fps-editor-editable-border)',
1628
+ 'border-radius:3px',
1629
+ 'background:var(--fps-editor-button-bg)',
1630
+ 'color:var(--fps-editor-text)',
1631
+ 'font-size:11px',
1632
+ 'font-weight:900',
1633
+ copyAction.disabled ? 'cursor:not-allowed' : 'cursor:pointer',
1634
+ copyAction.disabled ? 'opacity:0.72' : '',
1635
+ ].filter(Boolean).join(';');
1636
+ wrapper.appendChild(copyAction);
1637
+ }
1638
+ return wrapper;
1639
+ }
1027
1640
  function createInspectorReadonlyControl(doc, _target, property) {
1028
1641
  if (!property.mixed && isExpandableInspectorValue(property.value)) {
1029
1642
  return createInspectorObjectReadonlyControl(doc, property);
@@ -1218,6 +1831,18 @@ export function formatLocalEditorBrowserInspectorNumberValue(value) {
1218
1831
  return '0';
1219
1832
  return rounded.toFixed(3).replace(/\.?0+$/, '');
1220
1833
  }
1834
+ export function parseLocalEditorBrowserInspectorNumberValue(value, mode) {
1835
+ const trimmed = value.trim();
1836
+ if (trimmed === '')
1837
+ return mode === 'final' ? 0 : null;
1838
+ if (mode === 'live' && !isCompleteInspectorNumberInput(trimmed))
1839
+ return null;
1840
+ const numeric = Number(trimmed);
1841
+ return Number.isFinite(numeric) ? numeric : null;
1842
+ }
1843
+ function isCompleteInspectorNumberInput(value) {
1844
+ return /^[+-]?(?:\d+|\d*\.\d+)(?:[eE][+-]?\d+)?$/.test(value);
1845
+ }
1221
1846
  function formatInspectorEditableValue(value, property) {
1222
1847
  if (typeof value === 'number'
1223
1848
  && (property.control === 'number' || property.valueType === 'number' || property.control === 'vec2' || property.control === 'vec3')) {