@fps-games/editor 0.1.0 → 0.1.1-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 (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
@@ -1,4 +1,4 @@
1
- import { createEditorSession, validateSceneGraphDelete, validateSceneGraphDrop, validateSceneGraphRename, } from '@fps-games/editor-core';
1
+ import { createEditorSession, createInspectorRegistry, createInspectorEditPayload, mergeInspectorSections, serializedMultiObjectToInspectorObject, serializedObjectToInspectorObject, validateSceneGraphDelete, validateSceneGraphDrop, validateSceneGraphGroupSelection, validateSceneGraphMove, validateSceneGraphRename, } from '@fps-games/editor-core';
2
2
  import { createLocalEditorBrowserUi, } from '@fps-games/editor-browser';
3
3
  import { createBabylonEditorProjection, createBabylonEditorWorld, createBabylonProjectionSelectionController, createBabylonSceneViewCameraController, createBabylonSceneViewInputController, createBabylonTransformGizmoController, focusEditorViewportSelection, } from '@fps-games/editor-babylon';
4
4
  export function createLocalEditorHarness(options) {
@@ -24,11 +24,15 @@ export function createLocalEditorHarness(options) {
24
24
  transformConstraint: 'axis',
25
25
  resizeHandler: null,
26
26
  status: 'Game running',
27
+ statusTone: 'default',
28
+ statusToneStatus: 'Game running',
29
+ statusDetails: '',
27
30
  summary: '',
28
31
  };
29
32
  let harness;
30
33
  const ui = createLocalEditorBrowserUi({
31
34
  root,
35
+ inspector: options.inspector,
32
36
  callbacks: {
33
37
  onEnterEditor: () => {
34
38
  void runExclusive(state, harness.render, () => harness.enterEditor());
@@ -74,12 +78,26 @@ export function createLocalEditorHarness(options) {
74
78
  if (dropSceneGraphNode(state, options, intent))
75
79
  harness.render();
76
80
  },
81
+ onSceneGraphMove: (intent) => {
82
+ if (moveSceneGraphNodes(state, options, intent))
83
+ harness.render();
84
+ },
85
+ onSceneGraphGroupSelection: (intent) => {
86
+ if (groupSceneGraphSelection(state, options, intent))
87
+ harness.render();
88
+ },
89
+ onContextAction: (action) => {
90
+ if (handleContextAction(state, options, action))
91
+ harness.render();
92
+ },
77
93
  onAssetFilterChange: (value) => {
78
94
  state.assetFilter = value;
79
95
  harness.render();
80
96
  },
81
97
  onPropertyInput: (input) => {
82
- if (patchSerializedProperty(state, options, input))
98
+ const previousStatus = state.status;
99
+ const patched = patchSerializedProperty(state, options, input);
100
+ if (patched || state.status !== previousStatus)
83
101
  harness.render();
84
102
  },
85
103
  onTransformToolChange: (tool) => {
@@ -154,7 +172,11 @@ export function createLocalEditorHarness(options) {
154
172
  expectedRevision: source.ref.revision,
155
173
  });
156
174
  if (!hostResult.ok || !hostResult.document) {
157
- state.status = summarizeAuthoringFailure(hostResult);
175
+ const failureStatus = summarizeLocalEditorAuthoringFailure(hostResult);
176
+ state.status = failureStatus.status;
177
+ state.statusTone = 'error';
178
+ state.statusToneStatus = state.status;
179
+ state.statusDetails = failureStatus.details;
158
180
  state.summary = summarizeDocument(options, document, source);
159
181
  return false;
160
182
  }
@@ -188,6 +210,9 @@ export function createLocalEditorHarness(options) {
188
210
  }
189
211
  state.summary = result.summary ?? summarizeDocument(options, preparedDocument, savedSource ?? null);
190
212
  state.status = 'Scene saved';
213
+ state.statusTone = 'success';
214
+ state.statusToneStatus = state.status;
215
+ state.statusDetails = '';
191
216
  return true;
192
217
  },
193
218
  async saveAndRunGame() {
@@ -203,6 +228,9 @@ export function createLocalEditorHarness(options) {
203
228
  state.session = null;
204
229
  state.source = null;
205
230
  state.status = 'Reloading game';
231
+ state.statusTone = 'default';
232
+ state.statusToneStatus = state.status;
233
+ state.statusDetails = '';
206
234
  await options.persistenceAdapter.runGame();
207
235
  },
208
236
  dispose() {
@@ -214,6 +242,32 @@ export function createLocalEditorHarness(options) {
214
242
  harness.render();
215
243
  return harness;
216
244
  }
245
+ export function mergeLocalEditorHarnessInspectorComponentSections(input) {
246
+ const components = input.components;
247
+ if (!components)
248
+ return input.inspectorObject;
249
+ const context = {
250
+ ...input.inspectorObject.selection,
251
+ ...input.context,
252
+ targetIds: input.context?.targetIds ?? input.inspectorObject.targetIds,
253
+ activeId: input.context?.activeId ?? input.inspectorObject.activeId,
254
+ document: input.context?.document ?? input.inspectorObject.document ?? input.inspectorObject.selection.document,
255
+ };
256
+ const componentSections = getInspectorComponentSections({
257
+ components,
258
+ context,
259
+ componentConflict: input.componentConflict,
260
+ propertyConflict: input.propertyConflict,
261
+ });
262
+ if (componentSections.length === 0)
263
+ return input.inspectorObject;
264
+ return {
265
+ ...input.inspectorObject,
266
+ sections: mergeInspectorSections(input.inspectorObject.sections, componentSections, {
267
+ propertyConflict: input.propertyConflict,
268
+ }),
269
+ };
270
+ }
217
271
  async function createEditorWorld(state, options, render) {
218
272
  disposeEditorWorld(state);
219
273
  const canvas = options.worldAdapter.getCanvas();
@@ -406,6 +460,9 @@ async function runExclusive(state, render, action) {
406
460
  }
407
461
  catch (error) {
408
462
  state.status = error instanceof Error ? error.message : String(error);
463
+ state.statusTone = 'error';
464
+ state.statusToneStatus = state.status;
465
+ state.statusDetails = state.status;
409
466
  console.error('[LocalEditorHarness] action failed', error);
410
467
  }
411
468
  finally {
@@ -440,6 +497,38 @@ function selectItem(state, options, input) {
440
497
  };
441
498
  return dispatchSelectionCommand(state, options, command);
442
499
  }
500
+ function handleContextAction(state, options, action) {
501
+ if (action.region !== 'hierarchy')
502
+ return false;
503
+ if (action.action === 'focus') {
504
+ const activeId = action.activeId ?? action.targetIds[action.targetIds.length - 1] ?? null;
505
+ const selectionChanged = activeId && !state.session?.getState().selection.selectedIds.includes(activeId)
506
+ ? dispatchSelectionCommand(state, options, {
507
+ type: 'selection.replace',
508
+ selectedIds: [activeId],
509
+ activeId,
510
+ label: 'Select Context Target',
511
+ })
512
+ : false;
513
+ return focusSelectedProjection(state) || selectionChanged;
514
+ }
515
+ if (action.action === 'rename')
516
+ return false;
517
+ if (action.action === 'create-group') {
518
+ return createSceneGraphGroup(state, options, {
519
+ parentId: action.parentId ?? null,
520
+ activeId: action.activeId ?? null,
521
+ name: 'Group',
522
+ });
523
+ }
524
+ if (action.action === 'delete') {
525
+ return deleteSceneGraphNodes(state, options, {
526
+ ids: action.targetIds,
527
+ activeId: action.activeId ?? null,
528
+ });
529
+ }
530
+ return false;
531
+ }
443
532
  function dispatchSelectionCommand(state, options, command) {
444
533
  if (state.mode !== 'editor')
445
534
  return false;
@@ -601,6 +690,99 @@ function dropSceneGraphNode(state, options, intent) {
601
690
  state.status = patch.label ?? `Reparented ${intent.draggedId}`;
602
691
  return true;
603
692
  }
693
+ function moveSceneGraphNodes(state, options, intent) {
694
+ const document = state.session?.getState().workingDocument;
695
+ if (state.mode !== 'editor' || !state.session || !document)
696
+ return false;
697
+ cancelActiveOperation(state);
698
+ const hierarchy = options.documentAdapter.getHierarchyItems(document);
699
+ const coreValidation = validateSceneGraphMove(hierarchy, intent);
700
+ if (!coreValidation.ok) {
701
+ state.status = `Move rejected: ${coreValidation.reason ?? 'invalid scene graph move'}`;
702
+ return true;
703
+ }
704
+ const projectValidation = options.documentAdapter.validateSceneGraphMove?.(document, intent);
705
+ if (projectValidation && !projectValidation.ok) {
706
+ state.status = `Move rejected: ${projectValidation.reason ?? 'project validation failed'}`;
707
+ return true;
708
+ }
709
+ if (!options.documentAdapter.createSceneGraphMovePatch && intent.placement === 'inside' && intent.ids.length === 1 && intent.targetId) {
710
+ return dropSceneGraphNode(state, options, {
711
+ draggedId: intent.ids[0],
712
+ targetId: intent.targetId,
713
+ placement: 'inside',
714
+ preserveWorldTransform: intent.preserveWorldTransform,
715
+ });
716
+ }
717
+ const patch = options.documentAdapter.createSceneGraphMovePatch?.(document, intent);
718
+ if (!patch) {
719
+ state.status = 'Move rejected';
720
+ return true;
721
+ }
722
+ const result = state.session.dispatch({
723
+ type: 'document.patch',
724
+ label: patch.label ?? `Move ${intent.ids.length} node(s)`,
725
+ patch: patch.patch,
726
+ targetId: intent.ids[0] ?? undefined,
727
+ });
728
+ if (!result.documentChanged) {
729
+ state.status = 'Move unchanged';
730
+ return true;
731
+ }
732
+ const selection = sanitizeSelection(state, options, result.workingDocument, result.selection) ?? result.selection;
733
+ rebuildProjectionFromDocument(state, options, result.workingDocument, selection);
734
+ state.summary = summarizeDocument(options, result.workingDocument, state.session.getSource());
735
+ state.status = patch.label ?? `Moved ${patch.changedIds?.length ?? intent.ids.length} node(s)`;
736
+ return true;
737
+ }
738
+ function groupSceneGraphSelection(state, options, intent) {
739
+ const document = state.session?.getState().workingDocument;
740
+ if (state.mode !== 'editor' || !state.session || !document)
741
+ return false;
742
+ cancelActiveOperation(state);
743
+ const hierarchy = options.documentAdapter.getHierarchyItems(document);
744
+ const coreValidation = validateSceneGraphGroupSelection(hierarchy, intent);
745
+ if (!coreValidation.ok) {
746
+ state.status = `Group selection rejected: ${coreValidation.reason ?? 'invalid scene graph group selection'}`;
747
+ return true;
748
+ }
749
+ const projectValidation = options.documentAdapter.validateSceneGraphGroupSelection?.(document, intent);
750
+ if (projectValidation && !projectValidation.ok) {
751
+ state.status = `Group selection rejected: ${projectValidation.reason ?? 'project validation failed'}`;
752
+ return true;
753
+ }
754
+ const patch = options.documentAdapter.createSceneGraphGroupSelectionPatch?.(document, intent);
755
+ if (!patch) {
756
+ state.status = 'Group selection rejected';
757
+ return true;
758
+ }
759
+ const result = state.session.dispatch({
760
+ type: 'document.patch',
761
+ label: patch.label ?? 'Group Selection',
762
+ patch: patch.patch,
763
+ targetId: patch.createdId,
764
+ });
765
+ if (!result.documentChanged) {
766
+ state.status = 'Group selection unchanged';
767
+ return true;
768
+ }
769
+ let selection = result.selection;
770
+ if (patch.createdId && isNodeSelectableInDocument(options, result.workingDocument, patch.createdId)) {
771
+ selection = state.session.dispatch({
772
+ type: 'selection.replace',
773
+ selectedIds: [patch.createdId],
774
+ activeId: patch.createdId,
775
+ label: 'Select Created Group',
776
+ }).selection;
777
+ }
778
+ else {
779
+ selection = sanitizeSelection(state, options, result.workingDocument, selection) ?? selection;
780
+ }
781
+ rebuildProjectionFromDocument(state, options, result.workingDocument, selection);
782
+ state.summary = summarizeDocument(options, result.workingDocument, state.session.getSource());
783
+ state.status = patch.label ?? `Grouped ${patch.changedIds?.length ?? intent.ids.length} node(s)`;
784
+ return true;
785
+ }
604
786
  function sanitizeSelection(state, options, document, selection) {
605
787
  const selectedIds = selection.selectedIds.filter(id => isNodeSelectableInDocument(options, document, id));
606
788
  const activeId = selection.activeId && selectedIds.includes(selection.activeId)
@@ -668,19 +850,30 @@ function patchSerializedProperty(state, options, input) {
668
850
  return false;
669
851
  const document = state.session.getState().workingDocument;
670
852
  const targetIds = input.targetIds && input.targetIds.length > 0 ? input.targetIds : [input.targetId];
853
+ const transaction = createInspectorEditTransaction(state, options, document, input, targetIds);
854
+ if (!transaction.ok) {
855
+ state.status = transaction.message;
856
+ return false;
857
+ }
858
+ const payload = transaction.payload;
671
859
  if (targetIds.length > 1) {
672
860
  const patch = options.documentAdapter.createSerializedMultiPropertyPatch?.({
673
861
  document,
674
862
  targetIds,
675
863
  activeId: state.session.getState().selection.activeId,
676
- path: input.path,
677
- value: input.value,
864
+ path: payload.path,
865
+ value: payload.value,
866
+ control: payload.control,
867
+ valueType: payload.valueType,
868
+ commitMode: payload.commitMode,
869
+ persistence: payload.persistence,
870
+ source: payload.source,
678
871
  });
679
872
  if (!patch)
680
873
  return false;
681
874
  const result = state.session.dispatch({
682
875
  type: 'document.patch',
683
- label: patch.label ?? `Patch ${input.path} on ${targetIds.length} objects`,
876
+ label: patch.label ?? `Patch ${payload.path} on ${targetIds.length} objects`,
684
877
  patch: patch.patch,
685
878
  targetId: state.session.getState().selection.activeId ?? undefined,
686
879
  });
@@ -688,30 +881,96 @@ function patchSerializedProperty(state, options, input) {
688
881
  return false;
689
882
  const changedIds = patch.changedIds ?? targetIds;
690
883
  const workingDocument = result.workingDocument;
691
- syncProjectionForChangedIds(state, options, workingDocument, changedIds);
884
+ if (patch.reprojectIds?.length)
885
+ reprojectProjectionForChangedIds(state, options, workingDocument, patch.reprojectIds);
886
+ else
887
+ syncProjectionForChangedIds(state, options, workingDocument, changedIds);
692
888
  state.summary = summarizeDocument(options, workingDocument, state.session.getSource());
693
- state.status = patch.label ?? `Patch ${input.path} on ${targetIds.length} objects`;
889
+ state.status = patch.label ?? `Patch ${payload.path} on ${targetIds.length} objects`;
694
890
  return true;
695
891
  }
696
892
  const patch = options.documentAdapter.createSerializedPropertyPatch({
697
- ...input,
698
893
  document,
894
+ targetId: payload.targetId,
895
+ targetIds: payload.targetIds,
896
+ path: payload.path,
897
+ value: payload.value,
898
+ control: payload.control,
899
+ valueType: payload.valueType,
900
+ commitMode: payload.commitMode,
901
+ persistence: payload.persistence,
902
+ source: payload.source,
699
903
  });
700
904
  if (!patch)
701
905
  return false;
702
906
  const result = state.session.dispatch({
703
907
  type: 'document.patch',
704
- label: patch.label ?? `Patch ${input.path}`,
908
+ label: patch.label ?? `Patch ${payload.path}`,
705
909
  patch: patch.patch,
706
910
  });
707
- if (patch.changedIds)
911
+ if (patch.reprojectIds?.length)
912
+ reprojectProjectionForChangedIds(state, options, result.workingDocument, patch.reprojectIds);
913
+ else if (patch.changedIds)
708
914
  syncProjectionForChangedIds(state, options, result.workingDocument, patch.changedIds);
709
915
  else
710
- syncProjectionForDispatchResult(state, options, result, patch.changedId ?? input.targetId);
916
+ syncProjectionForDispatchResult(state, options, result, patch.changedId ?? payload.targetId);
711
917
  state.summary = summarizeDocument(options, result.workingDocument, state.session.getSource());
712
- state.status = patch.label ?? `Patched ${input.path}`;
918
+ state.status = patch.label ?? `Patched ${payload.path}`;
713
919
  return true;
714
920
  }
921
+ function createInspectorEditTransaction(state, options, document, input, targetIds) {
922
+ const property = findInspectorPropertyForEdit(state, options, document, input, targetIds);
923
+ if (!property) {
924
+ return { ok: false, message: `Inspector property not found: ${input.path}.` };
925
+ }
926
+ const result = createInspectorEditPayload(property, {
927
+ targetId: input.targetId,
928
+ targetIds: input.targetIds,
929
+ path: input.path,
930
+ value: input.value,
931
+ control: input.control,
932
+ valueType: input.valueType,
933
+ commitMode: input.commitMode,
934
+ persistence: input.persistence,
935
+ source: input.source,
936
+ });
937
+ if (!result.ok)
938
+ return { ok: false, message: result.message };
939
+ return { ok: true, payload: result.value };
940
+ }
941
+ function findInspectorPropertyForEdit(state, options, document, input, targetIds) {
942
+ const inspector = createInspectorObjectForEdit(state, options, document, input, targetIds);
943
+ if (!inspector)
944
+ return null;
945
+ return findInspectorPropertyByPath(inspector, input.path);
946
+ }
947
+ function createInspectorObjectForEdit(state, options, document, input, targetIds) {
948
+ if (targetIds.length > 1) {
949
+ const activeId = state.session?.getState().selection.activeId ?? input.targetId ?? null;
950
+ const inspector = options.documentAdapter.getInspectorMultiObject?.(document, targetIds, activeId) ?? null;
951
+ if (inspector)
952
+ return withInspectorComponentSections(state, options, document, inspector);
953
+ const serializedMultiObject = options.documentAdapter.getSerializedMultiObject?.(document, targetIds, activeId) ?? null;
954
+ return serializedMultiObject
955
+ ? withInspectorComponentSections(state, options, document, serializedMultiObjectToInspectorObject(serializedMultiObject, document))
956
+ : null;
957
+ }
958
+ const inspector = options.documentAdapter.getInspectorObject?.(document, input.targetId) ?? null;
959
+ if (inspector)
960
+ return withInspectorComponentSections(state, options, document, inspector);
961
+ const serializedObject = options.documentAdapter.getSerializedObject(document, input.targetId);
962
+ return serializedObject
963
+ ? withInspectorComponentSections(state, options, document, serializedObjectToInspectorObject(serializedObject, document))
964
+ : null;
965
+ }
966
+ function findInspectorPropertyByPath(inspector, path) {
967
+ for (const section of inspector.sections) {
968
+ const property = section.properties.find(candidate => candidate.path === path);
969
+ if (property)
970
+ return property;
971
+ }
972
+ return null;
973
+ }
715
974
  function commitGizmoTransform(state, options, event) {
716
975
  if (state.mode !== 'editor' || !state.session)
717
976
  return false;
@@ -865,15 +1124,45 @@ function syncProjectionForChangedIds(state, options, document, changedIds) {
865
1124
  const selection = state.session?.getState().selection ?? { selectedIds: [], activeId: null };
866
1125
  syncSelectionToProjection(state, selection);
867
1126
  }
1127
+ function reprojectProjectionForChangedIds(state, options, document, changedIds) {
1128
+ for (const changedId of changedIds) {
1129
+ const projectedNode = options.documentAdapter.getProjectionNode(document, changedId);
1130
+ if (projectedNode)
1131
+ state.projection?.projectNode(projectedNode);
1132
+ }
1133
+ const selection = state.session?.getState().selection ?? { selectedIds: [], activeId: null };
1134
+ syncSelectionToProjection(state, selection);
1135
+ state.gizmo?.refreshSelection();
1136
+ }
868
1137
  function createUiState(state, options) {
869
1138
  const sessionState = state.session?.getState();
870
1139
  const document = sessionState?.workingDocument ?? null;
871
1140
  const selectedIds = sessionState?.selection.selectedIds ?? [];
872
1141
  const activeId = sessionState?.selection.activeId ?? null;
1142
+ const serializedObject = document && activeId && selectedIds.length === 1
1143
+ ? options.documentAdapter.getSerializedObject(document, activeId)
1144
+ : null;
1145
+ const serializedMultiObject = document && selectedIds.length > 1
1146
+ ? options.documentAdapter.getSerializedMultiObject?.(document, selectedIds, activeId) ?? null
1147
+ : null;
1148
+ const inspectorObjectBase = document && activeId && selectedIds.length === 1
1149
+ ? options.documentAdapter.getInspectorObject?.(document, activeId) ?? (serializedObject ? serializedObjectToInspectorObject(serializedObject, document) : null)
1150
+ : null;
1151
+ const inspectorMultiObjectBase = document && selectedIds.length > 1
1152
+ ? options.documentAdapter.getInspectorMultiObject?.(document, selectedIds, activeId) ?? (serializedMultiObject ? serializedMultiObjectToInspectorObject(serializedMultiObject, document) : null)
1153
+ : null;
1154
+ const inspectorObject = document && inspectorObjectBase
1155
+ ? withRuntimeInspectorSections(state, options, document, inspectorObjectBase)
1156
+ : inspectorObjectBase;
1157
+ const inspectorMultiObject = document && inspectorMultiObjectBase
1158
+ ? withRuntimeInspectorSections(state, options, document, inspectorMultiObjectBase)
1159
+ : inspectorMultiObjectBase;
873
1160
  return {
874
1161
  mode: state.mode,
875
1162
  busy: state.busy,
876
1163
  status: state.status,
1164
+ statusTone: state.statusToneStatus === state.status ? state.statusTone : 'default',
1165
+ statusDetails: state.statusToneStatus === state.status ? state.statusDetails : '',
877
1166
  summary: state.summary,
878
1167
  assetFilter: state.assetFilter,
879
1168
  assets: state.assets
@@ -887,12 +1176,10 @@ function createUiState(state, options) {
887
1176
  count: selectedIds.length,
888
1177
  activeId,
889
1178
  },
890
- serializedObject: document && activeId && selectedIds.length === 1
891
- ? options.documentAdapter.getSerializedObject(document, activeId)
892
- : null,
893
- serializedMultiObject: document && selectedIds.length > 1
894
- ? options.documentAdapter.getSerializedMultiObject?.(document, selectedIds, activeId) ?? null
895
- : null,
1179
+ serializedObject,
1180
+ serializedMultiObject,
1181
+ inspectorObject,
1182
+ inspectorMultiObject,
896
1183
  boxSelection: state.boxSelection,
897
1184
  transformTool: {
898
1185
  activeTool: state.gizmo?.getState().tool ?? state.transformTool,
@@ -912,12 +1199,159 @@ function createUiState(state, options) {
912
1199
  : null,
913
1200
  };
914
1201
  }
1202
+ function withRuntimeInspectorSections(state, options, document, inspectorObject) {
1203
+ const baseInspectorObject = withInspectorComponentSections(state, options, document, inspectorObject);
1204
+ const activeId = inspectorObject.activeId;
1205
+ const projectionNode = activeId ? options.documentAdapter.getProjectionNode(document, activeId) : null;
1206
+ const projectedRoot = activeId ? state.projection?.getProjectedNode(activeId)?.root ?? null : null;
1207
+ const context = {
1208
+ document,
1209
+ targetIds: baseInspectorObject.targetIds,
1210
+ activeId,
1211
+ inspectorObject: baseInspectorObject,
1212
+ projectionNode,
1213
+ projectedRoot,
1214
+ };
1215
+ const runtimeSections = [
1216
+ ...createDefaultRuntimeInspectorSections(context),
1217
+ ...(options.documentAdapter.getRuntimeInspectorSections?.(context) ?? []),
1218
+ ];
1219
+ const sections = mergeInspectorSections(baseInspectorObject.sections, runtimeSections, {
1220
+ propertyConflict: options.inspector?.propertyConflict,
1221
+ });
1222
+ return {
1223
+ ...baseInspectorObject,
1224
+ sections,
1225
+ };
1226
+ }
1227
+ function withInspectorComponentSections(state, options, document, inspectorObject) {
1228
+ const context = createHarnessInspectorSelectionContext(state, document, inspectorObject);
1229
+ return mergeLocalEditorHarnessInspectorComponentSections({
1230
+ inspectorObject,
1231
+ components: options.inspector?.components,
1232
+ context,
1233
+ componentConflict: options.inspector?.componentConflict,
1234
+ propertyConflict: options.inspector?.propertyConflict,
1235
+ });
1236
+ }
1237
+ function getInspectorComponentSections(input) {
1238
+ if (isInspectorRegistry(input.components))
1239
+ return input.components.getSections(input.context);
1240
+ const registry = createInspectorRegistry({
1241
+ onConflict: input.componentConflict,
1242
+ propertyConflict: input.propertyConflict,
1243
+ });
1244
+ for (const component of input.components)
1245
+ registry.register(component);
1246
+ return registry.getSections(input.context);
1247
+ }
1248
+ function createHarnessInspectorSelectionContext(state, document, inspectorObject) {
1249
+ const activeId = inspectorObject.activeId;
1250
+ const projectedRoot = activeId ? state.projection?.getProjectedNode(activeId)?.root ?? null : null;
1251
+ return {
1252
+ ...inspectorObject.selection,
1253
+ targetIds: inspectorObject.targetIds,
1254
+ activeId,
1255
+ document,
1256
+ runtimeTarget: inspectorObject.selection.runtimeTarget ?? projectedRoot ?? undefined,
1257
+ };
1258
+ }
1259
+ function isInspectorRegistry(components) {
1260
+ return !Array.isArray(components)
1261
+ && typeof components.getSections === 'function';
1262
+ }
1263
+ function createDefaultRuntimeInspectorSections(context) {
1264
+ if (context.targetIds.length !== 1)
1265
+ return [];
1266
+ const root = context.projectedRoot;
1267
+ const projectionNode = context.projectionNode;
1268
+ if (!root && !projectionNode)
1269
+ return [];
1270
+ const properties = [];
1271
+ if (projectionNode) {
1272
+ properties.push(createRuntimeInspectorProperty('runtime.projection.nodeId', 'Projected ID', projectionNode.id, properties.length));
1273
+ const assetSource = projectionNode.asset?.sourceId ?? projectionNode.asset?.id ?? '';
1274
+ if (assetSource)
1275
+ properties.push(createRuntimeInspectorProperty('runtime.projection.assetSource', 'Asset Source', assetSource, properties.length));
1276
+ }
1277
+ const runtimeClass = readRuntimeClassName(root);
1278
+ if (runtimeClass)
1279
+ properties.push(createRuntimeInspectorProperty('runtime.root.className', 'Runtime Class', runtimeClass, properties.length));
1280
+ const runtimeName = readRuntimeStringProperty(root, 'name');
1281
+ if (runtimeName)
1282
+ properties.push(createRuntimeInspectorProperty('runtime.root.name', 'Runtime Name', runtimeName, properties.length));
1283
+ const childCount = readRuntimeChildCount(root);
1284
+ if (childCount != null)
1285
+ properties.push(createRuntimeInspectorProperty('runtime.root.children', 'Runtime Children', childCount, properties.length));
1286
+ if (properties.length === 0)
1287
+ return [];
1288
+ return [{
1289
+ id: 'runtimeDiagnostics',
1290
+ title: 'Runtime Diagnostics',
1291
+ order: 900,
1292
+ placement: 'body',
1293
+ persistence: 'runtime',
1294
+ runtimeOnly: true,
1295
+ properties,
1296
+ }];
1297
+ }
1298
+ function createRuntimeInspectorProperty(path, label, value, order) {
1299
+ return {
1300
+ path,
1301
+ label,
1302
+ valueType: typeof value === 'number' ? 'number' : typeof value === 'boolean' ? 'boolean' : typeof value === 'object' ? 'object' : 'string',
1303
+ control: 'readonly',
1304
+ value,
1305
+ readOnly: true,
1306
+ persistence: 'runtime',
1307
+ commitMode: 'blur',
1308
+ order,
1309
+ };
1310
+ }
1311
+ function readRuntimeClassName(value) {
1312
+ const record = isObjectRecord(value) ? value : null;
1313
+ const getter = record?.getClassName;
1314
+ if (typeof getter === 'function') {
1315
+ const className = getter.call(value);
1316
+ if (typeof className === 'string' && className.trim())
1317
+ return className;
1318
+ }
1319
+ const constructorName = record?.constructor && typeof record.constructor === 'function'
1320
+ ? record.constructor.name
1321
+ : '';
1322
+ return constructorName && constructorName !== 'Object' ? constructorName : null;
1323
+ }
1324
+ function readRuntimeStringProperty(value, key) {
1325
+ if (!isObjectRecord(value))
1326
+ return null;
1327
+ const raw = value[key];
1328
+ return typeof raw === 'string' && raw.trim() ? raw : null;
1329
+ }
1330
+ function readRuntimeChildCount(value) {
1331
+ if (!isObjectRecord(value))
1332
+ return null;
1333
+ const children = value.getChildren;
1334
+ if (typeof children === 'function') {
1335
+ const result = children.call(value);
1336
+ return Array.isArray(result) ? result.length : null;
1337
+ }
1338
+ return null;
1339
+ }
1340
+ function isObjectRecord(value) {
1341
+ return !!value && typeof value === 'object';
1342
+ }
915
1343
  function summarizeDocument(options, document, _source) {
916
1344
  return options.documentAdapter.summarize?.(document) ?? '';
917
1345
  }
918
- function summarizeAuthoringFailure(result) {
1346
+ export function summarizeLocalEditorAuthoringFailure(result) {
919
1347
  const diagnostic = result.diagnostics.find(item => item.severity === 'error') ?? result.diagnostics[0];
920
- return diagnostic?.message ?? result.reason ?? 'Authoring source commit failed';
1348
+ const status = diagnostic?.message ?? result.reason ?? 'Authoring source commit failed';
1349
+ const diagnostics = summarizeDiagnostics(result.diagnostics);
1350
+ const details = [
1351
+ result.reason ? `Reason: ${result.reason}` : '',
1352
+ diagnostics ? `Diagnostics: ${diagnostics}` : '',
1353
+ ].filter(Boolean).join('\n') || status;
1354
+ return { status, details };
921
1355
  }
922
1356
  function summarizeDiagnostics(diagnostics) {
923
1357
  if (!diagnostics?.length)