@flighthq/input 0.1.0
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/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/inputManager.d.ts +175 -0
- package/dist/inputManager.d.ts.map +1 -0
- package/dist/inputManager.js +1031 -0
- package/dist/inputManager.js.map +1 -0
- package/package.json +38 -0
- package/src/inputManager.test.ts +1229 -0
|
@@ -0,0 +1,1031 @@
|
|
|
1
|
+
import { connectSignal, createSignal, disconnectSignal, emitSignal } from '@flighthq/signals';
|
|
2
|
+
import { GamepadAxisKind as GamepadAxisKindValues, GamepadButtonKind as GamepadButtonKindValues, KeyCode, KeyModifier, } from '@flighthq/types';
|
|
3
|
+
// Maximum axis and button counts used for the compact gamepad-state encoding in InputState.
|
|
4
|
+
// Encoded key: gamepadIndex * MAX_GAMEPAD_AXES + axisIndex (axes) or
|
|
5
|
+
// gamepadIndex * MAX_GAMEPAD_BUTTONS + buttonIndex (buttons).
|
|
6
|
+
const MAX_GAMEPAD_AXES = 32;
|
|
7
|
+
const MAX_GAMEPAD_BUTTONS = 64;
|
|
8
|
+
/**
|
|
9
|
+
* Filters a single gamepad axis value through a simple dead zone.
|
|
10
|
+
* Values within `[-deadZone, deadZone]` are mapped to `0`; values outside
|
|
11
|
+
* are rescaled linearly to `[-1, 1]` so the live range is continuous.
|
|
12
|
+
* `deadZone` must be in `[0, 1)`.
|
|
13
|
+
*/
|
|
14
|
+
export function applyGamepadAxisDeadZone(value, deadZone) {
|
|
15
|
+
if (deadZone <= 0)
|
|
16
|
+
return value;
|
|
17
|
+
const abs = value < 0 ? -value : value;
|
|
18
|
+
if (abs <= deadZone)
|
|
19
|
+
return 0;
|
|
20
|
+
const sign = value < 0 ? -1 : 1;
|
|
21
|
+
return sign * ((abs - deadZone) / (1 - deadZone));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Filters a 2-D stick (left or right) through a **radial** dead zone.
|
|
25
|
+
* The magnitude of `(x, y)` is compared against `deadZone`; if within the
|
|
26
|
+
* dead zone the output is `(0, 0)`, otherwise the input direction is
|
|
27
|
+
* preserved and the magnitude is rescaled linearly to `[0, 1]`.
|
|
28
|
+
*
|
|
29
|
+
* Writes the filtered X and Y into `out.x` and `out.y`.
|
|
30
|
+
* Safe when `out` is the same object as the input (alias-safe).
|
|
31
|
+
*
|
|
32
|
+
* `deadZone` must be in `[0, 1)`.
|
|
33
|
+
*/
|
|
34
|
+
export function applyGamepadStickDeadZone(out, x, y, deadZone) {
|
|
35
|
+
if (deadZone <= 0) {
|
|
36
|
+
out.x = x;
|
|
37
|
+
out.y = y;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const mag = Math.sqrt(x * x + y * y);
|
|
41
|
+
if (mag <= deadZone) {
|
|
42
|
+
out.x = 0;
|
|
43
|
+
out.y = 0;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const scale = (mag - deadZone) / ((1 - deadZone) * mag);
|
|
47
|
+
out.x = x * scale;
|
|
48
|
+
out.y = y * scale;
|
|
49
|
+
}
|
|
50
|
+
export function attachGamepadInput(manager, target, options) {
|
|
51
|
+
const onGamepadConnected = (e) => {
|
|
52
|
+
if (!manager.enabled)
|
|
53
|
+
return;
|
|
54
|
+
const pad = e.gamepad;
|
|
55
|
+
const prev = getOrCreateGamepadPollState(manager);
|
|
56
|
+
prev.axes.set(pad.index, Array.from(pad.axes));
|
|
57
|
+
prev.buttons.set(pad.index, Array.from(pad.buttons, (b) => b.pressed));
|
|
58
|
+
_connectData.gamepad = pad.index;
|
|
59
|
+
_connectData.id = pad.id;
|
|
60
|
+
_connectData.mapping = pad.mapping === 'standard' ? 'standard' : pad.mapping === '' ? '' : 'raw';
|
|
61
|
+
emitSignal(manager.onGamepadConnect, _connectData);
|
|
62
|
+
};
|
|
63
|
+
const onGamepadDisconnected = (e) => {
|
|
64
|
+
if (!manager.enabled)
|
|
65
|
+
return;
|
|
66
|
+
const pad = e.gamepad;
|
|
67
|
+
const prev = getOrCreateGamepadPollState(manager);
|
|
68
|
+
prev.axes.delete(pad.index);
|
|
69
|
+
prev.buttons.delete(pad.index);
|
|
70
|
+
_connectData.gamepad = pad.index;
|
|
71
|
+
_connectData.id = pad.id;
|
|
72
|
+
_connectData.mapping = pad.mapping === 'standard' ? 'standard' : pad.mapping === '' ? '' : 'raw';
|
|
73
|
+
emitSignal(manager.onGamepadDisconnect, _connectData);
|
|
74
|
+
};
|
|
75
|
+
let rafId = 0;
|
|
76
|
+
const loop = () => {
|
|
77
|
+
pollGamepadInput(manager);
|
|
78
|
+
rafId = requestAnimationFrame(loop);
|
|
79
|
+
};
|
|
80
|
+
target.addEventListener('gamepadconnected', onGamepadConnected);
|
|
81
|
+
target.addEventListener('gamepaddisconnected', onGamepadDisconnected);
|
|
82
|
+
rafId = requestAnimationFrame(loop);
|
|
83
|
+
setInputBinding(manager, target, kGamepadInput, () => {
|
|
84
|
+
target.removeEventListener('gamepadconnected', onGamepadConnected);
|
|
85
|
+
target.removeEventListener('gamepaddisconnected', onGamepadDisconnected);
|
|
86
|
+
cancelAnimationFrame(rafId);
|
|
87
|
+
});
|
|
88
|
+
// Suppress unused-options warning; options accepted for API symmetry.
|
|
89
|
+
void options;
|
|
90
|
+
}
|
|
91
|
+
export function attachKeyboardInput(manager, target, options) {
|
|
92
|
+
const preventDefault = options?.preventDefault ?? true;
|
|
93
|
+
const onKeyDown = (e) => {
|
|
94
|
+
if (!manager.enabled)
|
|
95
|
+
return;
|
|
96
|
+
const ke = e;
|
|
97
|
+
if (preventDefault)
|
|
98
|
+
ke.preventDefault();
|
|
99
|
+
setInputKeyboardData(_keyboardData, ke);
|
|
100
|
+
emitSignal(manager.onKeyDown, _keyboardData);
|
|
101
|
+
};
|
|
102
|
+
const onKeyUp = (e) => {
|
|
103
|
+
if (!manager.enabled)
|
|
104
|
+
return;
|
|
105
|
+
const ke = e;
|
|
106
|
+
if (preventDefault)
|
|
107
|
+
ke.preventDefault();
|
|
108
|
+
setInputKeyboardData(_keyboardData, ke);
|
|
109
|
+
emitSignal(manager.onKeyUp, _keyboardData);
|
|
110
|
+
};
|
|
111
|
+
target.addEventListener('keydown', onKeyDown);
|
|
112
|
+
target.addEventListener('keyup', onKeyUp);
|
|
113
|
+
setInputBinding(manager, target, kKeyboardInput, () => {
|
|
114
|
+
target.removeEventListener('keydown', onKeyDown);
|
|
115
|
+
target.removeEventListener('keyup', onKeyUp);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
export function attachPointerInput(manager, element, options) {
|
|
119
|
+
const preventDefault = options?.preventDefault ?? true;
|
|
120
|
+
const onContextMenu = (e) => {
|
|
121
|
+
if (preventDefault)
|
|
122
|
+
e.preventDefault();
|
|
123
|
+
};
|
|
124
|
+
const onPointerCancel = (e) => {
|
|
125
|
+
if (!manager.enabled)
|
|
126
|
+
return;
|
|
127
|
+
if (preventDefault)
|
|
128
|
+
e.preventDefault();
|
|
129
|
+
setInputPointerData(_pointerData, e, 0, 0);
|
|
130
|
+
emitSignal(manager.onPointerCancel, _pointerData);
|
|
131
|
+
};
|
|
132
|
+
const onPointerDown = (e) => {
|
|
133
|
+
if (!manager.enabled)
|
|
134
|
+
return;
|
|
135
|
+
if (preventDefault)
|
|
136
|
+
e.preventDefault();
|
|
137
|
+
setInputPointerData(_pointerData, e, 0, 0);
|
|
138
|
+
emitSignal(manager.onPointerDown, _pointerData);
|
|
139
|
+
};
|
|
140
|
+
const onPointerMove = (e) => {
|
|
141
|
+
if (!manager.enabled)
|
|
142
|
+
return;
|
|
143
|
+
if (preventDefault)
|
|
144
|
+
e.preventDefault();
|
|
145
|
+
setInputPointerData(_pointerData, e, 0, 0);
|
|
146
|
+
emitSignal(manager.onPointerMove, _pointerData);
|
|
147
|
+
};
|
|
148
|
+
const onPointerUp = (e) => {
|
|
149
|
+
if (!manager.enabled)
|
|
150
|
+
return;
|
|
151
|
+
if (preventDefault)
|
|
152
|
+
e.preventDefault();
|
|
153
|
+
setInputPointerData(_pointerData, e, 0, 0);
|
|
154
|
+
emitSignal(manager.onPointerUp, _pointerData);
|
|
155
|
+
};
|
|
156
|
+
element.addEventListener('contextmenu', onContextMenu);
|
|
157
|
+
element.addEventListener('pointercancel', onPointerCancel);
|
|
158
|
+
element.addEventListener('pointerdown', onPointerDown);
|
|
159
|
+
element.addEventListener('pointermove', onPointerMove);
|
|
160
|
+
element.addEventListener('pointerup', onPointerUp);
|
|
161
|
+
setInputBinding(manager, element, kPointerInput, () => {
|
|
162
|
+
element.removeEventListener('contextmenu', onContextMenu);
|
|
163
|
+
element.removeEventListener('pointercancel', onPointerCancel);
|
|
164
|
+
element.removeEventListener('pointerdown', onPointerDown);
|
|
165
|
+
element.removeEventListener('pointermove', onPointerMove);
|
|
166
|
+
element.removeEventListener('pointerup', onPointerUp);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
export function attachRelativePointerInput(manager, element, options) {
|
|
170
|
+
const preventDefault = options?.preventDefault ?? true;
|
|
171
|
+
const target = element.ownerDocument;
|
|
172
|
+
const handler = (e) => {
|
|
173
|
+
if (!manager.enabled)
|
|
174
|
+
return;
|
|
175
|
+
const me = e;
|
|
176
|
+
if (preventDefault)
|
|
177
|
+
me.preventDefault();
|
|
178
|
+
setInputPointerData(_pointerData, me, me.movementX, me.movementY);
|
|
179
|
+
emitSignal(manager.onPointerMoveRelative, _pointerData);
|
|
180
|
+
};
|
|
181
|
+
target.addEventListener('mousemove', handler);
|
|
182
|
+
setInputBinding(manager, element, kRelativePointerInput, () => target.removeEventListener('mousemove', handler));
|
|
183
|
+
}
|
|
184
|
+
export function attachTextInput(manager, element, options) {
|
|
185
|
+
const onBeforeInput = (e) => {
|
|
186
|
+
if (!manager.enabled)
|
|
187
|
+
return;
|
|
188
|
+
const ie = e;
|
|
189
|
+
const text = ie.data ?? '';
|
|
190
|
+
_textData.isComposing = ie.isComposing;
|
|
191
|
+
_textData.text = text;
|
|
192
|
+
emitSignal(manager.onTextInput, _textData);
|
|
193
|
+
};
|
|
194
|
+
const onCompositionUpdate = (e) => {
|
|
195
|
+
if (!manager.enabled)
|
|
196
|
+
return;
|
|
197
|
+
const ce = e;
|
|
198
|
+
const text = ce.data ?? '';
|
|
199
|
+
_textData.isComposing = true;
|
|
200
|
+
_textData.text = text;
|
|
201
|
+
emitSignal(manager.onTextEdit, _textData);
|
|
202
|
+
};
|
|
203
|
+
element.addEventListener('beforeinput', onBeforeInput);
|
|
204
|
+
element.addEventListener('compositionupdate', onCompositionUpdate);
|
|
205
|
+
setInputBinding(manager, element, kTextInput, () => {
|
|
206
|
+
element.removeEventListener('beforeinput', onBeforeInput);
|
|
207
|
+
element.removeEventListener('compositionupdate', onCompositionUpdate);
|
|
208
|
+
});
|
|
209
|
+
// Suppress unused-options warning; options accepted for API symmetry.
|
|
210
|
+
void options;
|
|
211
|
+
}
|
|
212
|
+
export function attachWheelInput(manager, element, options) {
|
|
213
|
+
const preventDefault = options?.preventDefault ?? true;
|
|
214
|
+
const handler = (e) => {
|
|
215
|
+
if (!manager.enabled)
|
|
216
|
+
return;
|
|
217
|
+
const we = e;
|
|
218
|
+
if (preventDefault)
|
|
219
|
+
we.preventDefault();
|
|
220
|
+
setInputPointerData(_pointerData, we, we.deltaX, we.deltaY);
|
|
221
|
+
_pointerData.wheelMode = getMouseWheelModeFromDomWheelEvent(we);
|
|
222
|
+
emitSignal(manager.onWheel, _pointerData);
|
|
223
|
+
};
|
|
224
|
+
element.addEventListener('wheel', handler, { passive: !preventDefault });
|
|
225
|
+
setInputBinding(manager, element, kWheelInput, () => element.removeEventListener('wheel', handler));
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Subscribes `state` to all signals on `manager` to maintain a live held-state snapshot.
|
|
229
|
+
* Also tracks per-frame edge sets (`justPressedKeys`, `justReleasedKeys`,
|
|
230
|
+
* `justPressedGamepadButtons`, `justReleasedGamepadButtons`) that accumulate
|
|
231
|
+
* until `endInputStateFrame` is called.
|
|
232
|
+
* Returns a disposer that disconnects the subscriptions.
|
|
233
|
+
*/
|
|
234
|
+
export function connectInputStateToInputManager(state, manager) {
|
|
235
|
+
const onKeyDown = (data) => {
|
|
236
|
+
state.keysDown.add(data.keyCode);
|
|
237
|
+
state.justPressedKeys.add(data.keyCode);
|
|
238
|
+
state.justReleasedKeys.delete(data.keyCode);
|
|
239
|
+
};
|
|
240
|
+
const onKeyUp = (data) => {
|
|
241
|
+
state.keysDown.delete(data.keyCode);
|
|
242
|
+
state.justReleasedKeys.add(data.keyCode);
|
|
243
|
+
state.justPressedKeys.delete(data.keyCode);
|
|
244
|
+
};
|
|
245
|
+
const onPointerDown = (data) => {
|
|
246
|
+
const prev = state.pointerButtonsDown.get(data.pointerId) ?? 0;
|
|
247
|
+
state.pointerButtonsDown.set(data.pointerId, prev | (1 << data.button));
|
|
248
|
+
};
|
|
249
|
+
const onPointerUp = (data) => {
|
|
250
|
+
const prev = state.pointerButtonsDown.get(data.pointerId) ?? 0;
|
|
251
|
+
const next = prev & ~(1 << data.button);
|
|
252
|
+
if (next === 0) {
|
|
253
|
+
state.pointerButtonsDown.delete(data.pointerId);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
state.pointerButtonsDown.set(data.pointerId, next);
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
const onPointerCancel = (data) => {
|
|
260
|
+
state.pointerButtonsDown.delete(data.pointerId);
|
|
261
|
+
};
|
|
262
|
+
const onGamepadButtonDown = (data) => {
|
|
263
|
+
const key = data.gamepad * MAX_GAMEPAD_BUTTONS + data.button;
|
|
264
|
+
state.gamepadButtonsDown.add(key);
|
|
265
|
+
state.justPressedGamepadButtons.add(key);
|
|
266
|
+
state.justReleasedGamepadButtons.delete(key);
|
|
267
|
+
};
|
|
268
|
+
const onGamepadButtonUp = (data) => {
|
|
269
|
+
const key = data.gamepad * MAX_GAMEPAD_BUTTONS + data.button;
|
|
270
|
+
state.gamepadButtonsDown.delete(key);
|
|
271
|
+
state.justReleasedGamepadButtons.add(key);
|
|
272
|
+
state.justPressedGamepadButtons.delete(key);
|
|
273
|
+
};
|
|
274
|
+
const onGamepadAxisMove = (data) => {
|
|
275
|
+
state.axisValues.set(data.gamepad * MAX_GAMEPAD_AXES + data.axis, data.value);
|
|
276
|
+
};
|
|
277
|
+
const onGamepadConnect = (data) => {
|
|
278
|
+
// Clear stale state for a freshly-connected pad.
|
|
279
|
+
for (let b = 0; b < MAX_GAMEPAD_BUTTONS; b++) {
|
|
280
|
+
const key = data.gamepad * MAX_GAMEPAD_BUTTONS + b;
|
|
281
|
+
state.gamepadButtonsDown.delete(key);
|
|
282
|
+
state.justPressedGamepadButtons.delete(key);
|
|
283
|
+
state.justReleasedGamepadButtons.delete(key);
|
|
284
|
+
}
|
|
285
|
+
for (let a = 0; a < MAX_GAMEPAD_AXES; a++) {
|
|
286
|
+
state.axisValues.delete(data.gamepad * MAX_GAMEPAD_AXES + a);
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
const onGamepadDisconnect = (data) => {
|
|
290
|
+
for (let b = 0; b < MAX_GAMEPAD_BUTTONS; b++) {
|
|
291
|
+
const key = data.gamepad * MAX_GAMEPAD_BUTTONS + b;
|
|
292
|
+
state.gamepadButtonsDown.delete(key);
|
|
293
|
+
state.justPressedGamepadButtons.delete(key);
|
|
294
|
+
state.justReleasedGamepadButtons.delete(key);
|
|
295
|
+
}
|
|
296
|
+
for (let a = 0; a < MAX_GAMEPAD_AXES; a++) {
|
|
297
|
+
state.axisValues.delete(data.gamepad * MAX_GAMEPAD_AXES + a);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
connectSignal(manager.onKeyDown, onKeyDown);
|
|
301
|
+
connectSignal(manager.onKeyUp, onKeyUp);
|
|
302
|
+
connectSignal(manager.onPointerDown, onPointerDown);
|
|
303
|
+
connectSignal(manager.onPointerUp, onPointerUp);
|
|
304
|
+
connectSignal(manager.onPointerCancel, onPointerCancel);
|
|
305
|
+
connectSignal(manager.onGamepadButtonDown, onGamepadButtonDown);
|
|
306
|
+
connectSignal(manager.onGamepadButtonUp, onGamepadButtonUp);
|
|
307
|
+
connectSignal(manager.onGamepadAxisMove, onGamepadAxisMove);
|
|
308
|
+
connectSignal(manager.onGamepadConnect, onGamepadConnect);
|
|
309
|
+
connectSignal(manager.onGamepadDisconnect, onGamepadDisconnect);
|
|
310
|
+
return () => {
|
|
311
|
+
disconnectSignal(manager.onKeyDown, onKeyDown);
|
|
312
|
+
disconnectSignal(manager.onKeyUp, onKeyUp);
|
|
313
|
+
disconnectSignal(manager.onPointerDown, onPointerDown);
|
|
314
|
+
disconnectSignal(manager.onPointerUp, onPointerUp);
|
|
315
|
+
disconnectSignal(manager.onPointerCancel, onPointerCancel);
|
|
316
|
+
disconnectSignal(manager.onGamepadButtonDown, onGamepadButtonDown);
|
|
317
|
+
disconnectSignal(manager.onGamepadButtonUp, onGamepadButtonUp);
|
|
318
|
+
disconnectSignal(manager.onGamepadAxisMove, onGamepadAxisMove);
|
|
319
|
+
disconnectSignal(manager.onGamepadConnect, onGamepadConnect);
|
|
320
|
+
disconnectSignal(manager.onGamepadDisconnect, onGamepadDisconnect);
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Creates a key-repeat timer for non-DOM sources (gamepad d-pad buttons,
|
|
325
|
+
* virtual on-screen keys, native backends) that do not generate their own
|
|
326
|
+
* auto-repeat events.
|
|
327
|
+
*
|
|
328
|
+
* Call `start(callback)` when a "key" is pressed. The `callback` is invoked
|
|
329
|
+
* immediately on press, then again after `options.delay` ms, then every
|
|
330
|
+
* `options.interval` ms until `stop()` is called.
|
|
331
|
+
*
|
|
332
|
+
* Returns a handle with `start(callback)` and `stop()` methods.
|
|
333
|
+
* The handle may be reused across multiple press/release cycles.
|
|
334
|
+
*
|
|
335
|
+
* ```ts
|
|
336
|
+
* const timer = createInputKeyRepeatTimer({ delay: 500, interval: 33 });
|
|
337
|
+
* // on press:
|
|
338
|
+
* timer.start(() => emitSignal(manager.onKeyDown, dpadData));
|
|
339
|
+
* // on release:
|
|
340
|
+
* timer.stop();
|
|
341
|
+
* ```
|
|
342
|
+
*/
|
|
343
|
+
export function createInputKeyRepeatTimer(options) {
|
|
344
|
+
let delayId = 0;
|
|
345
|
+
let intervalId = 0;
|
|
346
|
+
const stop = () => {
|
|
347
|
+
clearTimeout(delayId);
|
|
348
|
+
clearInterval(intervalId);
|
|
349
|
+
delayId = 0;
|
|
350
|
+
intervalId = 0;
|
|
351
|
+
};
|
|
352
|
+
const start = (callback) => {
|
|
353
|
+
stop();
|
|
354
|
+
callback();
|
|
355
|
+
delayId = setTimeout(() => {
|
|
356
|
+
callback();
|
|
357
|
+
intervalId = setInterval(callback, options.interval);
|
|
358
|
+
}, options.delay);
|
|
359
|
+
};
|
|
360
|
+
return { start, stop };
|
|
361
|
+
}
|
|
362
|
+
export function createInputManager() {
|
|
363
|
+
return {
|
|
364
|
+
...createInputSignals(),
|
|
365
|
+
enabled: true,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
export function createInputSignals() {
|
|
369
|
+
return {
|
|
370
|
+
onGamepadAxisMove: createSignal(),
|
|
371
|
+
onGamepadButtonDown: createSignal(),
|
|
372
|
+
onGamepadButtonUp: createSignal(),
|
|
373
|
+
onGamepadConnect: createSignal(),
|
|
374
|
+
onGamepadDisconnect: createSignal(),
|
|
375
|
+
onKeyDown: createSignal(),
|
|
376
|
+
onKeyUp: createSignal(),
|
|
377
|
+
onPointerCancel: createSignal(),
|
|
378
|
+
onPointerDown: createSignal(),
|
|
379
|
+
onPointerMove: createSignal(),
|
|
380
|
+
onPointerMoveRelative: createSignal(),
|
|
381
|
+
onPointerUp: createSignal(),
|
|
382
|
+
onTextEdit: createSignal(),
|
|
383
|
+
onTextInput: createSignal(),
|
|
384
|
+
onWheel: createSignal(),
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Creates a fresh `InputState` with empty held-state maps/sets and empty
|
|
389
|
+
* frame-edge sets. Connect it to an `InputManager` via
|
|
390
|
+
* `connectInputStateToInputManager`, and call `endInputStateFrame` once per
|
|
391
|
+
* logical frame to roll the edge sets.
|
|
392
|
+
*/
|
|
393
|
+
export function createInputState() {
|
|
394
|
+
return {
|
|
395
|
+
axisValues: new Map(),
|
|
396
|
+
gamepadButtonsDown: new Set(),
|
|
397
|
+
justPressedGamepadButtons: new Set(),
|
|
398
|
+
justPressedKeys: new Set(),
|
|
399
|
+
justReleasedGamepadButtons: new Set(),
|
|
400
|
+
justReleasedKeys: new Set(),
|
|
401
|
+
keysDown: new Set(),
|
|
402
|
+
pointerButtonsDown: new Map(),
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
export function detachGamepadInput(manager, target) {
|
|
406
|
+
clearInputBinding(manager, target, kGamepadInput);
|
|
407
|
+
}
|
|
408
|
+
export function detachKeyboardInput(manager, target) {
|
|
409
|
+
clearInputBinding(manager, target, kKeyboardInput);
|
|
410
|
+
}
|
|
411
|
+
export function detachPointerInput(manager, element) {
|
|
412
|
+
clearInputBinding(manager, element, kPointerInput);
|
|
413
|
+
}
|
|
414
|
+
export function detachRelativePointerInput(manager, element) {
|
|
415
|
+
clearInputBinding(manager, element, kRelativePointerInput);
|
|
416
|
+
}
|
|
417
|
+
export function detachTextInput(manager, element) {
|
|
418
|
+
clearInputBinding(manager, element, kTextInput);
|
|
419
|
+
}
|
|
420
|
+
export function detachWheelInput(manager, element) {
|
|
421
|
+
clearInputBinding(manager, element, kWheelInput);
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Rolls the per-frame edge sets on `state`, clearing `justPressedKeys`,
|
|
425
|
+
* `justReleasedKeys`, `justPressedGamepadButtons`, and
|
|
426
|
+
* `justReleasedGamepadButtons`. Call this once at the end of each logical
|
|
427
|
+
* frame (or input-poll cycle) to prepare the edge sets for the next frame.
|
|
428
|
+
*/
|
|
429
|
+
export function endInputStateFrame(state) {
|
|
430
|
+
state.justPressedKeys.clear();
|
|
431
|
+
state.justReleasedKeys.clear();
|
|
432
|
+
state.justPressedGamepadButtons.clear();
|
|
433
|
+
state.justReleasedGamepadButtons.clear();
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Exits pointer lock, returning the pointer to normal movement.
|
|
437
|
+
* No-op if pointer lock is not currently active.
|
|
438
|
+
*/
|
|
439
|
+
export function exitInputPointerLock() {
|
|
440
|
+
if (document.exitPointerLock) {
|
|
441
|
+
document.exitPointerLock();
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Returns coalesced pointer event data for a `pointermove` event, iterating
|
|
446
|
+
* over the high-frequency intermediate positions captured since the last
|
|
447
|
+
* delivered event. Falls back to a single entry with the event itself when
|
|
448
|
+
* `getCoalescedEvents` is unavailable (e.g. in jsdom).
|
|
449
|
+
*
|
|
450
|
+
* The callback receives each coalesced `InputPointerData` in order. The
|
|
451
|
+
* payload object is reused across calls — do not retain a reference to it.
|
|
452
|
+
*/
|
|
453
|
+
export function getCoalescedInputPointerEvents(event, callback) {
|
|
454
|
+
const coalesced = typeof event.getCoalescedEvents === 'function' ? event.getCoalescedEvents() : null;
|
|
455
|
+
if (coalesced !== null && coalesced.length > 0) {
|
|
456
|
+
for (const e of coalesced) {
|
|
457
|
+
setInputPointerData(_pointerData, e, 0, 0);
|
|
458
|
+
callback(_pointerData);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
setInputPointerData(_pointerData, event, 0, 0);
|
|
463
|
+
callback(_pointerData);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Returns the semantic name string (a `GamepadAxisKind`) for `index` in the
|
|
468
|
+
* standard gamepad mapping, or `null` if `mapping` is not `'standard'` or
|
|
469
|
+
* `index` is out of the standard range.
|
|
470
|
+
*/
|
|
471
|
+
export function getGamepadAxisName(mapping, index) {
|
|
472
|
+
if (mapping !== 'standard')
|
|
473
|
+
return null;
|
|
474
|
+
return _standardAxisNames[index] ?? null;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Returns the semantic name string (a `GamepadButtonKind`) for `index` in the
|
|
478
|
+
* standard gamepad mapping, or `null` if `mapping` is not `'standard'` or
|
|
479
|
+
* `index` is out of the standard range.
|
|
480
|
+
*/
|
|
481
|
+
export function getGamepadButtonName(mapping, index) {
|
|
482
|
+
if (mapping !== 'standard')
|
|
483
|
+
return null;
|
|
484
|
+
return _standardButtonNames[index] ?? null;
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Returns the current value of a gamepad axis from `state`, or `0` if not recorded.
|
|
488
|
+
* `gamepad` is the gamepad index; `axis` is the axis index.
|
|
489
|
+
*/
|
|
490
|
+
export function getInputGamepadAxis(state, gamepad, axis) {
|
|
491
|
+
return state.axisValues.get(gamepad * MAX_GAMEPAD_AXES + axis) ?? 0;
|
|
492
|
+
}
|
|
493
|
+
export function getKeyCodeFromDomKeyboardEvent(event) {
|
|
494
|
+
const code = getKeyCodeFromDomKeyboardCode(event.code, event.location);
|
|
495
|
+
if (code !== KeyCode.UNKNOWN)
|
|
496
|
+
return code;
|
|
497
|
+
if (event.key.length === 1)
|
|
498
|
+
return event.key.toLowerCase().charCodeAt(0);
|
|
499
|
+
return keyCodesByKey[event.key] ?? KeyCode.UNKNOWN;
|
|
500
|
+
}
|
|
501
|
+
export function getKeyModifierFromDomKeyboardEvent(event) {
|
|
502
|
+
let modifier = KeyModifier.NONE;
|
|
503
|
+
if (event.altKey)
|
|
504
|
+
modifier |= event.location === KeyboardEvent.DOM_KEY_LOCATION_RIGHT ? KeyModifier.RIGHT_ALT : KeyModifier.LEFT_ALT;
|
|
505
|
+
if (event.ctrlKey)
|
|
506
|
+
modifier |=
|
|
507
|
+
event.location === KeyboardEvent.DOM_KEY_LOCATION_RIGHT ? KeyModifier.RIGHT_CTRL : KeyModifier.LEFT_CTRL;
|
|
508
|
+
if (event.metaKey)
|
|
509
|
+
modifier |=
|
|
510
|
+
event.location === KeyboardEvent.DOM_KEY_LOCATION_RIGHT ? KeyModifier.RIGHT_META : KeyModifier.LEFT_META;
|
|
511
|
+
if (event.shiftKey)
|
|
512
|
+
modifier |=
|
|
513
|
+
event.location === KeyboardEvent.DOM_KEY_LOCATION_RIGHT ? KeyModifier.RIGHT_SHIFT : KeyModifier.LEFT_SHIFT;
|
|
514
|
+
if (event.getModifierState?.('CapsLock') === true)
|
|
515
|
+
modifier |= KeyModifier.CAPS_LOCK;
|
|
516
|
+
if (event.getModifierState?.('NumLock') === true)
|
|
517
|
+
modifier |= KeyModifier.NUM_LOCK;
|
|
518
|
+
return modifier;
|
|
519
|
+
}
|
|
520
|
+
export function getMouseWheelModeFromDomWheelEvent(event) {
|
|
521
|
+
if (event.deltaMode === WheelEvent.DOM_DELTA_PIXEL)
|
|
522
|
+
return 'pixels';
|
|
523
|
+
if (event.deltaMode === WheelEvent.DOM_DELTA_LINE)
|
|
524
|
+
return 'lines';
|
|
525
|
+
if (event.deltaMode === WheelEvent.DOM_DELTA_PAGE)
|
|
526
|
+
return 'pages';
|
|
527
|
+
return 'unknown';
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Returns `true` if pointer lock is currently active on any element.
|
|
531
|
+
*/
|
|
532
|
+
export function hasInputPointerLock() {
|
|
533
|
+
return document.pointerLockElement !== null;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Returns `true` if the given gamepad button is currently held.
|
|
537
|
+
* `gamepad` is the gamepad index; `button` is the button index.
|
|
538
|
+
*/
|
|
539
|
+
export function isInputGamepadButtonDown(state, gamepad, button) {
|
|
540
|
+
return state.gamepadButtonsDown.has(gamepad * MAX_GAMEPAD_BUTTONS + button);
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Returns `true` if the given `keyCode` (from `KeyCode`) is currently held.
|
|
544
|
+
*/
|
|
545
|
+
export function isInputKeyDown(state, keyCode) {
|
|
546
|
+
return state.keysDown.has(keyCode);
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Returns `true` if the given pointer button is currently held for the given `pointerId`.
|
|
550
|
+
* `button` corresponds to `MouseEvent.button` (0 = primary, 1 = middle, 2 = secondary, …).
|
|
551
|
+
*/
|
|
552
|
+
export function isInputPointerButtonDown(state, pointerId, button) {
|
|
553
|
+
return ((state.pointerButtonsDown.get(pointerId) ?? 0) & (1 << button)) !== 0;
|
|
554
|
+
}
|
|
555
|
+
export function pollGamepadInput(manager) {
|
|
556
|
+
if (!manager.enabled || typeof navigator.getGamepads !== 'function')
|
|
557
|
+
return;
|
|
558
|
+
const now = performance.now();
|
|
559
|
+
const prev = getOrCreateGamepadPollState(manager);
|
|
560
|
+
const gamepads = navigator.getGamepads();
|
|
561
|
+
for (const pad of gamepads) {
|
|
562
|
+
if (pad === null)
|
|
563
|
+
continue;
|
|
564
|
+
const prevAxes = prev.axes.get(pad.index) ?? [];
|
|
565
|
+
const prevButtons = prev.buttons.get(pad.index) ?? [];
|
|
566
|
+
for (let i = 0; i < pad.axes.length; i++) {
|
|
567
|
+
const value = pad.axes[i];
|
|
568
|
+
if (value !== prevAxes[i]) {
|
|
569
|
+
prevAxes[i] = value;
|
|
570
|
+
_axisData.axis = i;
|
|
571
|
+
_axisData.gamepad = pad.index;
|
|
572
|
+
_axisData.timeStamp = now;
|
|
573
|
+
_axisData.value = value;
|
|
574
|
+
emitSignal(manager.onGamepadAxisMove, _axisData);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
for (let i = 0; i < pad.buttons.length; i++) {
|
|
578
|
+
const btn = pad.buttons[i];
|
|
579
|
+
const wasPressed = prevButtons[i] ?? false;
|
|
580
|
+
if (btn.pressed !== wasPressed) {
|
|
581
|
+
prevButtons[i] = btn.pressed;
|
|
582
|
+
_buttonData.button = i;
|
|
583
|
+
_buttonData.gamepad = pad.index;
|
|
584
|
+
_buttonData.timeStamp = now;
|
|
585
|
+
_buttonData.value = btn.value;
|
|
586
|
+
if (btn.pressed) {
|
|
587
|
+
emitSignal(manager.onGamepadButtonDown, _buttonData);
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
emitSignal(manager.onGamepadButtonUp, _buttonData);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
prev.axes.set(pad.index, prevAxes);
|
|
595
|
+
prev.buttons.set(pad.index, prevButtons);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Releases pointer capture for `pointerId` from `element`, allowing pointer
|
|
600
|
+
* events to fire on the element under the pointer again.
|
|
601
|
+
* No-op if `element` does not have capture for this pointer.
|
|
602
|
+
*/
|
|
603
|
+
export function releaseInputPointerCapture(element, pointerId) {
|
|
604
|
+
try {
|
|
605
|
+
element.releasePointerCapture(pointerId);
|
|
606
|
+
}
|
|
607
|
+
catch {
|
|
608
|
+
// Ignore — the pointer may have already been released.
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Requests pointer lock on `element`. Returns a `Promise<boolean>` that
|
|
613
|
+
* resolves to `true` when lock is granted or `false` if the request is
|
|
614
|
+
* rejected (e.g. outside a user gesture, or the browser denies it).
|
|
615
|
+
*/
|
|
616
|
+
export function requestInputPointerLock(element) {
|
|
617
|
+
try {
|
|
618
|
+
const result = element.requestPointerLock();
|
|
619
|
+
if (result instanceof Promise) {
|
|
620
|
+
return result.then(() => true, () => false);
|
|
621
|
+
}
|
|
622
|
+
return Promise.resolve(true);
|
|
623
|
+
}
|
|
624
|
+
catch {
|
|
625
|
+
return Promise.resolve(false);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Explicitly captures all pointer events for `pointerId` to `element`,
|
|
630
|
+
* regardless of where the pointer moves. Useful for drag operations.
|
|
631
|
+
* Automatically released on `pointerup` or `pointercancel` per the spec.
|
|
632
|
+
*/
|
|
633
|
+
export function setInputPointerCapture(element, pointerId) {
|
|
634
|
+
element.setPointerCapture(pointerId);
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Returns `true` if the gamepad button at `gamepad`/`button` was pressed
|
|
638
|
+
* this frame (i.e. transitioned from up → down since the last
|
|
639
|
+
* `endInputStateFrame` call).
|
|
640
|
+
*/
|
|
641
|
+
export function wasInputGamepadButtonPressed(state, gamepad, button) {
|
|
642
|
+
return state.justPressedGamepadButtons.has(gamepad * MAX_GAMEPAD_BUTTONS + button);
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Returns `true` if the gamepad button at `gamepad`/`button` was released
|
|
646
|
+
* this frame (i.e. transitioned from down → up since the last
|
|
647
|
+
* `endInputStateFrame` call).
|
|
648
|
+
*/
|
|
649
|
+
export function wasInputGamepadButtonReleased(state, gamepad, button) {
|
|
650
|
+
return state.justReleasedGamepadButtons.has(gamepad * MAX_GAMEPAD_BUTTONS + button);
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Returns `true` if the key with `keyCode` was pressed this frame (i.e.
|
|
654
|
+
* transitioned from up → down since the last `endInputStateFrame` call).
|
|
655
|
+
*/
|
|
656
|
+
export function wasInputKeyPressed(state, keyCode) {
|
|
657
|
+
return state.justPressedKeys.has(keyCode);
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Returns `true` if the key with `keyCode` was released this frame (i.e.
|
|
661
|
+
* transitioned from down → up since the last `endInputStateFrame` call).
|
|
662
|
+
*/
|
|
663
|
+
export function wasInputKeyReleased(state, keyCode) {
|
|
664
|
+
return state.justReleasedKeys.has(keyCode);
|
|
665
|
+
}
|
|
666
|
+
function getKeyCodeFromDomKeyboardCode(code, location) {
|
|
667
|
+
if (location === KeyboardEvent.DOM_KEY_LOCATION_NUMPAD && code in numpadKeyCodesByCode) {
|
|
668
|
+
return numpadKeyCodesByCode[code];
|
|
669
|
+
}
|
|
670
|
+
return keyCodesByCode[code] ?? KeyCode.UNKNOWN;
|
|
671
|
+
}
|
|
672
|
+
function getPointerTypeFromDomPointerEvent(event) {
|
|
673
|
+
return event.pointerType === 'mouse' || event.pointerType === 'pen' || event.pointerType === 'touch'
|
|
674
|
+
? event.pointerType
|
|
675
|
+
: 'unknown';
|
|
676
|
+
}
|
|
677
|
+
function setInputKeyboardData(out, event) {
|
|
678
|
+
const modifier = getKeyModifierFromDomKeyboardEvent(event);
|
|
679
|
+
out.altKey = event.altKey;
|
|
680
|
+
out.capsLock = (modifier & KeyModifier.CAPS_LOCK) !== 0;
|
|
681
|
+
out.code = event.code;
|
|
682
|
+
out.ctrlKey = event.ctrlKey;
|
|
683
|
+
out.key = event.key;
|
|
684
|
+
out.keyCode = getKeyCodeFromDomKeyboardEvent(event);
|
|
685
|
+
out.location = event.location;
|
|
686
|
+
out.metaKey = event.metaKey;
|
|
687
|
+
out.modifier = modifier;
|
|
688
|
+
out.numLock = (modifier & KeyModifier.NUM_LOCK) !== 0;
|
|
689
|
+
out.repeat = event.repeat;
|
|
690
|
+
out.shiftKey = event.shiftKey;
|
|
691
|
+
out.timeStamp = event.timeStamp;
|
|
692
|
+
}
|
|
693
|
+
function setInputPointerData(out, event, deltaX, deltaY) {
|
|
694
|
+
out.altKey = event.altKey;
|
|
695
|
+
out.button = event.button;
|
|
696
|
+
out.buttons = event.buttons;
|
|
697
|
+
out.ctrlKey = event.ctrlKey;
|
|
698
|
+
out.deltaX = deltaX;
|
|
699
|
+
out.deltaY = deltaY;
|
|
700
|
+
out.height = 'height' in event ? event.height : 1;
|
|
701
|
+
out.isPrimary = 'isPrimary' in event ? event.isPrimary : true;
|
|
702
|
+
out.metaKey = event.metaKey;
|
|
703
|
+
out.pointerId = 'pointerId' in event ? event.pointerId : 0;
|
|
704
|
+
out.pointerType = 'pointerType' in event ? getPointerTypeFromDomPointerEvent(event) : 'mouse';
|
|
705
|
+
out.pressure = 'pressure' in event ? event.pressure : 0;
|
|
706
|
+
out.shiftKey = event.shiftKey;
|
|
707
|
+
out.tiltX = 'tiltX' in event ? event.tiltX : 0;
|
|
708
|
+
out.tiltY = 'tiltY' in event ? event.tiltY : 0;
|
|
709
|
+
out.timeStamp = event.timeStamp;
|
|
710
|
+
out.twist = 'twist' in event ? event.twist : 0;
|
|
711
|
+
out.wheelMode = 'unknown';
|
|
712
|
+
out.width = 'width' in event ? event.width : 1;
|
|
713
|
+
out.x = event.clientX;
|
|
714
|
+
out.y = event.clientY;
|
|
715
|
+
}
|
|
716
|
+
// Standard gamepad mapping: button index → GamepadButtonKind string.
|
|
717
|
+
const _standardButtonNames = [
|
|
718
|
+
GamepadButtonKindValues.BUTTON_SOUTH, // 0
|
|
719
|
+
GamepadButtonKindValues.BUTTON_EAST, // 1
|
|
720
|
+
GamepadButtonKindValues.BUTTON_WEST, // 2
|
|
721
|
+
GamepadButtonKindValues.BUTTON_NORTH, // 3
|
|
722
|
+
GamepadButtonKindValues.SHOULDER_LEFT, // 4
|
|
723
|
+
GamepadButtonKindValues.SHOULDER_RIGHT, // 5
|
|
724
|
+
GamepadButtonKindValues.TRIGGER_LEFT, // 6
|
|
725
|
+
GamepadButtonKindValues.TRIGGER_RIGHT, // 7
|
|
726
|
+
GamepadButtonKindValues.SELECT, // 8
|
|
727
|
+
GamepadButtonKindValues.START, // 9
|
|
728
|
+
GamepadButtonKindValues.STICK_LEFT, // 10
|
|
729
|
+
GamepadButtonKindValues.STICK_RIGHT, // 11
|
|
730
|
+
GamepadButtonKindValues.DPAD_UP, // 12
|
|
731
|
+
GamepadButtonKindValues.DPAD_DOWN, // 13
|
|
732
|
+
GamepadButtonKindValues.DPAD_LEFT, // 14
|
|
733
|
+
GamepadButtonKindValues.DPAD_RIGHT, // 15
|
|
734
|
+
GamepadButtonKindValues.HOME, // 16
|
|
735
|
+
GamepadButtonKindValues.TOUCHPAD, // 17
|
|
736
|
+
];
|
|
737
|
+
// Standard gamepad mapping: axis index → GamepadAxisKind string.
|
|
738
|
+
const _standardAxisNames = [
|
|
739
|
+
GamepadAxisKindValues.STICK_LEFT_X, // 0
|
|
740
|
+
GamepadAxisKindValues.STICK_LEFT_Y, // 1
|
|
741
|
+
GamepadAxisKindValues.STICK_RIGHT_X, // 2
|
|
742
|
+
GamepadAxisKindValues.STICK_RIGHT_Y, // 3
|
|
743
|
+
];
|
|
744
|
+
// DOM KeyboardEvent.code → KeyCode. Exhaustive for all keys in the KeyCode enum
|
|
745
|
+
// that have a direct W3C code string.
|
|
746
|
+
const keyCodesByCode = {
|
|
747
|
+
Again: KeyCode.AGAIN,
|
|
748
|
+
AltLeft: KeyCode.LEFT_ALT,
|
|
749
|
+
AltRight: KeyCode.RIGHT_ALT,
|
|
750
|
+
ArrowDown: KeyCode.DOWN,
|
|
751
|
+
ArrowLeft: KeyCode.LEFT,
|
|
752
|
+
ArrowRight: KeyCode.RIGHT,
|
|
753
|
+
ArrowUp: KeyCode.UP,
|
|
754
|
+
AudioVolumeDown: KeyCode.AUDIO_MUTE, // browser-specific alias
|
|
755
|
+
Backspace: KeyCode.BACKSPACE,
|
|
756
|
+
BrowserBack: KeyCode.APP_CONTROL_BACK,
|
|
757
|
+
BrowserBookmarks: KeyCode.APP_CONTROL_BOOKMARKS,
|
|
758
|
+
BrowserForward: KeyCode.APP_CONTROL_FORWARD,
|
|
759
|
+
BrowserHome: KeyCode.APP_CONTROL_HOME,
|
|
760
|
+
BrowserRefresh: KeyCode.APP_CONTROL_REFRESH,
|
|
761
|
+
BrowserSearch: KeyCode.APP_CONTROL_SEARCH,
|
|
762
|
+
BrowserStop: KeyCode.APP_CONTROL_STOP,
|
|
763
|
+
CapsLock: KeyCode.CAPS_LOCK,
|
|
764
|
+
ContextMenu: KeyCode.APPLICATION,
|
|
765
|
+
ControlLeft: KeyCode.LEFT_CTRL,
|
|
766
|
+
ControlRight: KeyCode.RIGHT_CTRL,
|
|
767
|
+
Convert: KeyCode.UNKNOWN, // IME convert (Japanese) — no direct SDL equiv
|
|
768
|
+
Copy: KeyCode.COPY,
|
|
769
|
+
Cut: KeyCode.CUT,
|
|
770
|
+
Delete: KeyCode.DELETE,
|
|
771
|
+
Eject: KeyCode.EJECT,
|
|
772
|
+
End: KeyCode.END,
|
|
773
|
+
Enter: KeyCode.RETURN,
|
|
774
|
+
Escape: KeyCode.ESCAPE,
|
|
775
|
+
F1: KeyCode.F1,
|
|
776
|
+
F2: KeyCode.F2,
|
|
777
|
+
F3: KeyCode.F3,
|
|
778
|
+
F4: KeyCode.F4,
|
|
779
|
+
F5: KeyCode.F5,
|
|
780
|
+
F6: KeyCode.F6,
|
|
781
|
+
F7: KeyCode.F7,
|
|
782
|
+
F8: KeyCode.F8,
|
|
783
|
+
F9: KeyCode.F9,
|
|
784
|
+
F10: KeyCode.F10,
|
|
785
|
+
F11: KeyCode.F11,
|
|
786
|
+
F12: KeyCode.F12,
|
|
787
|
+
F13: KeyCode.F13,
|
|
788
|
+
F14: KeyCode.F14,
|
|
789
|
+
F15: KeyCode.F15,
|
|
790
|
+
F16: KeyCode.F16,
|
|
791
|
+
F17: KeyCode.F17,
|
|
792
|
+
F18: KeyCode.F18,
|
|
793
|
+
F19: KeyCode.F19,
|
|
794
|
+
F20: KeyCode.F20,
|
|
795
|
+
F21: KeyCode.F21,
|
|
796
|
+
F22: KeyCode.F22,
|
|
797
|
+
F23: KeyCode.F23,
|
|
798
|
+
F24: KeyCode.F24,
|
|
799
|
+
Find: KeyCode.FIND,
|
|
800
|
+
Help: KeyCode.HELP,
|
|
801
|
+
Home: KeyCode.HOME,
|
|
802
|
+
Insert: KeyCode.INSERT,
|
|
803
|
+
IntlBackslash: KeyCode.BACKSLASH,
|
|
804
|
+
LaunchApp1: KeyCode.COMPUTER,
|
|
805
|
+
LaunchApp2: KeyCode.CALCULATOR,
|
|
806
|
+
LaunchMail: KeyCode.MAIL,
|
|
807
|
+
LaunchMediaPlayer: KeyCode.MEDIA_SELECT,
|
|
808
|
+
MediaPlayPause: KeyCode.AUDIO_PLAY,
|
|
809
|
+
MediaStop: KeyCode.AUDIO_STOP,
|
|
810
|
+
MediaTrackNext: KeyCode.AUDIO_NEXT,
|
|
811
|
+
MediaTrackPrevious: KeyCode.AUDIO_PREVIOUS,
|
|
812
|
+
MetaLeft: KeyCode.LEFT_META,
|
|
813
|
+
MetaRight: KeyCode.RIGHT_META,
|
|
814
|
+
NonConvert: KeyCode.UNKNOWN, // IME non-convert — no direct SDL equiv
|
|
815
|
+
NumLock: KeyCode.NUM_LOCK,
|
|
816
|
+
PageDown: KeyCode.PAGE_DOWN,
|
|
817
|
+
PageUp: KeyCode.PAGE_UP,
|
|
818
|
+
Paste: KeyCode.PASTE,
|
|
819
|
+
Pause: KeyCode.PAUSE,
|
|
820
|
+
Power: KeyCode.POWER,
|
|
821
|
+
PrintScreen: KeyCode.PRINT_SCREEN,
|
|
822
|
+
ScrollLock: KeyCode.SCROLL_LOCK,
|
|
823
|
+
Select: KeyCode.SELECT,
|
|
824
|
+
ShiftLeft: KeyCode.LEFT_SHIFT,
|
|
825
|
+
ShiftRight: KeyCode.RIGHT_SHIFT,
|
|
826
|
+
Sleep: KeyCode.SLEEP,
|
|
827
|
+
Space: KeyCode.SPACE,
|
|
828
|
+
Tab: KeyCode.TAB,
|
|
829
|
+
Undo: KeyCode.UNDO,
|
|
830
|
+
VolumeDown: KeyCode.VOLUME_DOWN,
|
|
831
|
+
VolumeMute: KeyCode.AUDIO_MUTE,
|
|
832
|
+
VolumeUp: KeyCode.VOLUME_UP,
|
|
833
|
+
WakeUp: KeyCode.UNKNOWN, // no SDL equiv
|
|
834
|
+
WWW: KeyCode.WWW,
|
|
835
|
+
};
|
|
836
|
+
// DOM KeyboardEvent.key → KeyCode. Used as fallback when .code gives UNKNOWN.
|
|
837
|
+
const keyCodesByKey = {
|
|
838
|
+
// Navigation
|
|
839
|
+
Alt: KeyCode.LEFT_ALT,
|
|
840
|
+
ArrowDown: KeyCode.DOWN,
|
|
841
|
+
ArrowLeft: KeyCode.LEFT,
|
|
842
|
+
ArrowRight: KeyCode.RIGHT,
|
|
843
|
+
ArrowUp: KeyCode.UP,
|
|
844
|
+
Backspace: KeyCode.BACKSPACE,
|
|
845
|
+
CapsLock: KeyCode.CAPS_LOCK,
|
|
846
|
+
Control: KeyCode.LEFT_CTRL,
|
|
847
|
+
Delete: KeyCode.DELETE,
|
|
848
|
+
End: KeyCode.END,
|
|
849
|
+
Enter: KeyCode.RETURN,
|
|
850
|
+
Escape: KeyCode.ESCAPE,
|
|
851
|
+
Home: KeyCode.HOME,
|
|
852
|
+
Insert: KeyCode.INSERT,
|
|
853
|
+
Meta: KeyCode.LEFT_META,
|
|
854
|
+
NumLock: KeyCode.NUM_LOCK,
|
|
855
|
+
PageDown: KeyCode.PAGE_DOWN,
|
|
856
|
+
PageUp: KeyCode.PAGE_UP,
|
|
857
|
+
Pause: KeyCode.PAUSE,
|
|
858
|
+
PrintScreen: KeyCode.PRINT_SCREEN,
|
|
859
|
+
ScrollLock: KeyCode.SCROLL_LOCK,
|
|
860
|
+
Shift: KeyCode.LEFT_SHIFT,
|
|
861
|
+
Tab: KeyCode.TAB,
|
|
862
|
+
// Function keys
|
|
863
|
+
F1: KeyCode.F1,
|
|
864
|
+
F2: KeyCode.F2,
|
|
865
|
+
F3: KeyCode.F3,
|
|
866
|
+
F4: KeyCode.F4,
|
|
867
|
+
F5: KeyCode.F5,
|
|
868
|
+
F6: KeyCode.F6,
|
|
869
|
+
F7: KeyCode.F7,
|
|
870
|
+
F8: KeyCode.F8,
|
|
871
|
+
F9: KeyCode.F9,
|
|
872
|
+
F10: KeyCode.F10,
|
|
873
|
+
F11: KeyCode.F11,
|
|
874
|
+
F12: KeyCode.F12,
|
|
875
|
+
F13: KeyCode.F13,
|
|
876
|
+
F14: KeyCode.F14,
|
|
877
|
+
F15: KeyCode.F15,
|
|
878
|
+
F16: KeyCode.F16,
|
|
879
|
+
F17: KeyCode.F17,
|
|
880
|
+
F18: KeyCode.F18,
|
|
881
|
+
F19: KeyCode.F19,
|
|
882
|
+
F20: KeyCode.F20,
|
|
883
|
+
F21: KeyCode.F21,
|
|
884
|
+
F22: KeyCode.F22,
|
|
885
|
+
F23: KeyCode.F23,
|
|
886
|
+
F24: KeyCode.F24,
|
|
887
|
+
// Media keys
|
|
888
|
+
AudioVolumeDown: KeyCode.VOLUME_DOWN,
|
|
889
|
+
AudioVolumeMute: KeyCode.AUDIO_MUTE,
|
|
890
|
+
AudioVolumeUp: KeyCode.VOLUME_UP,
|
|
891
|
+
MediaPlayPause: KeyCode.AUDIO_PLAY,
|
|
892
|
+
MediaStop: KeyCode.AUDIO_STOP,
|
|
893
|
+
MediaTrackNext: KeyCode.AUDIO_NEXT,
|
|
894
|
+
MediaTrackPrevious: KeyCode.AUDIO_PREVIOUS,
|
|
895
|
+
// Browser keys
|
|
896
|
+
BrowserBack: KeyCode.APP_CONTROL_BACK,
|
|
897
|
+
BrowserBookmarks: KeyCode.APP_CONTROL_BOOKMARKS,
|
|
898
|
+
BrowserForward: KeyCode.APP_CONTROL_FORWARD,
|
|
899
|
+
BrowserHome: KeyCode.APP_CONTROL_HOME,
|
|
900
|
+
BrowserRefresh: KeyCode.APP_CONTROL_REFRESH,
|
|
901
|
+
BrowserSearch: KeyCode.APP_CONTROL_SEARCH,
|
|
902
|
+
BrowserStop: KeyCode.APP_CONTROL_STOP,
|
|
903
|
+
// Misc
|
|
904
|
+
ContextMenu: KeyCode.APPLICATION,
|
|
905
|
+
Copy: KeyCode.COPY,
|
|
906
|
+
Cut: KeyCode.CUT,
|
|
907
|
+
Find: KeyCode.FIND,
|
|
908
|
+
Help: KeyCode.HELP,
|
|
909
|
+
Paste: KeyCode.PASTE,
|
|
910
|
+
Select: KeyCode.SELECT,
|
|
911
|
+
Undo: KeyCode.UNDO,
|
|
912
|
+
};
|
|
913
|
+
const numpadKeyCodesByCode = {
|
|
914
|
+
Enter: KeyCode.NUMPAD_ENTER,
|
|
915
|
+
Numpad0: KeyCode.NUMPAD_0,
|
|
916
|
+
Numpad1: KeyCode.NUMPAD_1,
|
|
917
|
+
Numpad2: KeyCode.NUMPAD_2,
|
|
918
|
+
Numpad3: KeyCode.NUMPAD_3,
|
|
919
|
+
Numpad4: KeyCode.NUMPAD_4,
|
|
920
|
+
Numpad5: KeyCode.NUMPAD_5,
|
|
921
|
+
Numpad6: KeyCode.NUMPAD_6,
|
|
922
|
+
Numpad7: KeyCode.NUMPAD_7,
|
|
923
|
+
Numpad8: KeyCode.NUMPAD_8,
|
|
924
|
+
Numpad9: KeyCode.NUMPAD_9,
|
|
925
|
+
NumpadAdd: KeyCode.NUMPAD_PLUS,
|
|
926
|
+
NumpadBackspace: KeyCode.NUMPAD_BACKSPACE,
|
|
927
|
+
NumpadClear: KeyCode.NUMPAD_CLEAR,
|
|
928
|
+
NumpadClearEntry: KeyCode.NUMPAD_CLEAR_ENTRY,
|
|
929
|
+
NumpadComma: KeyCode.NUMPAD_COMMA,
|
|
930
|
+
NumpadDecimal: KeyCode.NUMPAD_PERIOD,
|
|
931
|
+
NumpadDivide: KeyCode.NUMPAD_DIVIDE,
|
|
932
|
+
NumpadEqual: KeyCode.NUMPAD_EQUALS,
|
|
933
|
+
NumpadHash: KeyCode.NUMPAD_HASH,
|
|
934
|
+
NumpadMemoryAdd: KeyCode.NUMPAD_MEM_ADD,
|
|
935
|
+
NumpadMemoryClear: KeyCode.NUMPAD_MEM_CLEAR,
|
|
936
|
+
NumpadMemoryRecall: KeyCode.NUMPAD_MEM_RECALL,
|
|
937
|
+
NumpadMemoryStore: KeyCode.NUMPAD_MEM_STORE,
|
|
938
|
+
NumpadMemorySubtract: KeyCode.NUMPAD_MEM_SUBTRACT,
|
|
939
|
+
NumpadMultiply: KeyCode.NUMPAD_MULTIPLY,
|
|
940
|
+
NumpadParenLeft: KeyCode.NUMPAD_LEFT_PARENTHESIS,
|
|
941
|
+
NumpadParenRight: KeyCode.NUMPAD_RIGHT_PARENTHESIS,
|
|
942
|
+
NumpadSubtract: KeyCode.NUMPAD_MINUS,
|
|
943
|
+
};
|
|
944
|
+
const _keyboardData = {
|
|
945
|
+
altKey: false,
|
|
946
|
+
capsLock: false,
|
|
947
|
+
code: '',
|
|
948
|
+
ctrlKey: false,
|
|
949
|
+
key: '',
|
|
950
|
+
keyCode: 0,
|
|
951
|
+
location: 0,
|
|
952
|
+
metaKey: false,
|
|
953
|
+
modifier: 0,
|
|
954
|
+
numLock: false,
|
|
955
|
+
repeat: false,
|
|
956
|
+
shiftKey: false,
|
|
957
|
+
timeStamp: 0,
|
|
958
|
+
};
|
|
959
|
+
const _pointerData = {
|
|
960
|
+
altKey: false,
|
|
961
|
+
button: 0,
|
|
962
|
+
buttons: 0,
|
|
963
|
+
ctrlKey: false,
|
|
964
|
+
deltaX: 0,
|
|
965
|
+
deltaY: 0,
|
|
966
|
+
height: 1,
|
|
967
|
+
isPrimary: true,
|
|
968
|
+
metaKey: false,
|
|
969
|
+
pointerId: 0,
|
|
970
|
+
pointerType: 'mouse',
|
|
971
|
+
pressure: 0,
|
|
972
|
+
shiftKey: false,
|
|
973
|
+
tiltX: 0,
|
|
974
|
+
tiltY: 0,
|
|
975
|
+
timeStamp: 0,
|
|
976
|
+
twist: 0,
|
|
977
|
+
wheelMode: 'unknown',
|
|
978
|
+
width: 1,
|
|
979
|
+
x: 0,
|
|
980
|
+
y: 0,
|
|
981
|
+
};
|
|
982
|
+
const _textData = {
|
|
983
|
+
isComposing: false,
|
|
984
|
+
text: '',
|
|
985
|
+
};
|
|
986
|
+
const _gamepadPollStates = new WeakMap();
|
|
987
|
+
function getOrCreateGamepadPollState(manager) {
|
|
988
|
+
let state = _gamepadPollStates.get(manager);
|
|
989
|
+
if (state === undefined) {
|
|
990
|
+
state = { axes: new Map(), buttons: new Map() };
|
|
991
|
+
_gamepadPollStates.set(manager, state);
|
|
992
|
+
}
|
|
993
|
+
return state;
|
|
994
|
+
}
|
|
995
|
+
const _axisData = { axis: 0, gamepad: 0, timeStamp: 0, value: 0 };
|
|
996
|
+
const _buttonData = { button: 0, gamepad: 0, timeStamp: 0, value: 0 };
|
|
997
|
+
const _connectData = { gamepad: 0, id: '', mapping: '' };
|
|
998
|
+
// Internal teardown registry: maps a manager to its per-target, per-input-kind cleanup closures.
|
|
999
|
+
// Kept off the public InputManager entity (a side table like `_gamepadPollStates`) so attach/detach
|
|
1000
|
+
// track bindings internally and callers hold nothing. The nested EventTarget key lets one manager
|
|
1001
|
+
// attach the same input kind to multiple elements and detach each precisely.
|
|
1002
|
+
const kGamepadInput = Symbol();
|
|
1003
|
+
const kKeyboardInput = Symbol();
|
|
1004
|
+
const kPointerInput = Symbol();
|
|
1005
|
+
const kRelativePointerInput = Symbol();
|
|
1006
|
+
const kTextInput = Symbol();
|
|
1007
|
+
const kWheelInput = Symbol();
|
|
1008
|
+
const _inputBindings = new WeakMap();
|
|
1009
|
+
function clearInputBinding(manager, target, kind) {
|
|
1010
|
+
const byKind = _inputBindings.get(manager)?.get(target);
|
|
1011
|
+
const cleanup = byKind?.get(kind);
|
|
1012
|
+
if (cleanup === undefined)
|
|
1013
|
+
return;
|
|
1014
|
+
cleanup();
|
|
1015
|
+
byKind.delete(kind);
|
|
1016
|
+
}
|
|
1017
|
+
function setInputBinding(manager, target, kind, cleanup) {
|
|
1018
|
+
let byTarget = _inputBindings.get(manager);
|
|
1019
|
+
if (byTarget === undefined) {
|
|
1020
|
+
byTarget = new Map();
|
|
1021
|
+
_inputBindings.set(manager, byTarget);
|
|
1022
|
+
}
|
|
1023
|
+
let byKind = byTarget.get(target);
|
|
1024
|
+
if (byKind === undefined) {
|
|
1025
|
+
byKind = new Map();
|
|
1026
|
+
byTarget.set(target, byKind);
|
|
1027
|
+
}
|
|
1028
|
+
byKind.get(kind)?.();
|
|
1029
|
+
byKind.set(kind, cleanup);
|
|
1030
|
+
}
|
|
1031
|
+
//# sourceMappingURL=inputManager.js.map
|