@volter/editor-game 0.5.65

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 (302) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +23 -0
  3. package/contributions/asset-budget-asset.menu.ts +26 -0
  4. package/contributions/asset-budget.action.ts +18 -0
  5. package/contributions/asset-budget.document.tsx +21 -0
  6. package/contributions/asset-budget.menu.ts +22 -0
  7. package/contributions/audio-unlock.service.ts +17 -0
  8. package/contributions/audio.utility.tsx +15 -0
  9. package/contributions/autoplay.service.ts +48 -0
  10. package/contributions/bridge.command.ts +172 -0
  11. package/contributions/build-profiles.document.tsx +21 -0
  12. package/contributions/build-progress.status.tsx +45 -0
  13. package/contributions/build.action.ts +26 -0
  14. package/contributions/build.command.ts +27 -0
  15. package/contributions/build.header.tsx +37 -0
  16. package/contributions/build.menu.ts +31 -0
  17. package/contributions/build.service.ts +31 -0
  18. package/contributions/connection.status.tsx +43 -0
  19. package/contributions/coverage.service.ts +118 -0
  20. package/contributions/edit-mode-audio.service.ts +23 -0
  21. package/contributions/edit-mode-networking.service.ts +30 -0
  22. package/contributions/game-document.service.ts +25 -0
  23. package/contributions/game-eval.command.ts +210 -0
  24. package/contributions/game.layout.ts +19 -0
  25. package/contributions/gameplay.command.ts +255 -0
  26. package/contributions/generation.service.ts +96 -0
  27. package/contributions/generations.status.tsx +53 -0
  28. package/contributions/ingest.service.ts +80 -0
  29. package/contributions/instances.command.ts +75 -0
  30. package/contributions/navmesh.menu.ts +39 -0
  31. package/contributions/navmesh.service.ts +19 -0
  32. package/contributions/network.utility.tsx +17 -0
  33. package/contributions/play.command.ts +355 -0
  34. package/contributions/profiler.action.ts +16 -0
  35. package/contributions/profiler.menu.ts +22 -0
  36. package/contributions/profiler.utility.tsx +17 -0
  37. package/contributions/react/component-board.service.ts +21 -0
  38. package/contributions/react/design-time-mount.service.ts +60 -0
  39. package/contributions/react/pasteboard.action.ts +41 -0
  40. package/contributions/react/react-inspector.service.ts +85 -0
  41. package/contributions/react/story-documents.service.ts +44 -0
  42. package/contributions/scene-document.service.ts +32 -0
  43. package/contributions/state-watch.action.ts +17 -0
  44. package/contributions/state-watch.menu.ts +23 -0
  45. package/contributions/state-watch.utility.tsx +20 -0
  46. package/contributions/team-playtest.service.ts +124 -0
  47. package/contributions/three/camera-runtime.inspector.tsx +34 -0
  48. package/contributions/three/component-board.service.ts +24 -0
  49. package/contributions/three/component-verbs.command.ts +110 -0
  50. package/contributions/three/component-verbs.service.ts +92 -0
  51. package/contributions/three/constraints.inspector.tsx +34 -0
  52. package/contributions/three/model-asset-sections.service.ts +26 -0
  53. package/contributions/three/reflection-probe-capture.inspector.tsx +32 -0
  54. package/contributions/three/story-documents.service.ts +31 -0
  55. package/contributions/three/three-authoring.service.ts +66 -0
  56. package/contributions/transport.header.tsx +19 -0
  57. package/contributions/xstate-behavior.action.ts +42 -0
  58. package/contributions/xstate-behavior.document.tsx +73 -0
  59. package/contributions/xstate-behavior.inspector.tsx +28 -0
  60. package/contributions/xstate-behavior.menu.ts +23 -0
  61. package/package.json +144 -0
  62. package/src/asset-budget/AssetBudgetPanel.tsx +1172 -0
  63. package/src/asset-budget/asset-budget-model.ts +799 -0
  64. package/src/asset-budget/basis-encoder.ts +165 -0
  65. package/src/asset-budget/gltf-io.ts +154 -0
  66. package/src/asset-budget/gltf-optimize.ts +384 -0
  67. package/src/asset-budget/image-dims.ts +78 -0
  68. package/src/asset-budget/optimize-apply.ts +221 -0
  69. package/src/audio/AudioDebuggerPanel.tsx +457 -0
  70. package/src/audio/audio-debugger-model.ts +87 -0
  71. package/src/bridge/call.ts +60 -0
  72. package/src/bridge/dispatch.ts +459 -0
  73. package/src/bridge/live-frames.ts +25 -0
  74. package/src/bridge/screenshot.ts +316 -0
  75. package/src/build/BuildProfilesPanel.tsx +476 -0
  76. package/src/build/build-session.ts +247 -0
  77. package/src/build/format-bytes.ts +14 -0
  78. package/src/command-results.ts +36 -0
  79. package/src/coverage/live-authoring-surface.ts +30 -0
  80. package/src/coverage/live-project-verbs.ts +162 -0
  81. package/src/coverage/native-system-coverage.ts +143 -0
  82. package/src/coverage/root-coverage.ts +79 -0
  83. package/src/coverage/session-coverage.ts +193 -0
  84. package/src/design-system-stories/ApplicationChrome.stories.tsx +100 -0
  85. package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +406 -0
  86. package/src/edit-mode/edit-mode-audio.ts +56 -0
  87. package/src/edit-mode/edit-mode-networking.ts +112 -0
  88. package/src/game-document/DevicePresetPicker.tsx +84 -0
  89. package/src/game-document/GameCaptureFrameButton.tsx +45 -0
  90. package/src/game-document/GameDocument.tsx +215 -0
  91. package/src/game-document/GamePanel.tsx +662 -0
  92. package/src/game-document/InstanceInspectorPicker.tsx +183 -0
  93. package/src/game-document/crowd-debug.ts +183 -0
  94. package/src/game-document/device-preview.ts +336 -0
  95. package/src/game-document/game-view-store.ts +107 -0
  96. package/src/game-document/physics-debug.ts +187 -0
  97. package/src/generation/GenerationActivity.tsx +421 -0
  98. package/src/generation/GenerationGallery.css +231 -0
  99. package/src/generation/generation-documents.tsx +338 -0
  100. package/src/generation/generation-jobs.ts +128 -0
  101. package/src/generation/generation-presentation.ts +257 -0
  102. package/src/host/adapter-reach.ts +445 -0
  103. package/src/host/adapter-runtime-bindings.ts +303 -0
  104. package/src/host/after-paint.ts +112 -0
  105. package/src/host/api/configurations.ts +66 -0
  106. package/src/host/authoring/babylon-authoring-adapter.ts +703 -0
  107. package/src/host/authoring/canvas-runtime-recognition.ts +50 -0
  108. package/src/host/authoring/contract-hierarchy-authoring.ts +144 -0
  109. package/src/host/authoring/contract-scenes-stories.ts +204 -0
  110. package/src/host/authoring/creation-site-related.ts +55 -0
  111. package/src/host/authoring/ephemeral-persistence.ts +29 -0
  112. package/src/host/authoring/gesture-persist.ts +84 -0
  113. package/src/host/authoring/ingest-data-writer.ts +232 -0
  114. package/src/host/authoring/ingest-source-persistence.ts +826 -0
  115. package/src/host/authoring/mount-isolated-pixi-screen.ts +250 -0
  116. package/src/host/authoring/mounted-authoring.ts +41 -0
  117. package/src/host/authoring/owned-pixi-ticker-listeners.ts +96 -0
  118. package/src/host/authoring/phaser-live-authoring-adapter.ts +265 -0
  119. package/src/host/authoring/pixi-authoring-adapter.ts +1554 -0
  120. package/src/host/authoring/pixi-creation-site-write-target.ts +60 -0
  121. package/src/host/authoring/pixi-isolation-assets.ts +25 -0
  122. package/src/host/authoring/pixi-live-write-target.ts +979 -0
  123. package/src/host/authoring/pixi-source-identity.ts +141 -0
  124. package/src/host/authoring/pixi-still-presentation.ts +71 -0
  125. package/src/host/authoring/pixi-structure-history.ts +237 -0
  126. package/src/host/authoring/pixi-transform-channels.ts +205 -0
  127. package/src/host/authoring/selection-remount-handoff.ts +23 -0
  128. package/src/host/authoring/source-persistence-backend.ts +373 -0
  129. package/src/host/authoring/source-refresh-revisions.ts +81 -0
  130. package/src/host/authoring/struct-write-pipe.ts +143 -0
  131. package/src/host/auto-frame-window.ts +89 -0
  132. package/src/host/binding-resolver.ts +393 -0
  133. package/src/host/browser-transpile.ts +631 -0
  134. package/src/host/canvas-entry-runtime.ts +95 -0
  135. package/src/host/components/CameraAuthoringOverlay.tsx +216 -0
  136. package/src/host/components/HeaderTelemetry.tsx +341 -0
  137. package/src/host/components/PixiIsolationSceneContent.tsx +280 -0
  138. package/src/host/components/ResolutionPicker.tsx +89 -0
  139. package/src/host/components/ThreeIsolationSceneContent.tsx +180 -0
  140. package/src/host/components/frame-debugger-model.ts +579 -0
  141. package/src/host/components/header-telemetry-model.ts +74 -0
  142. package/src/host/components/scene-document.tsx +447 -0
  143. package/src/host/components/utility-view-state.ts +87 -0
  144. package/src/host/components/world-root-stage-binding.tsx +133 -0
  145. package/src/host/components/world-root-stage.ts +1046 -0
  146. package/src/host/coverage/authoring-read-probe.ts +583 -0
  147. package/src/host/coverage/capability-coverage.ts +1587 -0
  148. package/src/host/coverage/coverage-accounting.ts +261 -0
  149. package/src/host/coverage/game-contract-seam-evidence.ts +82 -0
  150. package/src/host/coverage/project-verb-coverage.ts +148 -0
  151. package/src/host/coverage/system-adapter-coverage.ts +373 -0
  152. package/src/host/design-system-stories/StoryLayout.tsx +104 -0
  153. package/src/host/design-system-stories/fixtures/authoring.ts +247 -0
  154. package/src/host/design-system-stories/fixtures/editor-runtime.tsx +115 -0
  155. package/src/host/document-preview-three.ts +110 -0
  156. package/src/host/entry-adjudication.ts +89 -0
  157. package/src/host/game-location-guard.ts +150 -0
  158. package/src/host/game-module-access.ts +196 -0
  159. package/src/host/game-realm-page.ts +358 -0
  160. package/src/host/game-realm-reclaim.ts +55 -0
  161. package/src/host/game-realm-storage.ts +104 -0
  162. package/src/host/gameplay-export.ts +288 -0
  163. package/src/host/gameplay-recording.ts +717 -0
  164. package/src/host/gated-globals.ts +1511 -0
  165. package/src/host/history/json-history-resource.ts +254 -0
  166. package/src/host/ingest/registry.ts +261 -0
  167. package/src/host/instance-extract-actions.ts +120 -0
  168. package/src/host/instance-fork-actions.ts +120 -0
  169. package/src/host/play-control-hook.ts +26 -0
  170. package/src/host/playwright-shim.ts +479 -0
  171. package/src/host/projection/dom.ts +295 -0
  172. package/src/host/projection/pixi.ts +288 -0
  173. package/src/host/r3f-entry-runtime.ts +77 -0
  174. package/src/host/react-mount-runtime.ts +162 -0
  175. package/src/host/realm-services.ts +148 -0
  176. package/src/host/recording-preview.ts +106 -0
  177. package/src/host/roots/module-root.ts +203 -0
  178. package/src/host/roots/react-root.ts +181 -0
  179. package/src/host/same-realm-loop-gate.ts +544 -0
  180. package/src/host/scene-view-drawability.ts +69 -0
  181. package/src/host/sdk/tools.ts +31 -0
  182. package/src/host/served-bundle-runtime-modules.ts +331 -0
  183. package/src/host/server-log-bridge.ts +60 -0
  184. package/src/host/staged-projects.ts +24 -0
  185. package/src/host/stories/mounted-story-viewport-source.ts +64 -0
  186. package/src/host/stories/story-arg-descriptors.ts +50 -0
  187. package/src/host/stories/story-media-presence.ts +152 -0
  188. package/src/host/surface-content.ts +87 -0
  189. package/src/host/take-named-export.ts +23 -0
  190. package/src/host/three-ingest-runtime.ts +76 -0
  191. package/src/host/types-fastnoise-lite.d.ts +7 -0
  192. package/src/host/types-mikktspace.d.ts +20 -0
  193. package/src/host/types-troika-three-text.d.ts +7 -0
  194. package/src/host/use-active-performance-source.ts +53 -0
  195. package/src/host/viewport-pose-memory.ts +48 -0
  196. package/src/host/viewport-root-presentation.ts +40 -0
  197. package/src/ingest/active-ingest.ts +251 -0
  198. package/src/ingest/active-scene-navigation.ts +43 -0
  199. package/src/ingest/authoring/ingest-dom-surface-authoring.ts +191 -0
  200. package/src/ingest/authoring/ingest-root-adapter.ts +897 -0
  201. package/src/ingest/capture-wait-report.ts +122 -0
  202. package/src/ingest/deferred-ingest-play.ts +216 -0
  203. package/src/ingest/deferred-ingest-session.ts +23 -0
  204. package/src/ingest/discovery-public-ingest.ts +242 -0
  205. package/src/ingest/dom-stub-mark.ts +7 -0
  206. package/src/ingest/entry-load.ts +56 -0
  207. package/src/ingest/game-contract-realm.ts +34 -0
  208. package/src/ingest/game-pointer-lock.ts +89 -0
  209. package/src/ingest/held-scene-repaint.ts +73 -0
  210. package/src/ingest/host-surface-box.ts +174 -0
  211. package/src/ingest/ingest-boot-viewport.ts +54 -0
  212. package/src/ingest/ingest-canvas-scene-document.tsx +177 -0
  213. package/src/ingest/ingest-canvas-scene.ts +50 -0
  214. package/src/ingest/ingest-evidence-hook.ts +131 -0
  215. package/src/ingest/ingest-frame-snapshot.ts +183 -0
  216. package/src/ingest/ingest-play-commands.ts +134 -0
  217. package/src/ingest/ingest-play-control.ts +299 -0
  218. package/src/ingest/ingest-render-debug.ts +320 -0
  219. package/src/ingest/ingest-siblings.ts +487 -0
  220. package/src/ingest/ingest-status.ts +62 -0
  221. package/src/ingest/live-ingest-facet.ts +46 -0
  222. package/src/ingest/module-mode.ts +229 -0
  223. package/src/ingest/mount-canvas-ingest-root.ts +899 -0
  224. package/src/ingest/mount-coverage.ts +296 -0
  225. package/src/ingest/mount-dom-ingest-root.ts +282 -0
  226. package/src/ingest/mount-ingest-root.ts +744 -0
  227. package/src/ingest/mount-three-ingest-root.ts +366 -0
  228. package/src/ingest/resolve-canvas.ts +47 -0
  229. package/src/ingest/resolve-three.ts +123 -0
  230. package/src/ingest/served-bundle.ts +109 -0
  231. package/src/ingest/served-html-boot.ts +292 -0
  232. package/src/ingest/surface-canvas.ts +84 -0
  233. package/src/ingest/surface-dom.ts +84 -0
  234. package/src/ingest/surface-three.ts +128 -0
  235. package/src/ingest/types.ts +76 -0
  236. package/src/ingest/unmount-ingest-root.ts +224 -0
  237. package/src/navmesh/navmesh-actions.ts +27 -0
  238. package/src/navmesh/navmesh-handler.ts +237 -0
  239. package/src/navmesh/navmesh-workflow-store.ts +78 -0
  240. package/src/network/NetworkInspectorPanel.tsx +644 -0
  241. package/src/network/network-inspector-model.ts +225 -0
  242. package/src/play/play-boot-stall.ts +118 -0
  243. package/src/play/play-log-events.ts +26 -0
  244. package/src/play/play-mode.ts +2553 -0
  245. package/src/play/play-recording.ts +335 -0
  246. package/src/play/react-play-live-authoring.ts +165 -0
  247. package/src/play-bar/PlayBar.tsx +488 -0
  248. package/src/play-bar/PlayerCountPicker.tsx +100 -0
  249. package/src/profiler/FrameDebuggerPanel.tsx +458 -0
  250. package/src/profiler/PerformancePanel.tsx +1068 -0
  251. package/src/profiler/ProfilerPanel.tsx +53 -0
  252. package/src/profiler/frame-debugger-store.ts +97 -0
  253. package/src/profiler/main-thread-busy.ts +90 -0
  254. package/src/react/design-time-react-mount.ts +782 -0
  255. package/src/react/dom-authoring-adapter.ts +764 -0
  256. package/src/react/pasteboard-materialize.ts +154 -0
  257. package/src/react/react-inspector-section.tsx +2496 -0
  258. package/src/react/react-world-authoring-adapter.ts +3795 -0
  259. package/src/react/story-documents/story-args-section.ts +19 -0
  260. package/src/react/story-documents/story-documents.tsx +1380 -0
  261. package/src/react/story-paint-bounds.ts +94 -0
  262. package/src/react/ui-board-document.tsx +101 -0
  263. package/src/react/ui-board-title.ts +9 -0
  264. package/src/react/ui-component-board.ts +72 -0
  265. package/src/services/audio-pose-guard.ts +81 -0
  266. package/src/services/game-audio-unlock.ts +48 -0
  267. package/src/state-watch/StateWatchPanel.tsx +535 -0
  268. package/src/three/authoring/camera-runtime-inspector-section.tsx +135 -0
  269. package/src/three/authoring/constraint-inspector-section.tsx +189 -0
  270. package/src/three/authoring/design-time-renderer.ts +189 -0
  271. package/src/three/authoring/model-asset-inspector-section.css +41 -0
  272. package/src/three/authoring/model-asset-inspector-section.tsx +869 -0
  273. package/src/three/authoring/oid-source-persistence.ts +557 -0
  274. package/src/three/authoring/r3f-design-session.ts +1174 -0
  275. package/src/three/authoring/r3f-source-authoring-adapter.ts +5949 -0
  276. package/src/three/authoring/reflection-probe-inspector-section.tsx +76 -0
  277. package/src/three/authoring/spatial-audio-handles.ts +215 -0
  278. package/src/three/authoring/spatial-collider-handles.ts +229 -0
  279. package/src/three/authoring/spatial-joint-handles.ts +114 -0
  280. package/src/three/authoring/spatial-light-handles.ts +178 -0
  281. package/src/three/authoring/spatial-lod-handles.ts +93 -0
  282. package/src/three/authoring/spatial-particle-handles.ts +368 -0
  283. package/src/three/authoring/three-authoring-adapter.ts +1953 -0
  284. package/src/three/authoring/three-scene-identity.ts +19 -0
  285. package/src/three/authoring/three-spatial-handles.ts +161 -0
  286. package/src/three/authoring/typed-three-inspector.ts +528 -0
  287. package/src/three/component-verbs/extract-menu.ts +71 -0
  288. package/src/three/component-verbs/fork-menu.ts +78 -0
  289. package/src/three/component-verbs/internals-menu.ts +91 -0
  290. package/src/three/story-documents/three-story-documents.tsx +607 -0
  291. package/src/three/three-board/ThreeBoardDocument.tsx +888 -0
  292. package/src/three/three-board/board-framing.ts +412 -0
  293. package/src/three/three-board/board-layout.ts +401 -0
  294. package/src/three/three-board/board-scene.ts +901 -0
  295. package/src/three/three-board/three-component-board.ts +62 -0
  296. package/src/xstate/XStateBehaviorSection.tsx +130 -0
  297. package/src/xstate/XStateMachineInspector.tsx +566 -0
  298. package/src/xstate/character-animation-machine.fixture.ts +74 -0
  299. package/src/xstate/live-behaviors.ts +106 -0
  300. package/src/xstate/use-live-actor-state.ts +44 -0
  301. package/src/xstate/xstate-graph.ts +235 -0
  302. package/src/xstate/xstate-layout.ts +76 -0
@@ -0,0 +1,703 @@
1
+ /**
2
+ * Babylon.js authoring over the engine's native scene graph.
3
+ *
4
+ * Babylon stays the entity model. This adapter imports no Babylon package: it
5
+ * walks the engine's public `scenes` / `rootNodes` / `getChildren()` surface
6
+ * and writes the native Node transform and visibility fields directly.
7
+ */
8
+
9
+ import type {
10
+ AuthoringAdapter,
11
+ AuthoringCapabilities,
12
+ AuthoringProvenance,
13
+ EditorNode,
14
+ PickProvider,
15
+ PropertyDescriptor,
16
+ Transform,
17
+ WriteAck,
18
+ } from '@volter/editor-project/adapter';
19
+ import type { ChannelValue } from '@volter/editor-core/creation-site-edit';
20
+ import { creationSiteAnchor, instancesAtSite } from '@volter/editor-core/creation-site-registry';
21
+ import { editorConsole } from '@volter/editor-core/editor-console';
22
+ import type { EditorShellStore } from '@volter/editor-core/editor-shell-store';
23
+ import { type JournalSubject, JsonHistoryResource } from '../history/json-history-resource';
24
+ import { createEphemeralPersistence } from './ephemeral-persistence';
25
+ import { persistChannelWrite } from './gesture-persist';
26
+ import type { SourcePersistenceBackend, SourceWriteSubject } from './source-persistence-backend';
27
+
28
+ interface Vector3Like {
29
+ x: number;
30
+ y: number;
31
+ z: number;
32
+ }
33
+
34
+ interface QuaternionLike extends Vector3Like {
35
+ w: number;
36
+ }
37
+
38
+ export interface BabylonNodeLike {
39
+ name?: string;
40
+ id?: string;
41
+ uniqueId?: number;
42
+ position?: Vector3Like;
43
+ rotation?: Vector3Like;
44
+ rotationQuaternion?: QuaternionLike | null;
45
+ scaling?: Vector3Like;
46
+ isVisible?: boolean;
47
+ visibility?: number;
48
+ getClassName?: () => string;
49
+ getChildren?: () => BabylonNodeLike[];
50
+ isEnabled?: (checkAncestors?: boolean) => boolean;
51
+ setEnabled?: (enabled: boolean) => void;
52
+ }
53
+
54
+ export interface BabylonSceneLike {
55
+ uid?: string;
56
+ name?: string;
57
+ rootNodes?: BabylonNodeLike[];
58
+ isDisposed?: () => boolean;
59
+ activeCamera?: unknown;
60
+ pick?: (x: number, y: number) => { hit?: boolean; pickedMesh?: BabylonNodeLike | null };
61
+ onPointerObservable?: {
62
+ add(callback: (event: { pickInfo?: { pickedMesh?: BabylonNodeLike | null } }) => void): unknown;
63
+ remove(token: unknown): void;
64
+ };
65
+ }
66
+
67
+ export interface BabylonEngineLike {
68
+ scenes?: BabylonSceneLike[];
69
+ dispose?: () => void;
70
+ }
71
+
72
+ interface BabylonGizmoManagerLike {
73
+ positionGizmoEnabled: boolean;
74
+ rotationGizmoEnabled: boolean;
75
+ scaleGizmoEnabled: boolean;
76
+ usePointerToAttachGizmos: boolean;
77
+ attachToMesh(mesh: unknown): void;
78
+ attachToNode?(node: unknown): void;
79
+ dispose(): void;
80
+ }
81
+
82
+ interface BabylonApiLike {
83
+ GizmoManager?: new (scene: BabylonSceneLike) => BabylonGizmoManagerLike;
84
+ }
85
+
86
+ export interface BabylonAuthoringOptions {
87
+ readonly canvas?: HTMLCanvasElement;
88
+ readonly api?: BabylonApiLike;
89
+ readonly persistence?: SourcePersistenceBackend;
90
+ readonly provenance?: AuthoringProvenance;
91
+ /** The subject whose session this adapter's live edits are journaled
92
+ * under — the same `authoringJournal(worldId)` the Pixi sibling takes.
93
+ * Absent, no history entries are pushed (the play and ingest mounts). */
94
+ readonly journal?: JournalSubject;
95
+ }
96
+
97
+ /** One session-journal snapshot: every indexed node's channels, plus the
98
+ * edit record that alone identifies the resource (see `conflictIdentity`). */
99
+ interface BabylonHistoryState {
100
+ edits: number;
101
+ nodes: Record<string, Record<string, ChannelValue>>;
102
+ }
103
+
104
+ type IndexedValue =
105
+ | { kind: 'scene'; scene: BabylonSceneLike; node: EditorNode }
106
+ | { kind: 'node'; object: BabylonNodeLike; node: EditorNode };
107
+
108
+ function cleanSegment(value: string): string {
109
+ return encodeURIComponent(value || 'unnamed');
110
+ }
111
+
112
+ function className(object: BabylonNodeLike): string {
113
+ try {
114
+ return object.getClassName?.() || 'Node';
115
+ } catch {
116
+ return 'Node';
117
+ }
118
+ }
119
+
120
+ function directChildren(object: BabylonNodeLike): BabylonNodeLike[] {
121
+ try {
122
+ const children = object.getChildren?.();
123
+ return Array.isArray(children) ? children : [];
124
+ } catch {
125
+ return [];
126
+ }
127
+ }
128
+
129
+ function quaternionFromEuler(rotation: Vector3Like | undefined): QuaternionLike {
130
+ const x = rotation?.x ?? 0;
131
+ const y = rotation?.y ?? 0;
132
+ const z = rotation?.z ?? 0;
133
+ const cx = Math.cos(x / 2);
134
+ const sx = Math.sin(x / 2);
135
+ const cy = Math.cos(y / 2);
136
+ const sy = Math.sin(y / 2);
137
+ const cz = Math.cos(z / 2);
138
+ const sz = Math.sin(z / 2);
139
+ return {
140
+ x: sx * cy * cz + cx * sy * sz,
141
+ y: cx * sy * cz - sx * cy * sz,
142
+ z: cx * cy * sz + sx * sy * cz,
143
+ w: cx * cy * cz - sx * sy * sz,
144
+ };
145
+ }
146
+
147
+ function eulerFromQuaternion([x, y, z, w]: Transform['rotation']): Vector3Like {
148
+ const sinrCosp = 2 * (w * x + y * z);
149
+ const cosrCosp = 1 - 2 * (x * x + y * y);
150
+ const sinp = 2 * (w * y - z * x);
151
+ const sinyCosp = 2 * (w * z + x * y);
152
+ const cosyCosp = 1 - 2 * (y * y + z * z);
153
+ return {
154
+ x: Math.atan2(sinrCosp, cosrCosp),
155
+ y: Math.abs(sinp) >= 1 ? Math.sign(sinp) * (Math.PI / 2) : Math.asin(sinp),
156
+ z: Math.atan2(sinyCosp, cosyCosp),
157
+ };
158
+ }
159
+
160
+ function writeVector(target: Vector3Like | undefined, value: readonly number[]): void {
161
+ if (!target) return;
162
+ target.x = value[0] ?? target.x;
163
+ target.y = value[1] ?? target.y;
164
+ target.z = value[2] ?? target.z;
165
+ }
166
+
167
+ function writeEulerRotation(object: BabylonNodeLike, value: readonly number[]): void {
168
+ if (!object.rotationQuaternion) {
169
+ writeVector(object.rotation, value);
170
+ return;
171
+ }
172
+ const quaternion = quaternionFromEuler({
173
+ x: value[0] ?? 0,
174
+ y: value[1] ?? 0,
175
+ z: value[2] ?? 0,
176
+ });
177
+ writeVector(object.rotationQuaternion, [quaternion.x, quaternion.y, quaternion.z]);
178
+ object.rotationQuaternion.w = quaternion.w;
179
+ }
180
+
181
+ export class BabylonAuthoringAdapter implements AuthoringAdapter {
182
+ readonly capabilities: AuthoringCapabilities = {
183
+ transform: true,
184
+ inspectorFields: true,
185
+ persist: false,
186
+ };
187
+
188
+ readonly provenance: AuthoringProvenance;
189
+ readonly persistence;
190
+ readonly pickable?: PickProvider;
191
+ readonly truth;
192
+
193
+ private readonly indexed = new Map<string, IndexedValue>();
194
+ private readonly idsByObject = new Map<BabylonNodeLike, string>();
195
+ private readonly listeners = new Set<() => void>();
196
+ private readonly persistenceBackend: SourcePersistenceBackend | null;
197
+ private readonly baselines = new Map<string, Record<string, ChannelValue>>();
198
+ /** The history snapshot taken at `beginEdit`, journaled at `endEdit`. */
199
+ private readonly historyBefore = new Map<string, BabylonHistoryState>();
200
+ private historyResource: JsonHistoryResource<BabylonHistoryState> | null = null;
201
+ private edits = 0;
202
+ private readonly gizmos: BabylonGizmoManagerLike[] = [];
203
+ private readonly pointerTokens: Array<{ scene: BabylonSceneLike; token: unknown }> = [];
204
+ private readonly removeCanvasListeners: (() => void) | null;
205
+
206
+ constructor(
207
+ private readonly engine: BabylonEngineLike,
208
+ private readonly store: EditorShellStore,
209
+ private readonly options: BabylonAuthoringOptions = {},
210
+ ) {
211
+ this.persistenceBackend = options.persistence ?? null;
212
+ this.provenance =
213
+ options.provenance ??
214
+ ({
215
+ source: 'foreign',
216
+ label: 'live-only',
217
+ detail:
218
+ 'Babylon.js scenes and Nodes projected directly — edits apply to native game objects for this session only.',
219
+ } satisfies AuthoringProvenance);
220
+ const ephemeral = createEphemeralPersistence();
221
+ const backend = this.persistenceBackend;
222
+ this.persistence = backend
223
+ ? {
224
+ isDirty: () => false,
225
+ save: async () => {},
226
+ get destination(): string {
227
+ return backend.destination();
228
+ },
229
+ }
230
+ : ephemeral;
231
+ if (backend) {
232
+ backend.attach({
233
+ read: (id, property) => this.readChannel(id, property),
234
+ apply: (id, property, value) => this.applyChannel(id, property, value),
235
+ });
236
+ }
237
+ // THE SESSION JOURNAL EXISTS EXACTLY WHERE A DESTINATION DOES — the three
238
+ // lane's rule, transcribed. A live-only write (every position edit on
239
+ // the shipped example: its source assigns `new Vector3(0, 1, 0)` whole)
240
+ // used to journal NOTHING, so the edit landed on the live node and
241
+ // Ctrl+Z was a correct no-op over an empty stack — "undo is not working"
242
+ // (runhuman pass 149; traced on production build 75).
243
+ const history = store.projectHistory;
244
+ this.historyResource =
245
+ backend && history && options.journal
246
+ ? new JsonHistoryResource<BabylonHistoryState>({
247
+ history,
248
+ kind: 'session-state',
249
+ scope: 'session',
250
+ subject: { ...options.journal, id: `${options.journal.id}/live-babylon-edits` },
251
+ displayName: 'Live edits',
252
+ capture: () => this.captureHistoryState(),
253
+ // A running world animates its own nodes; only the edit record
254
+ // says whether something ELSE changed this resource (issue #81
255
+ // in the three lane — without this every transaction fails
256
+ // preflight with content-conflict and undo silently does nothing).
257
+ conflictIdentity: (state) => state.edits,
258
+ restore: async (state) => {
259
+ this.restoreHistoryState(state);
260
+ },
261
+ })
262
+ : null;
263
+ if (options.canvas) {
264
+ this.pickable = {
265
+ pick: (clientX: number, clientY: number): string | null => {
266
+ const rect = options.canvas!.getBoundingClientRect();
267
+ for (const scene of this.scenes()) {
268
+ const hit = scene.pick?.(clientX - rect.left, clientY - rect.top);
269
+ const object = hit?.hit ? hit.pickedMesh : null;
270
+ if (object) {
271
+ this.reindex();
272
+ return this.idsByObject.get(object) ?? null;
273
+ }
274
+ }
275
+ return null;
276
+ },
277
+ };
278
+ }
279
+ this.truth = {
280
+ resolve: (id: string, property: string) => {
281
+ const subject = this.subjectFor(id, property);
282
+ return { site: subject.anchor, writeAnchorKind: subject.anchorKind };
283
+ },
284
+ };
285
+ this.installNativeTools();
286
+ if (options.canvas) {
287
+ const onDown = (): void => {
288
+ for (const id of this.selection.get()) this.transforms.beginEdit(id);
289
+ };
290
+ const onUp = (): void => {
291
+ for (const id of this.selection.get()) void this.transforms.endEdit(id);
292
+ };
293
+ options.canvas.addEventListener('pointerdown', onDown);
294
+ options.canvas.addEventListener('pointerup', onUp);
295
+ this.removeCanvasListeners = () => {
296
+ options.canvas!.removeEventListener('pointerdown', onDown);
297
+ options.canvas!.removeEventListener('pointerup', onUp);
298
+ };
299
+ } else {
300
+ this.removeCanvasListeners = null;
301
+ }
302
+ }
303
+
304
+ private scenes(): BabylonSceneLike[] {
305
+ return (this.engine.scenes ?? []).filter((scene) => {
306
+ try {
307
+ return !scene.isDisposed?.();
308
+ } catch {
309
+ return true;
310
+ }
311
+ });
312
+ }
313
+
314
+ refresh(): { scenes: number; objects: number } {
315
+ this.reindex();
316
+ let scenes = 0;
317
+ let objects = 0;
318
+ for (const value of this.indexed.values()) {
319
+ if (value.kind === 'scene') scenes++;
320
+ else objects++;
321
+ }
322
+ return { scenes, objects };
323
+ }
324
+
325
+ private reindex(): void {
326
+ this.indexed.clear();
327
+ this.idsByObject.clear();
328
+ const visited = new Set<BabylonNodeLike>();
329
+ for (const [sceneIndex, scene] of this.scenes().entries()) {
330
+ const sceneName = scene.name || `Scene ${sceneIndex + 1}`;
331
+ const sceneId = `babylon:scene:${sceneIndex}:${cleanSegment(scene.uid || sceneName)}`;
332
+ const roots = Array.isArray(scene.rootNodes) ? scene.rootNodes : [];
333
+ const sceneNode: EditorNode = {
334
+ id: sceneId,
335
+ label: sceneName,
336
+ role: 'root',
337
+ kind: 'babylon-scene',
338
+ typeLabel: 'BABYLON.Scene',
339
+ parentId: null,
340
+ childIds: [],
341
+ };
342
+ this.indexed.set(sceneId, { kind: 'scene', scene, node: sceneNode });
343
+ sceneNode.childIds = roots.map((object, index) =>
344
+ this.indexNode(object, sceneId, `${sceneIndex}/${index}`, visited),
345
+ );
346
+ }
347
+ }
348
+
349
+ private indexNode(
350
+ object: BabylonNodeLike,
351
+ parentId: string,
352
+ path: string,
353
+ visited: Set<BabylonNodeLike>,
354
+ ): string {
355
+ const nativeType = className(object);
356
+ const nativeIdentity =
357
+ typeof object.uniqueId === 'number' ? String(object.uniqueId) : `path:${path}`;
358
+ const id = `babylon:node:${nativeIdentity}`;
359
+ const node: EditorNode = {
360
+ id,
361
+ label: object.name || nativeType,
362
+ role: 'entity',
363
+ kind: `babylon-${nativeType.toLowerCase()}`,
364
+ typeLabel: nativeType,
365
+ parentId,
366
+ childIds: [],
367
+ };
368
+ this.indexed.set(id, { kind: 'node', object, node });
369
+ this.idsByObject.set(object, id);
370
+ if (visited.has(object)) return id;
371
+ visited.add(object);
372
+ node.childIds = directChildren(object).map((child, index) =>
373
+ this.indexNode(child, id, `${path}/${index}`, visited),
374
+ );
375
+ return id;
376
+ }
377
+
378
+ readonly hierarchy = {
379
+ roots: (): EditorNode[] => {
380
+ this.reindex();
381
+ return [...this.indexed.values()]
382
+ .filter((value) => value.kind === 'scene')
383
+ .map((value) => value.node);
384
+ },
385
+ node: (id: string): EditorNode | null => {
386
+ this.reindex();
387
+ return this.indexed.get(id)?.node ?? null;
388
+ },
389
+ };
390
+
391
+ readonly selection = {
392
+ get: (): string[] => [...this.store.selectedEntityIds],
393
+ set: (ids: string[]): void => {
394
+ this.store.selectMultiple(ids);
395
+ this.attachGizmos(ids[0] ?? null);
396
+ this.notify();
397
+ },
398
+ };
399
+
400
+ readonly transforms = {
401
+ dimensions: (id: string): '3d' | null => {
402
+ this.reindex();
403
+ return this.indexed.get(id)?.kind === 'node' ? '3d' : null;
404
+ },
405
+ get: (id: string): Transform => {
406
+ this.reindex();
407
+ const value = this.indexed.get(id);
408
+ if (!value || value.kind !== 'node') {
409
+ return { position: [0, 0, 0], rotation: [0, 0, 0, 1], scale: [1, 1, 1] };
410
+ }
411
+ const object = value.object;
412
+ const q = object.rotationQuaternion ?? quaternionFromEuler(object.rotation);
413
+ return {
414
+ position: [object.position?.x ?? 0, object.position?.y ?? 0, object.position?.z ?? 0],
415
+ rotation: [q.x, q.y, q.z, q.w],
416
+ scale: [object.scaling?.x ?? 1, object.scaling?.y ?? 1, object.scaling?.z ?? 1],
417
+ };
418
+ },
419
+ beginEdit: (id: string): void => {
420
+ const baseline = this.captureChannels(id);
421
+ if (baseline) this.baselines.set(id, baseline);
422
+ if (this.historyResource) this.historyBefore.set(id, this.captureHistoryState());
423
+ },
424
+ apply: (id: string, transform: Transform): void => {
425
+ this.reindex();
426
+ const value = this.indexed.get(id);
427
+ if (!value || value.kind !== 'node') return;
428
+ const object = value.object;
429
+ writeVector(object.position, transform.position);
430
+ writeVector(object.scaling, transform.scale);
431
+ if (object.rotationQuaternion) {
432
+ writeVector(object.rotationQuaternion, transform.rotation);
433
+ object.rotationQuaternion.w = transform.rotation[3];
434
+ } else {
435
+ const rotation = eulerFromQuaternion(transform.rotation);
436
+ writeVector(object.rotation, [rotation.x, rotation.y, rotation.z]);
437
+ }
438
+ this.notify();
439
+ },
440
+ endEdit: async (id: string): Promise<WriteAck | undefined> => {
441
+ const baseline = this.baselines.get(id);
442
+ this.baselines.delete(id);
443
+ const before = this.historyBefore.get(id);
444
+ this.historyBefore.delete(id);
445
+ if (!baseline || !this.persistenceBackend) return;
446
+ const next = this.captureChannels(id);
447
+ if (!next) return;
448
+ const changed = (['position', 'rotation', 'scale'] as const).filter(
449
+ (property) => JSON.stringify(baseline[property]) !== JSON.stringify(next[property]),
450
+ );
451
+ if (changed.length === 0) return;
452
+ if (changed.length > 1) {
453
+ const reason = `this Babylon gesture changed ${changed.length} properties at once (${changed.join(', ')})`;
454
+ this.persistenceBackend.report(`Transform ${this.labelOf(id)}`, reason);
455
+ return { destination: 'live-only (not saved)', persisted: false };
456
+ }
457
+ const property = changed[0]!;
458
+ return persistChannelWrite({
459
+ backend: this.persistenceBackend,
460
+ subject: () => this.subjectFor(id, property),
461
+ baseline: baseline[property]!,
462
+ next: next[property]!,
463
+ label: `Transform ${this.labelOf(id)}`,
464
+ journal: () => this.recordHistory(`Transform ${this.labelOf(id)}`, before),
465
+ });
466
+ },
467
+ };
468
+
469
+ readonly inspector = {
470
+ properties: (id: string): PropertyDescriptor[] => {
471
+ this.reindex();
472
+ const value = this.indexed.get(id);
473
+ if (!value) return [];
474
+ if (value.kind === 'scene') {
475
+ return [
476
+ { path: 'name', label: 'Scene', type: 'string', readonly: true },
477
+ { path: 'uid', label: 'UID', type: 'string', readonly: true },
478
+ ];
479
+ }
480
+ const properties: PropertyDescriptor[] = [
481
+ { path: 'className', label: 'Type', type: 'string', readonly: true },
482
+ { path: 'name', label: 'Name', type: 'string' },
483
+ { path: 'id', label: 'ID', type: 'string', readonly: true },
484
+ { path: 'enabled', label: 'Enabled', type: 'boolean' },
485
+ ];
486
+ if (typeof value.object.isVisible === 'boolean') {
487
+ properties.push({ path: 'isVisible', label: 'Visible', type: 'boolean' });
488
+ }
489
+ if (typeof value.object.visibility === 'number') {
490
+ properties.push({ path: 'visibility', label: 'Visibility', type: 'number' });
491
+ }
492
+ return properties;
493
+ },
494
+ get: (id: string, path: string): unknown => {
495
+ this.reindex();
496
+ const value = this.indexed.get(id);
497
+ if (!value) return undefined;
498
+ if (value.kind === 'scene') {
499
+ return path === 'name' ? value.scene.name : path === 'uid' ? value.scene.uid : undefined;
500
+ }
501
+ if (path === 'className') return className(value.object);
502
+ if (path === 'enabled') return value.object.isEnabled?.(false);
503
+ return value.object[path as keyof BabylonNodeLike];
504
+ },
505
+ set: async (id: string, path: string, value: unknown): Promise<WriteAck | undefined> => {
506
+ this.reindex();
507
+ const indexed = this.indexed.get(id);
508
+ if (!indexed || indexed.kind !== 'node') return;
509
+ const baseline = this.readChannel(id, path);
510
+ const before = this.historyResource ? this.captureHistoryState() : undefined;
511
+ if (path === 'enabled') indexed.object.setEnabled?.(Boolean(value));
512
+ else if (path === 'name' || path === 'isVisible' || path === 'visibility') {
513
+ (indexed.object as unknown as Record<string, unknown>)[path] = value;
514
+ } else return;
515
+ this.notify();
516
+ const next = this.readChannel(id, path);
517
+ if (
518
+ !this.persistenceBackend ||
519
+ baseline === undefined ||
520
+ next === undefined ||
521
+ JSON.stringify(baseline) === JSON.stringify(next)
522
+ ) {
523
+ return;
524
+ }
525
+ return persistChannelWrite({
526
+ backend: this.persistenceBackend,
527
+ subject: () => this.subjectFor(id, path === 'isVisible' ? 'visible' : path),
528
+ baseline,
529
+ next,
530
+ label: `Set ${this.labelOf(id)} ${path}`,
531
+ journal: () => this.recordHistory(`Set ${this.labelOf(id)} ${path}`, before),
532
+ });
533
+ },
534
+ };
535
+
536
+ subscribe(listener: () => void): () => void {
537
+ this.listeners.add(listener);
538
+ return () => this.listeners.delete(listener);
539
+ }
540
+
541
+ dispose(): void {
542
+ this.removeCanvasListeners?.();
543
+ for (const { scene, token } of this.pointerTokens) scene.onPointerObservable?.remove(token);
544
+ for (const gizmo of this.gizmos) gizmo.dispose();
545
+ this.persistenceBackend?.dispose();
546
+ this.listeners.clear();
547
+ this.indexed.clear();
548
+ this.idsByObject.clear();
549
+ }
550
+
551
+ private labelOf(id: string): string {
552
+ return this.hierarchy.node(id)?.label ?? 'Babylon node';
553
+ }
554
+
555
+ private objectOf(id: string): BabylonNodeLike | null {
556
+ this.reindex();
557
+ const value = this.indexed.get(id);
558
+ return value?.kind === 'node' ? value.object : null;
559
+ }
560
+
561
+ private readChannel(id: string, property: string): ChannelValue | undefined {
562
+ const object = this.objectOf(id);
563
+ if (!object) return undefined;
564
+ if (property === 'position') {
565
+ return [object.position?.x ?? 0, object.position?.y ?? 0, object.position?.z ?? 0];
566
+ }
567
+ if (property === 'rotation') {
568
+ const q = object.rotationQuaternion;
569
+ const euler = q
570
+ ? eulerFromQuaternion([q.x, q.y, q.z, q.w])
571
+ : (object.rotation ?? { x: 0, y: 0, z: 0 });
572
+ return [euler.x, euler.y, euler.z];
573
+ }
574
+ if (property === 'scale') {
575
+ return [object.scaling?.x ?? 1, object.scaling?.y ?? 1, object.scaling?.z ?? 1];
576
+ }
577
+ if (property === 'visible') return object.isVisible;
578
+ return this.inspector.get(id, property) as ChannelValue | undefined;
579
+ }
580
+
581
+ private applyChannel(id: string, property: string, value: ChannelValue): void {
582
+ const object = this.objectOf(id);
583
+ if (!object) return;
584
+ if (property === 'position' && Array.isArray(value)) writeVector(object.position, value);
585
+ else if (property === 'rotation' && Array.isArray(value)) writeEulerRotation(object, value);
586
+ else if (property === 'scale' && Array.isArray(value)) writeVector(object.scaling, value);
587
+ else if (property === 'visible') object.isVisible = Boolean(value);
588
+ else if (property === 'name' || property === 'visibility') {
589
+ (object as unknown as Record<string, unknown>)[property] = value;
590
+ }
591
+ this.notify();
592
+ }
593
+
594
+ /** Journal a before→after snapshot pair into project history, LOUDLY: a
595
+ * rejected record means the edit is applied but cannot be undone. */
596
+ private recordHistory(label: string, before: BabylonHistoryState | undefined): void {
597
+ if (!this.historyResource || !before) return;
598
+ this.edits += 1;
599
+ void this.historyResource
600
+ .record(label, before, this.captureHistoryState())
601
+ .catch((err: unknown) => {
602
+ editorConsole.error(
603
+ `[babylon] failed to journal "${label}" into project history — the edit is applied ` +
604
+ `but has no history entry (it cannot be undone): ` +
605
+ `${err instanceof Error ? err.message : String(err)}`,
606
+ );
607
+ });
608
+ }
609
+
610
+ private captureHistoryState(): BabylonHistoryState {
611
+ this.reindex();
612
+ const nodes: BabylonHistoryState['nodes'] = {};
613
+ for (const [id, value] of this.indexed) {
614
+ if (value.kind !== 'node') continue;
615
+ const channels: Record<string, ChannelValue> = {};
616
+ for (const property of ['position', 'rotation', 'scale', 'visible', 'name', 'visibility']) {
617
+ const read = this.readChannel(id, property);
618
+ if (read !== undefined && read !== null) channels[property] = read;
619
+ }
620
+ nodes[id] = channels;
621
+ }
622
+ return { edits: this.edits, nodes };
623
+ }
624
+
625
+ private restoreHistoryState(state: BabylonHistoryState): void {
626
+ this.edits = state.edits;
627
+ for (const [id, channels] of Object.entries(state.nodes)) {
628
+ for (const [property, value] of Object.entries(channels)) {
629
+ this.applyChannel(id, property, value);
630
+ }
631
+ }
632
+ this.notify();
633
+ }
634
+
635
+ private captureChannels(id: string): Record<string, ChannelValue> | null {
636
+ const position = this.readChannel(id, 'position');
637
+ const rotation = this.readChannel(id, 'rotation');
638
+ const scale = this.readChannel(id, 'scale');
639
+ return position && rotation && scale ? { position, rotation, scale } : null;
640
+ }
641
+
642
+ private subjectFor(id: string, property: string): SourceWriteSubject {
643
+ const object = this.objectOf(id);
644
+ const anchor = creationSiteAnchor(object);
645
+ return {
646
+ entityId: id,
647
+ property,
648
+ surface: 'babylon',
649
+ anchor,
650
+ anchorKind: anchor.anchored ? 'construction-literal' : 'live-only',
651
+ instances: anchor.anchored && anchor.kind === 'source' ? instancesAtSite(anchor) : 0,
652
+ };
653
+ }
654
+
655
+ private installNativeTools(): void {
656
+ const Manager = this.options.api?.GizmoManager;
657
+ for (const scene of this.scenes()) {
658
+ if (Manager) {
659
+ const manager = new Manager(scene);
660
+ manager.positionGizmoEnabled = true;
661
+ manager.rotationGizmoEnabled = true;
662
+ manager.scaleGizmoEnabled = true;
663
+ manager.usePointerToAttachGizmos = false;
664
+ this.gizmos.push(manager);
665
+ }
666
+ const observable = scene.onPointerObservable;
667
+ if (observable) {
668
+ const token = observable.add((event) => {
669
+ const picked = event.pickInfo?.pickedMesh;
670
+ if (!picked) return;
671
+ this.reindex();
672
+ const id = this.idsByObject.get(picked);
673
+ if (id) this.selection.set([id]);
674
+ });
675
+ this.pointerTokens.push({ scene, token });
676
+ }
677
+ }
678
+ }
679
+
680
+ private attachGizmos(id: string | null): void {
681
+ const object = id ? this.objectOf(id) : null;
682
+ for (const gizmo of this.gizmos) {
683
+ if (!object) {
684
+ gizmo.attachToMesh(null);
685
+ continue;
686
+ }
687
+ try {
688
+ if (className(object).includes('Mesh')) gizmo.attachToMesh(object);
689
+ else if (gizmo.attachToNode) gizmo.attachToNode(object);
690
+ else gizmo.attachToMesh(null);
691
+ } catch {
692
+ // Cameras, lights, and extension nodes remain selectable even when a
693
+ // Babylon gizmo cannot attach to their native node type.
694
+ gizmo.attachToMesh(null);
695
+ }
696
+ }
697
+ }
698
+
699
+ private notify(): void {
700
+ for (const listener of this.listeners) listener();
701
+ this.store.notifyIngestEdit();
702
+ }
703
+ }