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