@vgai/engine 0.5.22 → 0.5.24
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-seams.d.ts +5 -0
- package/dist/adapter/ingest/game-contract-seams.d.ts.map +1 -1
- package/dist/adapter/ingest/game-contract-seams.js +1 -0
- package/dist/adapter/ingest/game-contract.d.ts +22 -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/native-debug-module.d.ts +40 -5
- package/dist/adapter/native-debug-module.d.ts.map +1 -1
- package/dist/adapter/native-debug-module.js +36 -9
- 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 +2 -2
- package/dist/adapter/renderer-config.js +2 -2
- 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/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 +5 -6
- package/dist/core/sim-clock.d.ts.map +1 -1
- package/dist/core/sim-clock.js +4 -5
- 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/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 +18 -5
- package/dist/input/input-manager.d.ts.map +1 -1
- package/dist/input/input-manager.js +33 -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/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-bridge.d.ts +12 -0
- package/dist/runtime/debug-bridge.d.ts.map +1 -1
- package/dist/runtime/debug-bridge.js +4 -0
- package/dist/runtime/debug-registry.d.ts +95 -8
- package/dist/runtime/debug-registry.d.ts.map +1 -1
- package/dist/runtime/debug-registry.js +24 -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 +2 -2
- package/dist/runtime/game-input-seams.d.ts.map +1 -1
- package/dist/runtime/game.d.ts +102 -41
- package/dist/runtime/game.d.ts.map +1 -1
- package/dist/runtime/game.js +80 -54
- 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 +8 -39
- 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 +18 -18
- 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/run-ticks-settled.d.ts +34 -0
- package/dist/runtime/run-ticks-settled.d.ts.map +1 -0
- package/dist/runtime/run-ticks-settled.js +59 -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/r3f-root-factory.d.ts +7 -7
- package/dist/world3d-react/r3f-root-factory.js +7 -7
- package/dist/world3d-react/rapier-physics-bridge.d.ts +9 -8
- package/dist/world3d-react/rapier-physics-bridge.d.ts.map +1 -1
- package/dist/world3d-react/rapier-physics-bridge.js +100 -48
- 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-seams.ts +1 -0
- package/src/adapter/ingest/game-contract.ts +22 -3
- package/src/adapter/ingest/scene-capture.ts +190 -39
- package/src/adapter/ingest/visible-capture-window.ts +35 -0
- package/src/adapter/native-debug-module.ts +66 -9
- package/src/adapter/rapier-physics-adapter.ts +76 -20
- package/src/adapter/renderer-config.ts +2 -2
- 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/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 +5 -6
- package/src/core/types.ts +1 -3
- package/src/defaults.ts +6 -14
- 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 +34 -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/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-bridge.ts +16 -0
- package/src/runtime/debug-registry.ts +121 -8
- package/src/runtime/dev-build.ts +2 -2
- package/src/runtime/game-input-seams.ts +2 -2
- package/src/runtime/game.ts +180 -80
- package/src/runtime/mount-game.ts +8 -56
- package/src/runtime/mount-manifest.ts +43 -34
- package/src/runtime/playtest.ts +22 -0
- package/src/runtime/run-ticks-settled.ts +72 -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/r3f-root-factory.tsx +7 -7
- package/src/world3d-react/rapier-physics-bridge.tsx +107 -50
- 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/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
|
@@ -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
|
+
}
|
package/src/core/frame-pacing.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Frame pacing — the accumulator/alpha arithmetic of the fixed-timestep loop,
|
|
3
|
-
* extracted PURE
|
|
3
|
+
* extracted PURE.
|
|
4
4
|
*
|
|
5
5
|
* `core/game-loop.ts` owns the browser side (rAF, `visibilitychange`,
|
|
6
6
|
* `performance.now`); this module owns the arithmetic, so the interesting
|
|
@@ -108,7 +108,7 @@ export function paceFrame(
|
|
|
108
108
|
const maxAccumulator = fixedDt * maxSubSteps;
|
|
109
109
|
|
|
110
110
|
// Clamp large frame gaps (a slow frame, a debugger pause) BEFORE scaling by
|
|
111
|
-
// timeScale
|
|
111
|
+
// timeScale.
|
|
112
112
|
const displayDt = Math.min(rawDt, maxAccumulator) * timeScale;
|
|
113
113
|
let next = accumulator + displayDt;
|
|
114
114
|
// Spiral-of-death guard: bound the accumulator regardless of timeScale or
|
package/src/core/game-loop.ts
CHANGED
|
@@ -9,9 +9,7 @@ const LOOP_STARVATION_MS = 1_000;
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Fixed-timestep game loop with accumulator pattern — SIM at a fixed rate,
|
|
12
|
-
* PRESENTATION at the display's rate
|
|
13
|
-
* flipped the former D1 "render is fixed-rate too" decision this comment used
|
|
14
|
-
* to state).
|
|
12
|
+
* PRESENTATION at the display's rate.
|
|
15
13
|
*
|
|
16
14
|
* Per real (rAF) frame:
|
|
17
15
|
*
|
|
@@ -42,7 +42,7 @@ export const GAMEPLAY_STREAM = 'gameplay';
|
|
|
42
42
|
export const DEFAULT_SEEDED_RANDOM_SEED = 0x9e3779b9;
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
|
-
*
|
|
45
|
+
* A game-scoped random surface.
|
|
46
46
|
* Callable (draws from the `'gameplay'` stream), plus:
|
|
47
47
|
* - `stream(name)` — an independent named generator, `[0, 1)` floats, same
|
|
48
48
|
* call signature as `Math.random`/the bare `SeededRandom` call itself.
|
package/src/core/sim-clock.ts
CHANGED
|
@@ -93,10 +93,9 @@
|
|
|
93
93
|
* controller cancels everything a `setup()` scheduled:
|
|
94
94
|
*
|
|
95
95
|
* ```ts
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* return { dispose: () => ac.abort() }; // cancels timers AND listeners
|
|
96
|
+
* const ac = new AbortController();
|
|
97
|
+
* clock.after(3, () => spawnWave(), { signal: ac.signal });
|
|
98
|
+
* // on dispose: ac.abort() — cancels timers AND listeners
|
|
100
99
|
* }
|
|
101
100
|
* ```
|
|
102
101
|
*
|
|
@@ -170,7 +169,7 @@ export interface SimClock {
|
|
|
170
169
|
/**
|
|
171
170
|
* The runtime-facing half — NOT for game code. Split off the public
|
|
172
171
|
* {@link SimClock} the same way `runtime/game.ts` splits `GameInternal` off
|
|
173
|
-
* `Game`:
|
|
172
|
+
* `Game`: game code only ever sees the public `SimClock`, so it cannot
|
|
174
173
|
* reach `flush`/`dispose`.
|
|
175
174
|
*/
|
|
176
175
|
export interface SimClockInternal extends SimClock {
|
|
@@ -394,7 +393,7 @@ export function registerSimClock(owner: object, clock: SimClockInternal): void {
|
|
|
394
393
|
* Returns the INTERNAL view because this registry is engine-only — but a
|
|
395
394
|
* caller that RESOLVES a clock here is not its owner and must not call
|
|
396
395
|
* `dispose()` on it (module header, ownership). Game code never reaches it at
|
|
397
|
-
* all:
|
|
396
|
+
* all: it only ever sees the public {@link SimClock}, which has
|
|
398
397
|
* neither `flush` nor `dispose`, the same way `Game` hides `GameInternal`. */
|
|
399
398
|
export function getSimClock(owner: object): SimClockInternal | null {
|
|
400
399
|
return clockByOwner.get(owner) ?? null;
|
package/src/core/types.ts
CHANGED
|
@@ -72,9 +72,7 @@ export interface GameLoopConfig {
|
|
|
72
72
|
*/
|
|
73
73
|
update: (dt: number) => void;
|
|
74
74
|
/**
|
|
75
|
-
* Called EXACTLY ONCE per real (rAF) frame — the PRESENTATION half
|
|
76
|
-
* (WO-P13, `docs/WORK.md` §2 P13; this is the callback whose absence the
|
|
77
|
-
* former D1 decision recorded).
|
|
75
|
+
* Called EXACTLY ONCE per real (rAF) frame — the PRESENTATION half.
|
|
78
76
|
*
|
|
79
77
|
* - `alpha` — `accumulator / fixedDt` after this frame's substeps were
|
|
80
78
|
* consumed, in `[0, 1]`: how far presentation sits past the last
|
package/src/defaults.ts
CHANGED
|
@@ -1,20 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Single source of truth for default values the engine's factories read.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* — wire the group to the factory that reads it, or delete it if truly unused —
|
|
11
|
-
* and there is no surviving factory to wire these to: `transform` restated
|
|
12
|
-
* THREE's own Object3D constructor values, `entity.locked`/ `pivot` were
|
|
13
|
-
* descriptor-only fields, and `fog`/`environment.envMapIntensity`/ `shadow` were
|
|
14
|
-
* the `.vscn` `environment` block that `render-env.ts` explicitly did NOT carry
|
|
15
|
-
* forward. Keeping them would have been exactly the dead documentation T1.13
|
|
16
|
-
* created this guard to prevent.
|
|
17
|
-
*
|
|
4
|
+
* A group belongs here only while a FACTORY reads it. A default with no
|
|
5
|
+
* reader is dead documentation: it looks authoritative, nothing consults it,
|
|
6
|
+
* and it drifts from the value the code actually uses. Restating a library's
|
|
7
|
+
* own constructor values (THREE's `Object3D` transform, say) is the same
|
|
8
|
+
* mistake in a different shape — the library is already the source of truth.
|
|
9
|
+
* So when a factory goes, its group goes with it.
|
|
18
10
|
*/
|
|
19
11
|
export const DEFAULTS = {
|
|
20
12
|
material: {
|
|
@@ -8,10 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Engine-owned and first-party, seeded by BOTH three-root mount paths — a game
|
|
10
10
|
* writes nothing to get these, and there is no capability to install:
|
|
11
|
-
* - `
|
|
12
|
-
* `setup(ctx)` world), at the same spot it seeds
|
|
13
|
-
* `systemAdapters.renderDebug`;
|
|
14
|
-
* - `world3d-react/r3f-adapter.tsx` (a TSX/R3F world — the shape EVERY
|
|
11
|
+
* - `world3d-react/r3f-root-factory.tsx` (a TSX/R3F world — the shape EVERY
|
|
15
12
|
* scaffolded project's three root has), right after fiber's first commit
|
|
16
13
|
* resolves the scene.
|
|
17
14
|
* Both is the whole point and not redundancy: they are two independent mounts
|
|
@@ -3,11 +3,10 @@
|
|
|
3
3
|
* the WebGL2 frame capture (`webgl-frame-capture.ts`), a live three.js scene,
|
|
4
4
|
* and the render adapter's per-frame render pass.
|
|
5
5
|
*
|
|
6
|
-
* Split OUT of `
|
|
6
|
+
* Split OUT of `world3d-react/r3f-root-factory.tsx` so the capture/attribution/restore
|
|
7
7
|
* flow is unit-testable in a headless Node test with a mock GL context and a
|
|
8
8
|
* plain scene — a real non-headless GPU mount (EffectComposer + WebGLRenderer)
|
|
9
|
-
* cannot run under vitest here
|
|
10
|
-
* `setup(ctx)` only through hotReload on a data scene. `mount()` owns the
|
|
9
|
+
* cannot run under vitest here. `mount()` owns the
|
|
11
10
|
* DECISION to construct this (only under a real WebGL2 context, never
|
|
12
11
|
* headless — see `frameCaptureContextFor`); this module owns the BEHAVIOR.
|
|
13
12
|
*
|
package/src/dev/render-vitals.ts
CHANGED
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
* OWNER: the caller of {@link createRenderVitals}, which allocates one state
|
|
44
44
|
* object and one `profiler.subscribe` registration. SHARER: none — the state
|
|
45
45
|
* is private to that call. TEARDOWN: the returned `dispose()`, the ONE path
|
|
46
|
-
* that ends the subscription (`
|
|
47
|
-
* `
|
|
46
|
+
* that ends the subscription (`world3d-react/r3f-root-factory.tsx` calls it
|
|
47
|
+
* from the mounted root's own `dispose()`).
|
|
48
48
|
*/
|
|
49
49
|
|
|
50
50
|
import type { PerformanceFrame, PerformanceProfiler } from './performance-profiler';
|
|
@@ -52,7 +52,7 @@ import type { PerformanceFrame, PerformanceProfiler } from './performance-profil
|
|
|
52
52
|
/**
|
|
53
53
|
* The profiler phase name the three adapter brackets its CPU render
|
|
54
54
|
* submission with — spelled HERE and nowhere else, so the producer
|
|
55
|
-
* (`
|
|
55
|
+
* (`world3d-react/r3f-root-factory.tsx`) and the consumer ({@link foldProfilerFrame})
|
|
56
56
|
* cannot drift apart. Dotted, so it reads as a decomposition of the enclosing
|
|
57
57
|
* `render` phase rather than a ninth peer of `SystemPhase`.
|
|
58
58
|
*/
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* WHY FIRST-PARTY, NOT spectorjs (recorded per the "use libraries directly, no
|
|
5
5
|
* wrappers" rule): the capture seam we need is the WebGL2 context this engine
|
|
6
6
|
* ALREADY owns end-to-end (`renderer.getContext()` in
|
|
7
|
-
* `
|
|
7
|
+
* `world3d-react/r3f-root-factory.tsx`). spectorjs is absent from node_modules, and its
|
|
8
8
|
* actual value is a bundled inspector UI we would discard — adopting it imports
|
|
9
9
|
* ~2MB of library to keep ~10% of it, and it wraps the context with its own
|
|
10
10
|
* global patching model rather than the instance-shadow-and-restore discipline
|
package/src/index.ts
CHANGED
|
@@ -10,21 +10,19 @@
|
|
|
10
10
|
* remain the primary way to reach engine internals — reach for a subpath
|
|
11
11
|
* first. This barrel exists only so the package's bare-name entry point is
|
|
12
12
|
* HONEST (resolves to something real and useful) instead of dangling; it
|
|
13
|
-
* covers just the handful of symbols a
|
|
14
|
-
*
|
|
13
|
+
* covers just the handful of symbols a consumer needs to mount a game
|
|
14
|
+
* from its manifest, plus the manifest file
|
|
15
15
|
* entry points. Keep it small when extending it — prefer a new subpath
|
|
16
16
|
* export over growing this file into a firehose.
|
|
17
17
|
*
|
|
18
18
|
* Groups (import order below is alphabetical-by-specifier, per the repo's
|
|
19
19
|
* biome `organizeImports` rule, not by group — see this list instead):
|
|
20
|
-
* - game setup contract: `GameContext`/`GameCleanup`/`GameSetupFn`, `fromSetup`
|
|
21
20
|
* - surface vocabulary: `AdapterSurface`
|
|
22
21
|
* - manifest-driven mount: `mountManifestRoots` + its option/result types
|
|
23
22
|
* - game manifest (vgai.project.json) entry point: `loadGameManifest`
|
|
24
23
|
*/
|
|
25
24
|
|
|
26
25
|
export type { AdapterSurface } from './adapter/adapter-surface';
|
|
27
|
-
export { fromSetup } from './adapter/setup-three-root-adapter';
|
|
28
26
|
export { AssetParseError } from './asset-parse-error';
|
|
29
27
|
export type {
|
|
30
28
|
AdapterRoot,
|
|
@@ -42,4 +40,3 @@ export type {
|
|
|
42
40
|
MountManifestOptions,
|
|
43
41
|
} from './runtime/mount-manifest';
|
|
44
42
|
export { mountManifestRoots } from './runtime/mount-manifest';
|
|
45
|
-
export type { GameCleanup, GameContext, GameSetupFn } from './runtime/types';
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The host POINTER stream, in the game's own coordinate space — the DOM edge
|
|
3
|
+
* `InputManager`'s touch backends (`setTouchButton` / `setTouchStick`) and any
|
|
4
|
+
* pointer-position read need, and which the manager itself deliberately does
|
|
5
|
+
* not own.
|
|
6
|
+
*
|
|
7
|
+
* `InputManager` listens on `window` for keyboard, mouse and focus, in CLIENT
|
|
8
|
+
* pixels, because an action map is surface-agnostic. A pointer is not: a tap is
|
|
9
|
+
* only meaningful relative to the surface it landed on, and a game's own
|
|
10
|
+
* coordinate space is usually NOT that surface's CSS pixels — a fixed design
|
|
11
|
+
* resolution presented by uniformly scaling and letterboxing is the common
|
|
12
|
+
* case, and a hit-test against a UI rect authored in design pixels has to
|
|
13
|
+
* happen in one space with the pointer. That mapping needs a surface and a
|
|
14
|
+
* design size the manager has no way to know, so it lives here, beside it.
|
|
15
|
+
*
|
|
16
|
+
* What this owns, and nothing more:
|
|
17
|
+
*
|
|
18
|
+
* - **Position** — the last pointer position over the surface, mapped into the
|
|
19
|
+
* design space. `setPosition` is the headless door (a harness or a game
|
|
20
|
+
* command driving the game with no DOM), the same shape as
|
|
21
|
+
* `InputManager.addMouseDelta`: inject through the accumulator the reader
|
|
22
|
+
* already reads, never a synthetic `pointermove`.
|
|
23
|
+
* - **Touches** — a per-frame queue of {@link HostPointerTouch} records with
|
|
24
|
+
* DENSE finger indices, drained by the game's own frame. `pointerId` is the
|
|
25
|
+
* browser's opaque, monotonically growing handle; a touch API that reports
|
|
26
|
+
* "finger 0 and finger 1" wants the lowest free slot, reused the moment a
|
|
27
|
+
* finger lifts, which is what {@link HostPointerTouch.index} is.
|
|
28
|
+
* - **Cursor** — the CSS cursor applied to the surface, because the surface is
|
|
29
|
+
* resolved here and nowhere else.
|
|
30
|
+
*
|
|
31
|
+
* **Gating.** Every listener returns early unless
|
|
32
|
+
* `InputManager.isInputActive()` — the SAME predicate the manager's own
|
|
33
|
+
* `keydown`/`mousemove` listeners pass, so play-mode isolation and window
|
|
34
|
+
* blur suppress pointer position and touches exactly as they suppress a key.
|
|
35
|
+
* The predicate is read from the manager rather than recomposed here
|
|
36
|
+
* (`isEnabled() && isFocused()` written out a second time is how a looser gate
|
|
37
|
+
* drifts in: a blurred or typing tab kept updating a pointer position under
|
|
38
|
+
* exactly that duplication). Text-entry focus is deliberately NOT part of it,
|
|
39
|
+
* matching `onMouseMove` rather than `onKeyDown` — moving a finger is not
|
|
40
|
+
* typing.
|
|
41
|
+
*
|
|
42
|
+
* **Ownership.** Owns its four pointer listeners on the resolved owner, the
|
|
43
|
+
* touch queue and the finger-index table. Shares the `InputManager`, which is
|
|
44
|
+
* the game's and is never mutated here — this reports; what a control DOES
|
|
45
|
+
* with a finger (which rect owns it, whether that becomes a `setTouchButton`)
|
|
46
|
+
* belongs to the UI that draws the control. Teardown is {@link
|
|
47
|
+
* HostPointer.dispose}, and only that: `InputManager.dispose` does not reach
|
|
48
|
+
* these listeners.
|
|
49
|
+
*
|
|
50
|
+
* Headless (no `window`): construction succeeds and binds nothing. Position is
|
|
51
|
+
* whatever {@link HostPointer.setPosition} last wrote, and the touch queue
|
|
52
|
+
* stays empty.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
import type { InputManager } from './input-manager';
|
|
56
|
+
import type { Vector2 } from './input-types';
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Cap on the undrained touch queue. A game that drains every frame cannot
|
|
60
|
+
* reach it — this is the bound for one that never drains at all (a project
|
|
61
|
+
* that reads no touches, on a device that has them), so the queue is a RING:
|
|
62
|
+
* the newest records win and the oldest are dropped.
|
|
63
|
+
*/
|
|
64
|
+
const TOUCH_QUEUE_MAX = 256;
|
|
65
|
+
|
|
66
|
+
/** One finger edge the host pointer stream produced. */
|
|
67
|
+
export interface HostPointerTouch {
|
|
68
|
+
/**
|
|
69
|
+
* `'start'` when the finger went down on the surface, `'move'` while it
|
|
70
|
+
* drags, `'end'` when it lifted or the browser cancelled it. A cancel is an
|
|
71
|
+
* end: the finger is gone either way, and a control that treats them
|
|
72
|
+
* differently is holding a button nothing will ever release.
|
|
73
|
+
*/
|
|
74
|
+
readonly phase: 'start' | 'move' | 'end';
|
|
75
|
+
/**
|
|
76
|
+
* DENSE finger index — the lowest slot free when this finger went down, and
|
|
77
|
+
* free again the moment it lifts. NOT the browser's `pointerId`, which never
|
|
78
|
+
* reuses a value and is therefore useless as "which finger is this".
|
|
79
|
+
*/
|
|
80
|
+
readonly index: number;
|
|
81
|
+
/** Position in the design space (see {@link HostPointerOptions.designSize}). */
|
|
82
|
+
readonly position: Vector2;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** What {@link createHostPointer} needs. */
|
|
86
|
+
export interface HostPointerOptions {
|
|
87
|
+
/** The game's own manager — read for its gate, never written. */
|
|
88
|
+
readonly input: InputManager;
|
|
89
|
+
/**
|
|
90
|
+
* The render surface pointer positions are relative to.
|
|
91
|
+
*
|
|
92
|
+
* A GETTER rather than an element because a renderer usually mints its
|
|
93
|
+
* canvas after the game's services exist. Absent (or returning `null`) falls
|
|
94
|
+
* back to the event's own target element, and listening falls back to
|
|
95
|
+
* `window` — which is what makes a finger that starts on the canvas and
|
|
96
|
+
* drags over a DOM overlay keep reporting.
|
|
97
|
+
*/
|
|
98
|
+
readonly surface?: (() => HTMLElement | null) | undefined;
|
|
99
|
+
/**
|
|
100
|
+
* The game's own logical resolution, if it has one. Surface CSS pixels are
|
|
101
|
+
* mapped into it, so a pointer and a UI rect authored at this size share one
|
|
102
|
+
* space regardless of how the host scaled and letterboxed the surface.
|
|
103
|
+
*
|
|
104
|
+
* Absent: positions are reported in the surface's own CSS pixels, and the
|
|
105
|
+
* position before any event is `{x: 0, y: 0}`. Present: the position before
|
|
106
|
+
* any event is the CENTRE of the design space — a pointer that has never
|
|
107
|
+
* moved is better described as the middle of the view than as a corner it
|
|
108
|
+
* was never at.
|
|
109
|
+
*/
|
|
110
|
+
readonly designSize?: Vector2 | undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The host pointer stream bound to one surface. One per mounted game. */
|
|
114
|
+
export interface HostPointer {
|
|
115
|
+
/** The last pointer position, in the design space. */
|
|
116
|
+
getPosition(): Vector2;
|
|
117
|
+
/** Headless / harness door onto {@link getPosition}'s value. */
|
|
118
|
+
setPosition(position: Vector2): void;
|
|
119
|
+
/**
|
|
120
|
+
* Drain the touch records queued since the last call. Returns them in
|
|
121
|
+
* arrival order and empties the queue, so a frame that drains sees each
|
|
122
|
+
* finger edge exactly once.
|
|
123
|
+
*/
|
|
124
|
+
takeTouches(): readonly HostPointerTouch[];
|
|
125
|
+
/**
|
|
126
|
+
* Set the CSS `cursor` applied to the surface. Applied on the next pointer
|
|
127
|
+
* move over it, which is also when the surface is known.
|
|
128
|
+
*/
|
|
129
|
+
setCursor(cursor: string): void;
|
|
130
|
+
/** Remove the listeners this owns. */
|
|
131
|
+
dispose(): void;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function createHostPointer(options: HostPointerOptions): HostPointer {
|
|
135
|
+
const { input, surface: surfaceOf, designSize } = options;
|
|
136
|
+
let position: Vector2 = designSize
|
|
137
|
+
? { x: designSize.x / 2, y: designSize.y / 2 }
|
|
138
|
+
: { x: 0, y: 0 };
|
|
139
|
+
let cursor = 'default';
|
|
140
|
+
|
|
141
|
+
const touches: HostPointerTouch[] = [];
|
|
142
|
+
/** Browser `pointerId` -> dense finger index, for fingers currently down. */
|
|
143
|
+
const fingerOfPointer = new Map<number, number>();
|
|
144
|
+
const freeFinger = (): number => {
|
|
145
|
+
const taken = new Set(fingerOfPointer.values());
|
|
146
|
+
let index = 0;
|
|
147
|
+
while (taken.has(index)) index += 1;
|
|
148
|
+
return index;
|
|
149
|
+
};
|
|
150
|
+
const queue = (touch: HostPointerTouch): void => {
|
|
151
|
+
touches.push(touch);
|
|
152
|
+
if (touches.length > TOUCH_QUEUE_MAX) touches.shift();
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const queueTouch = (event: PointerEvent, point: Vector2): void => {
|
|
156
|
+
if (event.type === 'pointerdown') {
|
|
157
|
+
const index = freeFinger();
|
|
158
|
+
fingerOfPointer.set(event.pointerId, index);
|
|
159
|
+
queue({ phase: 'start', index, position: point });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
// A move or a release from a pointer that never went down on this surface
|
|
163
|
+
// is not one of our fingers.
|
|
164
|
+
const index = fingerOfPointer.get(event.pointerId);
|
|
165
|
+
if (index === undefined) return;
|
|
166
|
+
if (event.type === 'pointerup' || event.type === 'pointercancel') {
|
|
167
|
+
fingerOfPointer.delete(event.pointerId);
|
|
168
|
+
queue({ phase: 'end', index, position: point });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
queue({ phase: 'move', index, position: point });
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/** A client-space event as a point in the design space, via the surface rect
|
|
175
|
+
* the host actually presented it at. */
|
|
176
|
+
const toDesignSpace = (event: PointerEvent, rect: DOMRect): Vector2 => {
|
|
177
|
+
const x = event.clientX - rect.left;
|
|
178
|
+
const y = event.clientY - rect.top;
|
|
179
|
+
if (!designSize) return { x, y };
|
|
180
|
+
return { x: (x * designSize.x) / rect.width, y: (y * designSize.y) / rect.height };
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const onPointer = (event: PointerEvent): void => {
|
|
184
|
+
// The manager's own gate, read from the manager. See this module's header
|
|
185
|
+
// for why it is not recomposed here.
|
|
186
|
+
if (!input.isInputActive()) return;
|
|
187
|
+
const surface = surfaceOf?.() ?? (event.target instanceof HTMLElement ? event.target : null);
|
|
188
|
+
if (surface === null) return;
|
|
189
|
+
const rect = surface.getBoundingClientRect();
|
|
190
|
+
if (rect.width <= 0 || rect.height <= 0) return;
|
|
191
|
+
const point = toDesignSpace(event, rect);
|
|
192
|
+
|
|
193
|
+
if (event.type === 'pointermove') {
|
|
194
|
+
// Every pointer type moves the position — a stylus and a dragging finger
|
|
195
|
+
// are as much "where the pointer is" as a mouse. Only the TOUCH queue is
|
|
196
|
+
// touch-only.
|
|
197
|
+
position = point;
|
|
198
|
+
surface.style.cursor = cursor;
|
|
199
|
+
}
|
|
200
|
+
// A mouse or a pen is not a finger. A control that answered them here would
|
|
201
|
+
// steal the clicks of a player who cannot see it.
|
|
202
|
+
if (event.pointerType === 'touch') queueTouch(event, point);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const KINDS = ['pointerdown', 'pointermove', 'pointerup', 'pointercancel'] as const;
|
|
206
|
+
const owner: EventTarget | null =
|
|
207
|
+
typeof window === 'undefined' ? null : (surfaceOf?.() ?? window);
|
|
208
|
+
if (owner !== null) {
|
|
209
|
+
for (const kind of KINDS) owner.addEventListener(kind, onPointer as EventListener);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
getPosition(): Vector2 {
|
|
214
|
+
return position;
|
|
215
|
+
},
|
|
216
|
+
setPosition(next): void {
|
|
217
|
+
position = { x: next.x, y: next.y };
|
|
218
|
+
},
|
|
219
|
+
takeTouches(): readonly HostPointerTouch[] {
|
|
220
|
+
return touches.splice(0, touches.length);
|
|
221
|
+
},
|
|
222
|
+
setCursor(next): void {
|
|
223
|
+
cursor = next;
|
|
224
|
+
},
|
|
225
|
+
dispose(): void {
|
|
226
|
+
if (owner === null) return;
|
|
227
|
+
for (const kind of KINDS) owner.removeEventListener(kind, onPointer as EventListener);
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|