@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,1326 @@
1
+ import { createPerformanceProfiler } from '../performance-profiler';
2
+ import type { AuthoringAdapter } from '@volter/editor-project/adapter';
3
+ import { viewportCaptureOutputPass } from '@volter/editor-threejs/capture/output-pass';
4
+ import { contentWorldBounds } from '@volter/editor-threejs/viewport/content-bounds';
5
+ import { setUserData } from '@volter/editor-threejs/ecs/user-data';
6
+ import {
7
+ type ViewportShadingMode,
8
+ ViewportShadingRenderer,
9
+ } from '@volter/editor-threejs/render/viewport-shading';
10
+ import type { EffectComposer, EffectPass, RenderPass } from 'postprocessing';
11
+ import * as THREE from 'three';
12
+ import { cameraPresetDirection, type ModelCameraPreset } from '../asset-workflow/model-inspection';
13
+ import { editorConsole } from '../editor-console';
14
+ import { type EditorViewport } from '../editor-viewport';
15
+ import { nativeSelectionColors, subscribeNativeSelectionTheme } from '../native-selection-style';
16
+ import { BoneSelectionHighlight } from '../three-viewport/bone-selection-highlight';
17
+ import { perspectiveDistanceToFitBox } from '../three-viewport/camera-fit';
18
+ import {
19
+ createThreeSelectionOutline,
20
+ setThreeSelectionOutlineColors,
21
+ syncThreeSelectionOutline,
22
+ } from '../three-viewport/selection-outline';
23
+ import { styleEditorSkeletonHelper } from '../three-viewport/skeleton-helper';
24
+ import { isEditorViewportShadingTarget } from '../viewport-shading-boundary';
25
+ import { setAuthoringSelection } from './consumer-actions';
26
+
27
+ export type Object3DDocumentViewMode = ViewportShadingMode | 'uv' | 'vertex-colors';
28
+
29
+ /**
30
+ * How a look flight ended. Never a rejection: "the human grabbed the view
31
+ * mid-orbit" is an ordinary outcome of a shared camera, not an error, and the
32
+ * caller needs to be able to tell it apart from "it ran to the end".
33
+ */
34
+ export interface DocumentLookOutcome {
35
+ readonly completed: boolean;
36
+ /** Only when `completed` is false. */
37
+ readonly cancelledBy?: 'human' | 'superseded' | 'closed';
38
+ /** Where the camera actually ended up, in the pivot's spherical frame. */
39
+ readonly azimuth: number;
40
+ readonly elevation: number;
41
+ readonly seconds: number;
42
+ }
43
+
44
+ /**
45
+ * One in-flight camera move. The pivot and radius are captured at LAUNCH from
46
+ * wherever the human left the view, so a flight always lerps from the current
47
+ * pose rather than snapping to a canonical one.
48
+ */
49
+ interface CameraFlight {
50
+ readonly pivot: THREE.Vector3;
51
+ readonly radius: number;
52
+ readonly startTheta: number;
53
+ readonly startPhi: number;
54
+ readonly deltaTheta: number;
55
+ readonly deltaPhi: number;
56
+ readonly duration: number;
57
+ readonly ease: (t: number) => number;
58
+ elapsed: number;
59
+ theta: number;
60
+ phi: number;
61
+ settle: ((outcome: DocumentLookOutcome) => void) | null;
62
+ }
63
+
64
+ /**
65
+ * Dihedral angle below which an interior edge is dropped from the topology
66
+ * overlay. At 1° a PLANAR quad's triangulation diagonal disappears while every
67
+ * real face boundary survives — which is the entire reason the overlay exists
68
+ * (`toBufferGeometry` fan-triangulates, so nothing downstream of the mesh kit
69
+ * still knows which pairs of triangles were one quad).
70
+ */
71
+ const TOPOLOGY_EDGE_THRESHOLD_DEGREES = 1;
72
+
73
+ /** OrbitControls' own poles-excluded range; orbiting past them flips the view. */
74
+ /** How long a presented-frame request waits on the host's draw loop before it
75
+ * gives the chrome door back `null` (see `requestPresentedFrame`). Two frames
76
+ * at 30fps is a loop that is running; longer means it is not. */
77
+ const PRESENTED_FRAME_WAIT_MS = 700;
78
+
79
+ const MIN_POLAR = 1e-3;
80
+ const MAX_POLAR = Math.PI - 1e-3;
81
+
82
+ const easeInOutCubic = (t: number): number => (t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2);
83
+ const linear = (t: number): number => t;
84
+
85
+ function finiteOr(value: unknown, fallback: number): number {
86
+ return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
87
+ }
88
+
89
+ export interface Object3DDocumentPresentationState {
90
+ readonly mode: Object3DDocumentViewMode;
91
+ readonly grid: boolean;
92
+ readonly background: 'neutral' | 'transparent';
93
+ readonly exposure: number;
94
+ readonly projection: 'perspective' | 'orthographic';
95
+ readonly lighting: 'studio' | 'outdoor' | 'flat';
96
+ readonly skeleton: boolean;
97
+ readonly bounds: boolean;
98
+ }
99
+
100
+ const INITIAL_PRESENTATION: Object3DDocumentPresentationState = {
101
+ mode: 'solid',
102
+ grid: true,
103
+ background: 'neutral',
104
+ exposure: 1,
105
+ projection: 'perspective',
106
+ lighting: 'studio',
107
+ skeleton: false,
108
+ bounds: false,
109
+ };
110
+
111
+ /**
112
+ * Live presentation controls for one native Object3D document. The model
113
+ * graph remains truth; this session owns only editor chrome and diagnostics.
114
+ */
115
+ export class Object3DDocumentSession {
116
+ readonly profiler = createPerformanceProfiler();
117
+ private readonly shading = new ViewportShadingRenderer();
118
+ private readonly helpers: THREE.Object3D[] = [];
119
+ private skeletonHelper: THREE.SkeletonHelper | null = null;
120
+ private boundsHelper: THREE.BoxHelper | null = null;
121
+ private boneSelectionHighlight: BoneSelectionHighlight | null = null;
122
+ private boneSelectionSignature = '';
123
+ private composer: EffectComposer | null = null;
124
+ /** One in-flight `import('postprocessing')` at a time — {@link ensureComposer}
125
+ * is called from every frame. */
126
+ private composerLoading = false;
127
+ private disposed = false;
128
+ private sceneRenderPass: RenderPass | null = null;
129
+ private selectionOutline: ReturnType<typeof createThreeSelectionOutline> | null = null;
130
+ private selectionOutlinePass: EffectPass | null = null;
131
+ private selectedObjects: THREE.Object3D[] = [];
132
+ private renderWidth = 1;
133
+ private renderHeight = 1;
134
+ private readonly orthographicCamera = new THREE.OrthographicCamera();
135
+ /** The offscreen render target's aspect, non-null ONLY inside
136
+ * {@link Object3DDocumentSession.captureImage} — whose buffer has its own
137
+ * shape (square by default, `{width, height}` when a caller asks for one)
138
+ * while the live camera's aspect is the panel's. */
139
+ private captureAspect: number | null = null;
140
+ /** The camera a capture door was handed, for the duration of that capture.
141
+ * A BLENDER RENDER IS NOT THE MODELING VIEWPORT: it photographs the scene
142
+ * through the scene's OWN camera, which has nothing to do with where the
143
+ * person is looking — see `blender-runtime-host.ts`. Before this existed the
144
+ * only way to photograph from another camera was to move the viewport onto
145
+ * it and move it back, which is why that code had a restore dance at all. */
146
+ private cameraOverride: THREE.Camera | null = null;
147
+ /** The host's mirror of the document scene onto the rendered scene — see
148
+ * {@link Object3DDocumentSession.setBeforeRender}. */
149
+ private beforeRender: (() => void) | null = null;
150
+ private state = INITIAL_PRESENTATION;
151
+ private version = 0;
152
+ private readonly listeners = new Set<() => void>();
153
+ private unsubscribeSelectionTheme: () => void = () => {};
154
+ /** The one in-flight look move. See {@link Object3DDocumentSession.orbit}. */
155
+ private flight: CameraFlight | null = null;
156
+ /** The camera position the live flight wrote LAST frame — the drift check
157
+ * in {@link Object3DDocumentSession.advanceLook} compares against it. */
158
+ private readonly flightPose = new THREE.Vector3();
159
+ /** Wireframe's edge overlay and the meshes each line set shadows. */
160
+ private topologyOverlay: THREE.Group | null = null;
161
+ private topologyFollowers: Array<{
162
+ readonly line: THREE.LineSegments;
163
+ readonly source: THREE.Object3D;
164
+ }> = [];
165
+
166
+ constructor(
167
+ readonly documentId: string,
168
+ public root: THREE.Object3D,
169
+ readonly scene: THREE.Scene,
170
+ readonly renderer: THREE.WebGLRenderer,
171
+ readonly viewport: EditorViewport,
172
+ private readonly editorLights: readonly THREE.Light[] = [],
173
+ private authoring?: AuthoringAdapter,
174
+ /** Asset Lab renders its neutral stage in editor chrome behind an alpha
175
+ * canvas; other document hosts may retain a real Three background. */
176
+ private neutralBackground: THREE.Color | THREE.Texture | null = new THREE.Color(0x20242a),
177
+ /** When set, Frame and view presets fit this box instead of the root AABB. */
178
+ private readonly frameBox: THREE.Box3 | null = null,
179
+ ) {
180
+ this.unsubscribeSelectionTheme = subscribeNativeSelectionTheme(
181
+ renderer.domElement,
182
+ this.syncSelectionTheme,
183
+ );
184
+ // THE HUMAN ALWAYS WINS THE CAMERA. OrbitControls fires `start` on the
185
+ // pointer-down that begins a real drag (its `update()` only ever fires
186
+ // `change`), so this is the one event that means "a person just grabbed
187
+ // this view". An agent flight yields at that instant, leaving the camera
188
+ // exactly where it is: no snap-back, no two writers fighting for the pose.
189
+ viewport.orbitControls.addEventListener('start', this.cancelLookForHuman);
190
+ }
191
+
192
+ /** Replace authored content without replacing the editor session or camera. */
193
+ replaceContent(root: THREE.Object3D, authoring: AuthoringAdapter): void {
194
+ this.selectionOutline?.selection.clear();
195
+ this.selectedObjects = [];
196
+ this.boneSelectionSignature = '';
197
+ this.clearBoneSelectionHighlight();
198
+ this.clearSkeletonHelper();
199
+ this.clearBoundsHelper();
200
+ this.clearTopologyOverlay();
201
+ this.root = root;
202
+ this.authoring = authoring;
203
+ if (this.state.mode === 'wireframe') this.buildTopologyOverlay();
204
+ if (this.state.bounds) {
205
+ this.boundsHelper = new THREE.BoxHelper(root, 0xffb454);
206
+ this.addHelper(this.boundsHelper, 'bounds');
207
+ }
208
+ this.syncSelectionPresentation();
209
+ this.refreshSkeletonHelper();
210
+ this.notify();
211
+ }
212
+
213
+ private readonly cancelLookForHuman = (): void => {
214
+ this.settleFlight('human');
215
+ };
216
+
217
+ private readonly syncSelectionTheme = (): void => {
218
+ const color = nativeSelectionColors(this.renderer.domElement).visible;
219
+ if (this.selectionOutline) {
220
+ setThreeSelectionOutlineColors(
221
+ this.selectionOutline,
222
+ nativeSelectionColors(this.renderer.domElement),
223
+ );
224
+ }
225
+ this.boneSelectionHighlight?.setColor(color);
226
+ };
227
+
228
+ subscribe = (listener: () => void): (() => void) => {
229
+ this.listeners.add(listener);
230
+ return () => this.listeners.delete(listener);
231
+ };
232
+
233
+ getSnapshot = (): number => this.version;
234
+
235
+ presentation(): Object3DDocumentPresentationState {
236
+ return this.state;
237
+ }
238
+
239
+ private resolveFrameBounds(): THREE.Box3 {
240
+ const selection = this.authoring?.selection?.get() ?? [];
241
+ if (selection.length > 0) {
242
+ const selected = new THREE.Box3();
243
+ let found = false;
244
+ for (const id of selection) {
245
+ const object = this.authoring?.hierarchy.object3D?.(id);
246
+ if (!object) continue;
247
+ const bounds = contentWorldBounds(object);
248
+ if (bounds.isEmpty()) continue;
249
+ selected.union(bounds);
250
+ found = true;
251
+ }
252
+ if (found) return selected;
253
+ }
254
+ if (this.frameBox && !this.frameBox.isEmpty()) return this.frameBox;
255
+ return contentWorldBounds(this.root);
256
+ }
257
+
258
+ /**
259
+ * Frame the subject: the selection if there is one, else this document's
260
+ * frame box, else the whole root. `fit` scales the fitted distance — 1 is
261
+ * the tight fit the toolbar's Frame button has always used, >1 pulls back.
262
+ */
263
+ frame(fit = 1): boolean {
264
+ const bounds = this.resolveFrameBounds();
265
+ if (bounds.isEmpty()) {
266
+ // A Frame that does nothing must say why — a model document whose
267
+ // mesh measures as nothing is a defect, never a quiet no-op.
268
+ let meshes = 0;
269
+ let positions = 0;
270
+ this.root.traverse((node) => {
271
+ const geometry = (node as THREE.Mesh).geometry as THREE.BufferGeometry | undefined;
272
+ if (!geometry) return;
273
+ meshes += 1;
274
+ positions += geometry.getAttribute('position')?.count ?? 0;
275
+ });
276
+ editorConsole.warn(
277
+ `frame: nothing to frame — root "${this.root.name}" (${this.root.children.length} children, ${meshes} meshes, ${positions} positions), selection ${JSON.stringify(this.authoring?.selection?.get() ?? [])}, frameBox ${this.frameBox ? (this.frameBox.isEmpty() ? 'empty' : 'set') : 'none'}`,
278
+ 'document',
279
+ );
280
+ return false;
281
+ }
282
+ this.settleFlight('superseded');
283
+ const center = bounds.getCenter(new THREE.Vector3());
284
+ const direction = this.viewport.camera.position
285
+ .clone()
286
+ .sub(this.viewport.orbitControls.target)
287
+ .normalize();
288
+ if (direction.lengthSq() === 0) direction.set(0.8, 0.5, -1).normalize();
289
+ const scale = Math.min(10, Math.max(0.1, finiteOr(fit, 1)));
290
+ const distance = perspectiveDistanceToFitBox(bounds, this.viewport.camera, direction) * scale;
291
+ this.viewport.setPose(center.clone().addScaledVector(direction, distance), center);
292
+ return true;
293
+ }
294
+
295
+ /**
296
+ * THE AGENT LOOKING AT THE MODEL, as an act a person can watch.
297
+ *
298
+ * Swing this document's ONE camera — the camera the human is looking
299
+ * through — around the framed subject by `azimuth`/`elevation` radians,
300
+ * animated over `duration` seconds (advanced in {@link renderViewport}, so
301
+ * every intermediate pose is a frame that actually got drawn). The returned
302
+ * promise settles when the move ends, either because it finished or because
303
+ * a human grabbed the view.
304
+ *
305
+ * The pivot and radius are read from the CURRENT pose at launch, so this
306
+ * composes with wherever the view already is instead of homing to a canon.
307
+ * A second flight supersedes the first rather than blending with it.
308
+ *
309
+ * The move is drawn by the document's rAF loop, so a document that is not
310
+ * rendering (a background tab, an inactive dock panel) does not orbit —
311
+ * which is the honest behavior for a verb whose whole point is being seen.
312
+ */
313
+ orbit(options: {
314
+ readonly azimuth?: number;
315
+ readonly elevation?: number;
316
+ readonly duration?: number;
317
+ }): Promise<DocumentLookOutcome> {
318
+ return this.launchFlight(
319
+ finiteOr(options.azimuth, 0),
320
+ finiteOr(options.elevation, 0),
321
+ Math.min(60, Math.max(0, finiteOr(options.duration, 0.6))),
322
+ easeInOutCubic,
323
+ );
324
+ }
325
+
326
+ /**
327
+ * A slow full orbit — {@link orbit} with the revolutions spelled in turns
328
+ * and a CONSTANT angular rate, because a turntable that eases in and out
329
+ * reads as a nervous camera rather than a rotating subject.
330
+ */
331
+ turntable(options: {
332
+ readonly seconds?: number;
333
+ readonly revolutions?: number;
334
+ }): Promise<DocumentLookOutcome> {
335
+ const revolutions = finiteOr(options.revolutions, 1);
336
+ return this.launchFlight(
337
+ revolutions * Math.PI * 2,
338
+ 0,
339
+ Math.min(60, Math.max(0, finiteOr(options.seconds, 6))),
340
+ linear,
341
+ );
342
+ }
343
+
344
+ /** Is a look flight drawing right now? */
345
+ looking(): boolean {
346
+ return this.flight !== null;
347
+ }
348
+
349
+ private launchFlight(
350
+ deltaAzimuth: number,
351
+ deltaElevation: number,
352
+ duration: number,
353
+ ease: (t: number) => number,
354
+ ): Promise<DocumentLookOutcome> {
355
+ this.settleFlight('superseded');
356
+ const camera = this.viewport.camera;
357
+ const pivot = this.viewport.orbitControls.target.clone();
358
+ const spherical = new THREE.Spherical().setFromVector3(camera.position.clone().sub(pivot));
359
+ // A degenerate radius (camera sitting on its own target) has no orbit to
360
+ // run; back off to something the subject's own scale justifies.
361
+ const radius =
362
+ spherical.radius > 1e-4
363
+ ? spherical.radius
364
+ : Math.max(this.resolveFrameBounds().getSize(new THREE.Vector3()).length(), 1);
365
+ const flight: CameraFlight = {
366
+ pivot,
367
+ radius,
368
+ startTheta: spherical.theta,
369
+ startPhi: spherical.phi,
370
+ deltaTheta: deltaAzimuth,
371
+ // Elevation is measured UP from the horizon; the polar angle is measured
372
+ // DOWN from +Y. Raising the camera therefore shrinks phi.
373
+ deltaPhi: -deltaElevation,
374
+ duration,
375
+ ease,
376
+ elapsed: 0,
377
+ theta: spherical.theta,
378
+ phi: spherical.phi,
379
+ settle: null,
380
+ };
381
+ this.flight = flight;
382
+ const settled = new Promise<DocumentLookOutcome>((resolve) => {
383
+ flight.settle = resolve;
384
+ });
385
+ // A zero-length move is still a move: apply it now so the pose is correct
386
+ // even on a surface whose next frame never comes.
387
+ this.applyFlightPose(flight, 0);
388
+ if (duration <= 0) this.settleFlight(null);
389
+ return settled;
390
+ }
391
+
392
+ private applyFlightPose(flight: CameraFlight, progress: number): void {
393
+ const k = flight.ease(progress);
394
+ flight.theta = flight.startTheta + flight.deltaTheta * k;
395
+ flight.phi = Math.min(MAX_POLAR, Math.max(MIN_POLAR, flight.startPhi + flight.deltaPhi * k));
396
+ const camera = this.viewport.camera;
397
+ camera.position
398
+ .setFromSphericalCoords(flight.radius, flight.phi, flight.theta)
399
+ .add(flight.pivot);
400
+ this.viewport.orbitControls.target.copy(flight.pivot);
401
+ camera.lookAt(flight.pivot);
402
+ camera.updateMatrixWorld();
403
+ this.flightPose.copy(camera.position);
404
+ }
405
+
406
+ /**
407
+ * Advance the live flight. Called from {@link renderViewport} — i.e. AFTER
408
+ * `EditorViewport.update()` has run OrbitControls for this frame — so the
409
+ * flight has the final say on the pose that is about to be drawn. (Same
410
+ * ordering rule the viewport's own snap-to-view animation follows.)
411
+ */
412
+ private advanceLook(deltaSeconds: number): void {
413
+ const flight = this.flight;
414
+ if (!flight) return;
415
+ // DID THE POSE I WROTE LAST FRAME SURVIVE? If not, something else owns the
416
+ // camera now, and it wins — yielding here rather than yanking the view
417
+ // back onto the arc is the difference between "the agent stopped" and two
418
+ // writers fighting over one camera.
419
+ //
420
+ // This is the door-independent half of cancellation, and it is not
421
+ // redundant with the OrbitControls `start` listener: three dispatches
422
+ // `start` for mouse, wheel and touch, but NOT for its keyboard pan, and a
423
+ // pointer gesture that cannot take pointer capture (every synthetic one)
424
+ // throws inside `onPointerDown` before reaching the dispatch. The listener
425
+ // names the human case early; this catches every writer that moves the
426
+ // camera without announcing itself.
427
+ const tolerance = Math.max(1e-4, flight.radius * 1e-3);
428
+ if (this.viewport.camera.position.distanceToSquared(this.flightPose) > tolerance * tolerance) {
429
+ this.settleFlight('human');
430
+ return;
431
+ }
432
+ if (deltaSeconds > 0 && Number.isFinite(deltaSeconds)) flight.elapsed += deltaSeconds;
433
+ const progress = flight.duration <= 0 ? 1 : Math.min(1, flight.elapsed / flight.duration);
434
+ this.applyFlightPose(flight, progress);
435
+ if (progress >= 1) this.settleFlight(null);
436
+ }
437
+
438
+ private settleFlight(cancelledBy: 'human' | 'superseded' | 'closed' | null): void {
439
+ const flight = this.flight;
440
+ if (!flight) return;
441
+ this.flight = null;
442
+ const ended = {
443
+ completed: cancelledBy === null,
444
+ azimuth: flight.theta,
445
+ elevation: Math.PI / 2 - flight.phi,
446
+ seconds: Math.min(flight.elapsed, flight.duration),
447
+ };
448
+ flight.settle?.(cancelledBy === null ? ended : { ...ended, cancelledBy });
449
+ }
450
+
451
+ select(ids: readonly string[]): void {
452
+ if (this.authoring) setAuthoringSelection(this.authoring, [...ids]);
453
+ }
454
+
455
+ selection(): string[] {
456
+ return [...(this.authoring?.selection?.get() ?? [])];
457
+ }
458
+
459
+ idForObject(object: THREE.Object3D): string | null {
460
+ return this.authoring?.hierarchy.idForObject3D?.(object) ?? null;
461
+ }
462
+
463
+ /** Keep native hierarchy selection legible in this document's one viewport. */
464
+ syncSelectionPresentation(): void {
465
+ const selected = (this.authoring?.selection?.get() ?? [])
466
+ .map((id) => this.authoring?.hierarchy.object3D?.(id) ?? null)
467
+ .filter((object): object is THREE.Object3D => object !== null);
468
+ this.selectedObjects = selected;
469
+ if (this.selectionOutline) {
470
+ syncThreeSelectionOutline(this.selectionOutline, this.selectionOutlineWanted ? selected : []);
471
+ this.syncComposerOutput();
472
+ }
473
+ const bones = selected.filter((object): object is THREE.Bone =>
474
+ Boolean((object as THREE.Bone).isBone),
475
+ );
476
+ const signature = bones.map((bone) => bone.uuid).join('\u0000');
477
+ if (signature === this.boneSelectionSignature) return;
478
+ this.boneSelectionSignature = signature;
479
+ this.clearBoneSelectionHighlight();
480
+ if (bones.length > 0) {
481
+ this.boneSelectionHighlight = new BoneSelectionHighlight(
482
+ bones,
483
+ nativeSelectionColors(this.renderer.domElement).visible,
484
+ );
485
+ this.scene.add(this.boneSelectionHighlight);
486
+ }
487
+ this.refreshSkeletonHelper();
488
+ }
489
+
490
+ frameSelection(): boolean {
491
+ return this.frameIds(this.authoring?.selection?.get() ?? []);
492
+ }
493
+
494
+ frameIds(ids: readonly string[]): boolean {
495
+ this.settleFlight('superseded');
496
+ const objects = ids
497
+ .map((id) => this.authoring?.hierarchy.object3D?.(id) ?? null)
498
+ .filter((object): object is THREE.Object3D => object !== null);
499
+ if (objects.length === 0) return false;
500
+ if (objects.every((object) => (object as THREE.Bone).isBone)) {
501
+ this.frameBones(objects as THREE.Bone[]);
502
+ return true;
503
+ }
504
+ this.viewport.focusOnMultiple(objects);
505
+ return true;
506
+ }
507
+
508
+ /**
509
+ * An AXIS view is ORTHOGRAPHIC, the way Blender's numpad 1/3/7 are: the
510
+ * reference frame for numpad 1 (`modeling-front-ortho.png`) says so in its
511
+ * own view text, "Front Orthographic". Ours left the stage in perspective,
512
+ * so a front view still showed the cube's side faces converging.
513
+ *
514
+ * `isometric` is the perspective preset (it is the stage's 3/4 opening
515
+ * direction, not an axis) and stays perspective. Both callers of this door
516
+ * already separate the two: the relay's `view-preset` maps its own
517
+ * `perspective` onto `isometric`, and the document toolbar's view menu
518
+ * carries the projection pair as its own second group — so nothing here
519
+ * reads an axis preset expecting perspective.
520
+ *
521
+ * The pose is still solved against the PERSPECTIVE camera, because the
522
+ * session's orthographic camera is derived from that pose every frame
523
+ * (`syncOrthographicCamera`) rather than posed independently.
524
+ */
525
+ setViewPreset(preset: ModelCameraPreset): void {
526
+ this.settleFlight('superseded');
527
+ const bounds = this.resolveFrameBounds();
528
+ if (bounds.isEmpty()) return;
529
+ const center = bounds.getCenter(new THREE.Vector3());
530
+ const direction = cameraPresetDirection(preset);
531
+ const distance = perspectiveDistanceToFitBox(bounds, this.viewport.camera, direction);
532
+ const position = center.clone().addScaledVector(direction, distance);
533
+ this.viewport.camera.up.set(0, preset === 'top' ? 0 : 1, preset === 'top' ? -1 : 0);
534
+ this.viewport.setPose(position, center);
535
+ this.setProjection(preset === 'isometric' ? 'perspective' : 'orthographic');
536
+ }
537
+
538
+ setCameraPose(
539
+ position: { x: number; y: number; z: number },
540
+ target: { x: number; y: number; z: number },
541
+ fov?: number,
542
+ ): void {
543
+ this.settleFlight('superseded');
544
+ this.viewport.setPose(position, target, fov);
545
+ }
546
+
547
+ cameraPose(): {
548
+ readonly position: [number, number, number];
549
+ readonly target: [number, number, number];
550
+ readonly fov: number;
551
+ } {
552
+ return {
553
+ position: this.viewport.camera.position.toArray(),
554
+ target: this.viewport.orbitControls.target.toArray(),
555
+ fov: this.viewport.camera.fov,
556
+ };
557
+ }
558
+
559
+ camera(): THREE.Camera {
560
+ if (this.cameraOverride) return this.cameraOverride;
561
+ if (this.state.projection === 'perspective') return this.viewport.camera;
562
+ this.syncOrthographicCamera();
563
+ return this.orthographicCamera;
564
+ }
565
+
566
+ /** Which projection the session is drawing with. Public because a caller
567
+ * that switches it for ONE frame — the Blender render capture — has to put
568
+ * it back, and cannot read it otherwise. */
569
+ projection(): 'perspective' | 'orthographic' {
570
+ return this.state.projection;
571
+ }
572
+
573
+ setProjection(projection: 'perspective' | 'orthographic'): void {
574
+ if (this.state.projection === projection) return;
575
+ this.state = { ...this.state, projection };
576
+ this.notify();
577
+ }
578
+
579
+ lightingAvailable(): boolean {
580
+ return this.editorLights.length > 0;
581
+ }
582
+
583
+ setLighting(lighting: 'studio' | 'outdoor' | 'flat'): void {
584
+ if (this.state.lighting === lighting) return;
585
+ this.setLightingValues(lighting);
586
+ this.state = { ...this.state, lighting };
587
+ this.notify();
588
+ }
589
+
590
+ setMode(mode: Object3DDocumentViewMode): void {
591
+ if (this.state.mode === mode) return;
592
+ this.clearDiagnosticPresentation();
593
+ this.state = { ...this.state, mode };
594
+ if (mode === 'uv') {
595
+ this.scene.overrideMaterial = new THREE.ShaderMaterial({
596
+ vertexShader:
597
+ 'varying vec2 vUv; void main(){ vUv=uv; gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0); }',
598
+ fragmentShader: 'varying vec2 vUv; void main(){ gl_FragColor=vec4(vUv,0.0,1.0); }',
599
+ });
600
+ } else if (mode === 'vertex-colors') {
601
+ this.scene.overrideMaterial = new THREE.MeshBasicMaterial({ vertexColors: true });
602
+ } else if (mode === 'wireframe') {
603
+ this.buildTopologyOverlay();
604
+ }
605
+ this.notify();
606
+ }
607
+
608
+ /**
609
+ * WIREFRAME ON THE MODELING SURFACE IS TOPOLOGY, NOT TRIANGLES.
610
+ *
611
+ * The shared `wireframe` shading mode flips every material's
612
+ * `wireframe` flag, which draws each RENDERABLE triangle — so a quad shows
613
+ * its triangulation diagonal and a modeller reads noise where they expect
614
+ * face flow. On the Asset Lab document viewport, where the mesh IS the
615
+ * subject, the mode instead draws neutral clay bodies (so wires behind the
616
+ * form are correctly occluded) under an edge overlay built with
617
+ * `EdgesGeometry`: coplanar diagonals fall out, quads read as quads.
618
+ *
619
+ * The overlay is editor chrome — tagged `editorHelper`, so picking, bakes
620
+ * and the shading swap itself all skip it, and gizmos/outlines are
621
+ * untouched. One honest limit: a line set follows its mesh's NODE transform,
622
+ * not its skin, so a skinned mesh shows rest-pose edges while a clip plays.
623
+ * This surface holds content time still by policy, so that is the rare case.
624
+ */
625
+ private buildTopologyOverlay(): void {
626
+ const group = new THREE.Group();
627
+ const followers: Array<{ line: THREE.LineSegments; source: THREE.Object3D }> = [];
628
+ this.root.updateMatrixWorld(true);
629
+ this.root.traverse((object) => {
630
+ const mesh = object as THREE.Mesh;
631
+ if (!mesh.isMesh || !mesh.geometry) return;
632
+ const line = new THREE.LineSegments(
633
+ new THREE.EdgesGeometry(mesh.geometry, TOPOLOGY_EDGE_THRESHOLD_DEGREES),
634
+ // Dark on the light clay body, and DEPTH-TEST OFF. An edge sits
635
+ // exactly on the surface it bounds, so a depth-tested line stipples
636
+ // itself away in z-fighting — photographed and read before this was
637
+ // written. Off is also the truer read: Blender's wireframe shading
638
+ // shows the far side too, and the far side is half the topology.
639
+ new THREE.LineBasicMaterial({
640
+ color: 0x11161d,
641
+ toneMapped: false,
642
+ transparent: true,
643
+ opacity: 0.85,
644
+ depthTest: false,
645
+ depthWrite: false,
646
+ }),
647
+ );
648
+ line.matrixAutoUpdate = false;
649
+ line.matrixWorldAutoUpdate = false;
650
+ line.matrixWorld.copy(mesh.matrixWorld);
651
+ line.frustumCulled = false;
652
+ line.renderOrder = 2;
653
+ group.add(line);
654
+ followers.push({ line, source: mesh });
655
+ });
656
+ if (followers.length === 0) return;
657
+ this.topologyOverlay = group;
658
+ this.topologyFollowers = followers;
659
+ this.addHelper(group, 'topology');
660
+ }
661
+
662
+ private syncTopologyOverlay(): void {
663
+ // A live module document keeps this session and SWAPS the built child on
664
+ // every save, which detaches the meshes these lines shadow. Rebuild from
665
+ // the current graph rather than drawing the previous revision's edges.
666
+ if (this.topologyFollowers.some(({ source }) => source.parent === null)) {
667
+ this.clearTopologyOverlay();
668
+ this.buildTopologyOverlay();
669
+ }
670
+ for (const { line, source } of this.topologyFollowers) {
671
+ line.matrixWorld.copy(source.matrixWorld);
672
+ line.visible = source.visible;
673
+ }
674
+ }
675
+
676
+ private clearTopologyOverlay(): void {
677
+ for (const { line } of this.topologyFollowers.splice(0)) {
678
+ line.geometry.dispose();
679
+ (line.material as THREE.Material).dispose();
680
+ }
681
+ const group = this.topologyOverlay;
682
+ this.topologyOverlay = null;
683
+ if (!group) return;
684
+ group.removeFromParent();
685
+ const index = this.helpers.indexOf(group);
686
+ if (index >= 0) this.helpers.splice(index, 1);
687
+ }
688
+
689
+ setBounds(bounds: boolean): void {
690
+ if (this.state.bounds === bounds) return;
691
+ this.state = { ...this.state, bounds };
692
+ if (bounds) {
693
+ this.boundsHelper = new THREE.BoxHelper(this.root, 0xffb454);
694
+ this.addHelper(this.boundsHelper, 'bounds');
695
+ } else {
696
+ this.clearBoundsHelper();
697
+ }
698
+ this.notify();
699
+ }
700
+
701
+ setSkeleton(skeleton: boolean): void {
702
+ if (this.state.skeleton === skeleton) return;
703
+ this.state = { ...this.state, skeleton };
704
+ this.refreshSkeletonHelper();
705
+ this.notify();
706
+ }
707
+
708
+ setGrid(grid: boolean): void {
709
+ if (this.state.grid === grid) return;
710
+ this.viewport.grid.visible = grid;
711
+ this.state = { ...this.state, grid };
712
+ this.notify();
713
+ }
714
+
715
+ /** The neutral backdrop as installed — what a re-tint replaces. */
716
+ neutralBackgroundTexture(): THREE.Color | THREE.Texture | null {
717
+ return this.neutralBackground;
718
+ }
719
+
720
+ /** Re-tint the neutral backdrop (a palette switch re-derives the dressing's
721
+ * gradient, `standard-viewport-dressing.ts`). Repaints when it is showing. */
722
+ setNeutralBackground(background: THREE.Color | THREE.Texture | null): void {
723
+ const showing = this.scene.background === this.neutralBackground;
724
+ this.neutralBackground = background;
725
+ if (showing) this.scene.background = background;
726
+ }
727
+
728
+ setBackground(background: 'neutral' | 'transparent'): void {
729
+ if (this.state.background === background) return;
730
+ this.scene.background = background === 'transparent' ? null : this.neutralBackground;
731
+ this.state = { ...this.state, background };
732
+ this.notify();
733
+ }
734
+
735
+ setExposure(exposure: number): void {
736
+ if (!Number.isFinite(exposure) || exposure < 0.2 || exposure > 2) return;
737
+ if (this.state.exposure === exposure) return;
738
+ this.renderer.toneMappingExposure = exposure;
739
+ this.state = { ...this.state, exposure };
740
+ this.notify();
741
+ }
742
+
743
+ resetPresentation(): void {
744
+ this.clearDiagnosticPresentation();
745
+ this.state = INITIAL_PRESENTATION;
746
+ this.refreshSkeletonHelper();
747
+ this.clearBoundsHelper();
748
+ this.viewport.grid.visible = true;
749
+ this.scene.background = this.neutralBackground;
750
+ this.renderer.toneMappingExposure = 1;
751
+ this.setLightingValues('studio');
752
+ this.viewport.camera.up.set(0, 1, 0);
753
+ this.frame();
754
+ this.notify();
755
+ }
756
+
757
+ /**
758
+ * The step the document host runs before EVERY draw of the rendered scene.
759
+ *
760
+ * The host keeps the document's own `THREE.Scene` nested inside the session's
761
+ * rendered scene and mirrors the world dressing (environment, fog, their
762
+ * intensities/rotations, background) outward. That mirror used to live in the
763
+ * host's per-frame `animate`, so an offscreen photograph taken between two
764
+ * viewport ticks drew whatever the LAST tick happened to mirror — a sky set
765
+ * synchronously before the capture was simply missing from it. Registering it
766
+ * here puts `renderViewport` and `captureImage` on one path.
767
+ *
768
+ * The host owns the step and clears it in its own teardown; the session only
769
+ * holds the reference.
770
+ */
771
+ setBeforeRender(step: (() => void) | null): void {
772
+ this.beforeRender = step;
773
+ }
774
+
775
+ /**
776
+ * THE PRESENTED FRAME — the stage exactly as the person is seeing it,
777
+ * including everything drawn OVER the document's own render: the
778
+ * orientation compass (`EditorViewport.renderViewCube`) above all.
779
+ *
780
+ * Why this is not {@link captureImage}: that door is an offscreen
781
+ * re-render of the document's content, for thumbnails and asset previews,
782
+ * and it deliberately draws the subject alone. The chrome door's promise is
783
+ * the opposite one — "photograph the editor as the person sees it" — and
784
+ * routing it through `captureImage` made the door LIE about this stage: the
785
+ * compass renders every frame into the default framebuffer (measured: the
786
+ * call is reached, `_threeSurfaceShowing` true, 24 objects, target=screen)
787
+ * and never appeared in a single chrome frame.
788
+ *
789
+ * The canvas has no `preserveDrawingBuffer`, so the only place its pixels
790
+ * can be read is inside the frame that drew them — which is why the host
791
+ * serves the request at the end of its own `animate`
792
+ * ({@link servePresentedFrame}), the same shape the ingest lane's
793
+ * `LiveSession.snapshotFrame` uses.
794
+ */
795
+ private presentedFrameWaiters: Array<(frame: HTMLCanvasElement | null) => void> = [];
796
+ /** True while a host render loop is servicing {@link servePresentedFrame}. */
797
+ private presentsFrames = false;
798
+
799
+ /** The host declares that its loop serves presented-frame requests. Without
800
+ * it a request answers `null` at once rather than waiting for a frame that
801
+ * is never coming (a document whose loop has stopped). */
802
+ setPresentsFrames(on: boolean): void {
803
+ this.presentsFrames = on;
804
+ if (!on) this.resolvePresentedFrame(null);
805
+ }
806
+
807
+ /** Ask for the next drawn frame. `null` when no loop is serving them, and
808
+ * `null` again when a loop that says it serves them does not draw within
809
+ * {@link PRESENTED_FRAME_WAIT_MS} — a hidden tab's rAF is throttled to a
810
+ * stop, and a photograph door may never hang on another loop's liveness. */
811
+ requestPresentedFrame(): Promise<HTMLCanvasElement | null> {
812
+ if (!this.presentsFrames) return Promise.resolve(null);
813
+ return new Promise((resolve) => {
814
+ let settled = false;
815
+ const once = (frame: HTMLCanvasElement | null): void => {
816
+ if (settled) return;
817
+ settled = true;
818
+ resolve(frame);
819
+ };
820
+ this.presentedFrameWaiters.push(once);
821
+ setTimeout(() => once(null), PRESENTED_FRAME_WAIT_MS);
822
+ });
823
+ }
824
+
825
+ /** Called by the host at the END of a frame, after every overlay pass. */
826
+ servePresentedFrame(): void {
827
+ if (this.presentedFrameWaiters.length === 0) return;
828
+ const source = this.renderer.domElement;
829
+ const copy = document.createElement('canvas');
830
+ copy.width = Math.max(1, source.width);
831
+ copy.height = Math.max(1, source.height);
832
+ const context = copy.getContext('2d');
833
+ if (!context) {
834
+ this.resolvePresentedFrame(null);
835
+ return;
836
+ }
837
+ context.drawImage(source, 0, 0);
838
+ this.resolvePresentedFrame(copy);
839
+ }
840
+
841
+ private resolvePresentedFrame(frame: HTMLCanvasElement | null): void {
842
+ const waiters = this.presentedFrameWaiters;
843
+ this.presentedFrameWaiters = [];
844
+ for (const resolve of waiters) resolve(frame);
845
+ }
846
+
847
+ render(renderSolid: (camera: THREE.Camera) => void): void {
848
+ this.beforeRender?.();
849
+ this.boneSelectionHighlight?.update();
850
+ const camera = this.camera();
851
+ const mode = this.state.mode;
852
+ this.boundsHelper?.update();
853
+ if (mode === 'uv' || mode === 'vertex-colors') {
854
+ renderSolid(camera);
855
+ return;
856
+ }
857
+ if (mode === 'wireframe' && this.topologyOverlay) {
858
+ this.syncTopologyOverlay();
859
+ // Clay bodies, not the triangle-wireframe swap: the overlay IS the wire,
860
+ // and the solid form behind it is what occludes the far side.
861
+ this.shading.render(
862
+ this.scene,
863
+ 'clay',
864
+ () => renderSolid(camera),
865
+ isEditorViewportShadingTarget,
866
+ );
867
+ return;
868
+ }
869
+ this.shading.render(this.scene, mode, () => renderSolid(camera), isEditorViewportShadingTarget);
870
+ }
871
+
872
+ /** Visible authoring draw with the editor-only native selection silhouette. */
873
+ renderViewport(deltaSeconds = 0): void {
874
+ this.advanceLook(deltaSeconds);
875
+ this.ensureComposer();
876
+ this.render((camera) => {
877
+ const composer = this.composer;
878
+ if (!composer) {
879
+ // The composer's library is still in flight (see `ensureComposer`).
880
+ // Draw straight through — the same call the offscreen capture makes —
881
+ // so the opening frames show the asset without its selection
882
+ // silhouette rather than nothing at all.
883
+ this.renderer.render(this.scene, camera);
884
+ return;
885
+ }
886
+ composer.setMainScene(this.scene);
887
+ composer.setMainCamera(camera);
888
+ composer.render(deltaSeconds);
889
+ });
890
+ }
891
+
892
+ resize(width: number, height: number): void {
893
+ this.renderWidth = Math.max(1, width);
894
+ this.renderHeight = Math.max(1, height);
895
+ this.composer?.setSize(this.renderWidth, this.renderHeight);
896
+ }
897
+
898
+ private renderCapture(renderSolid: (camera: THREE.Camera) => void, transparent: boolean): void {
899
+ this.render((camera) => {
900
+ if (!transparent) {
901
+ renderSolid(camera);
902
+ return;
903
+ }
904
+ // Apply after the host mirrors its world into this scene. Environment
905
+ // lighting stays intact; only the camera background is omitted.
906
+ const background = this.scene.background;
907
+ const clearColor = this.renderer.getClearColor(new THREE.Color());
908
+ const clearAlpha = this.renderer.getClearAlpha();
909
+ try {
910
+ this.scene.background = null;
911
+ this.renderer.setClearColor(0, 0);
912
+ renderSolid(camera);
913
+ } finally {
914
+ this.scene.background = background;
915
+ this.renderer.setClearColor(clearColor, clearAlpha);
916
+ }
917
+ });
918
+ }
919
+
920
+ /**
921
+ * Fresh offscreen capture through this document's own renderer and
922
+ * diagnostic pipeline. This does not depend on preserveDrawingBuffer and
923
+ * therefore remains truthful after the visible canvas has presented.
924
+ *
925
+ * A NUMBER is a square of that size — the default shape, and the right one
926
+ * for an unstaged look at a model. `{width, height}` renders the buffer AND
927
+ * the camera at that aspect, so a video-shaped look comes back already
928
+ * shaped instead of square-and-cropped. Whatever the shape, the camera is
929
+ * matched to the BUFFER for the duration: rendering the panel's aspect
930
+ * through a square buffer is what photographed a cube as a tall thin prism
931
+ * on a 3.05:1 document panel, through the one look instrument an agent has.
932
+ * The visible viewport is never touched.
933
+ */
934
+ captureImage(
935
+ size:
936
+ | number
937
+ | {
938
+ width: number;
939
+ height: number;
940
+ transparent?: boolean;
941
+ /** Photograph through this caller-owned camera without changing its projection. */
942
+ camera?: THREE.Camera;
943
+ } = 512,
944
+ ): string | null {
945
+ const requestedWidth = typeof size === 'number' ? size : size.width;
946
+ const requestedHeight = typeof size === 'number' ? size : size.height;
947
+ if (!Number.isFinite(requestedWidth) || requestedWidth < 1) return null;
948
+ if (!Number.isFinite(requestedHeight) || requestedHeight < 1) return null;
949
+ const outputWidth = Math.min(2048, Math.round(requestedWidth));
950
+ const outputHeight = Math.min(2048, Math.round(requestedHeight));
951
+ const renderWidth = outputWidth * 2;
952
+ const renderHeight = outputHeight * 2;
953
+ // Three disables material tone mapping on ordinary render targets. A
954
+ // byte target clips lit surfaces to white before display conversion can
955
+ // recover them. Keep HDR values, then use the same output resolve as the
956
+ // scene viewport so exposure, tone mapping and color space affect captures.
957
+ const sceneTarget = new THREE.WebGLRenderTarget(renderWidth, renderHeight, {
958
+ type: THREE.HalfFloatType,
959
+ });
960
+ const target = new THREE.WebGLRenderTarget(renderWidth, renderHeight);
961
+ const previousTarget = this.renderer.getRenderTarget();
962
+ const previousPixelRatio = this.renderer.getPixelRatio();
963
+ const perspective = this.viewport.camera;
964
+ const previousAspect = perspective.aspect;
965
+ try {
966
+ this.captureAspect = outputWidth / outputHeight;
967
+ const override = typeof size === 'number' ? undefined : size.camera;
968
+ if (override) this.cameraOverride = override;
969
+ else {
970
+ perspective.aspect = outputWidth / outputHeight;
971
+ perspective.updateProjectionMatrix();
972
+ }
973
+ this.renderer.setPixelRatio(1);
974
+ this.renderer.setRenderTarget(sceneTarget);
975
+ this.renderCapture(
976
+ (camera) => this.renderSolidForCapture(camera, sceneTarget, renderWidth, renderHeight),
977
+ typeof size !== 'number' && size.transparent === true,
978
+ );
979
+ viewportCaptureOutputPass(typeof size !== 'number' && size.transparent === true).render(
980
+ this.renderer,
981
+ target,
982
+ sceneTarget,
983
+ 0,
984
+ false,
985
+ );
986
+
987
+ const rowBytes = renderWidth * 4;
988
+ const pixels = new Uint8Array(rowBytes * renderHeight);
989
+ this.renderer.readRenderTargetPixels(target, 0, 0, renderWidth, renderHeight, pixels);
990
+ const fullCanvas = document.createElement('canvas');
991
+ fullCanvas.width = renderWidth;
992
+ fullCanvas.height = renderHeight;
993
+ const fullContext = fullCanvas.getContext('2d');
994
+ if (!fullContext) return null;
995
+ const imageData = fullContext.createImageData(renderWidth, renderHeight);
996
+ for (let y = 0; y < renderHeight; y++) {
997
+ const sourceRow = (renderHeight - 1 - y) * rowBytes;
998
+ const destinationRow = y * rowBytes;
999
+ imageData.data.set(pixels.subarray(sourceRow, sourceRow + rowBytes), destinationRow);
1000
+ }
1001
+ fullContext.putImageData(imageData, 0, 0);
1002
+ const output = document.createElement('canvas');
1003
+ output.width = outputWidth;
1004
+ output.height = outputHeight;
1005
+ const outputContext = output.getContext('2d');
1006
+ if (!outputContext) return null;
1007
+ outputContext.drawImage(fullCanvas, 0, 0, outputWidth, outputHeight);
1008
+ return output.toDataURL('image/png');
1009
+ } finally {
1010
+ this.captureAspect = null;
1011
+ this.cameraOverride = null;
1012
+ perspective.aspect = previousAspect;
1013
+ perspective.updateProjectionMatrix();
1014
+ this.renderer.setRenderTarget(previousTarget);
1015
+ this.renderer.setPixelRatio(previousPixelRatio);
1016
+ target.dispose();
1017
+ sceneTarget.dispose();
1018
+ }
1019
+ }
1020
+
1021
+ dispose(): void {
1022
+ this.disposed = true;
1023
+ this.settleFlight('closed');
1024
+ this.viewport.orbitControls.removeEventListener('start', this.cancelLookForHuman);
1025
+ this.unsubscribeSelectionTheme();
1026
+ this.clearDiagnosticPresentation();
1027
+ this.clearBoneSelectionHighlight();
1028
+ this.clearBoundsHelper();
1029
+ this.clearSkeletonHelper();
1030
+ // OutlineEffect's Selection owns temporary render-layer bits on every
1031
+ // target, so clear it before disposing the pass resources.
1032
+ this.selectionOutline?.selection.clear();
1033
+ this.composer?.dispose();
1034
+ this.sceneRenderPass = null;
1035
+ this.selectionOutline = null;
1036
+ this.selectionOutlinePass = null;
1037
+ this.composer = null;
1038
+ this.shading.dispose();
1039
+ this.listeners.clear();
1040
+ }
1041
+
1042
+ private addHelper(helper: THREE.Object3D, kind: string): void {
1043
+ helper.name = `__object3d_document_${kind}`;
1044
+ setUserData(helper, 'editorHelper', true);
1045
+ this.scene.add(helper);
1046
+ this.helpers.push(helper);
1047
+ }
1048
+
1049
+ /**
1050
+ * THE LAZY DOOR for `postprocessing` (~200 kB of passes and shaders).
1051
+ *
1052
+ * The composer exists for ONE feature — the editor-owned selection
1053
+ * silhouette; `syncComposerOutput` hands the screen back to the plain scene
1054
+ * pass whenever nothing is outlined — so the library belongs with the first
1055
+ * frame of this document, not with the module that draws it. Everything an
1056
+ * Asset Lab document does before that first frame (mount, adopt, frame the
1057
+ * content) is untouched by it.
1058
+ *
1059
+ * Called from every `renderViewport`, so it must be cheap and re-entrant:
1060
+ * `composerLoading` keeps exactly one import in flight, and the continuation
1061
+ * re-checks `disposed` because a document can close mid-load.
1062
+ */
1063
+ /**
1064
+ * WHETHER THIS DOCUMENT DRAWS THE SELECTION SILHOUETTE AT ALL — a LIVE
1065
+ * switch, not just a construction-time one.
1066
+ *
1067
+ * Two callers turn it off, for different reasons. The inspector's preview
1068
+ * lane shows a subject with nothing selectable inside it, so it never needs
1069
+ * the pass — or the effect it would build per selection — and says so once.
1070
+ * A modeling document says so whenever it is in EDIT mode: Blender draws no
1071
+ * object outline there (`modeling-edit-all.png`), and the silhouette
1072
+ * otherwise follows the hierarchy selection straight through the mode
1073
+ * change. That second caller flips it while the composer is already built,
1074
+ * so turning it off must drop the outline the composer is currently drawing
1075
+ * — gating construction alone leaves the frame exactly as it was.
1076
+ */
1077
+ get selectionOutlineEnabled(): boolean {
1078
+ return this.selectionOutlineWanted;
1079
+ }
1080
+ set selectionOutlineEnabled(value: boolean) {
1081
+ if (value === this.selectionOutlineWanted) return;
1082
+ this.selectionOutlineWanted = value;
1083
+ if (value) this.syncSelectionPresentation();
1084
+ else {
1085
+ this.selectionOutline?.selection.clear();
1086
+ this.syncComposerOutput();
1087
+ }
1088
+ }
1089
+ private selectionOutlineWanted = true;
1090
+
1091
+ private ensureComposer(): void {
1092
+ if (!this.selectionOutlineWanted) return;
1093
+ if (this.composer || this.composerLoading || this.disposed) return;
1094
+ this.composerLoading = true;
1095
+ void import('postprocessing')
1096
+ .then((postprocessing) => {
1097
+ this.composerLoading = false;
1098
+ if (this.disposed || this.composer) return;
1099
+ const camera = this.camera();
1100
+ const composer = new postprocessing.EffectComposer(this.renderer);
1101
+ this.sceneRenderPass = new postprocessing.RenderPass(this.scene, camera);
1102
+ composer.addPass(this.sceneRenderPass);
1103
+ this.selectionOutline = createThreeSelectionOutline(
1104
+ postprocessing,
1105
+ this.scene,
1106
+ camera,
1107
+ nativeSelectionColors(this.renderer.domElement),
1108
+ );
1109
+ this.syncSelectionTheme();
1110
+ syncThreeSelectionOutline(this.selectionOutline, this.selectedObjects);
1111
+ this.selectionOutlinePass = new postprocessing.EffectPass(camera, this.selectionOutline);
1112
+ composer.addPass(this.selectionOutlinePass);
1113
+ this.composer = composer;
1114
+ this.syncComposerOutput();
1115
+ composer.setSize(this.renderWidth, this.renderHeight);
1116
+ })
1117
+ .catch(() => {
1118
+ // A failed chunk load must not wedge the surface: the plain draw path
1119
+ // in `renderViewport` keeps the document visible, and the next frame
1120
+ // retries the import.
1121
+ this.composerLoading = false;
1122
+ });
1123
+ }
1124
+
1125
+ /**
1126
+ * EffectComposer marks the last added pass as the screen output. Disabling
1127
+ * that pass without transferring screen ownership to the scene pass leaves
1128
+ * the unoutlined frame stranded in the composer's offscreen buffer. Keep
1129
+ * selection responsible only for the outline, never for whether the asset
1130
+ * itself reaches the canvas.
1131
+ */
1132
+ /**
1133
+ * Draw the scene FOR A PHOTOGRAPH into `target` the way the screen sees it:
1134
+ * through the outline pass when a selection silhouette is on (the outline is
1135
+ * a pass, absent from a plain draw — a capture without it lied about the
1136
+ * screen), the plain draw otherwise.
1137
+ *
1138
+ * The two passes are driven DIRECTLY rather than through `composer.render()`,
1139
+ * because neither of the composer's own doors says where a frame landed:
1140
+ * `EffectComposer.render` ping-pongs LOCAL variables and never reassigns
1141
+ * `inputBuffer`/`outputBuffer`, and `CopyPass.render` ignores its
1142
+ * `outputBuffer` argument entirely (it always writes to its own render
1143
+ * target, 1x1 unless the composer sized it). Going through them wrote the
1144
+ * photograph nowhere — every capture of a selected object came back empty.
1145
+ * A pass's own contract is exact: `RenderPass` draws the scene into the
1146
+ * `inputBuffer` it is handed, `EffectPass` reads that and writes the
1147
+ * `outputBuffer` it is handed — so hand the outline pass the capture target.
1148
+ */
1149
+ private renderSolidForCapture(
1150
+ camera: THREE.Camera,
1151
+ target: THREE.WebGLRenderTarget,
1152
+ width: number,
1153
+ height: number,
1154
+ ): void {
1155
+ const composer = this.composer;
1156
+ const outlined = (this.selectionOutline?.selection.size ?? 0) > 0;
1157
+ if (!composer || !outlined || !this.sceneRenderPass || !this.selectionOutlinePass) {
1158
+ this.renderer.render(this.scene, camera);
1159
+ return;
1160
+ }
1161
+ const previousWidth = this.renderWidth;
1162
+ const previousHeight = this.renderHeight;
1163
+ try {
1164
+ this.sceneRenderPass.renderToScreen = false;
1165
+ this.selectionOutlinePass.renderToScreen = false;
1166
+ composer.setSize(width, height, false);
1167
+ this.sceneRenderPass.render(this.renderer, composer.inputBuffer, composer.outputBuffer, 0);
1168
+ this.selectionOutlinePass.render(this.renderer, composer.inputBuffer, target, 0);
1169
+ } finally {
1170
+ composer.setSize(previousWidth, previousHeight, false);
1171
+ this.syncComposerOutput();
1172
+ this.renderer.setRenderTarget(target);
1173
+ }
1174
+ }
1175
+
1176
+ private syncComposerOutput(): void {
1177
+ const outlined = (this.selectionOutline?.selection.size ?? 0) > 0;
1178
+ if (this.sceneRenderPass) this.sceneRenderPass.renderToScreen = !outlined;
1179
+ if (this.selectionOutlinePass) {
1180
+ this.selectionOutlinePass.enabled = outlined;
1181
+ this.selectionOutlinePass.renderToScreen = outlined;
1182
+ }
1183
+ }
1184
+
1185
+ private refreshSkeletonHelper(): void {
1186
+ const visible = this.state.skeleton || this.boneSelectionHighlight !== null;
1187
+ if (visible && !this.skeletonHelper) {
1188
+ this.skeletonHelper = new THREE.SkeletonHelper(this.root);
1189
+ styleEditorSkeletonHelper(this.skeletonHelper);
1190
+ this.scene.add(this.skeletonHelper);
1191
+ } else if (!visible) {
1192
+ this.clearSkeletonHelper();
1193
+ }
1194
+ }
1195
+
1196
+ private clearSkeletonHelper(): void {
1197
+ if (!this.skeletonHelper) return;
1198
+ this.skeletonHelper.removeFromParent();
1199
+ this.skeletonHelper.dispose();
1200
+ this.skeletonHelper = null;
1201
+ }
1202
+
1203
+ private clearBoneSelectionHighlight(): void {
1204
+ if (!this.boneSelectionHighlight) return;
1205
+ this.boneSelectionHighlight.removeFromParent();
1206
+ this.boneSelectionHighlight.dispose();
1207
+ this.boneSelectionHighlight = null;
1208
+ }
1209
+
1210
+ private clearBoundsHelper(): void {
1211
+ if (!this.boundsHelper) return;
1212
+ const helper = this.boundsHelper;
1213
+ this.boundsHelper = null;
1214
+ const index = this.helpers.indexOf(helper);
1215
+ if (index >= 0) this.helpers.splice(index, 1);
1216
+ helper.removeFromParent();
1217
+ helper.geometry.dispose();
1218
+ if (Array.isArray(helper.material))
1219
+ helper.material.forEach((material) => {
1220
+ material.dispose();
1221
+ });
1222
+ else helper.material.dispose();
1223
+ }
1224
+
1225
+ private frameBones(bones: readonly THREE.Bone[]): void {
1226
+ const bounds = new THREE.Box3();
1227
+ const point = new THREE.Vector3();
1228
+ for (const bone of bones) {
1229
+ bounds.expandByPoint(bone.getWorldPosition(point));
1230
+ if ((bone.parent as THREE.Bone | null)?.isBone) {
1231
+ bounds.expandByPoint(bone.parent!.getWorldPosition(point));
1232
+ }
1233
+ for (const child of bone.children) {
1234
+ if ((child as THREE.Bone).isBone) bounds.expandByPoint(child.getWorldPosition(point));
1235
+ }
1236
+ }
1237
+ const center = bounds.getCenter(new THREE.Vector3());
1238
+ const minimumSpan = Math.max(bounds.getSize(new THREE.Vector3()).length(), 0.2);
1239
+ bounds.expandByScalar(minimumSpan * 0.35);
1240
+ const direction = this.viewport.camera.position
1241
+ .clone()
1242
+ .sub(this.viewport.orbitControls.target)
1243
+ .normalize();
1244
+ const distance = perspectiveDistanceToFitBox(bounds, this.viewport.camera, direction);
1245
+ this.viewport.setPose(center.clone().addScaledVector(direction, distance), center);
1246
+ }
1247
+
1248
+ private syncOrthographicCamera(): void {
1249
+ const source = this.viewport.camera;
1250
+ const target = this.viewport.orbitControls.target;
1251
+ const distance = Math.max(source.position.distanceTo(target), 0.001);
1252
+ const halfHeight = Math.max(
1253
+ distance * Math.tan(THREE.MathUtils.degToRad(source.fov) / 2),
1254
+ 0.001,
1255
+ );
1256
+ const width = Math.max(this.renderer.domElement.clientWidth, 1);
1257
+ const height = Math.max(this.renderer.domElement.clientHeight, 1);
1258
+ // The offscreen capture renders into its OWN buffer shape — see
1259
+ // `captureImage`; outside one, the panel's box is the aspect.
1260
+ const aspect = this.captureAspect ?? width / height;
1261
+ this.orthographicCamera.left = -halfHeight * aspect;
1262
+ this.orthographicCamera.right = halfHeight * aspect;
1263
+ this.orthographicCamera.top = halfHeight;
1264
+ this.orthographicCamera.bottom = -halfHeight;
1265
+ this.orthographicCamera.near = source.near;
1266
+ this.orthographicCamera.far = source.far;
1267
+ this.orthographicCamera.position.copy(source.position);
1268
+ this.orthographicCamera.quaternion.copy(source.quaternion);
1269
+ this.orthographicCamera.up.copy(source.up);
1270
+ this.orthographicCamera.layers.mask = source.layers.mask;
1271
+ this.orthographicCamera.updateProjectionMatrix();
1272
+ this.orthographicCamera.updateMatrixWorld(true);
1273
+ }
1274
+
1275
+ private setLightingValues(lighting: 'studio' | 'outdoor' | 'flat'): void {
1276
+ for (const light of this.editorLights) {
1277
+ if (light instanceof THREE.HemisphereLight) {
1278
+ light.intensity = lighting === 'flat' ? 2 : lighting === 'outdoor' ? 1.15 : 1.7;
1279
+ } else if (light instanceof THREE.DirectionalLight) {
1280
+ light.intensity = lighting === 'flat' ? 0 : lighting === 'outdoor' ? 2.2 : 2.4;
1281
+ // Every preset is NEUTRAL now, like the dressing's key this retunes
1282
+ // (`standard-viewport-dressing.ts` carries the measurement against
1283
+ // Blender's object-mode cube). Studio was `0xfff3dd`, so picking it in
1284
+ // the preset menu put the warm cast back on a document the dressing
1285
+ // had already lit white — and measured live, this whole function is
1286
+ // dead until someone touches that menu: the initial `lighting:
1287
+ // 'studio'` state never applies its own values, which is why changing
1288
+ // them moved nothing until the dressing changed too.
1289
+ light.color.set(0xffffff);
1290
+ }
1291
+ }
1292
+ }
1293
+
1294
+ private clearDiagnosticPresentation(): void {
1295
+ this.clearTopologyOverlay();
1296
+ this.scene.overrideMaterial?.dispose();
1297
+ this.scene.overrideMaterial = null;
1298
+ for (const helper of this.helpers.splice(0)) {
1299
+ helper.removeFromParent();
1300
+ const line = helper as THREE.Line;
1301
+ line.geometry?.dispose();
1302
+ const material = line.material;
1303
+ if (Array.isArray(material))
1304
+ material.forEach((entry) => {
1305
+ entry.dispose();
1306
+ });
1307
+ else material?.dispose();
1308
+ }
1309
+ }
1310
+
1311
+ private notify(): void {
1312
+ this.version++;
1313
+ for (const listener of this.listeners) listener();
1314
+ }
1315
+ }
1316
+
1317
+ // Compatibility for existing internal callers/tests; lookup-only consumers
1318
+ // import the registry directly so they do not pull this implementation graph.
1319
+ export {
1320
+ __resetObject3DDocumentSessionsForTest,
1321
+ allObject3DDocumentSessions,
1322
+ object3DDocumentSession,
1323
+ object3DDocumentSessionsVersion,
1324
+ registerObject3DDocumentSession,
1325
+ subscribeObject3DDocumentSessions,
1326
+ } from './object3d-document-session-registry';