@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,34 @@
1
+ import { gameRealmForMountId } from '../host/gated-globals';
2
+ import {
3
+ readGameContract,
4
+ type VgaiGameContract,
5
+ } from '@volter/editor-project/adapter/ingest/game-contract';
6
+
7
+ /**
8
+ * The browser realm used by the singleton ingest routes.
9
+ *
10
+ * Vendored ingest entry graphs deliberately carry no `?vgai-mount=` query:
11
+ * they share the editor's Pixi/Three packages and run in the default gated
12
+ * realm. Realm hardening keeps game-owned expandos on that realm's window
13
+ * proxy, so host code must read the contract from the same proxy rather than
14
+ * from the editor's real `window`.
15
+ */
16
+ export function ingestGameRealmWindow(): Window {
17
+ return gameRealmForMountId().window;
18
+ }
19
+
20
+ /**
21
+ * Read the declaration from the realm in which ingest modules publish it.
22
+ *
23
+ * No DOM means no realm to build — `gameRealmForMountId` proxies the real
24
+ * `window`/`document`, so calling it in a DOM-less process is a bare `window`
25
+ * reference, not a null read. Answering `null` there is the same headless
26
+ * default `readGameContract` itself carries, and it is what keeps the
27
+ * DOM-free callers of this (the command relay's `recordContractSystemUse`,
28
+ * reached by every `bridge-call`) degrading instead of throwing
29
+ * `window is not defined` at them.
30
+ */
31
+ export function readIngestGameContract(): VgaiGameContract | null {
32
+ if (typeof window === 'undefined') return null;
33
+ return readGameContract(ingestGameRealmWindow());
34
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * POINTER LOCK IS A PAGE-WIDE SINGLETON, AND TWO THINGS WANT IT.
3
+ *
4
+ * The editor viewport takes pointer lock for its own camera drag
5
+ * (`editor-viewport.ts`). An ingested first-person game takes it the moment
6
+ * its canvas is clicked — unconditionally, from a listener it installed on its
7
+ * OWN element. Only one element can hold the lock, so in-realm those
8
+ * two fight: click the game canvas while merely INSPECTING it in Edit and the
9
+ * mouse disappears into a game that is not even playing.
10
+ *
11
+ * `gated-globals.ts` cannot see this. Its whole mechanism is a scoped
12
+ * `window`/`document`, and `requestPointerLock` is called on neither — it is
13
+ * an `Element` method invoked on the game's own canvas. So the gate goes on
14
+ * the method, ONCE, and is SCOPED BY ELEMENT: a request from inside a
15
+ * registered game surface (`gated-globals.ts`'s `gameSurfaces`) obeys that
16
+ * realm's input gate — the same predicate that decides whether the game's
17
+ * keydown listeners fire, so pointer lock and input can never disagree.
18
+ * A request from anywhere else — the editor's own viewport canvas, which is
19
+ * not inside any game surface — passes through untouched.
20
+ *
21
+ * Refused, never silent: the console line names the element, the gate and the
22
+ * one thing that makes it open (play running, Game tab active), because a
23
+ * mouse that silently does not capture reads as a broken game.
24
+ */
25
+
26
+ import { gameSurfaces } from '../host/gated-globals';
27
+
28
+ /** The real method, captured at install time so a refusal can still call
29
+ * through and a double install is impossible. */
30
+ let realRequestPointerLock: ((this: Element, options?: unknown) => unknown) | null = null;
31
+
32
+ /** The surface a pointer-lock request came from, or null when the element
33
+ * belongs to the editor rather than to any mounted game. */
34
+ function gameSurfaceOf(el: Element): { surface: HTMLElement; gate: () => boolean } | null {
35
+ for (const entry of gameSurfaces()) {
36
+ if (entry.surface.contains(el)) return entry;
37
+ }
38
+ return null;
39
+ }
40
+
41
+ /**
42
+ * Install the gate. Idempotent and process-wide — there is ONE
43
+ * `Element.prototype`, and scoping is by element, not by installation.
44
+ * Called by the ingest mount; never uninstalled, because the predicate it
45
+ * consults (`gameSurfaces()`) is empty when no game is mounted, which makes
46
+ * the patch a pure pass-through rather than something to unwind.
47
+ */
48
+ export function installGamePointerLockGate(): void {
49
+ if (realRequestPointerLock !== null) return;
50
+ if (typeof Element === 'undefined') return;
51
+ const proto = Element.prototype as unknown as {
52
+ requestPointerLock?: (this: Element, options?: unknown) => unknown;
53
+ };
54
+ const real = proto.requestPointerLock;
55
+ if (typeof real !== 'function') return;
56
+ realRequestPointerLock = real;
57
+ proto.requestPointerLock = function (this: Element, options?: unknown): unknown {
58
+ const entry = gameSurfaceOf(this);
59
+ if (entry && !entry.gate()) {
60
+ // A NEW native console.error site, suppressed to keep this task's diff at
61
+ // zero new lint warnings — and native on purpose: this fires from a game's
62
+ // own click handler in the page, and the page console is what `vgai
63
+ // status` reads.
64
+ // biome-ignore lint/suspicious/noConsole: see comment above
65
+ console.error(
66
+ 'Ingested game requested pointer lock while its input gate is closed — refused. ' +
67
+ 'A game only captures the mouse while play is running AND the Game tab is active ' +
68
+ '(the same predicate that gates its keyboard listeners); the editor viewport owns ' +
69
+ 'pointer lock otherwise. See packages/editor/src/ingest/game-pointer-lock.ts.',
70
+ );
71
+ // Match the spec's failure shape rather than throwing: the browser
72
+ // rejects a disallowed request, and a throw would abort the game's own
73
+ // click handler mid-way.
74
+ return Promise.reject(new Error('pointer lock refused: game input gate is closed'));
75
+ }
76
+ return real.call(this, options);
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Drop pointer lock if a GAME element holds it. Called when a mount disposes —
82
+ * a lock outliving the game that took it leaves the editor with a captured,
83
+ * invisible cursor and nothing left to release it.
84
+ */
85
+ export function releaseGamePointerLock(): void {
86
+ if (typeof document === 'undefined') return;
87
+ const locked = document.pointerLockElement;
88
+ if (locked && gameSurfaceOf(locked)) document.exitPointerLock();
89
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * PRESENTATION repaints for a scene switch performed while the mount is HELD.
3
+ *
4
+ * The defect this closes: a canvas ingest ends held in Edit
5
+ * (`holdIngestContentTimeForMode`) by stopping the game's own ticker — which is
6
+ * also what drives its rendering. The game's navigation runs on its own
7
+ * machinery (promises, gsap's own ticker), so picking a scene in the story
8
+ * picker genuinely moves `current()` — measured live on bubbo-bubbo — while the
9
+ * canvas keeps showing the OLD scene's last frame until ▶. The editor claims
10
+ * "you are looking at scene X" while showing scene Y.
11
+ *
12
+ * The line this walks is the `edit-mode-static` invariant's own: CONTENT time
13
+ * stays held (the ticker is never started, no game update runs), while
14
+ * PRESENTATION may move. `render()` draws the stage as it is; the scene
15
+ * transition's tweens advance on gsap's clock whether or not anyone draws, so
16
+ * drawing during the switch is showing the truth, not advancing it.
17
+ *
18
+ * Bounded by construction: frames are requested only between `apply` and
19
+ * SETTLE (the game's own `current()` reporting the requested scene) plus one
20
+ * grace window for the transition's tail, with a hard deadline for a scene
21
+ * that never arrives. No interval survives past that; an unheld mount renders
22
+ * nothing here (the running ticker already draws).
23
+ */
24
+
25
+ /** One frame-scheduler tick: `schedule(cb)` runs `cb(nowMs)` on the next
26
+ * presentation frame. Injectable so the loop is testable without wall-clock
27
+ * or rAF; production passes `requestAnimationFrame`. */
28
+ export type FrameScheduler = (cb: (nowMs: number) => void) => void;
29
+
30
+ export interface HeldSceneRepaintSeams {
31
+ /** Is content time currently held? (An unheld mount needs no help drawing.) */
32
+ readonly isHeld: () => boolean;
33
+ /** The game's own answer for which scene is up. */
34
+ readonly currentScene: () => string | null;
35
+ /** Draw one presentation frame of the stage as it currently is. */
36
+ readonly render: () => void;
37
+ readonly schedule: FrameScheduler;
38
+ /** Keep drawing this long after the scene settles, for the transition tail. */
39
+ readonly settleGraceMs?: number;
40
+ /** Give up on a scene that never arrives (a failed asset load refuses
41
+ * upstream and is reported there; this just stops drawing). */
42
+ readonly deadlineMs?: number;
43
+ }
44
+
45
+ const DEFAULT_GRACE_MS = 1500;
46
+ const DEFAULT_DEADLINE_MS = 10000;
47
+
48
+ /**
49
+ * Begin repainting toward `requestedSceneId`. Returns immediately; frames run
50
+ * on the scheduler. Re-entrant calls are safe: each call is its own bounded
51
+ * loop, and drawing the same stage twice in a frame is idempotent.
52
+ */
53
+ export function repaintHeldSceneSwitch(
54
+ requestedSceneId: string,
55
+ seams: HeldSceneRepaintSeams,
56
+ ): void {
57
+ const grace = seams.settleGraceMs ?? DEFAULT_GRACE_MS;
58
+ const deadline = seams.deadlineMs ?? DEFAULT_DEADLINE_MS;
59
+ let startMs: number | null = null;
60
+ let settledAtMs: number | null = null;
61
+ const tick = (nowMs: number): void => {
62
+ if (!seams.isHeld()) return; // released — the ticker draws from here on
63
+ startMs ??= nowMs;
64
+ if (settledAtMs === null && seams.currentScene() === requestedSceneId) {
65
+ settledAtMs = nowMs;
66
+ }
67
+ seams.render();
68
+ if (settledAtMs !== null && nowMs - settledAtMs >= grace) return;
69
+ if (settledAtMs === null && nowMs - startMs >= deadline) return;
70
+ seams.schedule(tick);
71
+ };
72
+ seams.schedule(tick);
73
+ }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Who owns the CSS box of an adopted ingest surface — S-4 (the SimCity ingest
3
+ * ledger).
4
+ *
5
+ * THE DEFECT. The host resized an ingested game with
6
+ * `renderer.setSize(w, h, false)`. `updateStyle: false` means "touch the
7
+ * backing store, never the style", so the canvas' CSS box keeps whatever
8
+ * three's OWN construction-time `setSize` (whose `updateStyle` defaults to
9
+ * `true`) stamped on it in literal pixels while the game still believed it
10
+ * owned the window. Adopt that canvas into a docked tab that happens to be
11
+ * 0-height at mount and it is frozen there: revealing the tab fires the host's
12
+ * ResizeObserver, the backing store is resized correctly — and the picture is
13
+ * still a black 0-height box, because nothing ever un-stamped the style.
14
+ * `updateStyle: false` cannot un-stamp a previous call; only a later write can.
15
+ *
16
+ * THE RULE, and it is the same one `engine/src/runtime/create-runtime.ts`
17
+ * already applies to first-party roots (see its E4.R1 comment): the HOST owns
18
+ * the adopted element's CSS box, the GAME owns only its backing resolution.
19
+ * Make the two fully independent — box asserted as "fill the pane", buffer
20
+ * driven by `setSize(w, h, false)` — and both a 0-height mount and a later
21
+ * pane resize are correct by construction, with no ordering to get right.
22
+ *
23
+ * Kept as its own module (rather than four inline style writes) because the
24
+ * rule now has three call sites across two mount paths, and because the box it
25
+ * asserts is a contract worth one test rather than three transcripts.
26
+ */
27
+
28
+ /**
29
+ * The CSS the host stamps on the element it adopted. `position`/`inset` place
30
+ * it against the pane (`hostEl` is `position:absolute;inset:0`), and the
31
+ * percentage `width`/`height` are what actually override a pixel stamp — a
32
+ * `width:1187px` left over from the game's own `setSize` survives `inset:0`
33
+ * alone, which is exactly how S-4 hid for so long.
34
+ *
35
+ * `zIndex` is cleared: a game that stacked itself over a whole page has no
36
+ * business stacking itself over editor chrome once adopted.
37
+ *
38
+ * `transform` is cleared for the same reason, and it is the OTHER half of the
39
+ * pixel stamp S-4 found. A game that centres its own canvas in the window
40
+ * writes `left:50%;top:50%;transform:translate(-50%,-50%)`.
41
+ * `inset:0` un-does the `left`/`top` half — they are
42
+ * the same longhands — but nothing un-does the transform, so the adopted
43
+ * canvas rendered half its own width up and to the left of the pane, with a
44
+ * hard black L down two edges. Like `width:100%`, this is a property the HOST
45
+ * now owns: the game's centring was correct while it owned the window and is
46
+ * simply not its decision any more.
47
+ */
48
+ export const HOST_ADOPTED_SURFACE_CSS = {
49
+ position: 'absolute',
50
+ inset: '0',
51
+ width: '100%',
52
+ height: '100%',
53
+ zIndex: '',
54
+ transform: '',
55
+ } as const;
56
+
57
+ /**
58
+ * The box for an adopted surface whose RESOLUTION THE HOST DOES NOT DRIVE.
59
+ *
60
+ * {@link HOST_ADOPTED_SURFACE_CSS} fills the pane, and that is right for the
61
+ * three lane precisely because the host also resizes the game's backing store
62
+ * to the pane on every change — box and buffer share an aspect by
63
+ * construction, so "fill" never stretches anything.
64
+ *
65
+ * A canvas ingest has no such half. A Pixi/Phaser/Babylon game lays itself out
66
+ * with imperative formulas against the resolution IT chose (`flappy` declares
67
+ * 480x640 and registers no resize listener), so the host has no licence to
68
+ * re-resolve it and filling the pane would simply report a picture the game
69
+ * does not draw — MEASURED, sighted: 480x640 stretched into a 1107x780 pane
70
+ * flattened the bird to an oval and squashed the HUD.
71
+ *
72
+ * `width`/`height: auto` with percentage maxima is the CSS that says exactly
73
+ * that. For a REPLACED element (a canvas is one) the max-constraint algorithm
74
+ * is defined to preserve the intrinsic ratio, so the used box is the game's own
75
+ * resolution, scaled DOWN only far enough to fit, never up and never
76
+ * distorted; `inset: 0` + `margin: auto` centres it in the pane.
77
+ *
78
+ * `object-fit` is the rejected alternative and it is worth naming, because it
79
+ * looks equivalent and is not: it letterboxes the PAINT inside a box that stays
80
+ * pane-sized, so the element's `getBoundingClientRect()` no longer describes
81
+ * what is on screen — and that rect is exactly what `composite-screenshot.ts`
82
+ * draws each canvas into. The screen would letterbox while every capture of it
83
+ * stretched, which is worse than either alone.
84
+ */
85
+ export const HOST_ADOPTED_UNRESIZED_SURFACE_CSS = {
86
+ position: 'absolute',
87
+ inset: '0',
88
+ margin: 'auto',
89
+ width: 'auto',
90
+ height: 'auto',
91
+ maxWidth: '100%',
92
+ maxHeight: '100%',
93
+ zIndex: '',
94
+ transform: '',
95
+ } as const;
96
+
97
+ /** The subset of `CSSStyleDeclaration` {@link claimHostSurfaceBox} and
98
+ * {@link claimUnresizedSurfaceBox} write — named explicitly (rather than as an
99
+ * index signature) so a real element's `.style` satisfies it structurally and
100
+ * a unit test can still pass a plain object. */
101
+ export interface MutableBoxStyle {
102
+ position: string;
103
+ inset: string;
104
+ margin: string;
105
+ width: string;
106
+ height: string;
107
+ maxWidth: string;
108
+ maxHeight: string;
109
+ zIndex: string;
110
+ transform: string;
111
+ }
112
+
113
+ /**
114
+ * Assert host ownership of `el`'s CSS box. Idempotent, and safe to re-run on
115
+ * every resize — which is deliberate: a game that installs its own
116
+ * `window.resize` handler re-stamps pixels whenever the editor window changes,
117
+ * so the host re-asserting on each pane resize is what keeps the box correct
118
+ * over a session rather than only at adoption.
119
+ */
120
+ export function claimHostSurfaceBox(el: { style: MutableBoxStyle }): void {
121
+ el.style.position = HOST_ADOPTED_SURFACE_CSS.position;
122
+ el.style.inset = HOST_ADOPTED_SURFACE_CSS.inset;
123
+ el.style.width = HOST_ADOPTED_SURFACE_CSS.width;
124
+ el.style.height = HOST_ADOPTED_SURFACE_CSS.height;
125
+ el.style.zIndex = HOST_ADOPTED_SURFACE_CSS.zIndex;
126
+ el.style.transform = HOST_ADOPTED_SURFACE_CSS.transform;
127
+ // The two policies write an overlapping property set, so each must clear what
128
+ // the other sets — otherwise re-adopting one surface after the other leaves a
129
+ // stale `margin: auto` / `max-width` behind and the box is neither policy.
130
+ el.style.margin = '';
131
+ el.style.maxWidth = '';
132
+ el.style.maxHeight = '';
133
+ }
134
+
135
+ /**
136
+ * Assert host ownership of `el`'s CSS box for a surface the host does NOT
137
+ * resize — see {@link HOST_ADOPTED_UNRESIZED_SURFACE_CSS}. Same idempotence and
138
+ * the same re-assertable contract as {@link claimHostSurfaceBox}.
139
+ */
140
+ export function claimUnresizedSurfaceBox(el: { style: MutableBoxStyle }): void {
141
+ el.style.position = HOST_ADOPTED_UNRESIZED_SURFACE_CSS.position;
142
+ el.style.inset = HOST_ADOPTED_UNRESIZED_SURFACE_CSS.inset;
143
+ el.style.margin = HOST_ADOPTED_UNRESIZED_SURFACE_CSS.margin;
144
+ el.style.width = HOST_ADOPTED_UNRESIZED_SURFACE_CSS.width;
145
+ el.style.height = HOST_ADOPTED_UNRESIZED_SURFACE_CSS.height;
146
+ el.style.maxWidth = HOST_ADOPTED_UNRESIZED_SURFACE_CSS.maxWidth;
147
+ el.style.maxHeight = HOST_ADOPTED_UNRESIZED_SURFACE_CSS.maxHeight;
148
+ el.style.zIndex = HOST_ADOPTED_UNRESIZED_SURFACE_CSS.zIndex;
149
+ el.style.transform = HOST_ADOPTED_UNRESIZED_SURFACE_CSS.transform;
150
+ }
151
+
152
+ /**
153
+ * The backing-store size to hand `renderer.setSize`. A host pane is measured
154
+ * with `getBoundingClientRect()`, which is fractional, and can legitimately
155
+ * report a collapsed dimension mid-transition; three multiplies by the pixel
156
+ * ratio and floors, so feeding it 0 yields a 0-wide framebuffer (a black
157
+ * canvas that no later correct resize necessarily repairs, since a WebGL
158
+ * context can be lost on a zero-size drawing buffer).
159
+ *
160
+ * Floor to whole CSS pixels and clamp to at least 1: the smallest size that is
161
+ * still a renderable surface. The CSS box is unaffected — it is `100%` and
162
+ * follows the pane exactly, so clamping the buffer never shows up as layout.
163
+ */
164
+ export function hostSurfaceBackingSize(
165
+ width: number,
166
+ height: number,
167
+ ): { width: number; height: number } {
168
+ return { width: clampDimension(width), height: clampDimension(height) };
169
+ }
170
+
171
+ function clampDimension(value: number): number {
172
+ if (!Number.isFinite(value)) return 1;
173
+ return Math.max(1, Math.floor(value));
174
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * WHERE A BOOT-TIME INGEST BECOMES EDITABLE.
3
+ *
4
+ * Game is a play-time document (`game-document.ts`). A boot-time ingest
5
+ * still acquires Game — that is the host the runtime mounts into — but the
6
+ * authoring surface is Scene. Three already landed on Edit; canvas and DOM
7
+ * forced the play tab, so opening Bubbo put Game in front with nothing
8
+ * having pressed ▶.
9
+ *
10
+ * This is also the ONE cold-landing boundary. Every successful ingest mount
11
+ * publishes its required pause surface before calling here, so hold content
12
+ * time BEFORE exposing Edit. Keeping the hold here prevents each surface
13
+ * adapter from independently remembering (or forgetting) the same invariant.
14
+ * A play-time ingest mount (`deferred-ingest-play.ts`) crosses this boundary
15
+ * while it constructs, then explicitly releases the hold and flips to Game.
16
+ */
17
+
18
+ import { editorConsole } from '@volter/editor-core/editor-console';
19
+ import type { EditorShellStore } from '@volter/editor-core/editor-shell-store';
20
+ import { projectAdapterFacet } from '@volter/editor-core/project-adapter';
21
+ import { sceneDocumentId } from '@volter/editor-core/scene-document-plan';
22
+ import { CANVAS_SCENE_DOCUMENT_ID, SCENE_DOCUMENT_ID } from '@volter/editor-core/workspace-document-ids';
23
+ import {
24
+ activateWorkspaceDocument,
25
+ openWorkspaceDocuments,
26
+ } from '@volter/editor-core/workspace-document-registry';
27
+ import { activeIngest } from './active-ingest';
28
+ import { activeIngestPauseGap, holdIngestContentTimeForMode } from './ingest-play-control';
29
+
30
+ export function landIngestBootInEdit(store: EditorShellStore): void {
31
+ const active = activeIngest();
32
+ if (active) {
33
+ const held = holdIngestContentTimeForMode('edit');
34
+ if (!held) {
35
+ throw new Error(
36
+ `Ingest "${active.worldId}" mounted without the required pause capability; refusing ` +
37
+ `to expose a running game in Edit — ${activeIngestPauseGap() ?? 'no lifecycle control was bound'}.`,
38
+ );
39
+ }
40
+ editorConsole.log(
41
+ `Ingest "${active.worldId}" is HELD — content time does not advance in Edit until ▶`,
42
+ 'ingest',
43
+ );
44
+ }
45
+ store.setActiveViewportTab('edit');
46
+ const open = openWorkspaceDocuments();
47
+ const defaultId = projectAdapterFacet()?.scenes.default;
48
+ const isolationId = defaultId ? sceneDocumentId(defaultId) : undefined;
49
+ const scene =
50
+ (isolationId ? open.find((d) => d.descriptor.id === isolationId) : undefined) ??
51
+ open.find((d) => d.descriptor.id === CANVAS_SCENE_DOCUMENT_ID) ??
52
+ open.find((d) => d.descriptor.id === SCENE_DOCUMENT_ID);
53
+ if (scene) activateWorkspaceDocument(scene.descriptor.id);
54
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * THE Scene tab for a canvas ingest.
3
+ *
4
+ * First-party canvas worlds get `workspace:canvas-scene` from
5
+ * `installRootDocuments`, which reads design-time layer candidates. An
6
+ * `{ ingest }` root is excluded from that list on purpose — remounting the
7
+ * game as a first-party `@pixi/react` world is a category error. The result
8
+ * was a workspace with only `workspace:game`: the live tree existed, and
9
+ * nothing presented it as a Scene.
10
+ *
11
+ * This module is the ingest half of that document. It opens the same pinned
12
+ * Scene id, shows the already-mounted game through the Scene chrome (editor
13
+ * camera backdrop + selection), and reparents the realm host into the Scene
14
+ * pane while that tab is in front. Game keeps the host the rest of the time.
15
+ */
16
+
17
+ import {
18
+ getMountFailureReports,
19
+ subscribeToMountFailures,
20
+ } from '@volter/editor-core/authoring/mount-failure-report';
21
+ import { installCanvasSceneNavigation } from '@volter/editor-core/authoring/react-canvas-navigation';
22
+ import { createRootViewController } from '@volter/editor-core/authoring/world-pan-state';
23
+ import {
24
+ CANVAS_SCENE_BACKGROUND,
25
+ CanvasSceneBackdrop,
26
+ CanvasSceneControls,
27
+ } from '@volter/editor-core/components/CanvasSceneViewport';
28
+ import { RootSelectionOverlay } from '@volter/editor-core/components/RootSelectionOverlay';
29
+ import { SurfaceStateOverlay } from '@volter/editor-core/components/SurfaceStateOverlay';
30
+ import type { EditorShellStore } from '@volter/editor-core/editor-shell-store';
31
+ import { liveDocumentContainer } from '@volter/editor-core/live-document';
32
+ import { projectAdapterFacet } from '@volter/editor-core/project-adapter';
33
+ import { readinessFacet, subscribeRootReadiness } from '@volter/editor-core/readiness';
34
+ import { explainSurface } from '@volter/editor-core/surface-state';
35
+ import {
36
+ registerWorkspaceDocumentSelection,
37
+ type WorkspaceDocumentContentProps,
38
+ } from '@volter/editor-core/workspace-document-registry';
39
+ import type { AuthoringAdapter } from '@volter/editor-project/adapter';
40
+ import { useEffect, useRef, useSyncExternalStore } from 'react';
41
+ import { activeIngest } from './active-ingest';
42
+ import { activeContractScenes } from './active-scene-navigation';
43
+ import { installIngestCanvasSceneDocument } from './ingest-canvas-scene';
44
+
45
+ /** The boot screen's contract id: the live `list()` entry whose label is the
46
+ * table default's own class name (`TitleScreen` → `title`). */
47
+ function defaultContractSceneId(): string | undefined {
48
+ const table = projectAdapterFacet()?.scenes;
49
+ if (!table?.default) return undefined;
50
+ const def = table.entries.find((entry) => entry.id === table.default);
51
+ const name = def?.source?.export ?? def?.id;
52
+ if (!name) return undefined;
53
+ const listed = activeContractScenes()?.storiesFor('') ?? [];
54
+ return listed.find((scene) => scene.label === name || scene.id === name)?.id;
55
+ }
56
+
57
+ export function IngestCanvasSceneContent({
58
+ active,
59
+ adapter,
60
+ documentId,
61
+ store,
62
+ worldId,
63
+ contractSceneId,
64
+ }: WorkspaceDocumentContentProps & {
65
+ adapter: AuthoringAdapter;
66
+ store: EditorShellStore;
67
+ worldId: string;
68
+ /** When set, activating this tab navigates the held ingest to that screen. */
69
+ contractSceneId?: string;
70
+ }) {
71
+ const containerRef = useRef<HTMLDivElement>(null);
72
+ const viewRef = useRef(createRootViewController());
73
+ const view = viewRef.current;
74
+ useSyncExternalStore(store.subscribe, store.getSnapshot);
75
+ const rootReadiness = useSyncExternalStore(subscribeRootReadiness, readinessFacet);
76
+ const mountFailures = useSyncExternalStore(subscribeToMountFailures, getMountFailureReports);
77
+ const live = activeIngest();
78
+ const roots = adapter.hierarchy.roots();
79
+ const emptyRootIds = new Set(adapter.rects?.emptyContainers?.().map((entry) => entry.id) ?? []);
80
+ const hasRenderableContent = roots.some((root) => !emptyRootIds.has(root.id));
81
+ const surfaceExplanation = explainSurface({
82
+ surface: 'Ingest scene',
83
+ rootIds: [worldId],
84
+ phase:
85
+ live?.kind === 'canvas' && live.worldId === worldId && live.session.hostEl
86
+ ? 'ready'
87
+ : 'loading',
88
+ content:
89
+ live?.kind !== 'canvas' || live.worldId !== worldId || !live.session.hostEl
90
+ ? 'unknown'
91
+ : hasRenderableContent
92
+ ? 'present'
93
+ : 'empty',
94
+ readiness: rootReadiness,
95
+ failures: mountFailures,
96
+ });
97
+
98
+ useEffect(() => {
99
+ return registerWorkspaceDocumentSelection(documentId, () => ({
100
+ adapter,
101
+ nodeId: [...store.selectedEntityIds].find((id) => adapter.hierarchy.node(id)) ?? null,
102
+ }));
103
+ }, [adapter, documentId, store]);
104
+
105
+ useEffect(() => {
106
+ const container = containerRef.current;
107
+ if (!container) return;
108
+ return installCanvasSceneNavigation(container, view);
109
+ }, [view]);
110
+
111
+ useEffect(() => {
112
+ if (!active) return;
113
+ const container = containerRef.current;
114
+ const live = activeIngest();
115
+ const hostEl = live?.kind === 'canvas' ? live.session.hostEl : undefined;
116
+ if (!container || !hostEl) return;
117
+ container.appendChild(hostEl);
118
+ return () => {
119
+ const gameContainer = liveDocumentContainer();
120
+ if (gameContainer && hostEl.parentElement === container) {
121
+ gameContainer.appendChild(hostEl);
122
+ }
123
+ };
124
+ }, [active]);
125
+
126
+ useEffect(() => {
127
+ if (!active) return;
128
+ const sceneId = contractSceneId ?? defaultContractSceneId();
129
+ if (!sceneId) return;
130
+ const scenes = activeContractScenes();
131
+ if (!scenes) return;
132
+ void scenes.goToScene(sceneId);
133
+ }, [active, contractSceneId]);
134
+
135
+ return (
136
+ <div
137
+ ref={containerRef}
138
+ data-testid="world-document:ingest-canvas-scene"
139
+ data-vgai-backdrop-color={active ? CANVAS_SCENE_BACKGROUND : undefined}
140
+ data-vgai-backdrop-policy={active ? 'dark-frost' : undefined}
141
+ style={{
142
+ position: 'absolute',
143
+ inset: 0,
144
+ overflow: 'hidden',
145
+ pointerEvents: 'auto',
146
+ backgroundColor: CANVAS_SCENE_BACKGROUND,
147
+ touchAction: 'none',
148
+ }}
149
+ >
150
+ <CanvasSceneBackdrop view={view} />
151
+ <RootSelectionOverlay adapter={adapter} view={view} transformModeAware />
152
+ <SurfaceStateOverlay
153
+ explanation={surfaceExplanation}
154
+ testId="ingest-canvas-surface-status"
155
+ style={{ zIndex: 20 }}
156
+ />
157
+ <CanvasSceneControls
158
+ active={active}
159
+ adapter={adapter}
160
+ containerRef={containerRef}
161
+ view={view}
162
+ />
163
+ </div>
164
+ );
165
+ }
166
+
167
+ /** Open the pinned Scene tab over the live canvas ingest, or no-op. */
168
+ export function installIngestCanvasScene(store: EditorShellStore): () => void {
169
+ const live = activeIngest();
170
+ if (live?.kind !== 'canvas' || !live.session.adapter) return () => {};
171
+ const adapter = live.session.adapter;
172
+ const worldId = live.worldId;
173
+ const Content = (props: WorkspaceDocumentContentProps) => (
174
+ <IngestCanvasSceneContent {...props} adapter={adapter} store={store} worldId={worldId} />
175
+ );
176
+ return installIngestCanvasSceneDocument(store, Content);
177
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Open/close the pinned canvas Scene document for a live canvas ingest.
3
+ * The React body lives in `ingest-canvas-scene-document.tsx` so this
4
+ * installer can be unit-tested without pulling Pixi/Three into jsdom.
5
+ */
6
+
7
+ import type { EditorShellStore } from '@volter/editor-core/editor-shell-store';
8
+ import { projectAdapterFacet } from '@volter/editor-core/project-adapter';
9
+ import { isolationTabsReplaceGenericScene } from '@volter/editor-core/scene-document-plan';
10
+ import { CANVAS_SCENE_DOCUMENT_ID } from '@volter/editor-core/workspace-document-ids';
11
+ import {
12
+ activateWorkspaceDocument,
13
+ closeWorkspaceDocument,
14
+ openWorkspaceDocument,
15
+ type WorkspaceDocumentContentProps,
16
+ } from '@volter/editor-core/workspace-document-registry';
17
+ import { registerRootDocumentRoute } from '@volter/editor-core/world-document-routing';
18
+ import type { ReactNode } from 'react';
19
+ import { activeIngest } from './active-ingest';
20
+
21
+ export function installIngestCanvasSceneDocument(
22
+ store: EditorShellStore,
23
+ Content: (props: WorkspaceDocumentContentProps) => ReactNode,
24
+ ): () => void {
25
+ const live = activeIngest();
26
+ if (live?.kind !== 'canvas' || !live.session.adapter) return () => {};
27
+ const table = projectAdapterFacet()?.scenes;
28
+ // Isolation tabs ARE Edit. The live host is Play's Game document.
29
+ if (table && isolationTabsReplaceGenericScene(table)) return () => {};
30
+ const worldId = live.worldId;
31
+
32
+ openWorkspaceDocument({
33
+ id: CANVAS_SCENE_DOCUMENT_ID,
34
+ title: 'Scene',
35
+ kind: 'world',
36
+ workspaceRole: 'authored-subject',
37
+ provenance: { rootId: worldId },
38
+ Content,
39
+ closeable: false,
40
+ presentation: () => ({ kind: 'world', id: worldId }),
41
+ onActivate: () => store.setActiveViewportTab('edit'),
42
+ });
43
+ activateWorkspaceDocument(CANVAS_SCENE_DOCUMENT_ID);
44
+ const unregisterRoute = registerRootDocumentRoute(worldId, CANVAS_SCENE_DOCUMENT_ID);
45
+
46
+ return () => {
47
+ unregisterRoute();
48
+ closeWorkspaceDocument(CANVAS_SCENE_DOCUMENT_ID, { discardDirty: true });
49
+ };
50
+ }