@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
@@ -7,14 +7,9 @@
7
7
  // has a `.describe()` (repo policy — powers `scripts/generate-schema.ts` and
8
8
  // the T4.1 schema-walk coverage test).
9
9
  //
10
- // Naming: this file follows the repo's `XxxSchema` (Zod schema) / `Xxx`
11
- // (inferred type) convention used throughout `scene/schema/` the design
12
- // doc's §3 sketch names the bare consts (`AdapterRoot`) without the
13
- // `Schema` suffix; that sketch is explicitly a
14
- // "Zod sketch" the doc tells implementers to adapt to real code style
15
- // ("mirror the style in scene/schema/scene-file.ts" per CLAUDE.md), so the
16
- // schema consts here are suffixed and the bare names are reserved for the
17
- // inferred types, matching every other schema file in the repo.
10
+ // Naming: `XxxSchema` is the Zod schema, `Xxx` the inferred type. Every schema
11
+ // file in this repo reads that way, so the bare names stay reserved for the
12
+ // types.
18
13
  //
19
14
  // Adapter surface values mirror `AdapterSurface` in
20
15
  // `packages/engine/src/runtime/game.ts` (`'three' | 'canvas' | 'dom'`)
@@ -56,7 +51,7 @@ export const GAME_MANIFEST_VERSION = 2 as const;
56
51
  // which is exactly why react is a `dom` root rather than its own kind.
57
52
  //
58
53
  // The pre-rename spellings `threejs`/`pixijs`/`react` are REMOVED, not
59
- // normalized (WO-LR1 item 2, the legacy-removal doctrine at
54
+ // normalized (the legacy-removal doctrine at
60
55
  // ARCHITECTURE-CORE §Vocabulary). Normalization was a second way to say the
61
56
  // same thing: two spellings both parsed, so both kept being written, and
62
57
  // SECOND readers grew (the dev server's surface probe, the hosted-editor
@@ -83,8 +78,8 @@ function removedSpellingMessage(bad: string, where: 'adapter' | 'adapter.surface
83
78
  const canonical =
84
79
  where === 'adapter' ? REMOVED_BARE_ADAPTER_SPELLING[bad] : REMOVED_SURFACE_SPELLING[bad];
85
80
  return (
86
- `Game manifest: \`${where}\`: "${bad}" — that spelling was REMOVED (WO-LR1; ` +
87
- 'ARCHITECTURE-CORE §Vocabulary). The surface vocabulary is ' +
81
+ `Game manifest: \`${where}\`: "${bad}" — that spelling was REMOVED ` +
82
+ '(ARCHITECTURE-CORE §Vocabulary). The surface vocabulary is ' +
88
83
  "'three' | 'canvas' | 'dom' — what the HOST HANDS the root, never which " +
89
84
  `library it uses.\nFix: change \`${where}\` to "${canonical}".`
90
85
  );
@@ -219,34 +214,6 @@ export const RootAdapterSchema = z
219
214
  'FAILS by name. A game that needs a long boot (streamed world assets, a menu ' +
220
215
  'the player must clear) raises it; the default is 10s.',
221
216
  ),
222
- world: z
223
- .object({
224
- entry: z
225
- .string()
226
- .describe(
227
- "Project-relative path to the game's OWN module exporting that component",
228
- ),
229
- export: z
230
- .string()
231
- .optional()
232
- .describe(
233
- 'Named export to read from that module; omit when the module default-exports it',
234
- ),
235
- })
236
- .strict()
237
- .optional()
238
- .describe(
239
- "The component this root's WORLD is, in the game's own source — a three root " +
240
- 'whose scene is a React/R3F component tree names it here. Two readers, and ' +
241
- 'they mount different things: the editor mounts THIS component alone as the ' +
242
- "root's design-time document (a still scene — its frameloop runs ONE bounded " +
243
- 'settle at mount, so physics-owned bodies rest at their authored spawn, and ' +
244
- 'is then frozen forever; nothing moves in Edit), while `entry` ' +
245
- 'above stays what PLAY mounts (the whole game, its own composition, its own ' +
246
- 'HUD and loop). Declaring it is what lets a scene be a scene in Edit rather ' +
247
- "than the running game's mount surfaces. A game whose world is not a " +
248
- 'component omits it and keeps mounting through `entry` in both modes.',
249
- ),
250
217
  })
251
218
  .strict()
252
219
  .describe(
@@ -306,6 +273,37 @@ export const AdapterRootSchema = z
306
273
  "the HUD is styled on the UI board's story cards and on the game surface without the " +
307
274
  'editor seeing a single one of those rules. The files themselves are never modified.',
308
275
  ),
276
+ world: z
277
+ .object({
278
+ entry: z
279
+ .string()
280
+ .describe("Project-relative path to the game's OWN module exporting that component"),
281
+ export: z
282
+ .string()
283
+ .optional()
284
+ .describe(
285
+ 'Named export to read from that module; omit when the module default-exports it',
286
+ ),
287
+ })
288
+ .strict()
289
+ .optional()
290
+ .describe(
291
+ "The component this root's WORLD is, in the game's own source. Two readers, and they " +
292
+ "mount different things: the EDITOR mounts THIS component alone as the root's " +
293
+ 'design-time document, while `entry` above stays what PLAY mounts (the whole game — ' +
294
+ 'its own composition, its own HUD, its own loop). It sits on the ROOT rather than ' +
295
+ 'inside any one adapter because the split it names is not about ingest: a game whose ' +
296
+ 'runtime entry is a SHELL over the thing an author actually edits has this shape ' +
297
+ 'whether the source is vendored or first-party. Two shipped cases, one field: an ' +
298
+ 'ingested game whose `entry` mounts game+HUD+loop while the named export is the world ' +
299
+ 'alone; and a translated Unity/Godot port, whose `entry` is a scene HOST that mounts ' +
300
+ 'the scene the built player starts on (`EditorBuildSettings` index 0 — routinely a ' +
301
+ 'menu with no 3D content at all) while the named export opens the scene an author ' +
302
+ "edits. That is Unity's own split: build settings say where the PLAYER starts; the " +
303
+ 'Scene view opens a scene, and Unity keeps that fact somewhere else entirely. A game ' +
304
+ 'whose world is not a separate component omits this and keeps mounting through ' +
305
+ '`entry` in both modes.',
306
+ ),
309
307
  zOrder: z
310
308
  .number()
311
309
  .int()
@@ -424,6 +422,7 @@ export const GameManifestSchema = z
424
422
  'identity pin, not a range',
425
423
  ),
426
424
  })
425
+ .strict()
427
426
  .describe('Engine version pin'),
428
427
  roots: z
429
428
  .array(AdapterRootSchema)
@@ -498,21 +497,25 @@ export const GameManifestSchema = z
498
497
  .describe('Optional editor labels keyed by adapter-root id'),
499
498
  groups: z
500
499
  .array(
501
- z.object({
502
- id: z.string().describe('Unique editor-only group id'),
503
- label: z.string().describe('Editor-only group label'),
504
- roots: z
505
- .array(z.string())
506
- .min(1)
507
- .describe('Adapter-root ids shown under this editor-only group'),
508
- }),
500
+ z
501
+ .object({
502
+ id: z.string().describe('Unique editor-only group id'),
503
+ label: z.string().describe('Editor-only group label'),
504
+ roots: z
505
+ .array(z.string())
506
+ .min(1)
507
+ .describe('Adapter-root ids shown under this editor-only group'),
508
+ })
509
+ .strict(),
509
510
  )
510
511
  .optional()
511
512
  .describe('Optional editor-only hierarchy groups'),
512
513
  })
514
+ .strict()
513
515
  .optional()
514
516
  .describe('Editor projection for native adapter roots'),
515
517
  })
518
+ .strict()
516
519
  .optional()
517
520
  .describe('Editor-only authoring metadata; never runtime ownership'),
518
521
  server: z
@@ -558,6 +561,7 @@ export const GameManifestSchema = z
558
561
  'Deploy-baked managed-relay credentials. NEVER hand-authored in source — written only into the staged manifest by `vgai deploy` and fetched at runtime by the standalone client',
559
562
  ),
560
563
  })
564
+ .strict()
561
565
  .optional()
562
566
  .describe('Multiplayer server config (Colyseus), if this game registers a room'),
563
567
  resolution: z
@@ -565,6 +569,7 @@ export const GameManifestSchema = z
565
569
  width: z.number().describe('Canvas width in pixels'),
566
570
  height: z.number().describe('Canvas height in pixels'),
567
571
  })
572
+ .strict()
568
573
  .optional()
569
574
  .describe('Canvas resolution'),
570
575
  rendering: z
@@ -599,6 +604,7 @@ export const GameManifestSchema = z
599
604
  'runtime reader is the debug-bridge installer (D18).',
600
605
  ),
601
606
  })
607
+ .strict()
602
608
  .optional()
603
609
  .describe(
604
610
  'Debug-bridge production gating (D18) — governs whether ?vgai-debug=1 installs ' +
@@ -631,6 +637,7 @@ export const GameManifestSchema = z
631
637
  "omit to fall back to the runtime's own fixed default seed.",
632
638
  ),
633
639
  })
640
+ .strict()
634
641
  .optional()
635
642
  .describe(
636
643
  'Seeded-RNG determinism contract (D15) — governs ' +
@@ -689,6 +696,7 @@ export const GameManifestSchema = z
689
696
  'deterministic under a seed.',
690
697
  ),
691
698
  })
699
+ .strict()
692
700
  .refine((budget) => Object.values(budget).some((value) => value !== undefined), {
693
701
  message:
694
702
  'Game manifest: `budget` must set at least one metric (frameCpuMsP95, physicsMsP95, ' +
@@ -5,17 +5,13 @@
5
5
  * shape is defined in absolute world units at creation time. Three.js meshes,
6
6
  * by contrast, scale visually for free via the normal Object3D hierarchy. To
7
7
  * keep the physical collider matching what's on screen, the LOADER must bake
8
- * the entity's composed WORLD scale (self × every transformed ancestor — see
9
- * T1.1's world-transform composition) into the collider's dimensions at
10
- * creation time.
8
+ * the entity's composed WORLD scale (self × every transformed ancestor) into
9
+ * the collider's dimensions at creation time.
11
10
  *
12
- * This is the ONE place that does that math, so both the runtime loader
13
- * (scene-loader.ts `createPhysicsBody`) and the editor's collider gizmo
14
- * (gizmo-registry.ts) agree on exactly the same numbers previously the
15
- * editor gizmo only rendered the entity's own live Object3D scale (via normal
16
- * Three.js parenting) while the Rapier body ignored scale entirely, so the
17
- * two silently agreed by coincidence for uniform scale and disagreed for
18
- * everything else.
11
+ * This is the ONE place that does that math, so every caller — the physics
12
+ * body creation path and the editor's collider gizmo alike — agrees on
13
+ * exactly the same numbers. Two copies silently agree by coincidence for
14
+ * uniform scale and disagree for everything else.
19
15
  *
20
16
  * Round colliders (ball/capsule) can only represent a uniform radius — Rapier
21
17
  * has no ellipsoid/elliptical-capsule shape. A non-uniform world scale on a
@@ -40,8 +36,8 @@ export interface ColliderWorldDimensions {
40
36
 
41
37
  /**
42
38
  * Compute a collider's WORLD-space dimensions given the entity's composed
43
- * world scale (self × ancestors, per-axis, in the entity's own local frame
44
- * X/Y/Z order matches `SceneEntity.transform.scale`).
39
+ * world scale (self × ancestors, per-axis, in the entity's own local frame,
40
+ * in X/Y/Z order).
45
41
  *
46
42
  * @param collider - the entity's authored (unscaled) collider definition.
47
43
  * @param worldScale - composed world scale `[x, y, z]`.
@@ -19,25 +19,22 @@
19
19
  */
20
20
 
21
21
  import {
22
+ type CaptureWaitOptions,
22
23
  documentVisibilityClock,
23
24
  startVisibleCaptureWindow,
24
- type VisibilityClock,
25
- type VisibleCaptureWindow,
26
25
  } from '../adapter/ingest/visible-capture-window';
27
26
 
28
27
  /**
29
- * Options for {@link SceneCapture2DHandle.waitForCapture} — the same shape the
30
- * three lane's `CaptureWaitOptions` has, because it is the same wait. Passing a
31
- * bare number is `{ timeoutMs }`, which is what every existing caller does.
28
+ * Options for {@link SceneCapture2DHandle.waitForCapture} — the WAIT's options,
29
+ * not this lane's, so they carry no `2D` suffix and are not declared here.
30
+ * They configure `visible-capture-window.ts`'s window, which both surfaces
31
+ * share; this lane used to restate them field-for-field under a `2D` name.
32
+ * The suffix marks what a surface genuinely owns
33
+ * ({@link CapturedRuntime2D} is a Pixi stage/renderer/app, and stays), and a
34
+ * budget of visible milliseconds is not that. Passing a bare number is
35
+ * `{ timeoutMs }`, which is what every existing caller does.
32
36
  */
33
- export interface CaptureWait2DOptions {
34
- /** The capture window, in VISIBLE milliseconds (default 10s). */
35
- timeoutMs?: number | undefined;
36
- /** Injected in tests; defaults to the document's own visibility. */
37
- visibility?: VisibilityClock | undefined;
38
- /** Live view of the wait while it runs, `null` the moment it ends. */
39
- onWait?: ((wait: VisibleCaptureWindow | null) => void) | undefined;
40
- }
37
+ export type { CaptureWaitOptions };
41
38
 
42
39
  /** A live Pixi runtime captured from an external PixiJS game. */
43
40
  export interface CapturedRuntime2D {
@@ -68,7 +65,7 @@ export interface SceneCapture2DHandle {
68
65
  * frame the tab draws after it comes forward is trapped exactly as it would
69
66
  * have been at boot. Rejects only when the window is spent while VISIBLE.
70
67
  */
71
- waitForCapture(options?: number | CaptureWait2DOptions): Promise<CapturedRuntime2D>;
68
+ waitForCapture(options?: number | CaptureWaitOptions): Promise<CapturedRuntime2D>;
72
69
  getDrawCount(): number;
73
70
  /** Pause/resume the captured game's own ticker (loop-host gating). */
74
71
  setPaused(paused: boolean): void;
@@ -138,7 +135,7 @@ export function installSceneCapture2D(
138
135
  else app.ticker.start();
139
136
  },
140
137
  waitForCapture(options) {
141
- const opts: CaptureWait2DOptions =
138
+ const opts: CaptureWaitOptions =
142
139
  typeof options === 'number' ? { timeoutMs: options } : (options ?? {});
143
140
  const timeoutMs = opts.timeoutMs ?? 10_000;
144
141
  if (captured) return Promise.resolve(captured);
@@ -27,10 +27,10 @@ import {
27
27
  } from 'react';
28
28
  import type { z } from 'zod';
29
29
  import type { RootStateObserver } from '../adapter';
30
+ import type { DebugCommandArgs } from '../runtime/debug-registry';
30
31
  import { getDebugRegistry } from '../runtime/debug-registry';
31
32
  import { createFrameSelectorCache, type Equals, shallow } from '../runtime/frame-selector-cache';
32
33
  import type { Game } from '../runtime/game';
33
- import type { DebugCommandArgs } from '../runtime/types';
34
34
 
35
35
  /** Re-exported for convenience — the opt-in equality for selectors that
36
36
  * return a fresh object/array/tuple every call (default is `Object.is`). */
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Fit a directional light's orthographic shadow box to the camera, for one
3
+ * frame — the geometry three does not supply.
4
+ *
5
+ * `DirectionalLightShadow` is an ordinary `OrthographicCamera` parented to the
6
+ * light, and it sits wherever the light's authored transform puts it, forever.
7
+ * That is fine for a scene the authored box contains and wrong for every game
8
+ * whose camera travels: the moment the player walks past the box, the casters
9
+ * out there stop having shadows, and no bias, map size or intensity fixes it
10
+ * because the geometry is simply outside the frustum. Three's own addon CSM is
11
+ * the multi-cascade answer to a different question and supplies neither of the
12
+ * two pieces below.
13
+ *
14
+ * ## The box is the frustum slice's BOUNDING SPHERE
15
+ *
16
+ * The standard fit, and the reason it cannot be authored as a constant: take
17
+ * the eight corners of the camera's frustum between its near plane and
18
+ * `maxDistance`, put the box centre at their centroid, and size the box to the
19
+ * largest distance from that centroid to a corner. That radius is decided by
20
+ * the camera's FIELD OF VIEW and aspect as much as by `maxDistance` — at a
21
+ * 70-degree vertical fov over 1024x600, a 40-unit slice has half-extents of
22
+ * 47.8 x 28.0 at its far plane and the sphere around it has radius ~58.9,
23
+ * nearly three times `maxDistance / 2`. A bounding SPHERE (rather than the
24
+ * tight box) is what makes the radius independent of camera yaw, so turning in
25
+ * place cannot resize the shadow.
26
+ *
27
+ * The corners are read through the camera's own `projectionMatrixInverse`, so
28
+ * a perspective and an orthographic camera are exact with no branch on type —
29
+ * and an asymmetric frustum (`setViewOffset`) is fitted over its real corners
30
+ * rather than an assumed centre.
31
+ *
32
+ * ## Texel snapping is not a polish detail
33
+ *
34
+ * A shadow box that follows a camera continuously re-rasterizes the same
35
+ * static geometry at sub-texel offsets, and the depth-test result flips along
36
+ * every shadow edge from frame to frame — the edges CRAWL, which is far more
37
+ * distracting than the missing shadows the fit is for. Quantizing the centre
38
+ * to whole shadow texels, in the LIGHT's own basis (the shadow map's u/v axes
39
+ * are the light's local x/y), makes the rasterized footprint of a static
40
+ * caster identical between frames. Depth (local z) is deliberately NOT snapped:
41
+ * it does not affect which texel a fragment lands in.
42
+ *
43
+ * ## What the caller keeps
44
+ *
45
+ * This is one pure function over three's own objects: it reads a camera and a
46
+ * quaternion and returns three numbers-and-a-vector. It moves nothing, and in
47
+ * particular it does not touch the light — how a fitted centre reaches the
48
+ * shadow camera is the caller's, because three derives the shadow camera's
49
+ * placement FROM the light every frame (`LightShadow.updateMatrices` copies the
50
+ * light's world position and looks at `light.target`), so the thing that has to
51
+ * move is the light NODE and only the caller knows how its light is parented,
52
+ * aimed and torn down. The depth range (`shadow.camera.near`/`far`) is the
53
+ * caller's for the same reason: it is what `shadow.bias` is normalized against.
54
+ *
55
+ * {@link DirectionalShadowFit.texelWorldSize} is published because everything
56
+ * sized in texels moves with the box — a receiver-side `shadow.normalBias`
57
+ * derived from a stale texel is wrong by exactly the factor the box grew, and
58
+ * it surfaces as self-shadow acne on curved casters rather than as anything a
59
+ * reviewer would call a shadow bug.
60
+ *
61
+ * Consumers today: `top-down-strategy`'s camera-fitted sun, and
62
+ * `godot-compat`'s Godot 3 `SHADOW_ORTHOGONAL` fit.
63
+ */
64
+ import { type Camera, Quaternion, Vector3 } from 'three';
65
+
66
+ /** The box a directional shadow is rendered through for one frame. */
67
+ export interface DirectionalShadowFit {
68
+ /** WORLD-space centre of the box, quantized to whole shadow texels in the light's basis. */
69
+ readonly center: Vector3;
70
+ /** Half-extent on both lateral axes — the frustum slice's bounding-sphere radius. */
71
+ readonly radius: number;
72
+ /** One shadow texel in world units at this radius: `2 * radius / mapSize`. */
73
+ readonly texelWorldSize: number;
74
+ }
75
+
76
+ const _inverseLight = new Quaternion();
77
+ const _corner = new Vector3();
78
+ const _centroid = new Vector3();
79
+ const _ndc = new Vector3();
80
+ /** The four lateral NDC corners; the slice's eight are these at two depths. */
81
+ const NDC_CORNERS: readonly (readonly [number, number])[] = [
82
+ [-1, -1],
83
+ [-1, 1],
84
+ [1, -1],
85
+ [1, 1],
86
+ ];
87
+
88
+ /** View-space depth `-z` as this camera's NDC z, through the camera's OWN projection — so the
89
+ * slice is exact for a perspective and an orthographic camera alike, with no branch on type. */
90
+ function ndcDepthOf(camera: Camera, viewDepth: number): number {
91
+ return _ndc.set(0, 0, -viewDepth).applyMatrix4(camera.projectionMatrix).z;
92
+ }
93
+
94
+ /**
95
+ * The bounding sphere of `camera`'s frustum between its near plane and
96
+ * `maxDistance`, with the centre quantized to whole shadow texels in the
97
+ * LIGHT's basis.
98
+ *
99
+ * `camera` is read through its own `projectionMatrix`/`projectionMatrixInverse`/`matrixWorld`, so a
100
+ * caller must have brought those up to date (three does this for every rendered camera).
101
+ * `maxDistance` is the view depth the shadow covers, measured from the camera and clamped to its
102
+ * far plane — there is nothing to shadow past the geometry the camera draws.
103
+ * `lightOrientation` is the light's WORLD quaternion, whose local x/y are the shadow map's own axes
104
+ * and therefore the axes the snap quantizes along. A `mapSize` of zero or less disables snapping
105
+ * rather than dividing by it.
106
+ */
107
+ export function fitDirectionalShadowToCamera(
108
+ camera: Camera,
109
+ lightOrientation: Quaternion,
110
+ maxDistance: number,
111
+ mapSize: number,
112
+ ): DirectionalShadowFit {
113
+ const frustum = camera as Camera & { near?: number; far?: number };
114
+ const camNear = typeof frustum.near === 'number' ? frustum.near : 0;
115
+ const camFar = typeof frustum.far === 'number' ? frustum.far : maxDistance;
116
+ // The two clamps are degenerate-input guards: far at least a hair past near, near never past far.
117
+ let far = Math.min(maxDistance, camFar);
118
+ far = Math.max(far, camNear + 0.001);
119
+ const near = Math.min(camNear, far);
120
+ const nearNdc = ndcDepthOf(camera, near);
121
+ const farNdc = ndcDepthOf(camera, far);
122
+
123
+ // The centroid of the eight corners. Both planes contribute four symmetric corners, so for an
124
+ // ordinary camera this is `(near + far) / 2` down -Z — but it is computed rather than assumed,
125
+ // because `setViewOffset` makes a frustum asymmetric and the fit is over the real corners.
126
+ _centroid.set(0, 0, 0);
127
+ for (const ndcZ of [nearNdc, farNdc]) {
128
+ for (const [x, y] of NDC_CORNERS) {
129
+ _centroid.add(_corner.set(x, y, ndcZ).applyMatrix4(camera.projectionMatrixInverse));
130
+ }
131
+ }
132
+ _centroid.multiplyScalar(1 / 8);
133
+
134
+ let radius = 0;
135
+ for (const ndcZ of [nearNdc, farNdc]) {
136
+ for (const [x, y] of NDC_CORNERS) {
137
+ radius = Math.max(
138
+ radius,
139
+ _corner.set(x, y, ndcZ).applyMatrix4(camera.projectionMatrixInverse).distanceTo(_centroid),
140
+ );
141
+ }
142
+ }
143
+ // One texel of margin, so the snapped box never clips its own outermost texel: the snap below
144
+ // moves the centre by up to half a texel on each lateral axis.
145
+ if (mapSize > 2) radius *= mapSize / (mapSize - 2);
146
+
147
+ const center = _centroid.clone().applyMatrix4(camera.matrixWorld);
148
+ const texelWorldSize = mapSize > 0 ? (2 * radius) / mapSize : 0;
149
+ if (texelWorldSize > 0) {
150
+ center.applyQuaternion(_inverseLight.copy(lightOrientation).invert());
151
+ center.x = Math.round(center.x / texelWorldSize) * texelWorldSize;
152
+ center.y = Math.round(center.y / texelWorldSize) * texelWorldSize;
153
+ center.applyQuaternion(lightOrientation);
154
+ }
155
+ return { center, radius, texelWorldSize };
156
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Restricting a scene for the duration of ONE environment capture, and asking
3
+ * a renderer whether it can run one at all.
4
+ *
5
+ * A cube capture renders the live scene into an offscreen target, so whatever
6
+ * it must exclude has to be turned off and turned back on around that single
7
+ * render — never left flipped, and never flipped through a renderer-wide
8
+ * switch (see {@link withCaptureShadows} for the measured reason). Both
9
+ * helpers restore in a `finally`, so a capture that throws does not leave the
10
+ * game altered.
11
+ *
12
+ * Consumers today: the `reflections` capability's native probe system, and
13
+ * `godot-compat`'s Godot 3 `ReflectionProbe`.
14
+ */
15
+ import type { Light, Object3D, Scene, WebGLRenderer } from 'three';
16
+
17
+ /**
18
+ * Hide everything a capture's cull mask excludes, for exactly one capture.
19
+ *
20
+ * The object's side of the test is three's OWN `Object3D.layers.mask`, never a
21
+ * parallel `userData` copy. An engine whose own per-object visibility layers
22
+ * are a 32-bit mask (Godot's `VisualInstance.layers` is the worked case) writes
23
+ * the authored mask straight onto the object and this reads it back. Both
24
+ * default an unannotated object to `1`, which no default mask excludes, so an
25
+ * ordinary scene is untouched.
26
+ */
27
+ export function withCaptureMask(scene: Scene, mask: number, capture: () => void): void {
28
+ const hidden: { object: Object3D; visible: boolean }[] = [];
29
+ scene.traverse((object) => {
30
+ if ((object.layers.mask & mask) !== 0) return;
31
+ hidden.push({ object, visible: object.visible });
32
+ object.visible = false;
33
+ });
34
+ try {
35
+ capture();
36
+ } finally {
37
+ for (const entry of hidden) entry.object.visible = entry.visible;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Suppress every shadow-casting light for the duration of ONE capture — as
43
+ * per-light `LightShadow.intensity`, never as `renderer.shadowMap.enabled`.
44
+ *
45
+ * **The distinction is not stylistic; the global flag silently deletes every
46
+ * shadow in the game.** `renderer.shadowMap.enabled` is a program PARAMETER
47
+ * (`WebGLPrograms.getParameters` → `shadowMapEnabled`), and
48
+ * `WebGLRenderer.setProgram`'s `needsProgramChange` list does NOT watch it —
49
+ * three requires a material version bump for a flip to reach a material that is
50
+ * already compiled. So flipping it around the capture does nothing for compiled
51
+ * materials and everything for the one material compiling INSIDE the window —
52
+ * and a probe system installing a shader override sets `material.needsUpdate =
53
+ * true` in the same `update()` call that runs the capture, so every patched
54
+ * material links with `USE_SHADOWMAP` absent and keeps that program for the
55
+ * rest of the run.
56
+ *
57
+ * Measured on the real Godot 3.6 binary against that lane's emitted port (a
58
+ * white ground, a floating box, one straight-down `DirectionalLight`,
59
+ * PanoramaSky ambient, Filmic white 6): with NO probe the port's shadow pixel
60
+ * is `92,109,181` against Godot's `92,109,181`; adding ONE `ReflectionProbe`
61
+ * moved the port's shadow pixel to `218,222,232` — byte-identical to its own
62
+ * LIT ground, i.e. the shadow was gone — while Godot's moved to `109,146,201`.
63
+ * Asking the capture not to touch shadows at all restored `108,146,200`, which
64
+ * is what named the mechanism. The defect was Godot-shaped; the mechanism is a
65
+ * three.js contract, which is why the fix lives here.
66
+ *
67
+ * `LightShadow.intensity` is a UNIFORM (`shadowmap_pars_fragment`:
68
+ * `mix(1.0, shadow, shadowIntensity)`), refreshed from the light on every
69
+ * `renderer.render()`, so zeroing it for the capture costs no recompile and
70
+ * cannot outlive the `finally` that restores it.
71
+ */
72
+ export function withCaptureShadows(scene: Scene, enabled: boolean, capture: () => void): void {
73
+ if (enabled) {
74
+ capture();
75
+ return;
76
+ }
77
+ const restored: { shadow: { intensity: number }; intensity: number }[] = [];
78
+ scene.traverse((object) => {
79
+ const light = object as Light & { shadow?: { intensity: number } };
80
+ if (!light.isLight || light.shadow === undefined) return;
81
+ restored.push({ shadow: light.shadow, intensity: light.shadow.intensity });
82
+ light.shadow.intensity = 0;
83
+ });
84
+ try {
85
+ capture();
86
+ } finally {
87
+ for (const entry of restored) entry.shadow.intensity = entry.intensity;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Whether this renderer can run an environment capture at all.
93
+ *
94
+ * The editor's still design-time tree and headless runtimes deliberately supply
95
+ * no GPU. They must keep the authored scene and gameplay; only the
96
+ * renderer-owned capture is unavailable, so a probe system asks this and skips
97
+ * the capture rather than refusing to mount.
98
+ */
99
+ export function canCaptureEnvironment(renderer: WebGLRenderer): boolean {
100
+ const candidate = renderer as WebGLRenderer & { compile?: unknown; coordinateSystem?: unknown };
101
+ return typeof candidate.compile === 'function' && candidate.coordinateSystem !== undefined;
102
+ }