@volter/editor-core 0.5.57

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 (719) hide show
  1. package/BUNDLED_NOTICES +1527 -0
  2. package/LICENSE +686 -0
  3. package/LICENSE-APACHE +202 -0
  4. package/NOTICE +18 -0
  5. package/README.md +24 -0
  6. package/dist/build/chunk-JS4D4XZ3.js +175 -0
  7. package/dist/build/chunk-JS4D4XZ3.js.map +7 -0
  8. package/dist/build/vite-plugin-product-contributions.js +100 -0
  9. package/dist/build/vite-plugin-product-contributions.js.map +7 -0
  10. package/dist/build/vite-plugin-shared-react.js +25 -0
  11. package/dist/build/vite-plugin-shared-react.js.map +7 -0
  12. package/dist/build/vite-plugin-shared-three.js +86 -0
  13. package/dist/build/vite-plugin-shared-three.js.map +7 -0
  14. package/dist/server/chunk-KMJC7AGO.js +98 -0
  15. package/dist/server/chunk-KMJC7AGO.js.map +7 -0
  16. package/dist/server/chunk-PSILUMGS.js +46 -0
  17. package/dist/server/chunk-PSILUMGS.js.map +7 -0
  18. package/dist/server/frame-proxy.js +255 -0
  19. package/dist/server/frame-proxy.js.map +7 -0
  20. package/dist/server/open-browser.js +53 -0
  21. package/dist/server/open-browser.js.map +7 -0
  22. package/dist/server/process-shutdown.js +127 -0
  23. package/dist/server/process-shutdown.js.map +7 -0
  24. package/dist/server/session-registry.js +141 -0
  25. package/dist/server/session-registry.js.map +7 -0
  26. package/dist/server/spawn-opener.js +11 -0
  27. package/dist/server/spawn-opener.js.map +7 -0
  28. package/dist/server/worktree-identity.js +11 -0
  29. package/dist/server/worktree-identity.js.map +7 -0
  30. package/dist-server/packaged.mjs +88816 -0
  31. package/package.json +118 -0
  32. package/scripts/build-plugins.mjs +14 -0
  33. package/scripts/build-server.mjs +26 -0
  34. package/scripts/build-session.mjs +9 -0
  35. package/server/account-credentials.ts +60 -0
  36. package/server/account-service.ts +1434 -0
  37. package/server/adapter-region-includes.ts +47 -0
  38. package/server/asset-catalog-v2.ts +175 -0
  39. package/server/asset-history-snapshots.ts +196 -0
  40. package/server/asset-ledger-store.ts +195 -0
  41. package/server/asset-library-routes.ts +1595 -0
  42. package/server/blender-wasm-artifact.ts +430 -0
  43. package/server/boot-timings.ts +106 -0
  44. package/server/canonical-path.ts +50 -0
  45. package/server/checkout-workspace-preflight.ts +349 -0
  46. package/server/cloud-asset-catalog.ts +158 -0
  47. package/server/coding-inference-launch.ts +237 -0
  48. package/server/collaboration-account-client.ts +202 -0
  49. package/server/collaboration-attribution.ts +122 -0
  50. package/server/collaboration-session.ts +934 -0
  51. package/server/comfyui-bridge.ts +248 -0
  52. package/server/console-ledger.ts +499 -0
  53. package/server/creation-site-transform.ts +304 -0
  54. package/server/creation-site-write.ts +648 -0
  55. package/server/data-file-serialize.ts +158 -0
  56. package/server/editor-brand-html.ts +72 -0
  57. package/server/editor-control-socket.ts +366 -0
  58. package/server/editor-server-options.ts +190 -0
  59. package/server/editor-server.ts +1483 -0
  60. package/server/editor-sse.ts +352 -0
  61. package/server/engine-provenance.ts +169 -0
  62. package/server/engine-source-restart.ts +117 -0
  63. package/server/frame-bridge.ts +115 -0
  64. package/server/frame-proxy.ts +490 -0
  65. package/server/frame-workbench.ts +259 -0
  66. package/server/frontend-handoff.ts +229 -0
  67. package/server/game-globals-shadow.ts +113 -0
  68. package/server/gameplay-session-retention.ts +149 -0
  69. package/server/gameplay-sessions.ts +123 -0
  70. package/server/generation-jobs.ts +201 -0
  71. package/server/generation-reconciler.ts +137 -0
  72. package/server/generative-execution-context.ts +59 -0
  73. package/server/git-workflow.ts +612 -0
  74. package/server/harness-chat-caller.ts +36 -0
  75. package/server/harness-chat-service.ts +1681 -0
  76. package/server/idle-shutdown.ts +170 -0
  77. package/server/js-profiling-policy.ts +52 -0
  78. package/server/launcher-settings.ts +84 -0
  79. package/server/local-asset-catalog.ts +410 -0
  80. package/server/managed-account-defaults.ts +22 -0
  81. package/server/mock-control-plane.ts +540 -0
  82. package/server/model-import-conversion.ts +345 -0
  83. package/server/native-credential-store.ts +162 -0
  84. package/server/open-browser.ts +118 -0
  85. package/server/packaged.ts +1192 -0
  86. package/server/play-stall.ts +126 -0
  87. package/server/process-shutdown.ts +218 -0
  88. package/server/product-presets.ts +55 -0
  89. package/server/project-build-artifact.ts +95 -0
  90. package/server/project-components.ts +100 -0
  91. package/server/project-dependency-invalidation.ts +114 -0
  92. package/server/project-file-scan.ts +235 -0
  93. package/server/project-hmr-files.ts +215 -0
  94. package/server/project-install-roots.ts +210 -0
  95. package/server/project-kinds.ts +71 -0
  96. package/server/project-mcp-servers.ts +82 -0
  97. package/server/project-module-freshness.ts +109 -0
  98. package/server/project-module-instance.ts +375 -0
  99. package/server/project-optimize-deps-entries.ts +827 -0
  100. package/server/project-output-writer.ts +679 -0
  101. package/server/project-package-origin.ts +63 -0
  102. package/server/project-root-surface.ts +831 -0
  103. package/server/project-scratch-path.ts +47 -0
  104. package/server/project-script-hmr.ts +619 -0
  105. package/server/project-serving-plugins.ts +236 -0
  106. package/server/project-tools.ts +653 -0
  107. package/server/project-validation.ts +250 -0
  108. package/server/project-verbs.ts +127 -0
  109. package/server/project-view.ts +52 -0
  110. package/server/project-watch.ts +1134 -0
  111. package/server/project-work-coordinator.ts +288 -0
  112. package/server/provider-credentials.ts +331 -0
  113. package/server/recent-projects-store.ts +98 -0
  114. package/server/redact-secrets.ts +71 -0
  115. package/server/repository-presence.ts +267 -0
  116. package/server/routes/account.ts +357 -0
  117. package/server/routes/agents.ts +60 -0
  118. package/server/routes/assets.ts +154 -0
  119. package/server/routes/build.ts +13 -0
  120. package/server/routes/collaboration.ts +595 -0
  121. package/server/routes/configurations.ts +419 -0
  122. package/server/routes/context.ts +264 -0
  123. package/server/routes/control-plane.ts +1811 -0
  124. package/server/routes/logs.ts +272 -0
  125. package/server/routes/project-identity.ts +302 -0
  126. package/server/routes/project-open.ts +429 -0
  127. package/server/routes/project-source.ts +686 -0
  128. package/server/routes/project-state.ts +383 -0
  129. package/server/routes/relay.ts +1029 -0
  130. package/server/routes/route-helpers.ts +29 -0
  131. package/server/routes/served-modules.ts +138 -0
  132. package/server/routes/session-tabs.ts +196 -0
  133. package/server/routes/settings.ts +121 -0
  134. package/server/routes/share-control.ts +486 -0
  135. package/server/routes/themes.ts +116 -0
  136. package/server/routes/tools.ts +230 -0
  137. package/server/routes/worktrees.ts +595 -0
  138. package/server/scoped-game-css.ts +461 -0
  139. package/server/server-utils.ts +1194 -0
  140. package/server/session-product.ts +112 -0
  141. package/server/session-registry.ts +230 -0
  142. package/server/share-claims.ts +138 -0
  143. package/server/share-host.ts +526 -0
  144. package/server/share-session-gateway.ts +1362 -0
  145. package/server/share-tunnel.ts +123 -0
  146. package/server/spawn-opener.ts +74 -0
  147. package/server/support/play/log-format.ts +76 -0
  148. package/server/support/play/session-record.ts +199 -0
  149. package/server/support/project/build-discipline.ts +718 -0
  150. package/server/support/project/inspection-node.ts +119 -0
  151. package/server/support/project/log-naming.ts +54 -0
  152. package/server/support/project/provenance.ts +290 -0
  153. package/server/support/project/run-name.ts +18 -0
  154. package/server/support/project/session-journal.ts +830 -0
  155. package/server/support/project/shared.ts +327 -0
  156. package/server/tab-bootstrap.ts +62 -0
  157. package/server/tab-heartbeat.ts +477 -0
  158. package/server/tab-lifecycle.ts +550 -0
  159. package/server/tab-presence.ts +1508 -0
  160. package/server/team-agent-mirror.ts +95 -0
  161. package/server/twin-auth.ts +138 -0
  162. package/server/vendored-lock-recorder.ts +968 -0
  163. package/server/worktree-identity.ts +121 -0
  164. package/server/worktree-management.ts +398 -0
  165. package/server/worktree-retention.d.mts +13 -0
  166. package/server/worktree-retention.mjs +205 -0
  167. package/src/EditorContext.tsx +169 -0
  168. package/src/account.ts +333 -0
  169. package/src/action-registry.ts +532 -0
  170. package/src/active-product.ts +74 -0
  171. package/src/active-project.ts +155 -0
  172. package/src/adapter-editor-config.ts +25 -0
  173. package/src/adapter-observation.ts +49 -0
  174. package/src/animation/stage-transport.ts +379 -0
  175. package/src/animation/three-clips-subject.ts +172 -0
  176. package/src/api/asset-library-wire.ts +45 -0
  177. package/src/api/assets.ts +365 -0
  178. package/src/api/base.ts +10 -0
  179. package/src/api/build.ts +99 -0
  180. package/src/api/git-wire.ts +56 -0
  181. package/src/api/logs.ts +92 -0
  182. package/src/api/project-identity.ts +74 -0
  183. package/src/api/project-open.ts +355 -0
  184. package/src/api/project-source.ts +162 -0
  185. package/src/api/project-state.ts +107 -0
  186. package/src/api/relay.ts +268 -0
  187. package/src/api/settings.ts +36 -0
  188. package/src/api/themes.ts +45 -0
  189. package/src/api/worktrees.ts +257 -0
  190. package/src/asset-compare-core.ts +171 -0
  191. package/src/asset-compare.ts +294 -0
  192. package/src/asset-editor-context.tsx +100 -0
  193. package/src/asset-events.ts +97 -0
  194. package/src/asset-inspector-actions.ts +87 -0
  195. package/src/asset-preview-framing.ts +357 -0
  196. package/src/asset-preview.ts +2802 -0
  197. package/src/asset-selection-viewer-registry.ts +113 -0
  198. package/src/asset-selection.ts +133 -0
  199. package/src/asset-workflow/asset-capabilities.ts +344 -0
  200. package/src/asset-workflow/asset-import-jobs.ts +106 -0
  201. package/src/asset-workflow/asset-ledger-backend.ts +126 -0
  202. package/src/asset-workflow/asset-ledger.ts +156 -0
  203. package/src/asset-workflow/asset-materialization-report.ts +140 -0
  204. package/src/asset-workflow/asset-pack-manifest.ts +320 -0
  205. package/src/asset-workflow/asset-types.ts +142 -0
  206. package/src/asset-workflow/asset-workflow-quality.ts +61 -0
  207. package/src/asset-workflow/audio-preview-player.ts +193 -0
  208. package/src/asset-workflow/audio-waveform.ts +22 -0
  209. package/src/asset-workflow/cloud-asset-client.ts +263 -0
  210. package/src/asset-workflow/folder-preview.ts +345 -0
  211. package/src/asset-workflow/hosted-asset-materialization.ts +236 -0
  212. package/src/asset-workflow/image-view-scale.ts +32 -0
  213. package/src/asset-workflow/import-contract.ts +124 -0
  214. package/src/asset-workflow/ledger-write-lock.ts +244 -0
  215. package/src/asset-workflow/model-inspection.ts +818 -0
  216. package/src/asset-workflow/pixi-spritesheet.ts +197 -0
  217. package/src/asset-workflow/preview-resource-lifetime.ts +44 -0
  218. package/src/asset-workflow/project-asset-commands.ts +23 -0
  219. package/src/asset-workflow/project-asset-health.ts +139 -0
  220. package/src/asset-workflow/project-asset-operations.ts +154 -0
  221. package/src/asset-workflow/project-asset-roots.ts +68 -0
  222. package/src/asset-workflow/project-content.ts +288 -0
  223. package/src/asset-workflow/project-source-index.ts +550 -0
  224. package/src/asset-workflow/thumbnail-system.ts +256 -0
  225. package/src/authoring/active-adapter.ts +199 -0
  226. package/src/authoring/active-systems.ts +422 -0
  227. package/src/authoring/adapter-key.ts +18 -0
  228. package/src/authoring/authoring-asset-url.ts +27 -0
  229. package/src/authoring/bootstrap-state.ts +49 -0
  230. package/src/authoring/boundary-authoring-adapter.ts +184 -0
  231. package/src/authoring/breakpoint-state.ts +43 -0
  232. package/src/authoring/canvas-scene-guides.ts +76 -0
  233. package/src/authoring/component-instance-root.ts +171 -0
  234. package/src/authoring/composite-authoring-adapter.ts +2110 -0
  235. package/src/authoring/consumer-actions.ts +520 -0
  236. package/src/authoring/css-numeric-style.ts +97 -0
  237. package/src/authoring/design-time-layers.ts +849 -0
  238. package/src/authoring/design-time-mount-registry.ts +235 -0
  239. package/src/authoring/design-time-settle.ts +343 -0
  240. package/src/authoring/edit-mode-authoring.ts +619 -0
  241. package/src/authoring/eyedropper-session.ts +60 -0
  242. package/src/authoring/instance-source-menu-register.ts +13 -0
  243. package/src/authoring/instance-source-menu.ts +135 -0
  244. package/src/authoring/layered-pick.ts +183 -0
  245. package/src/authoring/live-gesture-lock.ts +50 -0
  246. package/src/authoring/live-object-transform.ts +62 -0
  247. package/src/authoring/mount-failure-report.ts +154 -0
  248. package/src/authoring/mounted-root-subjects.ts +144 -0
  249. package/src/authoring/no-authoring-adapter.ts +55 -0
  250. package/src/authoring/null-inspection-subjects.tsx +75 -0
  251. package/src/authoring/object3d-document-persistence.ts +106 -0
  252. package/src/authoring/object3d-document-session-registry.ts +120 -0
  253. package/src/authoring/object3d-document-session.ts +1326 -0
  254. package/src/authoring/object3d-gesture-controller.ts +113 -0
  255. package/src/authoring/panel-authoring.ts +121 -0
  256. package/src/authoring/prefab-instance-inspector-section.tsx +237 -0
  257. package/src/authoring/prefab-instance-section-model.ts +55 -0
  258. package/src/authoring/project-authoring-session.ts +105 -0
  259. package/src/authoring/provenance.ts +99 -0
  260. package/src/authoring/quarks-particle-systems.ts +19 -0
  261. package/src/authoring/react-canvas-navigation.ts +255 -0
  262. package/src/authoring/react-design-canvas-style.ts +20 -0
  263. package/src/authoring/react-story-board.ts +937 -0
  264. package/src/authoring/selection-scope.ts +195 -0
  265. package/src/authoring/shell-document-ops.ts +169 -0
  266. package/src/authoring/shell-object3d-document-write-policy.ts +108 -0
  267. package/src/authoring/shell-viewport-policy.ts +48 -0
  268. package/src/authoring/source-object3d-authoring-adapter.ts +526 -0
  269. package/src/authoring/stories-scope.ts +35 -0
  270. package/src/authoring/story-board-chrome-fit.ts +107 -0
  271. package/src/authoring/story-board-presentation.ts +111 -0
  272. package/src/authoring/three-projection-core.ts +226 -0
  273. package/src/authoring/viewport-pick-context.ts +40 -0
  274. package/src/authoring/viewport-raycast.ts +240 -0
  275. package/src/authoring/viewport-tool-context.ts +73 -0
  276. package/src/authoring/world-canvas-viewport-state.ts +35 -0
  277. package/src/authoring/world-hidden-viewport.ts +152 -0
  278. package/src/authoring/world-pan-state.ts +198 -0
  279. package/src/authoring/world-session-state.ts +101 -0
  280. package/src/authoring/write-pipe.ts +173 -0
  281. package/src/availability-tick.ts +66 -0
  282. package/src/bitmap-label.ts +120 -0
  283. package/src/blender-tab-metrics.ts +161 -0
  284. package/src/board-open-actions.ts +20 -0
  285. package/src/boot-routing.ts +391 -0
  286. package/src/build-identity.ts +15 -0
  287. package/src/bytes-codec.ts +62 -0
  288. package/src/camera-authoring.ts +175 -0
  289. package/src/cancellation-reason.ts +58 -0
  290. package/src/canvas-preview-frames.ts +482 -0
  291. package/src/capture-camera-pose.ts +77 -0
  292. package/src/chrome-registry.ts +159 -0
  293. package/src/chrome-slot-registry.ts +91 -0
  294. package/src/collaboration-client.ts +264 -0
  295. package/src/collaboration-presence.ts +41 -0
  296. package/src/command-dispatch.ts +19 -0
  297. package/src/command-listener.ts +2743 -0
  298. package/src/command-registry.ts +70 -0
  299. package/src/component-board-registry.ts +205 -0
  300. package/src/component-states-registry.ts +200 -0
  301. package/src/components/AgentPresentationNotice.tsx +75 -0
  302. package/src/components/AlignToolbar.tsx +204 -0
  303. package/src/components/AppRoot.tsx +394 -0
  304. package/src/components/ApplicationMenus.tsx +415 -0
  305. package/src/components/AssetBrowser.tsx +2843 -0
  306. package/src/components/AssetEditorShell.tsx +216 -0
  307. package/src/components/AssetInspectorToolSection.tsx +124 -0
  308. package/src/components/BoardRulers.tsx +354 -0
  309. package/src/components/CameraInfo.tsx +81 -0
  310. package/src/components/CanvasSceneViewport.tsx +556 -0
  311. package/src/components/CapabilityCoverageSection.tsx +185 -0
  312. package/src/components/CenterDocuments.tsx +22 -0
  313. package/src/components/ChromeSlot.tsx +20 -0
  314. package/src/components/CodeView.tsx +470 -0
  315. package/src/components/CollaborationHeaderControl.css +74 -0
  316. package/src/components/CompactInspectorCard.tsx +190 -0
  317. package/src/components/CompactInspectorShell.tsx +39 -0
  318. package/src/components/ConsolePanel.css +55 -0
  319. package/src/components/ConsolePanel.tsx +270 -0
  320. package/src/components/DefaultEditorLayout.tsx +106 -0
  321. package/src/components/DocumentHeaderStrip.tsx +96 -0
  322. package/src/components/DocumentShelfRail.tsx +45 -0
  323. package/src/components/DocumentThumbnail.tsx +345 -0
  324. package/src/components/EditorLeaseGuard.tsx +372 -0
  325. package/src/components/ErrorBanner.tsx +123 -0
  326. package/src/components/FolderPreviewTile.tsx +159 -0
  327. package/src/components/GameHierarchy.tsx +3842 -0
  328. package/src/components/GameplaySessionTimeline.tsx +295 -0
  329. package/src/components/InspectionProjection.tsx +968 -0
  330. package/src/components/Inspector.tsx +233 -0
  331. package/src/components/InspectorCanvasPreview.tsx +35 -0
  332. package/src/components/InspectorFieldsSection.tsx +290 -0
  333. package/src/components/InspectorObjectPreview.tsx +57 -0
  334. package/src/components/InspectorStoriesSection.tsx +92 -0
  335. package/src/components/InspectorToolSection.tsx +96 -0
  336. package/src/components/InspectorTransformSection.tsx +245 -0
  337. package/src/components/LightExplorerPanel.tsx +433 -0
  338. package/src/components/MediaProperties.tsx +145 -0
  339. package/src/components/NonThreeAuthoringBootstrap.tsx +19 -0
  340. package/src/components/Object3DDocumentToolbar.css +100 -0
  341. package/src/components/Object3DDocumentToolbar.tsx +454 -0
  342. package/src/components/Object3DDocumentViewport.tsx +58 -0
  343. package/src/components/ProjectHeader.css +617 -0
  344. package/src/components/ProjectHeader.tsx +328 -0
  345. package/src/components/ProjectLayout.tsx +249 -0
  346. package/src/components/ReactCanvasControls.tsx +284 -0
  347. package/src/components/RootSelectionOverlay.tsx +3162 -0
  348. package/src/components/RootTextEditor.tsx +79 -0
  349. package/src/components/SaveStatus.tsx +70 -0
  350. package/src/components/StageHost.tsx +1954 -0
  351. package/src/components/StageOverlays.tsx +21 -0
  352. package/src/components/StartupErrorScreen.tsx +76 -0
  353. package/src/components/StartupLoadingScreen.tsx +65 -0
  354. package/src/components/StatsOverlay.tsx +78 -0
  355. package/src/components/SurfaceStateOverlay.tsx +24 -0
  356. package/src/components/ToolContributionSurfaces.tsx +65 -0
  357. package/src/components/ToolHost.tsx +370 -0
  358. package/src/components/ToolObject3DPreview.tsx +39 -0
  359. package/src/components/Toolbar.tsx +595 -0
  360. package/src/components/TransientHint.tsx +44 -0
  361. package/src/components/TransportStrip.tsx +174 -0
  362. package/src/components/VersionControlSection.tsx +470 -0
  363. package/src/components/VgaiLogo.css +83 -0
  364. package/src/components/VgaiLogo.tsx +35 -0
  365. package/src/components/ViewportControlsHint.tsx +60 -0
  366. package/src/components/ViewportFurniture.tsx +401 -0
  367. package/src/components/ViewportOverlay.tsx +145 -0
  368. package/src/components/ViewportOverlaysMenu.tsx +153 -0
  369. package/src/components/ViewportShadingMenu.tsx +300 -0
  370. package/src/components/ViewportViewMenu.tsx +101 -0
  371. package/src/components/WorkspaceDocumentSurface.tsx +204 -0
  372. package/src/components/WorkspaceUtilitySurface.tsx +22 -0
  373. package/src/components/WorktreeSwitcher.css +239 -0
  374. package/src/components/WorktreeSwitcher.tsx +890 -0
  375. package/src/components/account-documents.tsx +1162 -0
  376. package/src/components/asset-documents.tsx +812 -0
  377. package/src/components/asset-editor-persistence.ts +216 -0
  378. package/src/components/asset-selection-section.tsx +535 -0
  379. package/src/components/asset-thumbnails.tsx +312 -0
  380. package/src/components/asset-viewers/AudioViewer.tsx +201 -0
  381. package/src/components/asset-viewers/EntityModelDocument.tsx +122 -0
  382. package/src/components/asset-viewers/EnvironmentAssetDocument.tsx +440 -0
  383. package/src/components/asset-viewers/GenericJsonViewer.tsx +102 -0
  384. package/src/components/asset-viewers/ImageViewer.tsx +300 -0
  385. package/src/components/asset-viewers/JsonAssetDocument.tsx +93 -0
  386. package/src/components/asset-viewers/LiveModuleDocument.tsx +417 -0
  387. package/src/components/asset-viewers/LutAssetDocument.tsx +444 -0
  388. package/src/components/asset-viewers/ModelAssetDocument.tsx +105 -0
  389. package/src/components/asset-viewers/Object3DPreview.tsx +359 -0
  390. package/src/components/asset-viewers/OnlineAssetDetail.tsx +421 -0
  391. package/src/components/asset-viewers/PasteboardModuleDocument.tsx +67 -0
  392. package/src/components/asset-viewers/QuarksAssetDocument.tsx +526 -0
  393. package/src/components/asset-viewers/ShaderAssetDocument.tsx +743 -0
  394. package/src/components/asset-viewers/SourceAssetViewer.tsx +281 -0
  395. package/src/components/asset-viewers/SpritesheetSpriteView.tsx +102 -0
  396. package/src/components/asset-viewers/VideoViewer.tsx +101 -0
  397. package/src/components/asset-viewers/shader-source.ts +144 -0
  398. package/src/components/asset-workflow.css +731 -0
  399. package/src/components/board-guides.ts +150 -0
  400. package/src/components/compact-inspector.css +539 -0
  401. package/src/components/core-utilities.tsx +90 -0
  402. package/src/components/editor-notifications.css +40 -0
  403. package/src/components/engine-workspace.css +274 -0
  404. package/src/components/inspector-preview-section.tsx +238 -0
  405. package/src/components/inspector-property-grouping.ts +64 -0
  406. package/src/components/inspector-revert-label.ts +20 -0
  407. package/src/components/inspector-selection.ts +42 -0
  408. package/src/components/inspector-stories-gating.ts +171 -0
  409. package/src/components/inspector-transform-subject.ts +11 -0
  410. package/src/components/inspector-transform.ts +75 -0
  411. package/src/components/kind-documents.tsx +500 -0
  412. package/src/components/palette-action-publisher.tsx +140 -0
  413. package/src/components/primitives/DraftColorInput.tsx +74 -0
  414. package/src/components/product-shell.css +1829 -0
  415. package/src/components/project-tool-documents.tsx +410 -0
  416. package/src/components/scene-documents.tsx +223 -0
  417. package/src/components/stage-keyboard.tsx +37 -0
  418. package/src/components/stage-overlay-set.tsx +107 -0
  419. package/src/components/stage-presence-markers.ts +478 -0
  420. package/src/components/standard-viewport-dressing.ts +417 -0
  421. package/src/components/status-contributions.tsx +403 -0
  422. package/src/components/tool-documents.tsx +309 -0
  423. package/src/components/tool-schema-form.tsx +262 -0
  424. package/src/components/use-after-paint.ts +41 -0
  425. package/src/components/use-project-image-assets.ts +86 -0
  426. package/src/components/viewport-header-controls.css +90 -0
  427. package/src/components/viewport-surface-status.tsx +55 -0
  428. package/src/components/workspace-history.ts +32 -0
  429. package/src/components/workspace-static-panel-registry.tsx +152 -0
  430. package/src/components/workspace-surfaces.css +621 -0
  431. package/src/components/world-documents.tsx +586 -0
  432. package/src/components/world-overlay-gestures.ts +1694 -0
  433. package/src/composite-screenshot.ts +1641 -0
  434. package/src/console-sync.ts +131 -0
  435. package/src/constraint-helper.ts +338 -0
  436. package/src/content-entry-source-registry.ts +184 -0
  437. package/src/coverage/authoring-seam-evidence.ts +300 -0
  438. package/src/coverage/canvas-reveal.ts +192 -0
  439. package/src/coverage/design-time-surfaces.ts +101 -0
  440. package/src/coverage/live-seam-evidence.ts +11 -0
  441. package/src/coverage/ontology-invariants.ts +466 -0
  442. package/src/coverage/session-vitals.ts +501 -0
  443. package/src/coverage/system-seam-evidence.ts +72 -0
  444. package/src/crash-null-boundary.ts +36 -0
  445. package/src/creation-site-edit.ts +1479 -0
  446. package/src/creation-site-registry.ts +160 -0
  447. package/src/delegate-harness-registry.ts +143 -0
  448. package/src/document-context-registry.ts +177 -0
  449. package/src/document-open-registry.ts +200 -0
  450. package/src/document-preview-source.ts +20 -0
  451. package/src/document-renderer-session.ts +138 -0
  452. package/src/editor-api.ts +46 -0
  453. package/src/editor-chrome-capture.ts +139 -0
  454. package/src/editor-commands.ts +164 -0
  455. package/src/editor-console.ts +563 -0
  456. package/src/editor-current-view.ts +82 -0
  457. package/src/editor-document-probe.ts +881 -0
  458. package/src/editor-git-client.ts +115 -0
  459. package/src/editor-host-door.ts +462 -0
  460. package/src/editor-hotkeys.ts +842 -0
  461. package/src/editor-lease-view.ts +39 -0
  462. package/src/editor-lease.ts +415 -0
  463. package/src/editor-mode.ts +19 -0
  464. package/src/editor-notifications.ts +140 -0
  465. package/src/editor-presence.ts +563 -0
  466. package/src/editor-presentation-activity.ts +50 -0
  467. package/src/editor-presentation-notice.ts +42 -0
  468. package/src/editor-runtime.tsx +145 -0
  469. package/src/editor-server-response.ts +86 -0
  470. package/src/editor-session-attribution.ts +75 -0
  471. package/src/editor-session-mode.ts +54 -0
  472. package/src/editor-shell-store.ts +1304 -0
  473. package/src/editor-state-facets.ts +74 -0
  474. package/src/editor-styles.css +33 -0
  475. package/src/editor-view-presentation.ts +810 -0
  476. package/src/editor-viewport.ts +5302 -0
  477. package/src/entity-lod.ts +31 -0
  478. package/src/entity-object.ts +91 -0
  479. package/src/files/file-provider.ts +62 -0
  480. package/src/files/project-files.ts +264 -0
  481. package/src/finders/index.ts +136 -0
  482. package/src/finders/scenes-from-entrypoint-selection.ts +387 -0
  483. package/src/frame/bridge.tsx +1408 -0
  484. package/src/frame/product.ts +77 -0
  485. package/src/gameplay-dom-recording.ts +319 -0
  486. package/src/gameplay-export-state.ts +14 -0
  487. package/src/gameplay-replay.ts +417 -0
  488. package/src/gameplay-session-time.ts +9 -0
  489. package/src/gameplay-sessions.ts +204 -0
  490. package/src/harness-chat-types.ts +253 -0
  491. package/src/hierarchy-component-marks.ts +298 -0
  492. package/src/hierarchy-drop.ts +91 -0
  493. package/src/hierarchy-expansion-state.ts +80 -0
  494. package/src/hierarchy-header-slot.ts +52 -0
  495. package/src/hierarchy-internals.ts +197 -0
  496. package/src/hierarchy-kind-icon.ts +217 -0
  497. package/src/hierarchy-mark-reader.ts +73 -0
  498. package/src/hierarchy-menu-registry.ts +67 -0
  499. package/src/hierarchy-node-rows.ts +307 -0
  500. package/src/hierarchy-panel-view.ts +280 -0
  501. package/src/hierarchy-projection.ts +76 -0
  502. package/src/hierarchy-row-cache.ts +243 -0
  503. package/src/hierarchy-row-model.ts +308 -0
  504. package/src/hierarchy-rows.ts +11 -0
  505. package/src/hierarchy-walk.ts +86 -0
  506. package/src/history/editor-session.ts +25 -0
  507. package/src/history/history-commands.ts +147 -0
  508. package/src/history/history-delegate.ts +163 -0
  509. package/src/history/history-limit-notices.ts +43 -0
  510. package/src/history/history-service.ts +1173 -0
  511. package/src/history/persistence-coordinator.ts +35 -0
  512. package/src/history/project-file-history.ts +386 -0
  513. package/src/history/project-root-history-backends.ts +139 -0
  514. package/src/history/resource-registry.ts +209 -0
  515. package/src/history/snapshot-store.ts +103 -0
  516. package/src/history/source-history-backend.ts +546 -0
  517. package/src/history/types.ts +124 -0
  518. package/src/hmr-registration-group.ts +67 -0
  519. package/src/hmr-stable-react-context.ts +23 -0
  520. package/src/hotkeys.ts +188 -0
  521. package/src/inference-diagnostics.ts +69 -0
  522. package/src/initial-project.ts +80 -0
  523. package/src/inspection/active-subject.ts +578 -0
  524. package/src/inspection/active-surface.ts +142 -0
  525. package/src/inspection/compose.ts +1064 -0
  526. package/src/inspection/display.ts +170 -0
  527. package/src/inspection/document-subject.ts +109 -0
  528. package/src/inspection/game-subject.ts +85 -0
  529. package/src/inspection/model.ts +542 -0
  530. package/src/inspection/null-subject.ts +115 -0
  531. package/src/inspection/serialize.ts +357 -0
  532. package/src/inspection/use-active-inspection.ts +180 -0
  533. package/src/inspector-presentation.ts +201 -0
  534. package/src/inspector-section-registry.ts +221 -0
  535. package/src/instance-source-actions.ts +163 -0
  536. package/src/instanced-presentation.ts +164 -0
  537. package/src/js-heap.ts +71 -0
  538. package/src/key-actions.ts +91 -0
  539. package/src/keymap-presets.ts +401 -0
  540. package/src/layout-policy.ts +31 -0
  541. package/src/learn-links.ts +73 -0
  542. package/src/light-explorer-model.ts +134 -0
  543. package/src/live-canvas-frame.ts +55 -0
  544. package/src/live-document.ts +279 -0
  545. package/src/live-module-source.ts +230 -0
  546. package/src/live-session-registry.ts +220 -0
  547. package/src/live-transition.ts +633 -0
  548. package/src/manifest-project.ts +107 -0
  549. package/src/model-thumbnail.ts +523 -0
  550. package/src/native-selection-style.ts +202 -0
  551. package/src/object3d-document-write-policy.ts +137 -0
  552. package/src/packaged-runtime.ts +108 -0
  553. package/src/palettes/maya.palette.json +57 -0
  554. package/src/palettes/substance.palette.json +57 -0
  555. package/src/pasteboard-module.ts +81 -0
  556. package/src/performance-profiler.ts +367 -0
  557. package/src/performance-sources.ts +69 -0
  558. package/src/play-boot-phase.ts +145 -0
  559. package/src/presentation-surface.ts +248 -0
  560. package/src/project-adapter.ts +1143 -0
  561. package/src/project-asset-refresh.ts +26 -0
  562. package/src/project-declaration-refresh.ts +31 -0
  563. package/src/project-local-state.ts +118 -0
  564. package/src/project-manager.ts +243 -0
  565. package/src/project-module-changes.ts +163 -0
  566. package/src/project-module-split.ts +266 -0
  567. package/src/project-provenance.ts +115 -0
  568. package/src/project-ready.ts +42 -0
  569. package/src/project-session-reset.ts +67 -0
  570. package/src/project-shape.ts +68 -0
  571. package/src/project-tool-discovery.ts +66 -0
  572. package/src/project-tools.ts +107 -0
  573. package/src/project-work-types.ts +149 -0
  574. package/src/projection/three.ts +898 -0
  575. package/src/projection/types.ts +44 -0
  576. package/src/readiness.ts +113 -0
  577. package/src/reflection-probe-helper.ts +142 -0
  578. package/src/reported-play-state.ts +90 -0
  579. package/src/resolve-relative-specifier.ts +33 -0
  580. package/src/scene-document-plan.ts +315 -0
  581. package/src/scene-framing.ts +315 -0
  582. package/src/scene-live-open.ts +210 -0
  583. package/src/scene-view-fog.ts +89 -0
  584. package/src/scoped-game-css.ts +152 -0
  585. package/src/session-orphan-record.ts +193 -0
  586. package/src/session-tombstone.ts +126 -0
  587. package/src/settings/settings-provider.ts +82 -0
  588. package/src/settings-store.ts +345 -0
  589. package/src/shared-view-restore.ts +42 -0
  590. package/src/shell-store-door.ts +45 -0
  591. package/src/source-conflict.ts +122 -0
  592. package/src/spatial-handle-visuals.ts +332 -0
  593. package/src/stage-context.ts +341 -0
  594. package/src/stage-store-registry.ts +63 -0
  595. package/src/stale-chunk-recovery.ts +34 -0
  596. package/src/state-report-deferral.ts +73 -0
  597. package/src/storage/host-files-storage.ts +97 -0
  598. package/src/storage/http-storage.ts +174 -0
  599. package/src/storage/index.ts +75 -0
  600. package/src/storage/mem-storage.ts +149 -0
  601. package/src/storage/path-lock.ts +44 -0
  602. package/src/storage/paths.ts +26 -0
  603. package/src/storage/types.ts +116 -0
  604. package/src/stories/StoryComponentThumbnail.tsx +184 -0
  605. package/src/stories/StoryPreviewMount.tsx +306 -0
  606. package/src/stories/component-content-source.tsx +106 -0
  607. package/src/stories/component-states-source.ts +78 -0
  608. package/src/stories/compose-project-stories.ts +255 -0
  609. package/src/stories/pixi-story-model.ts +30 -0
  610. package/src/stories/prefabs-finder.ts +54 -0
  611. package/src/stories/prefabs-from-stories.ts +182 -0
  612. package/src/stories/project-story-discovery.ts +78 -0
  613. package/src/stories/project-story-regions.ts +24 -0
  614. package/src/stories/story-actions.ts +54 -0
  615. package/src/stories/story-capture-command.ts +124 -0
  616. package/src/stories/story-capture.ts +584 -0
  617. package/src/stories/story-declared-medium.ts +126 -0
  618. package/src/stories/story-discovery.ts +176 -0
  619. package/src/stories/story-document-openers.ts +36 -0
  620. package/src/stories/story-dom-runtime.ts +78 -0
  621. package/src/stories/story-grouping.ts +111 -0
  622. package/src/stories/story-lane.ts +88 -0
  623. package/src/stories/story-mount-turn.ts +27 -0
  624. package/src/stories/story-opener.ts +260 -0
  625. package/src/stories/story-pixi-preview.ts +408 -0
  626. package/src/stories/story-presentation.ts +215 -0
  627. package/src/stories/story-registry.ts +517 -0
  628. package/src/stories/story-three-preview.ts +807 -0
  629. package/src/stories/three-story-model.ts +96 -0
  630. package/src/story-three-preview-runtime.ts +56 -0
  631. package/src/surface-keyboard.ts +112 -0
  632. package/src/surface-state.ts +124 -0
  633. package/src/tab-bootstrap.js +628 -0
  634. package/src/tab-census.ts +198 -0
  635. package/src/tab-lifecycle-client.ts +196 -0
  636. package/src/theme-library.ts +844 -0
  637. package/src/theme-preference.ts +378 -0
  638. package/src/theme.css +3437 -0
  639. package/src/three-viewport/bone-selection-highlight.ts +119 -0
  640. package/src/three-viewport/camera-fit.ts +41 -0
  641. package/src/three-viewport/interactive-renderer.ts +132 -0
  642. package/src/three-viewport/selection-brackets.ts +310 -0
  643. package/src/three-viewport/selection-outline.ts +183 -0
  644. package/src/three-viewport/skeleton-helper.ts +61 -0
  645. package/src/three-viewport/source-color.ts +167 -0
  646. package/src/three-viewport/studio-environment.ts +96 -0
  647. package/src/three-viewport-presentation.ts +23 -0
  648. package/src/tool-contribution-play.ts +74 -0
  649. package/src/tool-loader.ts +1798 -0
  650. package/src/transform-mode-request.ts +57 -0
  651. package/src/transient-hint.ts +78 -0
  652. package/src/trigger-volume-helper.ts +116 -0
  653. package/src/ui-source/adapter-region-includes.ts +241 -0
  654. package/src/ui-source/ensure-import.ts +132 -0
  655. package/src/ui-source/file-region-resolver.ts +302 -0
  656. package/src/ui-source/inspect.ts +775 -0
  657. package/src/ui-source/oid-transform.ts +1845 -0
  658. package/src/ui-source/plan-csf-story.ts +196 -0
  659. package/src/ui-source/plan-extract-component.ts +421 -0
  660. package/src/ui-source/plan-fork-component.ts +621 -0
  661. package/src/ui-source/plan-named-style.ts +95 -0
  662. package/src/ui-source/plan-source-edit.ts +353 -0
  663. package/src/ui-source/r3f-contract-resolver.ts +327 -0
  664. package/src/ui-source/r3f-diagnostic-index.ts +165 -0
  665. package/src/ui-source/r3f-environment-binding.ts +133 -0
  666. package/src/ui-source/r3f-joint-binding.ts +251 -0
  667. package/src/ui-source/r3f-lod-binding.ts +91 -0
  668. package/src/ui-source/r3f-particle-binding.ts +235 -0
  669. package/src/ui-source/r3f-physics-binding.ts +483 -0
  670. package/src/ui-source/r3f-project-contracts.ts +444 -0
  671. package/src/ui-source/relative-import-specifier.ts +34 -0
  672. package/src/ui-source/reparent-guard.ts +544 -0
  673. package/src/ui-source/source-edit-request.ts +97 -0
  674. package/src/ui-source/source-write-backend.ts +618 -0
  675. package/src/ui-source/syntactic-prop-specs.ts +296 -0
  676. package/src/ui-source/tier-source-write-backend.ts +279 -0
  677. package/src/ui-source/ts-ast.ts +171 -0
  678. package/src/ui-source/utility-class-support.ts +169 -0
  679. package/src/ui-source/write-component-default.ts +133 -0
  680. package/src/ui-source/writer.ts +1989 -0
  681. package/src/viewport-activation-timings.ts +840 -0
  682. package/src/viewport-authoring-policy.ts +186 -0
  683. package/src/viewport-controls-hint.ts +57 -0
  684. package/src/viewport-door.ts +237 -0
  685. package/src/viewport-shading-boundary.ts +12 -0
  686. package/src/vite-error-surface.ts +141 -0
  687. package/src/wait-until.ts +37 -0
  688. package/src/workspace-areas.ts +156 -0
  689. package/src/workspace-aux-commands.ts +11 -0
  690. package/src/workspace-available-documents.ts +127 -0
  691. package/src/workspace-core-utilities.ts +31 -0
  692. package/src/workspace-document-ids.ts +59 -0
  693. package/src/workspace-document-registry.ts +612 -0
  694. package/src/workspace-document-restore.ts +145 -0
  695. package/src/workspace-host-commands.ts +141 -0
  696. package/src/workspace-persistence-gate.ts +40 -0
  697. package/src/workspace-play-utilities.ts +44 -0
  698. package/src/workspace-presets.ts +425 -0
  699. package/src/workspace-regions.ts +294 -0
  700. package/src/workspace-state-persistence.ts +536 -0
  701. package/src/workspace-static-panels.ts +73 -0
  702. package/src/workspace-status-registry.ts +121 -0
  703. package/src/workspace-style.ts +239 -0
  704. package/src/workspace-utility-commands.ts +74 -0
  705. package/src/workspace-utility-registry.ts +263 -0
  706. package/src/workspace-viewport-rect.ts +97 -0
  707. package/src/world-adoption.ts +115 -0
  708. package/src/world-document-routing.ts +104 -0
  709. package/vite-plugin-creation-site-write.ts +184 -0
  710. package/vite-plugin-creation-site.ts +80 -0
  711. package/vite-plugin-game-static.ts +303 -0
  712. package/vite-plugin-module-doorways.ts +336 -0
  713. package/vite-plugin-product-contributions.ts +197 -0
  714. package/vite-plugin-project-game-static.ts +125 -0
  715. package/vite-plugin-project-jsx-js.ts +65 -0
  716. package/vite-plugin-project-root-absolute-assets.ts +359 -0
  717. package/vite-plugin-shared-react.ts +420 -0
  718. package/vite-plugin-shared-three.ts +203 -0
  719. package/vite-plugin-ui-oid.ts +2106 -0
@@ -0,0 +1,1304 @@
1
+ /**
2
+ * EditorShellStore — the format-NEUTRAL half of the editor's central store.
3
+ *
4
+ * It holds exactly the format-neutral state: selection, the
5
+ * scene-adoption stack, the project-history handle, tool/viewport/play state, and the
6
+ * viewport action bus. It knows about `THREE.Object3D`s and ids — never about
7
+ * any document model or its persistence: an adapter's document is that
8
+ * adapter's private business, and the store names no document type at all
9
+ * (`authoring-inversion-guard.test.ts` is the tripwire).
10
+ *
11
+ * MEASURED: every authoring adapter touches exactly eight store members —
12
+ * `selectedEntityIds`, `selectMultiple`, `subscribe`, `notifyIngestEdit`,
13
+ * `objectMap`, `projectHistory`, `scene`, and `playState`. All eight are here.
14
+ */
15
+
16
+ import type { ViewportTab } from '@volter/editor-sdk';
17
+ import { viewportCaptureOutputPass } from '@volter/editor-threejs/capture/output-pass';
18
+ import { isEditorOwnedObject } from '@volter/editor-threejs/viewport/editor-layers';
19
+ import type { WorldRendererConfig } from '@volter/editor-threejs/adapter/renderer-config';
20
+ import { getUserData, setUserData } from '@volter/editor-threejs/ecs/user-data';
21
+ import type { ViewportShadingMode } from '@volter/editor-threejs/render/viewport-shading';
22
+ import * as THREE from 'three';
23
+ import type { BatchedRenderer } from 'three.quarks';
24
+ import { findEntityLod } from './entity-lod';
25
+ import { entityIdOf } from './entity-object';
26
+ import type { HistoryService } from './history/history-service';
27
+ import { withSceneFogNeutralized } from './scene-view-fog';
28
+
29
+ /** The store's persistence collaborator — installed by the shell, never
30
+ * imported by the store (see `attachStatePersistence`). */
31
+ export interface EditorStatePersistence {
32
+ save(state: Record<string, unknown>): unknown;
33
+ saveThumbnail?(dataUrl: string): unknown;
34
+ }
35
+
36
+ /**
37
+ * WHICH TOOL THE SHELF HAS ARMED — four that draw a transform gizmo, and one
38
+ * that draws none.
39
+ *
40
+ * `'select'` is Blender's Select Box, and it is a TOOL rather than an absence:
41
+ * its shelf opens on it (`space_toolsystem_toolbar.py`,
42
+ * `_defs_view3d_generic.select_box` first in the Object Mode `_tools_default`;
43
+ * photographed at the engine's pin in `gizmo-select-box.png`), so a selected
44
+ * object shows its outline and nothing else until Move / Rotate / Scale /
45
+ * Transform is armed. Before it there was no "no gizmo" state at all — this
46
+ * store's default was `'combined'` and every selection drew three tools'
47
+ * handles at once — which is why it is a member here rather than a flag beside
48
+ * it. `editor-viewport.ts` detaches on it, through the same branch a
49
+ * non-writable selection already took.
50
+ *
51
+ * Which member a STAGE is born with is the look's (`workspace-regions.ts`'s
52
+ * `shelfTool`); arming one is always a click in the shelf.
53
+ */
54
+ export type TransformMode = 'select' | 'combined' | 'translate' | 'rotate' | 'scale';
55
+ export type TransformSpace = 'world' | 'local';
56
+ export type PivotMode = 'active-element' | 'median-point' | 'individual-origins';
57
+
58
+ /**
59
+ * WHERE the transform gizmo is drawn for a single selection — the standard DCC
60
+ * affordance, and PRESENTATION ONLY: an edit made about the bounds centre writes
61
+ * the same source values it would about the pivot (`editor-viewport.ts`).
62
+ *
63
+ * `auto` is the default because the right answer is a property of the SUBJECT,
64
+ * not a standing preference: an ordinary node's pivot is where its content is,
65
+ * and a world-anchored instanced system's is not (`instanced-presentation.ts`) —
66
+ * that one draws its gizmo at (0,0,0) with every unit the reader can see
67
+ * somewhere else. Both explicit values remain reachable, because "put the gizmo
68
+ * back on the actual origin" is a legitimate thing to want on exactly that node.
69
+ */
70
+ export type GizmoAnchor = 'auto' | 'pivot' | 'center';
71
+
72
+ export type ShadingMode = ViewportShadingMode;
73
+
74
+ /**
75
+ * What a store notification says CHANGED.
76
+ *
77
+ * `'selection'` is the narrow one and it is claimed by exactly five writers
78
+ * (`select`, `selectMultiple`, `addToSelection`, `toggleSelection`,
79
+ * `applySelectionBeforePresentation`), each of which touches
80
+ * `_viewportSelections` and nothing else. `'content'` is the default and means
81
+ * "assume anything may have changed", so a mutation that forgets to classify
82
+ * itself is safe by construction — the failure mode of a wrong claim is a
83
+ * stale panel, so the burden of proof sits on the narrow value.
84
+ * See {@link EditorShellStore.contentVersion}.
85
+ */
86
+ export type NotifyScope = 'content' | 'selection';
87
+ type NotifyConcern = 'shell' | 'object-map';
88
+
89
+ /** Exact structural delta supplied by a live Three projection mutation. */
90
+ export interface IngestObjectMapStructureDelta {
91
+ readonly changedParentIds: ReadonlySet<string>;
92
+ readonly rootsChanged: boolean;
93
+ /** Exact projected membership changes; omission keeps legacy callers conservative. */
94
+ readonly addedIds?: ReadonlySet<string>;
95
+ readonly removedIds?: ReadonlySet<string>;
96
+ }
97
+
98
+ /** All projected parent rows changed after a previously observed gizmo epoch. */
99
+ export interface IngestObjectMapChanges {
100
+ readonly epoch: number;
101
+ readonly changedParentIds: ReadonlySet<string>;
102
+ }
103
+
104
+ /** Exact object-map identities whose membership changed after an observed gizmo epoch. */
105
+ export interface IngestObjectMapMembershipChanges {
106
+ readonly epoch: number;
107
+ readonly changedIds: ReadonlySet<string>;
108
+ }
109
+
110
+ /** Enough exact structural epochs to span deferred React consumers; older readers rebuild. */
111
+ const OBJECT_MAP_MEMBERSHIP_HISTORY = 256;
112
+
113
+ /** The scope a coalesced pair of notifications must fire with: `'content'`
114
+ * unless BOTH were selection-only. */
115
+ function widenNotifyScope(current: NotifyScope, incoming: NotifyScope): NotifyScope {
116
+ return current === 'selection' && incoming === 'selection' ? 'selection' : 'content';
117
+ }
118
+
119
+ /**
120
+ * Which persistence regime the current play session exposes. D19 makes play
121
+ * edits ephemeral for every world count, and since the `.vgai/` overlay
122
+ * sidecar was deleted outright (2026-08-02) `ephemeral` is the ONLY regime
123
+ * there is. `null` while not playing.
124
+ */
125
+ export type PlayEditRegime = 'ephemeral' | null;
126
+
127
+ /** The two viewport tabs, Edit · Play. Defined ONCE, in the SDK's shared
128
+ * vocabulary (`@volter/editor-sdk`), because the control API speaks it too;
129
+ * re-exported here so editor modules keep one import site. */
130
+ export type { ViewportTab };
131
+
132
+ export type AssetKind = 'model' | 'image' | 'video' | 'audio' | 'json' | 'source';
133
+
134
+ export interface OnlineAssetInfo {
135
+ source: 'polyhaven' | 'ambientcg' | 'local';
136
+ id: string;
137
+ name: string;
138
+ thumbnailUrl: string;
139
+ type: string;
140
+ tags: string[];
141
+ license?: string;
142
+ author?: string;
143
+ sourceUrl?: string;
144
+ }
145
+
146
+ export interface HelperVisibility {
147
+ bounds: boolean;
148
+ lights: boolean;
149
+ cameras: boolean;
150
+ colliders: boolean;
151
+ joints: boolean;
152
+ particles: boolean;
153
+ lod: boolean;
154
+ audio: boolean;
155
+ splines: boolean;
156
+ navmesh: boolean;
157
+ constraints: boolean;
158
+ reflectionProbes: boolean;
159
+ triggerVolumes: boolean;
160
+ skeletons: boolean;
161
+ /**
162
+ * The WEIGHT display — a mesh coloured by its active vertex group
163
+ * (`@volter/editor-blender`'s `blender-runtime-weights.ts`). Added 2026-09-19 (I4)
164
+ * because nothing in this set stood for it: `skeletons` is the bones, and
165
+ * Blender's own viewport overlay has a Bones checkbox but reaches weight
166
+ * colours through Weight Paint MODE, which an inspection surface has no
167
+ * brushes to enter. OFF by default, the way Blender shows no weights until
168
+ * you ask for them.
169
+ */
170
+ weights: boolean;
171
+ }
172
+
173
+ export type ViewportAction =
174
+ | { type: 'focus-entity'; id: string }
175
+ | { type: 'focus-selection' }
176
+ | { type: 'focus-scene'; gameCamera?: () => THREE.Camera | null }
177
+ | { type: 'snap-selection-to-floor' }
178
+ | { type: 'set-view-preset'; preset: 'top' | 'front' | 'right' | 'perspective' }
179
+ | {
180
+ type: 'set-camera-pose';
181
+ position: { x: number; y: number; z: number };
182
+ target: { x: number; y: number; z: number };
183
+ fov?: number;
184
+ };
185
+
186
+ export class EditorShellStore {
187
+ // --- Scene graph references (set via bindScene) ---
188
+ protected _scene: THREE.Scene | null = null;
189
+ /** Adoption stack (see enterPlayScene): each frame is the scene state that
190
+ * was active when an adoption replaced it, restored in LIFO order by
191
+ * exitPlayScene. A STACK (not a single slot) because adoptions nest for
192
+ * R3F projects: the design session adopts its fiber scene at design time,
193
+ * and play adopts the live game scene OVER it while the play-entry
194
+ * transition still needs the design scene alive — the design session then
195
+ * unwinds its own frame from the middle via releaseAdoptedScene. */
196
+ protected _adoptionStack: Array<{
197
+ scene: THREE.Scene;
198
+ objectMap: Map<string, THREE.Object3D>;
199
+ /** Opaque payload from {@link _captureAdoptionExtras} — a document half's
200
+ * own per-adoption snapshot. The shell never inspects it. */
201
+ extras: unknown;
202
+ selection: Set<string>;
203
+ /** The image config that was active BEFORE this adoption — same LIFO
204
+ * discipline as `scene`, so a frame spliced out of the middle simply
205
+ * stops being a restore point. See {@link _applyAdoptedImageConfig}. */
206
+ imageConfig: WorldRendererConfig | undefined;
207
+ }> = [];
208
+ protected _objectMap = new Map<string, THREE.Object3D>();
209
+ protected _renderer: THREE.WebGLRenderer | null = null;
210
+ protected _camera: THREE.Camera | null = null;
211
+ protected _batchedRenderer: BatchedRenderer | null = null;
212
+ protected _lastThumbnailTime = 0;
213
+ protected _lastEditorStateSaveTime = 0;
214
+ protected _orbitTarget: THREE.Vector3 | null = null;
215
+
216
+ // --- UI state (not stored in scene graph) ---
217
+ /** Selection is viewport-local. The public selection accessors always expose
218
+ * the ACTIVE viewport's set, so Edit and Play never leak subjects into one
219
+ * another while existing authoring adapters keep the same store contract. */
220
+ protected _viewportSelections: Record<ViewportTab, Set<string>> = {
221
+ edit: new Set<string>(),
222
+ play: new Set<string>(),
223
+ };
224
+ protected _transformMode: TransformMode = 'combined';
225
+ protected _transformSpace: TransformSpace = 'world';
226
+ protected _snapEnabled = false;
227
+ protected _snapValues = { translate: 1, rotate: 15, scale: 0.25 };
228
+ protected _snapToSurface = false;
229
+ protected _preserveChildrenTransform = false;
230
+ protected _pivotMode: PivotMode = 'active-element';
231
+ protected _gizmoAnchor: GizmoAnchor = 'auto';
232
+ protected _showGrid = true;
233
+ protected _showHelpers = true;
234
+ protected _helperVisibility: HelperVisibility = {
235
+ bounds: false,
236
+ lights: true,
237
+ cameras: true,
238
+ colliders: true,
239
+ joints: true,
240
+ particles: true,
241
+ lod: true,
242
+ audio: true,
243
+ splines: true,
244
+ navmesh: true,
245
+ constraints: true,
246
+ reflectionProbes: true,
247
+ triggerVolumes: true,
248
+ skeletons: false,
249
+ weights: false,
250
+ };
251
+ /**
252
+ * Bumped whenever gizmos must be re-applied without a structural signature
253
+ * change (W3a B1): a single-entity rebuild (`_rebuildEntity` →
254
+ * `updateEntityPreview`) disposes the old Object3D WITH all its helper
255
+ * children, but leaves the objectMap identity/size and every toggle
256
+ * untouched — the viewport's `gizmoSig` gate would otherwise skip both
257
+ * `applyEntityGizmos` and the per-type visibility pass until an unrelated
258
+ * event.
259
+ */
260
+ protected _gizmoEpoch = 0;
261
+ /** Last structural epoch at which each projected parent's direct children changed. */
262
+ protected _objectMapParentChangedAt = new Map<string, number>();
263
+ /** A root change (or an unclassified legacy call) requires a full hierarchy read. */
264
+ protected _objectMapFullChangeEpoch = 0;
265
+ /** Last exact add/remove epoch for each projected id, independently consumed by the viewport. */
266
+ protected _objectMapMembershipChangedAt = new Map<string, number>();
267
+ /** Missing membership detail requires the viewport to reconcile its complete helper indexes. */
268
+ protected _objectMapMembershipFullChangeEpoch = 0;
269
+ /** Epochs older than this were compacted from the bounded membership history. */
270
+ protected _objectMapMembershipHistoryFloor = 0;
271
+ protected _showStats = false;
272
+ protected _shadingMode: ShadingMode = 'solid';
273
+ protected _history: HistoryService | null = null;
274
+ protected _statePersistence: EditorStatePersistence | null = null;
275
+ protected _playState: 'stopped' | 'playing' | 'paused' = 'stopped';
276
+ /** See {@link PlayEditRegime} — set by play-mode.ts on play-enter, cleared on play-exit. */
277
+ protected _playEditRegime: PlayEditRegime = null;
278
+ protected _activeViewportTab: ViewportTab = 'edit';
279
+ protected _vertexSnapActive = false;
280
+ protected _listeners = new Set<() => void>();
281
+ protected _viewportActionListeners = new Set<(action: ViewportAction) => void>();
282
+ /** A relayed selection command applies its value synchronously but lets the
283
+ * hierarchy/Inspector render on the next task, after the control ack has
284
+ * left the page. Ordinary UI writes never use this timer. */
285
+ protected _deferredNotifyTimer: ReturnType<typeof setTimeout> | null = null;
286
+ /** The scope the pending deferred notify will fire with. A coalescing window
287
+ * that swallowed even one `'content'` write must fire as `'content'` — see
288
+ * {@link widenNotifyScope}. */
289
+ protected _deferredNotifyScope: NotifyScope = 'content';
290
+ protected _version = 0;
291
+ /**
292
+ * Stable across live object-map churn that cannot change shell presentation.
293
+ * Hierarchy/viewport consumers keep using {@link getSnapshot}; chrome that
294
+ * reads play state, selection, tools, or document state uses this narrower
295
+ * snapshot and therefore does not rerender for every runtime projectile.
296
+ */
297
+ protected _shellVersion = 0;
298
+ /**
299
+ * Bumped only when post-processing-relevant state changes (environment / scene
300
+ * structure / play-stop scene swap). The viewport rebuilds its EffectComposer
301
+ * only when this changes — NOT on every notify — so a transform drag/scrub (which
302
+ * notifies per frame but never touches these) no longer recompiles shaders 60×/s.
303
+ */
304
+ protected _composerVersion = 0;
305
+ /**
306
+ * Bumped on every notify EXCEPT one whose only change was WHICH NODES ARE
307
+ * SELECTED — see {@link contentVersion}.
308
+ */
309
+ protected _contentVersion = 0;
310
+ /**
311
+ * Bumped when existing hierarchy-row presentation facets may have changed.
312
+ * Exact object-map membership deltas do not move it: newly admitted rows
313
+ * compute their facets on first render, while surviving rows keep the same
314
+ * source editability, warnings, and reflected property values.
315
+ */
316
+ protected _hierarchyRowFacetVersion = 0;
317
+ bindScene(
318
+ scene: THREE.Scene,
319
+ renderer?: THREE.WebGLRenderer,
320
+ batchedRenderer?: BatchedRenderer | null,
321
+ camera?: THREE.Camera,
322
+ ): void {
323
+ this._scene = scene;
324
+ this._renderer = renderer ?? null;
325
+ this._batchedRenderer = batchedRenderer ?? null;
326
+ if (camera) this._camera = camera;
327
+ }
328
+
329
+ /** Set the orbit controls target reference for camera state persistence. */
330
+ setOrbitTarget(target: THREE.Vector3): void {
331
+ this._orbitTarget = target;
332
+ }
333
+
334
+ /** Update only the editor presentation camera (for perspective/orthographic
335
+ * switching) without rebinding or replacing an adopted game scene. */
336
+ setViewportCamera(camera: THREE.Camera): void {
337
+ this._camera = camera;
338
+ }
339
+
340
+ // --- React useSyncExternalStore integration ---
341
+ subscribe = (listener: () => void): (() => void) => {
342
+ this._listeners.add(listener);
343
+ return () => this._listeners.delete(listener);
344
+ };
345
+
346
+ getSnapshot = (): number => this._version;
347
+
348
+ getShellSnapshot = (): number => this._shellVersion;
349
+
350
+ /**
351
+ * The version a TREE-SCALE derivation caches against.
352
+ *
353
+ * `_version` moves on every notify, selection included, so a panel that keys
354
+ * a whole-tree walk on it re-walks the tree every time a human clicks a node
355
+ * — measured on the scale harness as 100-430ms of main-thread block per
356
+ * `select` on a 20 000-node world, in a verb whose reply came back in 3ms.
357
+ * The tree the walk reads did not change; only the highlight did.
358
+ *
359
+ * This counter therefore advances on every notify EXCEPT one whose sole
360
+ * change was the contents of `_viewportSelections` — the five selection
361
+ * writers below, and nothing else, notify with `'selection'`. Every other
362
+ * mutation (including `notifyIngestEdit`, history restores, play-state
363
+ * changes and viewport-tab switches) advances it, so a cache keyed on it can
364
+ * only ever be stale for a change that never happened. Subscribers still
365
+ * receive a selection notify and still re-render — this decides only what
366
+ * they are allowed to REUSE while doing so.
367
+ */
368
+ get contentVersion(): number {
369
+ return this._contentVersion;
370
+ }
371
+
372
+ /** See {@link _hierarchyRowFacetVersion}. */
373
+ get hierarchyRowFacetVersion(): number {
374
+ return this._hierarchyRowFacetVersion;
375
+ }
376
+
377
+ get projectHistory(): HistoryService | null {
378
+ return this._history;
379
+ }
380
+
381
+ protected _notify(
382
+ scope: NotifyScope = 'content',
383
+ concern: NotifyConcern = 'shell',
384
+ affectsHierarchyRowFacets = scope !== 'selection',
385
+ ): void {
386
+ if (this._deferredNotifyTimer !== null) {
387
+ clearTimeout(this._deferredNotifyTimer);
388
+ this._deferredNotifyTimer = null;
389
+ }
390
+ this._beforeNotify();
391
+ this._version++;
392
+ if (concern === 'shell') this._shellVersion++;
393
+ if (scope !== 'selection') this._contentVersion++;
394
+ if (affectsHierarchyRowFacets) this._hierarchyRowFacetVersion++;
395
+ for (const fn of this._listeners) fn();
396
+ }
397
+
398
+ /** Coalesce control-plane presentation behind the command-result microtask.
399
+ * The state mutation has already landed when this is called. */
400
+ protected _notifyDeferred(scope: NotifyScope = 'content'): void {
401
+ if (this._deferredNotifyTimer !== null) {
402
+ this._deferredNotifyScope = widenNotifyScope(this._deferredNotifyScope, scope);
403
+ return;
404
+ }
405
+ this._deferredNotifyScope = scope;
406
+ this._deferredNotifyTimer = setTimeout(() => {
407
+ this._deferredNotifyTimer = null;
408
+ const pending = this._deferredNotifyScope;
409
+ this._deferredNotifyScope = 'content';
410
+ this._notify(pending);
411
+ }, 0);
412
+ }
413
+
414
+ attachHistory(history: HistoryService): void {
415
+ if (this._history === history) return;
416
+ if (this._history) throw new Error('EditorStore is already attached to project history.');
417
+ this._history = history;
418
+ this._onHistoryAttached();
419
+ }
420
+
421
+ /**
422
+ * Where view/tool state and the autosave thumbnail go. The shell installs
423
+ * its transport here (`EditorContext` binds the server SDK); the store
424
+ * itself names no endpoint, so a document-local store (Asset Lab) or a
425
+ * fixture store simply never persists. Same collaborator shape as
426
+ * `attachHistory`, same one-owner rule.
427
+ */
428
+ attachStatePersistence(persistence: EditorStatePersistence): void {
429
+ if (this._statePersistence === persistence) return;
430
+ if (this._statePersistence) {
431
+ throw new Error('EditorStore is already attached to a state persistence.');
432
+ }
433
+ this._statePersistence = persistence;
434
+ }
435
+
436
+ onViewportAction = (fn: (action: ViewportAction) => void): (() => void) => {
437
+ this._viewportActionListeners.add(fn);
438
+ return () => this._viewportActionListeners.delete(fn);
439
+ };
440
+
441
+ protected _emitViewportAction(action: ViewportAction): void {
442
+ for (const fn of this._viewportActionListeners) fn(action);
443
+ }
444
+
445
+ /** Focus the viewport camera on a specific entity. */
446
+ focusOnEntity(id: string): void {
447
+ this._emitViewportAction({ type: 'focus-entity', id });
448
+ }
449
+
450
+ /** Focus the viewport camera on the current selection. */
451
+ focusOnSelection(): void {
452
+ this._emitViewportAction({ type: 'focus-selection' });
453
+ }
454
+
455
+ /**
456
+ * Frame the whole active scene — the first thing a reader should see when a
457
+ * world they did not author is adopted (an ingest mount). The viewport keeps
458
+ * asking until the world HAS content, because a game builds itself
459
+ * asynchronously; any camera input from the reader cancels it (see
460
+ * `the world root's stage`). A no-op for a scene the reader already has a camera on.
461
+ *
462
+ * `gameCamera` is a LIVE LOOKUP, not a camera: an adopted game's own camera
463
+ * may not exist yet on the frame it is captured on, so the viewport asks
464
+ * again while its window is open. When one turns up, its view is adopted
465
+ * instead of any measurement — see `scene-framing.ts`.
466
+ */
467
+ focusOnScene(gameCamera?: () => THREE.Camera | null): void {
468
+ this._emitViewportAction({
469
+ type: 'focus-scene',
470
+ ...(gameCamera ? { gameCamera } : {}),
471
+ });
472
+ }
473
+
474
+ /** Switch the viewport camera to a preset view (top, front, right, perspective). */
475
+ setViewPreset(preset: 'top' | 'front' | 'right' | 'perspective'): void {
476
+ this._emitViewportAction({ type: 'set-view-preset', preset });
477
+ }
478
+
479
+ /** Move the viewport camera to an arbitrary position/target/fov pose. */
480
+ setCameraPose(
481
+ position: { x: number; y: number; z: number },
482
+ target: { x: number; y: number; z: number },
483
+ fov?: number,
484
+ ): void {
485
+ this._emitViewportAction({
486
+ type: 'set-camera-pose',
487
+ position,
488
+ target,
489
+ ...(fov !== undefined ? { fov } : {}),
490
+ });
491
+ }
492
+
493
+ /**
494
+ * Live viewport camera pose, read straight from the bound Three.js camera +
495
+ * orbit target (`bindScene`/`setOrbitTarget` — wired by `world-root-stage.ts`).
496
+ * Null only when nothing has bound a camera yet (e.g. a headless store in a
497
+ * unit test that never mounted `the world root's stage`).
498
+ */
499
+ get cameraPose(): { position: THREE.Vector3; target: THREE.Vector3; fov: number } | null {
500
+ if (!this._camera) return null;
501
+ const fov = (this._camera as THREE.PerspectiveCamera).fov;
502
+ return {
503
+ position: this._camera.position.clone(),
504
+ target: this._orbitTarget ? this._orbitTarget.clone() : new THREE.Vector3(),
505
+ fov: typeof fov === 'number' ? fov : 0,
506
+ };
507
+ }
508
+
509
+ // --- Scene graph accessors ---
510
+
511
+ /** The object map: entity ID → Three.js Object3D. */
512
+ get objectMap(): Map<string, THREE.Object3D> {
513
+ return this._objectMap;
514
+ }
515
+
516
+ /** The bound Three.js scene. */
517
+ get scene(): THREE.Scene | null {
518
+ return this._scene;
519
+ }
520
+
521
+ /** Monotonic counter bumped when post-processing inputs change (see field doc). */
522
+ get composerVersion(): number {
523
+ return this._composerVersion;
524
+ }
525
+
526
+ // --- Selection ---
527
+ get selectedEntityId(): string | null {
528
+ const selection = this._viewportSelections[this._activeViewportTab];
529
+ if (selection.size === 0) return null;
530
+ return [...selection].at(-1)!;
531
+ }
532
+ get selectedEntityIds(): ReadonlySet<string> {
533
+ return this._viewportSelections[this._activeViewportTab];
534
+ }
535
+ get transformMode(): TransformMode {
536
+ return this._transformMode;
537
+ }
538
+ get transformSpace(): TransformSpace {
539
+ return this._transformSpace;
540
+ }
541
+ get snapEnabled(): boolean {
542
+ return this._snapEnabled;
543
+ }
544
+ get snapValues() {
545
+ return this._snapValues;
546
+ }
547
+ get snapToSurface(): boolean {
548
+ return this._snapToSurface;
549
+ }
550
+ get preserveChildrenTransform(): boolean {
551
+ return this._preserveChildrenTransform;
552
+ }
553
+ get pivotMode(): PivotMode {
554
+ return this._pivotMode;
555
+ }
556
+ get gizmoAnchor(): GizmoAnchor {
557
+ return this._gizmoAnchor;
558
+ }
559
+ get canUndo(): boolean {
560
+ return this._history?.getSnapshot().canUndo ?? false;
561
+ }
562
+ get canRedo(): boolean {
563
+ return this._history?.getSnapshot().canRedo ?? false;
564
+ }
565
+ get showGrid(): boolean {
566
+ return this._showGrid;
567
+ }
568
+ get showHelpers(): boolean {
569
+ return this._showHelpers;
570
+ }
571
+ get helperVisibility(): Readonly<HelperVisibility> {
572
+ return this._helperVisibility;
573
+ }
574
+ /** See {@link _gizmoEpoch} — folded into the viewport's gizmo signature. */
575
+ get gizmoEpoch(): number {
576
+ return this._gizmoEpoch;
577
+ }
578
+
579
+ /**
580
+ * Return the exact projected parents changed after `epoch`, or `null` when a
581
+ * root/unclassified change crossed that boundary and callers must rebuild.
582
+ * Parent timestamps make this safe across React batching: no consumer drains
583
+ * a shared queue, and several projectile commits collapse into one union.
584
+ */
585
+ ingestObjectMapChangesSince(epoch: number): IngestObjectMapChanges | null {
586
+ if (epoch < 0 || epoch > this._gizmoEpoch || epoch < this._objectMapFullChangeEpoch) {
587
+ return null;
588
+ }
589
+ const changedParentIds = new Set<string>();
590
+ for (const [id, changedAt] of this._objectMapParentChangedAt) {
591
+ if (changedAt > epoch) changedParentIds.add(id);
592
+ }
593
+ return { epoch: this._gizmoEpoch, changedParentIds };
594
+ }
595
+
596
+ /** Return exact added/removed identities since `epoch`, or `null` across an unclassified edit. */
597
+ ingestObjectMapMembershipChangesSince(epoch: number): IngestObjectMapMembershipChanges | null {
598
+ if (
599
+ epoch < 0 ||
600
+ epoch > this._gizmoEpoch ||
601
+ epoch < this._objectMapMembershipFullChangeEpoch ||
602
+ epoch < this._objectMapMembershipHistoryFloor
603
+ ) {
604
+ return null;
605
+ }
606
+ const changedIds = new Set<string>();
607
+ for (const [id, changedAt] of this._objectMapMembershipChangedAt) {
608
+ if (changedAt > epoch) changedIds.add(id);
609
+ }
610
+ return { epoch: this._gizmoEpoch, changedIds };
611
+ }
612
+
613
+ isSkeletonVisible(id: string): boolean {
614
+ return Boolean(getUserData(this._objectMap.get(id), 'skeletonVisible'));
615
+ }
616
+
617
+ setSkeletonVisible(id: string, visible: boolean): void {
618
+ const object = this._objectMap.get(id);
619
+ if (!object) return;
620
+ setUserData(object, 'skeletonVisible', visible);
621
+ object.traverse((child) => {
622
+ if (getUserData(child, 'editorHelperType') !== 'skeletons') return;
623
+ setUserData(child, 'skeletonEnabled', visible);
624
+ child.visible = visible || this._helperVisibility.skeletons;
625
+ });
626
+ this._notify();
627
+ }
628
+ get showStats(): boolean {
629
+ return this._showStats;
630
+ }
631
+ get shadingMode(): ShadingMode {
632
+ return this._shadingMode;
633
+ }
634
+ /** Notify subscribers after an ingest adapter mutated a live foreign object
635
+ * (material/visibility). Runtime-only — no dirty/autosave, because the edit
636
+ * lives on the live `Object3D` and nothing here owns a document. */
637
+ notifyIngestEdit(): void {
638
+ this._notify();
639
+ }
640
+
641
+ /** Notify after a live adapter changed `objectMap` membership.
642
+ *
643
+ * Unlike an ordinary reflected-property edit, a membership change requires
644
+ * the viewport to re-apply native helper visibility. The map is mutated in
645
+ * place, and a remove+add burst can keep its size unchanged, so neither map
646
+ * identity nor size is a complete signal. `_gizmoEpoch` is the viewport's
647
+ * existing explicit rebuild input; bump it only at this structural seam. */
648
+ notifyIngestObjectMapEdit(delta?: IngestObjectMapStructureDelta): void {
649
+ this._gizmoEpoch++;
650
+ if (delta === undefined || delta.rootsChanged) {
651
+ this._objectMapFullChangeEpoch = this._gizmoEpoch;
652
+ this._objectMapParentChangedAt.clear();
653
+ } else {
654
+ for (const id of delta.changedParentIds) {
655
+ this._objectMapParentChangedAt.set(id, this._gizmoEpoch);
656
+ }
657
+ }
658
+ if (delta?.addedIds === undefined || delta.removedIds === undefined) {
659
+ this._objectMapMembershipFullChangeEpoch = this._gizmoEpoch;
660
+ this._objectMapMembershipChangedAt.clear();
661
+ } else {
662
+ for (const id of delta.addedIds) this._objectMapMembershipChangedAt.set(id, this._gizmoEpoch);
663
+ for (const id of delta.removedIds) {
664
+ this._objectMapMembershipChangedAt.set(id, this._gizmoEpoch);
665
+ }
666
+ const historyFloor = Math.max(0, this._gizmoEpoch - OBJECT_MAP_MEMBERSHIP_HISTORY);
667
+ if (historyFloor > this._objectMapMembershipHistoryFloor) {
668
+ this._objectMapMembershipHistoryFloor = historyFloor;
669
+ for (const [id, changedAt] of this._objectMapMembershipChangedAt) {
670
+ if (changedAt <= historyFloor) this._objectMapMembershipChangedAt.delete(id);
671
+ }
672
+ }
673
+ }
674
+ const affectsShell =
675
+ delta === undefined ||
676
+ delta.rootsChanged ||
677
+ [...(delta.addedIds ?? []), ...(delta.removedIds ?? [])].some((id) =>
678
+ this.selectedEntityIds.has(id),
679
+ );
680
+ this._notify('content', affectsShell ? 'shell' : 'object-map', false);
681
+ }
682
+
683
+ // --- Play mode scene swap ---
684
+
685
+ /** Callback to sync transform edits to game ECS. Set by play-mode, cleared on exit.
686
+ * Receives the NODE ID as well as the live object: the `PhysicsAdapter` seam
687
+ * it ultimately feeds is id-keyed since P-4, and the id is already in hand
688
+ * at the one call site — so nothing downstream has to map an object back. */
689
+ protected _ecsSyncTransform: ((id: string, obj: THREE.Object3D) => void) | null = null;
690
+
691
+ /** Set a callback that syncs editor transform changes to the game's ECS state. */
692
+ setEcsSyncTransform(fn: ((id: string, obj: THREE.Object3D) => void) | null): void {
693
+ this._ecsSyncTransform = fn;
694
+ }
695
+
696
+ /**
697
+ * Swap the store's active scene to the game scene. Editor scene is preserved.
698
+ *
699
+ * Also snapshots whatever the active document half hands back from
700
+ * {@link _captureAdoptionExtras}. A half that keeps mutating its LIVE
701
+ * metadata during play (so its inspector fields keep working) must have
702
+ * those edits discarded when play stops — otherwise a play-time edit
703
+ * silently persists with `isDirty` false. `exitPlayScene` restores the
704
+ * snapshot.
705
+ */
706
+ enterPlayScene(
707
+ gameScene: THREE.Scene,
708
+ imageConfig?: WorldRendererConfig | undefined,
709
+ projectedObjects?: ReadonlyMap<string, THREE.Object3D>,
710
+ ): void {
711
+ if (!this._scene) return;
712
+ this._adoptionStack.push({
713
+ scene: this._scene,
714
+ objectMap: this._objectMap,
715
+ extras: this._captureAdoptionExtras(),
716
+ selection: new Set(this.selectedEntityIds),
717
+ imageConfig: this._adoptedImageConfig,
718
+ });
719
+ this._scene = gameScene;
720
+ this._applyAdoptedImageConfig(imageConfig);
721
+
722
+ // The store INDEXES an adopted scene; it no longer models it. A foreign
723
+ // adapter supplies its pure projection explicitly, so editor identity is
724
+ // never written into the game's Object3Ds. Source-backed/adopted worlds
725
+ // already carry native authoring stamps and retain the fallback walk.
726
+ const gameMap = projectedObjects
727
+ ? new Map(projectedObjects)
728
+ : (() => {
729
+ const stamped = new Map<string, THREE.Object3D>();
730
+ const visit = (obj: THREE.Object3D): void => {
731
+ if (isEditorOwnedObject(obj)) return;
732
+ const eid = entityIdOf(obj);
733
+ if (eid !== undefined) stamped.set(eid, obj);
734
+ for (const child of obj.children) visit(child);
735
+ };
736
+ for (const child of gameScene.children) visit(child);
737
+ return stamped;
738
+ })();
739
+ this._objectMap = gameMap;
740
+
741
+ this._viewportSelections[this._activeViewportTab] = new Set(
742
+ [...this.selectedEntityIds].filter((id) => gameMap.has(id)),
743
+ );
744
+ // A viewport LOD pin must not follow the entity id into the adopted game
745
+ // scene — there `WebGLRenderer.projectObject` owns level visibility (it
746
+ // calls `lod.update(camera)` for every visible LOD whose `autoUpdate` is
747
+ // still true, which is what a pin turns OFF), and per-frame enforcement
748
+ // would fight it.
749
+ this._lodForcedLevels.clear();
750
+ this._composerVersion++; // scene swapped → composer must rebuild against the game scene
751
+ this._notify();
752
+ }
753
+
754
+ /** Restore the previously-adopted scene (LIFO — see enterPlayScene). */
755
+ exitPlayScene(): void {
756
+ const frame = this._adoptionStack.pop();
757
+ if (!frame) return;
758
+ this._scene = frame.scene;
759
+ this._objectMap = frame.objectMap;
760
+ this._applyAdoptedImageConfig(frame.imageConfig);
761
+ // Discard any play-time env/name/ui edits — restore the pre-play snapshot.
762
+ this._restoreAdoptionExtras(frame.extras);
763
+ this._viewportSelections[this._activeViewportTab] = new Set(frame.selection);
764
+ this._composerVersion++; // scene swapped back → composer must rebuild against the editor scene
765
+ this._notify();
766
+ }
767
+
768
+ /**
769
+ * Release ONE adopter's adoption, wherever it sits in the stack. When
770
+ * `scene` is the ACTIVE scene this is exactly `exitPlayScene`; when a later
771
+ * adoption (play over the R3F design session) has already replaced it, the
772
+ * frame that would restore `scene` is unlinked from the middle of the stack
773
+ * instead — nothing visible changes now, and the LATER adopter's exit
774
+ * restores the state underneath as if this adoption never happened. No-op
775
+ * when `scene` was never adopted (or was already released).
776
+ */
777
+ releaseAdoptedScene(scene: THREE.Scene): void {
778
+ if (this._scene === scene) {
779
+ this.exitPlayScene();
780
+ return;
781
+ }
782
+ const index = this._adoptionStack.findIndex((frame) => frame.scene === scene);
783
+ if (index === -1) return;
784
+ this._adoptionStack.splice(index, 1);
785
+ }
786
+
787
+ /**
788
+ * THE ADOPTED CONTENT'S OWN COLOUR PIPELINE — what it is, not where it is
789
+ * applied.
790
+ *
791
+ * A mounted world may report the pipeline it was authored for
792
+ * (`MountedThreeRoot.rendererConfig`), and `applyWorldRendererConfig` puts it
793
+ * on the renderer its own host handed it. In the editor those are two
794
+ * DIFFERENT renderers: the design session mounts against a non-rasterizing
795
+ * one (`authoring/design-time-renderer.ts`) and the viewport draws the
796
+ * mounted scene with its own. So the declaration landed on a surface with no
797
+ * pixels while the surface with pixels kept the engine's defaults — measured
798
+ * on the translated Godot platformer, whose roughness-0 metal reads as chrome
799
+ * under the editor's ACES while the game renders it under its own curve.
800
+ *
801
+ * The store only REMEMBERS the declaration and bumps `_composerVersion` when
802
+ * it changes; `the world root's stage.rebuildComposer` is the one place that derives
803
+ * the viewport renderer's whole configuration, and it applies this last. That
804
+ * split is why there is no restore bookkeeping here: a rebuild always resets
805
+ * the renderer to the engine defaults first, so "unwind" is just the next
806
+ * rebuild seeing a different (or absent) config.
807
+ */
808
+ protected _adoptedImageConfig: WorldRendererConfig | undefined;
809
+
810
+ /** What the active adopted content declared about the renderer that draws it,
811
+ * or `undefined` when it declared nothing (leave the editor's own alone). */
812
+ get adoptedImageConfig(): WorldRendererConfig | undefined {
813
+ return this._adoptedImageConfig;
814
+ }
815
+
816
+ protected _applyAdoptedImageConfig(next: WorldRendererConfig | undefined): void {
817
+ if (next === this._adoptedImageConfig) return;
818
+ this._adoptedImageConfig = next;
819
+ this._composerVersion++;
820
+ }
821
+
822
+ /** True while an ADOPTED live game scene is the active scene — every
823
+ * `enterPlayScene` caller: play adoption, ingest mounts, and the R3F
824
+ * design session. Adopted scenes own their lighting/rendering completely;
825
+ * the viewport uses this to keep editor-fabricated presentation (the
826
+ * helper light rig) out of them. */
827
+ get isAdoptedSceneActive(): boolean {
828
+ return this._adoptionStack.length > 0;
829
+ }
830
+
831
+ // --- Play mode state ---
832
+ /**
833
+ * WHICH MODE THE EDITOR IS IN — the shell's own intent, not a claim that a
834
+ * game is running. Read it for anything whose answer must be true from the
835
+ * MOMENT play is entered: the structural-edit block, the design session's
836
+ * suspend, the Play bar, the viewport tab, the tool `when` predicates.
837
+ *
838
+ * It is NOT the answer to "is something actually running", and it must never
839
+ * be reported as one. `'playing'` is written by three subsystems — play mode,
840
+ * ingest and module mode — and every one of them writes it BEFORE its session
841
+ * exists: `enterPlayMode` sets it ~130 lines and three awaits before
842
+ * `_instance.session` is assigned, and an ingest mount sets it before the
843
+ * mount is even attempted. That is deliberate (it is what swaps the viewport
844
+ * onto the game surface and freezes edit-mode work while the boot runs), and
845
+ * it is exactly why reporting the flag verbatim once printed
846
+ * `playState: "playing"` for a mount that had already thrown.
847
+ *
848
+ * The other question has its own door and only one caller may use it:
849
+ * `reported-play-state.ts`'s `deriveReportedPlayState`, which resolves the
850
+ * live session slots and is what `collectState` (`vgai status`) reports. If
851
+ * you are about to send this value outside the editor, you want that instead.
852
+ */
853
+ get playState(): 'stopped' | 'playing' | 'paused' {
854
+ return this._playState;
855
+ }
856
+
857
+ /**
858
+ * Guard structural/history mutations while a game is running. During play the
859
+ * store's `_scene` is the LIVE game scene (see enterPlayScene), so undo/redo
860
+ * (which call clearPreview + buildPreview and rebuild the whole scene) or
861
+ * delete/duplicate (which splice the scene graph and objectMap) would tear the
862
+ * scene out from under the running GameSession. Returns true (and warns) when
863
+ * the named op must be blocked. The intended live-edit path during play is the
864
+ * incremental ECS-sync transform path, not these full-scene mutations.
865
+ */
866
+ protected _blockedDuringPlay(op: string): boolean {
867
+ if (this._playState === 'stopped') return false;
868
+ console.warn(`EditorStore: "${op}" is disabled during play mode — stop the game first.`);
869
+ return true;
870
+ }
871
+ setPlayState(state: 'stopped' | 'playing' | 'paused'): void {
872
+ const wasActive = this._playState !== 'stopped';
873
+ this._playState = state;
874
+ if (state === 'playing' && this._activeViewportTab === 'edit') {
875
+ this._activeViewportTab = 'play';
876
+ }
877
+ if (state === 'stopped') {
878
+ this._activeViewportTab = 'edit';
879
+ // Apply anything deferred while play/ingest was active. play-mode.ts /
880
+ // ingest/mount-ingest-root.ts call exitPlayScene() (restoring the editor scene) BEFORE
881
+ // this — so by the time we get here `_scene` is already the editor scene,
882
+ // never the just-torn-down game scene (see applyExternalUpdate/applyAssetMove).
883
+ if (wasActive) this._onPlayStopped();
884
+ }
885
+ this._notify();
886
+ }
887
+
888
+ /** See {@link PlayEditRegime}. Null while not playing. */
889
+ get playEditRegime(): PlayEditRegime {
890
+ return this._playEditRegime;
891
+ }
892
+
893
+ /** Set by play-mode.ts's enterPlayMode/exitPlayMode — see {@link PlayEditRegime}. */
894
+ setPlayEditRegime(regime: PlayEditRegime): void {
895
+ this._playEditRegime = regime;
896
+ this._notify();
897
+ }
898
+
899
+ // --- Viewport tab ---
900
+ get activeViewportTab(): ViewportTab {
901
+ return this._activeViewportTab;
902
+ }
903
+ setActiveViewportTab(tab: ViewportTab): void {
904
+ if (tab === this._activeViewportTab) return;
905
+ this._activeViewportTab = tab;
906
+ this._notify();
907
+ }
908
+ // --- Vertex snap ---
909
+ get vertexSnapActive(): boolean {
910
+ return this._vertexSnapActive;
911
+ }
912
+ setVertexSnapActive(active: boolean): void {
913
+ this._vertexSnapActive = active;
914
+ this._notify();
915
+ }
916
+
917
+ // --- Selection ---
918
+ // Every writer below notifies with `'selection'` because every writer below
919
+ // mutates `_viewportSelections` and NOTHING ELSE. That is the whole warrant
920
+ // for the scope — see `contentVersion`.
921
+ select(id: string | null, notification: 'immediate' | 'deferred' = 'immediate'): void {
922
+ const selection = this._viewportSelections[this._activeViewportTab];
923
+ selection.clear();
924
+ if (id) selection.add(id);
925
+ if (notification === 'deferred') this._notifyDeferred('selection');
926
+ else this._notify('selection');
927
+ }
928
+
929
+ selectMultiple(ids: string[], notification: 'immediate' | 'deferred' = 'immediate'): void {
930
+ this._viewportSelections[this._activeViewportTab] = new Set(ids);
931
+ if (notification === 'deferred') this._notifyDeferred('selection');
932
+ else this._notify('selection');
933
+ }
934
+
935
+ /**
936
+ * Apply a control-API selection NOW and return the notification that lets presentation catch up.
937
+ *
938
+ * The normal UI methods above remain synchronous. A relayed selection is different: its caller
939
+ * is waiting for an acknowledgement, while notifying React can synchronously reveal hundreds of
940
+ * hierarchy rows and build an inspector preview before `select()` returns. The command listener
941
+ * uses this method so `selectedEntityIds` already answers the new truth when it acks, then runs
942
+ * the returned notification only after the result POST has settled. No selection is optimistic:
943
+ * the set is mutated before this method returns; only presentation is deferred.
944
+ */
945
+ applySelectionBeforePresentation(ids: readonly string[]): () => void {
946
+ this._viewportSelections[this._activeViewportTab] = new Set(ids);
947
+ let presented = false;
948
+ return () => {
949
+ if (presented) return;
950
+ presented = true;
951
+ this._notify('selection');
952
+ };
953
+ }
954
+
955
+ addToSelection(id: string): void {
956
+ this._viewportSelections[this._activeViewportTab].add(id);
957
+ this._notify('selection');
958
+ }
959
+
960
+ toggleSelection(id: string): void {
961
+ const selection = this._viewportSelections[this._activeViewportTab];
962
+ if (selection.has(id)) {
963
+ selection.delete(id);
964
+ } else {
965
+ selection.add(id);
966
+ }
967
+ this._notify('selection');
968
+ }
969
+
970
+ protected static readonly THUMBNAIL_INTERVAL_MS = 60_000;
971
+ protected static readonly THUMBNAIL_SIZE = 256;
972
+
973
+ /**
974
+ * Render the bound camera's current view to a PNG data URL at `size`x`size`.
975
+ * Autosave thumbnails request game content only; an explicit SDK/editor
976
+ * viewport capture requests every camera layer so grid/helpers/gizmos are
977
+ * represented honestly. Both use the same offscreen render-target technique,
978
+ * so neither needs `preserveDrawingBuffer` on the live WebGL canvas.
979
+ *
980
+ * THE COLOUR PIPELINE IS THE WHOLE REASON THERE ARE TWO TARGETS. Three
981
+ * applies `renderer.toneMapping` and `renderer.outputColorSpace` ONLY when
982
+ * the destination is the canvas: rendering into an ordinary
983
+ * `WebGLRenderTarget` compiles every material with `NoToneMapping` and a
984
+ * LINEAR output transfer (`WebGLPrograms.getParameters` and
985
+ * `WebGLRenderer.setProgram` both gate on `currentRenderTarget === null`).
986
+ * So the single-target version of this function answered "what does the
987
+ * viewport look like" with a raw linear frame — measurably darker and
988
+ * flatter than the pixels on screen, and immune to every tone-mapping
989
+ * change (verified live: flipping the viewport to `NoToneMapping` moved the
990
+ * captured PNG by 0 bytes). That made this door — the only look at the EDIT
991
+ * viewport, and the source of every project thumbnail — unusable as
992
+ * evidence about the image.
993
+ *
994
+ * The fix is three's own: render the scene into a HALF-FLOAT linear buffer
995
+ * (so highlights survive to be tone-mapped) and resolve it through
996
+ * `OutputPass`, which reads `toneMapping`/`toneMappingExposure`/
997
+ * `outputColorSpace` off the renderer and applies exactly what the on-screen
998
+ * frame gets.
999
+ */
1000
+ protected _renderViewportImage(
1001
+ size: number | { width: number; height: number },
1002
+ includeEditorLayers: boolean,
1003
+ ): string | null {
1004
+ if (!this._renderer || !this._scene || !this._camera) return null;
1005
+ // A number is a SQUARE of that size — the default shape; `{width, height}`
1006
+ // renders the buffer and the camera at that aspect so a shaped look needs
1007
+ // no crop (`@volter/editor-sdk`'s `CaptureDimensions`).
1008
+ const width = typeof size === 'number' ? size : size.width;
1009
+ const height = typeof size === 'number' ? size : size.height;
1010
+ // The same floor `authoring/object3d-document-session.ts`'s `captureImage`
1011
+ // already sets, for the same reason: the doubled size is handed straight to
1012
+ // `createImageData`, so a non-finite size throws
1013
+ // `TypeError: Value is not of type 'long'` from the middle of the render
1014
+ // path instead of failing where the value entered.
1015
+ if (!Number.isFinite(width) || width < 1) return null;
1016
+ if (!Number.isFinite(height) || height < 1) return null;
1017
+
1018
+ // render at 2x then downscale for crisp result
1019
+ const renderWidth = width * 2;
1020
+ const renderHeight = height * 2;
1021
+ const renderer = this._renderer;
1022
+ const sceneTarget = new THREE.WebGLRenderTarget(renderWidth, renderHeight, {
1023
+ type: THREE.HalfFloatType,
1024
+ });
1025
+ const renderTarget = new THREE.WebGLRenderTarget(renderWidth, renderHeight);
1026
+ const prevTarget = renderer.getRenderTarget();
1027
+ const prevPixelRatio = renderer.getPixelRatio();
1028
+
1029
+ const camera = this._camera;
1030
+ const prevLayers = camera.layers.mask;
1031
+ const perspectiveCamera = camera instanceof THREE.PerspectiveCamera ? camera : null;
1032
+ const previousAspect = perspectiveCamera?.aspect;
1033
+ if (includeEditorLayers) {
1034
+ camera.layers.enableAll();
1035
+ } else {
1036
+ camera.layers.disableAll();
1037
+ camera.layers.enable(0); // clean project thumbnail: game content only
1038
+ }
1039
+
1040
+ try {
1041
+ if (perspectiveCamera) {
1042
+ perspectiveCamera.aspect = width / height;
1043
+ perspectiveCamera.updateProjectionMatrix();
1044
+ }
1045
+ renderer.setPixelRatio(1);
1046
+ renderer.setRenderTarget(sceneTarget);
1047
+ // The same view policy the on-screen draw applies (`scene-view-fog.ts`).
1048
+ // This capture is the ONLY look anyone gets at the Edit viewport — the
1049
+ // `vgai screenshot` evidence and every project thumbnail — so a capture
1050
+ // that renders the game's fog while the viewport does not would make the
1051
+ // one door onto the image disagree with the image.
1052
+ withSceneFogNeutralized(this._scene, () =>
1053
+ renderer.render(this._scene as THREE.Scene, camera),
1054
+ );
1055
+ viewportCaptureOutputPass().render(renderer, renderTarget, sceneTarget, 0, false);
1056
+ } finally {
1057
+ renderer.setRenderTarget(prevTarget);
1058
+ renderer.setPixelRatio(prevPixelRatio);
1059
+ camera.layers.mask = prevLayers;
1060
+ if (perspectiveCamera && previousAspect !== undefined) {
1061
+ perspectiveCamera.aspect = previousAspect;
1062
+ perspectiveCamera.updateProjectionMatrix();
1063
+ }
1064
+ }
1065
+
1066
+ // Read pixels from the render target
1067
+ const rowBytes = renderWidth * 4;
1068
+ const pixels = new Uint8Array(rowBytes * renderHeight);
1069
+ renderer.readRenderTargetPixels(renderTarget, 0, 0, renderWidth, renderHeight, pixels);
1070
+ renderTarget.dispose();
1071
+ sceneTarget.dispose();
1072
+
1073
+ // Flip vertically (WebGL reads bottom-up) and write to a full-size canvas
1074
+ const fullCanvas = document.createElement('canvas');
1075
+ fullCanvas.width = renderWidth;
1076
+ fullCanvas.height = renderHeight;
1077
+ const fullCtx = fullCanvas.getContext('2d')!;
1078
+ const imageData = fullCtx.createImageData(renderWidth, renderHeight);
1079
+ for (let y = 0; y < renderHeight; y++) {
1080
+ const srcRow = (renderHeight - 1 - y) * rowBytes;
1081
+ const dstRow = y * rowBytes;
1082
+ imageData.data.set(pixels.subarray(srcRow, srcRow + rowBytes), dstRow);
1083
+ }
1084
+ fullCtx.putImageData(imageData, 0, 0);
1085
+
1086
+ // Downscale to the requested size
1087
+ const canvas = document.createElement('canvas');
1088
+ canvas.width = width;
1089
+ canvas.height = height;
1090
+ const ctx2d = canvas.getContext('2d')!;
1091
+ ctx2d.drawImage(fullCanvas, 0, 0, width, height);
1092
+
1093
+ return canvas.toDataURL('image/png');
1094
+ }
1095
+
1096
+ protected _captureThumbnail(): void {
1097
+ const now = Date.now();
1098
+ if (now - this._lastThumbnailTime < EditorShellStore.THUMBNAIL_INTERVAL_MS) return;
1099
+ this._lastThumbnailTime = now;
1100
+ const dataUrl = this._renderViewportImage(EditorShellStore.THUMBNAIL_SIZE, false);
1101
+ if (dataUrl) void this._statePersistence?.saveThumbnail?.(dataUrl);
1102
+ }
1103
+
1104
+ /**
1105
+ * Fresh, on-demand viewport capture (unthrottled, unlike the periodic
1106
+ * autosave thumbnail above) — backs the `capture-viewport` relay command.
1107
+ * Returns null when no renderer/scene/camera is bound yet (e.g. before
1108
+ * `the world root's stage` has mounted).
1109
+ */
1110
+ captureViewportImage(
1111
+ size: number | { width: number; height: number } = EditorShellStore.THUMBNAIL_SIZE,
1112
+ ): string | null {
1113
+ return this._renderViewportImage(size, true);
1114
+ }
1115
+
1116
+ /** The live Scene viewport's own `<canvas>`, or null before a renderer is
1117
+ * bound. A photograph of the whole editor page (`editor-chrome-capture.ts`)
1118
+ * needs to recognise it: its WebGL context has no `preserveDrawingBuffer`,
1119
+ * so its pixels come from {@link captureViewportImage}, not a late read. */
1120
+ viewportCanvas(): HTMLCanvasElement | null {
1121
+ return this._renderer?.domElement ?? null;
1122
+ }
1123
+
1124
+ // --- Editor state persistence ---
1125
+
1126
+ protected static readonly EDITOR_STATE_INTERVAL_MS = 60_000;
1127
+
1128
+ protected _saveEditorStateIfDue(): void {
1129
+ if (!this._statePersistence) return;
1130
+ const now = Date.now();
1131
+ if (now - this._lastEditorStateSaveTime < EditorShellStore.EDITOR_STATE_INTERVAL_MS) return;
1132
+ this._lastEditorStateSaveTime = now;
1133
+
1134
+ const state: Record<string, unknown> = {
1135
+ transformMode: this._transformMode,
1136
+ transformSpace: this._transformSpace,
1137
+ snapEnabled: this._snapEnabled,
1138
+ snapValues: this._snapValues,
1139
+ preserveChildrenTransform: this._preserveChildrenTransform,
1140
+ pivotMode: this._pivotMode,
1141
+ gizmoAnchor: this._gizmoAnchor,
1142
+ showGrid: this._showGrid,
1143
+ showHelpers: this._showHelpers,
1144
+ helperVisibility: { ...this._helperVisibility },
1145
+ showStats: this._showStats,
1146
+ shadingMode: this._shadingMode,
1147
+ };
1148
+
1149
+ Object.assign(state, this._editorStateExtras());
1150
+
1151
+ if (this._camera) {
1152
+ const cam = this._camera as THREE.PerspectiveCamera;
1153
+ state['camera'] = {
1154
+ position: cam.position.toArray(),
1155
+ target: this._orbitTarget ? this._orbitTarget.toArray() : [0, 0, 0],
1156
+ };
1157
+ }
1158
+
1159
+ void this._statePersistence.save(state);
1160
+ }
1161
+
1162
+ // --- Mutations ---
1163
+
1164
+ /**
1165
+ * EDITOR-ONLY level-preview override (W2b): entity id → index into the
1166
+ * live THREE.LOD's (ascending-by-distance) levels to pin in the viewport.
1167
+ * Pure UI state — never written to the descriptor, so it can't leak into
1168
+ * the saved scene or the undo history. Enforced per frame by
1169
+ * `EditorViewport.update` (the glTF loads async and entity rebuilds replace
1170
+ * the THREE.LOD instance, so a one-shot apply would silently un-pin).
1171
+ */
1172
+ protected _lodForcedLevels = new Map<string, number>();
1173
+
1174
+ get lodForcedLevels(): ReadonlyMap<string, number> {
1175
+ return this._lodForcedLevels;
1176
+ }
1177
+
1178
+ getLodForcedLevel(id: string): number | null {
1179
+ return this._lodForcedLevels.get(id) ?? null;
1180
+ }
1181
+
1182
+ /** Pin an entity's viewport LOD display to one level, or `null` for Auto
1183
+ * (distance-based selection — the renderer's own `LOD.update(camera)`). */
1184
+ setLodForcedLevel(id: string, level: number | null): void {
1185
+ if (level === null) {
1186
+ this._lodForcedLevels.delete(id);
1187
+ // Hand control back to the renderer's distance-based selection.
1188
+ const obj = this._objectMap.get(id);
1189
+ const projectedObjects = new Set(this._objectMap.values());
1190
+ const lodObj = obj ? findEntityLod(obj, (node) => projectedObjects.has(node)) : null;
1191
+ if (lodObj) lodObj.autoUpdate = true;
1192
+ } else {
1193
+ this._lodForcedLevels.set(id, level);
1194
+ }
1195
+ this._notify();
1196
+ }
1197
+
1198
+ // --- Transform mode ---
1199
+ setTransformMode(mode: TransformMode): void {
1200
+ this._transformMode = mode;
1201
+ this._notify();
1202
+ }
1203
+
1204
+ setTransformSpace(space: TransformSpace): void {
1205
+ this._transformSpace = space;
1206
+ this._notify();
1207
+ }
1208
+
1209
+ toggleSnap(): void {
1210
+ this._snapEnabled = !this._snapEnabled;
1211
+ this._notify();
1212
+ }
1213
+
1214
+ setSnapValues(values: Partial<{ translate: number; rotate: number; scale: number }>): void {
1215
+ this._snapValues = { ...this._snapValues, ...values };
1216
+ this._notify();
1217
+ }
1218
+
1219
+ toggleSnapToSurface(): void {
1220
+ this._snapToSurface = !this._snapToSurface;
1221
+ this._notify();
1222
+ }
1223
+
1224
+ togglePreserveChildrenTransform(): void {
1225
+ this._preserveChildrenTransform = !this._preserveChildrenTransform;
1226
+ this._notify();
1227
+ }
1228
+
1229
+ snapSelectionToFloor(): void {
1230
+ this._emitViewportAction({ type: 'snap-selection-to-floor' });
1231
+ }
1232
+
1233
+ setPivotMode(mode: PivotMode): void {
1234
+ this._pivotMode = mode;
1235
+ this._notify();
1236
+ }
1237
+
1238
+ setGizmoAnchor(anchor: GizmoAnchor): void {
1239
+ this._gizmoAnchor = anchor;
1240
+ this._notify();
1241
+ }
1242
+
1243
+ toggleGrid(): void {
1244
+ this._showGrid = !this._showGrid;
1245
+ this._notify();
1246
+ }
1247
+
1248
+ toggleHelpers(): void {
1249
+ // The eye is a visibility gate, not a reset of the category checkboxes.
1250
+ this._showHelpers = !this._showHelpers;
1251
+ this._notify();
1252
+ }
1253
+
1254
+ toggleHelperType(type: keyof HelperVisibility): void {
1255
+ this._helperVisibility[type] = !this._helperVisibility[type];
1256
+ if (this._helperVisibility[type]) this._showHelpers = true;
1257
+ this._notify();
1258
+ }
1259
+
1260
+ toggleStats(): void {
1261
+ this._showStats = !this._showStats;
1262
+ this._notify();
1263
+ }
1264
+
1265
+ setShadingMode(mode: ShadingMode): void {
1266
+ this._shadingMode = mode;
1267
+ // View-only state. The viewport applies it during its own render and
1268
+ // restores native materials before returning to editor/game code.
1269
+ this._notify();
1270
+ }
1271
+
1272
+ // --- Subclass hooks -------------------------------------------------------
1273
+ // The ONLY way the shell reaches into a document half. Each is a no-op here,
1274
+ // so the shell runs standalone; a subclass that owns a document overrides the
1275
+ // ones it needs (`store-notify-scope.test.ts` exercises the deferral hooks).
1276
+
1277
+ /** Runs at the top of every `_notify()`. A document half uses it to close a
1278
+ * pending history edit. */
1279
+ protected _beforeNotify(): void {}
1280
+
1281
+ /** Runs after `attachHistory` binds the project `HistoryService`. A document
1282
+ * half uses it to register its document resource driver. */
1283
+ protected _onHistoryAttached(): void {}
1284
+
1285
+ /** Captured into the adoption frame by `enterPlayScene`, handed back by
1286
+ * `exitPlayScene`. A document half snapshots its metadata here so
1287
+ * play-time edits to it are discarded on exit. */
1288
+ protected _captureAdoptionExtras(): unknown {
1289
+ return undefined;
1290
+ }
1291
+
1292
+ /** Restore counterpart of {@link _captureAdoptionExtras}. */
1293
+ protected _restoreAdoptionExtras(_extras: unknown): void {}
1294
+
1295
+ /** Runs when `setPlayState('stopped')` ends an active play/ingest session.
1296
+ * A document half applies mutations it deferred while play was running. */
1297
+ protected _onPlayStopped(): void {}
1298
+
1299
+ /** Extra keys folded into the periodic editor-state save. A document half
1300
+ * contributes whatever it needs restored on the next boot. */
1301
+ protected _editorStateExtras(): Record<string, unknown> {
1302
+ return {};
1303
+ }
1304
+ }