@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,899 @@
1
+ /**
2
+ * Mount a `{ ingest }` root on the CANVAS surface — the sibling of
3
+ * `mount-three-ingest-root.ts` on the canvas substrate.
4
+ *
5
+ * `resolveIngest2DDescriptor` (ingest/resolve-canvas.ts) builds an `IngestGame2D`
6
+ * straight from a canvas-surface manifest root, and `mountIngestGame2D` runs
7
+ * the game's own modules in the editor's realm with the Pixi render trap
8
+ * installed. A canvas root says `canvas`; WHICH library draws on it is a fact
9
+ * about the live realm, never a manifest claim, so the mount RECOGNIZES it:
10
+ *
11
+ * - the host's pixi captured a frame -> the live canvas authoring adapter;
12
+ * - otherwise `authoring/canvas-runtime-recognition.ts` reads the runtimes'
13
+ * own public registries -> `PhaserLiveAuthoringAdapter` /
14
+ * `BabylonAuthoringAdapter`;
15
+ * - nothing recognized -> the mount FAILS BY NAME.
16
+ *
17
+ * Every branch installs the SAME full `AuthoringAdapter` contract — no
18
+ * separate editor data model, no fallback. A
19
+ * runtime the host cannot recognize is never presented as one it is not, nor
20
+ * as an editable tree the editor fabricated.
21
+ */
22
+
23
+ import { measureAdapterReach } from '../host/adapter-reach';
24
+ import { nextPaint } from '../host/after-paint';
25
+ import { setActiveAuthoring } from '@volter/editor-core/authoring/active-adapter';
26
+ import { setActiveSystems } from '@volter/editor-core/authoring/active-systems';
27
+ import { BabylonAuthoringAdapter } from '../host/authoring/babylon-authoring-adapter';
28
+ import {
29
+ type BabylonRealmLike,
30
+ findBabylonEngine,
31
+ findPhaserGame,
32
+ type PhaserRealmLike,
33
+ } from '../host/authoring/canvas-runtime-recognition';
34
+ import {
35
+ type ContractScenesStories,
36
+ createContractScenesStories,
37
+ } from '../host/authoring/contract-scenes-stories';
38
+ import { clearMountFailureReports } from '@volter/editor-core/authoring/mount-failure-report';
39
+ import { PhaserLiveAuthoringAdapter } from '../host/authoring/phaser-live-authoring-adapter';
40
+ import { PixiAuthoringAdapter } from '../host/authoring/pixi-authoring-adapter';
41
+ import { createCreationSiteCanvasWriteTarget } from '../host/authoring/pixi-creation-site-write-target';
42
+ import { resolveCanvasPixiForEditor } from '../host/canvas-entry-runtime';
43
+ import {
44
+ capturePixiDisplayObjectThumbnail,
45
+ registerPresentedPixiApps,
46
+ } from '@volter/editor-core/canvas-preview-frames';
47
+ import { editorConsole } from '@volter/editor-core/editor-console';
48
+ import type { EditorShellStore } from '@volter/editor-core/editor-shell-store';
49
+ import { GAME_SURFACE_CONTAINMENT_CSS } from '../host/game-realm-page';
50
+ import {
51
+ clearGameSurface,
52
+ gameLoopGate,
53
+ setGameInputGate,
54
+ setGameSurface,
55
+ } from '../host/gated-globals';
56
+ import { authoringJournal } from '../host/history/json-history-resource';
57
+ import { acquireLiveDocument, liveDocumentContainer } from '@volter/editor-core/live-document';
58
+ import { getCurrentProject } from '@volter/editor-core/project-manager';
59
+ import { recordRootReadiness } from '@volter/editor-core/readiness';
60
+ import type { MeasuredLoop } from '../host/same-realm-loop-gate';
61
+ import { projectContractSystemAdapters } from '@volter/game-runtime/adapter/ingest/contract-system-adapters';
62
+ import type { RenderDebugWiring } from '@volter/game-runtime/dev/render-debug-adapter';
63
+ import type { IngestGame2D } from '@volter/game-runtime/pixi/ingest';
64
+ import { INGEST_GAME_2D_LOAD_ERROR_NAME, mountIngestGame2D } from '@volter/game-runtime/pixi/ingest';
65
+ import { createPhysics2DRegistry } from '@volter/game-runtime/pixi/physics-registry';
66
+ import { installPixiRenderPassBracket } from '@volter/game-runtime/pixi/render-pass-bracket';
67
+ import {
68
+ composePhysicsAdapters2D,
69
+ createPhysicsAdapter2D,
70
+ type PhysicsAdapter2D,
71
+ } from '@volter/game-runtime/pixi/system-adapters';
72
+ import type { AuthoringAdapter } from '@volter/editor-project/adapter/authoring';
73
+ import { readGameReady } from '@volter/editor-project/adapter/ingest/game-contract';
74
+ import { displayKeyedPhysics } from '@volter/editor-project/adapter/system-adapter';
75
+ import type { ResolvedAdapterRoot } from '@volter/editor-project/manifest/load';
76
+ import type { Application, Container } from 'pixi.js';
77
+ import { serializeEntry, setActiveIngest } from './active-ingest';
78
+ import { withDetectedDomSurface } from './authoring/ingest-dom-surface-authoring';
79
+ import { setCaptureWait } from './capture-wait-report';
80
+ import { ingestGameRealmWindow, readIngestGameContract } from './game-contract-realm';
81
+ import { repaintHeldSceneSwitch } from './held-scene-repaint';
82
+ import { claimUnresizedSurfaceBox } from './host-surface-box';
83
+ import { landIngestBootInEdit } from './ingest-boot-viewport';
84
+ import { installIngestCanvasScene } from './ingest-canvas-scene-document';
85
+ import { ingestHookEvidence, publishIngestHook } from './ingest-evidence-hook';
86
+ import { bindIngestLifecycle, ingestPlaying } from './ingest-play-control';
87
+ import { wireIngestSystems } from './ingest-render-debug';
88
+ import { LOOP_PROBE_ABSENT, recordMountCoverage } from './mount-coverage';
89
+ import { resolveIngest2DDescriptor } from './resolve-canvas';
90
+ import { getIngestGame2D } from './surface-canvas';
91
+ import { exitActiveIngest } from './unmount-ingest-root';
92
+
93
+ /** How this mount reached the game's runtime, in the words a capability
94
+ * warning hands the reader. Mechanism only — never a grade. */
95
+ const CAPTURE_MECHANISM = "this game imports the host's pixi directly, in the editor's own realm";
96
+
97
+ /** What the pixi branch's render-debug wiring hands back to the mount, so the
98
+ * session's ONE teardown path can end both halves of it. */
99
+ interface CanvasRenderDebugHandle {
100
+ readonly wiring: RenderDebugWiring | null;
101
+ /** Takes the bracket back off the renderer's runners. */
102
+ readonly uninstall: () => void;
103
+ }
104
+
105
+ /**
106
+ * Publish the game's DECLARED system surface — the line this lane was missing.
107
+ *
108
+ * A canvas ingest used to hand `setActiveSystems({})` unconditionally, so
109
+ * `game.commands()`/`game.state()`/`game.waitSimTime()` had nothing to answer
110
+ * for EVERY canvas ingest, including one that declared a full contract. That is
111
+ * not the same as the honest empty: `command-listener.ts` already refuses those
112
+ * calls over a plane-less ingest by naming the missing plane, and that refusal
113
+ * stays exactly right for a game that declares nothing. What was wrong is that
114
+ * a game which DID declare could not be heard — a lane you can see but cannot
115
+ * DRIVE.
116
+ *
117
+ * `wireIngestSystems` (`ingest/ingest-render-debug.ts`) is the ONE assembly
118
+ * point, shared with the three lane, so a shim never learns which surface it is
119
+ * beside and no second assembly can publish a different surface from the same
120
+ * declaration.
121
+ *
122
+ * `renderDebug` is ENGINE-owned on both surfaces — no game declares it — and
123
+ * this lane reaches it through `renderer`: the captured Pixi renderer's own
124
+ * WebGL2 context is what the GL-level frame capture instruments, and its own
125
+ * `prerender`/`postrender` runners are the bracket. Passed `undefined` (the
126
+ * recognized Phaser/Babylon branch, where the host's pixi captured nothing) the
127
+ * slot stays absent, which is the honest answer and not a degraded one.
128
+ */
129
+ function publishCanvasIngestSystems(
130
+ gameName: string,
131
+ renderer: unknown,
132
+ physics: PhysicsAdapter2D | null,
133
+ ): CanvasRenderDebugHandle {
134
+ const contractSystems = readIngestGameContract()?.systems;
135
+ let uninstallBracket: (() => void) | null = null;
136
+ const { systems, renderDebug, debugCollisions } = wireIngestSystems({
137
+ contractSystems,
138
+ surface: 'canvas',
139
+ ...(physics ? { physics } : {}),
140
+ ...(renderer ? { canvasRuntime: { renderer } } : {}),
141
+ // The bracket is this surface's own seam. `wireIngestSystems` decides WHAT
142
+ // goes in the pass; the renderer's runners decide WHEN the pass is.
143
+ setRenderPassHooks: (hooks) => {
144
+ uninstallBracket?.();
145
+ uninstallBracket = hooks ? installPixiRenderPassBracket(renderer, hooks) : null;
146
+ },
147
+ });
148
+ setActiveSystems(systems);
149
+ editorConsole.log(
150
+ contractSystems
151
+ ? `canvas ingest: "${gameName}" declares the vgai game contract — ` +
152
+ `${contractSystems.commands?.length ?? 0} commands, ` +
153
+ `${contractSystems.state?.length ?? 0} state providers`
154
+ : `canvas ingest: "${gameName}" declares no vgai game contract — game.commands()/` +
155
+ 'game.state() have nothing to answer and will refuse by naming that',
156
+ 'ingest',
157
+ );
158
+ // A name two feeders both declare is REFUSED when called, by a coded error
159
+ // naming both. Said here too, because a refusal nobody sees until they call
160
+ // the name is a defect that ships.
161
+ for (const collision of debugCollisions) {
162
+ editorConsole.error(`canvas ingest: "${gameName}" — ${collision}; it will refuse`, 'ingest');
163
+ }
164
+ if (renderer) {
165
+ editorConsole.log(
166
+ renderDebug
167
+ ? `canvas ingest: "${gameName}" render debugging is live — the Frame debugger captures ` +
168
+ "through the game's own Pixi renderer (draws are unattributed: Pixi's batcher " +
169
+ 'merges display objects, so no display object is the author of a draw)'
170
+ : `canvas ingest: "${gameName}" has no render-debug capability — its renderer exposes ` +
171
+ 'no real WebGL2 context, or no prerender/postrender runners to bracket',
172
+ 'ingest',
173
+ );
174
+ }
175
+ return { wiring: renderDebug, uninstall: () => uninstallBracket?.() };
176
+ }
177
+
178
+ /**
179
+ * TWO failures, TWO sentences.
180
+ *
181
+ * `mountIngestGame2D` already distinguishes them — `IngestGame2DLoadError`
182
+ * when the game's own modules never ran, a spent capture window when they ran
183
+ * and nothing drew — and the mount's one catch used to flatten both into the
184
+ * capture trap's words: "the host's pixi captured no frame … or it bundles its
185
+ * own (un-shared) copy of pixi.js". That sentence names a module-IDENTITY
186
+ * defect and sends the reader to `vite-plugin-module-doorways.ts`. MEASURED on
187
+ * a packaged build whose real cause was a JSX-bearing `.js` module this host
188
+ * had no transform for: the console said un-shared pixi, the network said
189
+ * "Failed to fetch dynamically imported module … /src/game.js".
190
+ */
191
+ function unrecognizedCanvasMountMessage(gameId: string, failure: unknown): string {
192
+ if (failure instanceof Error && failure.name === INGEST_GAME_2D_LOAD_ERROR_NAME) {
193
+ return (
194
+ `canvas ingest: game "${gameId}" never ran — its own modules failed to load, so nothing ` +
195
+ `could render and there is no canvas runtime in the realm to recognize (${failure})`
196
+ );
197
+ }
198
+ return (
199
+ `canvas ingest: game "${gameId}" mounted no recognized canvas runtime — the host's pixi ` +
200
+ 'captured no frame, and the realm holds no Phaser game or Babylon.js engine on their ' +
201
+ `own public registries either (${failure})`
202
+ );
203
+ }
204
+
205
+ function rendererCanvasBelongsToHost(renderer: unknown, hostEl: HTMLElement): boolean {
206
+ const canvas = (renderer as { canvas?: unknown } | null)?.canvas;
207
+ return canvas instanceof Node && hostEl.contains(canvas);
208
+ }
209
+
210
+ /**
211
+ * The physics carrier THIS GAME declared, in the canvas surface's own
212
+ * (Container-keyed) vocabulary — or `null` when it declared none.
213
+ *
214
+ * The projection is the ONE validator: presence + typeof for the four members,
215
+ * plus the keying check that makes a node-id-keyed declaration on this surface
216
+ * `malformed` by name instead of a bound shape nothing here can call
217
+ * (`@volter/game-runtime/adapter/ingest/contract-system-adapters`). Both verdicts reach
218
+ * the reader through the coverage row for `system.physics`, so a refusal here
219
+ * is never silent.
220
+ */
221
+ function declaredCanvasPhysics(): PhysicsAdapter2D | null {
222
+ return displayKeyedPhysics(
223
+ projectContractSystemAdapters(readIngestGameContract()?.systems, 'canvas').bound.physics,
224
+ );
225
+ }
226
+
227
+ /** A canvas runtime the host RECOGNIZED in the live realm, with the structural
228
+ * authoring adapter that projects it. Nothing here is derived from the
229
+ * manifest, the mount route or the game's id. */
230
+ interface RecognizedCanvasRuntime {
231
+ readonly runtime: 'phaser' | 'babylon';
232
+ readonly adapter: AuthoringAdapter & { dispose(): void; refresh(): unknown };
233
+ /** Words a capability warning hands the reader — mechanism only. */
234
+ readonly mechanism: string;
235
+ }
236
+
237
+ /**
238
+ * Ask the REALM which non-pixi canvas runtime it actually contains, through
239
+ * `authoring/canvas-runtime-recognition.ts`'s reads of the runtimes' own public
240
+ * registries. `null` means the host recognizes nothing here — which fails the
241
+ * mount by name at the call site.
242
+ */
243
+ function recognizeStructuralCanvasRuntime(
244
+ store: EditorShellStore,
245
+ realm: PhaserRealmLike & BabylonRealmLike,
246
+ ): RecognizedCanvasRuntime | null {
247
+ const phaserGame = findPhaserGame(realm);
248
+ if (phaserGame) {
249
+ return {
250
+ runtime: 'phaser',
251
+ adapter: new PhaserLiveAuthoringAdapter(phaserGame, store),
252
+ mechanism:
253
+ "this game runs its own Phaser instance in the editor's own realm; the host reads " +
254
+ 'that live game’s active scenes and display lists',
255
+ };
256
+ }
257
+ const babylonEngine = findBabylonEngine(realm);
258
+ if (babylonEngine) {
259
+ return {
260
+ runtime: 'babylon',
261
+ adapter: new BabylonAuthoringAdapter(babylonEngine, store),
262
+ mechanism:
263
+ "this game runs its own Babylon.js engine in the editor's own realm; the host reads " +
264
+ 'that live engine’s scenes and root nodes',
265
+ };
266
+ }
267
+ return null;
268
+ }
269
+
270
+ /**
271
+ * Publish a recognized non-pixi canvas runtime as the live ingest session —
272
+ * the same session/authoring/coverage/hook handoff the pixi branch performs,
273
+ * over that runtime's own structural adapter.
274
+ *
275
+ * Pause runs through the same-realm loop gate (`gated-globals.ts` installs it
276
+ * over the game module's scheduling entry points), which is a real hold rather
277
+ * than a button that lies. No probe has measured that hold on this route, so
278
+ * the loop verdict stays `null` — unmeasured is its own answer.
279
+ */
280
+ function publishStructuralCanvasRuntime(
281
+ store: EditorShellStore,
282
+ game: IngestGame2D,
283
+ sessionId: string,
284
+ hostEl: HTMLElement,
285
+ recognized: RecognizedCanvasRuntime,
286
+ ): void {
287
+ const rawAuthoring = recognized.adapter;
288
+ rawAuthoring.refresh();
289
+ const adapter = withDetectedDomSurface({
290
+ primary: rawAuthoring,
291
+ worldId: sessionId,
292
+ hostEl,
293
+ store,
294
+ });
295
+
296
+ const gate = gameLoopGate();
297
+ const loopFacts = (): { loop: MeasuredLoop | null; reason: string } =>
298
+ gate
299
+ ? {
300
+ loop: null,
301
+ reason:
302
+ 'the same-realm loop gate holds this game’s scheduling, but no probe has measured ' +
303
+ 'that control on this route',
304
+ }
305
+ : { loop: null, reason: LOOP_PROBE_ABSENT };
306
+ const lifecycle = bindIngestLifecycle(
307
+ gate
308
+ ? {
309
+ setPaused: (paused) => {
310
+ if (paused) gate.hold();
311
+ else gate.release();
312
+ },
313
+ }
314
+ : {
315
+ pauseGap: `${recognized.runtime} exposes no host-controlled loop gate`,
316
+ },
317
+ );
318
+
319
+ let disposeScene = () => {};
320
+ setActiveIngest({
321
+ kind: 'canvas',
322
+ session: {
323
+ store,
324
+ adapter: rawAuthoring,
325
+ hostEl,
326
+ lifecycle,
327
+ loop: loopFacts,
328
+ // The ONE teardown path for the surface this branch inherited (see the
329
+ // ownership block in `mountCanvasIngestRootInner`).
330
+ dispose: () => {
331
+ disposeScene();
332
+ rawAuthoring.dispose();
333
+ clearGameSurface();
334
+ hostEl.remove();
335
+ },
336
+ },
337
+ siblings: [],
338
+ worldId: sessionId,
339
+ });
340
+ disposeScene = installIngestCanvasScene(store);
341
+
342
+ setGameInputGate(() => store.playState === 'playing' && store.activeViewportTab === 'play');
343
+ setActiveAuthoring(adapter);
344
+ // No captured Pixi renderer on this branch — the host's pixi drew nothing, so
345
+ // there is no context to instrument, no runner pair to bracket, and no
346
+ // host-owned registry carrier to compose with either: what the game itself
347
+ // declared is the only physics this route can have.
348
+ publishCanvasIngestSystems(game.name, undefined, declaredCanvasPhysics());
349
+ landIngestBootInEdit(store);
350
+ store.notifyIngestEdit();
351
+ clearMountFailureReports();
352
+
353
+ recordMountCoverage({
354
+ reach: measureAdapterReach({ authoring: adapter }),
355
+ reachMechanism: recognized.mechanism,
356
+ loop: loopFacts,
357
+ });
358
+
359
+ editorConsole.log(
360
+ `canvas ingest: recognized ${recognized.runtime} in the realm and captured "${game.name}"`,
361
+ 'ingest',
362
+ );
363
+
364
+ publishIngestHook('__vgaiIngest2D', {
365
+ gameId: sessionId,
366
+ ...ingestHookEvidence(adapter),
367
+ runtime: recognized.runtime,
368
+ // Live for the same reason the pixi branch's is: the game keeps building
369
+ // its tree after the mount, so a snapshot goes stale immediately.
370
+ get reflected() {
371
+ return rawAuthoring.refresh();
372
+ },
373
+ ...(lifecycle.setPaused ? { setPaused: lifecycle.setPaused } : {}),
374
+ reach: measureAdapterReach({ authoring: adapter }),
375
+ adapter,
376
+ store,
377
+ });
378
+ }
379
+
380
+ async function mountCanvasIngestRootInner(
381
+ store: EditorShellStore,
382
+ game: IngestGame2D,
383
+ sessionId: string,
384
+ ): Promise<void> {
385
+ // D-V1: tear down WHATEVER kind is currently active, not just a prior
386
+ // pixi session — closes the cross-kind leak (F26 residual).
387
+ exitActiveIngest();
388
+
389
+ // The Game document exists only while a runtime does, so install it (and
390
+ // wait for its panel to commit) before reading the container it owns.
391
+ await acquireLiveDocument();
392
+ const gameContainer = liveDocumentContainer();
393
+ if (!gameContainer) {
394
+ editorConsole.error('Game container not mounted — cannot ingest (canvas)', 'ingest');
395
+ return;
396
+ }
397
+
398
+ store.setPlayState('playing');
399
+ editorConsole.log(`Ingesting unmodified PixiJS game: ${game.name}`, 'ingest');
400
+
401
+ // PD-3: READINESS, RECORDED. The three lane's self-booting mount has always
402
+ // written this row (`authoring/ingest-root-adapter.ts`); the canvas lane
403
+ // never did, so a Pixi ingest was simply MISSING from the `readiness` facet —
404
+ // and a missing row is indistinguishable from a root nobody has asked about,
405
+ // which is the exact ambiguity the facet exists to remove.
406
+ //
407
+ // The mechanism is read the same way the three lane reads it, off the game's
408
+ // OWN contract, because `ReadinessMechanism` names HOW readiness is answered
409
+ // and there are exactly three answerers (see `readiness.ts`'s header). A lane
410
+ // name would be a fourth value on a different axis — which root mounted it is
411
+ // already knowable from the root itself.
412
+ const declaredReady = readGameReady(readIngestGameContract());
413
+ if (declaredReady.malformed !== null) {
414
+ // Same sentence the three lane says (`ingest-root-adapter.ts`): a broken
415
+ // declaration is loud, and the host measures instead.
416
+ editorConsole.error(
417
+ `Ingest game "${game.id}" ${declaredReady.malformed} — the host is measuring readiness ` +
418
+ 'instead.',
419
+ 'ingest',
420
+ );
421
+ }
422
+ const readinessMechanism = declaredReady.ready === null ? 'measured-wait' : 'contract-ready';
423
+ const readinessSource = declaredReady.ready === null ? 'measured' : 'declared';
424
+ recordRootReadiness({
425
+ rootId: game.id,
426
+ mechanism: readinessMechanism,
427
+ source: readinessSource,
428
+ state: 'waiting',
429
+ });
430
+
431
+ // ## RESOURCE OWNERSHIP — the host surface for a canvas ingest
432
+ //
433
+ // OWNER: this mount. SHARERS: the game's realm (it sees `hostEl` as
434
+ // `document.body`, via `setGameSurface`) and the pixel doors (they read the
435
+ // game's `Application` through `registerPresentedPixiApps`). ONE teardown
436
+ // path: the session's `dispose()` below, which unregisters the app, clears
437
+ // the realm surface and removes `hostEl` — removing the surface removes the
438
+ // game's ENTIRE DOM with it. The two early-exit paths (a recognized non-pixi
439
+ // runtime, and nothing recognized) hand that same teardown on or run it
440
+ // before throwing; nothing else may end this surface.
441
+ //
442
+ // THE GAME'S PAGE IS THIS BOX, from before its first module runs — the exact
443
+ // adoption the three ingest mount performs
444
+ // (`authoring/ingest-root-adapter.ts`, `game-realm-page.ts` reason 1), and
445
+ // its absence here is why an ingested Pixi game's canvas landed at PAGE
446
+ // level, below the editor's fold: MEASURED on the `flappy` fixture, a
447
+ // `body > canvas` at y=915 while the Game pane held an empty 300x150
448
+ // placeholder — so every pixel door photographed the placeholder and every
449
+ // one of them was honestly blank.
450
+ //
451
+ // The `background` is the LETTERBOX. Because this lane never re-resolves the
452
+ // game's renderer, a game whose aspect differs from the pane's leaves margins
453
+ // (`ingest/host-surface-box.ts`), and margins with no background are alpha-0
454
+ // in every capture — a region whose appearance is whatever the viewer happens
455
+ // to composite it over, and which reads as 64% #000000 to the frame's own
456
+ // flatness check. Black is both the letterbox convention and an answer the
457
+ // screen and the photograph agree on. It is a DEFAULT, not a claim: this
458
+ // element is the game's `document.body` in-realm, so a game that paints its
459
+ // own page background still wins.
460
+ const hostEl = document.createElement('div');
461
+ hostEl.id = 'container';
462
+ hostEl.style.cssText = `position:absolute;inset:0;width:100%;height:100%;overflow:hidden;background:#000;${GAME_SURFACE_CONTAINMENT_CSS}`;
463
+ gameContainer.appendChild(hostEl);
464
+ setGameSurface(hostEl);
465
+ // Same scoped-CSS contract the three lane holds (ingest-root-adapter.ts):
466
+ // the surface is the scope root, and the project's declared page sheet is
467
+ // installed BEFORE the game's entry runs so its first paint is styled.
468
+ // Measured missing here: babylon-space-truckers' declared canvas{100%}
469
+ // sizing never loaded on the canvas lane, leaving the game a default
470
+ // 300x150 canvas in the pane corner (2026-08-28).
471
+ {
472
+ const { markGameCssScope } = await import('@volter/editor-sdk/session/game-css-scope');
473
+ markGameCssScope(hostEl);
474
+ const scopedCssProject = getCurrentProject();
475
+ if (scopedCssProject) {
476
+ const { ensureScopedGameStyles } = await import('@volter/editor-core/scoped-game-css');
477
+ await ensureScopedGameStyles(scopedCssProject.rootPath);
478
+ }
479
+ }
480
+
481
+ // F26: same cold-mount announcement the three adapter makes
482
+ // (ingest-root-adapter.ts) — a pixi game may declare the game contract and
483
+ // defer its session until ▶; games that ignore the flag behave as before.
484
+ (window as unknown as { __vgaiMountCold?: boolean }).__vgaiMountCold = true;
485
+ // THE namespace the game's own modules will resolve `pixi.js` to.
486
+ //
487
+ // `installSceneCapture2D` wraps `Application.prototype.render` on the
488
+ // namespace it is handed, and the engine states the invariant in its own
489
+ // header: the trap must be installed on the SAME `pixi.js` module instance
490
+ // the game uses, or the game "cannot be captured at all, and its mount fails
491
+ // by name". Under the packaged runtime the game's entry is served as
492
+ // `/@fs/<project>/…` by the PROJECT-rooted Vite (the only Vite there — the
493
+ // editor's own shell is a prebuilt static bundle), so the game's `pixi.js`
494
+ // is the project's and a shell namespace traps a class nobody calls.
495
+ // MEASURED with the shell's: the `bunnymark` ingest fixture opened on a
496
+ // packaged build failed by name after 8s of visible time, quoting that same
497
+ // invariant back — "the game never rendered, or it bundles its own
498
+ // (un-shared) copy of pixi.js".
499
+ const pixi = await resolveCanvasPixiForEditor();
500
+ let mount: Exclude<Awaited<ReturnType<typeof mountIngestGame2D>>, null>;
501
+ try {
502
+ const candidate = await mountIngestGame2D(pixi, game, {
503
+ ...(game.captureTimeoutMs !== undefined ? { captureTimeoutMs: game.captureTimeoutMs } : {}),
504
+ // The editor can have several live Pixi Applications at once: prefab and
505
+ // screen previews render beside the game. The realm redirects the game's
506
+ // `document.body` into this host, so canvas containment is the honest
507
+ // ownership discriminator and prevents a preview's earlier frame from
508
+ // being mistaken for this ingest root.
509
+ acceptCapture: ({ renderer }) => rendererCanvasBelongsToHost(renderer, hostEl),
510
+ // The same writer the three lane installs (`ingest-root-adapter.ts`): the
511
+ // capture window is a budget of VISIBLE time and PARKS on a hidden tab, so
512
+ // without this the wait reads as a hung mount at every door.
513
+ onWait: (wait) => setCaptureWait(game.id, wait),
514
+ // A classic/module Babylon bundle cannot render through the host's Pixi
515
+ // namespace. When its carrier publishes a boot promise, await that
516
+ // concrete signal and dispatch structurally without first burning a
517
+ // visible-time Pixi timeout (which can remain parked forever in an
518
+ // unfocused editor window).
519
+ preferStructuralRuntime: async () => {
520
+ const realm = ingestGameRealmWindow() as unknown as PhaserRealmLike & BabylonRealmLike;
521
+ await realm.__vgaiBabylon?.ready;
522
+ return recognizeStructuralCanvasRuntime(store, realm) !== null;
523
+ },
524
+ });
525
+ if (candidate === null) {
526
+ const recognized = recognizeStructuralCanvasRuntime(
527
+ store,
528
+ ingestGameRealmWindow() as unknown as PhaserRealmLike & BabylonRealmLike,
529
+ );
530
+ if (recognized) {
531
+ publishStructuralCanvasRuntime(store, game, sessionId, hostEl, recognized);
532
+ return;
533
+ }
534
+ throw new Error(
535
+ `canvas ingest: game "${game.id}" reported a structural runtime, but it disappeared before dispatch`,
536
+ );
537
+ }
538
+ mount = candidate;
539
+ } catch (pixiFailure) {
540
+ // The host's pixi drew no capturable frame — but the game's OWN modules
541
+ // have already run (`mountIngestGame2D` calls `game.load()` before it waits
542
+ // for a frame), so the realm is now the place to ask what this game
543
+ // actually is. Recognized -> that runtime's structural authoring adapter,
544
+ // the same full contract the pixi branch installs. Recognized nothing ->
545
+ // fail by name below.
546
+ //
547
+ // `hostEl` STAYS: the game already appended its own DOM into it while it
548
+ // was the realm's page, so tearing it down here would strand a recognized
549
+ // runtime's canvas exactly the way the pixi branch used to be stranded.
550
+ // The recognized branch inherits the teardown; the unrecognized one runs
551
+ // it before throwing.
552
+ const recognized = recognizeStructuralCanvasRuntime(
553
+ store,
554
+ ingestGameRealmWindow() as unknown as PhaserRealmLike & BabylonRealmLike,
555
+ );
556
+ if (recognized) {
557
+ publishStructuralCanvasRuntime(store, game, sessionId, hostEl, recognized);
558
+ return;
559
+ }
560
+ clearGameSurface();
561
+ hostEl.remove();
562
+ // The row must not be left at `waiting` for a mount that is over: a parked
563
+ // wait and a dead one are the two states this facet exists to tell apart.
564
+ recordRootReadiness({
565
+ rootId: game.id,
566
+ mechanism: readinessMechanism,
567
+ source: readinessSource,
568
+ state: 'failed',
569
+ // Same split the three lane makes: a game that DECLARED ready and never
570
+ // produced a capturable render is a different defect from one that
571
+ // declared nothing and whose measured fallback simply expired.
572
+ failure:
573
+ declaredReady.ready === null ? 'no-readiness-declaration' : 'declared-ready-never-resolved',
574
+ });
575
+ throw new Error(unrecognizedCanvasMountMessage(game.id, pixiFailure));
576
+ }
577
+
578
+ const stage = mount.stage as Container;
579
+ // The game's async setup may add display objects after its first captured frame —
580
+ // wait for the stage to settle. `nextPaint()` rather than a bare rAF because
581
+ // this mount is awaited by `vgai play`: in a hidden tab (every agent-worktree
582
+ // `vgai edit` opens one — see `after-paint.ts`) no frame ever arrives, so a
583
+ // bare chain hangs this bounded loop on iteration 1 and play stalls in its
584
+ // boot phase with nothing to report.
585
+ for (let i = 0; i < 90 && (stage.children?.length ?? 0) === 0; i++) {
586
+ await nextPaint();
587
+ }
588
+
589
+ // The captured `Application` — the game's own, trapped on its first render
590
+ // (`@volter/game-runtime/pixi/scene-capture`). Two things hang off it, and neither is
591
+ // available anywhere else.
592
+ const app = mount.capture.captured?.app as Application | undefined;
593
+
594
+ // (1) ADOPT ITS CANVAS. A game that appended into the realm's page is already
595
+ // inside `hostEl`; one whose module was not realm-shadowed (an in-tree
596
+ // fixture, imported through the editor's own bundle) still has its canvas
597
+ // wherever it put it. Reparent either way, and claim its CSS box — a game
598
+ // sizes its canvas in literal pixels while it believes it owns the window,
599
+ // and nothing else ever un-stamps that. The box is the UNRESIZED policy, not
600
+ // the three lane's fill: this mount never re-resolves the game's renderer, so
601
+ // filling would stretch it (`ingest/host-surface-box.ts` states both, and why
602
+ // they differ).
603
+ const gameCanvas = app?.canvas as HTMLCanvasElement | undefined;
604
+ if (gameCanvas) {
605
+ claimUnresizedSurfaceBox(gameCanvas);
606
+ if (gameCanvas.parentElement !== hostEl) hostEl.appendChild(gameCanvas);
607
+ }
608
+
609
+ // (2) MAKE IT PHOTOGRAPHABLE. A Pixi canvas is created without
610
+ // `preserveDrawingBuffer`, so a compositor reading it after paint gets
611
+ // nothing — the measured blank-capture defect `stories/story-pixi-preview.ts`
612
+ // records for the story lane and the canvas board. The same registry is the
613
+ // answer here: the pixel doors ask `presentedPixiFrame` for a registered
614
+ // canvas and it extracts through Pixi's own renderer instead. Registered here
615
+ // for the same reason the first-party canvas root registers at ITS mount
616
+ // (`authoring/canvas-design-mount.ts`) — an ingested game and a first-party
617
+ // canvas world are the same surface, and the doors must not be able to tell
618
+ // them apart.
619
+ const unregisterPresentedApp = registerPresentedPixiApps(app ? [app] : []);
620
+
621
+ // TWO possible owners of a display object's pose, and they own DIFFERENT
622
+ // objects: the host's registry-backed carrier answers for bodies the editor
623
+ // itself created, and the game's declared carrier answers for the ones its
624
+ // own simulation drives. The registry is EMPTY on this route (an ingested
625
+ // game never registers into it), which is exactly why handing the write
626
+ // target that one alone left freeze/commit a no-op on every ingest — a gizmo
627
+ // drag over a simulated body was stomped by the game's next frame with
628
+ // nothing anywhere saying so.
629
+ //
630
+ // Composed ownership-first (`composePhysicsAdapters2D`): the declared carrier
631
+ // is asked first because it is the one with a real claim here, and the
632
+ // registry stays as the fallback rather than being dropped, so a host-created
633
+ // body keeps behaving exactly as before.
634
+ const declaredPhysics = declaredCanvasPhysics();
635
+ const registryPhysics = createPhysicsAdapter2D(createPhysics2DRegistry());
636
+ const physicsAdapter = declaredPhysics
637
+ ? composePhysicsAdapters2D([declaredPhysics, registryPhysics])
638
+ : registryPhysics;
639
+ // Every authored edit writes the GAME'S OWN SOURCE, at the line that
640
+ // constructed the object — the canvas half of ARCHITECTURE-CORE §Editor's
641
+ // ingest-authoring model, and the same destination the three ingest lane has
642
+ // (`structuralThree`). There is deliberately no branch here on whether a
643
+ // write is reachable: the backend answers that per edit, in the sentence the
644
+ // user reads, and an edit it cannot honestly anchor stays live-only WITH ITS
645
+ // REASON rather than falling back to a destination that is not the game's.
646
+ // The game's own declared SCENES, in the editor's stories vocabulary — the
647
+ // screens this game swaps between ARE its design-time states
648
+ // (`authoring/contract-scenes-stories.ts` states why the projection is
649
+ // world-level and why it omits `isolate`). `null` for a game that declared
650
+ // none, which the adapter passes on as an absent provider rather than an
651
+ // empty picker.
652
+ //
653
+ // The repaint is THIS MOUNT'S contribution to a scene switch, because it
654
+ // needs what only this mount owns: the captured `Application` to draw with
655
+ // and the hold state to gate on. A held mount's stopped ticker is also its
656
+ // renderer, so without this a scene switch in Edit moves `current()` under a
657
+ // frozen picture (`ingest/held-scene-repaint.ts` carries the measurement and
658
+ // the content-vs-presentation line it walks).
659
+ //
660
+ // Handed in as the projection's SEAM rather than wrapped around the object it
661
+ // returns: the picker's `apply` and the `open` protocol verb's `goToScene`
662
+ // are one path through that module, and a wrapper around the outer member
663
+ // would silently leave the other one unpainted.
664
+ // Annotated because the seam below reads `stories.active` — its own binding,
665
+ // one frame later — which makes the initializer's type circular otherwise.
666
+ const stories: ContractScenesStories | null = createContractScenesStories(
667
+ readIngestGameContract(),
668
+ {
669
+ onSwitchAccepted: (sceneId) =>
670
+ repaintHeldSceneSwitch(sceneId, {
671
+ isHeld: () => !ingestPlaying(),
672
+ currentScene: () => stories?.active('') ?? null,
673
+ render: () => app?.render(),
674
+ schedule: (cb) => requestAnimationFrame(cb),
675
+ }),
676
+ },
677
+ );
678
+ const rawAuthoring = new PixiAuthoringAdapter(stage, store, {
679
+ // The captured stage is the GAME's, so the adapter's namespace is the one
680
+ // the capture trap was installed on.
681
+ pixi,
682
+ // `sessionId` IS the world id (see this function's doc), which is exactly
683
+ // the subject a held surface's undo stack belongs to — it outlives every
684
+ // remount of this game.
685
+ journal: authoringJournal(sessionId),
686
+ target: createCreationSiteCanvasWriteTarget({
687
+ physics: physicsAdapter,
688
+ history: store.projectHistory,
689
+ }),
690
+ ...(stories ? { stories } : {}),
691
+ // The captured game canvas is the screen↔stage mapping `pickable`/`rects`
692
+ // need — the same argument the first-party canvas mount passes
693
+ // (`authoring/canvas-design-mount.ts`). A resolver because a restarted
694
+ // game presents a new canvas under this same adapter; 1:1 is correct here
695
+ // because `claimUnresizedSurfaceBox` above keeps the canvas at its natural
696
+ // CSS size, so stage units are its client pixels.
697
+ surface: () => (app?.canvas as HTMLElement | undefined) ?? null,
698
+ capturePreview: (object, size) => capturePixiDisplayObjectThumbnail(object, { ...size, pixi }),
699
+ });
700
+ const stats = rawAuthoring.refresh();
701
+ const adapter = withDetectedDomSurface({
702
+ primary: rawAuthoring,
703
+ worldId: sessionId,
704
+ hostEl,
705
+ store,
706
+ });
707
+
708
+ // Assigned by `publishCanvasIngestSystems` below and released by the session's
709
+ // ONE teardown path — declared here because that path is written first.
710
+ let renderDebugHandle: CanvasRenderDebugHandle | null = null;
711
+ let disposeScene = () => {};
712
+ const gate = gameLoopGate();
713
+ const lifecycle = bindIngestLifecycle({
714
+ setPaused: (paused) => {
715
+ // The captured ticker and the same-realm scheduling gate are one
716
+ // fallback capability. Holding only either half can freeze the picture
717
+ // while timers keep simulating, or park timers while Pixi keeps drawing.
718
+ mount.setPaused(paused);
719
+ if (paused) gate?.hold();
720
+ else gate?.release();
721
+ },
722
+ });
723
+
724
+ setActiveIngest({
725
+ kind: 'canvas',
726
+ session: {
727
+ store,
728
+ adapter: rawAuthoring,
729
+ hostEl,
730
+ lifecycle,
731
+ // THE one teardown path (see the ownership block above): the pixel doors
732
+ // stop seeing this app, the realm stops standing in for a surface that no
733
+ // longer exists, and removing `hostEl` removes the game's whole DOM. A
734
+ // re-mount therefore starts from nothing — no stale registration can make
735
+ // the next capture read the previous game's canvas.
736
+ dispose: () => {
737
+ // Take the render-pass bracket off the renderer's runners and reject
738
+ // any armed capture BEFORE the game goes, so nothing outlives the mount
739
+ // holding a patched GL context — the same ordering the three lane's
740
+ // dispose uses (`authoring/ingest-root-adapter.ts`).
741
+ disposeScene();
742
+ renderDebugHandle?.uninstall();
743
+ renderDebugHandle?.wiring?.dispose();
744
+ unregisterPresentedApp();
745
+ mount.dispose();
746
+ clearGameSurface();
747
+ hostEl.remove();
748
+ },
749
+ },
750
+ siblings: [],
751
+ worldId: sessionId,
752
+ });
753
+ disposeScene = installIngestCanvasScene(store);
754
+
755
+ // R2c: PixiJS ingest fixtures (`ingest/games/**`) are just as
756
+ // unshadowed-by-default as the three.js ones — drive the same gate as
757
+ // {@link mountThreeIngestRoot} (see that function's doc comment).
758
+ setGameInputGate(() => store.playState === 'playing' && store.activeViewportTab === 'play');
759
+
760
+ setActiveAuthoring(adapter);
761
+ // The captured game's OWN renderer — the one object that carries both halves
762
+ // of render debugging on this surface (its real WebGL2 context, and its
763
+ // published render-pass runners). Nothing else in the editor has it.
764
+ // The composed carrier — the SAME object the write target above drives — is
765
+ // what the bag publishes, and only when the game declared one: publishing the
766
+ // host's empty registry carrier would light a green `system.physics` row over
767
+ // a capability no ingested game has.
768
+ renderDebugHandle = publishCanvasIngestSystems(
769
+ game.name,
770
+ mount.capture.captured?.renderer,
771
+ declaredPhysics ? physicsAdapter : null,
772
+ );
773
+ landIngestBootInEdit(store);
774
+ store.notifyIngestEdit();
775
+ clearMountFailureReports(); // D-W3: mount succeeded
776
+ // PD-3: the mount stood — but `contract-ready` may only be recorded when the
777
+ // declared promise actually ANSWERED. The three lane's `awaitWorldReady`
778
+ // (`authoring/ingest-root-adapter.ts`) is the precedent: a game that declares
779
+ // `window.vgaiGame.ready` is awaited exactly once, and a declared signal that
780
+ // REJECTS does not fail the mount (the frame is already captured) — it
781
+ // degrades to the measured wait, LOUDLY, and the row then reports `measured`,
782
+ // which is the truth about which answer stood. Recording `contract-ready`
783
+ // without consulting the promise — as this path first shipped — attributes
784
+ // the answer to a declaration nobody read.
785
+ let readinessStood: 'declared' | 'measured' = 'measured';
786
+ if (declaredReady.ready) {
787
+ try {
788
+ await declaredReady.ready();
789
+ readinessStood = 'declared';
790
+ } catch (err) {
791
+ editorConsole.error(
792
+ `Ingest game "${game.id}" declared \`window.vgaiGame.ready\` and it REJECTED ` +
793
+ `(${String(err)}) — the frame was already captured, so the host fell back to the ` +
794
+ 'measured settle wait. This root now reports readiness as measured.',
795
+ 'ingest',
796
+ );
797
+ }
798
+ }
799
+ recordRootReadiness({
800
+ rootId: game.id,
801
+ mechanism: readinessStood === 'declared' ? 'contract-ready' : 'measured-wait',
802
+ source: readinessStood,
803
+ state: 'ready',
804
+ });
805
+
806
+ recordMountCoverage({
807
+ reach: measureAdapterReach({ authoring: adapter }),
808
+ reachMechanism: CAPTURE_MECHANISM,
809
+ // The captured Application's real ticker IS the control the play surface
810
+ // above drives — but naming a mechanism is not measuring it, and this used
811
+ // to emit the bare word `'gated'` on the strength of the sentence below.
812
+ // That word is also the manifest's declared intent vocabulary, so the
813
+ // coverage report printed `loop ✓ gated` about a capability nothing had
814
+ // probed: stopping the ticker stops what Pixi drives, and says nothing
815
+ // about a game that simulates on its own `setInterval` — the exact split
816
+ // `same-realm-loop-gate.ts`'s header was written about. No verdict until
817
+ // `verifySameRealmLoopControl` runs on this route.
818
+ loop: () => ({
819
+ loop: null,
820
+ reason:
821
+ 'the host stops and starts the captured PixiJS Application ticker directly, but no ' +
822
+ 'probe has measured whether that holds everything this game schedules',
823
+ }),
824
+ });
825
+
826
+ editorConsole.log(
827
+ `pixi ingest: captured "${game.name}" (${stats.count} nodes, ${stats.sprites} sprites)`,
828
+ 'ingest',
829
+ );
830
+
831
+ publishIngestHook('__vgaiIngest2D', {
832
+ gameId: sessionId,
833
+ ...ingestHookEvidence(adapter),
834
+ drawCount: () => mount.capture.getDrawCount(),
835
+ // Live, for the same reason as the three.js hook: a pixi game keeps
836
+ // adding display objects after its first captured frame (the settle loop
837
+ // right before this only waits for the stage to become NON-EMPTY), so a
838
+ // mount-time snapshot goes stale immediately.
839
+ get reflected() {
840
+ return rawAuthoring.refresh();
841
+ },
842
+ setPaused: lifecycle.setPaused,
843
+ // D-A3: the MEASURED editor reach of a headless-verifiable healthy mount.
844
+ reach: measureAdapterReach({ authoring: adapter }),
845
+ adapter,
846
+ store,
847
+ });
848
+ }
849
+
850
+ /**
851
+ * Mount an already-resolved {@link IngestGame2D} descriptor — the canvas
852
+ * sibling of `mountThreeIngestRoot`. `sessionId` is always the world id — the
853
+ * id this session is known by in logs and the dev hook. Its edits are written
854
+ * into the game's own source at the creation site of the edited object; an edit
855
+ * that cannot be honestly anchored there stays live-only AND SAYS WHY — never a
856
+ * sidecar, never an overlay document.
857
+ *
858
+ * Serialized through the shared entry queue; see `active-ingest.ts`'s
859
+ * `serializeEntry`.
860
+ */
861
+ export function mountCanvasIngestRoot(
862
+ store: EditorShellStore,
863
+ game: IngestGame2D,
864
+ sessionId: string,
865
+ ): Promise<void> {
866
+ return serializeEntry(() => mountCanvasIngestRootInner(store, game, sessionId));
867
+ }
868
+
869
+ /**
870
+ * Mount straight from a manifest's canvas-surface `{ ingest }` root — the
871
+ * canvas sibling of `mountThreeIngestRootFromManifest`. Builds the descriptor
872
+ * via `resolveIngest2DDescriptor` (ingest/resolve-canvas.ts) and mounts it exactly
873
+ * like any other {@link IngestGame2D}.
874
+ */
875
+ export async function mountCanvasIngestRootFromManifest(
876
+ store: EditorShellStore,
877
+ world: ResolvedAdapterRoot,
878
+ projectRoot: string,
879
+ ): Promise<void> {
880
+ const game = resolveIngest2DDescriptor(world, projectRoot);
881
+ return mountCanvasIngestRoot(store, game, world.id);
882
+ }
883
+
884
+ /**
885
+ * Mount an in-tree pixi fixture game by its discovered id
886
+ * (`ingest/surface-canvas.ts`'s glob discovery is the lookup). Canvas sibling
887
+ * of `mountThreeIngestRootById`.
888
+ */
889
+ export async function mountCanvasIngestRootById(
890
+ store: EditorShellStore,
891
+ gameId: string,
892
+ ): Promise<void> {
893
+ const game = getIngestGame2D(gameId);
894
+ if (!game) {
895
+ editorConsole.error(`Unknown pixi ingest game: ${gameId}`, 'ingest');
896
+ return;
897
+ }
898
+ return mountCanvasIngestRoot(store, game, gameId);
899
+ }