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