@volter/editor-game 0.5.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (302) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +23 -0
  3. package/contributions/asset-budget-asset.menu.ts +26 -0
  4. package/contributions/asset-budget.action.ts +18 -0
  5. package/contributions/asset-budget.document.tsx +21 -0
  6. package/contributions/asset-budget.menu.ts +22 -0
  7. package/contributions/audio-unlock.service.ts +17 -0
  8. package/contributions/audio.utility.tsx +15 -0
  9. package/contributions/autoplay.service.ts +48 -0
  10. package/contributions/bridge.command.ts +172 -0
  11. package/contributions/build-profiles.document.tsx +21 -0
  12. package/contributions/build-progress.status.tsx +45 -0
  13. package/contributions/build.action.ts +26 -0
  14. package/contributions/build.command.ts +27 -0
  15. package/contributions/build.header.tsx +37 -0
  16. package/contributions/build.menu.ts +31 -0
  17. package/contributions/build.service.ts +31 -0
  18. package/contributions/connection.status.tsx +43 -0
  19. package/contributions/coverage.service.ts +118 -0
  20. package/contributions/edit-mode-audio.service.ts +23 -0
  21. package/contributions/edit-mode-networking.service.ts +30 -0
  22. package/contributions/game-document.service.ts +25 -0
  23. package/contributions/game-eval.command.ts +210 -0
  24. package/contributions/game.layout.ts +19 -0
  25. package/contributions/gameplay.command.ts +255 -0
  26. package/contributions/generation.service.ts +96 -0
  27. package/contributions/generations.status.tsx +53 -0
  28. package/contributions/ingest.service.ts +80 -0
  29. package/contributions/instances.command.ts +75 -0
  30. package/contributions/navmesh.menu.ts +39 -0
  31. package/contributions/navmesh.service.ts +19 -0
  32. package/contributions/network.utility.tsx +17 -0
  33. package/contributions/play.command.ts +355 -0
  34. package/contributions/profiler.action.ts +16 -0
  35. package/contributions/profiler.menu.ts +22 -0
  36. package/contributions/profiler.utility.tsx +17 -0
  37. package/contributions/react/component-board.service.ts +21 -0
  38. package/contributions/react/design-time-mount.service.ts +60 -0
  39. package/contributions/react/pasteboard.action.ts +41 -0
  40. package/contributions/react/react-inspector.service.ts +85 -0
  41. package/contributions/react/story-documents.service.ts +44 -0
  42. package/contributions/scene-document.service.ts +32 -0
  43. package/contributions/state-watch.action.ts +17 -0
  44. package/contributions/state-watch.menu.ts +23 -0
  45. package/contributions/state-watch.utility.tsx +20 -0
  46. package/contributions/team-playtest.service.ts +124 -0
  47. package/contributions/three/camera-runtime.inspector.tsx +34 -0
  48. package/contributions/three/component-board.service.ts +24 -0
  49. package/contributions/three/component-verbs.command.ts +110 -0
  50. package/contributions/three/component-verbs.service.ts +92 -0
  51. package/contributions/three/constraints.inspector.tsx +34 -0
  52. package/contributions/three/model-asset-sections.service.ts +26 -0
  53. package/contributions/three/reflection-probe-capture.inspector.tsx +32 -0
  54. package/contributions/three/story-documents.service.ts +31 -0
  55. package/contributions/three/three-authoring.service.ts +66 -0
  56. package/contributions/transport.header.tsx +19 -0
  57. package/contributions/xstate-behavior.action.ts +42 -0
  58. package/contributions/xstate-behavior.document.tsx +73 -0
  59. package/contributions/xstate-behavior.inspector.tsx +28 -0
  60. package/contributions/xstate-behavior.menu.ts +23 -0
  61. package/package.json +144 -0
  62. package/src/asset-budget/AssetBudgetPanel.tsx +1172 -0
  63. package/src/asset-budget/asset-budget-model.ts +799 -0
  64. package/src/asset-budget/basis-encoder.ts +165 -0
  65. package/src/asset-budget/gltf-io.ts +154 -0
  66. package/src/asset-budget/gltf-optimize.ts +384 -0
  67. package/src/asset-budget/image-dims.ts +78 -0
  68. package/src/asset-budget/optimize-apply.ts +221 -0
  69. package/src/audio/AudioDebuggerPanel.tsx +457 -0
  70. package/src/audio/audio-debugger-model.ts +87 -0
  71. package/src/bridge/call.ts +60 -0
  72. package/src/bridge/dispatch.ts +459 -0
  73. package/src/bridge/live-frames.ts +25 -0
  74. package/src/bridge/screenshot.ts +316 -0
  75. package/src/build/BuildProfilesPanel.tsx +476 -0
  76. package/src/build/build-session.ts +247 -0
  77. package/src/build/format-bytes.ts +14 -0
  78. package/src/command-results.ts +36 -0
  79. package/src/coverage/live-authoring-surface.ts +30 -0
  80. package/src/coverage/live-project-verbs.ts +162 -0
  81. package/src/coverage/native-system-coverage.ts +143 -0
  82. package/src/coverage/root-coverage.ts +79 -0
  83. package/src/coverage/session-coverage.ts +193 -0
  84. package/src/design-system-stories/ApplicationChrome.stories.tsx +100 -0
  85. package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +406 -0
  86. package/src/edit-mode/edit-mode-audio.ts +56 -0
  87. package/src/edit-mode/edit-mode-networking.ts +112 -0
  88. package/src/game-document/DevicePresetPicker.tsx +84 -0
  89. package/src/game-document/GameCaptureFrameButton.tsx +45 -0
  90. package/src/game-document/GameDocument.tsx +215 -0
  91. package/src/game-document/GamePanel.tsx +662 -0
  92. package/src/game-document/InstanceInspectorPicker.tsx +183 -0
  93. package/src/game-document/crowd-debug.ts +183 -0
  94. package/src/game-document/device-preview.ts +336 -0
  95. package/src/game-document/game-view-store.ts +107 -0
  96. package/src/game-document/physics-debug.ts +187 -0
  97. package/src/generation/GenerationActivity.tsx +421 -0
  98. package/src/generation/GenerationGallery.css +231 -0
  99. package/src/generation/generation-documents.tsx +338 -0
  100. package/src/generation/generation-jobs.ts +128 -0
  101. package/src/generation/generation-presentation.ts +257 -0
  102. package/src/host/adapter-reach.ts +445 -0
  103. package/src/host/adapter-runtime-bindings.ts +303 -0
  104. package/src/host/after-paint.ts +112 -0
  105. package/src/host/api/configurations.ts +66 -0
  106. package/src/host/authoring/babylon-authoring-adapter.ts +703 -0
  107. package/src/host/authoring/canvas-runtime-recognition.ts +50 -0
  108. package/src/host/authoring/contract-hierarchy-authoring.ts +144 -0
  109. package/src/host/authoring/contract-scenes-stories.ts +204 -0
  110. package/src/host/authoring/creation-site-related.ts +55 -0
  111. package/src/host/authoring/ephemeral-persistence.ts +29 -0
  112. package/src/host/authoring/gesture-persist.ts +84 -0
  113. package/src/host/authoring/ingest-data-writer.ts +232 -0
  114. package/src/host/authoring/ingest-source-persistence.ts +826 -0
  115. package/src/host/authoring/mount-isolated-pixi-screen.ts +250 -0
  116. package/src/host/authoring/mounted-authoring.ts +41 -0
  117. package/src/host/authoring/owned-pixi-ticker-listeners.ts +96 -0
  118. package/src/host/authoring/phaser-live-authoring-adapter.ts +265 -0
  119. package/src/host/authoring/pixi-authoring-adapter.ts +1554 -0
  120. package/src/host/authoring/pixi-creation-site-write-target.ts +60 -0
  121. package/src/host/authoring/pixi-isolation-assets.ts +25 -0
  122. package/src/host/authoring/pixi-live-write-target.ts +979 -0
  123. package/src/host/authoring/pixi-source-identity.ts +141 -0
  124. package/src/host/authoring/pixi-still-presentation.ts +71 -0
  125. package/src/host/authoring/pixi-structure-history.ts +237 -0
  126. package/src/host/authoring/pixi-transform-channels.ts +205 -0
  127. package/src/host/authoring/selection-remount-handoff.ts +23 -0
  128. package/src/host/authoring/source-persistence-backend.ts +373 -0
  129. package/src/host/authoring/source-refresh-revisions.ts +81 -0
  130. package/src/host/authoring/struct-write-pipe.ts +143 -0
  131. package/src/host/auto-frame-window.ts +89 -0
  132. package/src/host/binding-resolver.ts +393 -0
  133. package/src/host/browser-transpile.ts +631 -0
  134. package/src/host/canvas-entry-runtime.ts +95 -0
  135. package/src/host/components/CameraAuthoringOverlay.tsx +216 -0
  136. package/src/host/components/HeaderTelemetry.tsx +341 -0
  137. package/src/host/components/PixiIsolationSceneContent.tsx +280 -0
  138. package/src/host/components/ResolutionPicker.tsx +89 -0
  139. package/src/host/components/ThreeIsolationSceneContent.tsx +180 -0
  140. package/src/host/components/frame-debugger-model.ts +579 -0
  141. package/src/host/components/header-telemetry-model.ts +74 -0
  142. package/src/host/components/scene-document.tsx +447 -0
  143. package/src/host/components/utility-view-state.ts +87 -0
  144. package/src/host/components/world-root-stage-binding.tsx +133 -0
  145. package/src/host/components/world-root-stage.ts +1046 -0
  146. package/src/host/coverage/authoring-read-probe.ts +583 -0
  147. package/src/host/coverage/capability-coverage.ts +1587 -0
  148. package/src/host/coverage/coverage-accounting.ts +261 -0
  149. package/src/host/coverage/game-contract-seam-evidence.ts +82 -0
  150. package/src/host/coverage/project-verb-coverage.ts +148 -0
  151. package/src/host/coverage/system-adapter-coverage.ts +373 -0
  152. package/src/host/design-system-stories/StoryLayout.tsx +104 -0
  153. package/src/host/design-system-stories/fixtures/authoring.ts +247 -0
  154. package/src/host/design-system-stories/fixtures/editor-runtime.tsx +115 -0
  155. package/src/host/document-preview-three.ts +110 -0
  156. package/src/host/entry-adjudication.ts +89 -0
  157. package/src/host/game-location-guard.ts +150 -0
  158. package/src/host/game-module-access.ts +196 -0
  159. package/src/host/game-realm-page.ts +358 -0
  160. package/src/host/game-realm-reclaim.ts +55 -0
  161. package/src/host/game-realm-storage.ts +104 -0
  162. package/src/host/gameplay-export.ts +288 -0
  163. package/src/host/gameplay-recording.ts +717 -0
  164. package/src/host/gated-globals.ts +1511 -0
  165. package/src/host/history/json-history-resource.ts +254 -0
  166. package/src/host/ingest/registry.ts +261 -0
  167. package/src/host/instance-extract-actions.ts +120 -0
  168. package/src/host/instance-fork-actions.ts +120 -0
  169. package/src/host/play-control-hook.ts +26 -0
  170. package/src/host/playwright-shim.ts +479 -0
  171. package/src/host/projection/dom.ts +295 -0
  172. package/src/host/projection/pixi.ts +288 -0
  173. package/src/host/r3f-entry-runtime.ts +77 -0
  174. package/src/host/react-mount-runtime.ts +162 -0
  175. package/src/host/realm-services.ts +148 -0
  176. package/src/host/recording-preview.ts +106 -0
  177. package/src/host/roots/module-root.ts +203 -0
  178. package/src/host/roots/react-root.ts +181 -0
  179. package/src/host/same-realm-loop-gate.ts +544 -0
  180. package/src/host/scene-view-drawability.ts +69 -0
  181. package/src/host/sdk/tools.ts +31 -0
  182. package/src/host/served-bundle-runtime-modules.ts +331 -0
  183. package/src/host/server-log-bridge.ts +60 -0
  184. package/src/host/staged-projects.ts +24 -0
  185. package/src/host/stories/mounted-story-viewport-source.ts +64 -0
  186. package/src/host/stories/story-arg-descriptors.ts +50 -0
  187. package/src/host/stories/story-media-presence.ts +152 -0
  188. package/src/host/surface-content.ts +87 -0
  189. package/src/host/take-named-export.ts +23 -0
  190. package/src/host/three-ingest-runtime.ts +76 -0
  191. package/src/host/types-fastnoise-lite.d.ts +7 -0
  192. package/src/host/types-mikktspace.d.ts +20 -0
  193. package/src/host/types-troika-three-text.d.ts +7 -0
  194. package/src/host/use-active-performance-source.ts +53 -0
  195. package/src/host/viewport-pose-memory.ts +48 -0
  196. package/src/host/viewport-root-presentation.ts +40 -0
  197. package/src/ingest/active-ingest.ts +251 -0
  198. package/src/ingest/active-scene-navigation.ts +43 -0
  199. package/src/ingest/authoring/ingest-dom-surface-authoring.ts +191 -0
  200. package/src/ingest/authoring/ingest-root-adapter.ts +897 -0
  201. package/src/ingest/capture-wait-report.ts +122 -0
  202. package/src/ingest/deferred-ingest-play.ts +216 -0
  203. package/src/ingest/deferred-ingest-session.ts +23 -0
  204. package/src/ingest/discovery-public-ingest.ts +242 -0
  205. package/src/ingest/dom-stub-mark.ts +7 -0
  206. package/src/ingest/entry-load.ts +56 -0
  207. package/src/ingest/game-contract-realm.ts +34 -0
  208. package/src/ingest/game-pointer-lock.ts +89 -0
  209. package/src/ingest/held-scene-repaint.ts +73 -0
  210. package/src/ingest/host-surface-box.ts +174 -0
  211. package/src/ingest/ingest-boot-viewport.ts +54 -0
  212. package/src/ingest/ingest-canvas-scene-document.tsx +177 -0
  213. package/src/ingest/ingest-canvas-scene.ts +50 -0
  214. package/src/ingest/ingest-evidence-hook.ts +131 -0
  215. package/src/ingest/ingest-frame-snapshot.ts +183 -0
  216. package/src/ingest/ingest-play-commands.ts +134 -0
  217. package/src/ingest/ingest-play-control.ts +299 -0
  218. package/src/ingest/ingest-render-debug.ts +320 -0
  219. package/src/ingest/ingest-siblings.ts +487 -0
  220. package/src/ingest/ingest-status.ts +62 -0
  221. package/src/ingest/live-ingest-facet.ts +46 -0
  222. package/src/ingest/module-mode.ts +229 -0
  223. package/src/ingest/mount-canvas-ingest-root.ts +899 -0
  224. package/src/ingest/mount-coverage.ts +296 -0
  225. package/src/ingest/mount-dom-ingest-root.ts +282 -0
  226. package/src/ingest/mount-ingest-root.ts +744 -0
  227. package/src/ingest/mount-three-ingest-root.ts +366 -0
  228. package/src/ingest/resolve-canvas.ts +47 -0
  229. package/src/ingest/resolve-three.ts +123 -0
  230. package/src/ingest/served-bundle.ts +109 -0
  231. package/src/ingest/served-html-boot.ts +292 -0
  232. package/src/ingest/surface-canvas.ts +84 -0
  233. package/src/ingest/surface-dom.ts +84 -0
  234. package/src/ingest/surface-three.ts +128 -0
  235. package/src/ingest/types.ts +76 -0
  236. package/src/ingest/unmount-ingest-root.ts +224 -0
  237. package/src/navmesh/navmesh-actions.ts +27 -0
  238. package/src/navmesh/navmesh-handler.ts +237 -0
  239. package/src/navmesh/navmesh-workflow-store.ts +78 -0
  240. package/src/network/NetworkInspectorPanel.tsx +644 -0
  241. package/src/network/network-inspector-model.ts +225 -0
  242. package/src/play/play-boot-stall.ts +118 -0
  243. package/src/play/play-log-events.ts +26 -0
  244. package/src/play/play-mode.ts +2553 -0
  245. package/src/play/play-recording.ts +335 -0
  246. package/src/play/react-play-live-authoring.ts +165 -0
  247. package/src/play-bar/PlayBar.tsx +488 -0
  248. package/src/play-bar/PlayerCountPicker.tsx +100 -0
  249. package/src/profiler/FrameDebuggerPanel.tsx +458 -0
  250. package/src/profiler/PerformancePanel.tsx +1068 -0
  251. package/src/profiler/ProfilerPanel.tsx +53 -0
  252. package/src/profiler/frame-debugger-store.ts +97 -0
  253. package/src/profiler/main-thread-busy.ts +90 -0
  254. package/src/react/design-time-react-mount.ts +782 -0
  255. package/src/react/dom-authoring-adapter.ts +764 -0
  256. package/src/react/pasteboard-materialize.ts +154 -0
  257. package/src/react/react-inspector-section.tsx +2496 -0
  258. package/src/react/react-world-authoring-adapter.ts +3795 -0
  259. package/src/react/story-documents/story-args-section.ts +19 -0
  260. package/src/react/story-documents/story-documents.tsx +1380 -0
  261. package/src/react/story-paint-bounds.ts +94 -0
  262. package/src/react/ui-board-document.tsx +101 -0
  263. package/src/react/ui-board-title.ts +9 -0
  264. package/src/react/ui-component-board.ts +72 -0
  265. package/src/services/audio-pose-guard.ts +81 -0
  266. package/src/services/game-audio-unlock.ts +48 -0
  267. package/src/state-watch/StateWatchPanel.tsx +535 -0
  268. package/src/three/authoring/camera-runtime-inspector-section.tsx +135 -0
  269. package/src/three/authoring/constraint-inspector-section.tsx +189 -0
  270. package/src/three/authoring/design-time-renderer.ts +189 -0
  271. package/src/three/authoring/model-asset-inspector-section.css +41 -0
  272. package/src/three/authoring/model-asset-inspector-section.tsx +869 -0
  273. package/src/three/authoring/oid-source-persistence.ts +557 -0
  274. package/src/three/authoring/r3f-design-session.ts +1174 -0
  275. package/src/three/authoring/r3f-source-authoring-adapter.ts +5949 -0
  276. package/src/three/authoring/reflection-probe-inspector-section.tsx +76 -0
  277. package/src/three/authoring/spatial-audio-handles.ts +215 -0
  278. package/src/three/authoring/spatial-collider-handles.ts +229 -0
  279. package/src/three/authoring/spatial-joint-handles.ts +114 -0
  280. package/src/three/authoring/spatial-light-handles.ts +178 -0
  281. package/src/three/authoring/spatial-lod-handles.ts +93 -0
  282. package/src/three/authoring/spatial-particle-handles.ts +368 -0
  283. package/src/three/authoring/three-authoring-adapter.ts +1953 -0
  284. package/src/three/authoring/three-scene-identity.ts +19 -0
  285. package/src/three/authoring/three-spatial-handles.ts +161 -0
  286. package/src/three/authoring/typed-three-inspector.ts +528 -0
  287. package/src/three/component-verbs/extract-menu.ts +71 -0
  288. package/src/three/component-verbs/fork-menu.ts +78 -0
  289. package/src/three/component-verbs/internals-menu.ts +91 -0
  290. package/src/three/story-documents/three-story-documents.tsx +607 -0
  291. package/src/three/three-board/ThreeBoardDocument.tsx +888 -0
  292. package/src/three/three-board/board-framing.ts +412 -0
  293. package/src/three/three-board/board-layout.ts +401 -0
  294. package/src/three/three-board/board-scene.ts +901 -0
  295. package/src/three/three-board/three-component-board.ts +62 -0
  296. package/src/xstate/XStateBehaviorSection.tsx +130 -0
  297. package/src/xstate/XStateMachineInspector.tsx +566 -0
  298. package/src/xstate/character-animation-machine.fixture.ts +74 -0
  299. package/src/xstate/live-behaviors.ts +106 -0
  300. package/src/xstate/use-live-actor-state.ts +44 -0
  301. package/src/xstate/xstate-graph.ts +235 -0
  302. package/src/xstate/xstate-layout.ts +76 -0
@@ -0,0 +1,1046 @@
1
+ /**
2
+ * THE WORLD ROOT'S CONTENT BINDING — what the one stage host shows when its
3
+ * `content` is `{ kind: 'world-root' }` (ARCHITECTURE-CORE §One stage).
4
+ *
5
+ * This is the scene panel's whole body, moved: the design session that mounts
6
+ * the manifest's three roots, the composer that draws them (the world's own
7
+ * image pipeline, the shading modes, the soft-particle depth pass, the
8
+ * adopted-image config), the camera-authoring host, the viewport action bus,
9
+ * the auto-frame window, and the presenter Play adopts through
10
+ * (`viewport-root-presentation.ts`). A `build` binding shows one Object3D the
11
+ * project constructed; THIS one shows the project's world, and Play adoption
12
+ * is the condition on it — a build binding presents nothing.
13
+ *
14
+ * WHY IT IS A SEPARATE MODULE, and loaded by dynamic import:
15
+ * `scripts/validate-editor-closure.mjs` pins the host because a BOUNDED HOST
16
+ * (`@volter/editor-blender`'s Model document) mounts it with no shell above it. Everything below reaches the shell's own estate — its transport
17
+ * (`editor-api.ts`), the collaboration client, the server-log bridge, the
18
+ * command listener, the world-adapter install. Static imports would have put
19
+ * all of that into every stage's closure, which is exactly the erosion that
20
+ * ratchet exists to stop. The condition that selects this content gates its
21
+ * LOAD too.
22
+ */
23
+
24
+ import type { ViewportPresentation, ViewportRoot } from '@volter/editor-sdk/host';
25
+ import { themeVars } from '@volter/editor-sdk/widgets';
26
+ import { createPerformanceProfiler } from '@volter/game-runtime/dev/performance-profiler';
27
+ import { createWebGLGpuTimer } from '@volter/game-runtime/dev/webgl-gpu-timer';
28
+ import type { AuthoringAdapter, Transform } from '@volter/editor-project/adapter';
29
+ import { markHostRenderer } from '@volter/editor-threejs/viewport/renderer-ownership';
30
+ import { applyWorldRendererConfig } from '@volter/threejs-runtime/adapter/renderer-config';
31
+ import { getUserData } from '@volter/threejs-runtime/ecs/user-data';
32
+ import { detectKtx2Support } from '@volter/threejs-runtime/loader';
33
+ import { resolveRenderSettings } from '@volter/threejs-runtime/render/render-settings';
34
+ import { createSoftParticleDepthPass } from '@volter/threejs-runtime/render/soft-particle-depth';
35
+ import { ViewportShadingRenderer } from '@volter/threejs-runtime/render/viewport-shading';
36
+ import {
37
+ applyRendererSettings,
38
+ applySceneRenderPipeline,
39
+ } from '@volter/threejs-runtime/setup/setup-renderer';
40
+ import {
41
+ BlendFunction,
42
+ EffectComposer,
43
+ EffectPass,
44
+ KernelSize,
45
+ OutlineEffect,
46
+ type Pass,
47
+ } from 'postprocessing';
48
+ import * as THREE from 'three';
49
+ import { getActiveAuthoring } from '@volter/editor-core/authoring/active-adapter';
50
+ import type { CompositeAuthoringAdapter } from '@volter/editor-core/authoring/composite-authoring-adapter';
51
+ import {
52
+ applyAuthoringTransform,
53
+ beginAuthoringTransformEdit,
54
+ endAuthoringTransformEdit,
55
+ } from '@volter/editor-core/authoring/consumer-actions';
56
+ import { designTimeMountFor } from '@volter/editor-core/authoring/design-time-mount-registry';
57
+ import { attachProjectAuthoringStage } from '@volter/editor-core/authoring/project-authoring-session';
58
+ import {
59
+ isThreejsSurfaceVisible,
60
+ resolveThreeViewportRootId,
61
+ } from '@volter/editor-core/authoring/world-hidden-viewport';
62
+ import { AutoFrameWindow } from '../auto-frame-window';
63
+ import {
64
+ type CameraAuthoringSubject,
65
+ type CameraViewMode,
66
+ cameraAuthoringPresentation,
67
+ installCameraAuthoringHost,
68
+ } from '@volter/editor-core/camera-authoring';
69
+ import { registerPresentedCanvasFrame } from '@volter/editor-core/canvas-preview-frames';
70
+ import { collectState } from '@volter/editor-core/command-listener';
71
+ import { getDownloadedAssetPath, getOnlineAssetFiles, reportEditorState } from '@volter/editor-core/editor-api';
72
+ import { editorConsole } from '@volter/editor-core/editor-console';
73
+ import { isEditorPresentationActive } from '@volter/editor-core/editor-presentation-activity';
74
+ import type { EditorStats } from '@volter/editor-core/editor-runtime';
75
+ import type { EditorShellStore } from '@volter/editor-core/editor-shell-store';
76
+ import { EditorViewport, type OnlineAssetDrop } from '@volter/editor-core/editor-viewport';
77
+ import { entityObject3D } from '@volter/editor-core/entity-object';
78
+ import {
79
+ nativeSelectionColors,
80
+ nativeViewportLook,
81
+ subscribeNativeSelectionTheme,
82
+ } from '@volter/editor-core/native-selection-style';
83
+ import { registerPerformanceSource } from '@volter/editor-core/performance-sources';
84
+ import { drawSceneUnlessRefused } from '../scene-view-drawability';
85
+ import { withSceneFogNeutralized } from '@volter/editor-core/scene-view-fog';
86
+ import { connectServerLogs } from '../server-log-bridge';
87
+ import { focusedStageStore } from '@volter/editor-core/stage-context';
88
+ import {
89
+ createThreeSelectionOutline,
90
+ setThreeSelectionOutlineColors,
91
+ syncThreeSelectionOutline,
92
+ } from '@volter/editor-core/three-viewport/selection-outline';
93
+ import {
94
+ setThreeViewportProjection,
95
+ subscribeThreeViewportPresentation,
96
+ threeViewportPresentation,
97
+ } from '@volter/editor-core/three-viewport-presentation';
98
+ import { recordViewportFirstFrame } from '@volter/editor-core/viewport-activation-timings';
99
+ import { savedViewportPose, saveViewportPose } from '../viewport-pose-memory';
100
+ import { presentThreeRoots } from '../viewport-root-presentation';
101
+ import { isEditorViewportShadingTarget } from '@volter/editor-core/viewport-shading-boundary';
102
+ import { downloadOnlineAssetWithHistory } from '@volter/editor-core/components/asset-editor-persistence';
103
+ import { bindStagePresenceMarkers } from '@volter/editor-core/components/stage-presence-markers';
104
+
105
+ /**
106
+ * The world root's renderer, built to the ENGINE's own render settings —
107
+ * opaque, high-performance, PCF soft shadows, KTX2 support taught to the
108
+ * shared loader. The host's `build` lane constructs a small alpha renderer for
109
+ * a studio stage instead; a stage's drawing surface belongs to what it draws.
110
+ */
111
+ export function mountWorldRootSurface(
112
+ canvasHost: HTMLDivElement,
113
+ container: HTMLDivElement,
114
+ displayName: string,
115
+ ): { canvas: HTMLCanvasElement; renderer: THREE.WebGLRenderer; lease: null } {
116
+ const canvas = document.createElement('canvas');
117
+ canvas.setAttribute('aria-label', `${displayName} authoring viewport`);
118
+ canvas.setAttribute('data-testid', 'editor-canvas');
119
+ canvas.setAttribute('aria-hidden', 'true');
120
+ canvas.tabIndex = -1;
121
+ canvas.style.width = '100%';
122
+ canvas.style.height = '100%';
123
+ canvas.style.display = 'block';
124
+ canvas.style.pointerEvents = 'auto';
125
+ canvasHost.appendChild(canvas);
126
+ // Construct WebGL at the viewport's real backing size. The old 300x150
127
+ // canvas default made the driver allocate once during context creation and
128
+ // again immediately below; under SwiftShader that second cold allocation
129
+ // alone is visible in the startup trace.
130
+ const initialWidth = Math.max(1, container.clientWidth);
131
+ const initialHeight = Math.max(1, container.clientHeight);
132
+ const initialPixelRatio = Math.min(window.devicePixelRatio || 1, 2);
133
+ canvas.width = Math.ceil(initialWidth * initialPixelRatio);
134
+ canvas.height = Math.ceil(initialHeight * initialPixelRatio);
135
+ const renderer = markHostRenderer(
136
+ new THREE.WebGLRenderer({
137
+ canvas,
138
+ antialias: Boolean(resolveRenderSettings(undefined)['antialias']),
139
+ powerPreference: 'high-performance',
140
+ }),
141
+ );
142
+ // Cap at 2, matching the runtime's own policy (create-runtime.ts) — an
143
+ // uncapped 3x display renders 2.25x the pixels the game itself would.
144
+ applyRendererSettings(renderer, undefined, initialPixelRatio);
145
+ renderer.setSize(initialWidth, initialHeight);
146
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap;
147
+ renderer.info.autoReset = false;
148
+ // Teach the shared KTX2Loader this GPU's compressed-format support, so a
149
+ // KHR_texture_basisu GLB (including the Asset Budget's own KTX2 output)
150
+ // loads in EDIT mode too, not only once a game runtime has booted.
151
+ // Idempotent; see `detectKtx2Support` in the engine's loader.
152
+ detectKtx2Support(renderer);
153
+ return { canvas, renderer, lease: null };
154
+ }
155
+
156
+ export interface WorldRootStageOptions {
157
+ /** The SHELL's store — the world root's stage runs on it until unit 4. */
158
+ readonly store: EditorShellStore;
159
+ readonly documentId: string;
160
+ readonly container: HTMLDivElement;
161
+ readonly canvas: HTMLCanvasElement;
162
+ readonly renderer: THREE.WebGLRenderer;
163
+ readonly stats: EditorStats;
164
+ /** The camera-authoring preview pane, read per frame (a React ref). */
165
+ readonly cameraPreview: () => HTMLDivElement | null;
166
+ readonly onMountStatus: (status: 'mounting' | 'ready') => void;
167
+ readonly onRootIds: (rootIds: readonly string[]) => void;
168
+ /** This stage's frame hook on the per-stage viewport door — the host owns
169
+ * the door binding, so it hands the stage the call rather than the id. */
170
+ readonly runFrame: (deltaSeconds: number) => void;
171
+ }
172
+
173
+ export interface WorldRootStage {
174
+ readonly scene: THREE.Scene;
175
+ readonly viewport: EditorViewport;
176
+ frame(timeMs: number, resumed: boolean): void;
177
+ present(roots: readonly ViewportRoot[]): ViewportPresentation | null;
178
+ setHelper(kind: string, object: THREE.Object3D | null): void;
179
+ dispose(): void;
180
+ }
181
+
182
+ /**
183
+ * THE SHELL'S HALF of a viewport online-asset drop — installed as the gizmo
184
+ * viewport's `onlineAssetResolver` so the viewport itself imports neither the
185
+ * editor's transport (`editor-api.ts`) nor the project's download history
186
+ * (`asset-editor-persistence.ts`). See `OnlineAssetResolver` in
187
+ * `editor-viewport.ts` for the contract; this is its one production
188
+ * implementation.
189
+ *
190
+ * Answers a PROJECT-LOCAL path, or `null` after reporting on the console why
191
+ * it could not — the viewport stays quiet on `null` precisely because this
192
+ * already spoke. A cached asset answers with the cache's own path; a fresh
193
+ * download answers with the served, leading-slash form.
194
+ */
195
+ async function resolveDroppedOnlineAsset(online: OnlineAssetDrop): Promise<string | null> {
196
+ const cachedPath = getDownloadedAssetPath(online.source, online.id);
197
+ if (cachedPath) return cachedPath;
198
+
199
+ let url = online.url;
200
+ let format = online.format ?? 'gltf';
201
+ let includes = online.includes;
202
+ if (!url) {
203
+ // No URL in the drag payload: ask the source for its file options and take
204
+ // the first, exactly as the asset browser's own import does.
205
+ const file = (await getOnlineAssetFiles(online.source, online.id))[0];
206
+ if (!file) {
207
+ editorConsole.error(`No downloadable files for ${online.name}.`, 'asset');
208
+ return null;
209
+ }
210
+ url = file.url;
211
+ format = file.format;
212
+ includes = file.includes;
213
+ }
214
+
215
+ const params: Parameters<typeof downloadOnlineAssetWithHistory>[0] = {
216
+ source: online.source,
217
+ id: online.id,
218
+ name: online.name,
219
+ url,
220
+ format,
221
+ };
222
+ if (includes) params.includes = includes;
223
+ const result = await downloadOnlineAssetWithHistory(params);
224
+ if (result.ok && result.path) return `/${result.path}`;
225
+ editorConsole.error(`Could not download ${online.name}.`, 'asset');
226
+ return null;
227
+ }
228
+
229
+ function cameraObjectOf(object: THREE.Object3D): THREE.Camera | null {
230
+ if ((object as THREE.Camera).isCamera) return object as THREE.Camera;
231
+ const owned = getUserData(object, '_camera');
232
+ return owned?.isCamera ? owned : null;
233
+ }
234
+
235
+ function cameraLensLabel(camera: THREE.Camera): string {
236
+ const perspective = camera as THREE.PerspectiveCamera;
237
+ if (perspective.isPerspectiveCamera) return `${perspective.fov.toFixed(0)}° perspective`;
238
+ const orthographic = camera as THREE.OrthographicCamera;
239
+ if (orthographic.isOrthographicCamera) return `Orthographic · ${orthographic.zoom.toFixed(2)}×`;
240
+ return camera.type;
241
+ }
242
+
243
+ function cameraPoseCapability(
244
+ adapter: AuthoringAdapter,
245
+ id: string,
246
+ ): { canAuthorPose: boolean; poseRefusal?: string } {
247
+ const transforms = adapter.transforms;
248
+ if (!transforms) {
249
+ return { canAuthorPose: false, poseRefusal: 'This camera has no transform authoring path.' };
250
+ }
251
+ const position = transforms.editability?.(id, 'position') ?? { writable: true };
252
+ const rotation = transforms.editability?.(id, 'rotation') ?? { writable: true };
253
+ if (position.writable && rotation.writable) return { canAuthorPose: true };
254
+ return {
255
+ canAuthorPose: false,
256
+ poseRefusal: position.reason ?? rotation.reason ?? 'This camera pose is read-only.',
257
+ };
258
+ }
259
+
260
+ function cameraSubjectFor(
261
+ adapter: AuthoringAdapter,
262
+ objectMap: Map<string, THREE.Object3D>,
263
+ id: string,
264
+ ): CameraAuthoringSubject | null {
265
+ // Camera authoring exists only in the adopted Edit scene, whose objectMap is
266
+ // the authoritative live index. Do not ask a temporarily suspended
267
+ // composite root to resolve stale Edit selection during play-stop handoff;
268
+ // that boundary correctly refuses ids it does not own and would turn an
269
+ // ordinary transition into an editor error.
270
+ const object = objectMap.get(id) ?? null;
271
+ if (!object) return null;
272
+ const camera = cameraObjectOf(object);
273
+ if (!camera) return null;
274
+ return {
275
+ id,
276
+ name: object.name || camera.name || 'Camera',
277
+ camera,
278
+ lens: cameraLensLabel(camera),
279
+ ...cameraPoseCapability(adapter, id),
280
+ };
281
+ }
282
+
283
+ /**
284
+ * `id -> tags` for the tag-referencing post effects (outline). Tags are read
285
+ * from the format-neutral `userData['tags']` key — `@volter/threejs-runtime/ecs/scene-query`'s
286
+ * canonical location, so every adapter's nodes carry them the same way.
287
+ */
288
+ function buildEntityTagMap(store: EditorShellStore): Map<string, string[]> {
289
+ const tagMap = new Map<string, string[]>();
290
+ for (const [id, obj] of store.objectMap) {
291
+ const tags = getUserData(obj, 'tags');
292
+ if (Array.isArray(tags) && tags.length > 0) tagMap.set(id, tags);
293
+ }
294
+ return tagMap;
295
+ }
296
+
297
+ export function installWorldRootStage(options: WorldRootStageOptions): WorldRootStage {
298
+ const { store, documentId, container, canvas, renderer, stats } = options;
299
+
300
+ // --- Post-processing ---
301
+ const composer = new EffectComposer(renderer);
302
+ // Renderer-coupled scene depth belongs to the renderer that presents the
303
+ // Scene document. The R3F design mount only constructs and settles the
304
+ // live Object3D graph; its detached no-draw renderer cannot produce depth,
305
+ // and its game camera is not the camera the editor is looking through.
306
+ //
307
+ // This pass is scene-generic: while no armed soft-particle renderer lives
308
+ // under the displayed scene it returns before traversing or drawing. When
309
+ // one does, it uses this viewport's real WebGL context and current camera.
310
+ const softParticleDepth = createSoftParticleDepthPass();
311
+ const viewportShading = new ViewportShadingRenderer();
312
+ const profiler = createPerformanceProfiler();
313
+ // Real GPU time for the viewport's own draw (EXT_disjoint_timer_query_
314
+ // webgl2 — same instrument the play path wires in setup-three-root-
315
+ // adapter). This loop used to hardcode `gpuMs: null`, which left the
316
+ // Profiler's GPU tile permanently "n/a" in edit mode and made a
317
+ // GPU-bound editor unexplainable from the panel. Results resolve a few
318
+ // frames late; `poll()` reports the most recently RESOLVED query, never
319
+ // an interpolation. Unsupported contexts stay honestly null.
320
+ const gpuTimer = createWebGLGpuTimer(renderer.getContext());
321
+ // Timing is gated on the profiler so a closed Profiler pays zero query
322
+ // overhead; end() self-pairs with begin(), so the gate lives here alone.
323
+ const drawTimed = (draw: () => void): void => {
324
+ if (profiler.enabled) gpuTimer.begin();
325
+ try {
326
+ draw();
327
+ } finally {
328
+ gpuTimer.end();
329
+ }
330
+ };
331
+ const pollGpuMs = (): number | null => (profiler.enabled ? gpuTimer.poll() : null);
332
+ const unregisterPerformanceSource = registerPerformanceSource({
333
+ id: documentId,
334
+ label: 'Scene viewport',
335
+ kind: 'scene',
336
+ profiler,
337
+ });
338
+
339
+ // --- Scene ---
340
+ const scene = new THREE.Scene();
341
+ // The palette's viewport background when it carries one; the editor's own
342
+ // grey otherwise. Theme changes follow through the viewport's look
343
+ // subscription (`EditorViewport`).
344
+ scene.background = new THREE.Color(nativeViewportLook(canvas).background ?? 0xaaaaaa);
345
+
346
+ // --- Bind store to scene ---
347
+ store.bindScene(scene, renderer);
348
+
349
+ // --- Viewport ---
350
+ // Bind navigation to the whole authoring surface. A DOM selection layer
351
+ // above the WebGL canvas must not disable right-drag orbit/fly controls.
352
+ const viewport = new EditorViewport(canvas, scene, store, container, {
353
+ renderer,
354
+ onlineAssetResolver: resolveDroppedOnlineAsset,
355
+ onProjectionChange: setThreeViewportProjection,
356
+ });
357
+ // Bind the REAL viewport camera + orbit target so store.cameraPose (the
358
+ // `editor.viewport.camera.get` facet, see command-listener.ts's
359
+ // collectState) and the thumbnail-capture path read live data instead of
360
+ // null — previously bindScene's `camera` param was never passed here, so
361
+ // `store.cameraPose` (and _captureThumbnail's camera read) stayed dark.
362
+ store.bindScene(scene, renderer, viewport.batchedRenderer, viewport.camera);
363
+ store.setOrbitTarget(viewport.orbitControls.target);
364
+ const cameraPosition = new THREE.Vector3();
365
+ const cameraTarget = new THREE.Vector3();
366
+ const cameraForward = new THREE.Vector3();
367
+ const readVisibleCameraPose = () => {
368
+ const camera = viewport.renderCamera;
369
+ camera.getWorldPosition(cameraPosition);
370
+ if (viewport.cameraViewMode === 'pilot') {
371
+ cameraTarget.copy(viewport.orbitControls.target);
372
+ } else if (!viewport.cameraViewMode) {
373
+ cameraTarget.copy(viewport.orbitControls.target);
374
+ } else {
375
+ camera.getWorldDirection(cameraForward);
376
+ cameraTarget.copy(cameraPosition).add(cameraForward.multiplyScalar(10));
377
+ }
378
+ const fov = (camera as THREE.PerspectiveCamera).fov;
379
+ return {
380
+ position: cameraPosition,
381
+ target: cameraTarget,
382
+ fov: typeof fov === 'number' ? fov : 0,
383
+ };
384
+ };
385
+ // THE PRESENCE this stage publishes, and the markers it paints for the other
386
+ // participants, are one module now (`stage-presence-markers.ts`) keyed by
387
+ // THIS stage's document id — the world root's is the Scene document's.
388
+ const presence = bindStagePresenceMarkers({
389
+ scene,
390
+ store,
391
+ documentId,
392
+ container,
393
+ canvas,
394
+ viewport,
395
+ readVisibleCameraPose,
396
+ });
397
+
398
+ let lastPresenceCamera = '';
399
+ // DEV/e2e hook: lets the per-game feature matrix assert the viewport camera
400
+ // reframes (view presets / focus / orbit) on an ingested scene.
401
+ if (import.meta.env.DEV) {
402
+ (window as unknown as Record<string, unknown>)['__vgaiViewport'] = viewport;
403
+ }
404
+
405
+ // --- Composer rebuild ---
406
+ // The un-apply half of the adopted image config below. `applySceneRender-
407
+ // Pipeline` re-derives toneMapping/exposure on every rebuild, but the
408
+ // seam's other fields (outputColorSpace, shadowMap.type, clearColor) have
409
+ // no re-derive path of their own — so each rebuild first runs the RESTORE
410
+ // the previous apply returned, putting back exactly what that apply found.
411
+ // Without this, an adoption ending (or a remount that drops the
412
+ // declaration) would leave the previous world's colour space on the
413
+ // editor's renderer until page reload.
414
+ let restoreAdoptedImage: (() => void) | null = null;
415
+ let selectionOutline: ReturnType<typeof createThreeSelectionOutline> | null = null;
416
+ let selectionOutlinePass: EffectPass | null = null;
417
+ let selectionOutlineUnderlayPass: Pass | null = null;
418
+ const syncSelectionTheme = (): void => {
419
+ if (selectionOutline)
420
+ setThreeSelectionOutlineColors(selectionOutline, nativeSelectionColors(renderer.domElement));
421
+ };
422
+ const unsubscribeSelectionTheme = subscribeNativeSelectionTheme(
423
+ renderer.domElement,
424
+ syncSelectionTheme,
425
+ );
426
+ const syncSelectionOutline = (): void => {
427
+ if (!selectionOutline) return;
428
+ const adapter = getActiveAuthoring(store);
429
+ const roots = [...store.selectedEntityIds]
430
+ // Edit↔Play swaps the active child before its native graph is rebuilt.
431
+ // Do not route a stale outgoing id through the composite during that
432
+ // handoff: it is temporarily absent, not an ownership error.
433
+ .filter((id) => adapter.hierarchy.node(id) !== null)
434
+ .map((id) => entityObject3D(adapter, store.objectMap, id))
435
+ .filter((object): object is THREE.Object3D => object !== null);
436
+ syncThreeSelectionOutline(selectionOutline, roots);
437
+ const active = selectionOutline.selection.size > 0;
438
+ if (selectionOutlinePass) {
439
+ selectionOutlinePass.enabled = active;
440
+ selectionOutlinePass.renderToScreen = active;
441
+ }
442
+ // EffectComposer assigns screen ownership only when passes are added;
443
+ // toggling its final pass later does not promote the preceding pass.
444
+ // Hand the screen back explicitly so deselection paints a fresh frame
445
+ // instead of leaving the last outlined frame in the canvas.
446
+ if (selectionOutlineUnderlayPass) selectionOutlineUnderlayPass.renderToScreen = !active;
447
+ };
448
+ function rebuildComposer(): void {
449
+ // The chain always builds BARE here: nothing authors a render
450
+ // environment for the editor's own composer, and an adapter-backed
451
+ // world composes its own post chain (R3F's fiber-native
452
+ // postprocessing). `editor-viewport.ts` bumps `composerVersion` (via
453
+ // `store.reapplyEnvironment()`) on the world hide/show edges, which is
454
+ // what re-runs this rebuild.
455
+ const hiddenRootId = resolveThreeViewportRootId(store);
456
+ void hiddenRootId;
457
+ // Unwind the previous adoption's config BEFORE re-deriving, so the
458
+ // re-derive below and the (possibly absent) re-apply at the end both
459
+ // start from the editor's own values — see `restoreAdoptedImage`'s decl.
460
+ restoreAdoptedImage?.();
461
+ restoreAdoptedImage = null;
462
+ // `applySceneRenderPipeline` disposes the previous pass but an
463
+ // OutlineEffect's Selection owns temporary render-layer bits on the
464
+ // selected objects. Clear those explicitly before dropping the effect.
465
+ selectionOutline?.selection.clear();
466
+ selectionOutline = null;
467
+ selectionOutlinePass = null;
468
+ selectionOutlineUnderlayPass = null;
469
+ applySceneRenderPipeline(
470
+ composer,
471
+ renderer,
472
+ store.scene ?? scene,
473
+ viewport.renderCamera,
474
+ undefined,
475
+ {
476
+ // Same cap as the renderer-construction call site above.
477
+ basePixelRatio: Math.min(window.devicePixelRatio || 1, 2),
478
+ objectMap: store.objectMap,
479
+ entityTags: buildEntityTagMap(store),
480
+ },
481
+ );
482
+ // Editor selection is appended AFTER the world's image pipeline. It is
483
+ // viewport chrome, never an authored post effect, and therefore remains
484
+ // legible under every shading mode without entering Game rendering.
485
+ // This stage already loaded `postprocessing` for its own composer, so it
486
+ // hands the factory its own bindings — no second module load, and the
487
+ // outline is attached synchronously exactly as before.
488
+ selectionOutline = createThreeSelectionOutline(
489
+ { BlendFunction, KernelSize, OutlineEffect },
490
+ store.scene ?? scene,
491
+ viewport.renderCamera,
492
+ nativeSelectionColors(renderer.domElement),
493
+ );
494
+ syncSelectionTheme();
495
+ selectionOutlineUnderlayPass = composer.passes.at(-1) ?? null;
496
+ selectionOutlinePass = new EffectPass(viewport.renderCamera, selectionOutline);
497
+ selectionOutlinePass.enabled = false;
498
+ composer.addPass(selectionOutlinePass);
499
+ syncSelectionOutline();
500
+ // LAST, and after the engine defaults above have been re-derived: the
501
+ // ADOPTED content's own declaration about the renderer that draws it
502
+ // (`EditorShellStore.adoptedImageConfig`). Everything before this line
503
+ // configures the renderer from the EDITOR's defaults, so a world that was
504
+ // authored for a different colour pipeline would otherwise be drawn in a
505
+ // pipeline it never asked for — see that getter's doc comment.
506
+ //
507
+ // Renderer PROPERTIES only. A world's own post chain stays its own: it
508
+ // renders from the world's camera, not the viewport's, and putting the
509
+ // game's grade on this frame would put it on the grid, the gizmo and
510
+ // every helper too. That line is what keeps editor chrome legible under
511
+ // any content.
512
+ const adoptedImage = store.adoptedImageConfig;
513
+ if (adoptedImage) {
514
+ restoreAdoptedImage = applyWorldRendererConfig(THREE, renderer, adoptedImage);
515
+ }
516
+ }
517
+ const applyViewportPresentation = (): void => {
518
+ viewport.setProjection(threeViewportPresentation().projection);
519
+ store.setViewportCamera(viewport.freeCamera);
520
+ rebuildComposer();
521
+ };
522
+ const unsubscribeViewportPresentation =
523
+ subscribeThreeViewportPresentation(applyViewportPresentation);
524
+ applyViewportPresentation();
525
+
526
+ const resolveCameraSubject = (id: string): CameraAuthoringSubject | null =>
527
+ cameraSubjectFor(getActiveAuthoring(store), store.objectMap, id);
528
+
529
+ let pilotGestureId: string | null = null;
530
+ const finishPilotGesture = (): void => {
531
+ if (!pilotGestureId) return;
532
+ endAuthoringTransformEdit(getActiveAuthoring(store), pilotGestureId);
533
+ pilotGestureId = null;
534
+ };
535
+ const beginPilotGesture = (): void => {
536
+ const view = cameraAuthoringPresentation().view;
537
+ if (view?.mode !== 'pilot' || !view.subject.canAuthorPose) return;
538
+ pilotGestureId = view.subject.id;
539
+ beginAuthoringTransformEdit(getActiveAuthoring(store), pilotGestureId);
540
+ };
541
+ const applyPilotGesture = (): void => {
542
+ if (!pilotGestureId || viewport.cameraViewMode !== 'pilot') return;
543
+ const subject = resolveCameraSubject(pilotGestureId);
544
+ const adapter = getActiveAuthoring(store);
545
+ const transforms = adapter.transforms;
546
+ if (!subject || !transforms) return;
547
+ // `transforms.get` REFUSES BY THROWING for a node with no transform truth
548
+ // (composite-authoring-adapter's unowned/untransformed refusals) rather
549
+ // than answering an identity pose. This runs on every OrbitControls
550
+ // 'change', so an escaping throw would kill the control loop AND repeat
551
+ // the message per frame: report it once and END the gesture, which is
552
+ // what a subject with no pose to write actually means.
553
+ let previous: Transform;
554
+ try {
555
+ previous = transforms.get(pilotGestureId);
556
+ } catch (error) {
557
+ editorConsole.error(
558
+ error instanceof Error ? error.message : String(error),
559
+ 'camera-authoring',
560
+ );
561
+ finishPilotGesture();
562
+ return;
563
+ }
564
+ applyAuthoringTransform(adapter, pilotGestureId, {
565
+ position: subject.camera.position.toArray() as [number, number, number],
566
+ rotation: subject.camera.quaternion.toArray() as [number, number, number, number],
567
+ scale: previous.scale,
568
+ });
569
+ store.notifyIngestEdit();
570
+ };
571
+ viewport.orbitControls.addEventListener('start', beginPilotGesture);
572
+ viewport.orbitControls.addEventListener('change', applyPilotGesture);
573
+ viewport.orbitControls.addEventListener('end', finishPilotGesture);
574
+
575
+ const disposeCameraAuthoring = installCameraAuthoringHost({
576
+ active: () => store.playState === 'stopped' && store.activeViewportTab === 'edit',
577
+ selected: () => {
578
+ const id = store.selectedEntityId;
579
+ return id ? resolveCameraSubject(id) : null;
580
+ },
581
+ resolve: resolveCameraSubject,
582
+ showView: (subject: CameraAuthoringSubject, mode: CameraViewMode) => {
583
+ finishPilotGesture();
584
+ viewport.setCameraView(subject.camera, mode);
585
+ rebuildComposer();
586
+ presence.reportCamera();
587
+ },
588
+ leaveView: () => {
589
+ finishPilotGesture();
590
+ viewport.clearCameraView();
591
+ rebuildComposer();
592
+ presence.reportCamera();
593
+ },
594
+ alignToViewport: (subject: CameraAuthoringSubject) => {
595
+ const adapter = getActiveAuthoring(store);
596
+ const transforms = adapter.transforms;
597
+ if (!transforms || !subject.canAuthorPose) return;
598
+ const targetObject = store.objectMap.get(subject.id) ?? null;
599
+ if (!targetObject) return;
600
+ const worldPosition = viewport.camera.getWorldPosition(new THREE.Vector3());
601
+ const worldQuaternion = viewport.camera.getWorldQuaternion(new THREE.Quaternion());
602
+ const localPosition = worldPosition.clone();
603
+ const localQuaternion = worldQuaternion.clone();
604
+ if (targetObject.parent) {
605
+ targetObject.parent.updateWorldMatrix(true, false);
606
+ targetObject.parent.worldToLocal(localPosition);
607
+ const parentWorldQuaternion = targetObject.parent.getWorldQuaternion(
608
+ new THREE.Quaternion(),
609
+ );
610
+ localQuaternion.premultiply(parentWorldQuaternion.invert());
611
+ }
612
+ // Same refusal contract as applyPilotGesture above — surface it and
613
+ // abort, before any beginEdit opens a gesture that cannot be written.
614
+ let previous: Transform;
615
+ try {
616
+ previous = transforms.get(subject.id);
617
+ } catch (error) {
618
+ editorConsole.error(
619
+ error instanceof Error ? error.message : String(error),
620
+ 'camera-authoring',
621
+ );
622
+ return;
623
+ }
624
+ beginAuthoringTransformEdit(adapter, subject.id);
625
+ applyAuthoringTransform(adapter, subject.id, {
626
+ position: localPosition.toArray() as [number, number, number],
627
+ rotation: localQuaternion.toArray() as [number, number, number, number],
628
+ scale: previous.scale,
629
+ });
630
+ store.notifyIngestEdit();
631
+ endAuthoringTransformEdit(adapter, subject.id);
632
+ },
633
+ subscribe: store.subscribe,
634
+ });
635
+
636
+ // --- Store subscription → sync viewport ---
637
+ // Rebuild the post-processing chain only when its inputs actually change
638
+ // (environment / scene structure / play-stop swap), tracked by composerVersion.
639
+ // Previously this ran on EVERY notify, so a gizmo drag — which notifies once
640
+ // per pointer-move — reconstructed every EffectPass and recompiled shaders
641
+ // 60×/s, causing severe stutter on any scene with post effects enabled.
642
+ // Spec 28 — figma mode's backdrop is the STANDARD transparency
643
+ // checkerboard, not a flat color: with no three surface showing, the area
644
+ // behind the DOM roots isn't "a gray scene", it's NOTHING, and the
645
+ // checkerboard is the universal signal for that. Achieved by fading the
646
+ // WebGL canvas to opacity 0 (keeps layout AND pointer flow identical — the
647
+ // layered-pick path still runs) so the container's own checkerboard shows
648
+ // through behind the DOM world layers appended above it. Driven off the
649
+ // store here rather than from React, because the canvas is the stage's and
650
+ // React never owns it.
651
+ let lastThreejsSurface: boolean | null = null;
652
+ const syncSurfaceVisibility = (): void => {
653
+ const visible = isThreejsSurfaceVisible(store);
654
+ if (visible === lastThreejsSurface) return;
655
+ lastThreejsSurface = visible;
656
+ canvas.style.opacity = visible ? '1' : '0';
657
+ // Dark-UI transparency checkerboard (VS Code image-preview style — the
658
+ // light Figma checker glares in a dark editor and washes out
659
+ // light-on-transparent HUD content). V-20 — the PATTERN itself is a
660
+ // deliberate, documented design choice (left alone); `themeVars.surface.raised`
661
+ // is an exact-value token match for the base tone, so swapped in. The
662
+ // gradient's `#444649` has no exact token in the current set, so it stays
663
+ // a literal per the audit's own "only swap exact matches" guidance.
664
+ container.style.backgroundColor = visible ? '' : themeVars.surface.raised;
665
+ container.style.backgroundImage = visible
666
+ ? ''
667
+ : 'linear-gradient(45deg, #444649 25%, transparent 25%, transparent 75%, #444649 75%), linear-gradient(45deg, #444649 25%, transparent 25%, transparent 75%, #444649 75%)';
668
+ container.style.backgroundPosition = visible ? '' : '0 0, 8px 8px';
669
+ container.style.backgroundSize = visible ? '' : '16px 16px';
670
+ };
671
+ syncSurfaceVisibility();
672
+
673
+ let lastComposerVersion = store.composerVersion;
674
+ const unsubStore = store.subscribe(() => {
675
+ syncSurfaceVisibility();
676
+ // Swap viewport scene if the store's scene changed (play mode enter/exit)
677
+ const storeScene = store.scene;
678
+ if (storeScene && storeScene !== viewport.currentScene) {
679
+ viewport.setScene(storeScene);
680
+ }
681
+ viewport.objectMap = store.objectMap;
682
+ viewport.syncFromStore();
683
+ if (store.composerVersion !== lastComposerVersion) {
684
+ lastComposerVersion = store.composerVersion;
685
+ rebuildComposer();
686
+ } else {
687
+ syncSelectionOutline();
688
+ }
689
+ });
690
+
691
+ // --- Pending scene auto-frame (see the 'focus-scene' action below) ---
692
+ /** Live lookup for the adopted game's own camera — never a snapshot; see
693
+ * the store. Null for a world that has none to wait for. */
694
+ let gameCameraLookup: (() => THREE.Camera | null) | null = null;
695
+ /** Everything about WHEN the camera may still move on its own, and the
696
+ * reader's gesture ending that, lives in `auto-frame-window.ts`. */
697
+ const autoFrame = new AutoFrameWindow({
698
+ seedFromGameCamera: () => {
699
+ const gameCamera = gameCameraLookup?.() ?? null;
700
+ return !!gameCamera && viewport.seedFromGameCamera(gameCamera);
701
+ },
702
+ frameContent: () => viewport.focusOnScene(),
703
+ });
704
+ const cancelAutoFrame = (): void => {
705
+ autoFrame.cancel();
706
+ gameCameraLookup = null;
707
+ };
708
+ viewport.orbitControls.addEventListener('start', cancelAutoFrame);
709
+ // A finished gesture is the pose worth remembering (viewport-pose-memory).
710
+ const rememberPose = (): void => saveViewportPose(viewport.camera, viewport.orbitControls.target);
711
+ viewport.orbitControls.addEventListener('end', rememberPose);
712
+
713
+ // --- Viewport action subscription ---
714
+ const unsubActions = store.onViewportAction((action) => {
715
+ switch (action.type) {
716
+ // Resolved through the ACTIVE ADAPTER first, same as selection and the
717
+ // gizmo — an adopted play scene's nodes live in the adapter's walk and
718
+ // never reach `objectMap`, so a store-only lookup silently framed
719
+ // nothing for every entity in a running game (`entity-object.ts`).
720
+ case 'focus-entity': {
721
+ const obj = entityObject3D(getActiveAuthoring(store), store.objectMap, action.id);
722
+ if (obj) viewport.focusOn(obj);
723
+ break;
724
+ }
725
+ case 'focus-scene': {
726
+ // A game builds itself asynchronously — a level-streaming game
727
+ // captures on its first frame and keeps parsing its world in for
728
+ // seconds afterwards, and its own camera can appear later still — so
729
+ // "open on the world" cannot be a single attempt at mount. The
730
+ // window (auto-frame-window.ts) tries now and keeps trying on a slow
731
+ // cadence; the frame hook below rides it.
732
+ // A remembered pose is a reader gesture that already happened, and
733
+ // the gesture outranks the whole window (auto-frame-window.ts) —
734
+ // restore it and never open the window. First ever open of a
735
+ // project still gets the designed first-look framing.
736
+ {
737
+ const remembered = savedViewportPose();
738
+ if (remembered) {
739
+ viewport.camera.position.set(...remembered.position);
740
+ viewport.orbitControls.target.set(...remembered.target);
741
+ viewport.orbitControls.update();
742
+ break;
743
+ }
744
+ }
745
+ gameCameraLookup = action.gameCamera ?? null;
746
+ autoFrame.begin(performance.now(), gameCameraLookup !== null);
747
+ break;
748
+ }
749
+ case 'focus-selection': {
750
+ const adapter = getActiveAuthoring(store);
751
+ const objects = [...store.selectedEntityIds]
752
+ .map((id) => entityObject3D(adapter, store.objectMap, id))
753
+ .filter((o): o is THREE.Object3D => o !== null);
754
+ viewport.focusOnMultiple(objects);
755
+ break;
756
+ }
757
+ case 'snap-selection-to-floor':
758
+ viewport.snapSelectionToFloor();
759
+ break;
760
+ case 'set-view-preset':
761
+ viewport.setViewPreset(action.preset);
762
+ break;
763
+ case 'set-camera-pose':
764
+ viewport.setPose(action.position, action.target, action.fov);
765
+ break;
766
+ }
767
+ });
768
+
769
+ // --- Error capture → editor console ---
770
+ // Owned by `installEditorConsoleCapture()` (main.tsx) since it became
771
+ // session-lifetime: this stage's mount/unmount is the wrong lifetime for a
772
+ // page-error listener (an error thrown before the viewport mounts, or after
773
+ // it unmounts, is still the session's news), and two owners would have
774
+ // double-logged every uncaught error.
775
+
776
+ // --- Server logs ---
777
+ connectServerLogs();
778
+
779
+ // --- Resize ---
780
+ const resizeObserver = new ResizeObserver(() => {
781
+ // Docking, HMR, and tab switches can briefly collapse the panel to 0x0.
782
+ // Three's EffectComposer allocates incomplete render targets at that
783
+ // size, then floods the console when the shared RAF renders the frame.
784
+ const w = Math.max(1, container.clientWidth);
785
+ const h = Math.max(1, container.clientHeight);
786
+ renderer.setSize(w, h);
787
+ composer.setSize(w, h);
788
+ viewport.resize(w, h);
789
+ });
790
+ resizeObserver.observe(container);
791
+
792
+ // --- Frame loop ---
793
+ let lastTime = performance.now();
794
+ let sceneNeedsFirstFrame = true;
795
+ // A queued callback can survive long enough to meet an HMR/unmount cleanup.
796
+ // Never let it touch the renderer after that renderer has been disposed.
797
+ let disposed = false;
798
+ // The renderer exists before the project's authoring surface does. A draw
799
+ // of the empty editor scene is not a loaded Scene viewport: wait until the
800
+ // complete edit-mode mount / ingest adoption chain has settled, then stamp
801
+ // the first draw that can actually contain the project's authored world.
802
+ let sceneContentReady = false;
803
+
804
+ function drawViewport(dt: number): boolean {
805
+ return drawSceneUnlessRefused(viewport.currentScene, () => {
806
+ // P26 attribution (SwiftShader, CitadelArena probe): a cold first
807
+ // draw took 2.275s while steady-state draws were ~1.3ms with or
808
+ // without the arena selected. Selection bounds were <=0.2ms for the
809
+ // live 80-node subtree (and ~4.1ms for a synthetic 20k-node root).
810
+ // Keep cold shader/driver work distinct from selected-subtree cost:
811
+ // the latter is invalidated in SelectionBrackets.updateIfNeeded().
812
+ // Take game-scene depth BEFORE `renderWithInfrastructure` attaches
813
+ // the grid, gizmos, selection helpers, and other editor-only
814
+ // objects. Those are presentation chrome and must never make an
815
+ // authored particle fade. The pass restores the previous render
816
+ // target before the ordinary composer draw begins.
817
+ softParticleDepth.render(renderer, viewport.currentScene, viewport.renderCamera);
818
+ viewport.renderWithInfrastructure(() => {
819
+ // The Scene camera is not the game's camera, so the game's
820
+ // distance-calibrated fog is not this view's look — see
821
+ // `scene-view-fog.ts`. Presentation-only and restored in a
822
+ // `finally`, exactly like the shading modes it wraps.
823
+ withSceneFogNeutralized(viewport.currentScene, () =>
824
+ viewportShading.render(
825
+ viewport.currentScene,
826
+ store.shadingMode,
827
+ () => composer.render(dt),
828
+ isEditorViewportShadingTarget,
829
+ ),
830
+ );
831
+ viewport.renderViewCube(renderer);
832
+ viewport.renderCameraPreview(
833
+ renderer,
834
+ cameraAuthoringPresentation().preview?.camera ?? null,
835
+ options.cameraPreview(),
836
+ );
837
+ });
838
+ });
839
+ }
840
+
841
+ const unregisterCanvasFrame = registerPresentedCanvasFrame(canvas, async () => {
842
+ if (
843
+ disposed ||
844
+ !isEditorPresentationActive() ||
845
+ !container.checkVisibility({ visibilityProperty: true, checkVisibilityCSS: true })
846
+ )
847
+ return null;
848
+ if (!drawViewport(0)) return null;
849
+ const frame = document.createElement('canvas');
850
+ frame.width = canvas.width;
851
+ frame.height = canvas.height;
852
+ frame.getContext('2d')!.drawImage(canvas, 0, 0);
853
+ return frame;
854
+ });
855
+
856
+ /** Whether this stage is the one the shared panels are following, and so the
857
+ * one that fills the shell's frame/camera readout — see its use below. */
858
+ const ownsShellReadout = (): boolean => focusedStageStore(store) === store;
859
+
860
+ function frame(now: number, resumed: boolean): void {
861
+ if (disposed) return;
862
+ // The Scene document stays MOUNTED during Play with its overlay ancestor
863
+ // hidden by inherited `visibility: hidden`. The host keeps
864
+ // this stage's loop alive so Edit resumes naturally AND so the play-entry
865
+ // camera flight keeps its clock through the cross-fade — but do
866
+ // absolutely no editor presentation/update/profiling work while that
867
+ // document is hidden.
868
+ if (resumed) lastTime = now;
869
+ if (!container.checkVisibility({ visibilityProperty: true, checkVisibilityCSS: true })) {
870
+ lastTime = now;
871
+ sceneNeedsFirstFrame = true;
872
+ return;
873
+ }
874
+
875
+ profiler.beginFrame();
876
+ const frameTime = now - lastTime;
877
+ const dt = frameTime / 1000;
878
+ // THE SHELL'S READOUT IS THE FOCUSED STAGE'S (ARCHITECTURE-CORE §One stage
879
+ // unit 4). `EditorStats` is one mutable struct the shell owns and
880
+ // `CameraInfo`/`StatsOverlay` read; every stage can fill it, and the one
881
+ // that does is whichever stage the panels are following — so a model
882
+ // document's readout reports the camera its reader is looking through,
883
+ // and this stage takes it back the moment focus returns (including while
884
+ // the Game tab holds focus during Play, which owns no stage).
885
+ if (ownsShellReadout()) {
886
+ stats.frameTime = frameTime;
887
+ stats.fps = 1000 / frameTime;
888
+ }
889
+ lastTime = now;
890
+ presence.syncMarkers(dt);
891
+
892
+ autoFrame.tick(now);
893
+
894
+ profiler.beginPhase();
895
+ viewport.update(dt);
896
+ // The door's frame hook (the play-entry camera flight rides it): after
897
+ // viewport.update so a flight has final say over the camera pose
898
+ // (orbit damping never fights it).
899
+ options.runFrame(dt);
900
+ // NOTHING ADVANCES CONTENT HERE. Edit is static on every surface, and the
901
+ // Scene view is no exception: the design world's frameloop is never run
902
+ // (`authoring/r3f-design-session.ts`), its one advance is the bounded
903
+ // settle at mount, and this loop drives only editor-owned presentation —
904
+ // orbit damping, the play-entry flight, the view cube, and the editor's
905
+ // own batched particle renderer, which lives on the editor layer.
906
+ viewport.batchedRenderer.update(dt);
907
+ profiler.endPhase('editor');
908
+ profiler.beginPhase();
909
+ renderer.info.reset();
910
+ // A world built by a three the editor did not make can be beyond this
911
+ // renderer's reach — the throw is what says so, once, and then this
912
+ // scene stops being drawn here. See `scene-view-drawability.ts`.
913
+ drawTimed(() => {
914
+ drawViewport(dt);
915
+ });
916
+ profiler.reportRender({
917
+ gpuMs: pollGpuMs(),
918
+ drawCalls: renderer.info.render.calls,
919
+ triangles: renderer.info.render.triangles,
920
+ geometries: renderer.info.memory.geometries,
921
+ textures: renderer.info.memory.textures,
922
+ });
923
+ profiler.endPhase('render');
924
+ if (ownsShellReadout()) {
925
+ stats.drawCalls = renderer.info.render.calls;
926
+ stats.triangles = renderer.info.render.triangles;
927
+ }
928
+ if (sceneNeedsFirstFrame && sceneContentReady) {
929
+ sceneNeedsFirstFrame = false;
930
+ recordViewportFirstFrame(documentId);
931
+ }
932
+ profiler.endFrame();
933
+
934
+ // Update camera info for CameraInfo panel
935
+ const visiblePose = readVisibleCameraPose();
936
+ const cam = visiblePose.position;
937
+ const tgt = visiblePose.target;
938
+ if (ownsShellReadout()) {
939
+ stats.cameraPosition.x = cam.x;
940
+ stats.cameraPosition.y = cam.y;
941
+ stats.cameraPosition.z = cam.z;
942
+ stats.cameraTarget.x = tgt.x;
943
+ stats.cameraTarget.y = tgt.y;
944
+ stats.cameraTarget.z = tgt.z;
945
+ }
946
+ const presenceCamera = `${cam.x},${cam.y},${cam.z},${tgt.x},${tgt.y},${tgt.z},${visiblePose.fov}`;
947
+ if (presenceCamera !== lastPresenceCamera) {
948
+ lastPresenceCamera = presenceCamera;
949
+ presence.reportCamera();
950
+ }
951
+ }
952
+
953
+ // --- Start ---
954
+ viewport.objectMap = store.objectMap;
955
+ viewport.syncFromStore();
956
+ rebuildComposer();
957
+
958
+ let editModeComposite: CompositeAuthoringAdapter | undefined;
959
+ let disposeR3FSession: (() => void) | undefined;
960
+
961
+ async function installStage(composite: CompositeAuthoringAdapter): Promise<void> {
962
+ if (disposed) return;
963
+ options.onMountStatus('mounting');
964
+ disposeR3FSession?.();
965
+ disposeR3FSession = undefined;
966
+ editModeComposite = composite;
967
+ options.onRootIds(
968
+ composite
969
+ .childAdapters()
970
+ .filter((child) => child.role === 'world' && child.kind === 'three')
971
+ .map((child) => child.worldId),
972
+ );
973
+ // Design-mount the three medium's design session on THIS stage, so EDIT
974
+ // mode authors the live scene with source write-back. The stage names no
975
+ // medium: `design-time-mount-registry.ts` answers by kind and the
976
+ // integration that owns the surface registered it — the same seam a `dom`
977
+ // or `canvas` layer mounts through, on the surface those media do not
978
+ // have. Nothing registered is a real answer: a product that composes no
979
+ // three integration leaves the world root's own Boundary node standing.
980
+ const stageMount = designTimeMountFor('three');
981
+ if (disposed || editModeComposite !== composite) return;
982
+ const disposeSession = stageMount
983
+ ? await stageMount.mountWorldRootSession({ store, composite, renderer })
984
+ : () => {};
985
+ if (disposed || editModeComposite !== composite) {
986
+ disposeSession();
987
+ return;
988
+ }
989
+ disposeR3FSession = disposeSession;
990
+ if (!disposed) options.onMountStatus('ready');
991
+ }
992
+
993
+ const authoringStage = attachProjectAuthoringStage(store, installStage);
994
+ authoringStage.ready
995
+ .then(() => reportEditorState(collectState(store)))
996
+ .catch((err) => {
997
+ editorConsole.error(
998
+ `Editor boot failed: ${err instanceof Error ? err.message : String(err)}`,
999
+ 'scene',
1000
+ );
1001
+ options.onMountStatus('ready');
1002
+ })
1003
+ .finally(() => {
1004
+ // Success and a surfaced terminal failure are both completed viewport
1005
+ // states. In either case the next visible draw is the first honest
1006
+ // frame a startup/activation measurement may certify.
1007
+ sceneContentReady = true;
1008
+ sceneNeedsFirstFrame = true;
1009
+ });
1010
+
1011
+ return {
1012
+ scene,
1013
+ viewport,
1014
+ frame,
1015
+ // PLAY ADOPTION IS THIS BINDING'S — the condition is "the stage is showing
1016
+ // the world root" (ARCHITECTURE-CORE §One stage). A `build` binding shows
1017
+ // one constructed Object3D and presents nothing, so its presenter declines
1018
+ // every root; this one hands the live scene to the store's adoption stack.
1019
+ present: (roots) => presentThreeRoots(store, roots),
1020
+ setHelper: (kind, object) => viewport.setHelper(kind, object),
1021
+ dispose: () => {
1022
+ disposed = true;
1023
+ unregisterPerformanceSource();
1024
+ unregisterCanvasFrame();
1025
+ resizeObserver.disconnect();
1026
+ unsubStore();
1027
+ unsubActions();
1028
+ disposeCameraAuthoring();
1029
+ viewport.orbitControls.removeEventListener('start', beginPilotGesture);
1030
+ viewport.orbitControls.removeEventListener('change', applyPilotGesture);
1031
+ viewport.orbitControls.removeEventListener('end', finishPilotGesture);
1032
+ viewport.orbitControls.removeEventListener('start', cancelAutoFrame);
1033
+ viewport.orbitControls.removeEventListener('end', rememberPose);
1034
+ authoringStage.dispose();
1035
+ disposeR3FSession?.();
1036
+ presence.dispose();
1037
+ viewportShading.dispose();
1038
+ gpuTimer.dispose();
1039
+ unsubscribeViewportPresentation();
1040
+ unsubscribeSelectionTheme();
1041
+ selectionOutline?.selection.clear();
1042
+ softParticleDepth.dispose();
1043
+ composer.dispose();
1044
+ },
1045
+ };
1046
+ }