@driftengine/core 4.8.1 → 4.8.2
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/CHANGELOG.json +86 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -0
- package/dist/input/actionMap.d.ts +6 -1
- package/dist/input/actionMap.js +20 -7
- package/dist/input/input.d.ts +28 -0
- package/dist/input/input.js +71 -9
- package/dist/input/inputHarness.d.ts +9 -0
- package/dist/input/inputHarness.js +34 -2
- package/dist/render/backend/webgl2/renderer.d.ts +52 -3
- package/dist/render/backend/webgl2/renderer.js +151 -5
- package/dist/render/backend/webgpu/buffers.d.ts +2 -0
- package/dist/render/backend/webgpu/buffers.js +2 -0
- package/dist/render/backend/webgpu/flatPass.js +16 -7
- package/dist/render/backend/webgpu/pipelineCache.d.ts +40 -8
- package/dist/render/backend/webgpu/pipelineCache.js +76 -9
- package/dist/render/backend/webgpu/renderer.d.ts +56 -1
- package/dist/render/backend/webgpu/renderer.js +151 -26
- package/dist/render/mapsWithoutUvs.d.ts +24 -0
- package/dist/render/mapsWithoutUvs.js +24 -0
- package/dist/render/mesh.d.ts +2 -0
- package/dist/render/mesh.js +3 -0
- package/dist/render/particlePool.d.ts +15 -0
- package/dist/render/particlePool.js +72 -37
- package/dist/render/pass.d.ts +12 -0
- package/dist/render/renderQuality.d.ts +7 -6
- package/dist/render/shaders/flat/glassTint.d.ts +28 -0
- package/dist/render/shaders/flat/glassTint.js +31 -0
- package/dist/render/shaders/flat/index.d.ts +12 -0
- package/dist/render/shaders/flat/index.js +7 -1
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +60 -25
- package/dist/render/shaders/flat/surfaceEffects.d.ts +1 -1
- package/dist/render/shaders/flat/surfaceEffects.js +6 -2
- package/dist/render/shaders/generated/flat.wgsl.d.ts +64 -0
- package/dist/render/shaders/generated/flat.wgsl.js +102 -38
- package/dist/render/surfaceTexture.d.ts +2 -0
- package/dist/render/surfaceTexture.js +4 -0
- package/dist/render/viewModel.d.ts +29 -0
- package/dist/render/viewModel.js +35 -0
- package/package.json +6 -6
- package/src/index.ts +9 -1
- package/src/input/actionMap.ts +23 -8
- package/src/input/input.ts +79 -9
- package/src/input/inputHarness.ts +27 -2
- package/src/render/backend/webgl2/renderer.ts +166 -13
- package/src/render/backend/webgpu/buffers.ts +5 -0
- package/src/render/backend/webgpu/flatPass.ts +41 -25
- package/src/render/backend/webgpu/pipelineCache.ts +82 -8
- package/src/render/backend/webgpu/renderer.ts +151 -26
- package/src/render/mapsWithoutUvs.ts +43 -0
- package/src/render/mesh.ts +3 -0
- package/src/render/particlePool.ts +101 -54
- package/src/render/pass.ts +12 -0
- package/src/render/renderQuality.ts +7 -6
- package/src/render/shaders/flat/glassTint.ts +48 -0
- package/src/render/shaders/flat/index.ts +19 -1
- package/src/render/shaders/flat/main.ts +60 -25
- package/src/render/shaders/flat/surfaceEffects.ts +6 -2
- package/src/render/shaders/generated/flat.wgsl.ts +102 -38
- package/src/render/surfaceTexture.ts +5 -0
- package/src/render/viewModel.ts +41 -0
|
@@ -199,6 +199,8 @@ export declare class SurfaceTexture {
|
|
|
199
199
|
*/
|
|
200
200
|
constructor(gl: WebGL2RenderingContext, source: TexImageSource | readonly TexImageSource[], options?: SurfaceTextureOptions);
|
|
201
201
|
/** Bind the effects table to `unit`, or `fallback` for a texture given none. */
|
|
202
|
+
/** Whether this texture carries an effects table, which the lit program reads only once one exists. */
|
|
203
|
+
get hasEffects(): boolean;
|
|
202
204
|
bindEffects(gl: WebGL2RenderingContext, unit: number, fallback: WebGLTexture): void;
|
|
203
205
|
/**
|
|
204
206
|
* Replace the pixels, keeping the GPU object and its sampler state.
|
|
@@ -108,6 +108,10 @@ export class SurfaceTexture {
|
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
/** Bind the effects table to `unit`, or `fallback` for a texture given none. */
|
|
111
|
+
/** Whether this texture carries an effects table, which the lit program reads only once one exists. */
|
|
112
|
+
get hasEffects() {
|
|
113
|
+
return this.effectsTable !== null;
|
|
114
|
+
}
|
|
111
115
|
bindEffects(gl, unit, fallback) {
|
|
112
116
|
gl.activeTexture(gl.TEXTURE0 + unit);
|
|
113
117
|
gl.bindTexture(gl.TEXTURE_2D, this.effectsTable ?? fallback);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The slice of depth a view model is drawn into: the nearest share of the range.
|
|
3
|
+
*
|
|
4
|
+
* **A first-person view model — arms and a held weapon — must never go into the wall it is pushed
|
|
5
|
+
* against**, and clearing depth to draw it on top costs every pass that reads depth afterwards:
|
|
6
|
+
* ambient occlusion, depth of field and the fog all see a hole where the world was. Squeezing the
|
|
7
|
+
* view model's depth into the nearest sliver of the range does neither. Anything past the near
|
|
8
|
+
* plane in the world lands behind it, the world's depth is kept, and the view model still sorts
|
|
9
|
+
* against itself, at a fraction of the precision.
|
|
10
|
+
*
|
|
11
|
+
* **The near end is where the convention says it is**: 1 under reversed depth and 0 under the
|
|
12
|
+
* conventional sense, which a WebGL2 context without `EXT_clip_control` runs. Taken from the
|
|
13
|
+
* renderer's own `reversedDepth`, never from the constant, for the reason `depthConvention.ts`
|
|
14
|
+
* gives at length.
|
|
15
|
+
*
|
|
16
|
+
* **What it gives up**: a view model is in front of everything, so it cannot be drawn reaching
|
|
17
|
+
* behind a world object in its own frame — which is what a view model is for. And under temporal
|
|
18
|
+
* reconstruction its pixels take no motion of their own, because the motion pass matches the frame's
|
|
19
|
+
* depth exactly and a squeezed draw matches nothing; draw a view model with multisampling.
|
|
20
|
+
*/
|
|
21
|
+
/** The share of depth a view model is drawn into when the caller names none: the nearest 1%. */
|
|
22
|
+
export declare const VIEW_MODEL_DEPTH_SHARE = 0.01;
|
|
23
|
+
/**
|
|
24
|
+
* Fill `out` with the `[min, max]` window depth a view model's draws are squeezed into.
|
|
25
|
+
*
|
|
26
|
+
* `share` is clamped to `[1e-4, 1]`: zero would collapse every view-model fragment onto one depth
|
|
27
|
+
* and lose its own sorting, and more than the whole range is no squeeze.
|
|
28
|
+
*/
|
|
29
|
+
export declare function viewModelDepthRange(reversed: boolean, share: number, out: [number, number] | Float32Array): void;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The slice of depth a view model is drawn into: the nearest share of the range.
|
|
3
|
+
*
|
|
4
|
+
* **A first-person view model — arms and a held weapon — must never go into the wall it is pushed
|
|
5
|
+
* against**, and clearing depth to draw it on top costs every pass that reads depth afterwards:
|
|
6
|
+
* ambient occlusion, depth of field and the fog all see a hole where the world was. Squeezing the
|
|
7
|
+
* view model's depth into the nearest sliver of the range does neither. Anything past the near
|
|
8
|
+
* plane in the world lands behind it, the world's depth is kept, and the view model still sorts
|
|
9
|
+
* against itself, at a fraction of the precision.
|
|
10
|
+
*
|
|
11
|
+
* **The near end is where the convention says it is**: 1 under reversed depth and 0 under the
|
|
12
|
+
* conventional sense, which a WebGL2 context without `EXT_clip_control` runs. Taken from the
|
|
13
|
+
* renderer's own `reversedDepth`, never from the constant, for the reason `depthConvention.ts`
|
|
14
|
+
* gives at length.
|
|
15
|
+
*
|
|
16
|
+
* **What it gives up**: a view model is in front of everything, so it cannot be drawn reaching
|
|
17
|
+
* behind a world object in its own frame — which is what a view model is for. And under temporal
|
|
18
|
+
* reconstruction its pixels take no motion of their own, because the motion pass matches the frame's
|
|
19
|
+
* depth exactly and a squeezed draw matches nothing; draw a view model with multisampling.
|
|
20
|
+
*/
|
|
21
|
+
/** The share of depth a view model is drawn into when the caller names none: the nearest 1%. */
|
|
22
|
+
export const VIEW_MODEL_DEPTH_SHARE = 0.01;
|
|
23
|
+
/**
|
|
24
|
+
* Fill `out` with the `[min, max]` window depth a view model's draws are squeezed into.
|
|
25
|
+
*
|
|
26
|
+
* `share` is clamped to `[1e-4, 1]`: zero would collapse every view-model fragment onto one depth
|
|
27
|
+
* and lose its own sorting, and more than the whole range is no squeeze.
|
|
28
|
+
*/
|
|
29
|
+
export function viewModelDepthRange(reversed, share, out) {
|
|
30
|
+
const width = Number.isFinite(share)
|
|
31
|
+
? Math.min(1, Math.max(1e-4, share))
|
|
32
|
+
: VIEW_MODEL_DEPTH_SHARE;
|
|
33
|
+
out[0] = reversed ? 1 - width : 0;
|
|
34
|
+
out[1] = reversed ? 1 : width;
|
|
35
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@driftengine/core",
|
|
3
|
-
"version": "4.8.
|
|
3
|
+
"version": "4.8.2",
|
|
4
4
|
"description": "The runtime of DriftEngine, a WebGPU game engine with a WebGL2 fallback: renderer, loop, input, cameras, scene graph and physics",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
],
|
|
30
30
|
"sideEffects": false,
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@driftengine/drft": "4.8.
|
|
33
|
-
"@driftengine/physics": "4.8.
|
|
32
|
+
"@driftengine/drft": "4.8.2",
|
|
33
|
+
"@driftengine/physics": "4.8.2",
|
|
34
34
|
"gl-matrix": "^3.4.3"
|
|
35
35
|
},
|
|
36
36
|
"author": "Drift Technologies",
|
|
@@ -67,8 +67,8 @@
|
|
|
67
67
|
"prepack": "node ../../scripts/build.mjs @driftengine/core"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
|
-
"@driftengine/nav": "4.8.
|
|
71
|
-
"@driftengine/network": "4.8.
|
|
72
|
-
"@driftengine/texture": "4.8.
|
|
70
|
+
"@driftengine/nav": "4.8.2",
|
|
71
|
+
"@driftengine/network": "4.8.2",
|
|
72
|
+
"@driftengine/texture": "4.8.2"
|
|
73
73
|
}
|
|
74
74
|
}
|
package/src/index.ts
CHANGED
|
@@ -743,6 +743,8 @@ export {
|
|
|
743
743
|
REVERSED_DEPTH,
|
|
744
744
|
conventionalDepth,
|
|
745
745
|
} from './render/depthConvention.ts';
|
|
746
|
+
/* How much of the depth range `beginViewModel` takes when it is given no share. See `viewModel.ts`. */
|
|
747
|
+
export { VIEW_MODEL_DEPTH_SHARE } from './render/viewModel.ts';
|
|
746
748
|
export type { ColliderSurfaceOptions } from './physics/colliderSurface.ts';
|
|
747
749
|
export { BoxSurface } from './physics/boxSurface.ts';
|
|
748
750
|
export { heightSurface } from './physics/heightSurface.ts';
|
|
@@ -864,7 +866,13 @@ export {
|
|
|
864
866
|
export { InputSource } from './input/input.ts';
|
|
865
867
|
export { isTypingTarget } from './input/typingTarget.ts';
|
|
866
868
|
export type { TouchPoint, InputCallbacks } from './input/input.ts';
|
|
867
|
-
export type {
|
|
869
|
+
export type {
|
|
870
|
+
GamepadIdentity,
|
|
871
|
+
GamepadView,
|
|
872
|
+
InputOptions,
|
|
873
|
+
InputSourceName,
|
|
874
|
+
MouseButton,
|
|
875
|
+
} from './input/input.ts';
|
|
868
876
|
export type { InputDevice } from './input/activeDevice.ts';
|
|
869
877
|
export type { GamepadFamily } from './input/gamepadIdentity.ts';
|
|
870
878
|
/* Positions rather than lettering, and the constant a consumer overrides to taste. */
|
package/src/input/actionMap.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { KeyValueStore } from '../core/storage.ts';
|
|
2
|
-
import type { InputSource } from './input.ts';
|
|
2
|
+
import type { InputSource, MouseButton } from './input.ts';
|
|
3
3
|
import type { GamepadButton } from './gamepadMapping.ts';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -18,12 +18,15 @@ import type { GamepadButton } from './gamepadMapping.ts';
|
|
|
18
18
|
/** One thing that can satisfy an action. */
|
|
19
19
|
export type Binding =
|
|
20
20
|
| { readonly device: 'keyboard'; readonly code: string }
|
|
21
|
-
| { readonly device: 'gamepad'; readonly button: GamepadButton }
|
|
21
|
+
| { readonly device: 'gamepad'; readonly button: GamepadButton }
|
|
22
|
+
| { readonly device: 'mouse'; readonly button: MouseButton };
|
|
22
23
|
|
|
23
24
|
/** Jump, fire, pause: a state, bound to keys and buttons. */
|
|
24
25
|
export interface DigitalAction {
|
|
25
26
|
readonly keys?: readonly string[];
|
|
26
27
|
readonly buttons?: readonly GamepadButton[];
|
|
28
|
+
/** Mouse buttons, pressed over the input source's target: fire on the left, aim on the right. */
|
|
29
|
+
readonly mouseButtons?: readonly MouseButton[];
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
/**
|
|
@@ -92,7 +95,9 @@ export class ActionMap {
|
|
|
92
95
|
return this.any(action, (binding) =>
|
|
93
96
|
binding.device === 'keyboard'
|
|
94
97
|
? this.input.isDown(binding.code)
|
|
95
|
-
:
|
|
98
|
+
: binding.device === 'mouse'
|
|
99
|
+
? this.input.mouseDown(binding.button)
|
|
100
|
+
: (this.input.pad(0)?.down(binding.button) ?? false),
|
|
96
101
|
);
|
|
97
102
|
}
|
|
98
103
|
|
|
@@ -101,7 +106,9 @@ export class ActionMap {
|
|
|
101
106
|
return this.any(action, (binding) =>
|
|
102
107
|
binding.device === 'keyboard'
|
|
103
108
|
? this.input.keyPressed(binding.code)
|
|
104
|
-
:
|
|
109
|
+
: binding.device === 'mouse'
|
|
110
|
+
? this.input.mousePressed(binding.button)
|
|
111
|
+
: (this.input.pad(0)?.pressed(binding.button) ?? false),
|
|
105
112
|
);
|
|
106
113
|
}
|
|
107
114
|
|
|
@@ -118,6 +125,8 @@ export class ActionMap {
|
|
|
118
125
|
for (const binding of bindings) {
|
|
119
126
|
if (binding.device === 'keyboard') {
|
|
120
127
|
if (this.input.consumeKeyPress(binding.code)) return true;
|
|
128
|
+
} else if (binding.device === 'mouse') {
|
|
129
|
+
if (this.input.consumeMousePress(binding.button)) return true;
|
|
121
130
|
} else if (this.input.pad(0)?.consumePress(binding.button) === true) {
|
|
122
131
|
return true;
|
|
123
132
|
}
|
|
@@ -354,16 +363,19 @@ function bindingsOf(definition: DigitalAction): Binding[] {
|
|
|
354
363
|
const list: Binding[] = [];
|
|
355
364
|
for (const code of definition.keys ?? []) list.push({ device: 'keyboard', code });
|
|
356
365
|
for (const button of definition.buttons ?? []) list.push({ device: 'gamepad', button });
|
|
366
|
+
for (const button of definition.mouseButtons ?? []) list.push({ device: 'mouse', button });
|
|
357
367
|
return list;
|
|
358
368
|
}
|
|
359
369
|
|
|
360
370
|
function sameBinding(a: Binding, b: Binding): boolean {
|
|
361
|
-
if (a.device
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
371
|
+
if (a.device === 'keyboard' || b.device === 'keyboard') {
|
|
372
|
+
return a.device === 'keyboard' && b.device === 'keyboard' && a.code === b.code;
|
|
373
|
+
}
|
|
374
|
+
return a.device === b.device && a.button === b.button;
|
|
365
375
|
}
|
|
366
376
|
|
|
377
|
+
const MOUSE_BUTTONS: ReadonlySet<string> = new Set(['left', 'middle', 'right', 'back', 'forward']);
|
|
378
|
+
|
|
367
379
|
function sameList(a: readonly Binding[], b: readonly Binding[]): boolean {
|
|
368
380
|
return a.length === b.length && a.every((entry, at) => sameBinding(entry, b[at] as Binding));
|
|
369
381
|
}
|
|
@@ -373,5 +385,8 @@ function isBinding(value: unknown): value is Binding {
|
|
|
373
385
|
if (typeof value !== 'object' || value === null) return false;
|
|
374
386
|
const candidate = value as { device?: unknown; code?: unknown; button?: unknown };
|
|
375
387
|
if (candidate.device === 'keyboard') return typeof candidate.code === 'string';
|
|
388
|
+
if (candidate.device === 'mouse') {
|
|
389
|
+
return typeof candidate.button === 'string' && MOUSE_BUTTONS.has(candidate.button);
|
|
390
|
+
}
|
|
376
391
|
return candidate.device === 'gamepad' && typeof candidate.button === 'string';
|
|
377
392
|
}
|
package/src/input/input.ts
CHANGED
|
@@ -104,6 +104,23 @@ export interface TouchPoint {
|
|
|
104
104
|
/** The devices this source watches, and the granularity a consumer may switch off. */
|
|
105
105
|
export type InputSourceName = 'keyboard' | 'mouse' | 'touch' | 'gamepad';
|
|
106
106
|
|
|
107
|
+
/** A mouse button by what it is, in `MouseEvent.button` order. */
|
|
108
|
+
export type MouseButton = 'left' | 'middle' | 'right' | 'back' | 'forward';
|
|
109
|
+
|
|
110
|
+
/** One bit a button, so the held, pressed and claimed states are integers and a read allocates nothing. */
|
|
111
|
+
const MOUSE_BIT: Readonly<Record<MouseButton, number>> = {
|
|
112
|
+
left: 1,
|
|
113
|
+
middle: 2,
|
|
114
|
+
right: 4,
|
|
115
|
+
back: 8,
|
|
116
|
+
forward: 16,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/** `MouseEvent.button` as a bit, or 0 for a button past the five this names. */
|
|
120
|
+
function mouseBit(button: number): number {
|
|
121
|
+
return button >= 0 && button < 5 ? 1 << button : 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
107
124
|
export interface InputOptions {
|
|
108
125
|
/**
|
|
109
126
|
* Turn a device off. Absent means on.
|
|
@@ -420,6 +437,19 @@ export class InputSource {
|
|
|
420
437
|
private mouseDx = 0;
|
|
421
438
|
private mouseDy = 0;
|
|
422
439
|
private mouseButtonDown = false;
|
|
440
|
+
/**
|
|
441
|
+
* Each mouse button's own state, as the keyboard's sets are for keys: held, pressed since the
|
|
442
|
+
* last poll, claimed this frame, and waiting for somebody to act on it. Bits, by `MOUSE_BIT`.
|
|
443
|
+
*
|
|
444
|
+
* **Beside `mouseButtonDown`, not instead of it.** That flag is "a button is held", which is what
|
|
445
|
+
* an unlocked drag-to-look reads; a game binding fire to the left button and aim to the right
|
|
446
|
+
* needs to know which, and to see a release of one while the other stays down.
|
|
447
|
+
*/
|
|
448
|
+
private mouseHeld = 0;
|
|
449
|
+
private mousePressedNow = 0;
|
|
450
|
+
private mousePressedNext = 0;
|
|
451
|
+
private mouseClaimed = 0;
|
|
452
|
+
private mouseUnclaimed = 0;
|
|
423
453
|
private readonly disposers: Array<() => void> = [];
|
|
424
454
|
|
|
425
455
|
/** Which device the player last actually used, and who wants telling when that changes. */
|
|
@@ -531,13 +561,21 @@ export class InputSource {
|
|
|
531
561
|
this.keysUnclaimed.clear();
|
|
532
562
|
});
|
|
533
563
|
|
|
534
|
-
this.listen(target, 'mousedown', () => {
|
|
564
|
+
this.listen(target, 'mousedown', (e: MouseEvent) => {
|
|
535
565
|
if (!this.enabled.mouse) return;
|
|
536
566
|
this.mouseButtonDown = true;
|
|
567
|
+
const bit = mouseBit(e.button);
|
|
568
|
+
this.mouseHeld |= bit;
|
|
569
|
+
this.mousePressedNext |= bit;
|
|
570
|
+
this.mouseUnclaimed |= bit;
|
|
537
571
|
this.active.use('mouse');
|
|
538
572
|
});
|
|
539
|
-
|
|
573
|
+
/* On the window, so a press that becomes a drag off the canvas still comes back up. */
|
|
574
|
+
this.listen(window, 'mouseup', (e: MouseEvent) => {
|
|
540
575
|
this.mouseButtonDown = false;
|
|
576
|
+
const bit = mouseBit(e.button);
|
|
577
|
+
this.mouseHeld &= ~bit;
|
|
578
|
+
this.mouseUnclaimed &= ~bit;
|
|
541
579
|
});
|
|
542
580
|
/*
|
|
543
581
|
* **Released by anything that can eat the mouseup, exactly as the keys are.**
|
|
@@ -559,15 +597,11 @@ export class InputSource {
|
|
|
559
597
|
* `visibilitychange` as well as `blur`, because a tab switched away with a button held
|
|
560
598
|
* comes back to a document that never saw the release either.
|
|
561
599
|
*/
|
|
562
|
-
this.listen(window, 'blur', () =>
|
|
563
|
-
|
|
564
|
-
});
|
|
565
|
-
this.listen(window, 'pointercancel', () => {
|
|
566
|
-
this.mouseButtonDown = false;
|
|
567
|
-
});
|
|
600
|
+
this.listen(window, 'blur', () => this.releaseMouse());
|
|
601
|
+
this.listen(window, 'pointercancel', () => this.releaseMouse());
|
|
568
602
|
/* Not in `GlobalEventHandlersEventMap`, so it is bound directly and disposed by hand. */
|
|
569
603
|
const onHidden = (): void => {
|
|
570
|
-
if (document.visibilityState !== 'visible') this.
|
|
604
|
+
if (document.visibilityState !== 'visible') this.releaseMouse();
|
|
571
605
|
};
|
|
572
606
|
document.addEventListener('visibilitychange', onHidden);
|
|
573
607
|
this.disposers.push(() => document.removeEventListener('visibilitychange', onHidden));
|
|
@@ -676,6 +710,10 @@ export class InputSource {
|
|
|
676
710
|
spent.clear();
|
|
677
711
|
this.keysPressedNext = spent;
|
|
678
712
|
this.keysClaimed.clear();
|
|
713
|
+
/* The mouse buttons' frame boundary, on the same poll and by the same rule. */
|
|
714
|
+
this.mousePressedNow = this.mousePressedNext;
|
|
715
|
+
this.mousePressedNext = 0;
|
|
716
|
+
this.mouseClaimed = 0;
|
|
679
717
|
|
|
680
718
|
if (!this.enabled.gamepad) return;
|
|
681
719
|
const source = (globalThis as { navigator?: { getGamepads?: () => (RawPad | null)[] } })
|
|
@@ -768,6 +806,38 @@ export class InputSource {
|
|
|
768
806
|
return true;
|
|
769
807
|
}
|
|
770
808
|
|
|
809
|
+
/**
|
|
810
|
+
* Whether a mouse button is held: pressed over the target and not yet released anywhere.
|
|
811
|
+
*
|
|
812
|
+
* A browser opens its context menu on the right button's release; a game binding that button
|
|
813
|
+
* cancels `contextmenu` on its canvas, which is a decision about the page and not this source's.
|
|
814
|
+
*/
|
|
815
|
+
mouseDown(button: MouseButton): boolean {
|
|
816
|
+
return (this.mouseHeld & MOUSE_BIT[button]) !== 0;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/** Whether a mouse button went down since the last poll, as `keyPressed` is for a key. */
|
|
820
|
+
mousePressed(button: MouseButton): boolean {
|
|
821
|
+
const bit = MOUSE_BIT[button];
|
|
822
|
+
return (this.mousePressedNow & bit) !== 0 && (this.mouseClaimed & bit) === 0;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/** The same edge, claimed, as `consumeKeyPress` is for a key: true for exactly one caller. */
|
|
826
|
+
consumeMousePress(button: MouseButton): boolean {
|
|
827
|
+
const bit = MOUSE_BIT[button];
|
|
828
|
+
if ((this.mouseUnclaimed & bit) === 0) return false;
|
|
829
|
+
this.mouseUnclaimed &= ~bit;
|
|
830
|
+
this.mouseClaimed |= bit;
|
|
831
|
+
return true;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/** Every button up and every waiting press gone: what a lost focus or a cancelled pointer means. */
|
|
835
|
+
private releaseMouse(): void {
|
|
836
|
+
this.mouseButtonDown = false;
|
|
837
|
+
this.mouseHeld = 0;
|
|
838
|
+
this.mouseUnclaimed = 0;
|
|
839
|
+
}
|
|
840
|
+
|
|
771
841
|
/**
|
|
772
842
|
* Add a device-event subscriber. Multiple reusable control layers may listen
|
|
773
843
|
* without overwriting one another; interpretation still happens outside the
|
|
@@ -96,9 +96,19 @@ export function fakeBrowser(id = 'Xbox 360 Controller (XInput STANDARD GAMEPAD)'
|
|
|
96
96
|
const frameCallbacks: ((time: number) => void)[] = [];
|
|
97
97
|
let nextFrameHandle = 1;
|
|
98
98
|
|
|
99
|
+
const targetListeners = new Map<string, ((event: unknown) => void)[]>();
|
|
99
100
|
const target = {
|
|
100
|
-
addEventListener: () => {
|
|
101
|
-
|
|
101
|
+
addEventListener: (type: string, fn: (event: unknown) => void) => {
|
|
102
|
+
const existing = targetListeners.get(type);
|
|
103
|
+
if (existing === undefined) targetListeners.set(type, [fn]);
|
|
104
|
+
else existing.push(fn);
|
|
105
|
+
},
|
|
106
|
+
removeEventListener: (type: string, fn: (event: unknown) => void) => {
|
|
107
|
+
const existing = targetListeners.get(type);
|
|
108
|
+
if (existing === undefined) return;
|
|
109
|
+
const at = existing.indexOf(fn);
|
|
110
|
+
if (at >= 0) existing.splice(at, 1);
|
|
111
|
+
},
|
|
102
112
|
requestPointerLock: () => undefined,
|
|
103
113
|
getBoundingClientRect: () => ({ left: 0, top: 0, width: 640, height: 480 }),
|
|
104
114
|
} as unknown as HTMLElement;
|
|
@@ -204,6 +214,21 @@ export function fakeBrowser(id = 'Xbox 360 Controller (XInput STANDARD GAMEPAD)'
|
|
|
204
214
|
keyUp(code: string): void {
|
|
205
215
|
for (const fn of windowListeners.get('keyup') ?? []) fn({ code });
|
|
206
216
|
},
|
|
217
|
+
/**
|
|
218
|
+
* Press a mouse button over the target, by `MouseEvent.button`: 0 left, 1 middle, 2 right.
|
|
219
|
+
* Pressed on the target and released on the window, as a browser delivers a drag that ends
|
|
220
|
+
* off the canvas.
|
|
221
|
+
*/
|
|
222
|
+
mouseDown(button: number): void {
|
|
223
|
+
for (const fn of targetListeners.get('mousedown') ?? []) fn({ button, movementX: 0 });
|
|
224
|
+
},
|
|
225
|
+
mouseUp(button: number): void {
|
|
226
|
+
for (const fn of windowListeners.get('mouseup') ?? []) fn({ button });
|
|
227
|
+
},
|
|
228
|
+
/** The window losing focus, which eats every release that would have followed. */
|
|
229
|
+
blur(): void {
|
|
230
|
+
for (const fn of windowListeners.get('blur') ?? []) fn({});
|
|
231
|
+
},
|
|
207
232
|
/** Run exactly one animation frame, which is what a poll rides on. */
|
|
208
233
|
frame(): void {
|
|
209
234
|
const pending = frameCallbacks.splice(0, frameCallbacks.length);
|