@fps-games/editor 0.1.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 (186) hide show
  1. package/dist/authoring-apply.d.ts +21 -0
  2. package/dist/authoring-apply.d.ts.map +1 -0
  3. package/dist/authoring-apply.js +45 -0
  4. package/dist/authoring-apply.js.map +1 -0
  5. package/dist/index.d.ts +85 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +539 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/local-editor-harness.d.ts +173 -0
  10. package/dist/local-editor-harness.d.ts.map +1 -0
  11. package/dist/local-editor-harness.js +969 -0
  12. package/dist/local-editor-harness.js.map +1 -0
  13. package/node_modules/@fps-games/editor-babylon/dist/camera-controller.d.ts +20 -0
  14. package/node_modules/@fps-games/editor-babylon/dist/camera-controller.d.ts.map +1 -0
  15. package/node_modules/@fps-games/editor-babylon/dist/camera-controller.js +239 -0
  16. package/node_modules/@fps-games/editor-babylon/dist/camera-controller.js.map +1 -0
  17. package/node_modules/@fps-games/editor-babylon/dist/edit-session.d.ts +30 -0
  18. package/node_modules/@fps-games/editor-babylon/dist/edit-session.d.ts.map +1 -0
  19. package/node_modules/@fps-games/editor-babylon/dist/edit-session.js +297 -0
  20. package/node_modules/@fps-games/editor-babylon/dist/edit-session.js.map +1 -0
  21. package/node_modules/@fps-games/editor-babylon/dist/editor-world.d.ts +30 -0
  22. package/node_modules/@fps-games/editor-babylon/dist/editor-world.d.ts.map +1 -0
  23. package/node_modules/@fps-games/editor-babylon/dist/editor-world.js +67 -0
  24. package/node_modules/@fps-games/editor-babylon/dist/editor-world.js.map +1 -0
  25. package/node_modules/@fps-games/editor-babylon/dist/event-guard.d.ts +13 -0
  26. package/node_modules/@fps-games/editor-babylon/dist/event-guard.d.ts.map +1 -0
  27. package/node_modules/@fps-games/editor-babylon/dist/event-guard.js +101 -0
  28. package/node_modules/@fps-games/editor-babylon/dist/event-guard.js.map +1 -0
  29. package/node_modules/@fps-games/editor-babylon/dist/index.d.ts +19 -0
  30. package/node_modules/@fps-games/editor-babylon/dist/index.d.ts.map +1 -0
  31. package/node_modules/@fps-games/editor-babylon/dist/index.js +18 -0
  32. package/node_modules/@fps-games/editor-babylon/dist/index.js.map +1 -0
  33. package/node_modules/@fps-games/editor-babylon/dist/input-controller.d.ts +3 -0
  34. package/node_modules/@fps-games/editor-babylon/dist/input-controller.d.ts.map +1 -0
  35. package/node_modules/@fps-games/editor-babylon/dist/input-controller.js +182 -0
  36. package/node_modules/@fps-games/editor-babylon/dist/input-controller.js.map +1 -0
  37. package/node_modules/@fps-games/editor-babylon/dist/inspector-adapter.d.ts +13 -0
  38. package/node_modules/@fps-games/editor-babylon/dist/inspector-adapter.d.ts.map +1 -0
  39. package/node_modules/@fps-games/editor-babylon/dist/inspector-adapter.js +94 -0
  40. package/node_modules/@fps-games/editor-babylon/dist/inspector-adapter.js.map +1 -0
  41. package/node_modules/@fps-games/editor-babylon/dist/inspector-host.d.ts +43 -0
  42. package/node_modules/@fps-games/editor-babylon/dist/inspector-host.d.ts.map +1 -0
  43. package/node_modules/@fps-games/editor-babylon/dist/inspector-host.js +510 -0
  44. package/node_modules/@fps-games/editor-babylon/dist/inspector-host.js.map +1 -0
  45. package/node_modules/@fps-games/editor-babylon/dist/legacy-runtime.d.ts +5 -0
  46. package/node_modules/@fps-games/editor-babylon/dist/legacy-runtime.d.ts.map +1 -0
  47. package/node_modules/@fps-games/editor-babylon/dist/legacy-runtime.js +5 -0
  48. package/node_modules/@fps-games/editor-babylon/dist/legacy-runtime.js.map +1 -0
  49. package/node_modules/@fps-games/editor-babylon/dist/material-property-adapter.d.ts +22 -0
  50. package/node_modules/@fps-games/editor-babylon/dist/material-property-adapter.d.ts.map +1 -0
  51. package/node_modules/@fps-games/editor-babylon/dist/material-property-adapter.js +408 -0
  52. package/node_modules/@fps-games/editor-babylon/dist/material-property-adapter.js.map +1 -0
  53. package/node_modules/@fps-games/editor-babylon/dist/monitor.d.ts +39 -0
  54. package/node_modules/@fps-games/editor-babylon/dist/monitor.d.ts.map +1 -0
  55. package/node_modules/@fps-games/editor-babylon/dist/monitor.js +780 -0
  56. package/node_modules/@fps-games/editor-babylon/dist/monitor.js.map +1 -0
  57. package/node_modules/@fps-games/editor-babylon/dist/outline-adapter.d.ts +37 -0
  58. package/node_modules/@fps-games/editor-babylon/dist/outline-adapter.d.ts.map +1 -0
  59. package/node_modules/@fps-games/editor-babylon/dist/outline-adapter.js +268 -0
  60. package/node_modules/@fps-games/editor-babylon/dist/outline-adapter.js.map +1 -0
  61. package/node_modules/@fps-games/editor-babylon/dist/projection-selection-controller.d.ts +34 -0
  62. package/node_modules/@fps-games/editor-babylon/dist/projection-selection-controller.d.ts.map +1 -0
  63. package/node_modules/@fps-games/editor-babylon/dist/projection-selection-controller.js +221 -0
  64. package/node_modules/@fps-games/editor-babylon/dist/projection-selection-controller.js.map +1 -0
  65. package/node_modules/@fps-games/editor-babylon/dist/projection.d.ts +90 -0
  66. package/node_modules/@fps-games/editor-babylon/dist/projection.d.ts.map +1 -0
  67. package/node_modules/@fps-games/editor-babylon/dist/projection.js +479 -0
  68. package/node_modules/@fps-games/editor-babylon/dist/projection.js.map +1 -0
  69. package/node_modules/@fps-games/editor-babylon/dist/runtime-globals.d.ts +3 -0
  70. package/node_modules/@fps-games/editor-babylon/dist/runtime-globals.d.ts.map +1 -0
  71. package/node_modules/@fps-games/editor-babylon/dist/runtime-globals.js +7 -0
  72. package/node_modules/@fps-games/editor-babylon/dist/runtime-globals.js.map +1 -0
  73. package/node_modules/@fps-games/editor-babylon/dist/scene-view-camera-controller.d.ts +18 -0
  74. package/node_modules/@fps-games/editor-babylon/dist/scene-view-camera-controller.d.ts.map +1 -0
  75. package/node_modules/@fps-games/editor-babylon/dist/scene-view-camera-controller.js +213 -0
  76. package/node_modules/@fps-games/editor-babylon/dist/scene-view-camera-controller.js.map +1 -0
  77. package/node_modules/@fps-games/editor-babylon/dist/scene-view-input-controller.d.ts +36 -0
  78. package/node_modules/@fps-games/editor-babylon/dist/scene-view-input-controller.d.ts.map +1 -0
  79. package/node_modules/@fps-games/editor-babylon/dist/scene-view-input-controller.js +272 -0
  80. package/node_modules/@fps-games/editor-babylon/dist/scene-view-input-controller.js.map +1 -0
  81. package/node_modules/@fps-games/editor-babylon/dist/selection-controller.d.ts +27 -0
  82. package/node_modules/@fps-games/editor-babylon/dist/selection-controller.d.ts.map +1 -0
  83. package/node_modules/@fps-games/editor-babylon/dist/selection-controller.js +308 -0
  84. package/node_modules/@fps-games/editor-babylon/dist/selection-controller.js.map +1 -0
  85. package/node_modules/@fps-games/editor-babylon/dist/tool-controller.d.ts +17 -0
  86. package/node_modules/@fps-games/editor-babylon/dist/tool-controller.d.ts.map +1 -0
  87. package/node_modules/@fps-games/editor-babylon/dist/tool-controller.js +83 -0
  88. package/node_modules/@fps-games/editor-babylon/dist/tool-controller.js.map +1 -0
  89. package/node_modules/@fps-games/editor-babylon/dist/transform-gizmo-controller.d.ts +47 -0
  90. package/node_modules/@fps-games/editor-babylon/dist/transform-gizmo-controller.d.ts.map +1 -0
  91. package/node_modules/@fps-games/editor-babylon/dist/transform-gizmo-controller.js +689 -0
  92. package/node_modules/@fps-games/editor-babylon/dist/transform-gizmo-controller.js.map +1 -0
  93. package/node_modules/@fps-games/editor-babylon/dist/types.d.ts +111 -0
  94. package/node_modules/@fps-games/editor-babylon/dist/types.d.ts.map +1 -0
  95. package/node_modules/@fps-games/editor-babylon/dist/types.js +2 -0
  96. package/node_modules/@fps-games/editor-babylon/dist/types.js.map +1 -0
  97. package/node_modules/@fps-games/editor-babylon/package.json +25 -0
  98. package/node_modules/@fps-games/editor-browser/dist/index.d.ts +28 -0
  99. package/node_modules/@fps-games/editor-browser/dist/index.d.ts.map +1 -0
  100. package/node_modules/@fps-games/editor-browser/dist/index.js +54 -0
  101. package/node_modules/@fps-games/editor-browser/dist/index.js.map +1 -0
  102. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-input-router.d.ts +12 -0
  103. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-input-router.d.ts.map +1 -0
  104. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-input-router.js +80 -0
  105. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-input-router.js.map +1 -0
  106. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panel-registry.d.ts +11 -0
  107. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panel-registry.d.ts.map +1 -0
  108. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panel-registry.js +33 -0
  109. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panel-registry.js.map +1 -0
  110. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.d.ts +9 -0
  111. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.d.ts.map +1 -0
  112. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.js +444 -0
  113. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.js.map +1 -0
  114. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-primitives.d.ts +21 -0
  115. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-primitives.d.ts.map +1 -0
  116. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-primitives.js +103 -0
  117. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-primitives.js.map +1 -0
  118. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.d.ts +12 -0
  119. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.d.ts.map +1 -0
  120. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.js +123 -0
  121. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.js.map +1 -0
  122. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.d.ts +2 -0
  123. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.d.ts.map +1 -0
  124. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.js +115 -0
  125. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.js.map +1 -0
  126. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-theme.d.ts +3 -0
  127. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-theme.d.ts.map +1 -0
  128. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-theme.js +68 -0
  129. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-theme.js.map +1 -0
  130. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts +183 -0
  131. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts.map +1 -0
  132. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.js +2 -0
  133. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.js.map +1 -0
  134. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.d.ts +58 -0
  135. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.d.ts.map +1 -0
  136. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.js +136 -0
  137. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.js.map +1 -0
  138. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.d.ts +4 -0
  139. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.d.ts.map +1 -0
  140. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js +589 -0
  141. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js.map +1 -0
  142. package/node_modules/@fps-games/editor-browser/package.json +16 -0
  143. package/node_modules/@fps-games/editor-core/dist/authoring-source.d.ts +211 -0
  144. package/node_modules/@fps-games/editor-core/dist/authoring-source.d.ts.map +1 -0
  145. package/node_modules/@fps-games/editor-core/dist/authoring-source.js +511 -0
  146. package/node_modules/@fps-games/editor-core/dist/authoring-source.js.map +1 -0
  147. package/node_modules/@fps-games/editor-core/dist/editor-session.d.ts +132 -0
  148. package/node_modules/@fps-games/editor-core/dist/editor-session.d.ts.map +1 -0
  149. package/node_modules/@fps-games/editor-core/dist/editor-session.js +265 -0
  150. package/node_modules/@fps-games/editor-core/dist/editor-session.js.map +1 -0
  151. package/node_modules/@fps-games/editor-core/dist/host-services.d.ts +24 -0
  152. package/node_modules/@fps-games/editor-core/dist/host-services.d.ts.map +1 -0
  153. package/node_modules/@fps-games/editor-core/dist/host-services.js +2 -0
  154. package/node_modules/@fps-games/editor-core/dist/host-services.js.map +1 -0
  155. package/node_modules/@fps-games/editor-core/dist/index.d.ts +106 -0
  156. package/node_modules/@fps-games/editor-core/dist/index.d.ts.map +1 -0
  157. package/node_modules/@fps-games/editor-core/dist/index.js +217 -0
  158. package/node_modules/@fps-games/editor-core/dist/index.js.map +1 -0
  159. package/node_modules/@fps-games/editor-core/dist/scene-graph.d.ts +44 -0
  160. package/node_modules/@fps-games/editor-core/dist/scene-graph.d.ts.map +1 -0
  161. package/node_modules/@fps-games/editor-core/dist/scene-graph.js +86 -0
  162. package/node_modules/@fps-games/editor-core/dist/scene-graph.js.map +1 -0
  163. package/node_modules/@fps-games/editor-core/dist/scene-view-input.d.ts +41 -0
  164. package/node_modules/@fps-games/editor-core/dist/scene-view-input.d.ts.map +1 -0
  165. package/node_modules/@fps-games/editor-core/dist/scene-view-input.js +2 -0
  166. package/node_modules/@fps-games/editor-core/dist/scene-view-input.js.map +1 -0
  167. package/node_modules/@fps-games/editor-core/dist/serialized-object.d.ts +53 -0
  168. package/node_modules/@fps-games/editor-core/dist/serialized-object.d.ts.map +1 -0
  169. package/node_modules/@fps-games/editor-core/dist/serialized-object.js +32 -0
  170. package/node_modules/@fps-games/editor-core/dist/serialized-object.js.map +1 -0
  171. package/node_modules/@fps-games/editor-core/dist/transform-gizmo.d.ts +48 -0
  172. package/node_modules/@fps-games/editor-core/dist/transform-gizmo.d.ts.map +1 -0
  173. package/node_modules/@fps-games/editor-core/dist/transform-gizmo.js +2 -0
  174. package/node_modules/@fps-games/editor-core/dist/transform-gizmo.js.map +1 -0
  175. package/node_modules/@fps-games/editor-core/package.json +19 -0
  176. package/node_modules/@fps-games/editor-forge-play/dist/index.d.ts +46 -0
  177. package/node_modules/@fps-games/editor-forge-play/dist/index.d.ts.map +1 -0
  178. package/node_modules/@fps-games/editor-forge-play/dist/index.js +156 -0
  179. package/node_modules/@fps-games/editor-forge-play/dist/index.js.map +1 -0
  180. package/node_modules/@fps-games/editor-forge-play/package.json +19 -0
  181. package/node_modules/@fps-games/editor-protocol/dist/index.d.ts +239 -0
  182. package/node_modules/@fps-games/editor-protocol/dist/index.d.ts.map +1 -0
  183. package/node_modules/@fps-games/editor-protocol/dist/index.js +54 -0
  184. package/node_modules/@fps-games/editor-protocol/dist/index.js.map +1 -0
  185. package/node_modules/@fps-games/editor-protocol/package.json +16 -0
  186. package/package.json +40 -0
@@ -0,0 +1,969 @@
1
+ import { createEditorSession, validateSceneGraphDelete, validateSceneGraphDrop, validateSceneGraphRename, } from '@fps-games/editor-core';
2
+ import { createLocalEditorBrowserUi, } from '@fps-games/editor-browser';
3
+ import { createBabylonEditorProjection, createBabylonEditorWorld, createBabylonProjectionSelectionController, createBabylonSceneViewCameraController, createBabylonSceneViewInputController, createBabylonTransformGizmoController, focusEditorViewportSelection, } from '@fps-games/editor-babylon';
4
+ export function createLocalEditorHarness(options) {
5
+ const root = options.root ?? document.body;
6
+ const state = {
7
+ mode: 'game',
8
+ busy: false,
9
+ session: null,
10
+ source: null,
11
+ assets: [],
12
+ assetFilter: '',
13
+ babylon: null,
14
+ engine: null,
15
+ world: null,
16
+ projection: null,
17
+ gizmo: null,
18
+ sceneViewInput: null,
19
+ sceneViewCamera: null,
20
+ selectionController: null,
21
+ boxSelection: null,
22
+ transformTool: 'select',
23
+ transformSpace: 'world',
24
+ transformConstraint: 'axis',
25
+ resizeHandler: null,
26
+ status: 'Game running',
27
+ summary: '',
28
+ };
29
+ let harness;
30
+ const ui = createLocalEditorBrowserUi({
31
+ root,
32
+ callbacks: {
33
+ onEnterEditor: () => {
34
+ void runExclusive(state, harness.render, () => harness.enterEditor());
35
+ },
36
+ onSaveScene: () => {
37
+ void runExclusive(state, harness.render, () => harness.saveScene());
38
+ },
39
+ onSaveAndRunGame: () => {
40
+ void runExclusive(state, harness.render, () => harness.saveAndRunGame());
41
+ },
42
+ onDiscardAndRunGame: () => {
43
+ void runExclusive(state, harness.render, () => harness.discardAndRunGame());
44
+ },
45
+ onUndo: () => {
46
+ if (undoSessionChange(state, options))
47
+ harness.render();
48
+ },
49
+ onRedo: () => {
50
+ if (redoSessionChange(state, options))
51
+ harness.render();
52
+ },
53
+ onCreateFromAsset: (assetId) => {
54
+ if (addAssetToDocument(state, options, assetId))
55
+ harness.render();
56
+ },
57
+ onSelectHierarchyItem: (input) => {
58
+ if (selectItem(state, options, input))
59
+ harness.render();
60
+ },
61
+ onSceneGraphRename: (intent) => {
62
+ if (renameSceneGraphNode(state, options, intent))
63
+ harness.render();
64
+ },
65
+ onSceneGraphCreateGroup: (intent) => {
66
+ if (createSceneGraphGroup(state, options, intent))
67
+ harness.render();
68
+ },
69
+ onSceneGraphDelete: (intent) => {
70
+ if (deleteSceneGraphNodes(state, options, intent))
71
+ harness.render();
72
+ },
73
+ onSceneGraphDrop: (intent) => {
74
+ if (dropSceneGraphNode(state, options, intent))
75
+ harness.render();
76
+ },
77
+ onAssetFilterChange: (value) => {
78
+ state.assetFilter = value;
79
+ harness.render();
80
+ },
81
+ onPropertyInput: (input) => {
82
+ if (patchSerializedProperty(state, options, input))
83
+ harness.render();
84
+ },
85
+ onTransformToolChange: (tool) => {
86
+ state.transformTool = tool;
87
+ state.gizmo?.setTool(tool);
88
+ harness.render();
89
+ },
90
+ onTransformSpaceChange: (space) => {
91
+ state.transformSpace = space;
92
+ state.gizmo?.setSpace(space);
93
+ harness.render();
94
+ },
95
+ onTransformConstraintChange: (constraint) => {
96
+ state.transformConstraint = constraint;
97
+ state.gizmo?.setConstraint(constraint);
98
+ harness.render();
99
+ },
100
+ onFocusSelection: () => {
101
+ if (focusSelectedProjection(state))
102
+ harness.render();
103
+ },
104
+ onCancelActiveOperation: () => {
105
+ cancelActiveOperation(state);
106
+ harness.render();
107
+ },
108
+ },
109
+ });
110
+ harness = {
111
+ render() {
112
+ ui.update(createUiState(state, options));
113
+ },
114
+ getHostServices() {
115
+ return options.hostServices ?? null;
116
+ },
117
+ getWorkingDocument() {
118
+ return state.session?.getState().workingDocument ?? null;
119
+ },
120
+ async enterEditor() {
121
+ const loadedSource = options.persistenceAdapter.loadAuthoringSource
122
+ ? await options.persistenceAdapter.loadAuthoringSource()
123
+ : null;
124
+ const [document, assets] = loadedSource
125
+ ? [loadedSource.document, loadedSource.assets ?? await options.persistenceAdapter.loadAssets()]
126
+ : [await loadDocumentFallback(options), await options.persistenceAdapter.loadAssets()];
127
+ const source = loadedSource?.source ?? null;
128
+ const preparedDocument = options.documentAdapter.prepareDocument?.(document, assets) ?? document;
129
+ state.assets = assets;
130
+ state.source = source;
131
+ state.session = createEditorSession({
132
+ source: source ?? undefined,
133
+ persistedDocument: preparedDocument,
134
+ cloneDocument: options.documentAdapter.cloneDocument,
135
+ compareDocuments: options.documentAdapter.compareDocuments,
136
+ reduceDocument: options.documentAdapter.reduceDocument,
137
+ });
138
+ await options.worldAdapter.disposeGameWorld();
139
+ await createEditorWorld(state, options, harness.render);
140
+ state.mode = 'editor';
141
+ state.summary = loadedSource?.summary ?? summarizeDocument(options, preparedDocument, source);
142
+ state.status = `GameWorld disposed; EditorWorld active; assets=${assets.length}`;
143
+ },
144
+ async saveScene() {
145
+ cancelActiveOperation(state);
146
+ const document = state.session?.getState().workingDocument ?? await loadDocumentFallback(options);
147
+ const source = state.session?.getState().source ?? state.source;
148
+ let savedSource = source;
149
+ let result;
150
+ if (source && options.authoringHost) {
151
+ const hostResult = await options.authoringHost.commitSource({
152
+ source,
153
+ document,
154
+ expectedRevision: source.ref.revision,
155
+ });
156
+ if (!hostResult.ok || !hostResult.document) {
157
+ state.status = summarizeAuthoringFailure(hostResult);
158
+ state.summary = summarizeDocument(options, document, source);
159
+ return false;
160
+ }
161
+ savedSource = hostResult.source ?? source;
162
+ result = {
163
+ document: hostResult.document,
164
+ summary: hostResult.summary ?? summarizeDiagnostics(hostResult.diagnostics),
165
+ };
166
+ }
167
+ else if (source && options.persistenceAdapter.saveAuthoringSource) {
168
+ const sourceResult = await options.persistenceAdapter.saveAuthoringSource({ source, document });
169
+ savedSource = sourceResult.source;
170
+ result = sourceResult;
171
+ }
172
+ else {
173
+ result = await saveDocumentFallback(options, document);
174
+ }
175
+ const preparedDocument = options.documentAdapter.prepareDocument?.(result.document, state.assets) ?? result.document;
176
+ state.source = savedSource ?? null;
177
+ if (state.session) {
178
+ state.session.markSaved(preparedDocument, savedSource ?? undefined);
179
+ }
180
+ else {
181
+ state.session = createEditorSession({
182
+ source: savedSource ?? undefined,
183
+ persistedDocument: preparedDocument,
184
+ cloneDocument: options.documentAdapter.cloneDocument,
185
+ compareDocuments: options.documentAdapter.compareDocuments,
186
+ reduceDocument: options.documentAdapter.reduceDocument,
187
+ });
188
+ }
189
+ state.summary = result.summary ?? summarizeDocument(options, preparedDocument, savedSource ?? null);
190
+ state.status = 'Scene saved';
191
+ return true;
192
+ },
193
+ async saveAndRunGame() {
194
+ const saved = await harness.saveScene();
195
+ if (!saved)
196
+ return false;
197
+ await harness.discardAndRunGame();
198
+ return true;
199
+ },
200
+ async discardAndRunGame() {
201
+ cancelActiveOperation(state);
202
+ disposeEditorWorld(state);
203
+ state.session = null;
204
+ state.source = null;
205
+ state.status = 'Reloading game';
206
+ await options.persistenceAdapter.runGame();
207
+ },
208
+ dispose() {
209
+ disposeEditorWorld(state);
210
+ state.session = null;
211
+ ui.dispose();
212
+ },
213
+ };
214
+ harness.render();
215
+ return harness;
216
+ }
217
+ async function createEditorWorld(state, options, render) {
218
+ disposeEditorWorld(state);
219
+ const canvas = options.worldAdapter.getCanvas();
220
+ if (!canvas)
221
+ throw new Error('Editor canvas not found');
222
+ const babylon = await options.worldAdapter.loadBabylon();
223
+ const engine = options.worldAdapter.createEngine(babylon, canvas);
224
+ const world = createBabylonEditorWorld({
225
+ engine,
226
+ canvas,
227
+ babylon,
228
+ cameraTarget: options.world?.cameraTarget,
229
+ cameraRadius: options.world?.cameraRadius,
230
+ clearColor: options.world?.clearColor,
231
+ useRightHandedSystem: options.world?.useRightHandedSystem,
232
+ enableDefaultCameraControls: false,
233
+ });
234
+ options.createGrid?.(babylon, world.scene);
235
+ const projection = createBabylonEditorProjection({
236
+ babylon,
237
+ scene: world.scene,
238
+ importModel: options.worldAdapter.importProjectionModel,
239
+ logger: console,
240
+ });
241
+ const gizmo = createBabylonTransformGizmoController({
242
+ babylon,
243
+ scene: world.scene,
244
+ projection,
245
+ initialTool: state.transformTool,
246
+ initialSpace: state.transformSpace,
247
+ logger: console,
248
+ onDragStart(event) {
249
+ state.status = event.targetIds.length > 1
250
+ ? `Dragging ${event.tool} ${event.targetIds.length} objects`
251
+ : `Dragging ${event.tool} ${event.nodeId ?? event.activeId ?? 'selection'}`;
252
+ render();
253
+ },
254
+ onDragUpdate() {
255
+ render();
256
+ },
257
+ onDragEnd(event) {
258
+ commitGizmoTransform(state, options, event);
259
+ render();
260
+ },
261
+ onDragCancel(event) {
262
+ state.status = event.targetIds.length > 1
263
+ ? `Canceled ${event.tool} ${event.targetIds.length} objects`
264
+ : `Canceled ${event.tool} ${event.nodeId ?? event.activeId ?? 'selection'}`;
265
+ render();
266
+ },
267
+ });
268
+ gizmo.setConstraint(state.transformConstraint);
269
+ const selectionController = createBabylonProjectionSelectionController({
270
+ scene: world.scene,
271
+ canvas,
272
+ projection,
273
+ getTool: () => state.transformTool,
274
+ getSelection: () => state.session?.getSelection() ?? { selectedIds: [], activeId: null },
275
+ isSelectable: (nodeId) => isDocumentNodeSelectable(state, options, nodeId),
276
+ isLocked: (nodeId) => isDocumentNodeLocked(state, options, nodeId),
277
+ isOperationBlocked: () => state.gizmo?.getState().dragPhase === 'dragging',
278
+ onSelectionCommand(command) {
279
+ if (dispatchSelectionCommand(state, options, command))
280
+ render();
281
+ },
282
+ onFocusIntent(nodeId) {
283
+ if (focusProjectionNode(state, nodeId))
284
+ render();
285
+ },
286
+ onBoxSelectionChange(box) {
287
+ state.boxSelection = box;
288
+ render();
289
+ },
290
+ });
291
+ const sceneViewInput = createBabylonSceneViewInputController({
292
+ canvas,
293
+ isEnabled: () => state.mode === 'editor',
294
+ isGizmoDragCandidate: (event) => gizmo.isGizmoDragCandidate(event),
295
+ isBoxSelectCandidate: (event) => selectionController.isBoxSelectionCandidate(event),
296
+ isViewPlaneMoveCandidate: (event) => gizmo.isViewPlaneMoveCandidate(event),
297
+ onPointerIntentStart(event) {
298
+ if (event.state.intent === 'view-plane-move') {
299
+ if (gizmo.beginViewPlaneMove(event.originalEvent))
300
+ render();
301
+ return;
302
+ }
303
+ if (event.state.intent === 'selection-click' || event.state.intent === 'box-select') {
304
+ selectionController.beginPointerSelection(event.originalEvent);
305
+ }
306
+ },
307
+ onPointerIntentMove(event) {
308
+ if (event.state.intent === 'view-plane-move') {
309
+ if (gizmo.updateViewPlaneMove(event.originalEvent))
310
+ render();
311
+ return;
312
+ }
313
+ if (state.sceneViewCamera?.handlePointerIntentMove(event)) {
314
+ render();
315
+ return;
316
+ }
317
+ if (event.state.intent === 'selection-click' || event.state.intent === 'box-select') {
318
+ selectionController.updatePointerSelection(event.originalEvent, event.state.intent);
319
+ }
320
+ },
321
+ onPointerIntentEnd(event) {
322
+ if (event.state.intent === 'view-plane-move') {
323
+ if (gizmo.endViewPlaneMove(event.originalEvent))
324
+ render();
325
+ return;
326
+ }
327
+ if (event.state.intent === 'selection-click' || event.state.intent === 'box-select') {
328
+ selectionController.endPointerSelection(event.originalEvent, event.state.intent);
329
+ }
330
+ },
331
+ onPointerIntentCancel(event) {
332
+ if (event.state.intent === 'view-plane-move') {
333
+ gizmo.cancelDrag();
334
+ render();
335
+ return;
336
+ }
337
+ if (event.state.intent === 'selection-click' || event.state.intent === 'box-select') {
338
+ selectionController.cancelBoxSelection();
339
+ render();
340
+ }
341
+ },
342
+ onDoubleClick(event) {
343
+ selectionController.handleDoubleClick(event);
344
+ },
345
+ onWheel(event) {
346
+ if (state.sceneViewCamera?.handleWheel(event))
347
+ render();
348
+ },
349
+ });
350
+ const sceneViewCamera = createBabylonSceneViewCameraController({
351
+ babylon,
352
+ scene: world.scene,
353
+ camera: world.camera,
354
+ input: sceneViewInput,
355
+ });
356
+ const document = state.session?.getState().workingDocument;
357
+ if (document) {
358
+ projection.projectNodes(options.documentAdapter.getProjectionNodes(document));
359
+ const selection = state.session?.getState().selection ?? { selectedIds: [], activeId: null };
360
+ projection.syncSelection(selection);
361
+ gizmo.setSelection(selection);
362
+ }
363
+ const resize = () => engine.resize?.();
364
+ window.addEventListener('resize', resize);
365
+ engine.runRenderLoop?.(() => world.render());
366
+ state.babylon = babylon;
367
+ state.engine = engine;
368
+ state.world = world;
369
+ state.projection = projection;
370
+ state.gizmo = gizmo;
371
+ state.sceneViewInput = sceneViewInput;
372
+ state.sceneViewCamera = sceneViewCamera;
373
+ state.selectionController = selectionController;
374
+ state.resizeHandler = resize;
375
+ }
376
+ function disposeEditorWorld(state) {
377
+ if (state.resizeHandler) {
378
+ window.removeEventListener('resize', state.resizeHandler);
379
+ state.resizeHandler = null;
380
+ }
381
+ state.sceneViewCamera?.dispose();
382
+ state.sceneViewCamera = null;
383
+ state.sceneViewInput?.dispose();
384
+ state.sceneViewInput = null;
385
+ state.selectionController?.dispose();
386
+ state.selectionController = null;
387
+ state.boxSelection = null;
388
+ state.gizmo?.dispose();
389
+ state.gizmo = null;
390
+ state.projection?.dispose();
391
+ state.projection = null;
392
+ state.engine?.stopRenderLoop?.();
393
+ state.world?.dispose();
394
+ state.engine?.dispose?.();
395
+ state.babylon = null;
396
+ state.world = null;
397
+ state.engine = null;
398
+ }
399
+ async function runExclusive(state, render, action) {
400
+ if (state.busy)
401
+ return;
402
+ state.busy = true;
403
+ render();
404
+ try {
405
+ await action();
406
+ }
407
+ catch (error) {
408
+ state.status = error instanceof Error ? error.message : String(error);
409
+ console.error('[LocalEditorHarness] action failed', error);
410
+ }
411
+ finally {
412
+ state.busy = false;
413
+ render();
414
+ }
415
+ }
416
+ function selectItem(state, options, input) {
417
+ if (state.mode !== 'editor')
418
+ return false;
419
+ if (!isDocumentNodeSelectable(state, options, input.id))
420
+ return false;
421
+ const command = input.toggle
422
+ ? {
423
+ type: 'selection.toggle',
424
+ selectedIds: [input.id],
425
+ activeId: input.id,
426
+ label: 'Toggle Selection',
427
+ }
428
+ : input.additive
429
+ ? {
430
+ type: 'selection.add',
431
+ selectedIds: [input.id],
432
+ activeId: input.id,
433
+ label: 'Add Selection',
434
+ }
435
+ : {
436
+ type: 'selection.replace',
437
+ selectedIds: [input.id],
438
+ activeId: input.id,
439
+ label: 'Select Item',
440
+ };
441
+ return dispatchSelectionCommand(state, options, command);
442
+ }
443
+ function dispatchSelectionCommand(state, options, command) {
444
+ if (state.mode !== 'editor')
445
+ return false;
446
+ cancelActiveOperation(state);
447
+ const session = state.session;
448
+ if (!session)
449
+ return false;
450
+ const result = session.dispatch(command);
451
+ const sanitized = sanitizeSelection(state, options, result.workingDocument, result.selection);
452
+ const selection = sanitized ?? result.selection;
453
+ syncSelectionToProjection(state, selection);
454
+ return result.selectionChanged || !!sanitized;
455
+ }
456
+ function renameSceneGraphNode(state, options, intent) {
457
+ const document = state.session?.getState().workingDocument;
458
+ if (state.mode !== 'editor' || !state.session || !document)
459
+ return false;
460
+ cancelActiveOperation(state);
461
+ const validation = validateSceneGraphRename(options.documentAdapter.getHierarchyItems(document), intent);
462
+ if (!validation.ok) {
463
+ state.status = `Rename rejected: ${validation.reason ?? 'invalid scene graph rename'}`;
464
+ return true;
465
+ }
466
+ const patch = options.documentAdapter.createSceneGraphRenamePatch?.(document, intent);
467
+ if (!patch) {
468
+ state.status = `Rename rejected: ${intent.id}`;
469
+ return true;
470
+ }
471
+ const result = state.session.dispatch({
472
+ type: 'document.patch',
473
+ label: patch.label ?? `Rename ${intent.id}`,
474
+ patch: patch.patch,
475
+ targetId: intent.id,
476
+ });
477
+ if (!result.documentChanged) {
478
+ state.status = `Rename unchanged: ${intent.id}`;
479
+ return true;
480
+ }
481
+ rebuildProjectionFromDocument(state, options, result.workingDocument, result.selection);
482
+ state.summary = summarizeDocument(options, result.workingDocument, state.session.getSource());
483
+ state.status = patch.label ?? `Renamed ${intent.id}`;
484
+ return true;
485
+ }
486
+ function createSceneGraphGroup(state, options, intent) {
487
+ const document = state.session?.getState().workingDocument;
488
+ if (state.mode !== 'editor' || !state.session || !document)
489
+ return false;
490
+ cancelActiveOperation(state);
491
+ const patch = options.documentAdapter.createSceneGraphCreateGroupPatch?.(document, intent);
492
+ if (!patch) {
493
+ state.status = 'Create group rejected';
494
+ return true;
495
+ }
496
+ const result = state.session.dispatch({
497
+ type: 'document.patch',
498
+ label: patch.label ?? 'Create Empty Group',
499
+ patch: patch.patch,
500
+ targetId: patch.createdId ?? undefined,
501
+ });
502
+ if (!result.documentChanged) {
503
+ state.status = 'Create group unchanged';
504
+ return true;
505
+ }
506
+ const createdId = patch.createdId ?? null;
507
+ let selection = result.selection;
508
+ if (createdId && isNodeSelectableInDocument(options, result.workingDocument, createdId)) {
509
+ selection = state.session.dispatch({
510
+ type: 'selection.replace',
511
+ selectedIds: [createdId],
512
+ activeId: createdId,
513
+ label: 'Select Created Group',
514
+ }).selection;
515
+ }
516
+ rebuildProjectionFromDocument(state, options, result.workingDocument, selection);
517
+ state.summary = summarizeDocument(options, result.workingDocument, state.session.getSource());
518
+ state.status = patch.label ?? (createdId ? `Created group ${createdId}` : 'Created group');
519
+ return true;
520
+ }
521
+ function deleteSceneGraphNodes(state, options, intent) {
522
+ const document = state.session?.getState().workingDocument;
523
+ if (state.mode !== 'editor' || !state.session || !document)
524
+ return false;
525
+ cancelActiveOperation(state);
526
+ const validation = validateSceneGraphDelete(options.documentAdapter.getHierarchyItems(document), intent);
527
+ if (!validation.ok) {
528
+ state.status = `Delete rejected: ${validation.reason ?? 'invalid scene graph delete'}`;
529
+ return true;
530
+ }
531
+ const patch = options.documentAdapter.createSceneGraphDeletePatch?.(document, intent);
532
+ if (!patch) {
533
+ state.status = 'Delete rejected';
534
+ return true;
535
+ }
536
+ const result = state.session.dispatch({
537
+ type: 'document.patch',
538
+ label: patch.label ?? `Delete ${intent.ids.length} node(s)`,
539
+ patch: patch.patch,
540
+ targetId: intent.activeId ?? undefined,
541
+ });
542
+ if (!result.documentChanged) {
543
+ state.status = 'Delete unchanged';
544
+ return true;
545
+ }
546
+ const fallbackSelectionId = patch.fallbackSelectionId ?? null;
547
+ let selection = result.selection;
548
+ if (fallbackSelectionId && isNodeSelectableInDocument(options, result.workingDocument, fallbackSelectionId)) {
549
+ selection = state.session.dispatch({
550
+ type: 'selection.replace',
551
+ selectedIds: [fallbackSelectionId],
552
+ activeId: fallbackSelectionId,
553
+ label: 'Select Delete Fallback',
554
+ }).selection;
555
+ }
556
+ else {
557
+ selection = sanitizeSelection(state, options, result.workingDocument, selection) ?? selection;
558
+ }
559
+ rebuildProjectionFromDocument(state, options, result.workingDocument, selection);
560
+ state.summary = summarizeDocument(options, result.workingDocument, state.session.getSource());
561
+ state.status = patch.label ?? `Deleted ${patch.deletedIds?.length ?? intent.ids.length} node(s)`;
562
+ return true;
563
+ }
564
+ function dropSceneGraphNode(state, options, intent) {
565
+ const document = state.session?.getState().workingDocument;
566
+ if (state.mode !== 'editor' || !state.session || !document)
567
+ return false;
568
+ cancelActiveOperation(state);
569
+ if (intent.placement !== 'inside') {
570
+ state.status = 'Reparent rejected: sorting is not implemented yet';
571
+ return true;
572
+ }
573
+ const hierarchy = options.documentAdapter.getHierarchyItems(document);
574
+ const coreValidation = validateSceneGraphDrop(hierarchy, intent);
575
+ if (!coreValidation.ok) {
576
+ state.status = `Reparent rejected: ${coreValidation.reason ?? 'invalid scene graph drop'}`;
577
+ return true;
578
+ }
579
+ const projectValidation = options.documentAdapter.validateSceneGraphDrop?.(document, intent);
580
+ if (projectValidation && !projectValidation.ok) {
581
+ state.status = `Reparent rejected: ${projectValidation.reason ?? 'project validation failed'}`;
582
+ return true;
583
+ }
584
+ const patch = options.documentAdapter.createSceneGraphDropPatch?.(document, intent);
585
+ if (!patch) {
586
+ state.status = 'Reparent rejected';
587
+ return true;
588
+ }
589
+ const result = state.session.dispatch({
590
+ type: 'document.patch',
591
+ label: patch.label ?? `Reparent ${intent.draggedId}`,
592
+ patch: patch.patch,
593
+ targetId: intent.draggedId,
594
+ });
595
+ if (!result.documentChanged) {
596
+ state.status = `Reparent unchanged: ${intent.draggedId}`;
597
+ return true;
598
+ }
599
+ rebuildProjectionFromDocument(state, options, result.workingDocument, result.selection);
600
+ state.summary = summarizeDocument(options, result.workingDocument, state.session.getSource());
601
+ state.status = patch.label ?? `Reparented ${intent.draggedId}`;
602
+ return true;
603
+ }
604
+ function sanitizeSelection(state, options, document, selection) {
605
+ const selectedIds = selection.selectedIds.filter(id => isNodeSelectableInDocument(options, document, id));
606
+ const activeId = selection.activeId && selectedIds.includes(selection.activeId)
607
+ ? selection.activeId
608
+ : selectedIds[selectedIds.length - 1] ?? null;
609
+ if (activeId === selection.activeId && selectedIds.length === selection.selectedIds.length)
610
+ return null;
611
+ const result = state.session?.dispatch({
612
+ type: 'selection.replace',
613
+ selectedIds,
614
+ activeId,
615
+ label: 'Sanitize Selection',
616
+ });
617
+ return result?.selection ?? { selectedIds, activeId };
618
+ }
619
+ function syncSelectionToProjection(state, selection) {
620
+ state.projection?.syncSelection(selection);
621
+ state.gizmo?.setSelection(selection);
622
+ state.gizmo?.refreshSelection();
623
+ }
624
+ function addAssetToDocument(state, options, assetId) {
625
+ if (state.mode !== 'editor')
626
+ return false;
627
+ cancelActiveOperation(state);
628
+ const session = state.session;
629
+ const beforeDocument = session?.getState().workingDocument;
630
+ if (!session || !beforeDocument)
631
+ return false;
632
+ const asset = state.assets.find(candidate => resolveAssetId(options, candidate) === assetId);
633
+ if (!asset)
634
+ return false;
635
+ const patch = options.documentAdapter.createPatchFromAsset(asset);
636
+ const result = session.dispatch({
637
+ type: 'document.patch',
638
+ label: patch.label ?? 'Create Object From Asset',
639
+ patch: patch.patch,
640
+ });
641
+ const createdId = options.documentAdapter.findCreatedId?.(beforeDocument, result.workingDocument);
642
+ let selectionResult = null;
643
+ if (createdId) {
644
+ selectionResult = session.dispatch({
645
+ type: 'selection.replace',
646
+ selectedIds: [createdId],
647
+ activeId: createdId,
648
+ label: 'Select Created Item',
649
+ });
650
+ }
651
+ state.summary = summarizeDocument(options, result.workingDocument, session.getSource());
652
+ state.status = `Added ${assetId}`;
653
+ if (createdId) {
654
+ const projectedNode = options.documentAdapter.getProjectionNode(result.workingDocument, createdId);
655
+ if (projectedNode) {
656
+ state.projection?.projectNode(projectedNode);
657
+ if (selectionResult)
658
+ syncSelectionToProjection(state, selectionResult.selection);
659
+ }
660
+ }
661
+ return true;
662
+ }
663
+ function patchSerializedProperty(state, options, input) {
664
+ if (state.mode !== 'editor')
665
+ return false;
666
+ cancelActiveOperation(state);
667
+ if (!state.session)
668
+ return false;
669
+ const document = state.session.getState().workingDocument;
670
+ const targetIds = input.targetIds && input.targetIds.length > 0 ? input.targetIds : [input.targetId];
671
+ if (targetIds.length > 1) {
672
+ const patch = options.documentAdapter.createSerializedMultiPropertyPatch?.({
673
+ document,
674
+ targetIds,
675
+ activeId: state.session.getState().selection.activeId,
676
+ path: input.path,
677
+ value: input.value,
678
+ });
679
+ if (!patch)
680
+ return false;
681
+ const result = state.session.dispatch({
682
+ type: 'document.patch',
683
+ label: patch.label ?? `Patch ${input.path} on ${targetIds.length} objects`,
684
+ patch: patch.patch,
685
+ targetId: state.session.getState().selection.activeId ?? undefined,
686
+ });
687
+ if (!result.documentChanged)
688
+ return false;
689
+ const changedIds = patch.changedIds ?? targetIds;
690
+ const workingDocument = result.workingDocument;
691
+ syncProjectionForChangedIds(state, options, workingDocument, changedIds);
692
+ state.summary = summarizeDocument(options, workingDocument, state.session.getSource());
693
+ state.status = patch.label ?? `Patch ${input.path} on ${targetIds.length} objects`;
694
+ return true;
695
+ }
696
+ const patch = options.documentAdapter.createSerializedPropertyPatch({
697
+ ...input,
698
+ document,
699
+ });
700
+ if (!patch)
701
+ return false;
702
+ const result = state.session.dispatch({
703
+ type: 'document.patch',
704
+ label: patch.label ?? `Patch ${input.path}`,
705
+ patch: patch.patch,
706
+ });
707
+ if (patch.changedIds)
708
+ syncProjectionForChangedIds(state, options, result.workingDocument, patch.changedIds);
709
+ else
710
+ syncProjectionForDispatchResult(state, options, result, patch.changedId ?? input.targetId);
711
+ state.summary = summarizeDocument(options, result.workingDocument, state.session.getSource());
712
+ state.status = patch.label ?? `Patched ${input.path}`;
713
+ return true;
714
+ }
715
+ function commitGizmoTransform(state, options, event) {
716
+ if (state.mode !== 'editor' || !state.session)
717
+ return false;
718
+ const document = state.session.getState().workingDocument;
719
+ if (isTransformBatchCommit(event)) {
720
+ const patch = options.documentAdapter.createTransformBatchPatch?.({
721
+ ...event,
722
+ document,
723
+ });
724
+ if (!patch) {
725
+ restoreBatchTransformPreview(state, event.targets);
726
+ state.status = `Ignored ${event.tool} ${event.targetIds.length} objects`;
727
+ return false;
728
+ }
729
+ const result = state.session.dispatch({
730
+ type: 'document.patch',
731
+ label: patch.label ?? `${event.tool} ${event.targetIds.length} objects`,
732
+ patch: patch.patch,
733
+ targetId: event.activeId ?? undefined,
734
+ });
735
+ syncProjectionForDispatchResult(state, options, result);
736
+ syncProjectionForChangedIds(state, options, result.workingDocument, patch.changedIds ?? event.targetIds);
737
+ state.summary = summarizeDocument(options, result.workingDocument, state.session.getSource());
738
+ state.status = patch.label ?? `${event.tool} ${event.targetIds.length} objects`;
739
+ return result.documentChanged;
740
+ }
741
+ const patch = options.documentAdapter.createTransformPatch?.({
742
+ document,
743
+ targetId: event.nodeId,
744
+ tool: event.tool,
745
+ space: event.space,
746
+ before: event.before,
747
+ after: event.after,
748
+ });
749
+ if (!patch) {
750
+ state.projection?.setNodeTransformPreview(event.nodeId, event.before);
751
+ state.status = `Ignored ${event.tool} ${event.nodeId}`;
752
+ return false;
753
+ }
754
+ const result = state.session.dispatch({
755
+ type: 'document.patch',
756
+ label: patch.label ?? `${event.tool} ${event.nodeId}`,
757
+ patch: patch.patch,
758
+ targetId: event.nodeId,
759
+ });
760
+ if (patch.changedIds)
761
+ syncProjectionForChangedIds(state, options, result.workingDocument, patch.changedIds);
762
+ else
763
+ syncProjectionForDispatchResult(state, options, result, patch.changedId ?? event.nodeId);
764
+ state.summary = summarizeDocument(options, result.workingDocument, state.session.getSource());
765
+ state.status = patch.label ?? `${event.tool} ${event.nodeId}`;
766
+ return result.documentChanged;
767
+ }
768
+ function isTransformBatchCommit(event) {
769
+ return 'targets' in event;
770
+ }
771
+ function restoreBatchTransformPreview(state, targets) {
772
+ const transforms = {};
773
+ for (const target of targets)
774
+ transforms[target.id] = target.before;
775
+ state.projection?.setNodeTransformsPreview(transforms);
776
+ }
777
+ function cancelActiveGizmoDrag(state) {
778
+ state.gizmo?.cancelDrag();
779
+ }
780
+ function cancelActiveOperation(state) {
781
+ state.sceneViewInput?.cancelActiveIntent();
782
+ state.selectionController?.cancelBoxSelection();
783
+ cancelActiveGizmoDrag(state);
784
+ }
785
+ function focusSelectedProjection(state) {
786
+ if (state.mode !== 'editor')
787
+ return false;
788
+ const activeId = state.session?.getState().selection.activeId ?? null;
789
+ if (!activeId) {
790
+ state.status = 'Focus failed: no selection';
791
+ return true;
792
+ }
793
+ return focusProjectionNode(state, activeId);
794
+ }
795
+ function focusProjectionNode(state, nodeId) {
796
+ if (state.mode !== 'editor')
797
+ return false;
798
+ const root = state.projection?.getAttachableRoot(nodeId) ?? null;
799
+ if (!root) {
800
+ state.status = `Focus failed: missing projection for ${nodeId}`;
801
+ return true;
802
+ }
803
+ const focused = focusEditorViewportSelection(state.world?.camera ?? null, root, {
804
+ babylon: state.babylon ?? undefined,
805
+ });
806
+ state.status = focused
807
+ ? `Focused ${nodeId} · ${formatEditorStatusTime(Date.now())}`
808
+ : `Focus failed: camera could not frame ${nodeId}`;
809
+ return true;
810
+ }
811
+ function formatEditorStatusTime(timestamp) {
812
+ const date = new Date(timestamp);
813
+ const pad = (value) => String(value).padStart(2, '0');
814
+ return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
815
+ }
816
+ function undoSessionChange(state, options) {
817
+ cancelActiveOperation(state);
818
+ const result = state.session?.undo();
819
+ if (!result)
820
+ return false;
821
+ rebuildProjection(state, options, result);
822
+ state.summary = summarizeDocument(options, result.workingDocument, state.session?.getSource());
823
+ state.status = `Undo ${result.transaction.label}`;
824
+ return true;
825
+ }
826
+ function redoSessionChange(state, options) {
827
+ cancelActiveGizmoDrag(state);
828
+ const result = state.session?.redo();
829
+ if (!result)
830
+ return false;
831
+ rebuildProjection(state, options, result);
832
+ state.summary = summarizeDocument(options, result.workingDocument, state.session?.getSource());
833
+ state.status = `Redo ${result.transaction.label}`;
834
+ return true;
835
+ }
836
+ function rebuildProjection(state, options, result) {
837
+ rebuildProjectionFromDocument(state, options, result.workingDocument, result.selection);
838
+ }
839
+ function rebuildProjectionFromDocument(state, options, document, selection) {
840
+ state.projection?.rebuild(options.documentAdapter.getProjectionNodes(document));
841
+ const sanitized = sanitizeSelection(state, options, document, selection);
842
+ syncSelectionToProjection(state, sanitized ?? selection);
843
+ }
844
+ function syncProjectionForDispatchResult(state, options, result, changedId) {
845
+ if (result.documentChanged && changedId && result.workingDocument) {
846
+ const projectedNode = options.documentAdapter.getProjectionNode(result.workingDocument, changedId);
847
+ if (projectedNode)
848
+ state.projection?.syncNodeTransform(projectedNode);
849
+ }
850
+ if (result.selectionChanged) {
851
+ syncSelectionToProjection(state, result.selection ?? { selectedIds: [], activeId: null });
852
+ }
853
+ else if (result.documentChanged) {
854
+ const selection = state.session?.getState().selection ?? { selectedIds: [], activeId: null };
855
+ const sanitized = result.workingDocument ? sanitizeSelection(state, options, result.workingDocument, selection) : null;
856
+ syncSelectionToProjection(state, sanitized ?? selection);
857
+ }
858
+ }
859
+ function syncProjectionForChangedIds(state, options, document, changedIds) {
860
+ for (const changedId of changedIds) {
861
+ const projectedNode = options.documentAdapter.getProjectionNode(document, changedId);
862
+ if (projectedNode)
863
+ state.projection?.syncNodeTransform(projectedNode);
864
+ }
865
+ const selection = state.session?.getState().selection ?? { selectedIds: [], activeId: null };
866
+ syncSelectionToProjection(state, selection);
867
+ }
868
+ function createUiState(state, options) {
869
+ const sessionState = state.session?.getState();
870
+ const document = sessionState?.workingDocument ?? null;
871
+ const selectedIds = sessionState?.selection.selectedIds ?? [];
872
+ const activeId = sessionState?.selection.activeId ?? null;
873
+ return {
874
+ mode: state.mode,
875
+ busy: state.busy,
876
+ status: state.status,
877
+ summary: state.summary,
878
+ assetFilter: state.assetFilter,
879
+ assets: state.assets
880
+ .filter(asset => asset.placeable !== false)
881
+ .map(asset => toBrowserAssetItem(options, asset)),
882
+ assetCountLabel: `${state.assets.length} assets`,
883
+ hierarchy: document ? options.documentAdapter.getHierarchyItems(document) : [],
884
+ selectedIds,
885
+ activeId,
886
+ selectionSummary: {
887
+ count: selectedIds.length,
888
+ activeId,
889
+ },
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,
896
+ boxSelection: state.boxSelection,
897
+ transformTool: {
898
+ activeTool: state.gizmo?.getState().tool ?? state.transformTool,
899
+ activeSpace: state.gizmo?.getState().space ?? state.transformSpace,
900
+ activeConstraint: state.gizmo?.getState().constraint ?? state.transformConstraint,
901
+ dragPhase: state.gizmo?.getState().dragPhase ?? 'idle',
902
+ draggingNodeId: state.gizmo?.getState().draggingNodeId ?? null,
903
+ },
904
+ session: sessionState
905
+ ? {
906
+ source: sessionState.source,
907
+ dirty: sessionState.dirty,
908
+ canUndo: sessionState.canUndo,
909
+ canRedo: sessionState.canRedo,
910
+ history: sessionState.history,
911
+ }
912
+ : null,
913
+ };
914
+ }
915
+ function summarizeDocument(options, document, _source) {
916
+ return options.documentAdapter.summarize?.(document) ?? '';
917
+ }
918
+ function summarizeAuthoringFailure(result) {
919
+ const diagnostic = result.diagnostics.find(item => item.severity === 'error') ?? result.diagnostics[0];
920
+ return diagnostic?.message ?? result.reason ?? 'Authoring source commit failed';
921
+ }
922
+ function summarizeDiagnostics(diagnostics) {
923
+ if (!diagnostics?.length)
924
+ return undefined;
925
+ return diagnostics.map(diagnostic => diagnostic.message).join('; ');
926
+ }
927
+ async function loadDocumentFallback(options) {
928
+ if (!options.persistenceAdapter.loadDocument) {
929
+ throw new Error('LocalEditorHarness requires loadAuthoringSource or loadDocument.');
930
+ }
931
+ return options.persistenceAdapter.loadDocument();
932
+ }
933
+ async function saveDocumentFallback(options, document) {
934
+ if (!options.persistenceAdapter.saveDocument) {
935
+ throw new Error('LocalEditorHarness requires saveAuthoringSource or saveDocument.');
936
+ }
937
+ return options.persistenceAdapter.saveDocument(document);
938
+ }
939
+ function isDocumentNodeSelectable(state, options, id) {
940
+ const document = state.session?.getState().workingDocument;
941
+ if (!document)
942
+ return false;
943
+ return isNodeSelectableInDocument(options, document, id);
944
+ }
945
+ function isDocumentNodeLocked(state, options, id) {
946
+ const document = state.session?.getState().workingDocument;
947
+ if (!document)
948
+ return false;
949
+ return options.documentAdapter.isLocked?.(document, id) ?? false;
950
+ }
951
+ function isNodeSelectableInDocument(options, document, id) {
952
+ if (options.documentAdapter.isLocked?.(document, id))
953
+ return false;
954
+ return options.documentAdapter.isSelectable?.(document, id) ?? true;
955
+ }
956
+ function resolveAssetId(options, asset) {
957
+ return options.worldAdapter.resolveAssetId?.(asset)
958
+ ?? asset.id;
959
+ }
960
+ function toBrowserAssetItem(options, asset) {
961
+ return options.worldAdapter.toBrowserAssetItem?.(asset)
962
+ ?? {
963
+ id: asset.id,
964
+ label: asset.label,
965
+ meta: asset.meta,
966
+ disabled: asset.placeable === false,
967
+ };
968
+ }
969
+ //# sourceMappingURL=local-editor-harness.js.map