@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,1954 @@
1
+ import type {
2
+ ToolObject3DAuthoringProps,
3
+ ToolObject3DDocumentAuthoring,
4
+ ToolObject3DPreviewSource,
5
+ ToolViewportDressing,
6
+ } from '@volter/editor-sdk/contributions';
7
+ import type { StageTransportSnapshot } from '@volter/editor-sdk/host';
8
+ import { EditorIcon, editorIcons, IconButton, themeVars } from '@volter/editor-sdk/widgets';
9
+ import type { AuthoringAdapter } from '@volter/editor-project/adapter';
10
+ import { contentWorldBounds } from '@volter/editor-threejs/viewport/content-bounds';
11
+ import { isInEditorOwnedSubtree } from '@volter/editor-threejs/viewport/editor-layers';
12
+ import { createStandardEnvironment } from '@volter/editor-threejs/viewport/environment';
13
+ import {
14
+ acquireInspectorPreviewRenderer,
15
+ type InspectorPreviewLease,
16
+ } from '@volter/editor-threejs/viewport/preview-renderer';
17
+ import { setUserData } from '@volter/editor-threejs/ecs/user-data';
18
+ import {
19
+ lazy,
20
+ type RefObject,
21
+ Suspense,
22
+ useCallback,
23
+ useEffect,
24
+ useRef,
25
+ useState,
26
+ useSyncExternalStore,
27
+ } from 'react';
28
+ import * as THREE from 'three';
29
+ import { registerStageTransport, StageTransport } from '../animation/stage-transport';
30
+ import { scanClipSubjects } from '../animation/three-clips-subject';
31
+ import { liveGestureActive, whenLiveGestureIdle } from '../authoring/live-gesture-lock';
32
+ import {
33
+ type Object3DDocumentPresentationState,
34
+ Object3DDocumentSession,
35
+ } from '../authoring/object3d-document-session';
36
+ import {
37
+ announceObject3DDocumentStage,
38
+ registerObject3DDocumentSession,
39
+ } from '../authoring/object3d-document-session-registry';
40
+ import { Object3DGestureController } from '../authoring/object3d-gesture-controller';
41
+ import { SourceObject3DAuthoringAdapter } from '../authoring/source-object3d-authoring-adapter';
42
+ import { registerDesignTimeSurface } from '../coverage/design-time-surfaces';
43
+ import { DocumentRendererSession } from '../document-renderer-session';
44
+ import { useOptionalEditorStats, useOptionalEditorStore } from '../editor-runtime';
45
+ import { EditorShellStore } from '../editor-shell-store';
46
+ import { EditorViewport } from '../editor-viewport';
47
+ import {
48
+ lookDeclaresViewportColors,
49
+ nativeViewportLook,
50
+ subscribeNativeSelectionTheme,
51
+ } from '../native-selection-style';
52
+ import {
53
+ type Object3DDocumentPersistenceSession,
54
+ object3DDocumentWritePolicy,
55
+ } from '../object3d-document-write-policy';
56
+ import { registerPerformanceSource } from '../performance-sources';
57
+ import { shellStoreForHost } from '../shell-store-door';
58
+ import {
59
+ assetSubjectApplies,
60
+ documentStageContext,
61
+ focusedStageStore,
62
+ identityRowApplies,
63
+ studioStageApplies,
64
+ } from '../stage-context';
65
+ import { registerStageStore } from '../stage-store-registry';
66
+ import { perspectiveDistanceToFitBox } from '../three-viewport/camera-fit';
67
+ import {
68
+ acquireInteractiveViewportRenderer,
69
+ type InteractiveViewportRendererLease,
70
+ } from '../three-viewport/interactive-renderer';
71
+ import type { ThreeViewportProjection } from '../three-viewport-presentation';
72
+ import {
73
+ activeViewportBreakdownDocumentId,
74
+ markViewportConstructReady,
75
+ markViewportConstructStarted,
76
+ markViewportFirstRenderStart,
77
+ markViewportRafResume,
78
+ markViewportReactActive,
79
+ markViewportSegment,
80
+ recordViewportFirstFrame,
81
+ } from '../viewport-activation-timings';
82
+ import { bindViewportRig, runViewportFrame } from '../viewport-door';
83
+ import {
84
+ notifyWorkspaceDocumentSelectionChanged,
85
+ openWorkspaceDocuments,
86
+ registerWorkspaceDocumentSelection,
87
+ } from '../workspace-document-registry';
88
+ import { AssetEditorShell } from './AssetEditorShell';
89
+ import { StageOverlays } from './StageOverlays';
90
+ import { TransportStrip } from './TransportStrip';
91
+
92
+ // The stage's own keyboard actions, behind the same lazy boundary as the
93
+ // overlays and for the same reason — a bounded host pays for neither
94
+ // (`stage-keyboard.tsx`).
95
+ const LazyStageKeyboard = lazy(async () => {
96
+ const module = await import('./stage-keyboard');
97
+ return { default: module.StageKeyboardBinding };
98
+ });
99
+
100
+ import {
101
+ applyStandardViewportDressing,
102
+ createGradientBackgroundTexture,
103
+ STANDARD_ENVIRONMENT_INTENSITY,
104
+ type StandardViewportDressing,
105
+ watchPaletteBackdrop,
106
+ } from './standard-viewport-dressing';
107
+ import { ViewportFurniture } from './ViewportFurniture';
108
+ import { OBJECT3D_SURFACE_BUILDING, ViewportSurfaceStatus } from './viewport-surface-status';
109
+ import { workspaceHistoryService } from './workspace-history';
110
+
111
+ /** WHERE THIS DOCUMENT'S BYTES GO — the one collaborator the shell installs
112
+ * (`object3d-document-write-policy.ts`). The tier's source recorder, the
113
+ * project file history and the thumbnail manifest all live behind it, so the
114
+ * Asset Lab 3D document carries no editor transport in its own closure
115
+ * (ARCHITECTURE-CORE §Editor chrome, "the viewport stack is separable").
116
+ * With no shell above it every member refuses by name, which is the same
117
+ * refusal a read-only tier already produced. */
118
+ const projectWrites = () => object3DDocumentWritePolicy();
119
+
120
+ /**
121
+ * How far a BARE dock-panel stage bleeds past the box a person sees, so the
122
+ * rendered image fills the panel edge to edge under its padding. THE ONE
123
+ * PLACE this number lives: the stage box takes it negative, the DOM furniture
124
+ * takes it back positive, and the canvas-drawn orientation gizmo takes it
125
+ * through `EditorViewportOptions.chromeInsetPx` — three readers, one value.
126
+ * A chromeless mount and the Asset Editor shell fill their own box exactly
127
+ * and bleed nothing.
128
+ */
129
+ const STAGE_BLEED_PX = 12;
130
+
131
+ function boxFromFrameBounds(
132
+ bounds:
133
+ | {
134
+ readonly min: readonly [number, number, number];
135
+ readonly max: readonly [number, number, number];
136
+ }
137
+ | undefined,
138
+ ): THREE.Box3 | null {
139
+ if (!bounds) return null;
140
+ const box = new THREE.Box3(
141
+ new THREE.Vector3(bounds.min[0], bounds.min[1], bounds.min[2]),
142
+ new THREE.Vector3(bounds.max[0], bounds.max[1], bounds.max[2]),
143
+ );
144
+ return box.isEmpty() ? null : box;
145
+ }
146
+
147
+ /**
148
+ * Per-document overrides of the standard viewport dressing
149
+ * (`standard-viewport-dressing.ts`) — explicit opt-outs/opt-ins, never
150
+ * re-implementations. Omitted means the standard look.
151
+ *
152
+ * THE SHAPE IS THE SDK'S (`ToolViewportDressing`), because a contributed
153
+ * document is what asks for it: `@volter/editor-blender`'s Model document hands over
154
+ * its view-locked studio through this door. One declaration, so the public
155
+ * surface and the host cannot drift.
156
+ */
157
+ export type StandardDressingProps = ToolViewportDressing;
158
+
159
+ /** What an editor-side source lane gets to build its authoring adapter over
160
+ * the document's own store and scene. */
161
+ export interface SourceDocumentAuthoringContext {
162
+ readonly store: EditorShellStore;
163
+ readonly scene: THREE.Scene;
164
+ readonly hierarchyRoots?: readonly THREE.Object3D[];
165
+ }
166
+
167
+ export type SourceDocumentAuthoringFactory = (
168
+ context: SourceDocumentAuthoringContext,
169
+ ) => ToolObject3DDocumentAuthoring;
170
+
171
+ /** A document owns the Object3D returned by its build callback. Its kind
172
+ * supplies authoring behavior and chrome; this host owns renderer lifetime,
173
+ * capture and disposal. Runtime world mounting belongs to its contributing
174
+ * product and is not installed by the modeling host. */
175
+
176
+ /** Module-level so its identity is stable across renders, which
177
+ * `useSyncExternalStore` requires; the theme root is global, so no stage's
178
+ * own element narrows it. */
179
+ function subscribeThemeViewportGroup(onChange: () => void): () => void {
180
+ return subscribeNativeSelectionTheme(null, onChange);
181
+ }
182
+
183
+ export interface Object3DDocumentViewportProps extends ToolObject3DAuthoringProps {
184
+ /** See `@volter/editor-sdk`'s `ToolObject3DAuthoringProps.audit`. */
185
+ readonly audit?: boolean;
186
+ /** Reuse the canonical Asset Lab viewport with NONE of the document chrome —
187
+ * no toolbar, no animation widget, no workspace-document registration —
188
+ * so it can fill a small box (the inspector's preview section). */
189
+ readonly chromeless?: boolean;
190
+ readonly modelSource?:
191
+ | { readonly kind: 'project-file'; readonly path: string }
192
+ | { readonly kind: 'entity'; readonly entityId: string };
193
+ /** Asset Lab's native subject kind for this Object3D document. */
194
+ readonly assetType?: string;
195
+ /**
196
+ * Editor-side authoring for a document whose write authority is project
197
+ * SOURCE (a mounted R3F composition's stamped JSX, today). The caller that
198
+ * knows the source lane supplies the adapter; this document constructs
199
+ * none of them, so it stays mountable without any source lane in its
200
+ * closure (ARCHITECTURE-CORE §Editor chrome, "the viewport stack is
201
+ * separable"). Raw model/artifact documents omit this and remain honestly
202
+ * read-only through the native source adapter. Consulted only when the
203
+ * project `authoring` factory declined. MUST be referentially stable for
204
+ * the document's lifetime, like `build`: it is an activation dependency.
205
+ */
206
+ readonly sourceAuthoring?: SourceDocumentAuthoringFactory;
207
+ /**
208
+ * Where this viewport's `WebGLRenderer` comes from.
209
+ *
210
+ * `own` (the default) constructs one for this mount — the right answer for a
211
+ * document that lives as long as its panel does. `inspector-preview` draws
212
+ * through the shared inspector-preview renderer
213
+ * (`inspector-preview-renderer.ts`): that lane REMOUNTS PER SELECTION, and a
214
+ * renderer construction there is ~1.9 s of frozen main thread plus a cold
215
+ * shader cache and a fresh PMREM bake, every time you click something.
216
+ */
217
+ readonly rendererLane?: 'own' | 'inspector-preview';
218
+ /**
219
+ * Double-click on a picked node — the document's "open THIS" gesture,
220
+ * reported with the node's own `Object3D` so the owner can resolve whatever
221
+ * it means by it. Single click stays plain selection; a document that
222
+ * declares no handler has no open gesture at all.
223
+ *
224
+ * Sole caller: the `3D` board, where an exhibit opens that
225
+ * story's own turntable document (`three-board/ThreeBoardDocument.tsx`).
226
+ */
227
+ readonly onOpenNode?: (object: THREE.Object3D) => void;
228
+ /**
229
+ * When set, Frame with no selection and the view presets fit THIS box
230
+ * instead of `contentWorldBounds(root)`. The 3D board passes the union of
231
+ * each exhibit's presence as its true-scale overview.
232
+ */
233
+ readonly frameBounds?: {
234
+ readonly min: readonly [number, number, number];
235
+ readonly max: readonly [number, number, number];
236
+ };
237
+ /** Optional first-paint frame distinct from the document's Frame fallback.
238
+ * The 3D component board opens on one readable exhibit, while clearing the
239
+ * selection and pressing Frame still restores its true-scale overview. */
240
+ readonly openingFrameBounds?: {
241
+ readonly min: readonly [number, number, number];
242
+ readonly max: readonly [number, number, number];
243
+ };
244
+ /** Multiplies the opening fit distance; `1` fills the view. */
245
+ readonly openingFit?: number;
246
+ }
247
+
248
+ /**
249
+ * Distinguishes the chromeless mounts of ONE document from each other and from
250
+ * that document's viewport, so each publishes its own design-time surface
251
+ * instead of overwriting a sibling's (see the registration below).
252
+ */
253
+ let chromelessSurfaceSequence = 0;
254
+
255
+ interface RetainedObject3DStageState {
256
+ readonly documentId: string;
257
+ readonly store: EditorShellStore;
258
+ inUse: boolean;
259
+ source: ToolObject3DPreviewSource | null;
260
+ camera: { position: THREE.Vector3; target: THREE.Vector3; fov: number } | null;
261
+ presentation: Object3DDocumentPresentationState | null;
262
+ transport: StageTransportSnapshot | null;
263
+ contentSeconds: number;
264
+ transportAdvances: number;
265
+ }
266
+
267
+ /**
268
+ * Per-pane state outlives the React surface Code-OSS mounts for the active tab.
269
+ * A second visible pane showing the same document claims a second state slot;
270
+ * an inactive slot is reused only after its former pane has unmounted.
271
+ */
272
+ const retainedObject3DStages = new Map<string, RetainedObject3DStageState[]>();
273
+
274
+ function claimRetainedObject3DStage(documentId: string): RetainedObject3DStageState {
275
+ const states = retainedObject3DStages.get(documentId) ?? [];
276
+ let state = states.find((candidate) => !candidate.inUse);
277
+ if (!state) {
278
+ state = {
279
+ documentId,
280
+ store: new EditorShellStore(),
281
+ inUse: false,
282
+ source: null,
283
+ camera: null,
284
+ presentation: null,
285
+ transport: null,
286
+ contentSeconds: 0,
287
+ transportAdvances: 0,
288
+ };
289
+ states.push(state);
290
+ retainedObject3DStages.set(documentId, states);
291
+ }
292
+ state.inUse = true;
293
+ return state;
294
+ }
295
+
296
+ function releaseRetainedObject3DStage(state: RetainedObject3DStageState): void {
297
+ state.inUse = false;
298
+ // Switching tabs unmounts the Code-OSS pane while its document remains in
299
+ // the open set. An actual close removes it first; discard retained CPU state
300
+ // after that registry mutation so closed documents do not form a new cache.
301
+ queueMicrotask(() => {
302
+ if (state.inUse) return;
303
+ if (openWorkspaceDocuments().some((document) => document.descriptor.id === state.documentId))
304
+ return;
305
+ state.source?.dispose();
306
+ state.source = null;
307
+ const states = retainedObject3DStages.get(state.documentId);
308
+ if (!states) return;
309
+ const remaining = states.filter((candidate) => candidate !== state || candidate.inUse);
310
+ if (remaining.length > 0) retainedObject3DStages.set(state.documentId, remaining);
311
+ else retainedObject3DStages.delete(state.documentId);
312
+ });
313
+ }
314
+
315
+ /**
316
+ * The element the viewport draws INTO — an EMPTY host div in both lanes.
317
+ * React never owns the canvas. Both lanes mount a leased renderer canvas into
318
+ * this host imperatively, then return it when the pane hides. A lost canvas is
319
+ * destroyed rather than reused; a healthy one can safely serve the next pane.
320
+ */
321
+ function ViewportSurface({
322
+ canvasHostRef,
323
+ }: {
324
+ readonly canvasHostRef: RefObject<HTMLDivElement | null>;
325
+ }) {
326
+ return <div ref={canvasHostRef} style={{ position: 'absolute', inset: 0 }} />;
327
+ }
328
+
329
+ /**
330
+ * The viewport's drawing surface for one mount: either the shared
331
+ * inspector-preview renderer or the interactive-document renderer pool. The
332
+ * lane decides which exclusive lease to mount into `canvasHost`.
333
+ */
334
+ function mountViewportSurface(
335
+ canvasHost: HTMLDivElement,
336
+ lane: 'own' | 'inspector-preview',
337
+ displayName: string,
338
+ initialSize: { readonly width: number; readonly height: number },
339
+ onContextLost?: () => void,
340
+ ): {
341
+ canvas: HTMLCanvasElement;
342
+ renderer: THREE.WebGLRenderer;
343
+ lease: InspectorPreviewLease | InteractiveViewportRendererLease | null;
344
+ } {
345
+ if (lane === 'inspector-preview') {
346
+ const lease = acquireInspectorPreviewRenderer();
347
+ lease.canvas.setAttribute('aria-label', `${displayName} authoring viewport`);
348
+ canvasHost.appendChild(lease.canvas);
349
+ return { canvas: lease.canvas, renderer: lease.renderer, lease };
350
+ }
351
+ const lease = acquireInteractiveViewportRenderer(
352
+ initialSize.width,
353
+ initialSize.height,
354
+ onContextLost,
355
+ );
356
+ const { canvas, renderer } = lease;
357
+ canvas.setAttribute('aria-label', `${displayName} authoring viewport`);
358
+ canvasHost.appendChild(canvas);
359
+ return { canvas, renderer, lease };
360
+ }
361
+
362
+ /** The drawing surface belongs to the document, not to a source revision.
363
+ * Source lifetimes borrow it; closing waits for any asynchronous gesture
364
+ * cleanup to return its borrow before releasing the context. */
365
+
366
+
367
+ interface DocumentContentBinding {
368
+ readonly scene: THREE.Scene;
369
+ settle(): Promise<void>;
370
+ dispose(): Promise<void>;
371
+ }
372
+
373
+ /** Persistent editor state. Source revisions own only their content binding. */
374
+ class Object3DDocumentHost {
375
+ readonly scene = new THREE.Scene();
376
+ /** This document's store, optionally supplied by its owner. */
377
+ readonly store: EditorShellStore;
378
+ readonly cleanups: Array<() => void> = [];
379
+ readonly rendererSession = new DocumentRendererSession(
380
+ (time, resumed) => this.frame?.(time, resumed),
381
+ () => {},
382
+ );
383
+ viewport: EditorViewport | null = null;
384
+ session: Object3DDocumentSession | null = null;
385
+ dressing: StandardViewportDressing | null = null;
386
+ defaultEnvironment: THREE.Texture | null = null;
387
+ defaultBackground: THREE.Color | THREE.Texture | null = null;
388
+ adapter: AuthoringAdapter | null = null;
389
+ content: DocumentContentBinding | null = null;
390
+ frame: ((time: number, resumed: boolean) => void) | null = null;
391
+ /** Mirrors the document scene's world dressing onto the rendered scene. The
392
+ * HOST owns it; the active session holds a reference and runs it before
393
+ * every draw (see {@link Object3DDocumentSession.setBeforeRender}). */
394
+ syncHostScene: (() => void) | null = null;
395
+ /** The other participants on THIS stage, once its module has loaded
396
+ * (`stage-presence-markers.ts`). Null on a chromeless mount and until then. */
397
+ presence: { syncMarkers(dtSeconds: number): void } | null = null;
398
+ initialized = false;
399
+ contentSeconds = 0;
400
+ transportAdvances = 0;
401
+ private users = 0;
402
+ private closed = false;
403
+ private released = false;
404
+
405
+ /** Whether {@link close} has run — a late async install must not push a
406
+ * cleanup onto a host whose cleanups have already been drained. */
407
+ get isClosed(): boolean {
408
+ return this.closed;
409
+ }
410
+
411
+ /** THIS STAGE's transport — the one holder of the content-time door below.
412
+ * Built from this stage's own store, because `driver` is that store's
413
+ * `playState` and nothing else (`animation/stage-transport.ts`). */
414
+ readonly transport: StageTransport;
415
+
416
+ constructor(
417
+ readonly lane: 'own' | 'inspector-preview',
418
+ readonly surface: ReturnType<typeof mountViewportSurface>,
419
+ store?: EditorShellStore,
420
+ ) {
421
+ this.store = store ?? new EditorShellStore();
422
+ this.transport = new StageTransport(this.store);
423
+ }
424
+
425
+ borrow(): () => void {
426
+ this.users++;
427
+ let returned = false;
428
+ return () => {
429
+ if (returned) return;
430
+ returned = true;
431
+ this.users--;
432
+ this.releaseIfClosed();
433
+ };
434
+ }
435
+
436
+ close(): void {
437
+ this.closed = true;
438
+ this.frame = null;
439
+ this.syncHostScene = null;
440
+ this.session?.setBeforeRender(null);
441
+ this.session?.setPresentsFrames(false);
442
+ this.rendererSession.setActive(false);
443
+ void this.content?.dispose();
444
+ // The one teardown path allowed to end the transport (its header states
445
+ // the ownership): the document that owns this stage is unmounting.
446
+ this.transport.dispose();
447
+ this.releaseIfClosed();
448
+ }
449
+
450
+ private releaseIfClosed(): void {
451
+ if (!this.closed || this.users !== 0 || this.released) return;
452
+ this.released = true;
453
+ const { lease, renderer, canvas } = this.surface;
454
+ for (const cleanup of [
455
+ ...this.cleanups.splice(0),
456
+ () => this.session?.dispose(),
457
+ () => this.viewport?.dispose(),
458
+ () => this.rendererSession.dispose(),
459
+ () => this.dressing?.dispose(),
460
+ ...(lease
461
+ ? [() => lease.release()]
462
+ : [() => renderer.dispose(), () => renderer.forceContextLoss(), () => canvas.remove()]),
463
+ ]) {
464
+ try {
465
+ cleanup();
466
+ } catch (error) {
467
+ // biome-ignore lint/suspicious/noConsole: all remaining resources must still be released
468
+ console.error('Object3D document cleanup failed.', error);
469
+ }
470
+ }
471
+ }
472
+ }
473
+
474
+ /**
475
+ * Editor-owned authoring host for a project-supplied native Object3D factory.
476
+ * Project code supplies only source construction; this component owns the
477
+ * real EditorViewport, document-scoped AuthoringAdapter, selection, framing,
478
+ * animation preview, resize, and teardown.
479
+ *
480
+ * ## TIME ON THIS SURFACE — the one policy, inherited by everything mounted here
481
+ *
482
+ * This host is the shared stack: Asset Lab documents, story turntables, 3D board
483
+ * exhibits and the inspector's preview card all render through it. So the
484
+ * Edit-static rule is enforced HERE, once, rather than at each of them:
485
+ *
486
+ * - PRESENTATION time always advances — orbit damping, the view cube, the
487
+ * standard dressing, the editor's own helpers and overlays. It is
488
+ * editor-owned and it moves while the editor is in Edit.
489
+ * - CONTENT time does NOT. The source's own `update(dt)` and any clip a
490
+ * subject shows move only through the stage TRANSPORT, and only while a
491
+ * human is holding it (`animation/stage-transport.ts`, WORK.md §The stage
492
+ * transport and the animation door). A mounted
493
+ * document plays nothing on its own: that is the defect this closes (a GLB
494
+ * train animating in the Asset Lab while every instrument called the editor
495
+ * still, owner find 2026-08-15).
496
+ *
497
+ * Whatever content time this surface has reached is published as its content
498
+ * clock (`coverage/design-time-surfaces.ts`), so the Edit-static vitals row
499
+ * measures THIS surface by name instead of speaking only for the Scene view.
500
+ */
501
+ export function Object3DDocumentViewport({
502
+ documentId,
503
+ sourcePath,
504
+ build,
505
+ displayName = 'Source Object3D',
506
+ // No default: an omitted background means the STANDARD dressing's gradient;
507
+ // a value is an explicit flat-color override (SDK contributions may pass it).
508
+ background,
509
+ cameraDirection,
510
+ persistence,
511
+ documentSource,
512
+ authoring,
513
+ interaction,
514
+ selectionOutline = true,
515
+ active = true,
516
+ audit = true,
517
+ chromeless = false,
518
+ modelSource,
519
+ assetType,
520
+ sourceAuthoring,
521
+ dressing,
522
+ rendererLane = 'own',
523
+ onOpenNode,
524
+ frameBounds,
525
+ openingFrameBounds,
526
+ openingFit,
527
+ statistics,
528
+ }: Object3DDocumentViewportProps) {
529
+ // ANNOUNCE THE STAGE (`authoring/object3d-document-session-registry.ts`).
530
+ // The lazy boundary announces for the documents that go through it; the
531
+ // asset viewers import THIS component directly (`asset-viewers/
532
+ // ModelAssetDocument.tsx:19` and its two siblings), so the announcement is
533
+ // made here too. Announcing is counted, so the two overlap harmlessly.
534
+ useEffect(() => {
535
+ if (chromeless) return;
536
+ return announceObject3DDocumentStage(documentId);
537
+ }, [documentId, chromeless]);
538
+ // The SHELL store, not this host's own: what a stage is showing is a fact
539
+ // about the workspace's document, and `stage-context.ts` is where every
540
+ // stage capability asks it (ARCHITECTURE-CORE §One stage). OPTIONAL because
541
+ // this host is a bounded-host surface — `@volter/editor-blender`'s Model document
542
+ // mounts it with no `EditorProvider` above it, and the throwing hook took
543
+ // that document off the screen with "useEditorStore must be used within
544
+ // <EditorProvider>" (measured live, 2026-09-18). No shell, no shell frame.
545
+ const shellStore = useOptionalEditorStore();
546
+ /**
547
+ * THE STORE THE STAGE CONTEXT IS ASKED AGAINST — the session's own, reached
548
+ * without a React provider.
549
+ *
550
+ * `useOptionalEditorStore()` above answers a NARROWER question than the
551
+ * context needs: it is whether the shell's own React tree is above this
552
+ * host, and for every package-contributed document it is not.
553
+ * `@volter/editor-blender`'s Model document mounts this host through `ToolHost`, so the
554
+ * context store is null there — and reading only it left `stageCtx` null,
555
+ * which made EVERY condition in `stage-context.ts` unanswerable for that
556
+ * document. That is a capability decided by where a document's React tree
557
+ * happens to mount, which is the origin-deciding reading §One stage
558
+ * retired. MEASURED 2026-09-18 on the models scaffold: the Model document's
559
+ * controls hint (the one overlay every three stage carries) is absent, and
560
+ * with it the whole context — so the studio arm of `studioStageApplies`
561
+ * could never fire for a model no matter what the look declared.
562
+ *
563
+ * `shellStoreForHost()` supplies the session store that
564
+ * `focusedStageStore()` falls back to. It is deliberately NOT used for the
565
+ * overlay set or the shell readout below: those are the SHELL's estate and
566
+ * their condition is the shell's React presence, exactly as §One stage
567
+ * unit 3 measured it — a stage that is not inside the shell has no shell
568
+ * overlays. That gap is real and named, not closed here: unit 1's bar is
569
+ * that the Model document's Blender frame does not move.
570
+ */
571
+ const contextStore = shellStore ?? shellStoreForHost();
572
+ /**
573
+ * WHAT THIS STAGE IS SHOWING, asked once (ARCHITECTURE-CORE §One stage).
574
+ * `chrome` is the host's own answer and nothing infers it: an EMBEDDED
575
+ * preview has no workspace document at all, which is exactly why the
576
+ * subject would come back `unknown` for one.
577
+ */
578
+ const stageCtx = contextStore
579
+ ? documentStageContext(contextStore, documentId, chromeless ? 'embedded' : 'document')
580
+ : null;
581
+ /**
582
+ * THE STUDIO PRESENTATION — the alpha clear, the suppressed palette
583
+ * backdrop, the identity-row frame and the asset flavour of this stage's
584
+ * provenance. It replaced a `documentKind` prop the CALLER chose, which is
585
+ * exactly the origin-of-the-document reading §One stage retired: every
586
+ * mount that passed `documentKind="asset"` is a story, an artifact or an
587
+ * embedded preview, and each still answers true.
588
+ */
589
+ // The LOOK is one of `studioStageApplies`'s live inputs (a data subject takes
590
+ // the studio only where the look leaves the backdrop to the editor), and the
591
+ // one that changes with no store write behind it: switching the style
592
+ // re-emits the tokens onto the theme root's inline `style`, which is exactly
593
+ // what this observes. Without the subscription the presentation would flip
594
+ // only on the next unrelated render, and a style switch is the gesture the
595
+ // condition exists for.
596
+ const lookPaintsViewport = useSyncExternalStore(
597
+ subscribeThemeViewportGroup,
598
+ lookDeclaresViewportColors,
599
+ );
600
+ const studioStage = stageCtx !== null && studioStageApplies(stageCtx, lookPaintsViewport);
601
+ const studioStageRef = useRef(studioStage);
602
+ studioStageRef.current = studioStage;
603
+ // An ARTIFACT document gets the identity-row shell — the frame, the context
604
+ // activation and the SELECTION REGISTRATION (`AssetEditorShell`'s
605
+ // `selection` prop). Asked here, beside the studio presentation, because the
606
+ // mount effect needs it too: whoever does NOT get the shell registers its own
607
+ // workspace selection instead, and exactly one of the two must.
608
+ const hasShell = stageCtx !== null && identityRowApplies(stageCtx);
609
+ const hasShellRef = useRef(hasShell);
610
+ hasShellRef.current = hasShell;
611
+ // The ASSET FLAVOUR of this stage's provenance: the source adapter's
612
+ // `documentKind` ("derived asset", `model-asset`) and the performance
613
+ // source's kind. It is the studio presentation MINUS the data arm — a mesh
614
+ // module opens in an isolation scene but is not a derived asset — and was
615
+ // read off `studioStage` while the two were the same answer.
616
+ const assetSubject = stageCtx !== null && assetSubjectApplies(stageCtx);
617
+ const assetSubjectRef = useRef(assetSubject);
618
+ assetSubjectRef.current = assetSubject;
619
+ // The focused document fills the shell's readout. Null without a shell.
620
+ const shellStats = useOptionalEditorStats();
621
+ const shellStatsRef = useRef(shellStats);
622
+ shellStatsRef.current = shellStats;
623
+ const containerRef = useRef<HTMLDivElement>(null);
624
+ // The shared lane mounts the pool's own canvas here instead of rendering one.
625
+ const canvasHostRef = useRef<HTMLDivElement>(null);
626
+ const documentHostRef = useRef<Object3DDocumentHost | null>(null);
627
+ const retainedRef = useRef<RetainedObject3DStageState | null>(null);
628
+ if (!retainedRef.current || retainedRef.current.documentId !== documentId) {
629
+ if (retainedRef.current) releaseRetainedObject3DStage(retainedRef.current);
630
+ retainedRef.current = claimRetainedObject3DStage(documentId);
631
+ }
632
+ useEffect(() => {
633
+ const retained = retainedRef.current!;
634
+ return () => releaseRetainedObject3DStage(retained);
635
+ }, [documentId]);
636
+ const retainedState = retainedRef.current;
637
+ // Only a visible pane owns an interactive attachment. Hidden documents
638
+ // retain their state and return their renderer to the bounded pool.
639
+ const wantsSurface = active;
640
+ const [surfaceAttached, setSurfaceAttached] = useState(wantsSurface);
641
+ useEffect(() => setSurfaceAttached(wantsSurface), [wantsSurface]);
642
+ // The selection silhouette is a LIVE prop: a modeling document turns it off
643
+ // the moment it enters its own sub-object mode and back on when it leaves,
644
+ // and the session applies that to the composer it has already built. The ref
645
+ // is what the mount path reads, since the session is built inside an effect
646
+ // that does not depend on this prop.
647
+ const selectionOutlineRef = useRef(selectionOutline);
648
+ useEffect(() => {
649
+ selectionOutlineRef.current = selectionOutline;
650
+ const session = documentHostRef.current?.session;
651
+ if (session)
652
+ session.selectionOutlineEnabled = rendererLane !== 'inspector-preview' && selectionOutline;
653
+ }, [selectionOutline, rendererLane]);
654
+ const contentUpdateTail = useRef<Promise<void>>(Promise.resolve());
655
+ const retainSourceOnDisposeRef = useRef(false);
656
+ useEffect(() => {
657
+ if (!surfaceAttached) return;
658
+ firstFrameGateRef.current = false;
659
+ setSurfaceStatus('building');
660
+ return () => {
661
+ const host = documentHostRef.current;
662
+ if (host) {
663
+ // Inactive tabs stay open in the workspace. Their source remains
664
+ // mounted in retained CPU state while the expensive attachment goes
665
+ // back to the pool. A source revision or final close still disposes it.
666
+ retainSourceOnDisposeRef.current = !activeRef.current;
667
+ retainedState.camera = host.store.cameraPose;
668
+ retainedState.presentation = host.session?.presentation() ?? null;
669
+ retainedState.transport = host.transport.snapshot();
670
+ retainedState.contentSeconds = host.contentSeconds;
671
+ retainedState.transportAdvances = host.transportAdvances;
672
+ }
673
+ documentStateRef.current = null;
674
+ persistenceSessionRef.current = null;
675
+ interactionExtensionRef.current = null;
676
+ rendererSessionRef.current = null;
677
+ host?.close();
678
+ documentHostRef.current = null;
679
+ };
680
+ }, [documentId, rendererLane, retainedState, surfaceAttached]);
681
+ // Read through a ref: the one big lifetime effect below owns the canvas
682
+ // listener, and re-running it on a new handler identity would tear the whole
683
+ // renderer down and rebuild it.
684
+ const onOpenNodeRef = useRef(onOpenNode);
685
+ onOpenNodeRef.current = onOpenNode;
686
+ const frameBoundsRef = useRef(frameBounds);
687
+ frameBoundsRef.current = frameBounds;
688
+ const openingFrameBoundsRef = useRef(openingFrameBounds);
689
+ openingFrameBoundsRef.current = openingFrameBounds;
690
+ // The current prop is still read by asynchronous installation. Attachment
691
+ // lifetime is controlled separately by `surfaceAttached` above.
692
+ const activeRef = useRef(active);
693
+ activeRef.current = active;
694
+ const previousActiveRef = useRef(active);
695
+ const sourceIdentityRef = useRef({ build, sourcePath });
696
+ useEffect(() => {
697
+ const previous = sourceIdentityRef.current;
698
+ sourceIdentityRef.current = { build, sourcePath };
699
+ if (previous.build === build && previous.sourcePath === sourcePath) return;
700
+ // An HMR revision received while hidden invalidates the prepared source.
701
+ // The next reveal constructs the current revision while retaining the
702
+ // pane's camera, tool, helpers, selection, and transport state.
703
+ if (!surfaceAttached && retainedState.source) {
704
+ retainedState.source.dispose();
705
+ retainedState.source = null;
706
+ }
707
+ }, [build, retainedState, sourcePath, surfaceAttached]);
708
+ const rendererSessionRef = useRef<DocumentRendererSession | null>(null);
709
+ const firstFrameGateRef = useRef(false);
710
+ const interactionExtensionRef = useRef<ReturnType<
711
+ NonNullable<typeof interaction>['setup']
712
+ > | null>(null);
713
+ const documentStateRef = useRef<{
714
+ root: THREE.Object3D;
715
+ animations: readonly THREE.AnimationClip[];
716
+ } | null>(null);
717
+ const persistenceSessionRef = useRef<Promise<Object3DDocumentPersistenceSession> | null>(null);
718
+ const [error, setError] = useState<string | null>(null);
719
+ const [surfaceStatus, setSurfaceStatus] = useState<'building' | 'ready'>('building');
720
+ const [projection, setProjection] = useState<ThreeViewportProjection>('perspective');
721
+ // The live authoring adapter this document's inspection composes from. It
722
+ // only exists once the graph is built, so it is state rather than a ref:
723
+ // its arrival is what re-renders the aside below.
724
+ const [documentAdapter, setDocumentAdapter] = useState<AuthoringAdapter | null>(null);
725
+ const documentAdapterRef = useRef<AuthoringAdapter | null>(null);
726
+ documentAdapterRef.current = documentAdapter;
727
+ const cameraX = cameraDirection?.[0];
728
+ const cameraY = cameraDirection?.[1];
729
+ const cameraZ = cameraDirection?.[2];
730
+ const modelSourceKind = modelSource?.kind;
731
+ const modelSourcePath = modelSource?.kind === 'project-file' ? modelSource.path : undefined;
732
+ const modelSourceEntityId = modelSource?.kind === 'entity' ? modelSource.entityId : undefined;
733
+ const dressingEnvironment = dressing?.environment;
734
+ const dressingBackground = dressing?.background;
735
+ const dressingKeyLight = dressing?.keyLight;
736
+ const dressingGrid = dressing?.grid;
737
+ const dressingViewLocked = dressing?.viewLocked;
738
+ const dressingToneMapping = dressing?.toneMapping;
739
+ if (active) markViewportReactActive(documentId);
740
+
741
+ // Document identity owns the host. Source changes prepare a separate native
742
+ // graph, then publish it into that host without recreating editor state.
743
+ useEffect(() => {
744
+ let cancelled = false;
745
+ if (!surfaceAttached) return;
746
+ const install = async () => {
747
+ await documentHostRef.current?.content?.settle();
748
+ while (liveGestureActive()) await whenLiveGestureIdle();
749
+ if (cancelled) return;
750
+ const container = containerRef.current;
751
+ const canvasHost = canvasHostRef.current;
752
+ if (!container || !canvasHost) return;
753
+ if (!build || sourcePath === undefined) {
754
+ // `build` content is one Object3D the caller constructs from one file.
755
+ // Both are required by the SDK; validate callers at the runtime boundary.
756
+ setError('This stage shows `build` content and was given no `build()`/`sourcePath`.');
757
+ return;
758
+ }
759
+ let source: ReturnType<NonNullable<typeof build>>;
760
+ try {
761
+ source = retainedState.source ?? build();
762
+ retainedState.source = null;
763
+ } catch (caught) {
764
+ setError(caught instanceof Error ? caught.message : String(caught));
765
+ return;
766
+ }
767
+ try {
768
+ if (!documentHostRef.current) {
769
+ markViewportConstructStarted();
770
+ documentHostRef.current = new Object3DDocumentHost(
771
+ rendererLane,
772
+ mountViewportSurface(
773
+ canvasHost,
774
+ rendererLane,
775
+ displayName,
776
+ {
777
+ width: Math.max(1, container.clientWidth),
778
+ height: Math.max(1, container.clientHeight),
779
+ },
780
+ () => {
781
+ // A lost context is never returned to the pool. Rebuild this
782
+ // visible attachment from the retained document state.
783
+ setSurfaceAttached(false);
784
+ window.setTimeout(() => {
785
+ if (activeRef.current) setSurfaceAttached(true);
786
+ }, 0);
787
+ },
788
+ ),
789
+ retainedState.store,
790
+ );
791
+ documentHostRef.current.contentSeconds = retainedState.contentSeconds;
792
+ documentHostRef.current.transportAdvances = retainedState.transportAdvances;
793
+ }
794
+ } catch (caught) {
795
+ source.dispose();
796
+ throw caught;
797
+ }
798
+ const host = documentHostRef.current!;
799
+ const returnSurface = host.borrow();
800
+ const { renderer, canvas, lease } = host.surface;
801
+ const store = host.store;
802
+ // This stage's store, reachable by the shared panels through
803
+ // `focusedStageStore()` (ARCHITECTURE-CORE §One stage unit 4). A
804
+ // CHROMELESS preview is not a stage anything can focus — it has no
805
+ // workspace document — so it registers nothing.
806
+ if (!chromeless) {
807
+ host.cleanups.push(registerStageStore(documentId, store));
808
+ host.cleanups.push(registerStageTransport(documentId, host.transport));
809
+ }
810
+ const history = workspaceHistoryService();
811
+ if (history) store.attachHistory(history);
812
+ const shared = rendererLane === 'inspector-preview';
813
+ const studioStage = studioStageRef.current;
814
+ const hasShell = hasShellRef.current;
815
+ const assetSubject = assetSubjectRef.current;
816
+ const scene = new THREE.Scene();
817
+ // The document's content scene is the HOST's own object here, and the
818
+ // host's `syncHostScene` mirrors its environment fields onto the
819
+ // rendered scene before every draw. So the studio IBL's strength is
820
+ // set on THIS scene, at creation: a value set on the rendered scene, or
821
+ // by the dressing (which dresses the rendered scene), is overwritten
822
+ // each frame by this scene's untouched default of 1 — measured twice,
823
+ // once by the long-tail pass and once by the orchestrator, with zero
824
+ // movement. The value is the dressing's own, fitted with its key light.
825
+ // A document whose adapter authors an intensity sets it after this and
826
+ // wins, as authored lighting always does.
827
+ //
828
+ // `environment: false` sets NEITHER: no map and no strength for it. The
829
+ // strength alone is inert with no map — but the mirror above copies
830
+ // this field onto the rendered scene every draw, so leaving a studio
831
+ // number here would scale ANY environment the document's own adapter
832
+ // later applies (a Blender render's world, which is applied to the
833
+ // presented root's scene) by a value that document opted out of.
834
+ if (dressingEnvironment !== false)
835
+ scene.environmentIntensity = STANDARD_ENVIRONMENT_INTENSITY;
836
+ const sourceParent = source.root.parent;
837
+ scene.add(source.root);
838
+ if (source.animations) source.root.animations = [...source.animations];
839
+ const documentState = { root: source.root, animations: source.root.animations };
840
+ let defaultAdapter: SourceObject3DAuthoringAdapter | null = null;
841
+ let projectAuthoring: ToolObject3DDocumentAuthoring | null = null;
842
+ let activePersistenceSession: Promise<Object3DDocumentPersistenceSession> | null = null;
843
+ let interactionExtension: ReturnType<NonNullable<typeof interaction>['setup']> | null = null;
844
+ let gestureController: Object3DGestureController | null = null;
845
+ let pointerDownListener: ((event: PointerEvent) => void) | null = null;
846
+ let pointerMoveListener: ((event: PointerEvent) => void) | null = null;
847
+ let pointerUpListener: ((event: PointerEvent) => void) | null = null;
848
+ let pointerCancelListener: ((event: PointerEvent) => void) | null = null;
849
+ let escapeListener: ((event: KeyboardEvent) => void) | null = null;
850
+ let activateInteraction: (() => void) | null = null;
851
+ let rollback: (() => void) | null = null;
852
+ let published = false;
853
+ let disposed = false;
854
+ let cleanup: Promise<void> | null = null;
855
+ const binding: DocumentContentBinding = {
856
+ scene,
857
+ settle: () => gestureController?.settle() ?? Promise.resolve(),
858
+ dispose: () => {
859
+ if (cleanup) return cleanup;
860
+ disposed = true;
861
+ if (pointerDownListener)
862
+ container.removeEventListener('pointerdown', pointerDownListener, true);
863
+ if (pointerMoveListener)
864
+ container.removeEventListener('pointermove', pointerMoveListener, true);
865
+ if (pointerUpListener)
866
+ container.removeEventListener('pointerup', pointerUpListener, true);
867
+ if (pointerCancelListener)
868
+ container.removeEventListener('pointercancel', pointerCancelListener, true);
869
+ if (escapeListener) window.removeEventListener('keydown', escapeListener);
870
+ cleanup = (async () => {
871
+ await gestureController?.settle();
872
+ const failures: unknown[] = [];
873
+ for (const release of [
874
+ () => interactionExtension?.dispose(),
875
+ () => projectAuthoring?.dispose?.(),
876
+ () => defaultAdapter?.dispose(),
877
+ () => scene.removeFromParent(),
878
+ () => {
879
+ if (retainSourceOnDisposeRef.current) retainedState.source = source;
880
+ else source.dispose();
881
+ retainSourceOnDisposeRef.current = false;
882
+ },
883
+ returnSurface,
884
+ ]) {
885
+ try {
886
+ release();
887
+ } catch (error) {
888
+ failures.push(error);
889
+ }
890
+ }
891
+ if (failures.length) {
892
+ // biome-ignore lint/suspicious/noConsole: teardown must remain diagnosable
893
+ console.error('Object3D source cleanup failed.', ...failures);
894
+ }
895
+ })();
896
+ return cleanup;
897
+ },
898
+ };
899
+ try {
900
+ const commitProjectDocument = async (label?: string): Promise<boolean> => {
901
+ try {
902
+ const document = documentState;
903
+ if (documentSource && !persistence) {
904
+ // The whole document is ONE source module: the same seam the
905
+ // animation binding writes through — checksum-guarded whole-file
906
+ // replace, recorded in canonical history — never the resource
907
+ // route, which refuses executable source by rule.
908
+ if (!document)
909
+ throw new Error('Project history is unavailable; the edit was not saved.');
910
+ const history = workspaceHistoryService();
911
+ if (!history) {
912
+ throw new Error('Project history is unavailable; the edit was not saved.');
913
+ }
914
+ const source = documentSource.serialize(document);
915
+ if (source === null) {
916
+ // The document says nothing changed: a real no-change outcome,
917
+ // never a write of the emitted form over a hand-written file.
918
+ setError(null);
919
+ return false;
920
+ }
921
+ const changed = await projectWrites().replaceSource(history, {
922
+ file: documentSource.path,
923
+ source,
924
+ label: label ?? documentSource.label ?? `Edit ${displayName}`,
925
+ });
926
+ setError(null);
927
+ return changed;
928
+ }
929
+ if (!persistence) {
930
+ throw new Error('This Asset Lab document has no persistence binding.');
931
+ }
932
+ if (!activePersistenceSession || !document) {
933
+ throw new Error('Project history is unavailable; the edit was not saved.');
934
+ }
935
+ // The pipe's ack for THIS commit: `persisted` is whether a byte moved
936
+ // (a serializer that reproduced the bytes already on disk is a real
937
+ // no-change outcome; a genuine failure throws below).
938
+ const { persisted } = await (await activePersistenceSession).commit(document, label);
939
+ setError(null);
940
+ return persisted;
941
+ } catch (caught) {
942
+ setError(caught instanceof Error ? caught.message : JSON.stringify(caught));
943
+ throw caught;
944
+ }
945
+ };
946
+ defaultAdapter = new SourceObject3DAuthoringAdapter(store, scene, {
947
+ documentId,
948
+ title: displayName,
949
+ sourcePath,
950
+ documentKind: assetSubject ? 'asset' : 'source',
951
+ audit,
952
+ ...(source.hierarchyRoots ? { hierarchyRoots: source.hierarchyRoots } : {}),
953
+ ...(modelSourceKind === 'project-file' && modelSourcePath
954
+ ? { modelSource: { kind: modelSourceKind, path: modelSourcePath } as const }
955
+ : modelSourceKind === 'entity' && modelSourceEntityId
956
+ ? { modelSource: { kind: modelSourceKind, entityId: modelSourceEntityId } as const }
957
+ : {}),
958
+ });
959
+ const authoringContext = {
960
+ documentId,
961
+ sourcePath,
962
+ root: source.root,
963
+ animations: source.root.animations,
964
+ scene,
965
+ defaultAdapter,
966
+ commit: commitProjectDocument,
967
+ };
968
+ projectAuthoring = authoring?.(authoringContext) ?? null;
969
+ if (!projectAuthoring && sourceAuthoring) {
970
+ projectAuthoring = sourceAuthoring({
971
+ store,
972
+ scene,
973
+ ...(source.hierarchyRoots ? { hierarchyRoots: source.hierarchyRoots } : {}),
974
+ });
975
+ defaultAdapter.dispose();
976
+ }
977
+ // Project authoring may establish the module-local artifact that its
978
+ // serializers read (a rig document is the concrete example). Open the
979
+ // persistence baseline only after that factory has adopted the root;
980
+ // opening it before authoring made every clean rig mount reject with
981
+ // "Object3D is not a project rig artifact."
982
+ activePersistenceSession =
983
+ persistence && history
984
+ ? projectWrites().openPersistence({
985
+ binding: persistence,
986
+ document: documentState,
987
+ history,
988
+ })
989
+ : null;
990
+ if (activePersistenceSession) {
991
+ void activePersistenceSession.catch((caught) => {
992
+ if (!disposed && documentHostRef.current?.content === binding) {
993
+ setError(caught instanceof Error ? caught.message : JSON.stringify(caught));
994
+ }
995
+ });
996
+ }
997
+ const adapter: AuthoringAdapter = projectAuthoring?.adapter ?? defaultAdapter;
998
+
999
+ // All editor furniture belongs to the persistent scene. Adapters and
1000
+ // project extensions see only this revision's isolated content scene.
1001
+ if (!host.dressing) {
1002
+ renderer.outputColorSpace = THREE.SRGBColorSpace;
1003
+ // The stage's VIEW TRANSFORM. ACES unless the document states its
1004
+ // own — see `ToolViewportDressing.toneMapping`. It is set here, with
1005
+ // the rest of the renderer's fixed state, because it belongs to the
1006
+ // surface rather than to any one material: three compiles the
1007
+ // operator into every `toneMapped` program.
1008
+ renderer.toneMapping = dressingToneMapping ?? THREE.ACESFilmicToneMapping;
1009
+ renderer.shadowMap.enabled = true;
1010
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap;
1011
+ renderer.info.autoReset = false;
1012
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
1013
+ if (studioStage) renderer.setClearColor(0x000000, 0);
1014
+ const environment =
1015
+ dressingEnvironment === false
1016
+ ? null
1017
+ : lease && 'environment' in lease
1018
+ ? lease.environment()
1019
+ : createStandardEnvironment(renderer);
1020
+ // A palette that carries a viewport background (Blender's flat grey)
1021
+ // paints it in place of the dressing's; the viewport follows theme
1022
+ // changes from there (`EditorViewport`'s look subscription).
1023
+ const look = nativeViewportLook(canvas);
1024
+ host.dressing = applyStandardViewportDressing(host.scene, {
1025
+ environment,
1026
+ background:
1027
+ !studioStage &&
1028
+ background === undefined &&
1029
+ dressingBackground !== false &&
1030
+ look.background === null,
1031
+ keyLight: dressingKeyLight !== false,
1032
+ grid: dressingGrid === true,
1033
+ content: source.root,
1034
+ });
1035
+ if (!studioStage && background !== undefined)
1036
+ host.scene.background = new THREE.Color(background);
1037
+ else if (!studioStage && look.background !== null)
1038
+ host.scene.background = new THREE.Color(look.background);
1039
+ host.defaultEnvironment = host.scene.environment;
1040
+ host.defaultBackground = host.scene.background;
1041
+ host.cleanups.push(() => {
1042
+ if (
1043
+ host.defaultBackground instanceof THREE.Texture &&
1044
+ host.defaultBackground !== host.dressing?.backgroundTexture
1045
+ )
1046
+ host.defaultBackground.dispose();
1047
+ });
1048
+ }
1049
+ const pick = (clientX: number, clientY: number): string | null => {
1050
+ const current = host.adapter;
1051
+ const viewport = host.viewport;
1052
+ if (!current || !viewport) return null;
1053
+ const rect = canvas.getBoundingClientRect();
1054
+ const pointer = new THREE.Vector2(
1055
+ ((clientX - rect.left) / Math.max(rect.width, 1)) * 2 - 1,
1056
+ -((clientY - rect.top) / Math.max(rect.height, 1)) * 2 + 1,
1057
+ );
1058
+ const raycaster = new THREE.Raycaster();
1059
+ raycaster.layers.enableAll();
1060
+ raycaster.setFromCamera(pointer, host.session?.camera() ?? viewport.camera);
1061
+ for (const hit of raycaster.intersectObjects([...store.objectMap.values()], true)) {
1062
+ if (isInEditorOwnedSubtree(hit.object)) continue;
1063
+ let object: THREE.Object3D | null = hit.object;
1064
+ while (object) {
1065
+ const id = current.hierarchy.idForObject3D?.(object);
1066
+ if (id) return id;
1067
+ object = object.parent;
1068
+ }
1069
+ }
1070
+ return null;
1071
+ };
1072
+ if (!host.viewport) {
1073
+ host.viewport = new EditorViewport(canvas, host.scene, store, container, {
1074
+ renderer,
1075
+ authoring: () => host.adapter ?? adapter,
1076
+ pick,
1077
+ publishPickContext: false,
1078
+ onProjectionChange: setProjection,
1079
+ // The same expression that places the DOM furniture, for the one
1080
+ // piece of furniture that is drawn on the canvas instead.
1081
+ chromeInsetPx: chromeless || hasShell ? 0 : STAGE_BLEED_PX,
1082
+ // A document that turns the dressing's key light off has said it
1083
+ // lights itself, and the editor's design-time rig answers exactly
1084
+ // the same question. MEASURED on the Model stage before this
1085
+ // existed: the rig's ambient 0.5 + directional 1.0 sit on
1086
+ // `EDITOR_LAYER`, but three filters lights by the CAMERA's layers
1087
+ // (`WebGLRenderer.projectObject`) and the one camera that draws
1088
+ // this stage enables that layer to see the grid and the gizmo —
1089
+ // so the layer scopes them away from nothing at all and they lit
1090
+ // the content.
1091
+ lightRig: dressingKeyLight !== false,
1092
+ });
1093
+ if (dressingGrid === true) host.viewport.grid.removeFromParent();
1094
+ const open = (event: MouseEvent) => {
1095
+ const id = pick(event.clientX, event.clientY);
1096
+ const object = id ? host.adapter?.hierarchy.object3D?.(id) : null;
1097
+ if (object) onOpenNodeRef.current?.(object);
1098
+ };
1099
+ container.addEventListener('dblclick', open);
1100
+ host.cleanups.push(() => container.removeEventListener('dblclick', open));
1101
+ }
1102
+ const viewport = host.viewport;
1103
+ if (dressingViewLocked) {
1104
+ // VIEW-LOCKED DRESSING. The object turns with the view because it
1105
+ // hangs off the camera, and the camera joins the RENDERED scene for
1106
+ // the same reason: three collects lights while it walks the scene
1107
+ // (`WebGLRenderer.projectObject`), so a light parented to a camera
1108
+ // that is not in the scene is never collected at all. A camera in
1109
+ // the graph draws nothing and picks nothing — it carries no
1110
+ // geometry — and the scene's own `updateMatrixWorld` is then what
1111
+ // carries the view pose down to its children each frame.
1112
+ //
1113
+ // ONE camera is the view pose for this stage: the session's
1114
+ // orthographic camera copies `viewport.camera`'s position and
1115
+ // quaternion before every draw it makes (`object3d-document-
1116
+ // session.ts::syncOrthographicCamera`), so locking to the
1117
+ // perspective camera locks to both projections. A capture with its
1118
+ // OWN camera (a Blender render photographs from the scene's camera)
1119
+ // is deliberately not followed: a render is lit by the scene.
1120
+ const camera = viewport.camera;
1121
+ host.scene.add(camera);
1122
+ camera.add(dressingViewLocked);
1123
+ host.cleanups.push(() => {
1124
+ dressingViewLocked.removeFromParent();
1125
+ camera.removeFromParent();
1126
+ });
1127
+ }
1128
+ if (interaction) {
1129
+ const overlay = new THREE.Group();
1130
+ overlay.name = '__object3d_document_overlay';
1131
+ setUserData(overlay, 'editorHelper', true);
1132
+ scene.add(overlay);
1133
+ interactionExtension = interaction.setup({
1134
+ root: source.root,
1135
+ scene,
1136
+ renderer,
1137
+ overlay,
1138
+ camera: () => host.session?.camera() ?? viewport.camera,
1139
+ writable: persistence !== undefined || documentSource !== undefined,
1140
+ });
1141
+ const pointerEvent = (event: PointerEvent) => {
1142
+ const rect = canvas.getBoundingClientRect();
1143
+ const pointer = new THREE.Vector2(
1144
+ ((event.clientX - rect.left) / Math.max(rect.width, 1)) * 2 - 1,
1145
+ -((event.clientY - rect.top) / Math.max(rect.height, 1)) * 2 + 1,
1146
+ );
1147
+ const raycaster = new THREE.Raycaster();
1148
+ raycaster.layers.enableAll();
1149
+ raycaster.setFromCamera(pointer, host.session?.camera() ?? viewport.camera);
1150
+ return {
1151
+ pointerId: event.pointerId,
1152
+ button: event.button,
1153
+ buttons: event.buttons,
1154
+ clientX: event.clientX,
1155
+ clientY: event.clientY,
1156
+ altKey: event.altKey,
1157
+ ctrlKey: event.ctrlKey,
1158
+ metaKey: event.metaKey,
1159
+ shiftKey: event.shiftKey,
1160
+ ray: raycaster.ray.clone(),
1161
+ hits: raycaster.intersectObjects([source!.root, overlay], true),
1162
+ };
1163
+ };
1164
+ gestureController = new Object3DGestureController({
1165
+ begin: (event) => {
1166
+ if (!persistence && !documentSource) {
1167
+ throw new Error(
1168
+ 'This Asset Lab document has no persistence binding; project gestures are read-only.',
1169
+ );
1170
+ }
1171
+ return interactionExtension!.begin(event);
1172
+ },
1173
+ persist: async (label) => {
1174
+ await commitProjectDocument(label);
1175
+ },
1176
+ reportError: (caught) =>
1177
+ setError(caught instanceof Error ? caught.message : JSON.stringify(caught)),
1178
+ });
1179
+ const claim = (event: PointerEvent) => {
1180
+ event.preventDefault();
1181
+ event.stopPropagation();
1182
+ };
1183
+ pointerDownListener = (event) => {
1184
+ // A control painted OVER the stage owns its own press. These
1185
+ // listeners sit on the CONTAINER in the capture phase, so they run
1186
+ // before that control's own handler and a `stopPropagation` there
1187
+ // would be too late: pressing the rolled-back-operation notice's
1188
+ // Dismiss also started a box-select on the mesh (measured — the
1189
+ // notice's text changed from the failed op to "Box select").
1190
+ // The viewport's navigation cluster (`ViewportFurniture`) is the
1191
+ // same case and was missing here: its Pan button drives the camera
1192
+ // from `onPointerDown`, which React dispatches at the root, so
1193
+ // claiming the press here meant `startPan` never ran and the
1194
+ // control was inert (measured — the camera floats were identical
1195
+ // before and after a full pointer gesture, while the cluster's
1196
+ // `onClick` buttons beside it moved the camera every time).
1197
+ if (
1198
+ event.target instanceof Element &&
1199
+ event.target.closest('[role="alert"], [role="toolbar"]') !== null
1200
+ ) {
1201
+ return;
1202
+ }
1203
+ if (!gestureController?.begin(pointerEvent(event))) return;
1204
+ claim(event);
1205
+ // DOM-only editor automation dispatches honest untrusted pointer
1206
+ // events, which have no browser-owned active pointer and therefore
1207
+ // cannot be captured. Hardware pointers still take capture so a
1208
+ // gesture can leave the viewport without getting stranded.
1209
+ if (event.isTrusted) canvas.setPointerCapture?.(event.pointerId);
1210
+ };
1211
+ pointerMoveListener = (event) => {
1212
+ if (!gestureController?.hasActiveGesture()) return;
1213
+ claim(event);
1214
+ gestureController.update(pointerEvent(event));
1215
+ };
1216
+ pointerUpListener = (event) => {
1217
+ if (!gestureController?.hasActiveGesture()) return;
1218
+ claim(event);
1219
+ if (canvas.hasPointerCapture?.(event.pointerId)) {
1220
+ canvas.releasePointerCapture?.(event.pointerId);
1221
+ }
1222
+ void gestureController.commit(pointerEvent(event));
1223
+ };
1224
+ pointerCancelListener = (event) => {
1225
+ if (!gestureController?.hasActiveGesture()) return;
1226
+ claim(event);
1227
+ void gestureController.cancel(event.pointerId);
1228
+ };
1229
+ escapeListener = (event) => {
1230
+ if (event.key !== 'Escape' || !gestureController?.hasActiveGesture()) return;
1231
+ event.preventDefault();
1232
+ void gestureController.cancel();
1233
+ };
1234
+ activateInteraction = () => {
1235
+ container.addEventListener('pointerdown', pointerDownListener!, true);
1236
+ container.addEventListener('pointermove', pointerMoveListener!, true);
1237
+ container.addEventListener('pointerup', pointerUpListener!, true);
1238
+ container.addEventListener('pointercancel', pointerCancelListener!, true);
1239
+ window.addEventListener('keydown', escapeListener!);
1240
+ };
1241
+ }
1242
+ // Candidate construction has succeeded. Publish all source references in
1243
+ // one synchronous turn; no frame or input event can see a half-swap.
1244
+ const previous = host.content;
1245
+ const selected = [...store.selectedEntityIds];
1246
+ const previousAdapter = host.adapter;
1247
+ const previousObjects = new Map(store.objectMap);
1248
+ const previousRoot = host.session?.root;
1249
+ const previousFrame = host.frame;
1250
+ const previousSyncHostScene = host.syncHostScene;
1251
+ const previousDocument = documentStateRef.current;
1252
+ const previousPersistence = persistenceSessionRef.current;
1253
+ const previousInteraction = interactionExtensionRef.current;
1254
+ const previousBackground = host.session?.neutralBackgroundTexture();
1255
+ const previousLights = host.dressing.lights.map((light) => light.visible);
1256
+ const cleanupCount = host.cleanups.length;
1257
+ const wasInitialized = host.initialized;
1258
+ rollback = () => {
1259
+ scene.removeFromParent();
1260
+ if (sourceParent) sourceParent.add(source.root);
1261
+ if (previous) host.scene.add(previous.scene);
1262
+ host.adapter = previousAdapter;
1263
+ host.content = previous;
1264
+ host.frame = previousFrame;
1265
+ host.syncHostScene = previousSyncHostScene;
1266
+ host.initialized = wasInitialized;
1267
+ for (const cleanup of host.cleanups.splice(cleanupCount)) cleanup();
1268
+ if (!previousRoot && host.session) {
1269
+ host.session.dispose();
1270
+ host.session = null;
1271
+ }
1272
+ store.objectMap.clear();
1273
+ for (const [id, object] of previousObjects) store.objectMap.set(id, object);
1274
+ if (previous)
1275
+ store.bindScene(previous.scene, renderer, viewport.batchedRenderer, viewport.camera);
1276
+ if (previousRoot && previousAdapter)
1277
+ host.session?.replaceContent(previousRoot, previousAdapter);
1278
+ documentStateRef.current = previousDocument;
1279
+ persistenceSessionRef.current = previousPersistence;
1280
+ interactionExtensionRef.current = previousInteraction;
1281
+ documentAdapterRef.current = previousAdapter;
1282
+ setDocumentAdapter(previousAdapter);
1283
+ if (previousBackground !== undefined)
1284
+ host.session?.setNeutralBackground(previousBackground);
1285
+ host.dressing?.lights.forEach((light, index) => {
1286
+ light.visible = previousLights[index] ?? true;
1287
+ });
1288
+ if (previousRoot) host.dressing?.frameContent(previousRoot);
1289
+ store.selectMultiple(selected);
1290
+ store.notifyIngestObjectMapEdit();
1291
+ };
1292
+ previous?.scene.removeFromParent();
1293
+ host.scene.add(scene);
1294
+ host.adapter = adapter;
1295
+ host.content = binding;
1296
+ store.bindScene(scene, renderer, viewport.batchedRenderer, viewport.camera);
1297
+ store.setOrbitTarget(viewport.orbitControls.target);
1298
+ store.objectMap.clear();
1299
+ scene.traverse((object) => {
1300
+ const id = adapter.hierarchy.idForObject3D?.(object);
1301
+ if (id) store.objectMap.set(id, object);
1302
+ });
1303
+ if (!host.session) {
1304
+ const overviewFrame = boxFromFrameBounds(frameBoundsRef.current);
1305
+ const openingFrame = boxFromFrameBounds(openingFrameBoundsRef.current) ?? overviewFrame;
1306
+ if (!openingFrame) viewport.focusOn(source.root);
1307
+ if (cameraX !== undefined && cameraY !== undefined && cameraZ !== undefined) {
1308
+ const box = openingFrame ?? contentWorldBounds(source.root);
1309
+ const center = box.getCenter(new THREE.Vector3());
1310
+ const direction = new THREE.Vector3(cameraX, cameraY, cameraZ).normalize();
1311
+ const distance =
1312
+ perspectiveDistanceToFitBox(box, viewport.camera, direction) *
1313
+ Math.min(10, Math.max(0.1, openingFit ?? 1));
1314
+ viewport.camera.position.copy(center).add(direction.multiplyScalar(distance));
1315
+ viewport.orbitControls.target.copy(center);
1316
+ }
1317
+ host.session = new Object3DDocumentSession(
1318
+ documentId,
1319
+ source.root,
1320
+ host.scene,
1321
+ renderer,
1322
+ viewport,
1323
+ host.dressing.lights,
1324
+ adapter,
1325
+ studioStage
1326
+ ? null
1327
+ : background === undefined
1328
+ ? host.dressing.backgroundTexture
1329
+ : new THREE.Color(background),
1330
+ overviewFrame,
1331
+ );
1332
+ host.session.selectionOutlineEnabled = !shared && selectionOutlineRef.current;
1333
+ if (!studioStage && background === undefined && host.dressing.backgroundTexture) {
1334
+ const session = host.session;
1335
+ host.cleanups.push(
1336
+ watchPaletteBackdrop(() => {
1337
+ // A palette that names a flat viewport background owns the
1338
+ // backdrop; the gradient is for palettes that name none.
1339
+ const flat = nativeViewportLook(canvas).background;
1340
+ if (flat !== null) {
1341
+ host.scene.background = new THREE.Color(flat);
1342
+ return;
1343
+ }
1344
+ const previous = host.defaultBackground;
1345
+ host.defaultBackground = createGradientBackgroundTexture();
1346
+ if (session.neutralBackgroundTexture() === previous)
1347
+ session.setNeutralBackground(host.defaultBackground);
1348
+ if (
1349
+ previous instanceof THREE.Texture &&
1350
+ previous !== host.dressing?.backgroundTexture
1351
+ )
1352
+ previous.dispose();
1353
+ }),
1354
+ );
1355
+ }
1356
+ } else host.session.replaceContent(source.root, adapter);
1357
+ const retainedCamera = retainedState.camera;
1358
+ if (retainedCamera) {
1359
+ viewport.setProjection(projection);
1360
+ viewport.setPose(
1361
+ retainedCamera.position,
1362
+ retainedCamera.target,
1363
+ retainedCamera.fov || undefined,
1364
+ );
1365
+ retainedState.camera = null;
1366
+ }
1367
+ const documentSession = host.session;
1368
+ documentStateRef.current = documentState;
1369
+ persistenceSessionRef.current = activePersistenceSession;
1370
+ interactionExtensionRef.current = interactionExtension;
1371
+ documentAdapterRef.current = adapter;
1372
+ setDocumentAdapter(adapter);
1373
+ let sourceHasLight = false;
1374
+ source.root.traverse((object) => {
1375
+ if ((object as THREE.Light).isLight) sourceHasLight = true;
1376
+ });
1377
+ for (const light of host.dressing.lights) light.visible = !sourceHasLight;
1378
+ host.dressing.frameContent(source.root);
1379
+ store.selectMultiple(selected.filter((id) => store.objectMap.has(id)));
1380
+ store.notifyIngestObjectMapEdit();
1381
+ documentSession.syncSelectionPresentation();
1382
+ activateInteraction?.();
1383
+ setError(null);
1384
+ if (!chromeless) notifyWorkspaceDocumentSelectionChanged(documentId);
1385
+ let nativeBackground = scene.background;
1386
+ documentSession.setNeutralBackground(nativeBackground ?? host.defaultBackground);
1387
+ const retainedPresentation = retainedState.presentation;
1388
+ if (retainedPresentation) {
1389
+ documentSession.setMode(retainedPresentation.mode);
1390
+ documentSession.setGrid(retainedPresentation.grid);
1391
+ documentSession.setBackground(retainedPresentation.background);
1392
+ documentSession.setExposure(retainedPresentation.exposure);
1393
+ documentSession.setProjection(retainedPresentation.projection);
1394
+ documentSession.setLighting(retainedPresentation.lighting);
1395
+ documentSession.setSkeleton(retainedPresentation.skeleton);
1396
+ documentSession.setBounds(retainedPresentation.bounds);
1397
+ retainedState.presentation = null;
1398
+ }
1399
+ // The document's world dressing lives on its OWN scene, nested inside the
1400
+ // rendered `host.scene`. Every draw of the rendered scene mirrors it out
1401
+ // first — the viewport frame and every offscreen photograph alike — so a
1402
+ // capture can never show the dressing the last viewport tick happened to
1403
+ // leave behind. The session runs this before it renders; nothing else may
1404
+ // hold a second copy of this field list.
1405
+ const syncHostScene = () => {
1406
+ host.scene.environment = scene.environment ?? host.defaultEnvironment;
1407
+ host.scene.fog = scene.fog;
1408
+ host.scene.environmentIntensity = scene.environmentIntensity;
1409
+ host.scene.environmentRotation.copy(scene.environmentRotation);
1410
+ host.scene.backgroundIntensity = scene.backgroundIntensity;
1411
+ host.scene.backgroundBlurriness = scene.backgroundBlurriness;
1412
+ host.scene.backgroundRotation.copy(scene.backgroundRotation);
1413
+ if (nativeBackground !== scene.background) {
1414
+ nativeBackground = scene.background;
1415
+ documentSession.setNeutralBackground(nativeBackground ?? host.defaultBackground);
1416
+ }
1417
+ };
1418
+ host.syncHostScene = syncHostScene;
1419
+ documentSession.setBeforeRender(() => host.syncHostScene?.());
1420
+ // This loop draws the compass and every other overlay pass over the
1421
+ // document's own render, so it — and only it — can serve the chrome
1422
+ // door a frame that matches the screen.
1423
+ documentSession.setPresentsFrames(true);
1424
+ // THE CONTENT-TIME DOOR, re-created in Step 3 with exactly one caller
1425
+ // (the transport's `tick` below). The mixer line the deleted estate
1426
+ // had here is gone for good: subjects seek THEMSELVES, so content time
1427
+ // is only ever the bookkeeping plus the source's own update.
1428
+ const advanceContent = (deltaSeconds: number): void => {
1429
+ if (disposed || !(deltaSeconds > 0) || !Number.isFinite(deltaSeconds)) return;
1430
+ host.contentSeconds += deltaSeconds;
1431
+ host.transportAdvances++;
1432
+ source.update?.(deltaSeconds);
1433
+ };
1434
+ // What this stage can show at a time. Scanned once here; the world
1435
+ // root's stage rescans (below) because its tree arrives later.
1436
+ const clipScan = scanClipSubjects(source.root, host.transport);
1437
+ host.cleanups.push(() => clipScan.dispose());
1438
+ const retainedTransport = retainedState.transport;
1439
+ if (retainedTransport) {
1440
+ if (retainedTransport.activeSubject)
1441
+ host.transport.setActiveSubject(retainedTransport.activeSubject);
1442
+ host.transport.setLoop(retainedTransport.range.loop);
1443
+ host.transport.setTimeScale(retainedTransport.timeScale);
1444
+ host.transport.seek(retainedTransport.time);
1445
+ if (retainedTransport.playbackState === 'playing') host.transport.play();
1446
+ else if (retainedTransport.playbackState === 'paused') host.transport.pause();
1447
+ retainedState.transport = null;
1448
+ }
1449
+ let previousTime = performance.now();
1450
+ const animate = (time: number, resumed: boolean) => {
1451
+ if (disposed || !renderer || !viewport) return;
1452
+ const activeRenderer = renderer;
1453
+ const activeViewport = viewport;
1454
+ const profiler = documentSession?.profiler;
1455
+ profiler?.beginFrame();
1456
+ if (resumed) previousTime = time;
1457
+ const delta = Math.min((time - previousTime) / 1000, 0.1);
1458
+ previousTime = time;
1459
+ profiler?.beginPhase();
1460
+ // CONTENT TIME moves ONLY through the stage transport, and only
1461
+ // while a human is holding it: `tick` is inert unless playing and
1462
+ // the editor drives, and it returns exactly the seconds it advanced.
1463
+ const advanced = host.transport.tick(delta);
1464
+ if (advanced > 0) advanceContent(advanced);
1465
+ interactionExtension?.prepareFrame?.(delta);
1466
+ interactionExtension?.update?.(delta);
1467
+ profiler?.endPhase('animation');
1468
+ profiler?.beginPhase();
1469
+ host.presence?.syncMarkers(delta);
1470
+ viewport.update(delta);
1471
+ // This stage's frame hook. After viewport.update, the same order the
1472
+ // scene panel uses, so anything riding the loop (a camera flight)
1473
+ // has final say over the pose and orbit damping never fights it.
1474
+ if (!chromeless) runViewportFrame(documentId, delta);
1475
+ viewport.batchedRenderer.update(delta);
1476
+ profiler?.endPhase('editor');
1477
+ profiler?.beginPhase();
1478
+ activeRenderer.info.reset();
1479
+ const timeFirstRender =
1480
+ !firstFrameGateRef.current && activeViewportBreakdownDocumentId() === documentId;
1481
+ if (timeFirstRender) markViewportFirstRenderStart(documentId);
1482
+ const tFirstRender = timeFirstRender ? Date.now() : 0;
1483
+ const presentationOverride = host.scene.overrideMaterial;
1484
+ const mode = documentSession.presentation().mode;
1485
+ if (mode !== 'uv' && mode !== 'vertex-colors')
1486
+ host.scene.overrideMaterial = scene.overrideMaterial;
1487
+ try {
1488
+ activeViewport.renderWithInfrastructure(() => {
1489
+ documentSession.renderViewport(delta);
1490
+ activeViewport.renderViewCube(activeRenderer);
1491
+ });
1492
+ } finally {
1493
+ host.scene.overrideMaterial = presentationOverride;
1494
+ }
1495
+ // Inside the frame that drew them — the canvas has no
1496
+ // preserveDrawingBuffer, so this is the only moment its pixels exist.
1497
+ documentSession.servePresentedFrame();
1498
+ if (!firstFrameGateRef.current) {
1499
+ firstFrameGateRef.current = true;
1500
+ if (timeFirstRender) markViewportSegment('first-render', Date.now() - tFirstRender);
1501
+ recordViewportFirstFrame(documentId);
1502
+ setSurfaceStatus('ready');
1503
+ }
1504
+ profiler?.reportRender({
1505
+ gpuMs: null,
1506
+ drawCalls: activeRenderer.info.render.calls,
1507
+ triangles: activeRenderer.info.render.triangles,
1508
+ geometries: activeRenderer.info.memory.geometries,
1509
+ textures: activeRenderer.info.memory.textures,
1510
+ });
1511
+ profiler?.endPhase('render');
1512
+ profiler?.endFrame();
1513
+ // THE SHELL'S READOUT IS THE FOCUSED STAGE'S (ARCHITECTURE-CORE
1514
+ // §One stage unit 4). `CameraInfo` and `StatsOverlay` read one
1515
+ // struct the shell owns; the stage the panels are following fills
1516
+ // it, so a model or prefab document reports ITS frame and ITS
1517
+ // camera. Inactive documents leave the shared readout alone.
1518
+ const shellReadout = shellStatsRef.current;
1519
+ if (shellReadout && shellStore && focusedStageStore(shellStore) === store) {
1520
+ const pose = documentSession.cameraPose();
1521
+ shellReadout.frameTime = delta * 1000;
1522
+ shellReadout.fps = delta > 0 ? 1 / delta : 0;
1523
+ shellReadout.drawCalls = activeRenderer.info.render.calls;
1524
+ shellReadout.triangles = activeRenderer.info.render.triangles;
1525
+ shellReadout.cameraPosition.x = pose.position[0];
1526
+ shellReadout.cameraPosition.y = pose.position[1];
1527
+ shellReadout.cameraPosition.z = pose.position[2];
1528
+ shellReadout.cameraTarget.x = pose.target[0];
1529
+ shellReadout.cameraTarget.y = pose.target[1];
1530
+ shellReadout.cameraTarget.z = pose.target[2];
1531
+ }
1532
+ };
1533
+
1534
+ host.frame = animate;
1535
+ rendererSessionRef.current = host.rendererSession;
1536
+ if (!host.initialized) {
1537
+ if (!chromeless) {
1538
+ // --- The viewport door (viewport-door.ts) ---
1539
+ // This host is ONE STAGE among the mounted 3D documents
1540
+ // (ARCHITECTURE-CORE §One stage), and it binds under the id of the
1541
+ // document it draws, so an SDK reader reaches THIS stage's rig,
1542
+ // helper sink and frame loop through `host.viewport.stages()`.
1543
+ // It presents no live roots yet — Play's adoption is the world
1544
+ // root's, and moves onto this host in unit 3 — so the presenter
1545
+ // declines every root rather than pretending to a subject.
1546
+ // A CHROMELESS mount (the inspector's object preview) binds
1547
+ // nothing: it is a thumbnail of a document, not a stage of its own,
1548
+ // and several can be alive for one document id at once.
1549
+ host.cleanups.push(
1550
+ bindViewportRig(
1551
+ {
1552
+ camera: viewport.camera,
1553
+ orbit: viewport.orbitControls,
1554
+ scene: host.scene,
1555
+ },
1556
+ () => null,
1557
+ (kind, object) => viewport.setHelper(kind, object),
1558
+ { documentId },
1559
+ ),
1560
+ );
1561
+ // --- The other participants, on THIS stage ---
1562
+ // Presence was the scene panel's, so only the world root had it.
1563
+ // It is a capability with a condition — a stage painting a three
1564
+ // surface — and a `build` stage always paints one, so every
1565
+ // document with chrome mounts it and a prefab story shows the
1566
+ // same camera frusta, selection boxes and pointer rays the Scene
1567
+ // does (ARCHITECTURE-CORE §One stage; WORK.md §Presence and the
1568
+ // substrate, presence unit 4). Dynamically imported: it reaches
1569
+ // the collaboration client, which a bounded host must not pay for.
1570
+ void import('./stage-presence-markers').then((markers) => {
1571
+ if (host.isClosed || documentHostRef.current !== host || host.presence) return;
1572
+ const binding = markers.bindStagePresenceMarkers({
1573
+ // The HOST's scene, not this revision's content scene: a
1574
+ // source update swaps the content and the markers must not go
1575
+ // with it.
1576
+ scene: host.scene,
1577
+ store,
1578
+ documentId,
1579
+ container,
1580
+ canvas,
1581
+ viewport,
1582
+ readVisibleCameraPose: () => {
1583
+ const camera = host.session?.camera() ?? viewport.renderCamera;
1584
+ const position = camera.getWorldPosition(new THREE.Vector3());
1585
+ const fov = (camera as THREE.PerspectiveCamera).fov;
1586
+ return {
1587
+ position,
1588
+ target: viewport.orbitControls.target,
1589
+ fov: typeof fov === 'number' ? fov : 0,
1590
+ };
1591
+ },
1592
+ });
1593
+ host.presence = binding;
1594
+ host.cleanups.push(() => binding.dispose());
1595
+ });
1596
+ host.cleanups.push(registerObject3DDocumentSession(documentSession));
1597
+ host.cleanups.push(
1598
+ registerPerformanceSource({
1599
+ id: documentId,
1600
+ label: displayName,
1601
+ kind: assetSubject ? 'asset' : 'source',
1602
+ profiler: documentSession.profiler,
1603
+ }),
1604
+ );
1605
+ }
1606
+ // EXACTLY ONE registrar of this document's workspace selection: the
1607
+ // identity-row shell does it through its own `selection` prop, so
1608
+ // the host does it wherever that shell is absent. The condition was
1609
+ // `!studioStage` while the shell and the studio were the same
1610
+ // answer; they parted when a DATA subject gained the studio
1611
+ // (§A model is data), and reading the old one left the Model
1612
+ // document with NEITHER registrar — the Hierarchy said "No
1613
+ // authoring adapter" and the mesh inspector emptied (measured live
1614
+ // on the models scaffold, 2026-09-18).
1615
+ if (!chromeless && !hasShell) {
1616
+ host.cleanups.push(
1617
+ registerWorkspaceDocumentSelection(documentId, () => ({
1618
+ adapter: host.adapter!,
1619
+ nodeId: host.adapter?.selection?.get()[0] ?? null,
1620
+ })),
1621
+ );
1622
+ }
1623
+ const designTimeSurfaceId = chromeless
1624
+ ? `${documentId}#chromeless-${++chromelessSurfaceSequence}`
1625
+ : documentId;
1626
+ host.cleanups.push(
1627
+ registerDesignTimeSurface({
1628
+ id: designTimeSurfaceId,
1629
+ label: displayName,
1630
+ contentClock: () => host.contentSeconds,
1631
+ transportAdvances: () => host.transportAdvances,
1632
+ }),
1633
+ );
1634
+ // THE VIEWPORT ACTION BUS, on a document stage. The world root has
1635
+ // answered these since the scene panel existed; a document stage
1636
+ // never did, which is why Frame (`F`) was inert on a prefab or a
1637
+ // model — the hotkey pushes `focus-selection` onto the stage's own
1638
+ // store and nothing was listening (ARCHITECTURE-CORE §One stage:
1639
+ // the capability is present wherever its condition holds).
1640
+ host.cleanups.push(
1641
+ store.onViewportAction((action) => {
1642
+ switch (action.type) {
1643
+ case 'focus-selection':
1644
+ case 'focus-scene':
1645
+ // The session frames the selection, else the whole subject,
1646
+ // and says so on the console when there is nothing to frame.
1647
+ if (!host.session?.frame() && host.session?.root)
1648
+ viewport.focusOn(host.session.root);
1649
+ break;
1650
+ case 'focus-entity': {
1651
+ const object = host.adapter?.hierarchy.object3D?.(action.id) ?? null;
1652
+ if (object) viewport.focusOn(object);
1653
+ break;
1654
+ }
1655
+ case 'snap-selection-to-floor':
1656
+ viewport.snapSelectionToFloor();
1657
+ break;
1658
+ case 'set-view-preset':
1659
+ viewport.setViewPreset(action.preset);
1660
+ break;
1661
+ case 'set-camera-pose':
1662
+ viewport.setPose(action.position, action.target, action.fov);
1663
+ break;
1664
+ }
1665
+ }),
1666
+ );
1667
+ let selectionSignature = '';
1668
+ host.cleanups.push(
1669
+ store.subscribe(() => {
1670
+ viewport.objectMap = store.objectMap;
1671
+ viewport.syncFromStore();
1672
+ host.session?.syncSelectionPresentation();
1673
+ const next = (host.adapter?.selection?.get() ?? []).join('\u0000');
1674
+ if (next !== selectionSignature) {
1675
+ selectionSignature = next;
1676
+ if (!chromeless) notifyWorkspaceDocumentSelectionChanged(documentId);
1677
+ }
1678
+ }),
1679
+ );
1680
+ const resize = () => {
1681
+ const width = Math.max(1, container.clientWidth);
1682
+ const height = Math.max(1, container.clientHeight);
1683
+ const ratio = renderer.getPixelRatio();
1684
+ if (
1685
+ canvas.width !== Math.floor(width * ratio) ||
1686
+ canvas.height !== Math.floor(height * ratio)
1687
+ )
1688
+ renderer.setSize(width, height, false);
1689
+ viewport.resize(width, height);
1690
+ host.session?.resize(width, height);
1691
+ host.rendererSession.redraw();
1692
+ };
1693
+ const observer = new ResizeObserver(resize);
1694
+ observer.observe(container);
1695
+ host.cleanups.push(() => observer.disconnect());
1696
+ resize();
1697
+ host.initialized = true;
1698
+ markViewportConstructReady();
1699
+ }
1700
+ viewport.objectMap = store.objectMap;
1701
+ viewport.syncFromStore();
1702
+ host.rendererSession.setActive(activeRef.current);
1703
+ host.rendererSession.redraw();
1704
+ published = true;
1705
+ // Old graph stays owned until the new binding is fully published.
1706
+ void previous?.dispose();
1707
+ } catch (caught) {
1708
+ if (!published) {
1709
+ try {
1710
+ rollback?.();
1711
+ } finally {
1712
+ if (sourceParent && source.root.parent === scene) sourceParent.add(source.root);
1713
+ void binding.dispose();
1714
+ }
1715
+ }
1716
+ // Candidate construction leaves the last good binding and pixels live.
1717
+ // biome-ignore lint/suspicious/noConsole: source failure must remain diagnosable
1718
+ console.error(`[object3d document] ${displayName}: source update failed`, caught);
1719
+ setError(caught instanceof Error ? caught.message : String(caught));
1720
+ }
1721
+ };
1722
+ const update = contentUpdateTail.current.then(install);
1723
+ contentUpdateTail.current = update.catch((caught) => {
1724
+ if (!cancelled) setError(caught instanceof Error ? caught.message : String(caught));
1725
+ });
1726
+ return () => {
1727
+ cancelled = true;
1728
+ };
1729
+ }, [
1730
+ authoring,
1731
+ background,
1732
+ build,
1733
+ cameraX,
1734
+ cameraY,
1735
+ cameraZ,
1736
+ displayName,
1737
+ documentId,
1738
+ studioStage,
1739
+ dressingBackground,
1740
+ dressingEnvironment,
1741
+ dressingGrid,
1742
+ dressingKeyLight,
1743
+ dressingViewLocked,
1744
+ dressingToneMapping,
1745
+ chromeless,
1746
+ modelSourceEntityId,
1747
+ modelSourceKind,
1748
+ modelSourcePath,
1749
+ interaction,
1750
+ persistence,
1751
+ documentSource,
1752
+ rendererLane,
1753
+ retainedState,
1754
+ surfaceAttached,
1755
+ sourceAuthoring,
1756
+ sourcePath,
1757
+ ]);
1758
+
1759
+ useEffect(() => {
1760
+ const session = rendererSessionRef.current;
1761
+ const becameActive = active && !previousActiveRef.current;
1762
+ previousActiveRef.current = active;
1763
+ if (!session) return;
1764
+ if (becameActive) {
1765
+ firstFrameGateRef.current = false;
1766
+ markViewportRafResume(documentId);
1767
+ }
1768
+ session.setActive(active);
1769
+ }, [active, documentId]);
1770
+
1771
+ // The stage's own handle for the capabilities that drive it directly (the
1772
+ // viewport hotkeys). `surfaceStatus` is what re-renders once the stage has
1773
+ // finished building, so reading the ref here is safe and never stale.
1774
+ const host = documentHostRef.current;
1775
+ const stageHandle =
1776
+ surfaceStatus === 'ready' && host?.viewport
1777
+ ? { store: host.store, viewport: host.viewport, canvas: host.surface.canvas }
1778
+ : null;
1779
+ // The document's own selection changes as you pick inside it; the registry
1780
+ // reads this on demand, so the picked part follows without re-registering.
1781
+ // With nothing picked the DOCUMENT NODE is the subject — the same reading
1782
+ // as clicking a model's root in the scene.
1783
+ const readSelection = useCallback(() => {
1784
+ const owner = documentAdapterRef.current;
1785
+ return owner
1786
+ ? {
1787
+ adapter: owner,
1788
+ nodeId: owner.selection?.get()[0] ?? owner.hierarchy.roots()[0]?.id ?? null,
1789
+ }
1790
+ : null;
1791
+ }, []);
1792
+ const viewport = (
1793
+ <div
1794
+ data-testid="tool-object3d-authoring"
1795
+ data-vgai-chromeless={chromeless || undefined}
1796
+ className={studioStage ? 'vgai-object3d-studio-stage' : undefined}
1797
+ style={{
1798
+ position: chromeless || hasShell ? 'relative' : 'absolute',
1799
+ // Inside the Asset Editor shell the stage fills its own box exactly;
1800
+ // in a bare dock panel it bleeds under the panel's padding.
1801
+ ...(chromeless
1802
+ ? { width: '100%', height: '100%' }
1803
+ : hasShell
1804
+ ? { flex: 1, minHeight: 0 }
1805
+ : { inset: -STAGE_BLEED_PX }),
1806
+ overflow: 'hidden',
1807
+ // Asset documents paint the shared studio stage through their class.
1808
+ // An inline background here would win the cascade and hide it.
1809
+ background: studioStage ? undefined : themeVars.surface.raised,
1810
+ pointerEvents: 'auto',
1811
+ }}
1812
+ >
1813
+ <div
1814
+ ref={containerRef}
1815
+ style={{ position: 'absolute', inset: 0, minHeight: 0, overflow: 'hidden' }}
1816
+ >
1817
+ <ViewportSurface canvasHostRef={canvasHostRef} />
1818
+ {/* WHICH KEYS THIS STAGE ANSWERS — the transform modes, the view
1819
+ presets, Frame, pivot, snap. Beside the stage rather than inside
1820
+ the overlay set, because a package-contributed document mounts
1821
+ with no shell above it and reached none of them
1822
+ (`stage-keyboard.tsx`). The registry holds ONE stage's set, so the
1823
+ condition is the ACTIVE document's stage. */}
1824
+ {active && stageHandle && !chromeless ? (
1825
+ <Suspense fallback={null}>
1826
+ <LazyStageKeyboard stage={stageHandle} />
1827
+ </Suspense>
1828
+ ) : null}
1829
+ {/* The stage's own overlays, co-located with the canvas the way
1830
+ `RootSelectionOverlay`'s DOM contract requires (its marquee is
1831
+ measured against this container's box). A stage with no shell
1832
+ above it — a bounded host — renders none of them and never loads
1833
+ them. */}
1834
+ {shellStore && !chromeless ? (
1835
+ <StageOverlays
1836
+ store={shellStore}
1837
+ documentId={documentId}
1838
+ stage={stageHandle}
1839
+ active={active}
1840
+ chrome="document"
1841
+ />
1842
+ ) : null}
1843
+ {/* A ROLLED-BACK operation (an animation write, a capture, a source
1844
+ write the history guarded) — every writer of `error` restores the
1845
+ prior state, so the surface below is live and must stay visible.
1846
+ It used to mount a full-bleed opaque curtain with no dismiss: the
1847
+ stage read as dead until the session restarted (measured). A
1848
+ notice over the stage, dismissible, and cleared by the next
1849
+ gesture that succeeds. */}
1850
+ {error && (
1851
+ <div
1852
+ role="alert"
1853
+ style={{
1854
+ position: 'absolute',
1855
+ insetInline: 0,
1856
+ top: 0,
1857
+ display: 'flex',
1858
+ justifyContent: 'center',
1859
+ padding: 'var(--vgai-space-3)',
1860
+ pointerEvents: 'none',
1861
+ }}
1862
+ >
1863
+ <div
1864
+ className="vgai-chrome-island"
1865
+ style={{
1866
+ display: 'flex',
1867
+ alignItems: 'center',
1868
+ gap: 'var(--vgai-space-3)',
1869
+ maxWidth: '80%',
1870
+ padding: 'var(--vgai-space-2) var(--vgai-space-3)',
1871
+ borderRadius: 'var(--vgai-radius-md)',
1872
+ border: `var(--vgai-stroke-resting) solid ${themeVars.semantic.danger}`,
1873
+ background: themeVars.surface.raised,
1874
+ color: themeVars.semantic.danger,
1875
+ pointerEvents: 'auto',
1876
+ }}
1877
+ >
1878
+ <span>{error}</span>
1879
+ <IconButton
1880
+ size="compact"
1881
+ aria-label="Dismiss"
1882
+ onClick={() => setError(null)}
1883
+ data-testid="object3d-document-error-dismiss"
1884
+ >
1885
+ <EditorIcon icon={editorIcons.action.close} />
1886
+ </IconButton>
1887
+ </div>
1888
+ </div>
1889
+ )}
1890
+ {!error && surfaceStatus === 'building' && (
1891
+ <ViewportSurfaceStatus testId="object3d-document-status">
1892
+ {OBJECT3D_SURFACE_BUILDING}
1893
+ </ViewportSurfaceStatus>
1894
+ )}
1895
+ {/* The stage BLEEDS 12px under the dock panel's padding (`inset: -12`
1896
+ above) so the rendered image fills it edge to edge. The furniture is
1897
+ CHROME, not image: it has to sit inside the panel the person can
1898
+ actually see, so it gets that bleed back. Without this the view text
1899
+ started at page x=-6 — the "U" of "User Perspective" was clipped off
1900
+ the window — and the navigation cluster's right edge landed 4px past
1901
+ the panel, under the Inspector's section rail (both measured). */}
1902
+ <div
1903
+ style={{
1904
+ position: 'absolute',
1905
+ inset: chromeless || hasShell ? 0 : STAGE_BLEED_PX,
1906
+ pointerEvents: 'none',
1907
+ }}
1908
+ >
1909
+ {!chromeless && surfaceStatus === 'ready' && documentHostRef.current && (
1910
+ <ViewportFurniture
1911
+ viewport={documentHostRef.current.viewport}
1912
+ session={documentHostRef.current.session}
1913
+ store={documentHostRef.current.store}
1914
+ projection={projection}
1915
+ displayName={displayName}
1916
+ {...(statistics ? { statistics } : {})}
1917
+ objectName={(id) =>
1918
+ documentHostRef.current?.adapter?.hierarchy.object3D?.(id)?.name ??
1919
+ // Document builders may supply names through their store index.
1920
+ documentHostRef.current?.store.objectMap.get(id)?.name ??
1921
+ null
1922
+ }
1923
+ />
1924
+ )}
1925
+ </div>
1926
+ </div>
1927
+ </div>
1928
+ );
1929
+ if (!hasShell) return viewport;
1930
+ return (
1931
+ <AssetEditorShell
1932
+ documentId={documentId}
1933
+ active={active}
1934
+ type={assetType ?? (modelSourceEntityId ? 'entity' : 'model')}
1935
+ title={displayName}
1936
+ status={sourcePath}
1937
+ selection={readSelection}
1938
+ fill
1939
+ >
1940
+ <div className="vgai-object3d-asset-workspace">
1941
+ {viewport}
1942
+ {/* Where the deleted preview strip was. It draws nothing until this
1943
+ stage's transport has a subject, so a document with nothing to show
1944
+ at a time is unchanged. */}
1945
+ <TransportStrip transport={documentHostRef.current?.transport ?? null} />
1946
+ </div>
1947
+ </AssetEditorShell>
1948
+ );
1949
+ }
1950
+
1951
+ /** Public project contribution surface over the editor's native Object3D document host. */
1952
+ export function ToolObject3DAuthoring(props: ToolObject3DAuthoringProps) {
1953
+ return <Object3DDocumentViewport {...props} />;
1954
+ }