@fps-games/editor 0.1.0 → 0.1.1-beta.0

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 (68) hide show
  1. package/dist/local-editor-harness.d.ts +70 -26
  2. package/dist/local-editor-harness.d.ts.map +1 -1
  3. package/dist/local-editor-harness.js +455 -21
  4. package/dist/local-editor-harness.js.map +1 -1
  5. package/node_modules/@fps-games/editor-babylon/package.json +4 -4
  6. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-context-menu.d.ts +15 -0
  7. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-context-menu.d.ts.map +1 -0
  8. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-context-menu.js +163 -0
  9. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-context-menu.js.map +1 -0
  10. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.d.ts +28 -0
  11. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.d.ts.map +1 -0
  12. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.js +164 -0
  13. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.js.map +1 -0
  14. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.d.ts +20 -0
  15. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.d.ts.map +1 -0
  16. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.js +483 -0
  17. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.js.map +1 -0
  18. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-tree.d.ts +56 -0
  19. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-tree.d.ts.map +1 -0
  20. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-tree.js +216 -0
  21. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-tree.js.map +1 -0
  22. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-view.d.ts +13 -0
  23. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-view.d.ts.map +1 -0
  24. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-view.js +138 -0
  25. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-view.js.map +1 -0
  26. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-input-router.d.ts +3 -0
  27. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-input-router.d.ts.map +1 -1
  28. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-input-router.js +29 -5
  29. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-input-router.js.map +1 -1
  30. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.d.ts +10 -2
  31. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.d.ts.map +1 -1
  32. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.js +554 -93
  33. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.js.map +1 -1
  34. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-primitives.d.ts +2 -0
  35. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-primitives.d.ts.map +1 -1
  36. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-primitives.js +7 -3
  37. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-primitives.js.map +1 -1
  38. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.d.ts.map +1 -1
  39. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.js +2 -0
  40. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.js.map +1 -1
  41. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts +148 -2
  42. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts.map +1 -1
  43. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.d.ts +11 -2
  44. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.d.ts.map +1 -1
  45. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js +206 -173
  46. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js.map +1 -1
  47. package/node_modules/@fps-games/editor-browser/package.json +1 -1
  48. package/node_modules/@fps-games/editor-core/dist/index.d.ts +2 -0
  49. package/node_modules/@fps-games/editor-core/dist/index.d.ts.map +1 -1
  50. package/node_modules/@fps-games/editor-core/dist/index.js +2 -0
  51. package/node_modules/@fps-games/editor-core/dist/index.js.map +1 -1
  52. package/node_modules/@fps-games/editor-core/dist/inspector.d.ts +138 -0
  53. package/node_modules/@fps-games/editor-core/dist/inspector.d.ts.map +1 -0
  54. package/node_modules/@fps-games/editor-core/dist/inspector.js +298 -0
  55. package/node_modules/@fps-games/editor-core/dist/inspector.js.map +1 -0
  56. package/node_modules/@fps-games/editor-core/dist/scene-graph.d.ts +28 -0
  57. package/node_modules/@fps-games/editor-core/dist/scene-graph.d.ts.map +1 -1
  58. package/node_modules/@fps-games/editor-core/dist/scene-graph.js +143 -2
  59. package/node_modules/@fps-games/editor-core/dist/scene-graph.js.map +1 -1
  60. package/node_modules/@fps-games/editor-core/dist/transform-math.d.ts +30 -0
  61. package/node_modules/@fps-games/editor-core/dist/transform-math.d.ts.map +1 -0
  62. package/node_modules/@fps-games/editor-core/dist/transform-math.js +339 -0
  63. package/node_modules/@fps-games/editor-core/dist/transform-math.js.map +1 -0
  64. package/node_modules/@fps-games/editor-core/package.json +2 -2
  65. package/node_modules/@fps-games/editor-forge-play/dist/index.js.map +1 -1
  66. package/node_modules/@fps-games/editor-forge-play/package.json +2 -2
  67. package/node_modules/@fps-games/editor-protocol/package.json +1 -1
  68. package/package.json +11 -6
@@ -258,127 +258,185 @@ function formatHistoryTimestamp(createdAt) {
258
258
  pad(date.getSeconds()),
259
259
  ].join('');
260
260
  }
261
- export function renderInspectorPanel(doc, panel, state) {
261
+ export function renderInspectorPanel(doc, panel, state, filter = '', options = {}) {
262
262
  clearElement(panel);
263
263
  const title = doc.createElement('h2');
264
264
  title.textContent = 'Inspector';
265
265
  title.style.cssText = 'font-size:13px;margin:-8px -8px 8px;padding:7px 8px;border-bottom:1px solid var(--fps-editor-divider);background:var(--fps-editor-chrome-dark);font-weight:800;color:var(--fps-editor-text-strong)';
266
266
  panel.appendChild(title);
267
+ const search = createInspectorSearchInput(doc, filter);
268
+ panel.appendChild(search);
267
269
  const selectionCount = state.selectionSummary?.count ?? state.selectedIds.length;
268
- if (selectionCount > 1) {
269
- appendMultiSelectionInspector(doc, panel, state);
270
+ const inspectorObject = selectionCount > 1
271
+ ? state.inspectorMultiObject
272
+ ?? (state.serializedMultiObject ? createLegacyInspectorObject(state.serializedMultiObject) : createSelectionSummaryInspectorObject(state))
273
+ : state.inspectorObject ?? (state.serializedObject ? createLegacyInspectorObject(state.serializedObject) : null);
274
+ if (!inspectorObject) {
275
+ const empty = doc.createElement('div');
276
+ empty.textContent = '请从层级树或 Scene View 中选择一个 GameObject。';
277
+ empty.style.cssText = 'color:var(--fps-editor-muted);line-height:1.45';
278
+ panel.appendChild(empty);
270
279
  return;
271
280
  }
272
- const serializedObject = state.serializedObject;
273
- if (!serializedObject) {
281
+ const controlRegistry = createLocalEditorBrowserInspectorControlRegistry(options.controls, options.controlConflict);
282
+ appendInspectorSummary(doc, panel, inspectorObject, selectionCount);
283
+ const visibleSections = filterInspectorSections(inspectorObject.sections.filter(section => section.placement !== 'summary'), filter);
284
+ if (visibleSections.length === 0) {
285
+ if (!filter.trim() && inspectorObject.sections.length === 0)
286
+ return;
274
287
  const empty = doc.createElement('div');
275
- empty.textContent = '请从层级树或 Scene View 中选择一个 GameObject。';
288
+ empty.textContent = '没有匹配的 Inspector 字段。';
276
289
  empty.style.cssText = 'color:var(--fps-editor-muted);line-height:1.45';
277
290
  panel.appendChild(empty);
278
291
  return;
279
292
  }
280
- appendGameObjectHeader(doc, panel, serializedObject);
281
- const sections = groupSerializedProperties(serializedObject.properties.filter(property => !property.path.startsWith('gameObject.')));
282
- for (const section of sections) {
283
- const block = createInspectorComponentBlock(doc);
284
- const sectionTitle = doc.createElement('h3');
285
- sectionTitle.textContent = section.title;
286
- sectionTitle.style.cssText = 'font-size:12px;margin:0 0 8px;font-weight:900;color:var(--fps-editor-text-strong)';
287
- block.appendChild(sectionTitle);
288
- for (const group of section.groups) {
289
- if (group.kind === 'vec3')
290
- appendSerializedVec3Inputs(doc, block, serializedObject, group.label, group.properties);
291
- else
292
- appendSerializedPropertyRow(doc, block, serializedObject, group.property);
293
- }
294
- panel.appendChild(block);
293
+ for (const section of visibleSections) {
294
+ panel.appendChild(createInspectorSectionBlock(doc, inspectorObject, section, controlRegistry));
295
295
  }
296
296
  }
297
- function appendMultiSelectionInspector(doc, panel, state) {
298
- const block = createInspectorComponentBlock(doc);
299
- const title = doc.createElement('h3');
300
- title.textContent = '多选';
301
- title.style.cssText = 'font-size:12px;margin:0 0 8px;font-weight:900;color:var(--fps-editor-text-strong)';
302
- block.appendChild(title);
303
- appendReadOnlyRow(doc, block, '已选', String(state.selectionSummary?.count ?? state.selectedIds.length));
304
- appendReadOnlyRow(doc, block, '活动对象', state.selectionSummary?.activeId ?? state.activeId ?? '无');
305
- panel.appendChild(block);
306
- const serializedMultiObject = state.serializedMultiObject;
307
- if (!serializedMultiObject)
308
- return;
309
- const sections = groupSerializedProperties(serializedMultiObject.properties.filter(property => property.path.startsWith('transform.')));
310
- for (const section of sections) {
311
- const sectionBlock = createInspectorComponentBlock(doc);
312
- const sectionTitle = doc.createElement('h3');
313
- sectionTitle.textContent = section.title;
314
- sectionTitle.style.cssText = 'font-size:12px;margin:0 0 8px;font-weight:900;color:var(--fps-editor-text-strong)';
315
- sectionBlock.appendChild(sectionTitle);
316
- for (const group of section.groups) {
317
- if (group.kind === 'vec3')
318
- appendSerializedVec3Inputs(doc, sectionBlock, serializedMultiObject, group.label, group.properties);
319
- else
320
- appendSerializedPropertyRow(doc, sectionBlock, serializedMultiObject, group.property);
321
- }
322
- panel.appendChild(sectionBlock);
323
- }
297
+ function createInspectorSearchInput(doc, value) {
298
+ const input = doc.createElement('input');
299
+ input.dataset.editorInspectorSearch = 'true';
300
+ input.value = value;
301
+ input.placeholder = 'Search...';
302
+ input.style.cssText = [
303
+ 'width:100%',
304
+ 'height:30px',
305
+ 'box-sizing:border-box',
306
+ 'margin:0 0 8px',
307
+ 'border:1px solid var(--fps-editor-border)',
308
+ 'border-radius:3px',
309
+ 'background:var(--fps-editor-field)',
310
+ 'color:var(--fps-editor-text)',
311
+ 'font-size:12px',
312
+ 'padding:0 8px',
313
+ 'outline:none',
314
+ ].join(';');
315
+ return input;
324
316
  }
325
- function appendGameObjectHeader(doc, panel, serializedObject) {
317
+ function appendInspectorSummary(doc, panel, inspectorObject, selectionCount) {
326
318
  const block = createInspectorComponentBlock(doc);
327
319
  const title = doc.createElement('h3');
328
- title.textContent = 'GameObject';
320
+ title.textContent = selectionCount > 1 ? 'Selection' : 'GameObject';
329
321
  title.style.cssText = 'font-size:12px;margin:0 0 8px;font-weight:900;color:var(--fps-editor-text-strong)';
330
322
  block.appendChild(title);
331
- const nameProperty = serializedObject.properties.find(property => property.path === 'gameObject.name');
332
- const idProperty = serializedObject.properties.find(property => property.path === 'gameObject.id');
333
- appendReadOnlyRow(doc, block, '名称', String(nameProperty?.value ?? serializedObject.label ?? serializedObject.targetId));
334
- appendReadOnlyRow(doc, block, 'ID', String(idProperty?.value ?? serializedObject.targetId));
323
+ appendReadOnlyRow(doc, block, selectionCount > 1 ? 'Selected' : 'Name', inspectorObject.label ?? inspectorObject.activeId ?? inspectorObject.targetIds[0] ?? '');
324
+ appendReadOnlyRow(doc, block, 'Active ID', inspectorObject.activeId ?? '');
325
+ if (selectionCount > 1)
326
+ appendReadOnlyRow(doc, block, 'Count', String(selectionCount));
335
327
  panel.appendChild(block);
336
328
  }
329
+ function filterInspectorSections(sections, filter) {
330
+ const needle = filter.trim().toLowerCase();
331
+ if (!needle)
332
+ return sections;
333
+ return sections
334
+ .map(section => ({
335
+ ...section,
336
+ properties: section.properties.filter(property => inspectorPropertyMatches(section, property, needle)),
337
+ }))
338
+ .filter(section => section.properties.length > 0 || inspectorSectionMatches(section, needle));
339
+ }
340
+ function inspectorSectionMatches(section, needle) {
341
+ return [section.id, section.title, section.summary ?? '', ...(section.tags ?? [])]
342
+ .some(value => value.toLowerCase().includes(needle));
343
+ }
344
+ function inspectorPropertyMatches(section, property, needle) {
345
+ return inspectorSectionMatches(section, needle)
346
+ || [property.path, property.label, property.control, property.valueType, ...(property.tags ?? [])]
347
+ .some(value => String(value).toLowerCase().includes(needle));
348
+ }
349
+ function createInspectorSectionBlock(doc, inspectorObject, section, controlRegistry) {
350
+ const block = doc.createElement('details');
351
+ block.dataset.editorInspectorSection = section.id;
352
+ block.open = section.collapsedByDefault !== true;
353
+ block.style.cssText = createInspectorBlockStyle();
354
+ const sectionTitle = doc.createElement('summary');
355
+ sectionTitle.style.cssText = [
356
+ 'font-size:12px',
357
+ 'margin:-2px 0 8px',
358
+ 'font-weight:900',
359
+ 'color:var(--fps-editor-text-strong)',
360
+ 'display:flex',
361
+ 'align-items:center',
362
+ 'justify-content:space-between',
363
+ 'gap:8px',
364
+ 'cursor:pointer',
365
+ 'list-style:none',
366
+ ].join(';');
367
+ const label = doc.createElement('span');
368
+ label.textContent = section.title;
369
+ sectionTitle.appendChild(label);
370
+ const meta = doc.createElement('span');
371
+ meta.style.cssText = 'display:flex;align-items:center;gap:6px;min-width:0;color:var(--fps-editor-muted);font-size:10px;text-transform:uppercase;letter-spacing:0';
372
+ if (section.summary) {
373
+ const summary = doc.createElement('span');
374
+ summary.textContent = section.summary;
375
+ summary.style.cssText = 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-transform:none;font-size:11px';
376
+ meta.appendChild(summary);
377
+ }
378
+ if (section.persistence === 'runtime' || section.runtimeOnly || section.persistence === 'readonly') {
379
+ const badge = doc.createElement('span');
380
+ badge.textContent = section.persistence === 'runtime' || section.runtimeOnly ? 'Runtime' : 'Readonly';
381
+ badge.style.cssText = 'flex:0 0 auto';
382
+ meta.appendChild(badge);
383
+ }
384
+ sectionTitle.appendChild(meta);
385
+ block.appendChild(sectionTitle);
386
+ const content = doc.createElement('div');
387
+ content.dataset.editorInspectorSectionContent = section.id;
388
+ for (const group of groupInspectorProperties(section.properties)) {
389
+ if (group.kind === 'vector')
390
+ appendInspectorVectorInputs(doc, content, inspectorObject, group.label, group.properties);
391
+ else
392
+ appendInspectorPropertyRow(doc, content, inspectorObject, group.property, controlRegistry);
393
+ }
394
+ block.appendChild(content);
395
+ return block;
396
+ }
337
397
  function createInspectorComponentBlock(doc) {
338
398
  const block = doc.createElement('div');
339
- block.style.cssText = [
399
+ block.style.cssText = createInspectorBlockStyle();
400
+ return block;
401
+ }
402
+ function createInspectorBlockStyle() {
403
+ return [
340
404
  'margin:0 0 8px',
341
405
  'padding:9px',
342
406
  'border:1px solid var(--fps-editor-border)',
343
407
  'border-radius:3px',
344
408
  'background:var(--fps-editor-panel-soft)',
345
409
  ].join(';');
346
- return block;
347
410
  }
348
- function groupSerializedProperties(properties) {
349
- const bySection = new Map();
350
- for (const property of properties) {
351
- const sectionName = toTitle(property.path.split('.')[0] ?? 'Properties');
352
- const entries = bySection.get(sectionName) ?? [];
353
- entries.push(property);
354
- bySection.set(sectionName, entries);
355
- }
356
- return [...bySection.entries()].map(([title, entries]) => ({
357
- title,
358
- groups: groupVectorProperties(entries),
359
- }));
360
- }
361
- function groupVectorProperties(properties) {
411
+ function groupInspectorProperties(properties) {
362
412
  const groups = [];
363
413
  const consumed = new Set();
364
414
  for (const property of properties) {
365
415
  if (consumed.has(property.path))
366
416
  continue;
367
- const match = property.path.match(/^(.*)\.(x|y|z)$/);
417
+ if (property.control === 'vec2' || property.control === 'vec3') {
418
+ groups.push({ kind: 'property', property });
419
+ consumed.add(property.path);
420
+ continue;
421
+ }
422
+ const match = property.path.match(/^(.*)\.(x|y|z|r|g|b)$/);
368
423
  if (!match || property.valueType !== 'number') {
369
424
  groups.push({ kind: 'property', property });
370
425
  consumed.add(property.path);
371
426
  continue;
372
427
  }
373
428
  const basePath = match[1];
374
- const vector = ['x', 'y', 'z']
429
+ const axes = ['x', 'y', 'z'].every(axis => properties.some(candidate => candidate.path === `${basePath}.${axis}`))
430
+ ? ['x', 'y', 'z']
431
+ : ['r', 'g', 'b'];
432
+ const vector = axes
375
433
  .map(axis => properties.find(candidate => candidate.path === `${basePath}.${axis}` && candidate.valueType === 'number'))
376
434
  .filter((candidate) => !!candidate);
377
435
  if (vector.length === 3) {
378
436
  for (const entry of vector)
379
437
  consumed.add(entry.path);
380
438
  const parts = basePath.split('.');
381
- groups.push({ kind: 'vec3', label: toTitle(parts[parts.length - 1] ?? basePath), properties: vector });
439
+ groups.push({ kind: 'vector', label: toTitle(parts[parts.length - 1] ?? basePath), properties: vector });
382
440
  }
383
441
  else {
384
442
  groups.push({ kind: 'property', property });
@@ -387,15 +445,135 @@ function groupVectorProperties(properties) {
387
445
  }
388
446
  return groups;
389
447
  }
390
- function appendSerializedPropertyRow(doc, parent, serializedObject, property) {
391
- if (property.readOnly || property.valueType !== 'number') {
392
- appendReadOnlyRow(doc, parent, property.label, String(property.value ?? ''));
393
- return;
448
+ const builtinInspectorControlRegistrations = [
449
+ {
450
+ id: 'builtin.readonly',
451
+ order: 100,
452
+ control: 'readonly',
453
+ render: ({ doc, target, property }) => createInspectorReadonlyControl(doc, target, property),
454
+ },
455
+ {
456
+ id: 'builtin.string',
457
+ order: 100,
458
+ control: 'string',
459
+ render: ({ doc, target, property }) => createInspectorTextControl(doc, target, property),
460
+ },
461
+ {
462
+ id: 'builtin.number',
463
+ order: 100,
464
+ control: 'number',
465
+ render: ({ doc, target, property }) => createInspectorNumberControl(doc, target, property),
466
+ },
467
+ {
468
+ id: 'builtin.boolean',
469
+ order: 100,
470
+ control: 'boolean',
471
+ render: ({ doc, target, property }) => createInspectorBooleanControl(doc, target, property),
472
+ },
473
+ {
474
+ id: 'builtin.enum',
475
+ order: 100,
476
+ control: 'enum',
477
+ render: ({ doc, target, property }) => createInspectorEnumControl(doc, target, property),
478
+ },
479
+ {
480
+ id: 'builtin.vec2',
481
+ order: 100,
482
+ control: 'vec2',
483
+ render: ({ doc, target, property }) => createInspectorVectorControl(doc, target, property),
484
+ },
485
+ {
486
+ id: 'builtin.vec3',
487
+ order: 100,
488
+ control: 'vec3',
489
+ render: ({ doc, target, property }) => createInspectorVectorControl(doc, target, property),
490
+ },
491
+ {
492
+ id: 'builtin.color',
493
+ order: 100,
494
+ control: 'color',
495
+ render: ({ doc, target, property }) => createInspectorColorControl(doc, target, property),
496
+ },
497
+ {
498
+ id: 'builtin.asset',
499
+ order: 100,
500
+ control: 'asset',
501
+ render: ({ doc, target, property }) => createInspectorTextControl(doc, target, property),
502
+ },
503
+ {
504
+ id: 'builtin.object',
505
+ order: 100,
506
+ control: 'object',
507
+ render: ({ doc, target, property }) => createInspectorReadonlyControl(doc, target, property),
508
+ },
509
+ {
510
+ id: 'builtin.custom',
511
+ order: 1000,
512
+ control: 'custom',
513
+ render: ({ doc, target, property }) => createInspectorReadonlyControl(doc, target, property),
514
+ },
515
+ ];
516
+ function appendInspectorPropertyRow(doc, parent, inspectorObject, property, controlRegistry) {
517
+ const control = renderInspectorControl(doc, controlRegistry, inspectorObject, property);
518
+ parent.appendChild(createPropertyRow(doc, property.label, control));
519
+ }
520
+ export function createLocalEditorBrowserInspectorControlRegistry(controls = [], conflict = 'error') {
521
+ const registrations = new Map();
522
+ for (const control of builtinInspectorControlRegistrations) {
523
+ registerInspectorControl(registrations, control, 'error');
394
524
  }
395
- const input = createPropertyInput(doc, serializedObject, property);
396
- parent.appendChild(createPropertyRow(doc, property.label, input));
525
+ for (const control of controls) {
526
+ registerInspectorControl(registrations, control, conflict);
527
+ }
528
+ return [...registrations.values()].sort(compareInspectorControlRegistrations);
397
529
  }
398
- function appendSerializedVec3Inputs(doc, parent, serializedObject, label, properties) {
530
+ function registerInspectorControl(registrations, registration, conflict) {
531
+ const id = registration.id.trim();
532
+ if (!id)
533
+ throw new Error('Inspector control id is required.');
534
+ const normalized = id === registration.id ? registration : { ...registration, id };
535
+ if (registrations.has(id)) {
536
+ if (conflict === 'error')
537
+ throw new Error(`Inspector control "${id}" is already registered.`);
538
+ if (conflict === 'ignore')
539
+ return;
540
+ }
541
+ registrations.set(id, normalized);
542
+ }
543
+ function compareInspectorControlRegistrations(left, right) {
544
+ return (left.order ?? 0) - (right.order ?? 0) || left.id.localeCompare(right.id);
545
+ }
546
+ function renderInspectorControl(doc, registrations, target, property) {
547
+ if (property.readOnly || property.persistence !== 'document')
548
+ return createInspectorReadonlyControl(doc, target, property);
549
+ const context = {
550
+ doc,
551
+ target,
552
+ property,
553
+ bindInput(element, options) {
554
+ applyLocalEditorBrowserInspectorControlBinding(element, target, property, options);
555
+ },
556
+ };
557
+ const registration = resolveLocalEditorBrowserInspectorControlRegistration(registrations, context);
558
+ if (registration)
559
+ return registration.render(context);
560
+ return createInspectorReadonlyControl(doc, target, property);
561
+ }
562
+ export function resolveLocalEditorBrowserInspectorControlRegistration(registrations, context) {
563
+ for (const registration of registrations) {
564
+ if (localEditorBrowserInspectorControlSupports(registration, context))
565
+ return registration;
566
+ }
567
+ return null;
568
+ }
569
+ function localEditorBrowserInspectorControlSupports(registration, context) {
570
+ if (registration.control && registration.control !== context.property.control)
571
+ return false;
572
+ if (registration.customControl && registration.customControl !== context.property.customControl)
573
+ return false;
574
+ return registration.supports?.(context) ?? true;
575
+ }
576
+ function appendInspectorVectorInputs(doc, parent, inspectorObject, label, properties) {
399
577
  const wrapper = doc.createElement('div');
400
578
  wrapper.style.cssText = 'margin:8px 0';
401
579
  const labelElement = doc.createElement('div');
@@ -405,25 +583,156 @@ function appendSerializedVec3Inputs(doc, parent, serializedObject, label, proper
405
583
  const fields = doc.createElement('div');
406
584
  fields.style.cssText = 'display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:6px';
407
585
  for (const property of properties) {
408
- fields.appendChild(createPropertyInput(doc, serializedObject, property));
586
+ fields.appendChild(createInspectorNumberControl(doc, inspectorObject, property));
409
587
  }
410
588
  wrapper.appendChild(fields);
411
589
  parent.appendChild(wrapper);
412
590
  }
413
- function createPropertyInput(doc, target, property) {
591
+ function createInspectorInputBase(doc, target, property) {
414
592
  const input = doc.createElement('input');
415
- input.type = property.valueType === 'number' ? 'number' : 'text';
416
- if (property.valueType === 'number')
417
- input.step = '0.1';
418
593
  input.value = property.mixed ? '' : String(property.value);
419
594
  input.placeholder = property.mixed ? '--' : '';
420
- input.dataset.serializedTargetId = 'targetId' in target ? target.targetId : target.activeId ?? target.targetIds[0] ?? '';
421
- if ('targetIds' in target)
422
- input.dataset.serializedTargetIds = target.targetIds.join(',');
423
- input.dataset.serializedPath = property.path;
424
- input.title = property.label;
425
- input.disabled = property.readOnly === true;
426
- input.style.cssText = [
595
+ applyLocalEditorBrowserInspectorControlBinding(input, target, property);
596
+ input.style.cssText = createInspectorInputStyle();
597
+ return input;
598
+ }
599
+ function createInspectorNumberControl(doc, target, property) {
600
+ const input = createInspectorInputBase(doc, target, property);
601
+ input.type = 'number';
602
+ input.step = String(property.step ?? 0.1);
603
+ if (property.min != null)
604
+ input.min = String(property.min);
605
+ if (property.max != null)
606
+ input.max = String(property.max);
607
+ return input;
608
+ }
609
+ function createInspectorTextControl(doc, target, property) {
610
+ const input = createInspectorInputBase(doc, target, property);
611
+ input.type = 'text';
612
+ return input;
613
+ }
614
+ function createInspectorBooleanControl(doc, target, property) {
615
+ const input = createInspectorInputBase(doc, target, property);
616
+ input.type = 'checkbox';
617
+ input.checked = property.mixed ? false : property.value === true;
618
+ input.style.cssText = 'width:16px;height:16px;accent-color:var(--fps-editor-accent);justify-self:start';
619
+ return input;
620
+ }
621
+ function createInspectorEnumControl(doc, target, property) {
622
+ const select = doc.createElement('select');
623
+ applyLocalEditorBrowserInspectorControlBinding(select, target, property);
624
+ select.style.cssText = createInspectorInputStyle();
625
+ for (const option of property.options ?? []) {
626
+ const item = doc.createElement('option');
627
+ item.value = String(option.value);
628
+ item.dataset.serializedOptionValue = JSON.stringify(option.value);
629
+ item.textContent = option.label;
630
+ item.disabled = option.disabled === true;
631
+ if (!property.mixed && option.value === property.value)
632
+ item.selected = true;
633
+ select.appendChild(item);
634
+ }
635
+ if (property.mixed) {
636
+ const mixed = doc.createElement('option');
637
+ mixed.value = '';
638
+ mixed.textContent = '--';
639
+ mixed.selected = true;
640
+ select.prepend(mixed);
641
+ }
642
+ return select;
643
+ }
644
+ function createInspectorVectorControl(doc, target, property) {
645
+ const wrapper = doc.createElement('div');
646
+ wrapper.dataset.inspectorVectorControl = 'true';
647
+ wrapper.style.cssText = 'display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:6px;width:100%';
648
+ const axes = property.control === 'vec2' ? ['x', 'y'] : ['x', 'y', 'z'];
649
+ if (property.control === 'vec2')
650
+ wrapper.style.gridTemplateColumns = 'repeat(2,minmax(0,1fr))';
651
+ const value = isRecord(property.value) ? property.value : {};
652
+ for (const axis of axes) {
653
+ const input = createInspectorInputBase(doc, target, property);
654
+ input.type = 'number';
655
+ input.step = String(property.step ?? 0.1);
656
+ input.dataset.serializedVectorAxis = axis;
657
+ input.value = property.mixed ? '' : String(value[axis] ?? 0);
658
+ input.title = `${property.label}.${axis}`;
659
+ wrapper.appendChild(input);
660
+ }
661
+ return wrapper;
662
+ }
663
+ function createInspectorColorControl(doc, target, property) {
664
+ const input = createInspectorInputBase(doc, target, property);
665
+ input.type = 'color';
666
+ input.value = colorValueToHex(property.value);
667
+ input.style.cssText = 'width:34px;height:26px;border:1px solid var(--fps-editor-border);border-radius:3px;background:transparent;padding:0';
668
+ return input;
669
+ }
670
+ function createInspectorReadonlyControl(doc, _target, property) {
671
+ if (!property.mixed && isExpandableInspectorValue(property.value)) {
672
+ return createInspectorObjectReadonlyControl(doc, property);
673
+ }
674
+ const valueElement = doc.createElement('div');
675
+ valueElement.textContent = property.mixed ? '--' : formatLocalEditorBrowserInspectorValue(property.value);
676
+ valueElement.style.cssText = [
677
+ `color:${property.persistence === 'runtime' ? 'var(--fps-editor-muted)' : 'var(--fps-editor-text)'}`,
678
+ 'overflow:hidden',
679
+ 'text-overflow:ellipsis',
680
+ 'white-space:nowrap',
681
+ property.persistence === 'runtime' ? 'font-style:italic' : '',
682
+ ].filter(Boolean).join(';');
683
+ if (property.persistence === 'runtime')
684
+ valueElement.title = 'Runtime-only context';
685
+ return valueElement;
686
+ }
687
+ function createInspectorObjectReadonlyControl(doc, property) {
688
+ const details = doc.createElement('details');
689
+ details.style.cssText = [
690
+ 'min-width:0',
691
+ 'max-width:100%',
692
+ `color:${property.persistence === 'runtime' ? 'var(--fps-editor-muted)' : 'var(--fps-editor-text)'}`,
693
+ ].join(';');
694
+ const summary = doc.createElement('summary');
695
+ summary.textContent = formatInspectorObjectSummary(property.value);
696
+ summary.style.cssText = 'cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap';
697
+ details.appendChild(summary);
698
+ const pre = doc.createElement('pre');
699
+ pre.textContent = formatLocalEditorBrowserInspectorValue(property.value);
700
+ pre.style.cssText = [
701
+ 'max-height:220px',
702
+ 'overflow:auto',
703
+ 'white-space:pre-wrap',
704
+ 'word-break:break-word',
705
+ 'margin:6px 0 0',
706
+ 'padding:7px',
707
+ 'border:1px solid var(--fps-editor-border)',
708
+ 'border-radius:3px',
709
+ 'background:var(--fps-editor-field)',
710
+ 'font-size:11px',
711
+ 'line-height:1.45',
712
+ ].join(';');
713
+ details.appendChild(pre);
714
+ if (property.persistence === 'runtime')
715
+ details.title = 'Runtime-only context';
716
+ return details;
717
+ }
718
+ export function applyLocalEditorBrowserInspectorControlBinding(element, target, property, options) {
719
+ element.dataset.serializedTargetId = target.activeId ?? target.targetIds[0] ?? '';
720
+ if (target.targetIds.length > 1)
721
+ element.dataset.serializedTargetIds = target.targetIds.join(',');
722
+ element.dataset.serializedPath = property.path;
723
+ element.dataset.serializedControl = property.control;
724
+ element.dataset.serializedValueType = property.valueType;
725
+ element.dataset.serializedCommitMode = property.commitMode;
726
+ element.dataset.serializedPersistence = property.persistence;
727
+ if (options?.source)
728
+ element.dataset.serializedEditSource = options.source;
729
+ element.title = property.tooltip ?? property.label;
730
+ if ('disabled' in element) {
731
+ element.disabled = property.readOnly === true || property.persistence !== 'document';
732
+ }
733
+ }
734
+ function createInspectorInputStyle() {
735
+ return [
427
736
  'min-width:0',
428
737
  'height:28px',
429
738
  'border:1px solid var(--fps-editor-border)',
@@ -433,7 +742,6 @@ function createPropertyInput(doc, target, property) {
433
742
  'font-size:12px',
434
743
  'padding:0 6px',
435
744
  ].join(';');
436
- return input;
437
745
  }
438
746
  function appendReadOnlyRow(doc, parent, label, value) {
439
747
  const valueElement = doc.createElement('div');
@@ -441,4 +749,157 @@ function appendReadOnlyRow(doc, parent, label, value) {
441
749
  valueElement.style.cssText = 'color:var(--fps-editor-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap';
442
750
  parent.appendChild(createPropertyRow(doc, label, valueElement));
443
751
  }
752
+ function createLegacyInspectorObject(serializedObject) {
753
+ const targetIds = 'targetIds' in serializedObject ? serializedObject.targetIds : [serializedObject.targetId];
754
+ const activeId = 'targetId' in serializedObject ? serializedObject.targetId : serializedObject.activeId;
755
+ return {
756
+ targetIds,
757
+ activeId,
758
+ label: serializedObject.label,
759
+ document: serializedObject.document,
760
+ selection: {
761
+ targetIds,
762
+ activeId,
763
+ document: serializedObject.document,
764
+ },
765
+ sections: createLegacyInspectorSections(serializedObject.properties),
766
+ };
767
+ }
768
+ function createLegacyInspectorSections(properties) {
769
+ const sections = new Map();
770
+ for (const [index, property] of properties.entries()) {
771
+ const id = property.path.split('.')[0] || 'properties';
772
+ const section = sections.get(id) ?? { order: sections.size, properties: [] };
773
+ section.properties.push({
774
+ path: property.path,
775
+ label: property.label,
776
+ valueType: property.valueType,
777
+ control: inferLegacyInspectorControl(property),
778
+ value: property.value,
779
+ mixed: property.mixed,
780
+ readOnly: property.readOnly === true,
781
+ persistence: property.readOnly === true ? 'readonly' : 'document',
782
+ commitMode: inferLegacyInspectorCommitMode(property),
783
+ order: index,
784
+ document: property.document,
785
+ });
786
+ sections.set(id, section);
787
+ }
788
+ return [...sections.entries()].map(([id, section]) => ({
789
+ id,
790
+ title: toTitle(id),
791
+ order: section.order,
792
+ placement: id === 'gameObject' ? 'summary' : 'body',
793
+ persistence: section.properties.some(property => property.persistence === 'document') ? 'document' : 'readonly',
794
+ properties: section.properties,
795
+ }));
796
+ }
797
+ function createSelectionSummaryInspectorObject(state) {
798
+ const targetIds = state.selectedIds;
799
+ if (targetIds.length === 0)
800
+ return null;
801
+ const activeId = state.selectionSummary?.activeId ?? state.activeId ?? targetIds[0] ?? null;
802
+ return {
803
+ targetIds,
804
+ activeId,
805
+ label: `${targetIds.length} objects`,
806
+ selection: {
807
+ targetIds,
808
+ activeId,
809
+ },
810
+ sections: [],
811
+ };
812
+ }
813
+ function inferLegacyInspectorControl(property) {
814
+ if (property.readOnly)
815
+ return 'readonly';
816
+ if (property.valueType === 'string')
817
+ return 'string';
818
+ if (property.valueType === 'number')
819
+ return 'number';
820
+ if (property.valueType === 'boolean')
821
+ return 'boolean';
822
+ if (property.valueType === 'enum')
823
+ return 'enum';
824
+ if (property.valueType === 'asset')
825
+ return 'asset';
826
+ if (property.valueType === 'object')
827
+ return 'object';
828
+ return 'readonly';
829
+ }
830
+ function inferLegacyInspectorCommitMode(property) {
831
+ switch (property.valueType) {
832
+ case 'number':
833
+ return 'live';
834
+ case 'boolean':
835
+ case 'enum':
836
+ case 'asset':
837
+ return 'immediate';
838
+ default:
839
+ return 'blur';
840
+ }
841
+ }
842
+ export function formatLocalEditorBrowserInspectorValue(value) {
843
+ if (value == null)
844
+ return '';
845
+ if (typeof value === 'object')
846
+ return JSON.stringify(toStableInspectorJsonValue(value), null, 2);
847
+ if (typeof value === 'function')
848
+ return `[Function ${value.name || 'anonymous'}]`;
849
+ if (typeof value === 'symbol')
850
+ return String(value);
851
+ return String(value);
852
+ }
853
+ function isRecord(value) {
854
+ return !!value && typeof value === 'object' && !Array.isArray(value);
855
+ }
856
+ function isExpandableInspectorValue(value) {
857
+ return !!value && typeof value === 'object';
858
+ }
859
+ function formatInspectorObjectSummary(value) {
860
+ if (Array.isArray(value))
861
+ return `Array(${value.length})`;
862
+ if (!isRecord(value))
863
+ return 'Object';
864
+ const keys = Object.keys(value).sort();
865
+ if (keys.length === 0)
866
+ return 'Object {}';
867
+ const shown = keys.slice(0, 4).join(', ');
868
+ return keys.length > 4 ? `Object { ${shown}, ... }` : `Object { ${shown} }`;
869
+ }
870
+ function toStableInspectorJsonValue(value, seen = new WeakSet(), depth = 0) {
871
+ if (value == null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean')
872
+ return value;
873
+ if (typeof value === 'bigint')
874
+ return String(value);
875
+ if (typeof value === 'function')
876
+ return `[Function ${value.name || 'anonymous'}]`;
877
+ if (typeof value === 'symbol')
878
+ return String(value);
879
+ if (typeof value !== 'object')
880
+ return String(value);
881
+ if (seen.has(value))
882
+ return '[Circular]';
883
+ if (depth > 6)
884
+ return '[MaxDepth]';
885
+ seen.add(value);
886
+ if (Array.isArray(value))
887
+ return value.map(item => toStableInspectorJsonValue(item, seen, depth + 1));
888
+ const record = value;
889
+ const output = {};
890
+ for (const key of Object.keys(record).sort()) {
891
+ output[key] = toStableInspectorJsonValue(record[key], seen, depth + 1);
892
+ }
893
+ return output;
894
+ }
895
+ function colorValueToHex(value) {
896
+ if (!value || typeof value !== 'object')
897
+ return '#ffffff';
898
+ const color = value;
899
+ const toHex = (channel) => {
900
+ const value = typeof channel === 'number' && Number.isFinite(channel) ? channel : 1;
901
+ return Math.round(Math.max(0, Math.min(1, value)) * 255).toString(16).padStart(2, '0');
902
+ };
903
+ return `#${toHex(color.r)}${toHex(color.g)}${toHex(color.b)}`;
904
+ }
444
905
  //# sourceMappingURL=local-editor-ui-panels.js.map