@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
@@ -1,634 +0,0 @@
1
- /**
2
- * `createR3FAdapter` — wraps a react-three-fiber tree as a first-party
3
- * `RootAdapter` so an R3F scene mounts as an ordinary `kind: "three"`
4
- * world: the engine hands `mount()` a `ThreeHostContext` (ITS OWN renderer +
5
- * canvas + gated loop, per `../adapter/host-context.ts`); this returns a
6
- * `MountedThreeRoot` (`../adapter/root-adapter.ts`) whose `scene`/`camera`
7
- * are fiber's REAL `THREE.Scene`/`THREE.Camera` instances.
8
- *
9
- * Upstreamed from `examples/r3f-first-party/src/r3f-adapter.tsx` (R4 — the
10
- * `@engine/world3d-react` opt-in module, mirroring how `pixi/` is the
11
- * PixiJS surface's opt-in home). The mount semantics
12
- * are byte-for-byte the example's live-proven Phase-1 bridge. External adopters
13
- * (alien-stories-style) keep the exact same calling contract.
14
- *
15
- * three.js identity: this module never imports `three` itself for scene objects
16
- * — fiber's internal `import * as THREE from 'three'` must resolve to the SAME
17
- * instance the engine's `ThreeHostContext.three` points at, which the importing
18
- * project guarantees via its Vite `resolve.dedupe: ['three', 'react',
19
- * 'react-dom']` (see `examples/r3f-first-party/vite.config.ts`).
20
- */
21
- import { advance, createRoot, extend, flushSync, events as pointerEvents, useFrame, } from '@react-three/fiber';
22
- import { createElement, Fragment, useEffect, useMemo } from 'react';
23
- import { applyWorldRendererConfig } from '../adapter/renderer-config';
24
- import { registerRenderVitals } from '../dev/register-render-vitals';
25
- import { createRenderDebugAdapter, frameCaptureContextFor, } from '../dev/render-debug-adapter';
26
- import { collectRenderMemory } from '../dev/render-memory';
27
- import { RENDER_SUBMIT_PHASE } from '../dev/render-vitals';
28
- import { createWebGLFrameCapture } from '../dev/webgl-frame-capture';
29
- import { WorldProvider } from '../react/world-state';
30
- import { createSoftParticleDepthPass } from '../render/soft-particle-depth';
31
- import { getDebugRegistry } from '../runtime/debug-registry';
32
- import { devBuildEnabled } from '../runtime/dev-build';
33
- import { EngineBridge } from './engine-bridge';
34
- import { createR3FRootContext, DEFAULT_INPUT_MAP_PATH, wireGameInputSeams } from './world-context';
35
- /**
36
- * Scene depth for any soft-particle system this world mounted, drawn between
37
- * gameplay and the draw.
38
- *
39
- * `useFrame`'s priority is the whole reason this is a component rather than an
40
- * engine phase: `systems.run(dt)` below finishes BEFORE `advance()`, and
41
- * `advance()` is where every `useFrame` gameplay callback moves the frame's
42
- * transforms — depth taken from an engine phase would be one frame stale.
43
- * Priority `0` puts it after every negative-priority callback (fiber sorts
44
- * subscribers by priority) and before both a positive-priority composer and
45
- * fiber's own `state.gl.render` — and, unlike a positive priority, it does NOT
46
- * flip fiber into manual-render mode (`internal.priority` only counts
47
- * subscribers with `priority > 0`), so mounting it can never stop an ordinary
48
- * world from drawing itself.
49
- *
50
- * Rendered as the LAST child of the adapter's tree so its subscription lands
51
- * after the world's own (React completes children before siblings, and fiber's
52
- * sort is stable) — the tie-break among priority-0 callbacks is insertion
53
- * order.
54
- *
55
- * It is mounted only when THIS host carries a real WebGL context. The editor's
56
- * design-time host deliberately reconciles the scene without one because the
57
- * editor viewport owns the real renderer and camera; that viewport runs the
58
- * same generic depth pass immediately before it presents the adopted scene.
59
- *
60
- * Costs nothing while no soft-particle system is armed — see
61
- * `../render/soft-particle-depth.ts` for what "armed" means and what the pass
62
- * spends when it is.
63
- */
64
- function SoftParticleDepth() {
65
- const pass = useMemo(() => createSoftParticleDepthPass(), []);
66
- useEffect(() => () => pass.dispose(), [pass]);
67
- useFrame(({ gl, scene, camera }) => pass.render(gl, scene, camera), 0);
68
- return null;
69
- }
70
- /**
71
- * Build a `RootAdapter` that mounts `options.content` through
72
- * react-three-fiber, gated entirely by the host's own loop and rendering
73
- * through the host's own `WebGLRenderer` — never a second renderer, never a
74
- * second `requestAnimationFrame` loop (design §1.C).
75
- */
76
- export function createR3FAdapter(options) {
77
- const { id, content, inputMapPath, engineRuntime = true, renderer: rendererConfig } = options;
78
- return {
79
- id,
80
- async mount(host) {
81
- // Headless honesty — every `mount()` must work headless: the
82
- // conformance kit / CI
83
- // runs `mount()` in Node — no canvas, no WebGL, no fiber reconciler.
84
- // Guard the ENTIRE fiber mount behind `!host.headless` and return a
85
- // bare, real scene + camera instead (still `instanceof
86
- // host.three.Scene` / `.Camera` — the identity rule holds even here,
87
- // since this uses the host's own `three` instance, not a fresh
88
- // import). Residual: the hierarchy is empty under headless
89
- // conformance — recorded as design O2, not hidden; a react-nil-style
90
- // headless reconciler mount is the known fix, out of scope for v1.
91
- if (host.headless) {
92
- const scene = new host.three.Scene();
93
- const camera = new host.three.PerspectiveCamera();
94
- return {
95
- kind: 'three',
96
- scene,
97
- camera,
98
- drivesOwnLoop: false,
99
- dispose() {
100
- /* nothing was ever mounted */
101
- },
102
- };
103
- }
104
- // Fiber v9 made the THREE catalogue tree-shakeable: `<Canvas>` calls
105
- // `extend(THREE)` for you, a bare `createRoot` does NOT — without this,
106
- // the FIRST three intrinsic in the tree (`<color>`, `<ambientLight>`,
107
- // …) throws "X is not part of the THREE namespace! Did you forget to
108
- // extend?" at reconcile time. Extending with `host.three` (not a fresh
109
- // `import * as THREE`) keeps the catalogue on the host's deduped three
110
- // instance — the same identity rule the returned scene/camera rely on.
111
- // `extend` merges into a module-global catalogue, so calling it once
112
- // per mount is idempotent.
113
- extend(host.three);
114
- // The engine runtime this world OWNS: a real SystemRunner,
115
- // provided to the R3F tree via `EngineBridge`.
116
- // `update(dt)` below runs the phases under the host loop, so registered
117
- // systems tick in engine phase order and freeze under
118
- // pause exactly like `useFrame` work does.
119
- //
120
- // HONESTY NOTE: the manager's ctx is an `R3FGameContext`, not the full
121
- // `GameContext`. The shared game-scoped subsystems are REAL
122
- // classic-adapter parity (`./world-context.ts`):
123
- // `ctx.debug`/`ctx.random`/`ctx.game`/`ctx.roots`, and `ctx.input` (the
124
- // game-owned InputManager, with this world's virtual-input/actions/trace
125
- // debug seams registered and the project's input map loaded). The
126
- // subsystems an R3F world genuinely does not build (Rapier, composer,
127
- // audio, particles, debugDraw) are absent from that TYPE, so reaching
128
- // for one is a compile error rather than anything that can happen at
129
- // runtime. `scene`/`camera` are backfilled right after fiber's first
130
- // commit resolves them, before any tick can run.
131
- const runtime = engineRuntime ? createR3FRootContext(host, { id, inputMapPath }) : null;
132
- // `ctx` rides the bridge so ordinary components can reach engine
133
- // services through `useGameContext()`.
134
- const bridge = runtime ? { ctx: runtime.ctx } : null;
135
- const gameDebugRegistry = host.game ? getDebugRegistry(host.game) : null;
136
- // THE HOST STILL WIRES THIS GAME'S INPUT WHEN THE WORLD TAKES NO CONTEXT.
137
- //
138
- // A default-exported world mounts with `engineRuntime: false` — no vgai
139
- // runtime context enters its React tree, by design. That says nothing
140
- // about the GAME-scoped input the host owns from the outside: the
141
- // project's declared map, and this root's `game.input.*` seams on the
142
- // debug registry. Skipping those made a fresh scaffold's bot/autoplay/
143
- // `vgai eval` input doors throw `DEBUG_INPUT_UNAVAILABLE` and left the
144
- // declared actions unloaded — in the ONE world shape `vgai create`
145
- // produces. Same host-vs-context split as `WorldProvider` below.
146
- //
147
- // `inputMapPath` unset (what the default-export factory passes) means
148
- // "the conventional path, if this project ships one" — quiet when it
149
- // does not, since a brand-new project declares no actions yet.
150
- const hostInputReady = runtime || !gameDebugRegistry
151
- ? null
152
- : wireGameInputSeams(host, gameDebugRegistry, {
153
- id,
154
- inputMapPath: inputMapPath ?? DEFAULT_INPUT_MAP_PATH,
155
- optionalInputMap: inputMapPath === undefined,
156
- });
157
- // Actions must exist before any component's init()/first frame reads
158
- // them — wait for the (never-rejecting) input-map load before the first
159
- // commit.
160
- await (runtime?.inputMapReady ?? hostInputReady);
161
- // The world's own colour pipeline. Applied AFTER `root.configure()` below — see the comment
162
- // at its call site for why "before" is the version that silently does nothing.
163
- let restoreRendererConfig = () => { };
164
- const canvas = host.surface.canvas;
165
- const root = createRoot(canvas);
166
- // `RootState` (the live scene/camera/gl fiber built) only arrives via
167
- // the `onCreated` callback — `root.render()`'s return value is
168
- // TECHNICALLY the same store, but `onCreated` is the hook the design
169
- // doc's §1.C sketch names, and waiting for it (rather than assuming
170
- // the first commit already ran synchronously) is the honest choice
171
- // under React 19's concurrent renderer, which does not guarantee a
172
- // synchronous first commit the way legacy ReactDOM.render did.
173
- let resolveState;
174
- const statePromise = new Promise((resolve) => {
175
- resolveState = resolve;
176
- });
177
- let resolveEffectsReady;
178
- const effectsReady = new Promise((resolve) => {
179
- resolveEffectsReady = resolve;
180
- });
181
- // Rendered after the game's content. React runs passive effects in tree
182
- // order, so when this fires every useDebugProvider/useDebugCommand in
183
- // the initial tree has registered. A Play acknowledgement is a claim
184
- // that the mounted game's command surface exists, not merely that Fiber
185
- // allocated a scene.
186
- function MountEffectsReady() {
187
- useEffect(resolveEffectsReady, []);
188
- return null;
189
- }
190
- // `root.configure()` is ASYNC in fiber v9 (`Promise<ReconcilerRoot>`)
191
- // — the design doc's §1.C sketch shows it called synchronously; the
192
- // real v9 API (verified against
193
- // `node_modules/@react-three/fiber/dist/declarations/src/core/index.d.ts`)
194
- // requires awaiting it before `render()`.
195
- await root.configure({
196
- // The engine's renderer, not a second one (design §1.C) — fiber
197
- // renders THROUGH `host.renderer` instead of constructing its own
198
- // `WebGLRenderer`.
199
- gl: host.renderer,
200
- // …and the host's shadow map, not fiber's.
201
- //
202
- // `configure()` runs `gl.shadowMap.enabled = !!shadows`
203
- // UNCONDITIONALLY, against its own default of `false`. Passing nothing
204
- // therefore does not mean "leave it alone" — it means "turn shadows
205
- // off on the renderer the host just configured". Every three root
206
- // silently did that (FX-5): `applyRendererSettings` sets
207
- // `shadowMap.enabled` from the `shadows` render feature, which
208
- // defaults ON, and the setting had no surviving reader on this path —
209
- // so `castShadow`/`receiveShadow` all over a world produced nothing
210
- // and the scene read flat, with no error anywhere.
211
- //
212
- // Reading it back off the renderer is exactly "don't clobber": a
213
- // boolean also pins `shadowMap.type` to `PCFSoftShadowMap`, which is
214
- // what `createHostRenderer` already sets, so the round trip is
215
- // lossless and the HOST stays the one deciding.
216
- //
217
- // Optional-chained even though `WebGLRenderer.shadowMap` is not
218
- // optional: a headless/stub host renderer is a real shape in this
219
- // repo's tests, and fiber's own `if (gl.shadowMap)` guard means the
220
- // value it reads there is ignored anyway.
221
- shadows: host.renderer.shadowMap?.enabled ?? false,
222
- // The engine's gated loop is the ONLY loop — fiber must never run
223
- // its own rAF (that would defeat editor pause; design §1.C's
224
- // `drivesOwnLoop: false` contract, guide §2's loop-model note).
225
- frameloop: 'never',
226
- // `RenderProps.size` types as the FULL `Size` (width/height/top/left),
227
- // not `Partial<Size>` — the design sketch's `{ width, height }` alone
228
- // does not satisfy fiber v9's real type, so `top`/`left` are pinned
229
- // to 0 (this bridge always fills its whole canvas; no offset
230
- // viewport in v1).
231
- size: { width: host.surface.width, height: host.surface.height, top: 0, left: 0 },
232
- // Fiber's pointer-event manager, passed EXPLICITLY.
233
- //
234
- // This comment used to say `events` was "deliberately left at fiber's
235
- // default (binds to `gl.domElement`)". That was false, and it cost
236
- // every R3F world in this engine its mesh-level pointer props —
237
- // `onClick`, `onPointerOver`, `onPointerMissed` were dead everywhere.
238
- // There IS no default for a bare `createRoot`: the
239
- // `events = createPointerEvents` default lives in the `<Canvas>`
240
- // COMPONENT, which this bridge does not use, and `configure` gates on
241
- // `if (events && !state.events.handlers)`. With `events` undefined the
242
- // store kept its initial `{ priority, enabled, connected: false }` —
243
- // no handlers, nothing bound to the canvas, no error.
244
- //
245
- // Found by the WO-P6 builder, whose control experiment isolated the
246
- // variable properly: the same stub renderer, the same
247
- // `frameloop: 'never'`, the same jsdom canvas and pointer sequence,
248
- // differing ONLY in passing `events` — and `onClick` fired. Fiber is
249
- // fine under a host-driven loop; we omitted one property.
250
- //
251
- // (Design O1's open question about a delegating multi-canvas input
252
- // router still only matters for a shell+net composite; this bridge is
253
- // the single-canvas world O1 says is unaffected.)
254
- events: pointerEvents,
255
- onCreated: (state) => resolveState(state),
256
- });
257
- // THE WORLD'S OWN COLOUR PIPELINE — and it must land HERE, after `configure()`.
258
- //
259
- // `configure()` writes `outputColorSpace` and `toneMapping` onto the renderer
260
- // UNCONDITIONALLY, from its own `linear`/`flat` props whose defaults are `false`/`false` —
261
- // i.e. sRGB output and ACES. That is the SAME trap as the `shadows` note above, and it is
262
- // silent: applying a world's config before `configure()` leaves every field set and every
263
- // pixel unchanged (measured on the squash-the-creeps Godot port — the ground stayed at byte
264
- // 138 with `toneMapping: 'none'` declared and applied).
265
- //
266
- // Applying it here is still before any frame: `advance()` only runs from `update()` below,
267
- // and `render()` on the next line merely reconciles.
268
- if (rendererConfig !== undefined) {
269
- restoreRendererConfig = applyWorldRendererConfig(host.three, host.renderer, rendererConfig);
270
- }
271
- // No `<StrictMode>` (design §1.C: "no StrictMode: host mounts once")
272
- // — this bridge mounts exactly once per `mount()` call; StrictMode's
273
- // deliberate double-invoke of effects would double-subscribe
274
- // `useFrame` callbacks against a host loop that only ticks once per
275
- // frame. The `EngineBridge` provider threads the component runtime
276
- // (above) into the tree (W3), where `useGameContext()` reads it.
277
- // R3F IS React, so the React seam every dom root already has —
278
- // `useWorldState`, `useDebugProvider`/`useDebugCommand`/`useDebugEmit`
279
- // (`../react/world-state`) — should work identically inside a three root.
280
- // Without this provider a behavior-as-a-hook could only reach the debug
281
- // registry by destructuring `useGameContext()` and calling
282
- // `debug?.registerStateProvider(...)` by hand, which has no unmount
283
- // story: the template's follow-camera hook leaked a provider per remount
284
- // until this landed. `host.game` is genuinely absent in bare/headless
285
- // hosts, so the provider is conditional and those hooks stay the inert
286
- // no-ops they already document themselves to be.
287
- //
288
- // READ FROM `host.game`, NEVER FROM `runtime` — the two are not the same
289
- // question, and conflating them silently dark-ended the seam for every
290
- // scaffolded game. The IDIOMATIC three world (`export default function
291
- // World()`) mounts through `resolveR3FEntryAdapter` with
292
- // `engineRuntime: false` (`r3f-root-factory.tsx`), so it has no
293
- // `runtime` and no `bridge` — by design, because the vgai runtime
294
- // CONTEXT (`useGameContext`) must not enter an ecosystem-native tree.
295
- // `WorldProvider` is not that context: it carries the Game handle the
296
- // host already holds, and it is the only way a game's own
297
- // `useDebugProvider('stat', …)` can reach the ONE game-scoped registry
298
- // the editor/`vgai eval` read. Keyed off `runtime`, every
299
- // default-exported world's registrations no-opped in silence and
300
- // `vgai eval 'await game.providers()'` listed only the built-ins.
301
- // The canvas lane never had the gap (`canvas-react/
302
- // pixi-react-root-factory.tsx` keeps its entry runtime on).
303
- const game = host.game;
304
- // `SoftParticleDepth` trails the world's own tree deliberately — see its
305
- // docblock for why the subscription must land last. A no-context design
306
- // mount omits this fiber callback because the adopting editor viewport
307
- // runs the pass with its own renderer and camera instead.
308
- const world = createElement(Fragment, null, content, runtime && typeof host.renderer.getContext === 'function'
309
- ? createElement(SoftParticleDepth, { key: 'vgai-soft-particle-depth' })
310
- : null, createElement(MountEffectsReady, { key: 'vgai-mount-effects-ready' }));
311
- // The two providers are INDEPENDENT: `EngineBridge` rides `bridge` (the
312
- // vgai runtime context, absent for a native default-exported world),
313
- // `WorldProvider` rides `host.game` (the React seam, present whenever a
314
- // Game hosts this world). Nesting the second inside the first is what
315
- // made the seam a hostage of the first.
316
- const provided = game ? createElement(WorldProvider, { game }, world) : world;
317
- root.render(bridge ? createElement(EngineBridge.Provider, { value: bridge }, provided) : provided);
318
- // A reconcile-time crash (e.g. a missing `extend` catalogue entry)
319
- // surfaces as an uncaught window error and `onCreated` never fires —
320
- // without this guard, `mount()` would await `statePromise` FOREVER and
321
- // silently wedge every world declared after this one (roots mount
322
- // sequentially). Convert that class of failure into a loud mount error.
323
- const state = await new Promise((resolve, reject) => {
324
- const onError = (event) => {
325
- cleanup();
326
- root.unmount();
327
- runtime?.dispose();
328
- if (!runtime)
329
- gameDebugRegistry?.strip(id);
330
- restoreRendererConfig();
331
- reject(new Error(`r3f-adapter: fiber crashed before its first commit — ${event.message} ` +
332
- '(mount() fails loudly instead of hanging on onCreated)'));
333
- };
334
- const timer = setTimeout(() => {
335
- cleanup();
336
- root.unmount();
337
- runtime?.dispose();
338
- if (!runtime)
339
- gameDebugRegistry?.strip(id);
340
- restoreRendererConfig();
341
- reject(new Error('r3f-adapter: onCreated did not fire within 10s — the R3F tree never reached ' +
342
- 'its first commit (mount() fails loudly instead of hanging)'));
343
- }, 10_000);
344
- const cleanup = () => {
345
- clearTimeout(timer);
346
- window.removeEventListener('error', onError);
347
- };
348
- window.addEventListener('error', onError);
349
- void Promise.all([statePromise, effectsReady]).then(([s]) => {
350
- cleanup();
351
- resolve(s);
352
- });
353
- });
354
- // Clock hardening (field-proven by the first external adopter,
355
- // alien-stories' proto): fiber's internal `update()` calls
356
- // `state.clock.getDelta()` BEFORE its `frameloop:'never'` branch, and a
357
- // RUNNING (or autoStart) `THREE.Clock` accumulates WALL time into
358
- // `elapsedTime` as a side effect — skewing the deltas the 'never'
359
- // branch derives from the game timestamps `update(dt)` feeds below.
360
- // Stopped + autoStart=false makes `getDelta()` a pure no-op, so
361
- // `useFrame` deltas come from game time alone.
362
- state.clock.autoStart = false;
363
- state.clock.stop();
364
- // Backfill the component runtime's ctx with fiber's real scene/camera
365
- // (W3 — see the HONESTY NOTE above). This happens before the first
366
- // `update(dt)` tick, so no component ever observes them missing.
367
- runtime?.setSceneCamera(state.scene, state.camera);
368
- // THE STORE IS THE TRUTH, NOT THE `onCreated` SNAPSHOT.
369
- //
370
- // `state` is one immutable zustand snapshot: fiber's store is
371
- // `create<RootState>()`, and every `set(...)` produces a NEW state
372
- // object rather than mutating this one. So a world that legitimately
373
- // REPLACES the default camera — `useThree(s => s.set)({ camera })`,
374
- // which is exactly what drei's `makeDefault` does — never reaches
375
- // anything read off `state` directly.
376
- //
377
- // That is not cosmetic: fiber's own `update()` renders
378
- // `state.gl.render(state.scene, state.camera)` off the state object it
379
- // is HANDED, so `advance(t, true, state)` below pinned the render
380
- // camera to fiber's default `PerspectiveCamera` at (0, 0, 5) forever.
381
- // Measured on the squash-the-creeps Godot port (WO-G14): the world set
382
- // its authored `OrthographicCamera` and the frame still rendered from
383
- // (0, 0, 5) — a point INSIDE that game's 60x2x60 ground box, so every
384
- // ground polygon was back-facing, was culled, and the "ground" was the
385
- // cleared canvas. Nothing anywhere reported an error; edit mode looked
386
- // right because the editor renders with its own camera.
387
- //
388
- // `state.get()` is fiber's own `StoreApi.getState`, carried on
389
- // `RootState` for precisely this reason, so reading it per frame is the
390
- // supported way to see the live root. It is a plain map read.
391
- const live = () => state.get();
392
- // --- Live render vitals (issue #1504), for an R3F world -----------------
393
- // The SAME door the classic adapter seeds
394
- // (`../adapter/setup-three-root-adapter.ts`), seeded here for the same
395
- // reason: a running game must be able to explain its own frame cost
396
- // through the debug registry, with no capability to install and nothing
397
- // for a game to write. It has to be seeded in BOTH adapters because they
398
- // are two independent mount paths onto the same `Game` — and a TSX/R3F
399
- // world is the only shape a scaffolded project's three root has, so a
400
- // door wired only into the classic path is a door no scaffolded project
401
- // ever gets (issue #1518: `game.state('render.vitals')` answered "no
402
- // state provider registered" in the field while every other debug
403
- // reading of the same world worked).
404
- //
405
- // The gate is the same three conditions, minus the one that cannot
406
- // arise: `devBuildEnabled()` (`../runtime/dev-build.ts`, the ONE owner
407
- // of "is this a dev context" — a ship build registers nothing and pays
408
- // nothing), and a `Game` shell (the readings are folded out of that
409
- // game's profiler frames). "Not headless" needs no term here: a headless
410
- // mount returned above, before fiber was ever configured.
411
- //
412
- // Two module instances, one answer: under the packaged editor this
413
- // module is served out of the PROJECT's own Vite graph (see
414
- // `packages/editor/vite-plugin-react-world-runtime.ts` — the packaged
415
- // shell imports `resolveR3FEntryAdapter` through the project-rooted
416
- // server precisely so fiber/react/three identity is the project's), so
417
- // `import.meta.env.DEV` here is the project server's `true`, not the
418
- // prebuilt shell's `false`.
419
- //
420
- // OWNER of the registration+fold: this mount, ended by `dispose()` below
421
- // (the fold) and `runtime.dispose()`'s scoped `strip(id)` (the
422
- // registrations) — the same split the classic adapter documents.
423
- let renderVitals = null;
424
- if (host.game && devBuildEnabled()) {
425
- // Same auto-enable as the classic adapter: the readings are folded out
426
- // of profiler frames, and the profiler is a flag the editor already
427
- // sets on play — under the dev gate this is the same cost arriving a
428
- // little earlier, not a new one.
429
- host.game.profiler.enabled = true;
430
- if (!gameDebugRegistry) {
431
- throw new Error('r3f-adapter: mounted Game has no debug registry.');
432
- }
433
- renderVitals = registerRenderVitals({
434
- registry: gameDebugRegistry,
435
- worldId: id,
436
- profiler: host.game.profiler,
437
- scene: live().scene,
438
- renderer: host.renderer,
439
- });
440
- }
441
- // Engine-owned render introspection, built over the SAME renderer and
442
- // scene Fiber uses. A design-time stand-in has no context and a
443
- // headless mount returned above, so both honestly omit the capability.
444
- // `frameCaptureContextFor` is the same construction gate as the classic
445
- // Three adapter; no editor-side renderer inference is involved.
446
- const rendererContext = typeof host.renderer.getContext === 'function' ? host.renderer.getContext() : undefined;
447
- const frameCaptureContext = frameCaptureContextFor(false, rendererContext);
448
- const frameCapture = frameCaptureContext
449
- ? createWebGLFrameCapture(frameCaptureContext)
450
- : null;
451
- const renderDebugWiring = frameCapture
452
- ? createRenderDebugAdapter({
453
- capture: frameCapture,
454
- scene: state.scene,
455
- memory: () => collectRenderMemory(live().scene, host.renderer.info),
456
- })
457
- : null;
458
- // The session debug registry is host instrumentation, not component
459
- // context. Publishing the existing adapter here lets coverage and
460
- // editor panels see the same door without wrapping the R3F tree.
461
- const systemAdapters = runtime?.systemAdapters ?? (gameDebugRegistry ? { debug: gameDebugRegistry.adapter } : {});
462
- if (renderDebugWiring)
463
- systemAdapters.renderDebug = renderDebugWiring.adapter;
464
- // The engine drives every `useFrame` through the mounted world's
465
- // `update(dt)` hook, never off a raw host-loop callback. That is the
466
- // whole pause story: `runFrameImpl` (`runtime/game.ts`) calls
467
- // `mounted.update?.(dt)` per substep for a host-driven
468
- // (`drivesOwnLoop: false`) world and SKIPS it while that world is
469
- // frozen, and `Game.play.step()` ticks it exactly once — whereas any
470
- // callback run outside `game.runFrame`'s gate would fire
471
- // unconditionally, pause or not. Advancing fiber from such a callback
472
- // LOOKS right and silently breaks acceptance gate 2 — proven by the
473
- // 36-r3f-first-party e2e, whose paused instance-matrix samples kept
474
- // moving until this moved to `update`. (The former `ThreeHostContext.loop`
475
- // seam that made the wrong path reachable was deleted, zero consumers.)
476
- //
477
- // `advance(timestamp, runGlobalEffects, state)`'s `timestamp` is
478
- // consumed as `THREE.Clock.elapsedTime` DIRECTLY when
479
- // `frameloop:'never'` (verified against fiber's `update()`
480
- // implementation, not just its `.d.ts` — the declared signature alone
481
- // doesn't say this): `delta = timestamp - clock.elapsedTime;
482
- // clock.elapsedTime = timestamp`. So `timestamp` must be a
483
- // monotonically increasing SECONDS value — GAME time, not wall time:
484
- // `update` simply isn't called while this world is frozen, so
485
- // accumulating its `dt` (seconds; the loop runs a fixed 1/60 timestep)
486
- // means fiber's clock does not advance across a pause. Wall clock
487
- // (`performance.now()`) would leak the pause duration into the first
488
- // resumed frame as one giant `useFrame` delta — the exact "time passed
489
- // while frozen" illusion acceptance gate 2 forbids.
490
- let elapsed = 0;
491
- /** The legacy component ctx camera last backfilled by `update()`. */
492
- let backfilledCamera = state.camera;
493
- return {
494
- kind: 'three',
495
- // Fiber's REAL `THREE.Scene`/`THREE.Camera`. Checkout development
496
- // dedupes `three`; a registry-installed project and the prebundled
497
- // editor can legitimately have distinct constructor identities, so
498
- // editor boundaries recognize Three's stable structural brands
499
- // (`isScene`, `isCamera`) rather than relying only on `instanceof`.
500
- // `state.camera`'s type (`Camera = (OrthographicCamera |
501
- // PerspectiveCamera) & { manual?: boolean }`) is a structural subtype
502
- // of `THREE.Camera`, so no cast is needed.
503
- //
504
- // Both are GETTERS over the live store, not snapshot fields: the host
505
- // and the editor must see the camera the world actually renders
506
- // through, including one the world swapped in after the first commit
507
- // (see `live` above).
508
- get scene() {
509
- return live().scene;
510
- },
511
- get camera() {
512
- return live().camera;
513
- },
514
- drivesOwnLoop: false,
515
- // REPORTED for a host that draws this scene with a renderer other than
516
- // the one it mounted us against — see `MountedThreeRoot.rendererConfig`.
517
- // It is the same object already applied to `host.renderer` above; the
518
- // report never applies anything itself.
519
- rendererConfig: rendererConfig,
520
- // Adapter surface: `debug` (the shared game registry) and, when a real
521
- // WebGL2 context exists, `renderDebug` are engine-seeded. Game code
522
- // adds its own capabilities via `ctx.registerSystemAdapter` exactly
523
- // as in a classic world.
524
- systems: systemAdapters,
525
- update(dt) {
526
- elapsed += dt;
527
- const current = live();
528
- // A world that swapped the default camera gets it onto `ctx` too —
529
- // `ctx.camera` and the rendered camera are the same fact, and a
530
- // component reading one while the frame uses the other is the
531
- // silent kind of wrong. Identity-gated, so this is one comparison
532
- // per frame in the overwhelmingly common no-swap case.
533
- if (runtime && current.camera !== backfilledCamera) {
534
- backfilledCamera = current.camera;
535
- runtime.setSceneCamera(current.scene, current.camera);
536
- }
537
- // Engine phases FIRST (registered systems mutate transforms),
538
- // then fiber's advance (useFrame callbacks + the actual render see
539
- // the fresh state). Both are host-gated: neither runs while frozen.
540
- runtime?.systems.run(dt);
541
- renderDebugWiring?.beforeRender();
542
- try {
543
- const profiler = host.game?.profiler;
544
- if (!profiler?.enabled) {
545
- advance(elapsed, true, current);
546
- return;
547
- }
548
- // Issue #1504's measurement, produced where the draw actually
549
- // happens. `advance()` under `frameloop: 'never'` is what calls
550
- // `gl.render(scene, camera)`, so this bracket — not a phase span
551
- // inside `systems` — is the frame's CPU submission cost, and it is
552
- // also what makes this frame a PRESENTATION as far as the vitals
553
- // fold is concerned (`../dev/render-vitals.ts`: no bracket, no
554
- // display frame). The profiler's phase clock is a stack, so
555
- // bracketing here cannot truncate an enclosing phase.
556
- const info = host.renderer.info;
557
- const passesBefore = info?.render.frame ?? 0;
558
- profiler.beginPhase();
559
- try {
560
- advance(elapsed, true, current);
561
- }
562
- finally {
563
- profiler.endPhase(RENDER_SUBMIT_PHASE);
564
- }
565
- // Checked, not assumed — a duck-typed/design-time renderer supplies
566
- // only what a mount needs, and a mount with no counters must report
567
- // NOTHING rather than throw once per frame (the classic adapter's
568
- // `postFrame` pays for the same assumption for the same reason).
569
- // `gpuMs` is null: this path runs no GPU timer, and the profiler
570
- // keeps the previous reading rather than inventing a zero.
571
- if (info) {
572
- profiler.reportRender({
573
- gpuMs: null,
574
- drawCalls: info.render.calls,
575
- triangles: info.render.triangles,
576
- geometries: info.memory.geometries,
577
- textures: info.memory.textures,
578
- renderPasses: info.render.frame - passesBefore,
579
- });
580
- }
581
- }
582
- finally {
583
- renderDebugWiring?.afterRender();
584
- }
585
- },
586
- resize(width, height) {
587
- live().setSize(width, height);
588
- },
589
- dispose() {
590
- // Reject/restore a pending capture while its renderables still
591
- // exist, then unmount component effects and strip runtime state.
592
- renderDebugWiring?.dispose();
593
- // The editor audits and disposes the mounted game's window/document realm immediately
594
- // after `session.stop()`. Fiber's ordinary `unmount()` submits a concurrent reconciler
595
- // update, so component effect cleanups could still be pending at that boundary and the
596
- // audit would truthfully report/reclaim listeners the world was about to remove itself.
597
- // Force the unmount COMMIT (not fiber's deliberately delayed renderer/context disposal)
598
- // to finish here: a stopped root owns no live component lifecycle beyond this method.
599
- flushSync(() => root.unmount());
600
- // The vitals fold owns two `profiler.subscribe` registrations, and
601
- // `runtime.dispose()`'s `strip(id)` removes the REGISTRATIONS only —
602
- // this is the one path that ends the subscriptions, exactly as the
603
- // classic adapter's teardown documents. Before the strip, so a
604
- // subscription can never outlive the door it feeds.
605
- renderVitals?.dispose();
606
- renderVitals = null;
607
- runtime?.dispose();
608
- // The no-context path has no `runtime.dispose()` to carry its scoped
609
- // strip, so it ends its own registrations here — the same
610
- // `strip(id)`, at the same moment. That is what ends this world's
611
- // PROVIDERS AND COMMANDS (the render-vitals door above is the one
612
- // this path actually registers under `id`); sibling roots are
613
- // untouched.
614
- //
615
- // It does NOT end the input seams `wireGameInputSeams` set, and
616
- // nothing else does either: `strip` walks providers/commands only
617
- // (`runtime/debug-registry.ts`), so `virtualInputTargets`/
618
- // `inputActionsSources`/`inputTraceSources` keep this world's id
619
- // until a remount of the same id overwrites it. The runtime path has
620
- // always been identical, and the residue is inert rather than
621
- // hidden: every one of those closures reads GAME-scoped state
622
- // (`game.input`, `game.loop.fixedDt`, the game's seed), so a stale
623
- // entry actuates exactly what a live one would. What it can still do
624
- // is name a dead world in `DEBUG_INPUT_WORLD_NOT_FOUND`'s registered
625
- // list. Do not write "the strip clears the input seams" here again
626
- // without changing `strip` to actually do it.
627
- if (!runtime)
628
- gameDebugRegistry?.strip(id);
629
- restoreRendererConfig();
630
- },
631
- };
632
- },
633
- };
634
- }