@vgai/engine 0.5.21 → 0.5.23

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 (339) hide show
  1. package/README.md +6 -3
  2. package/dist/adapter/adapter-module.d.ts.map +1 -1
  3. package/dist/adapter/adapter-module.js +2 -4
  4. package/dist/adapter/adapter-surface.d.ts +1 -1
  5. package/dist/adapter/adapter-surface.js +1 -1
  6. package/dist/adapter/authoring.d.ts +19 -21
  7. package/dist/adapter/authoring.d.ts.map +1 -1
  8. package/dist/adapter/authoring.js +2 -2
  9. package/dist/adapter/binding.d.ts +341 -0
  10. package/dist/adapter/binding.d.ts.map +1 -0
  11. package/dist/adapter/binding.js +148 -0
  12. package/dist/adapter/colyseus-networking-adapter.d.ts.map +1 -1
  13. package/dist/adapter/colyseus-networking-adapter.js +23 -4
  14. package/dist/adapter/entrypoint-selection-readers.d.ts +24 -0
  15. package/dist/adapter/entrypoint-selection-readers.d.ts.map +1 -0
  16. package/dist/adapter/entrypoint-selection-readers.js +63 -0
  17. package/dist/adapter/entrypoint-selection-source.d.ts.map +1 -1
  18. package/dist/adapter/entrypoint-selection-source.js +1 -44
  19. package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts.map +1 -1
  20. package/dist/adapter/finders/scenes-from-entrypoint-selection.js +1 -52
  21. package/dist/adapter/host-context.d.ts +46 -15
  22. package/dist/adapter/host-context.d.ts.map +1 -1
  23. package/dist/adapter/host-context.js +2 -9
  24. package/dist/adapter/index.d.ts +11 -12
  25. package/dist/adapter/index.d.ts.map +1 -1
  26. package/dist/adapter/index.js +9 -10
  27. package/dist/adapter/ingest/game-contract.d.ts +0 -3
  28. package/dist/adapter/ingest/game-contract.d.ts.map +1 -1
  29. package/dist/adapter/ingest/game-contract.js +0 -3
  30. package/dist/adapter/ingest/scene-capture.d.ts +19 -23
  31. package/dist/adapter/ingest/scene-capture.d.ts.map +1 -1
  32. package/dist/adapter/ingest/scene-capture.js +143 -17
  33. package/dist/adapter/ingest/visible-capture-window.d.ts +34 -0
  34. package/dist/adapter/ingest/visible-capture-window.d.ts.map +1 -1
  35. package/dist/adapter/manifest-interpreter.d.ts +59 -0
  36. package/dist/adapter/manifest-interpreter.d.ts.map +1 -0
  37. package/dist/adapter/manifest-interpreter.js +50 -0
  38. package/dist/adapter/native-debug-module.d.ts +71 -3
  39. package/dist/adapter/native-debug-module.d.ts.map +1 -1
  40. package/dist/adapter/native-debug-module.js +146 -10
  41. package/dist/adapter/rapier-physics-adapter.d.ts +31 -1
  42. package/dist/adapter/rapier-physics-adapter.d.ts.map +1 -1
  43. package/dist/adapter/rapier-physics-adapter.js +50 -36
  44. package/dist/adapter/renderer-config.d.ts +3 -3
  45. package/dist/adapter/renderer-config.js +3 -3
  46. package/dist/adapter/root-adapter.d.ts +7 -7
  47. package/dist/adapter/root-adapter.js +4 -4
  48. package/dist/adapter/system-adapter.d.ts +45 -7
  49. package/dist/adapter/system-adapter.d.ts.map +1 -1
  50. package/dist/adapter/system-seam-contract.d.ts +3 -3
  51. package/dist/adapter/system-seam-contract.js +3 -3
  52. package/dist/adapter/system-slot.d.ts +168 -0
  53. package/dist/adapter/system-slot.d.ts.map +1 -0
  54. package/dist/adapter/system-slot.js +229 -0
  55. package/dist/adapter/transform.d.ts +11 -1
  56. package/dist/adapter/transform.d.ts.map +1 -1
  57. package/dist/ai/navigation.d.ts +17 -1
  58. package/dist/ai/navigation.d.ts.map +1 -1
  59. package/dist/ai/navigation.js +26 -5
  60. package/dist/animation/anim-graph-types.d.ts +11 -3
  61. package/dist/animation/anim-graph-types.d.ts.map +1 -1
  62. package/dist/animation/theatre-clock-binding.d.ts +4 -4
  63. package/dist/animation/theatre-clock-binding.js +4 -4
  64. package/dist/animation/xstate-animation-binding.d.ts.map +1 -1
  65. package/dist/animation/xstate-animation-binding.js +2 -2
  66. package/dist/animation/xstate-animation-meta.d.ts +13 -0
  67. package/dist/animation/xstate-animation-meta.d.ts.map +1 -1
  68. package/dist/animation/xstate-animation-meta.js +14 -0
  69. package/dist/asset-formats/index.d.ts +4 -7
  70. package/dist/asset-formats/index.d.ts.map +1 -1
  71. package/dist/asset-formats/index.js +4 -7
  72. package/dist/asset-formats/render-env.d.ts +5 -8
  73. package/dist/asset-formats/render-env.d.ts.map +1 -1
  74. package/dist/asset-formats/render-env.js +3 -6
  75. package/dist/asset-parse-error.d.ts +2 -5
  76. package/dist/asset-parse-error.d.ts.map +1 -1
  77. package/dist/asset-parse-error.js +2 -5
  78. package/dist/audio/bus-mixer.d.ts +101 -0
  79. package/dist/audio/bus-mixer.d.ts.map +1 -0
  80. package/dist/audio/bus-mixer.js +115 -0
  81. package/dist/canvas-react/index.d.ts +5 -12
  82. package/dist/canvas-react/index.d.ts.map +1 -1
  83. package/dist/canvas-react/index.js +5 -12
  84. package/dist/canvas-react/pixi-react-root-factory.d.ts +34 -21
  85. package/dist/canvas-react/pixi-react-root-factory.d.ts.map +1 -1
  86. package/dist/canvas-react/pixi-react-root-factory.js +274 -36
  87. package/dist/core/countdown-timer.d.ts +118 -0
  88. package/dist/core/countdown-timer.d.ts.map +1 -0
  89. package/dist/core/countdown-timer.js +137 -0
  90. package/dist/core/deferred-commands.d.ts +127 -0
  91. package/dist/core/deferred-commands.d.ts.map +1 -0
  92. package/dist/core/deferred-commands.js +132 -0
  93. package/dist/core/frame-pacing.d.ts +1 -1
  94. package/dist/core/frame-pacing.js +2 -2
  95. package/dist/core/game-loop.d.ts +1 -3
  96. package/dist/core/game-loop.d.ts.map +1 -1
  97. package/dist/core/game-loop.js +1 -3
  98. package/dist/core/seeded-random.d.ts +1 -1
  99. package/dist/core/sim-clock.d.ts +19 -8
  100. package/dist/core/sim-clock.d.ts.map +1 -1
  101. package/dist/core/sim-clock.js +18 -7
  102. package/dist/core/types.d.ts +1 -3
  103. package/dist/core/types.d.ts.map +1 -1
  104. package/dist/defaults.d.ts +6 -14
  105. package/dist/defaults.d.ts.map +1 -1
  106. package/dist/defaults.js +6 -14
  107. package/dist/dev/instruments.d.ts +38 -11
  108. package/dist/dev/instruments.d.ts.map +1 -1
  109. package/dist/dev/instruments.js +81 -11
  110. package/dist/dev/register-render-vitals.d.ts +1 -4
  111. package/dist/dev/register-render-vitals.d.ts.map +1 -1
  112. package/dist/dev/register-render-vitals.js +1 -4
  113. package/dist/dev/render-debug-adapter.d.ts +2 -3
  114. package/dist/dev/render-debug-adapter.d.ts.map +1 -1
  115. package/dist/dev/render-debug-adapter.js +2 -3
  116. package/dist/dev/render-vitals.d.ts +3 -3
  117. package/dist/dev/render-vitals.js +3 -3
  118. package/dist/dev/webgl-frame-capture.d.ts +1 -1
  119. package/dist/dev/webgl-frame-capture.js +1 -1
  120. package/dist/index.d.ts +2 -5
  121. package/dist/index.d.ts.map +1 -1
  122. package/dist/index.js +2 -4
  123. package/dist/input/host-pointer.d.ts +122 -0
  124. package/dist/input/host-pointer.d.ts.map +1 -0
  125. package/dist/input/host-pointer.js +160 -0
  126. package/dist/input/input-manager.d.ts +33 -5
  127. package/dist/input/input-manager.d.ts.map +1 -1
  128. package/dist/input/input-manager.js +48 -13
  129. package/dist/input/rebind-controller.d.ts.map +1 -1
  130. package/dist/input/rebind-controller.js +6 -8
  131. package/dist/input/schema.d.ts.map +1 -1
  132. package/dist/input/schema.js +56 -28
  133. package/dist/manifest/filename.d.ts.map +1 -1
  134. package/dist/manifest/filename.js +8 -12
  135. package/dist/manifest/load.d.ts +10 -10
  136. package/dist/manifest/load.d.ts.map +1 -1
  137. package/dist/manifest/load.js +9 -6
  138. package/dist/manifest/schema.d.ts +17 -21
  139. package/dist/manifest/schema.d.ts.map +1 -1
  140. package/dist/manifest/schema.js +45 -35
  141. package/dist/physics/collider-dimensions.d.ts +8 -12
  142. package/dist/physics/collider-dimensions.d.ts.map +1 -1
  143. package/dist/physics/collider-dimensions.js +8 -12
  144. package/dist/pixi/scene-capture.d.ts +11 -13
  145. package/dist/pixi/scene-capture.d.ts.map +1 -1
  146. package/dist/react/world-state.d.ts +1 -1
  147. package/dist/react/world-state.d.ts.map +1 -1
  148. package/dist/render/directional-shadow-fit.d.ts +88 -0
  149. package/dist/render/directional-shadow-fit.d.ts.map +1 -0
  150. package/dist/render/directional-shadow-fit.js +133 -0
  151. package/dist/render/environment-capture.d.ts +68 -0
  152. package/dist/render/environment-capture.d.ts.map +1 -0
  153. package/dist/render/environment-capture.js +90 -0
  154. package/dist/render/ibl-override-material.d.ts +97 -0
  155. package/dist/render/ibl-override-material.d.ts.map +1 -0
  156. package/dist/render/ibl-override-material.js +114 -0
  157. package/dist/render/light-camera-factory.d.ts +5 -7
  158. package/dist/render/light-camera-factory.d.ts.map +1 -1
  159. package/dist/render/light-camera-factory.js +8 -11
  160. package/dist/render/material-factory.js +1 -1
  161. package/dist/render/render-batch-system.d.ts.map +1 -1
  162. package/dist/render/render-features.d.ts +17 -6
  163. package/dist/render/render-features.d.ts.map +1 -1
  164. package/dist/render/render-features.js +0 -28
  165. package/dist/runtime/create-runtime.d.ts +34 -25
  166. package/dist/runtime/create-runtime.d.ts.map +1 -1
  167. package/dist/runtime/create-runtime.js +218 -93
  168. package/dist/runtime/debug-registry.d.ts +77 -8
  169. package/dist/runtime/debug-registry.d.ts.map +1 -1
  170. package/dist/runtime/debug-registry.js +3 -3
  171. package/dist/runtime/dev-build.d.ts +2 -2
  172. package/dist/runtime/dev-build.js +2 -2
  173. package/dist/runtime/game-input-seams.d.ts +51 -0
  174. package/dist/runtime/game-input-seams.d.ts.map +1 -0
  175. package/dist/runtime/game-input-seams.js +86 -0
  176. package/dist/runtime/game.d.ts +104 -40
  177. package/dist/runtime/game.d.ts.map +1 -1
  178. package/dist/runtime/game.js +119 -61
  179. package/dist/runtime/mount-game.d.ts +0 -16
  180. package/dist/runtime/mount-game.d.ts.map +1 -1
  181. package/dist/runtime/mount-game.js +26 -41
  182. package/dist/runtime/mount-manifest.d.ts +22 -14
  183. package/dist/runtime/mount-manifest.d.ts.map +1 -1
  184. package/dist/runtime/mount-manifest.js +22 -20
  185. package/dist/runtime/playtest.d.ts +22 -0
  186. package/dist/runtime/playtest.d.ts.map +1 -0
  187. package/dist/runtime/playtest.js +12 -0
  188. package/dist/runtime/state-bridge.d.ts +2 -3
  189. package/dist/runtime/state-bridge.d.ts.map +1 -1
  190. package/dist/runtime/state-bridge.js +1 -0
  191. package/dist/setup/setup-audio.d.ts +12 -0
  192. package/dist/setup/setup-audio.d.ts.map +1 -1
  193. package/dist/setup/setup-audio.js +9 -10
  194. package/dist/setup/setup-renderer.d.ts +2 -25
  195. package/dist/setup/setup-renderer.d.ts.map +1 -1
  196. package/dist/setup/setup-renderer.js +2 -31
  197. package/dist/world3d-react/index.d.ts +15 -25
  198. package/dist/world3d-react/index.d.ts.map +1 -1
  199. package/dist/world3d-react/index.js +15 -25
  200. package/dist/world3d-react/r3f-root-factory.d.ts +28 -34
  201. package/dist/world3d-react/r3f-root-factory.d.ts.map +1 -1
  202. package/dist/world3d-react/r3f-root-factory.js +403 -57
  203. package/dist/world3d-react/rapier-physics-bridge.d.ts +30 -17
  204. package/dist/world3d-react/rapier-physics-bridge.d.ts.map +1 -1
  205. package/dist/world3d-react/rapier-physics-bridge.js +143 -58
  206. package/package.json +1 -1
  207. package/schemas/engine-capabilities.json +16 -14
  208. package/schemas/vgai-project.schema.json +35 -26
  209. package/src/adapter/adapter-module.ts +2 -4
  210. package/src/adapter/adapter-surface.ts +1 -1
  211. package/src/adapter/authoring.ts +19 -21
  212. package/src/adapter/binding.ts +468 -0
  213. package/src/adapter/colyseus-networking-adapter.ts +20 -5
  214. package/src/adapter/entrypoint-selection-readers.ts +66 -0
  215. package/src/adapter/entrypoint-selection-source.ts +1 -40
  216. package/src/adapter/finders/scenes-from-entrypoint-selection.ts +1 -48
  217. package/src/adapter/host-context.ts +45 -15
  218. package/src/adapter/index.ts +23 -11
  219. package/src/adapter/ingest/game-contract.ts +0 -3
  220. package/src/adapter/ingest/scene-capture.ts +190 -39
  221. package/src/adapter/ingest/visible-capture-window.ts +35 -0
  222. package/src/adapter/manifest-interpreter.ts +64 -0
  223. package/src/adapter/native-debug-module.ts +248 -15
  224. package/src/adapter/rapier-physics-adapter.ts +76 -20
  225. package/src/adapter/renderer-config.ts +3 -3
  226. package/src/adapter/root-adapter.ts +7 -7
  227. package/src/adapter/system-adapter.ts +43 -8
  228. package/src/adapter/system-seam-contract.ts +3 -3
  229. package/src/adapter/system-slot.ts +291 -0
  230. package/src/adapter/transform.ts +18 -1
  231. package/src/ai/navigation.ts +30 -3
  232. package/src/animation/anim-graph-types.ts +11 -3
  233. package/src/animation/theatre-clock-binding.ts +4 -4
  234. package/src/animation/xstate-animation-binding.ts +2 -2
  235. package/src/animation/xstate-animation-meta.ts +16 -0
  236. package/src/asset-formats/index.ts +4 -7
  237. package/src/asset-formats/render-env.ts +3 -6
  238. package/src/asset-parse-error.ts +2 -5
  239. package/src/audio/bus-mixer.ts +161 -0
  240. package/src/canvas-react/index.ts +5 -21
  241. package/src/canvas-react/pixi-react-root-factory.tsx +301 -39
  242. package/src/core/countdown-timer.ts +188 -0
  243. package/src/core/deferred-commands.ts +174 -0
  244. package/src/core/frame-pacing.ts +2 -2
  245. package/src/core/game-loop.ts +1 -3
  246. package/src/core/seeded-random.ts +1 -1
  247. package/src/core/sim-clock.ts +19 -8
  248. package/src/core/types.ts +1 -3
  249. package/src/defaults.ts +6 -14
  250. package/src/dev/instruments.ts +108 -11
  251. package/src/dev/register-render-vitals.ts +1 -4
  252. package/src/dev/render-debug-adapter.ts +2 -3
  253. package/src/dev/render-vitals.ts +3 -3
  254. package/src/dev/webgl-frame-capture.ts +1 -1
  255. package/src/index.ts +2 -5
  256. package/src/input/host-pointer.ts +230 -0
  257. package/src/input/input-manager.ts +49 -13
  258. package/src/input/input-types.ts +1 -1
  259. package/src/input/rebind-controller.ts +6 -8
  260. package/src/input/schema.ts +216 -188
  261. package/src/manifest/filename.ts +8 -12
  262. package/src/manifest/load.ts +17 -13
  263. package/src/manifest/schema.ts +55 -47
  264. package/src/physics/collider-dimensions.ts +8 -12
  265. package/src/pixi/scene-capture.ts +12 -15
  266. package/src/react/world-state.tsx +1 -1
  267. package/src/render/directional-shadow-fit.ts +156 -0
  268. package/src/render/environment-capture.ts +102 -0
  269. package/src/render/ibl-override-material.ts +170 -0
  270. package/src/render/light-camera-factory.ts +8 -11
  271. package/src/render/material-factory.ts +1 -1
  272. package/src/render/render-batch-system.ts +14 -6
  273. package/src/render/render-features.ts +17 -35
  274. package/src/runtime/create-runtime.ts +270 -108
  275. package/src/runtime/debug-registry.ts +84 -8
  276. package/src/runtime/dev-build.ts +2 -2
  277. package/src/runtime/game-input-seams.ts +108 -0
  278. package/src/runtime/game.ts +231 -85
  279. package/src/runtime/mount-game.ts +31 -57
  280. package/src/runtime/mount-manifest.ts +47 -36
  281. package/src/runtime/playtest.ts +22 -0
  282. package/src/runtime/state-bridge.ts +3 -3
  283. package/src/setup/setup-audio.ts +21 -11
  284. package/src/setup/setup-renderer.ts +2 -61
  285. package/src/world3d-react/index.ts +15 -31
  286. package/src/world3d-react/r3f-root-factory.tsx +451 -60
  287. package/src/world3d-react/rapier-physics-bridge.tsx +154 -58
  288. package/dist/adapter/setup-three-root-adapter.d.ts +0 -86
  289. package/dist/adapter/setup-three-root-adapter.d.ts.map +0 -1
  290. package/dist/adapter/setup-three-root-adapter.js +0 -908
  291. package/dist/animation/clip-map.d.ts +0 -12
  292. package/dist/animation/clip-map.d.ts.map +0 -1
  293. package/dist/animation/clip-map.js +0 -31
  294. package/dist/asset-registry.d.ts +0 -38
  295. package/dist/asset-registry.d.ts.map +0 -1
  296. package/dist/asset-registry.js +0 -66
  297. package/dist/canvas-react/engine-bridge.d.ts +0 -45
  298. package/dist/canvas-react/engine-bridge.d.ts.map +0 -1
  299. package/dist/canvas-react/engine-bridge.js +0 -45
  300. package/dist/canvas-react/pixi-react-adapter.d.ts +0 -77
  301. package/dist/canvas-react/pixi-react-adapter.d.ts.map +0 -1
  302. package/dist/canvas-react/pixi-react-adapter.js +0 -294
  303. package/dist/canvas-react/world-context.d.ts +0 -98
  304. package/dist/canvas-react/world-context.d.ts.map +0 -1
  305. package/dist/canvas-react/world-context.js +0 -173
  306. package/dist/dev/debug-draw.d.ts +0 -24
  307. package/dist/dev/debug-draw.d.ts.map +0 -1
  308. package/dist/dev/debug-draw.js +0 -73
  309. package/dist/render/auto-batcher.d.ts +0 -34
  310. package/dist/render/auto-batcher.d.ts.map +0 -1
  311. package/dist/render/auto-batcher.js +0 -140
  312. package/dist/render/lod.d.ts +0 -13
  313. package/dist/render/lod.d.ts.map +0 -1
  314. package/dist/render/lod.js +0 -16
  315. package/dist/runtime/types.d.ts +0 -303
  316. package/dist/runtime/types.d.ts.map +0 -1
  317. package/dist/runtime/types.js +0 -1
  318. package/dist/world3d-react/engine-bridge.d.ts +0 -47
  319. package/dist/world3d-react/engine-bridge.d.ts.map +0 -1
  320. package/dist/world3d-react/engine-bridge.js +0 -73
  321. package/dist/world3d-react/r3f-adapter.d.ts +0 -58
  322. package/dist/world3d-react/r3f-adapter.d.ts.map +0 -1
  323. package/dist/world3d-react/r3f-adapter.js +0 -634
  324. package/dist/world3d-react/world-context.d.ts +0 -182
  325. package/dist/world3d-react/world-context.d.ts.map +0 -1
  326. package/dist/world3d-react/world-context.js +0 -235
  327. package/src/adapter/setup-three-root-adapter.ts +0 -1032
  328. package/src/animation/clip-map.ts +0 -34
  329. package/src/asset-registry.ts +0 -89
  330. package/src/canvas-react/engine-bridge.ts +0 -59
  331. package/src/canvas-react/pixi-react-adapter.tsx +0 -356
  332. package/src/canvas-react/world-context.ts +0 -253
  333. package/src/dev/debug-draw.ts +0 -80
  334. package/src/render/auto-batcher.ts +0 -168
  335. package/src/render/lod.ts +0 -17
  336. package/src/runtime/types.ts +0 -328
  337. package/src/world3d-react/engine-bridge.ts +0 -86
  338. package/src/world3d-react/r3f-adapter.tsx +0 -717
  339. package/src/world3d-react/world-context.ts +0 -358
@@ -0,0 +1,161 @@
1
+ /**
2
+ * A named-bus mixer over Web Audio: buses created on demand, routed into each
3
+ * other by name, and set in DECIBELS.
4
+ *
5
+ * Every mixer a game reaches for is this shape — Unity's `AudioMixerGroup`,
6
+ * Godot's bus layout, FMOD's VCAs, and the `music`/`sfx`/`voice` trio
7
+ * `setup/setup-audio.ts` hands a first-party three world. What they share is
8
+ * not the protocol (that part is genuinely per-ecosystem and stays with the
9
+ * lane that speaks it) but the machinery underneath: a `GainNode` per bus, a
10
+ * name → node map, a parent to route into, and the one conversion below. This
11
+ * file owns exactly that, and nothing that names an ecosystem.
12
+ *
13
+ * ## dB is not a gain, and the mistake is silent
14
+ *
15
+ * `GainNode.gain` is a LINEAR amplitude multiplier; every mixer's authored
16
+ * volume is logarithmic dB, where 0 dB is unity and −80 dB is silence. Writing
17
+ * the dB number straight onto `gain.value` gives you a gain of −80 (a phase
18
+ * inversion at 80x) where the author asked for silence, and it does not throw,
19
+ * warn, or look wrong in a debugger — it is just loud. {@link dbToLinear} is
20
+ * the one conversion, `10^(dB/20)`, which is both Godot's `db_to_linear`
21
+ * (`core/math/math_funcs.h`: `exp(db * 0.11512925464970228…)`, the same
22
+ * function written in base e) and Unity's mixer volume curve.
23
+ *
24
+ * It deliberately does NOT clamp. A mixer's RANGE is protocol — Unity's
25
+ * exposed parameters saturate at its slider's ends, Godot's `volume_db` does
26
+ * not — so the lane that knows the range applies it before calling here, and
27
+ * this file cannot silently mute or silently boost anyone's authored value.
28
+ *
29
+ * ## Create-on-demand, and why it is the contract rather than a convenience
30
+ *
31
+ * {@link AudioBusMixer.bus} MINTS a bus the first time it is named and returns
32
+ * the SAME node forever after. The identity is the contract, not the minting:
33
+ * a source connects to a bus long before anything sets that bus's volume, and
34
+ * a parent is usually named by its child before anyone asks for it directly
35
+ * (`bus('music', 'master')` builds `master` too, and the later `bus('master')`
36
+ * must be that same node). A `bus()` that minted per call would hand the
37
+ * volume control a SECOND node — the sources would keep feeding the first one,
38
+ * every later write would land on a node nothing plays through, and nothing
39
+ * would throw or look wrong. So the map is authoritative, and the only way a
40
+ * name stops resolving to its node is {@link AudioBusMixer.remove} or
41
+ * {@link AudioBusMixer.disconnect}.
42
+ *
43
+ * A re-ask that names a DIFFERENT parent throws by name. Routing is decided at
44
+ * creation (the node is already connected by then), so honouring the second
45
+ * parent would mean silently re-routing live audio and ignoring it would mean
46
+ * silently keeping the first — a loud error is the only reading that is not a
47
+ * lie.
48
+ *
49
+ * ## RESOURCE OWNERSHIP
50
+ *
51
+ * No module-scoped state: {@link createAudioBusMixer} is a factory and the
52
+ * OWNER of an instance is whoever called it (a `setupAudio` graph, one
53
+ * translated port's mixer store). What an instance ALLOCATES is one `GainNode`
54
+ * per named bus, and disconnecting those is the whole of its teardown
55
+ * obligation — {@link AudioBusMixer.disconnect}, called once by that same
56
+ * owner. The `context` and the `destination` node are NOT its own: they are
57
+ * passed in, shared with everything else hanging off the same graph, and this
58
+ * file never creates, closes, suspends or disconnects either one.
59
+ */
60
+
61
+ /**
62
+ * The graph an {@link AudioBusMixer} hangs off, as the two members it touches.
63
+ *
64
+ * Narrow on purpose: `createGain` is all it needs from the context, so a real
65
+ * `AudioContext`, an `OfflineAudioContext` and a headless stand-in are all
66
+ * acceptable without a cast.
67
+ */
68
+ export interface AudioBusMixerOptions {
69
+ readonly context: { createGain(): GainNode };
70
+ /** The node every parentless bus feeds. Usually a master gain or the context's destination. */
71
+ readonly destination: AudioNode;
72
+ }
73
+
74
+ export interface AudioBusMixer {
75
+ /**
76
+ * The bus named `name`, created on the first ask and connected to `parent`
77
+ * (itself created on demand) or to the destination.
78
+ *
79
+ * The returned node is the real `GainNode` — set `gain.value`, schedule
80
+ * automation on `gain`, connect sources to it. Nothing here stands between a
81
+ * caller and Web Audio's own API.
82
+ */
83
+ bus(name: string, parent?: string): GainNode;
84
+ /** The bus named `name` if it EXISTS, else null. Never mints one. */
85
+ find(name: string): GainNode | null;
86
+ /** Set this bus's gain from decibels, creating it if absent. Clamp before calling — see the header. */
87
+ setDb(name: string, db: number, parent?: string): void;
88
+ /**
89
+ * Disconnect one bus and forget its name, so a later {@link bus} mints a
90
+ * fresh node. Buses that named it as their parent are NOT re-routed — they
91
+ * keep feeding the removed node, which now reaches nothing.
92
+ */
93
+ remove(name: string): void;
94
+ /** Disconnect every bus and forget them all. The owner's ONE teardown. */
95
+ disconnect(): void;
96
+ }
97
+
98
+ /** dB → linear amplitude: `10^(dB/20)`. 0 dB is 1, −6 dB is ~0.5, −80 dB is ~0.0001. Never clamps. */
99
+ export function dbToLinear(db: number): number {
100
+ return 10 ** (db / 20);
101
+ }
102
+
103
+ interface BusRecord {
104
+ readonly node: GainNode;
105
+ readonly parent: string | undefined;
106
+ }
107
+
108
+ function safeDisconnect(node: GainNode): void {
109
+ try {
110
+ node.disconnect();
111
+ } catch {
112
+ // already disconnected
113
+ }
114
+ }
115
+
116
+ /** Create one named-bus mixer. See the header for what owns the instance and what it allocates. */
117
+ export function createAudioBusMixer(options: AudioBusMixerOptions): AudioBusMixer {
118
+ const { context, destination } = options;
119
+ const buses = new Map<string, BusRecord>();
120
+
121
+ function bus(name: string, parent?: string): GainNode {
122
+ const existing = buses.get(name);
123
+ if (existing !== undefined) {
124
+ if (parent !== undefined && parent !== existing.parent) {
125
+ throw new Error(
126
+ `audio bus ${JSON.stringify(name)} already routes into ` +
127
+ `${existing.parent === undefined ? 'the destination' : JSON.stringify(existing.parent)}` +
128
+ `; it cannot also route into ${JSON.stringify(parent)}. Routing is decided when a bus ` +
129
+ 'is created (see packages/engine/src/audio/bus-mixer.ts).',
130
+ );
131
+ }
132
+ return existing.node;
133
+ }
134
+ // The parent is resolved (and minted, recursively) before this bus is
135
+ // seated, so a conflicting-parent throw upstream leaves nothing half-built.
136
+ const into = parent === undefined ? destination : bus(parent);
137
+ const node = context.createGain();
138
+ node.gain.value = 1;
139
+ node.connect(into);
140
+ buses.set(name, { node, parent });
141
+ return node;
142
+ }
143
+
144
+ return {
145
+ bus,
146
+ find: (name) => buses.get(name)?.node ?? null,
147
+ setDb: (name, db, parent) => {
148
+ bus(name, parent).gain.value = dbToLinear(db);
149
+ },
150
+ remove: (name) => {
151
+ const record = buses.get(name);
152
+ if (record === undefined) return;
153
+ buses.delete(name);
154
+ safeDisconnect(record.node);
155
+ },
156
+ disconnect: () => {
157
+ for (const record of buses.values()) safeDisconnect(record.node);
158
+ buses.clear();
159
+ },
160
+ };
161
+ }
@@ -15,19 +15,15 @@
15
15
  * or the Pixi reconciler.
16
16
  *
17
17
  * Surface:
18
- * - `createPixiReactAdapter({ id, content })` — mount a `@pixi/react` tree as
19
- * a first-party `surface: "canvas"` root under the host's gated loop.
20
18
  * - `pixiReactRootFactory` / `resolveCanvasEntryAdapter` — what a canvas entry
21
- * module MEANS: a default-exported component (or an explicit `adapter`).
19
+ * module MEANS: a default-exported component, mounted as a first-party
20
+ * `surface: "canvas"` root under the host's gated loop. The world's vgai
21
+ * surface is its entry module's static exports
22
+ * (`export { debug, systems } from './commands'`), connected by the host at
23
+ * mount.
22
24
  * - `PixiPrimitive` / `adoptNow` — the canvas surface's `<primitive object={…}>`:
23
25
  * render a container the game already owns, and place a spawn in the
24
26
  * display tree ahead of the commit that renders it.
25
- * - `useGameContext()` — this root's `CanvasGameContext` (`input`, `debug`,
26
- * `random`, `clock`, `game`, `roots`) from inside any component in the tree.
27
- * Behavior in a canvas world is components + hooks, never a class under a
28
- * string key.
29
- * - `EngineBridge` / `EngineBridgeValue` — the context contract, exported for
30
- * adopters building their own bridge.
31
27
  *
32
28
  * The Pixi runtime itself is NOT re-exported here. A component reaches the live
33
29
  * `Application` (and therefore `stage`, `renderer`, `ticker`) with
@@ -35,17 +31,5 @@
35
31
  * assets with Pixi's own `Assets` — the library's API, not a second one.
36
32
  */
37
33
 
38
- export { EngineBridge, type EngineBridgeValue, useGameContext } from './engine-bridge';
39
34
  export { adoptNow, PixiPrimitive, type PixiPrimitiveProps } from './pixi-primitive';
40
- export {
41
- type CreatePixiReactAdapterOptions,
42
- createPixiReactAdapter,
43
- } from './pixi-react-adapter';
44
35
  export { pixiReactRootFactory, resolveCanvasEntryAdapter } from './pixi-react-root-factory';
45
- export {
46
- type CanvasGameContext,
47
- type CanvasRootContextOptions,
48
- type CanvasRootRuntime,
49
- createCanvasRootContext,
50
- DEFAULT_INPUT_MAP_PATH,
51
- } from './world-context';
@@ -1,8 +1,6 @@
1
1
  /**
2
2
  * `pixiReactRootFactory` — mount a `canvas` root whose entry module
3
- * DEFAULT-EXPORTS a React component.
4
- *
5
- * A model asked for a 2D world writes:
3
+ * DEFAULT-EXPORTS a React component. This is the ONE first-party canvas mount:
6
4
  *
7
5
  * ```tsx
8
6
  * export default function World() {
@@ -10,11 +8,13 @@
10
8
  * }
11
9
  * ```
12
10
  *
13
- * It does NOT write `export const adapter = createPixiReactAdapter({ id, content })`.
14
- * That export is vgai ceremony wrapped around a component the author already
15
- * wrote. The `three` and `dom` roots both settled this convention read
16
- * `entryModule.default` and wrap it and this is the same contract for canvas
17
- * roots, so all three surfaces answer "what do I export?" the same way.
11
+ * The world is an ordinary `@pixi/react` app. Everything vgai-shaped stays at
12
+ * the entry module's STATIC surface (`export { debug, systems } from
13
+ * './commands'`) and on the host's side of the seam; no vgai runtime context
14
+ * ever enters the React tree. The host advances Pixi's real (never-started)
15
+ * ticker on GAME time, wires the game-scoped input seams from outside
16
+ * (`../runtime/game-input-seams.ts`), and installs `WorldProvider` off the
17
+ * Game handle so the lib-legal react doors resolve.
18
18
  *
19
19
  * ## Why this is NOT in `mount-game.ts`
20
20
  *
@@ -24,22 +24,298 @@
24
24
  * the opt-in module that owns those dependencies, so the factory lives here and
25
25
  * a project opts in with `registerAdapter('canvas', pixiReactRootFactory)`.
26
26
  *
27
- * ## Compatibility
27
+ * ## The loop contract
28
+ *
29
+ * `Application` is initialized with `autoStart: false` and `sharedTicker:
30
+ * false`, so it owns a native Pixi ticker that is NEVER started and therefore
31
+ * never requests an animation frame. `mounted.update(dt)` is the only thing
32
+ * that advances that ticker, and it feeds GAME time, not wall time — which is
33
+ * what makes the world freeze exactly when the host freezes it, and what makes
34
+ * `game.waitSimTime` work in a hidden tab (a hidden tab has no rAF; the engine
35
+ * loop's own catch-up still calls `update`, and Pixi has no independent clock
36
+ * to disagree with it).
28
37
  *
29
- * An `adapter` export still wins, so a game needing full control over its own
30
- * `RootAdapter<'canvas'>` keeps it.
38
+ * ## Component catalogue
39
+ *
40
+ * `@pixi/react` resolves `<pixiSprite>` etc. through a module-global catalogue
41
+ * that starts EMPTY — the first intrinsic in an un-extended tree throws
42
+ * "X is not part of the PIXI namespace! Did you forget to extend?". This
43
+ * module extends it with the whole `pixi.js` namespace once per mount
44
+ * (idempotent `Object.assign`). A world that adds its own classes
45
+ * (`@pixi/tilemap`, a custom `Container` subclass) calls `extend`/`useExtend`
46
+ * itself.
31
47
  */
32
48
 
33
- import { type ComponentType, createElement } from 'react';
34
- import type { RootAdapter } from '../adapter/root-adapter';
49
+ import { createRoot, extend } from '@pixi/react';
50
+ import type { Application, ApplicationOptions } from 'pixi.js';
51
+ import * as PIXI from 'pixi.js';
52
+ import { type ComponentType, createElement, Fragment, useEffect, useLayoutEffect } from 'react';
53
+ import type { CanvasHostContext, MountedPixiRoot, RootAdapter } from '../adapter';
54
+ import type { SystemAdapters } from '../adapter/system-adapter';
55
+ import { WorldProvider } from '../react/world-state';
56
+ import { getDebugRegistry } from '../runtime/debug-registry';
57
+ import { DEFAULT_INPUT_MAP_PATH, wireGameInputSeams } from '../runtime/game-input-seams';
35
58
  import type { AdapterSurfaceFactory } from '../runtime/mount-game';
36
- import { createPixiReactAdapter } from './pixi-react-adapter';
59
+
60
+ /** How long `mount()` waits for the tree's first commit before failing loudly
61
+ * rather than hanging (and wedging every root declared after this one, since
62
+ * roots mount sequentially). */
63
+ const FIRST_COMMIT_TIMEOUT_MS = 10_000;
64
+
65
+ /**
66
+ * `@pixi/react` keys its reconciler roots off the canvas element in a
67
+ * module-global map, and its own unmount path (which would clear that key) is
68
+ * not exported. So a canvas this lane has already mounted must never be handed
69
+ * to a second mount: `createRoot` would silently hand back the FIRST root,
70
+ * whose `Application` this lane already destroyed. Every host in this repo
71
+ * builds a fresh canvas per mount, so this only ever fires on a genuine bug —
72
+ * and it fires as a named error rather than as a dead world.
73
+ */
74
+ const MOUNTED_CANVASES = new WeakSet<HTMLCanvasElement>();
75
+
76
+ /** `@pixi/react`'s reconciler-root handle. Its `Root` type is internal (the
77
+ * package exports the FUNCTION, not the type), so name it off the function. */
78
+ type PixiReactRoot = ReturnType<typeof createRoot>;
37
79
 
38
80
  interface CanvasEntryModuleExports {
39
- /** The idiomatic shape: the world IS a component. */
81
+ /** The one shape: the world IS a component. */
40
82
  readonly default?: ComponentType;
41
- /** Full control, and still the winner when present. */
42
- readonly adapter?: RootAdapter<'canvas'>;
83
+ }
84
+
85
+ /**
86
+ * Build the `RootAdapter<'canvas'>` for one default-exported Pixi world.
87
+ * Mounts through `@pixi/react`, gated entirely by the host's own loop and
88
+ * rendering into the host's own canvas — never a second canvas, never a
89
+ * second `requestAnimationFrame` loop.
90
+ */
91
+ function canvasWorldAdapter(id: string, component: ComponentType): RootAdapter<'canvas'> {
92
+ const content = createElement(component);
93
+
94
+ return {
95
+ id,
96
+
97
+ async mount(host: CanvasHostContext): Promise<MountedPixiRoot> {
98
+ // THE HOST WIRES THIS GAME'S INPUT FROM OUTSIDE THE TREE — the project's
99
+ // declared map (conventional path, optional: a brand-new project
100
+ // declares no actions yet) and this root's `game.input.*` seams on the
101
+ // debug registry. Actions must exist before any component's first tick
102
+ // reads them — wait for the (never-rejecting) load before the first
103
+ // commit.
104
+ const registry = host.game ? getDebugRegistry(host.game) : null;
105
+ if (registry) {
106
+ await wireGameInputSeams(host, registry, {
107
+ id,
108
+ inputMapPath: DEFAULT_INPUT_MAP_PATH,
109
+ optionalInputMap: true,
110
+ });
111
+ }
112
+
113
+ const canvas = host.canvas;
114
+ if (MOUNTED_CANVASES.has(canvas)) {
115
+ throw new Error(
116
+ `canvas world "${id}": this lane has already mounted this canvas. @pixi/react keys ` +
117
+ 'its reconciler roots off the canvas element, so re-using one would resurrect a ' +
118
+ 'root whose Application is destroyed. Hand each mount a fresh canvas (every vgai ' +
119
+ 'host already does).',
120
+ );
121
+ }
122
+ MOUNTED_CANVASES.add(canvas);
123
+
124
+ // The catalogue is module-global and starts empty; this is idempotent.
125
+ extend(PIXI as unknown as Parameters<typeof extend>[0]);
126
+
127
+ let resolveCommitted!: () => void;
128
+ const committed = new Promise<void>((resolve) => {
129
+ resolveCommitted = resolve;
130
+ });
131
+ let resolveEffectsReady!: () => void;
132
+ const effectsReady = new Promise<void>((resolve) => {
133
+ resolveEffectsReady = resolve;
134
+ });
135
+ let resolveUnmounted!: () => void;
136
+ const unmounted = new Promise<void>((resolve) => {
137
+ resolveUnmounted = resolve;
138
+ });
139
+ /**
140
+ * Signals the tree's FIRST COMMIT and its LAST. A layout effect,
141
+ * deliberately, and rendered as the LAST child, deliberately:
142
+ *
143
+ * - mount: layout effects run synchronously inside the commit, before
144
+ * any passive effect and before any tick can be scheduled;
145
+ * - unmount: React commits deletion effects in tree order, each node
146
+ * before its own children, so the LAST sibling's cleanup is the last
147
+ * to run. That is what makes this a reliable "every component has
148
+ * finished tearing down" signal — and it is load-bearing, because
149
+ * `useTick`'s own cleanup calls `ticker.remove()`. Destroying the
150
+ * Application before that cleanup lands throws inside React's commit
151
+ * on every teardown of any world with a tick hook.
152
+ */
153
+ function Lifecycle(): null {
154
+ useLayoutEffect(() => {
155
+ resolveCommitted();
156
+ return resolveUnmounted;
157
+ }, []);
158
+ // This component is the last sibling. Reaching its passive effect
159
+ // means the initial tree's provider/command effects have registered,
160
+ // so mount() can truthfully back Play's ack.
161
+ useEffect(resolveEffectsReady, []);
162
+ return null;
163
+ }
164
+
165
+ // `WorldProvider` rides `host.game` — the React seam every dom root
166
+ // already has, and the only way a capability hook can reach the ONE
167
+ // game-scoped registry the editor/`vgai eval` read. `host.game` is
168
+ // genuinely absent in bare/foreign hosts, so the provider is
169
+ // conditional and those hooks stay the inert no-ops they already
170
+ // document themselves to be.
171
+ const game = host.game;
172
+ const world = createElement(Fragment, null, content, createElement(Lifecycle));
173
+ const element = game ? createElement(WorldProvider, { game }, world) : world;
174
+
175
+ const root: PixiReactRoot = createRoot(canvas);
176
+
177
+ // The host owns size, dpr, transparency and capture cost; the loop
178
+ // contract owns `autoStart`/`sharedTicker`.
179
+ const initOptions: Partial<ApplicationOptions> = {
180
+ antialias: true,
181
+ width: host.width,
182
+ height: host.height,
183
+ resolution: host.dpr ?? globalThis.devicePixelRatio ?? 1,
184
+ autoDensity: true,
185
+ // Stacked ABOVE a sibling root -> clear with alpha 0 so the layer below
186
+ // shows through (COMPOSITION-DESIGN D5 §1).
187
+ backgroundAlpha: host.transparent ? 0 : 1,
188
+ preserveDrawingBuffer: host.preserveDrawingBuffer ?? false,
189
+ autoStart: false,
190
+ sharedTicker: false,
191
+ };
192
+
193
+ // Install the reconcile-error listener BEFORE `root.render`: Pixi hands
194
+ // the Application back after `init()` but React can already have thrown
195
+ // during that same turn. Attaching after the await loses the real error
196
+ // and turns it into a misleading ten-second timeout.
197
+ let reportReconcileFailure!: (why: string) => void;
198
+ const reconcileFailure = new Promise<string>((resolve) => {
199
+ reportReconcileFailure = resolve;
200
+ });
201
+ const onError = (event: ErrorEvent): void => {
202
+ reportReconcileFailure(
203
+ `the Pixi tree crashed before its first commit — ${event.message} ` +
204
+ '(mount() fails loudly instead of hanging)',
205
+ );
206
+ };
207
+ globalThis.addEventListener?.('error', onError);
208
+
209
+ let app: Application | undefined;
210
+ let timer: ReturnType<typeof setTimeout> | undefined;
211
+ try {
212
+ // `root.render` is async: it awaits `Application.init()` (which is what
213
+ // creates the renderer) and only then hands the tree to the reconciler.
214
+ // A crash inside init rejects here, which is the loud failure we want.
215
+ const renderedApp = await root.render(element, initOptions);
216
+ app = renderedApp;
217
+
218
+ // A reconcile-time crash (e.g. an un-extended component) surfaces as
219
+ // an uncaught window error and the first commit never lands. Race that
220
+ // exact signal against the commit and the last-resort ceiling.
221
+ const timeout = new Promise<string>((resolve) => {
222
+ timer = setTimeout(
223
+ () =>
224
+ resolve(
225
+ `no first commit within ${FIRST_COMMIT_TIMEOUT_MS / 1000}s ` +
226
+ '(mount() fails loudly instead of hanging)',
227
+ ),
228
+ FIRST_COMMIT_TIMEOUT_MS,
229
+ );
230
+ });
231
+ const failure = await Promise.race([
232
+ Promise.all([committed, effectsReady]).then(() => null),
233
+ reconcileFailure,
234
+ timeout,
235
+ ]);
236
+ if (failure !== null) {
237
+ throw new Error(`canvas world "${id}": ${failure}`);
238
+ }
239
+ } catch (error) {
240
+ try {
241
+ app?.destroy(false, { children: true });
242
+ } catch {
243
+ /* the world never fully came up; teardown failure must not mask why */
244
+ }
245
+ throw error;
246
+ } finally {
247
+ if (timer !== undefined) clearTimeout(timer);
248
+ globalThis.removeEventListener?.('error', onError);
249
+ }
250
+
251
+ // `app` is assigned before the first-commit race. Reaching here proves
252
+ // both initialization and commit succeeded, so narrow once for the live
253
+ // mounted-root methods below.
254
+ if (app === undefined) {
255
+ throw new Error(`canvas world "${id}": initialized without an Application`);
256
+ }
257
+ const mountedApp = app;
258
+
259
+ // Pin the ticker's clock to GAME time. `Ticker.lastTime` starts at -1, so
260
+ // without this the first `update()` would report a 1ms phantom delta on
261
+ // top of the real one. From here every `deltaMS` a `useTick` callback
262
+ // sees is exactly the `dt` the host handed us — no wall clock anywhere,
263
+ // which is what makes a paused world's resume frame ordinary rather than
264
+ // one giant catch-up step.
265
+ mountedApp.ticker.lastTime = 0;
266
+ let elapsedMs = 0;
267
+ let disposed = false;
268
+ // Adapter surface: the host's shared debug registry. The game's own
269
+ // capabilities arrive as the entry module's declared `systems`.
270
+ const systemAdapters: SystemAdapters = registry ? { debug: registry.adapter } : {};
271
+ const disposeComplete = unmounted.then(() => {
272
+ mountedApp.destroy(false, { children: true });
273
+ });
274
+
275
+ return {
276
+ kind: 'canvas',
277
+ // The reconciler's real root container — the same `PIXI.Container`
278
+ // `RootInstance.pixiStage()` hands the editor.
279
+ stage: mountedApp.stage,
280
+ drivesOwnLoop: false,
281
+ systems: systemAdapters,
282
+ disposeComplete,
283
+ update(dt: number): void {
284
+ if (disposed) return;
285
+ elapsedMs += dt * 1000;
286
+ // The real Pixi ticker remains the one scheduler `useTick` observes;
287
+ // the host only gates/advances it.
288
+ mountedApp.ticker.update(elapsedMs);
289
+ },
290
+ resize(width: number, height: number): void {
291
+ if (disposed) return;
292
+ // `autoDensity: true` makes Pixi re-stamp `canvas.style.width`/
293
+ // `.height` from this call, so the on-screen box follows the logical
294
+ // size with no explicit CSS write from the host.
295
+ mountedApp.renderer.resize(width, height);
296
+ },
297
+ dispose(): void {
298
+ if (disposed) return;
299
+ disposed = true;
300
+ // Pixi's EventSystem owns document.pointermove + window.pointerup
301
+ // (and its event ticker's global listener). React's deletion commit
302
+ // is asynchronous, so waiting to destroy the Application leaves
303
+ // those listeners live when the host audits this game realm. Detach
304
+ // the input target synchronously; the later Application.destroy()
305
+ // repeats this idempotently after every useTick cleanup has landed.
306
+ mountedApp.renderer.events.setTargetElement(null as unknown as HTMLElement);
307
+ // Rendering `null` children is the only unmount path `@pixi/react`
308
+ // exposes publicly (its `unmountRoot` helper is internal), and React
309
+ // commits that deletion on its own schedule — NOT synchronously. So
310
+ // the Application is destroyed on the `Lifecycle` cleanup signal
311
+ // instead of inline: `useTick`'s cleanup calls `ticker.remove()`, and
312
+ // a ticker destroyed before it lands throws inside React's commit.
313
+ // `removeView` stays false — the canvas belongs to the HOST.
314
+ void root.render(null, initOptions);
315
+ },
316
+ };
317
+ },
318
+ };
43
319
  }
44
320
 
45
321
  /**
@@ -48,45 +324,31 @@ interface CanvasEntryModuleExports {
48
324
  * Two callers need this answer and must never disagree about it: the runtime
49
325
  * mount ({@link pixiReactRootFactory}, below) and the EDITOR, which resolves
50
326
  * the same entry for play mode and for the design-time layer. When only one
51
- * side knows about the default-export shape, a world plays fine and shows
52
- * "(no scene loaded)" in edit mode — the exact split the three lane already
53
- * paid for.
327
+ * side knows the shape, a world plays fine and shows "(no scene loaded)" in
328
+ * edit mode.
54
329
  *
55
- * Returns `null` when the module is neither shape — the callers differ on what
56
- * to do about that (the factory throws; the editor reports a mount failure on
57
- * that root's boundary node and leaves every sibling working).
330
+ * Returns `null` when the module has no default-exported component — the
331
+ * callers differ on what to do about that (the factory throws; the editor
332
+ * reports a mount failure on that root's boundary node and leaves every
333
+ * sibling working).
58
334
  */
59
335
  export function resolveCanvasEntryAdapter(
60
336
  entryModule: unknown,
61
337
  rootId: string,
62
338
  ): RootAdapter<'canvas'> | null {
63
339
  const mod = entryModule as CanvasEntryModuleExports | undefined;
64
- if (mod?.adapter) return mod.adapter;
65
- if (typeof mod?.default === 'function') {
66
- return createPixiReactAdapter({
67
- id: rootId,
68
- content: createElement(mod.default),
69
- // The supported Canvas portfolio still consumes the compatibility
70
- // context. Removing it belongs to the tracked component-root migration;
71
- // disabling it here first made every current default-exported game fail
72
- // to mount before that migration had happened.
73
- });
74
- }
340
+ if (typeof mod?.default === 'function') return canvasWorldAdapter(rootId, mod.default);
75
341
  return null;
76
342
  }
77
343
 
78
344
  /**
79
345
  * Register with `registerAdapter('canvas', pixiReactRootFactory)`.
80
- *
81
- * Resolution order mirrors the three lane's (explicit adapter wins) and then
82
- * accepts the default-exported component.
83
346
  */
84
347
  export const pixiReactRootFactory: AdapterSurfaceFactory = (root, ctx) => {
85
348
  const adapter = resolveCanvasEntryAdapter(ctx.entryModule, root.id);
86
349
  if (adapter) return { kind: 'canvas', adapter };
87
350
  throw new Error(
88
351
  `pixiReactRootFactory: entry module "${root.entry ?? '(none)'}" for root "${root.id}" must ` +
89
- 'default-export a React component (`export default function World() { … }`) — or export ' +
90
- 'an `adapter` for full control.',
352
+ 'default-export a React component (`export default function World() { … }`).',
91
353
  );
92
354
  };