@forgeax/engine-input 0.1.19 → 0.1.21
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 +15 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/browser-backend-edge-latch.test.d.ts +2 -0
- package/dist/__tests__/browser-backend-edge-latch.test.d.ts.map +1 -0
- package/dist/__tests__/browser-edge-latch.browser.test.d.ts +2 -0
- package/dist/__tests__/browser-edge-latch.browser.test.d.ts.map +1 -0
- package/dist/__tests__/pointer-lock.browser.test.d.ts +2 -0
- package/dist/__tests__/pointer-lock.browser.test.d.ts.map +1 -0
- package/dist/browser-backend.d.ts.map +1 -1
- package/dist/composite-backend.d.ts +4 -1
- package/dist/composite-backend.d.ts.map +1 -1
- package/dist/frame-start-scan-system.d.ts +2 -2
- package/dist/index.mjs +118 -35
- package/dist/index.mjs.map +1 -1
- package/dist/input-snapshot.d.ts +13 -3
- package/dist/input-snapshot.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/browser-backend-edge-latch.test.ts +154 -0
- package/src/__tests__/browser-backend-focus-loss.test.ts +4 -0
- package/src/__tests__/browser-backend-lock-error.test.ts +25 -11
- package/src/__tests__/browser-backend-lock-gate.test.ts +8 -1
- package/src/__tests__/browser-edge-latch.browser.test.ts +42 -0
- package/src/__tests__/composite-backend.test.ts +18 -0
- package/src/__tests__/input-snapshot.test.ts +35 -0
- package/src/__tests__/pointer-lock.browser.test.ts +59 -0
- package/src/browser-backend.ts +71 -19
- package/src/composite-backend.ts +70 -3
- package/src/frame-start-scan-system.ts +2 -2
- package/src/input-snapshot.ts +45 -25
package/src/browser-backend.ts
CHANGED
|
@@ -176,7 +176,11 @@ export function attachBrowserInputBackend(
|
|
|
176
176
|
const upEdges = new Set<string>();
|
|
177
177
|
const heldCodes = new Set<string>();
|
|
178
178
|
const upCodeEdges = new Set<string>();
|
|
179
|
+
const pressedKeys = new Set<string>();
|
|
180
|
+
const pressedCodes = new Set<string>();
|
|
179
181
|
const buttons: [boolean, boolean, boolean] = [false, false, false];
|
|
182
|
+
const pressedButtons: [boolean, boolean, boolean] = [false, false, false];
|
|
183
|
+
const releasedButtons: [boolean, boolean, boolean] = [false, false, false];
|
|
180
184
|
let mvx = 0;
|
|
181
185
|
let mvy = 0;
|
|
182
186
|
let wheelAccum = 0;
|
|
@@ -195,6 +199,15 @@ export function attachBrowserInputBackend(
|
|
|
195
199
|
// gameGate is a command-set boolean (setPointerLockAllowed), default true.
|
|
196
200
|
let gameGate = true;
|
|
197
201
|
|
|
202
|
+
function emitLockError(path: 'w3c' | 'provider', cause: unknown): void {
|
|
203
|
+
try {
|
|
204
|
+
options.onLockError?.({ path, cause });
|
|
205
|
+
} catch {
|
|
206
|
+
// Lock diagnostics are observational; a throwing host callback must not
|
|
207
|
+
// create a second unhandled rejection from the browser Promise path.
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
198
211
|
// w8 (D-1/D-3): release the provider lock idempotently. Calls the injected
|
|
199
212
|
// exitLock (routing any throw to onLockError as { path: 'provider' }) and
|
|
200
213
|
// clears providerLocked unconditionally. Shared by every provider-release
|
|
@@ -205,7 +218,7 @@ export function attachBrowserInputBackend(
|
|
|
205
218
|
try {
|
|
206
219
|
options.lockProvider.exitLock();
|
|
207
220
|
} catch (err: unknown) {
|
|
208
|
-
|
|
221
|
+
emitLockError('provider', err);
|
|
209
222
|
}
|
|
210
223
|
}
|
|
211
224
|
providerLocked = false;
|
|
@@ -348,11 +361,11 @@ export function attachBrowserInputBackend(
|
|
|
348
361
|
clear();
|
|
349
362
|
return;
|
|
350
363
|
}
|
|
364
|
+
if (!heldKeys.has(ev.key)) pressedKeys.add(ev.key);
|
|
351
365
|
heldKeys.add(ev.key);
|
|
352
|
-
upEdges.delete(ev.key);
|
|
353
366
|
if (ev.code) {
|
|
367
|
+
if (!heldCodes.has(ev.code)) pressedCodes.add(ev.code);
|
|
354
368
|
heldCodes.add(ev.code);
|
|
355
|
-
upCodeEdges.delete(ev.code);
|
|
356
369
|
}
|
|
357
370
|
// w8 (D-1): ESC releases provider lock (W3C path handles ESC via browser
|
|
358
371
|
// pointerlockchange). Only acts when providerLocked is true.
|
|
@@ -379,6 +392,7 @@ export function attachBrowserInputBackend(
|
|
|
379
392
|
// Only mouse-type pointers affect the mouse button cluster (D-3).
|
|
380
393
|
if (ev.pointerType === 'mouse') {
|
|
381
394
|
if (ev.button === 0 || ev.button === 1 || ev.button === 2) {
|
|
395
|
+
if (!buttons[ev.button]) pressedButtons[ev.button] = true;
|
|
382
396
|
buttons[ev.button] = true;
|
|
383
397
|
}
|
|
384
398
|
}
|
|
@@ -453,6 +467,7 @@ export function attachBrowserInputBackend(
|
|
|
453
467
|
if (isUiEvent(ev)) return;
|
|
454
468
|
if (ev.pointerType === 'mouse') {
|
|
455
469
|
if (ev.button === 0 || ev.button === 1 || ev.button === 2) {
|
|
470
|
+
if (buttons[ev.button]) releasedButtons[ev.button] = true;
|
|
456
471
|
buttons[ev.button] = false;
|
|
457
472
|
}
|
|
458
473
|
}
|
|
@@ -547,6 +562,14 @@ export function attachBrowserInputBackend(
|
|
|
547
562
|
// sole consumer as it did before ownership routing existed.
|
|
548
563
|
upEdges.clear();
|
|
549
564
|
upCodeEdges.clear();
|
|
565
|
+
pressedKeys.clear();
|
|
566
|
+
pressedCodes.clear();
|
|
567
|
+
pressedButtons[0] = false;
|
|
568
|
+
pressedButtons[1] = false;
|
|
569
|
+
pressedButtons[2] = false;
|
|
570
|
+
releasedButtons[0] = false;
|
|
571
|
+
releasedButtons[1] = false;
|
|
572
|
+
releasedButtons[2] = false;
|
|
550
573
|
heldKeys.clear();
|
|
551
574
|
heldCodes.clear();
|
|
552
575
|
buttons[0] = false;
|
|
@@ -623,6 +646,11 @@ export function attachBrowserInputBackend(
|
|
|
623
646
|
}
|
|
624
647
|
safeAdd(doc, 'pointerlockchange', onPointerLockChange as EventListener);
|
|
625
648
|
|
|
649
|
+
function onPointerLockError(event: Event): void {
|
|
650
|
+
emitLockError('w3c', event);
|
|
651
|
+
}
|
|
652
|
+
safeAdd(doc, 'pointerlockerror', onPointerLockError as EventListener);
|
|
653
|
+
|
|
626
654
|
// PointerLock entry must be triggered by user activation (W3C requires
|
|
627
655
|
// a click / keydown handler to call `requestPointerLock()`). The MVP
|
|
628
656
|
// wires a click listener on the canvas as the activation surface;
|
|
@@ -645,9 +673,6 @@ export function attachBrowserInputBackend(
|
|
|
645
673
|
// the cursor outside the play-game quadrant) skip the lock entirely. Default
|
|
646
674
|
// is always-allow, so the standalone game runtime is unchanged.
|
|
647
675
|
if (options.pointerLockAllowed && !options.pointerLockAllowed()) return;
|
|
648
|
-
// Pointer Lock requires window focus; skip silently when unfocused.
|
|
649
|
-
if (typeof doc.hasFocus === 'function' && !doc.hasFocus()) return;
|
|
650
|
-
|
|
651
676
|
// D-2 / D-7: lockProvider path takes priority over W3C.
|
|
652
677
|
if (options.lockProvider) {
|
|
653
678
|
providerLocked = true; // D-7 optimistic placement
|
|
@@ -656,17 +681,13 @@ export function attachBrowserInputBackend(
|
|
|
656
681
|
if (result && typeof (result as Promise<void>).catch === 'function') {
|
|
657
682
|
(result as Promise<void>).catch((cause: unknown) => {
|
|
658
683
|
providerLocked = false;
|
|
659
|
-
|
|
660
|
-
options.onLockError({ path: 'provider', cause });
|
|
661
|
-
}
|
|
684
|
+
emitLockError('provider', cause);
|
|
662
685
|
});
|
|
663
686
|
}
|
|
664
687
|
} catch (cause: unknown) {
|
|
665
688
|
// Synchronous throw from requestLock.
|
|
666
689
|
providerLocked = false;
|
|
667
|
-
|
|
668
|
-
options.onLockError({ path: 'provider', cause });
|
|
669
|
-
}
|
|
690
|
+
emitLockError('provider', cause);
|
|
670
691
|
}
|
|
671
692
|
return;
|
|
672
693
|
}
|
|
@@ -674,13 +695,15 @@ export function attachBrowserInputBackend(
|
|
|
674
695
|
// W3C path: standard requestPointerLock.
|
|
675
696
|
const fn = canvas.requestPointerLock;
|
|
676
697
|
if (typeof fn !== 'function') return;
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
(r as Promise<void>).catch
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
}
|
|
683
|
-
}
|
|
698
|
+
try {
|
|
699
|
+
const r = fn.call(canvas) as unknown;
|
|
700
|
+
if (r && typeof (r as Promise<void>).catch === 'function') {
|
|
701
|
+
(r as Promise<void>).catch((cause: unknown) => {
|
|
702
|
+
emitLockError('w3c', cause);
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
} catch (cause: unknown) {
|
|
706
|
+
emitLockError('w3c', cause);
|
|
684
707
|
}
|
|
685
708
|
}
|
|
686
709
|
safeAdd(canvas, 'click', onCanvasClick as EventListener);
|
|
@@ -787,7 +810,19 @@ export function attachBrowserInputBackend(
|
|
|
787
810
|
upKeys: new Set(upEdges),
|
|
788
811
|
downCodes: new Set(heldCodes),
|
|
789
812
|
upCodes: new Set(upCodeEdges),
|
|
813
|
+
pressedKeys: new Set(pressedKeys),
|
|
814
|
+
pressedCodes: new Set(pressedCodes),
|
|
790
815
|
buttons: [buttons[0], buttons[1], buttons[2]] as readonly [boolean, boolean, boolean],
|
|
816
|
+
pressedButtons: [pressedButtons[0], pressedButtons[1], pressedButtons[2]] as readonly [
|
|
817
|
+
boolean,
|
|
818
|
+
boolean,
|
|
819
|
+
boolean,
|
|
820
|
+
],
|
|
821
|
+
releasedButtons: [releasedButtons[0], releasedButtons[1], releasedButtons[2]] as readonly [
|
|
822
|
+
boolean,
|
|
823
|
+
boolean,
|
|
824
|
+
boolean,
|
|
825
|
+
],
|
|
791
826
|
movementX: mvx,
|
|
792
827
|
movementY: mvy,
|
|
793
828
|
...(mouseX !== undefined && mouseY !== undefined ? { mouseX, mouseY } : {}),
|
|
@@ -806,6 +841,14 @@ export function attachBrowserInputBackend(
|
|
|
806
841
|
// Reset per-frame accumulators (movement delta + key edges + wheel notches + phase queue).
|
|
807
842
|
upEdges.clear();
|
|
808
843
|
upCodeEdges.clear();
|
|
844
|
+
pressedKeys.clear();
|
|
845
|
+
pressedCodes.clear();
|
|
846
|
+
pressedButtons[0] = false;
|
|
847
|
+
pressedButtons[1] = false;
|
|
848
|
+
pressedButtons[2] = false;
|
|
849
|
+
releasedButtons[0] = false;
|
|
850
|
+
releasedButtons[1] = false;
|
|
851
|
+
releasedButtons[2] = false;
|
|
809
852
|
mvx = 0;
|
|
810
853
|
mvy = 0;
|
|
811
854
|
wheelAccum = 0;
|
|
@@ -831,8 +874,10 @@ export function attachBrowserInputBackend(
|
|
|
831
874
|
function clear(): void {
|
|
832
875
|
heldKeys.clear();
|
|
833
876
|
upEdges.clear();
|
|
877
|
+
pressedKeys.clear();
|
|
834
878
|
heldCodes.clear();
|
|
835
879
|
upCodeEdges.clear();
|
|
880
|
+
pressedCodes.clear();
|
|
836
881
|
pointerMap.clear();
|
|
837
882
|
phaseQueue.length = 0;
|
|
838
883
|
prevGamepadFrame.clear();
|
|
@@ -841,6 +886,12 @@ export function attachBrowserInputBackend(
|
|
|
841
886
|
buttons[0] = false;
|
|
842
887
|
buttons[1] = false;
|
|
843
888
|
buttons[2] = false;
|
|
889
|
+
pressedButtons[0] = false;
|
|
890
|
+
pressedButtons[1] = false;
|
|
891
|
+
pressedButtons[2] = false;
|
|
892
|
+
releasedButtons[0] = false;
|
|
893
|
+
releasedButtons[1] = false;
|
|
894
|
+
releasedButtons[2] = false;
|
|
844
895
|
mvx = 0;
|
|
845
896
|
mvy = 0;
|
|
846
897
|
wheelAccum = 0;
|
|
@@ -866,6 +917,7 @@ export function attachBrowserInputBackend(
|
|
|
866
917
|
safeRemove(canvas, 'wheel', onWheel as EventListener);
|
|
867
918
|
safeRemove(doc, 'visibilitychange', onVisibilityChange as EventListener);
|
|
868
919
|
safeRemove(doc, 'pointerlockchange', onPointerLockChange as EventListener);
|
|
920
|
+
safeRemove(doc, 'pointerlockerror', onPointerLockError as EventListener);
|
|
869
921
|
safeRemove(canvas, 'click', onCanvasClick as EventListener);
|
|
870
922
|
clear();
|
|
871
923
|
// D-5: restore original touch-action value.
|
package/src/composite-backend.ts
CHANGED
|
@@ -59,7 +59,10 @@ export interface CompositeInputBackend extends InputBackend {
|
|
|
59
59
|
addMovement(dx: number, dy: number): void;
|
|
60
60
|
/** Accumulate injected wheel notches (drained on next sample). */
|
|
61
61
|
addWheel(notches: number): void;
|
|
62
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* Drop all injected state and pending frame edges; currently-held keys/buttons
|
|
64
|
+
* emit one clean release edge so a previously observed hold is closed.
|
|
65
|
+
*/
|
|
63
66
|
clearInjected(): void;
|
|
64
67
|
/** Toggle the yield-to-human gate at runtime. */
|
|
65
68
|
setYieldToHuman(yield_: boolean): void;
|
|
@@ -76,12 +79,22 @@ export function makeCompositeBackend(
|
|
|
76
79
|
const buttons: [boolean, boolean, boolean] = [false, false, false];
|
|
77
80
|
// Injected per-frame accumulators (drained on each sample()).
|
|
78
81
|
const upEdges = new Set<string>();
|
|
82
|
+
const pressedKeys = new Set<string>();
|
|
83
|
+
const pressedButtons: [boolean, boolean, boolean] = [false, false, false];
|
|
84
|
+
const releasedButtons: [boolean, boolean, boolean] = [false, false, false];
|
|
79
85
|
let mvx = 0;
|
|
80
86
|
let mvy = 0;
|
|
81
87
|
let wheel = 0;
|
|
82
88
|
|
|
83
89
|
function injectionActive(): boolean {
|
|
84
|
-
return
|
|
90
|
+
return (
|
|
91
|
+
heldKeys.size > 0 ||
|
|
92
|
+
upEdges.size > 0 ||
|
|
93
|
+
pressedKeys.size > 0 ||
|
|
94
|
+
buttons.some((b) => b) ||
|
|
95
|
+
pressedButtons.some((b) => b) ||
|
|
96
|
+
releasedButtons.some((b) => b)
|
|
97
|
+
);
|
|
85
98
|
}
|
|
86
99
|
|
|
87
100
|
function sample(): InputBackendSample {
|
|
@@ -111,12 +124,38 @@ export function makeCompositeBackend(
|
|
|
111
124
|
mergedUp.add(k);
|
|
112
125
|
}
|
|
113
126
|
|
|
127
|
+
const mergedPressed =
|
|
128
|
+
base.pressedKeys === undefined && pressedKeys.size === 0
|
|
129
|
+
? undefined
|
|
130
|
+
: new Set([...(base.pressedKeys ?? []), ...pressedKeys]);
|
|
131
|
+
if (yieldToHuman && base.downKeys.size > 0 && mergedPressed !== undefined) {
|
|
132
|
+
for (const key of pressedKeys) {
|
|
133
|
+
if (base.downKeys.has(key)) mergedPressed.delete(key);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
114
137
|
// buttons: OR per slot.
|
|
115
138
|
const mergedButtons: readonly [boolean, boolean, boolean] = [
|
|
116
139
|
base.buttons[0] || buttons[0],
|
|
117
140
|
base.buttons[1] || buttons[1],
|
|
118
141
|
base.buttons[2] || buttons[2],
|
|
119
142
|
];
|
|
143
|
+
const mergedPressedButtons =
|
|
144
|
+
base.pressedButtons === undefined && !pressedButtons.some(Boolean)
|
|
145
|
+
? undefined
|
|
146
|
+
: ([
|
|
147
|
+
(base.pressedButtons?.[0] ?? false) || pressedButtons[0],
|
|
148
|
+
(base.pressedButtons?.[1] ?? false) || pressedButtons[1],
|
|
149
|
+
(base.pressedButtons?.[2] ?? false) || pressedButtons[2],
|
|
150
|
+
] as const);
|
|
151
|
+
const mergedReleasedButtons =
|
|
152
|
+
base.releasedButtons === undefined && !releasedButtons.some(Boolean)
|
|
153
|
+
? undefined
|
|
154
|
+
: ([
|
|
155
|
+
(base.releasedButtons?.[0] ?? false) || releasedButtons[0],
|
|
156
|
+
(base.releasedButtons?.[1] ?? false) || releasedButtons[1],
|
|
157
|
+
(base.releasedButtons?.[2] ?? false) || releasedButtons[2],
|
|
158
|
+
] as const);
|
|
120
159
|
|
|
121
160
|
// focused: keep true while WE are injecting, so the scan system does not
|
|
122
161
|
// treat a headless/backgrounded tab (inner.focused === false) as a reason
|
|
@@ -127,7 +166,11 @@ export function makeCompositeBackend(
|
|
|
127
166
|
...base, // carry inner optional fields (pointers/gamepads/gestures/...) untouched
|
|
128
167
|
downKeys,
|
|
129
168
|
upKeys: mergedUp,
|
|
169
|
+
...(mergedPressed === undefined ? {} : { pressedKeys: mergedPressed }),
|
|
170
|
+
...(base.pressedCodes === undefined ? {} : { pressedCodes: base.pressedCodes }),
|
|
130
171
|
buttons: mergedButtons,
|
|
172
|
+
...(mergedPressedButtons === undefined ? {} : { pressedButtons: mergedPressedButtons }),
|
|
173
|
+
...(mergedReleasedButtons === undefined ? {} : { releasedButtons: mergedReleasedButtons }),
|
|
131
174
|
movementX: base.movementX + mvx,
|
|
132
175
|
movementY: base.movementY + mvy,
|
|
133
176
|
wheelDelta: base.wheelDelta + wheel,
|
|
@@ -137,6 +180,13 @@ export function makeCompositeBackend(
|
|
|
137
180
|
|
|
138
181
|
// Drain injected per-frame accumulators (mirrors inner's own drain).
|
|
139
182
|
upEdges.clear();
|
|
183
|
+
pressedKeys.clear();
|
|
184
|
+
pressedButtons[0] = false;
|
|
185
|
+
pressedButtons[1] = false;
|
|
186
|
+
pressedButtons[2] = false;
|
|
187
|
+
releasedButtons[0] = false;
|
|
188
|
+
releasedButtons[1] = false;
|
|
189
|
+
releasedButtons[2] = false;
|
|
140
190
|
mvx = 0;
|
|
141
191
|
mvy = 0;
|
|
142
192
|
wheel = 0;
|
|
@@ -158,13 +208,15 @@ export function makeCompositeBackend(
|
|
|
158
208
|
detach: () => inner.detach(),
|
|
159
209
|
|
|
160
210
|
press(key) {
|
|
211
|
+
if (!heldKeys.has(key)) pressedKeys.add(key);
|
|
161
212
|
heldKeys.add(key);
|
|
162
|
-
upEdges.delete(key); // re-press cancels a pending release edge
|
|
163
213
|
},
|
|
164
214
|
release(key) {
|
|
165
215
|
if (heldKeys.delete(key)) upEdges.add(key); // held -> emit one up-edge
|
|
166
216
|
},
|
|
167
217
|
setButton(slot, down) {
|
|
218
|
+
if (down && !buttons[slot]) pressedButtons[slot] = true;
|
|
219
|
+
if (!down && buttons[slot]) releasedButtons[slot] = true;
|
|
168
220
|
buttons[slot] = down;
|
|
169
221
|
},
|
|
170
222
|
addMovement(dx, dy) {
|
|
@@ -175,8 +227,23 @@ export function makeCompositeBackend(
|
|
|
175
227
|
wheel += notches;
|
|
176
228
|
},
|
|
177
229
|
clearInjected() {
|
|
230
|
+
// Clearing is a lease boundary: discard every pending frame edge first,
|
|
231
|
+
// then emit only the releases required to close state that was held
|
|
232
|
+
// across the boundary. This prevents a press/release queued before a
|
|
233
|
+
// revoke from becoming a ghost edge for the next consumer.
|
|
234
|
+
upEdges.clear();
|
|
235
|
+
pressedKeys.clear();
|
|
178
236
|
for (const k of heldKeys) upEdges.add(k); // clean release edge for each
|
|
179
237
|
heldKeys.clear();
|
|
238
|
+
pressedButtons[0] = false;
|
|
239
|
+
pressedButtons[1] = false;
|
|
240
|
+
pressedButtons[2] = false;
|
|
241
|
+
releasedButtons[0] = false;
|
|
242
|
+
releasedButtons[1] = false;
|
|
243
|
+
releasedButtons[2] = false;
|
|
244
|
+
if (buttons[0]) releasedButtons[0] = true;
|
|
245
|
+
if (buttons[1]) releasedButtons[1] = true;
|
|
246
|
+
if (buttons[2]) releasedButtons[2] = true;
|
|
180
247
|
buttons[0] = buttons[1] = buttons[2] = false;
|
|
181
248
|
mvx = 0;
|
|
182
249
|
mvy = 0;
|
|
@@ -50,8 +50,8 @@ export const INPUT_BACKEND_KEY = 'InputBackend' as const;
|
|
|
50
50
|
* Module-level `defineSystem` with the real fn body — no factory, no closure.
|
|
51
51
|
* Each `world.update()` tick:
|
|
52
52
|
* 1. reads the {@link InputBackend} from `INPUT_BACKEND_KEY` and calls
|
|
53
|
-
* `backend.sample()` to drain the per-frame
|
|
54
|
-
*
|
|
53
|
+
* `backend.sample()` once to atomically drain the per-frame accumulators
|
|
54
|
+
* (movement delta plus key/code/button press and release edge sets);
|
|
55
55
|
* 2. derives a fresh `InputSnapshot` via `snapshotFromSample`;
|
|
56
56
|
* 3. writes it under `INPUT_SNAPSHOT_RESOURCE_KEY` via
|
|
57
57
|
* `world.insertResource` -- idempotent overwrite, charter P4
|
package/src/input-snapshot.ts
CHANGED
|
@@ -349,12 +349,14 @@ export interface InputSnapshot {
|
|
|
349
349
|
export interface InputBackend {
|
|
350
350
|
/**
|
|
351
351
|
* Produce one frame's worth of input data and reset the per-frame
|
|
352
|
-
* accumulators (movement delta +
|
|
353
|
-
* held-button state survive across calls.
|
|
352
|
+
* accumulators (movement delta + edge sets). Held-key state and
|
|
353
|
+
* held-button state survive across calls. Browser producers latch edge
|
|
354
|
+
* transitions when events arrive, so a down+up sequence between two scans
|
|
355
|
+
* remains observable instead of being lost to the held-state projection.
|
|
354
356
|
*
|
|
355
357
|
* Contract notes:
|
|
356
358
|
* - `downKeys` is the held-key set as of the call (held across frames)
|
|
357
|
-
* - `upKeys` is the
|
|
359
|
+
* - `upKeys` is the release-edge set since the previous sample (cleared
|
|
358
360
|
* after the call so the edge appears in exactly one frame)
|
|
359
361
|
* - `buttons` is the held-button slot tuple `[b0, b1, b2]`
|
|
360
362
|
* - `movementX` / `movementY` are the accumulated PointerLock delta
|
|
@@ -392,7 +394,15 @@ export interface InputBackendSample {
|
|
|
392
394
|
readonly downCodes?: ReadonlySet<string>;
|
|
393
395
|
/** One-frame physical code release edges, when the producer has them. */
|
|
394
396
|
readonly upCodes?: ReadonlySet<string>;
|
|
397
|
+
/** Logical key press edges latched since the previous frame-start scan. */
|
|
398
|
+
readonly pressedKeys?: ReadonlySet<string>;
|
|
399
|
+
/** Physical code press edges latched since the previous frame-start scan. */
|
|
400
|
+
readonly pressedCodes?: ReadonlySet<string>;
|
|
395
401
|
readonly buttons: readonly [boolean, boolean, boolean];
|
|
402
|
+
/** Mouse button press edges latched since the previous frame-start scan. */
|
|
403
|
+
readonly pressedButtons?: readonly [boolean, boolean, boolean];
|
|
404
|
+
/** Mouse button release edges latched since the previous frame-start scan. */
|
|
405
|
+
readonly releasedButtons?: readonly [boolean, boolean, boolean];
|
|
396
406
|
readonly movementX: number;
|
|
397
407
|
readonly movementY: number;
|
|
398
408
|
/** Latest canvas-pixel mouse position; absent for non-pointer backends. */
|
|
@@ -530,35 +540,45 @@ export function snapshotFromSample(
|
|
|
530
540
|
// a derived view of the producer's state at one instant.
|
|
531
541
|
const heldKeys = new Set<string>(sample.downKeys);
|
|
532
542
|
const upEdges = new Set<string>(sample.upKeys);
|
|
533
|
-
const justPressedKeys =
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
543
|
+
const justPressedKeys =
|
|
544
|
+
sample.pressedKeys === undefined
|
|
545
|
+
? new Set<string>(
|
|
546
|
+
[...heldKeys].filter(
|
|
547
|
+
(key) => previousSnapshot === undefined || !previousSnapshot.keyboard.down(key),
|
|
548
|
+
),
|
|
549
|
+
)
|
|
550
|
+
: new Set<string>(sample.pressedKeys);
|
|
539
551
|
const heldCodes = new Set<string>(sample.downCodes ?? []);
|
|
540
552
|
const upCodeEdges = new Set<string>(sample.upCodes ?? []);
|
|
541
|
-
const justPressedCodes =
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
553
|
+
const justPressedCodes =
|
|
554
|
+
sample.pressedCodes === undefined
|
|
555
|
+
? new Set<string>(
|
|
556
|
+
[...heldCodes].filter(
|
|
557
|
+
(code) => previousSnapshot === undefined || !previousSnapshot.keyboard.downCode(code),
|
|
558
|
+
),
|
|
559
|
+
)
|
|
560
|
+
: new Set<string>(sample.pressedCodes);
|
|
547
561
|
const buttons: readonly [boolean, boolean, boolean] = [
|
|
548
562
|
sample.buttons[0],
|
|
549
563
|
sample.buttons[1],
|
|
550
564
|
sample.buttons[2],
|
|
551
565
|
];
|
|
552
|
-
const justPressedButtons: readonly [boolean, boolean, boolean] =
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
566
|
+
const justPressedButtons: readonly [boolean, boolean, boolean] =
|
|
567
|
+
sample.pressedButtons === undefined
|
|
568
|
+
? [
|
|
569
|
+
buttons[0] && (previousSnapshot === undefined || !previousSnapshot.mouse.button(0)),
|
|
570
|
+
buttons[1] && (previousSnapshot === undefined || !previousSnapshot.mouse.button(1)),
|
|
571
|
+
buttons[2] && (previousSnapshot === undefined || !previousSnapshot.mouse.button(2)),
|
|
572
|
+
]
|
|
573
|
+
: [sample.pressedButtons[0], sample.pressedButtons[1], sample.pressedButtons[2]];
|
|
574
|
+
const justReleasedButtons: readonly [boolean, boolean, boolean] =
|
|
575
|
+
sample.releasedButtons === undefined
|
|
576
|
+
? [
|
|
577
|
+
sample.focusReset !== true && previousSnapshot?.mouse.button(0) === true && !buttons[0],
|
|
578
|
+
sample.focusReset !== true && previousSnapshot?.mouse.button(1) === true && !buttons[1],
|
|
579
|
+
sample.focusReset !== true && previousSnapshot?.mouse.button(2) === true && !buttons[2],
|
|
580
|
+
]
|
|
581
|
+
: [sample.releasedButtons[0], sample.releasedButtons[1], sample.releasedButtons[2]];
|
|
562
582
|
const movementDelta = Object.freeze({ x: sample.movementX, y: sample.movementY });
|
|
563
583
|
const mousePosition =
|
|
564
584
|
sample.mouseX === undefined || sample.mouseY === undefined
|