@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,447 @@
1
+ /**
2
+ * THE SCENE DOCUMENT (`workspace:scene`): the active Three scene, titled
3
+ * `Scene` (the subject, not its entry file — see SCENE_DOCUMENT_TITLE),
4
+ * closeable and discoverable in Content, content = the world root's stage.
5
+ *
6
+ * Transcribed from the Scene half of vgai's `components/CenterDocuments.tsx`.
7
+ * The kit keeps the other half (`@volter/editor-core/components/CenterDocuments`
8
+ * `useCenterDocuments`): it binds the Game document's tab, reports the
9
+ * live/stopped edge and closes every document on session teardown. What moved
10
+ * here is only what the kit no longer does — synthesize the Scene document
11
+ * the kit still routes every three root's edit document to, and mirror the
12
+ * store's `activeViewportTab` into registry activation for the Scene/Game pair.
13
+ *
14
+ * THE INPUT-GATE CONTRACT (CLAUDE.md T6.3): `store.activeViewportTab`
15
+ * remains the single source of truth that `play-mode.ts`/`ingest/mount-ingest-root.ts`
16
+ * read (`playState === 'playing' && activeViewportTab === 'play'`) — this
17
+ * module NEVER replaces that predicate, it mirrors registry activation into
18
+ * it (descriptor `onActivate` → `setActiveViewportTab`) and store changes
19
+ * back into registry activation (`syncSceneDocument`, run on every store
20
+ * notify).
21
+ */
22
+
23
+ import {
24
+ activeDocumentSourcePath,
25
+ activeSaveDestination,
26
+ activeSaveState,
27
+ } from '@volter/editor-core/authoring/shell-document-ops';
28
+ import { Object3DDocumentViewport } from '@volter/editor-core/components/Object3DDocumentViewport';
29
+ import type { DocumentPreviewSource } from '@volter/editor-core/document-preview-source';
30
+ import { editorConsole } from '@volter/editor-core/editor-console';
31
+ import type { EditorShellStore, ViewportTab } from '@volter/editor-core/editor-shell-store';
32
+ import { projectAdapterFacet, subscribeProjectAdapter } from '@volter/editor-core/project-adapter';
33
+ import { getCurrentProject } from '@volter/editor-core/project-manager';
34
+ import { isolationTabsReplaceGenericScene, sceneTabRow } from '@volter/editor-core/scene-document-plan';
35
+ import {
36
+ registerAvailableWorkspaceDocument,
37
+ unregisterAvailableWorkspaceDocument,
38
+ } from '@volter/editor-core/workspace-available-documents';
39
+ import { GAME_DOCUMENT_ID, SCENE_DOCUMENT_ID } from '@volter/editor-core/workspace-document-ids';
40
+ import {
41
+ activateWorkspaceDocument,
42
+ activeWorkspaceDocumentId,
43
+ openWorkspaceDocuments,
44
+ setWorkspaceDocumentDirty,
45
+ type WorkspaceDocumentContentProps,
46
+ type WorkspaceDocumentDescriptor,
47
+ } from '@volter/editor-core/workspace-document-registry';
48
+ import { Button, themeVars } from '@volter/editor-sdk/widgets';
49
+ import { Component, type ErrorInfo, type ReactNode, useEffect, useState } from 'react';
50
+ import { scheduleAfterPaint } from '../after-paint';
51
+
52
+ /**
53
+ * The scene document's tab title. It is the WORD `Scene`, not the entry file's
54
+ * name: this document is the project's one three world, and a strip that reads
55
+ * `Scene · 3D · UI` names four subjects the same way instead of leaking one
56
+ * root's implementation file (`world.tsx`) into a rail of plain nouns. The
57
+ * authored source path is still the document's `provenance.sourcePath`, and
58
+ * the unsaved-changes dot is registry state (`dirty`) rather than title text,
59
+ * so both survive a fixed title.
60
+ */
61
+ export const SCENE_DOCUMENT_TITLE = 'Scene';
62
+
63
+ /** The narrow store surface this module reads/writes — kept minimal so the
64
+ * bridge reads against a plain fake (no full EditorStore). */
65
+ export interface CenterDocumentsStore {
66
+ subscribe(listener: () => void): () => void;
67
+ readonly playState: 'stopped' | 'playing' | 'paused';
68
+ readonly activeViewportTab: ViewportTab;
69
+ /** Where the ACTIVE authoring adapter's edits persist (its
70
+ * `PersistenceProvider.destination`), or null when it persists nothing. */
71
+ readonly savePath: string | null;
72
+ /** Project-relative source path for an immediate-write document whose
73
+ * adapter intentionally exposes no PersistenceProvider. */
74
+ readonly sourcePath?: string | null;
75
+ /** Whether the active authoring adapter has unsaved edits. */
76
+ readonly isDirty: boolean;
77
+ setActiveViewportTab(tab: ViewportTab): void;
78
+ }
79
+
80
+ /** The root Scene's preview stays available after its tab closes. Its source
81
+ * identity is read lazily because the three authoring adapter arrives after
82
+ * the workspace document is registered on a cold boot. */
83
+ function rootScenePreviewSource(store: CenterDocumentsStore): DocumentPreviewSource {
84
+ const source = (): { path: string; exportName?: string } | null => {
85
+ const table = projectAdapterFacet()?.scenes;
86
+ const row = table
87
+ ? sceneTabRow(table).find((item) => item.plan.kind === 'root-document')
88
+ : undefined;
89
+ const declared = row?.entry.source;
90
+ const path = declared?.path ?? store.sourcePath ?? store.savePath;
91
+ return path
92
+ ? { path, ...(declared?.export === undefined ? {} : { exportName: declared.export }) }
93
+ : null;
94
+ };
95
+ return {
96
+ revision: () => {
97
+ const current = source();
98
+ return current ? `${current.path}:${current.exportName ?? 'default'}` : 'unavailable';
99
+ },
100
+ subscribe: (listener) => {
101
+ let identity = JSON.stringify(source());
102
+ const changed = () => {
103
+ const next = JSON.stringify(source());
104
+ if (next === identity) return;
105
+ identity = next;
106
+ listener();
107
+ };
108
+ const stopStore = store.subscribe(changed);
109
+ const stopAdapter = subscribeProjectAdapter(changed);
110
+ return () => {
111
+ stopStore();
112
+ stopAdapter();
113
+ };
114
+ },
115
+ capture: async (request) => {
116
+ const current = source();
117
+ if (!current) throw new Error('The Scene has no declared source module to preview.');
118
+ const owner = await import('./ThreeIsolationSceneContent');
119
+ return owner.captureThreeIsolationScenePreview(
120
+ {
121
+ path: current.path,
122
+ exportName: current.exportName,
123
+ label: SCENE_DOCUMENT_TITLE,
124
+ },
125
+ request,
126
+ );
127
+ },
128
+ };
129
+ }
130
+
131
+ interface ViewportErrorBoundaryState {
132
+ error: Error | null;
133
+ /** Whether an automatic retry is currently scheduled — tracked as state
134
+ * (not inferred from the timer field) so the "retrying…" copy is part of
135
+ * the same render pass `componentDidCatch` triggers when it schedules one. */
136
+ autoRetryPending: boolean;
137
+ }
138
+
139
+ /** THREE's context-creation failure message for the transient
140
+ * Chromium/SwiftShader GPU-channel race that occasionally loses at editor
141
+ * boot — distinct from a genuinely dead/unsupported renderer, which fails
142
+ * differently. */
143
+ const TRANSIENT_RENDERER_STARTUP_FAILURE_PATTERN = /creating WebGL context/i;
144
+
145
+ /** The SAME transient class in its other spelling: the context was created but
146
+ * came back already dead, so THREE's capability probe reads `precision` off
147
+ * the `null` that `getShaderPrecisionFormat` returns on a lost context. Seen
148
+ * live while a project's thumbnail sweep was churning short-lived contexts —
149
+ * the same renderer boots fine on the retry, exactly like the class above. */
150
+ const TRANSIENT_DEAD_CONTEXT_PATTERN = /null \(reading 'precision'\)/;
151
+
152
+ /** True when `error` matches the transient renderer-startup failure class. */
153
+ export function isTransientRendererStartupFailure(error: Error): boolean {
154
+ return (
155
+ TRANSIENT_RENDERER_STARTUP_FAILURE_PATTERN.test(error.message) ||
156
+ TRANSIENT_DEAD_CONTEXT_PATTERN.test(error.message)
157
+ );
158
+ }
159
+
160
+ /** Bound on automatic retries per boundary instance — see `componentDidCatch`. */
161
+ const MAX_AUTO_RETRIES = 3;
162
+
163
+ /** A renderer failure must degrade the Scene document, not unmount the whole
164
+ * workspace. The surrounding hierarchy, Inspector, Console, menus,
165
+ * and project resources remain usable, with an explicit retry after the old
166
+ * renderer/context has been torn down.
167
+ *
168
+ * A transient WebGL context-creation failure at boot would otherwise kill the
169
+ * viewport permanently even though the SAME renderer boots fine moments
170
+ * later. For that specific, recognizable failure class the boundary retries
171
+ * automatically with backoff BEFORE surfacing the manual button — bounded to
172
+ * 3 attempts per boundary instance so a genuinely dead renderer still ends up
173
+ * in front of the user with the manual UI. */
174
+ export class ViewportErrorBoundary extends Component<
175
+ { children: ReactNode },
176
+ ViewportErrorBoundaryState
177
+ > {
178
+ override state: ViewportErrorBoundaryState = { error: null, autoRetryPending: false };
179
+
180
+ /** Automatic retries already used by THIS boundary instance — never reset
181
+ * by a manual retry click, so a manual click after the auto-budget is
182
+ * exhausted does not re-open the budget. */
183
+ private autoRetryCount = 0;
184
+ // `window.setTimeout` (DOM lib) returns `number`; typed explicitly rather
185
+ // than via `ReturnType<typeof setTimeout>` because @types/node's ambient
186
+ // overload would otherwise mis-widen this to `Timeout`.
187
+ private retryTimer: number | null = null;
188
+
189
+ static getDerivedStateFromError(error: Error): ViewportErrorBoundaryState {
190
+ return { error, autoRetryPending: false };
191
+ }
192
+
193
+ private clearPendingRetry(): void {
194
+ if (this.retryTimer !== null) {
195
+ window.clearTimeout(this.retryTimer);
196
+ this.retryTimer = null;
197
+ }
198
+ }
199
+
200
+ override componentDidCatch(error: Error, info: ErrorInfo): void {
201
+ editorConsole.error(`Viewport failed to start: ${error.message}`, 'editor');
202
+ // Keep the component stack in the browser console for developer diagnosis.
203
+ // biome-ignore lint/suspicious/noConsole: renderer startup failures need their React component stack in developer tools as well as the user-facing editor Console entry
204
+ console.error('[vgai] Viewport failed to start', error, info.componentStack);
205
+
206
+ if (isTransientRendererStartupFailure(error) && this.autoRetryCount < MAX_AUTO_RETRIES) {
207
+ this.clearPendingRetry();
208
+ const delayMs = 500 * 2 ** this.autoRetryCount;
209
+ this.autoRetryCount++;
210
+ this.retryTimer = window.setTimeout(() => {
211
+ this.retryTimer = null;
212
+ this.setState({ error: null, autoRetryPending: false });
213
+ }, delayMs);
214
+ this.setState({ autoRetryPending: true });
215
+ }
216
+ }
217
+
218
+ override componentWillUnmount(): void {
219
+ this.clearPendingRetry();
220
+ }
221
+
222
+ private handleManualRetry = (): void => {
223
+ // A manual click also cancels any pending auto-retry (the boundary must
224
+ // not race a scheduled retry against the user's own), but deliberately
225
+ // does NOT reset `autoRetryCount` — the auto-retry budget is per
226
+ // boundary instance, not per error occurrence.
227
+ this.clearPendingRetry();
228
+ this.setState({ error: null, autoRetryPending: false });
229
+ };
230
+
231
+ override render() {
232
+ if (!this.state.error) return this.props.children;
233
+ const retryPending = this.state.autoRetryPending;
234
+ return (
235
+ <div
236
+ data-testid="viewport-startup-error"
237
+ role="alert"
238
+ style={{
239
+ position: 'absolute',
240
+ inset: 0,
241
+ display: 'flex',
242
+ flexDirection: 'column',
243
+ alignItems: 'center',
244
+ justifyContent: 'center',
245
+ gap: 8,
246
+ padding: 24,
247
+ background: themeVars.surface.shell,
248
+ color: themeVars.content.muted,
249
+ textAlign: 'center',
250
+ }}
251
+ >
252
+ <div style={{ color: themeVars.content.primary, fontSize: 14 }}>
253
+ {retryPending ? 'Viewport unavailable — retrying…' : 'Viewport unavailable'}
254
+ </div>
255
+ <div style={{ maxWidth: 520, color: themeVars.content.dim, fontSize: 11 }}>
256
+ {this.state.error.message}
257
+ </div>
258
+ <Button
259
+ type="button"
260
+ className="vgai-btn"
261
+ data-testid="retry-viewport"
262
+ onClick={this.handleManualRetry}
263
+ >
264
+ Retry Viewport
265
+ </Button>
266
+ </div>
267
+ );
268
+ }
269
+ }
270
+
271
+ /**
272
+ * The Scene document's content: the kit's ONE 3D STAGE HOST, showing the
273
+ * world root. It is a document like any other — same host, same furniture,
274
+ * same overlays, same per-stage door — and the ONE thing it says that a
275
+ * prefab's stage does not is WHAT it shows (`content`): the world, through
276
+ * this package's world-root binding, loaded only when a Scene stage mounts.
277
+ *
278
+ * The after-paint gate owns the mount of everything below it — the viewport
279
+ * door's binding included — so it may never be scheduled on a bare rAF chain.
280
+ * A hidden tab fires no frames at all and the editor would boot to a
281
+ * heartbeating shell that can never start play.
282
+ */
283
+ const SCENE_CONTENT = {
284
+ kind: 'world-root',
285
+ load: () => import('./world-root-stage-binding').then((module) => module.worldRootStageBinding),
286
+ } as const;
287
+
288
+ function SceneDocumentContent({ documentId, active }: WorkspaceDocumentContentProps) {
289
+ const [ready, setReady] = useState(false);
290
+ useEffect(() => scheduleAfterPaint(() => setReady(true)), []);
291
+ return (
292
+ <ViewportErrorBoundary>
293
+ {ready ? (
294
+ <Object3DDocumentViewport
295
+ documentId={documentId}
296
+ active={active}
297
+ content={SCENE_CONTENT}
298
+ displayName={SCENE_DOCUMENT_TITLE}
299
+ />
300
+ ) : null}
301
+ </ViewportErrorBoundary>
302
+ );
303
+ }
304
+
305
+ /** Build the Scene document descriptor bound to a store. `onActivate` is the
306
+ * registry→store half of the bridge: activating the tab (click, open,
307
+ * close-neighbor fallback) writes the store's `activeViewportTab`, which is
308
+ * what the T6.3 input gate reads. */
309
+ export function createCenterDocumentDescriptors(store: CenterDocumentsStore): {
310
+ scene: WorkspaceDocumentDescriptor;
311
+ } {
312
+ const documentPath = store.sourcePath ?? store.savePath;
313
+ const scene: WorkspaceDocumentDescriptor = {
314
+ id: SCENE_DOCUMENT_ID,
315
+ title: SCENE_DOCUMENT_TITLE,
316
+ kind: 'scene',
317
+ workspaceRole: 'authored-subject',
318
+ ...(documentPath ? { provenance: { sourcePath: documentPath } } : {}),
319
+ Content: SceneDocumentContent,
320
+ preview: rootScenePreviewSource(store),
321
+ presentation: () =>
322
+ store.savePath
323
+ ? { kind: 'scene', path: store.savePath }
324
+ : { kind: 'workspace', id: SCENE_DOCUMENT_ID },
325
+ onActivate: () => store.setActiveViewportTab('edit'),
326
+ };
327
+ return { scene };
328
+ }
329
+
330
+ /**
331
+ * The store→registry half of the bridge for the Scene/Game pair. Idempotent —
332
+ * safe (and designed) to run on EVERY store notify; re-entrant calls triggered
333
+ * by its own `onActivate` → `setActiveViewportTab` → notify chain settle as
334
+ * no-ops. The Game document's live/stopped edge is the kit's
335
+ * (`useCenterDocuments`), not repeated here.
336
+ *
337
+ * 1. the scene document is available when the manifest has a Three root;
338
+ * 2. its tab's dirty dot tracks `isDirty` live (the title is fixed);
339
+ * 3. the active document mirrors `activeViewportTab` (auto-switch on
340
+ * play/stop, `set-viewport-tab` control commands, the ingest mount lane's
341
+ * direct writes). A `play` tab value with no Game document open (stopped,
342
+ * or a transient store state) activates the scene if its tab is open.
343
+ */
344
+ export function syncCenterDocuments(
345
+ store: CenterDocumentsStore,
346
+ docs: { scene: WorkspaceDocumentDescriptor },
347
+ hasThreeRoot = true,
348
+ ): void {
349
+ const isOpen = (id: string) => openWorkspaceDocuments().some((d) => d.descriptor.id === id);
350
+ // Opening a registry document activates it. Capture the caller's requested
351
+ // viewport before touching the open set, then restore that intent below.
352
+ const requestedViewportTab = store.activeViewportTab;
353
+
354
+ const table = projectAdapterFacet()?.scenes;
355
+ if (hasThreeRoot && !(table && isolationTabsReplaceGenericScene(table))) {
356
+ const row = table
357
+ ? sceneTabRow(table).find((item) => item.plan.kind === 'root-document')
358
+ : undefined;
359
+ registerAvailableWorkspaceDocument(
360
+ {
361
+ ...docs.scene,
362
+ title: row?.plan.kind === 'root-document' ? row.plan.title : docs.scene.title,
363
+ provenance: {
364
+ ...docs.scene.provenance,
365
+ ...(row?.entry.source?.path ? { sourcePath: row.entry.source.path } : {}),
366
+ ...(row?.plan.kind === 'root-document' ? { rootId: row.plan.regionId } : {}),
367
+ },
368
+ },
369
+ {
370
+ category: 'scene',
371
+ rootId: row?.plan.kind === 'root-document' ? row.plan.regionId : undefined,
372
+ default: table !== undefined && (row?.isDefault ?? table.entries.length === 0),
373
+ },
374
+ );
375
+ setWorkspaceDocumentDirty(SCENE_DOCUMENT_ID, store.isDirty);
376
+ } else {
377
+ unregisterAvailableWorkspaceDocument(SCENE_DOCUMENT_ID);
378
+ }
379
+
380
+ // Mirror the store tab into registry activation. A THIRD-party document
381
+ // being active is a legitimate state (its own onActivate already asserted
382
+ // the store's tab, so the T6.3 gate is off). The mirror therefore only
383
+ // corrects genuine disagreements about the SCENE/GAME pair:
384
+ // - tab says 'play' (play auto-switch, `set-viewport-tab play`) → the
385
+ // Game document must be active;
386
+ // - tab says 'edit' while the GAME document is active (external
387
+ // `set-viewport-tab edit` during play) → back to the scene document;
388
+ // - no active document at all (first run) → the scene document.
389
+ // An 'edit' tab with an asset/story document active is CONSISTENT and must
390
+ // not be "corrected".
391
+ const activeId = activeWorkspaceDocumentId();
392
+ if (requestedViewportTab === 'play' && isOpen(GAME_DOCUMENT_ID)) {
393
+ if (activeId !== GAME_DOCUMENT_ID) activateWorkspaceDocument(GAME_DOCUMENT_ID);
394
+ } else if (hasThreeRoot && (activeId === GAME_DOCUMENT_ID || activeId === null)) {
395
+ activateWorkspaceDocument(SCENE_DOCUMENT_ID);
396
+ }
397
+ }
398
+
399
+ /**
400
+ * Installs the Scene document for one session store and keeps it synced. The
401
+ * returned teardown stops syncing; the service withdraws the Scene document, and the kit's
402
+ * `useCenterDocuments` closes every open document on session teardown.
403
+ */
404
+ export function bindSceneDocument(store: EditorShellStore): () => void {
405
+ // `savePath`/`isDirty` are the ACTIVE authoring adapter's, read live
406
+ // through getters rather than copied — the shell owns neither.
407
+ // Keep the authored source identity across Edit → Play: Play deliberately
408
+ // swaps in a live adapter with no source document, but the Scene
409
+ // tab still represents the same authored TSX document.
410
+ let lastSourcePath: string | null = null;
411
+ const view: CenterDocumentsStore = {
412
+ subscribe: (listener) => store.subscribe(listener),
413
+ get playState() {
414
+ return store.playState;
415
+ },
416
+ get activeViewportTab() {
417
+ return store.activeViewportTab;
418
+ },
419
+ get savePath() {
420
+ return activeSaveDestination(store);
421
+ },
422
+ get sourcePath() {
423
+ const current = activeDocumentSourcePath(store);
424
+ if (current) lastSourcePath = current;
425
+ return lastSourcePath;
426
+ },
427
+ get isDirty() {
428
+ return activeSaveState(store) === 'unsaved';
429
+ },
430
+ setActiveViewportTab: (tab) => store.setActiveViewportTab(tab),
431
+ };
432
+ const docs = createCenterDocumentDescriptors(view);
433
+ // Missing is the legacy-safe default: manifests read by current editor
434
+ // versions always carry this derived flag.
435
+ const run = () =>
436
+ syncCenterDocuments(view, docs, getCurrentProject()?.config.hasThreeRoot !== false);
437
+ run();
438
+ const unsubscribe = store.subscribe(run);
439
+ const unsubscribeAdapter = subscribeProjectAdapter(run);
440
+ // Unbinding follows the session's store and leaves the document registered:
441
+ // a store arriving again at boot must not close a restored Scene tab. The
442
+ // service withdraws the document when it stops.
443
+ return () => {
444
+ unsubscribe();
445
+ unsubscribeAdapter();
446
+ };
447
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Selected view for each bottom-drawer utility that hosts more than one
3
+ * instrument.
4
+ *
5
+ * Every store is the usual `useSyncExternalStore` triple (subscribe, version,
6
+ * active) plus `open`, which selects the view AND reveals its own utility
7
+ * through the semantic dock command. Callers name the utility they mean:
8
+ * `profilerView` is the engine instrument bench (Profiler/Frame).
9
+ *
10
+ * A game's own faces are deliberately not among them. They are PROJECT
11
+ * contributions — the game's own vendored source, mounted through the
12
+ * contribution registries — and a contribution owns whatever state it needs
13
+ * inside its own module, so there is nothing for this editor-owned store to
14
+ * hold on its behalf. This store survives on the host side only because HOST
15
+ * chrome reads it: the header heartbeat and the Game toolbar's Capture frame
16
+ * button both select a view of a bench that now ships in `@volter/editor-game`.
17
+ */
18
+
19
+ import { showWorkspaceUtility } from '@volter/editor-core/workspace-host-commands';
20
+
21
+ export type ProfilerViewId = 'profiler' | 'frame';
22
+
23
+ export interface UtilityViewState<T extends string> {
24
+ subscribe(listener: () => void): () => void;
25
+ /** Monotonic change counter — the `getSnapshot` for `useSyncExternalStore`. */
26
+ version(): number;
27
+ active(): T;
28
+ select(view: T): void;
29
+ /** Select the view and reveal its utility in the bottom region. */
30
+ open(view: T): void;
31
+ /** Re-publish without changing the view (state this store carries beside
32
+ * the selected view, e.g. the selected machine, notifies through here). */
33
+ notify(): void;
34
+ reset(): void;
35
+ }
36
+
37
+ function createUtilityViewState<T extends string>(
38
+ utilityId: string,
39
+ initial: T,
40
+ ): UtilityViewState<T> {
41
+ let active = initial;
42
+ let version = 0;
43
+ const listeners = new Set<() => void>();
44
+ const notify = () => {
45
+ version++;
46
+ for (const listener of listeners) listener();
47
+ };
48
+ const select = (view: T) => {
49
+ if (active === view) return;
50
+ active = view;
51
+ notify();
52
+ };
53
+ return {
54
+ subscribe(listener) {
55
+ listeners.add(listener);
56
+ return () => {
57
+ listeners.delete(listener);
58
+ };
59
+ },
60
+ version: () => version,
61
+ active: () => active,
62
+ select,
63
+ open(view) {
64
+ select(view);
65
+ showWorkspaceUtility(utilityId);
66
+ },
67
+ notify,
68
+ reset() {
69
+ active = initial;
70
+ notify();
71
+ },
72
+ };
73
+ }
74
+
75
+ /** Engine instruments: the Profiler and the single-frame draw-call capture. */
76
+ export const profilerView = createUtilityViewState<ProfilerViewId>(
77
+ // The bench is `@volter/editor-game`'s `profiler.utility` contribution, so its
78
+ // registered id carries the host's `tool:` namespace. This store stays on
79
+ // the host side because host chrome reads it (`HeaderTelemetry`'s heartbeat,
80
+ // the Game toolbar's Capture frame button).
81
+ 'tool:profiler.utility',
82
+ 'profiler',
83
+ );
84
+
85
+ export function __resetUtilityViewStateForTest(): void {
86
+ profilerView.reset();
87
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * THE GAME'S WORLD-ROOT BINDING for the kit's stage host
3
+ * (`@volter/editor-core/components/world-root-binding`): the world's surface,
4
+ * its stage, and the overlays the stage shows only over a world.
5
+ *
6
+ * `Overlays` is the world-root half of vgai's `stage-overlay-set.tsx` — every
7
+ * part it rendered only when `worldRoot` held (the root selection layer, the
8
+ * camera-authoring pin, the controls hint while the world mounts, the scene's
9
+ * surface-state card). The rest of that overlay set is the kit's
10
+ * `StageOverlays`, which the host renders beside this.
11
+ */
12
+ import { EDITOR_LAYER } from '@volter/editor-threejs/viewport/editor-layers';
13
+ import { activeAuthoringVersion, subscribeActiveAuthoring } from '@volter/editor-core/authoring/active-adapter';
14
+ import {
15
+ getMountFailureReports,
16
+ subscribeToMountFailures,
17
+ } from '@volter/editor-core/authoring/mount-failure-report';
18
+ import {
19
+ object3DDocumentSessionsVersion,
20
+ subscribeObject3DDocumentSessions,
21
+ } from '@volter/editor-core/authoring/object3d-document-session-registry';
22
+ import { RootSelectionOverlay } from '@volter/editor-core/components/RootSelectionOverlay';
23
+ import { SurfaceStateOverlay } from '@volter/editor-core/components/SurfaceStateOverlay';
24
+ import { ViewportControlsHint } from '@volter/editor-core/components/ViewportControlsHint';
25
+ import type {
26
+ WorldRootOverlayProps,
27
+ WorldRootStageBinding,
28
+ } from '@volter/editor-core/components/world-root-binding';
29
+ import type { EditorShellStore } from '@volter/editor-core/editor-shell-store';
30
+ import { readinessFacet, subscribeRootReadiness } from '@volter/editor-core/readiness';
31
+ import { documentStageContext, threeSelectionToolsApply } from '@volter/editor-core/stage-context';
32
+ import { explainSurface } from '@volter/editor-core/surface-state';
33
+ import {
34
+ subscribeWorkspaceDocuments,
35
+ workspaceDocumentRegistryVersion,
36
+ } from '@volter/editor-core/workspace-document-registry';
37
+ import { useMemo, useSyncExternalStore } from 'react';
38
+ import type * as THREE from 'three';
39
+ import { threeSceneHasRenderableContent } from '../surface-content';
40
+ import { CameraAuthoringOverlay } from './CameraAuthoringOverlay';
41
+ import { installWorldRootStage, mountWorldRootSurface } from './world-root-stage';
42
+
43
+ /**
44
+ * The scene-status overlay is the sole viewport child whose truth can change
45
+ * from object-map membership alone (an empty live scene may gain its first
46
+ * renderable object). Keep that exact broad subscription here so runtime
47
+ * structure does not rerender the canvas, chrome, or collaboration surface.
48
+ */
49
+ function SceneViewportStateOverlay({
50
+ store,
51
+ rootIds,
52
+ mountStatus,
53
+ }: {
54
+ store: EditorShellStore;
55
+ rootIds: readonly string[];
56
+ mountStatus: 'mounting' | 'ready';
57
+ }) {
58
+ useSyncExternalStore(store.subscribe, store.getSnapshot);
59
+ const rootReadiness = useSyncExternalStore(subscribeRootReadiness, readinessFacet);
60
+ const mountFailures = useSyncExternalStore(subscribeToMountFailures, getMountFailureReports);
61
+ const sceneBackground = store.scene?.background as THREE.Color | null | undefined;
62
+ const sceneBackgroundIsAuthored =
63
+ sceneBackground !== null &&
64
+ !(sceneBackground?.isColor === true && sceneBackground.getHex() === 0xaaaaaa);
65
+ const sceneHasRenderableObjects = useMemo(
66
+ () =>
67
+ Boolean(
68
+ store.scene &&
69
+ threeSceneHasRenderableContent(store.scene, {
70
+ includeBackground: false,
71
+ ignoredLayer: EDITOR_LAYER,
72
+ }),
73
+ ),
74
+ [store.scene, store.contentVersion],
75
+ );
76
+ const sceneHasContent = Boolean(
77
+ store.scene && (sceneBackgroundIsAuthored || sceneHasRenderableObjects),
78
+ );
79
+ const explanation = explainSurface({
80
+ surface: 'Scene',
81
+ rootIds,
82
+ phase: mountStatus === 'mounting' ? 'loading' : 'ready',
83
+ content: sceneHasContent ? 'present' : mountStatus === 'mounting' ? 'unknown' : 'empty',
84
+ readiness: rootReadiness,
85
+ failures: mountFailures,
86
+ });
87
+ return <SurfaceStateOverlay explanation={explanation} testId="scene-viewport-status" />;
88
+ }
89
+
90
+ function WorldRootOverlays({
91
+ store,
92
+ documentId,
93
+ cameraPreviewRef,
94
+ mountStatus,
95
+ rootIds,
96
+ }: WorldRootOverlayProps) {
97
+ useSyncExternalStore(store.subscribe, store.getShellSnapshot ?? store.getSnapshot);
98
+ useSyncExternalStore(subscribeActiveAuthoring, activeAuthoringVersion);
99
+ useSyncExternalStore(subscribeObject3DDocumentSessions, object3DDocumentSessionsVersion);
100
+ useSyncExternalStore(subscribeWorkspaceDocuments, workspaceDocumentRegistryVersion);
101
+ // The world root's stage is the Scene document's: the kit's overlay props
102
+ // carry no document id, and the Scene document is the one world-root stage.
103
+ const ctx = documentStageContext(store, documentId, 'document');
104
+ const showsSelectionTools = threeSelectionToolsApply(ctx);
105
+ return (
106
+ <>
107
+ {/* Three-scene selection overlay. React/Pixi documents mount their own
108
+ co-located overlay beside the isolated world host, and the worlds it
109
+ draws over are the manifest's — so its condition is the stage showing
110
+ them, the same fact the surface-state card reads. */}
111
+ {ctx.surface !== null ? <RootSelectionOverlay /> : null}
112
+ {/* A pinned camera preview survives deselection. The camera-authoring
113
+ host is installed by the world root's own binding
114
+ (`world-root-stage.ts`), so the pin belongs to the stage that
115
+ installed it. */}
116
+ {ctx.surface === 'three' ? <CameraAuthoringOverlay previewRef={cameraPreviewRef} /> : null}
117
+ {/* The hint also shows WHILE the world is still mounting: a new user
118
+ reads the screen during those boot seconds and looks for the camera
119
+ controls exactly then. The kit's overlay set shows it on a three
120
+ surface; this adds only the mounting case it does not cover. */}
121
+ {!showsSelectionTools && ctx.surface !== 'three' && mountStatus === 'mounting' ? (
122
+ <ViewportControlsHint />
123
+ ) : null}
124
+ <SceneViewportStateOverlay store={store} rootIds={rootIds} mountStatus={mountStatus} />
125
+ </>
126
+ );
127
+ }
128
+
129
+ export const worldRootStageBinding: WorldRootStageBinding = {
130
+ mountWorldRootSurface,
131
+ installWorldRootStage,
132
+ Overlays: WorldRootOverlays,
133
+ };