@yagejs/input 0.3.0 → 0.5.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/api-BaItBbSe.d.cts +685 -0
- package/dist/api-BaItBbSe.d.ts +685 -0
- package/dist/api.cjs +34 -0
- package/dist/api.cjs.map +1 -0
- package/dist/api.d.cts +2 -0
- package/dist/api.d.ts +2 -0
- package/dist/api.js +7 -0
- package/dist/api.js.map +1 -0
- package/dist/chunk-O2U7FZ7Q.js +12 -0
- package/dist/chunk-O2U7FZ7Q.js.map +1 -0
- package/dist/index.cjs +1322 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -164
- package/dist/index.d.ts +4 -164
- package/dist/index.js +1326 -62
- package/dist/index.js.map +1 -1
- package/package.json +13 -3
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
InputManagerKey,
|
|
3
|
+
__name
|
|
4
|
+
} from "./chunk-O2U7FZ7Q.js";
|
|
3
5
|
|
|
4
6
|
// src/InputPlugin.ts
|
|
5
7
|
import { RendererAdapterKey } from "@yagejs/core";
|
|
@@ -7,6 +9,42 @@ import { DebugRegistryKey } from "@yagejs/debug/api";
|
|
|
7
9
|
|
|
8
10
|
// src/InputManager.ts
|
|
9
11
|
import { Vec2 } from "@yagejs/core";
|
|
12
|
+
var MOUSE_BUTTON_CODES = ["MouseLeft", "MouseMiddle", "MouseRight"];
|
|
13
|
+
var STANDARD_BUTTON_CODES = [
|
|
14
|
+
"GamepadA",
|
|
15
|
+
"GamepadB",
|
|
16
|
+
"GamepadX",
|
|
17
|
+
"GamepadY",
|
|
18
|
+
"GamepadLB",
|
|
19
|
+
"GamepadRB",
|
|
20
|
+
"GamepadLT",
|
|
21
|
+
"GamepadRT",
|
|
22
|
+
"GamepadSelect",
|
|
23
|
+
"GamepadStart",
|
|
24
|
+
"GamepadLeftStick",
|
|
25
|
+
"GamepadRightStick",
|
|
26
|
+
"GamepadDPadUp",
|
|
27
|
+
"GamepadDPadDown",
|
|
28
|
+
"GamepadDPadLeft",
|
|
29
|
+
"GamepadDPadRight",
|
|
30
|
+
"GamepadHome"
|
|
31
|
+
];
|
|
32
|
+
var TRIGGER_LEFT_INDEX = 6;
|
|
33
|
+
var TRIGGER_RIGHT_INDEX = 7;
|
|
34
|
+
var STICK_AXIS_KEYS = {
|
|
35
|
+
left: { x: "leftX", y: "leftY" },
|
|
36
|
+
right: { x: "rightX", y: "rightY" }
|
|
37
|
+
};
|
|
38
|
+
var TRIGGER_AXIS_KEYS = {
|
|
39
|
+
left: "leftTrigger",
|
|
40
|
+
right: "rightTrigger"
|
|
41
|
+
};
|
|
42
|
+
var STANDARD_AXIS_KEYS = [
|
|
43
|
+
"leftX",
|
|
44
|
+
"leftY",
|
|
45
|
+
"rightX",
|
|
46
|
+
"rightY"
|
|
47
|
+
];
|
|
10
48
|
var InputManager = class {
|
|
11
49
|
static {
|
|
12
50
|
__name(this, "InputManager");
|
|
@@ -15,13 +53,71 @@ var InputManager = class {
|
|
|
15
53
|
justPressedKeys = /* @__PURE__ */ new Set();
|
|
16
54
|
justReleasedKeys = /* @__PURE__ */ new Set();
|
|
17
55
|
holdStart = /* @__PURE__ */ new Map();
|
|
56
|
+
syntheticPressedActions = /* @__PURE__ */ new Set();
|
|
57
|
+
syntheticActionStarts = /* @__PURE__ */ new Map();
|
|
18
58
|
actionMap = /* @__PURE__ */ new Map();
|
|
19
59
|
defaultBindings = /* @__PURE__ */ new Map();
|
|
20
60
|
groups = /* @__PURE__ */ new Map();
|
|
21
61
|
actionGroups = /* @__PURE__ */ new Map();
|
|
22
62
|
disabledGroups = /* @__PURE__ */ new Set();
|
|
23
|
-
|
|
24
|
-
|
|
63
|
+
/** Tracked pointers keyed by `pointerId`. Mouse persists; touch/pen removed on up/cancel. */
|
|
64
|
+
pointers = /* @__PURE__ */ new Map();
|
|
65
|
+
/** Id of the pointer the browser last marked `isPrimary`, or `null` when none are tracked. */
|
|
66
|
+
primaryPointerId = null;
|
|
67
|
+
/**
|
|
68
|
+
* Aggregate "any pointer has this button held" cache. The action-map codes
|
|
69
|
+
* `MouseLeft`/`MouseMiddle`/`MouseRight` are driven from edges into/out of this
|
|
70
|
+
* set so two simultaneous taps holding button 0 do not double-fire.
|
|
71
|
+
* Consumed pointers are excluded from the aggregate so UI-claimed presses
|
|
72
|
+
* never propagate to gameplay actions.
|
|
73
|
+
*/
|
|
74
|
+
mouseButtonAggregate = /* @__PURE__ */ new Set();
|
|
75
|
+
/**
|
|
76
|
+
* Pointers marked as "claimed" via {@link consumePointer} (or auto-claimed by
|
|
77
|
+
* the renderer's UI hit-test fallback). Lifetime is per-pointer-event-cycle:
|
|
78
|
+
* cleared when the pointer's last button releases (drained `pointerUp`) or on
|
|
79
|
+
* `pointercancel`.
|
|
80
|
+
*/
|
|
81
|
+
consumedPointers = /* @__PURE__ */ new Set();
|
|
82
|
+
/** Wheel-edge gate flipped by {@link consumeWheel}. Cleared at end of frame. */
|
|
83
|
+
consumedWheelThisFrame = false;
|
|
84
|
+
/** Buffered DOM-originated events awaiting drain at `Phase.EarlyUpdate`. */
|
|
85
|
+
inputQueue = [];
|
|
86
|
+
/**
|
|
87
|
+
* Renderer reference for the optional `hitTestUI(x, y)` lookup. Stashed by
|
|
88
|
+
* {@link _setRenderer} during `InputPlugin.install` so the drain step can
|
|
89
|
+
* read it cheaply each frame.
|
|
90
|
+
*/
|
|
91
|
+
renderer = null;
|
|
92
|
+
pointerDownListeners = [];
|
|
93
|
+
pointerUpListeners = [];
|
|
94
|
+
pointerMoveListeners = [];
|
|
95
|
+
keyDownListenersAny = [];
|
|
96
|
+
keyUpListenersAny = [];
|
|
97
|
+
keyDownListeners = /* @__PURE__ */ new Map();
|
|
98
|
+
keyUpListeners = /* @__PURE__ */ new Map();
|
|
99
|
+
actionListeners = /* @__PURE__ */ new Map();
|
|
100
|
+
actionReleasedListeners = /* @__PURE__ */ new Map();
|
|
101
|
+
wheelListeners = [];
|
|
102
|
+
/** Real-pad axis values keyed by `${padIndex}:${axisKey}`. */
|
|
103
|
+
gamepadAxisState = /* @__PURE__ */ new Map();
|
|
104
|
+
/** Synthetic axis values for fireGamepadAxis injection (test path). */
|
|
105
|
+
syntheticAxisState = /* @__PURE__ */ new Map();
|
|
106
|
+
/** "Any pad" aggregate of currently-pressed gamepad codes. */
|
|
107
|
+
lastButtonState = /* @__PURE__ */ new Map();
|
|
108
|
+
/** Per-pad "anything happening" flag, used to detect rising-edge activity for active-pad promotion. */
|
|
109
|
+
lastPadActivity = /* @__PURE__ */ new Map();
|
|
110
|
+
/** Pads currently known to the engine (populated via events or polling). */
|
|
111
|
+
connectedPads = /* @__PURE__ */ new Map();
|
|
112
|
+
/** Index of the pad whose analog input is read by default. `null` when no pad is connected. */
|
|
113
|
+
activePadIndex = null;
|
|
114
|
+
gamepadConnectListeners = [];
|
|
115
|
+
gamepadDisconnectListeners = [];
|
|
116
|
+
activePadListeners = [];
|
|
117
|
+
stickDeadzone = 0.15;
|
|
118
|
+
triggerDeadzone = 0.05;
|
|
119
|
+
triggerThreshold = 0.5;
|
|
120
|
+
pollingEnabled = true;
|
|
25
121
|
camera = null;
|
|
26
122
|
elapsedMs = 0;
|
|
27
123
|
listenResolve = null;
|
|
@@ -29,12 +125,12 @@ var InputManager = class {
|
|
|
29
125
|
/** Whether any key mapped to this action is currently held. */
|
|
30
126
|
isPressed(action) {
|
|
31
127
|
if (!this.isActionEnabled(action)) return false;
|
|
32
|
-
return this.anyKeyInSet(action, this.pressedKeys);
|
|
128
|
+
return this.syntheticPressedActions.has(action) || this.anyKeyInSet(action, this.pressedKeys);
|
|
33
129
|
}
|
|
34
130
|
/** Whether any key mapped to this action was pressed this frame. */
|
|
35
131
|
isJustPressed(action) {
|
|
36
132
|
if (!this.isActionEnabled(action)) return false;
|
|
37
|
-
return this.anyKeyInSet(action, this.justPressedKeys);
|
|
133
|
+
return this.syntheticPressedActions.has(action) || this.anyKeyInSet(action, this.justPressedKeys);
|
|
38
134
|
}
|
|
39
135
|
/** Whether any key mapped to this action was released this frame. */
|
|
40
136
|
isJustReleased(action) {
|
|
@@ -54,14 +150,18 @@ var InputManager = class {
|
|
|
54
150
|
getHoldDuration(action) {
|
|
55
151
|
if (!this.isActionEnabled(action)) return 0;
|
|
56
152
|
const keys = this.actionMap.get(action);
|
|
57
|
-
if (!keys) return 0;
|
|
153
|
+
if (!keys && !this.syntheticPressedActions.has(action)) return 0;
|
|
58
154
|
let maxDuration = 0;
|
|
59
|
-
for (const key of keys) {
|
|
155
|
+
for (const key of keys ?? []) {
|
|
60
156
|
const start = this.holdStart.get(key);
|
|
61
157
|
if (start !== void 0) {
|
|
62
158
|
maxDuration = Math.max(maxDuration, this.elapsedMs - start);
|
|
63
159
|
}
|
|
64
160
|
}
|
|
161
|
+
const syntheticStart = this.syntheticActionStarts.get(action);
|
|
162
|
+
if (syntheticStart !== void 0) {
|
|
163
|
+
maxDuration = Math.max(maxDuration, this.elapsedMs - syntheticStart);
|
|
164
|
+
}
|
|
65
165
|
return maxDuration;
|
|
66
166
|
}
|
|
67
167
|
/** Whether the action has been held for at least `minTime` ms. */
|
|
@@ -82,24 +182,216 @@ var InputManager = class {
|
|
|
82
182
|
return new Vec2(x, y);
|
|
83
183
|
}
|
|
84
184
|
// -- Pointer --
|
|
85
|
-
/**
|
|
185
|
+
/**
|
|
186
|
+
* Primary pointer's position in world coordinates (via Camera), or screen
|
|
187
|
+
* coords if no camera. Returns `Vec2.ZERO` when no pointer is tracked.
|
|
188
|
+
*
|
|
189
|
+
* For multi-pointer access (touch UIs etc.) iterate {@link getPointers} and
|
|
190
|
+
* convert each `screenPos` via the camera as needed.
|
|
191
|
+
*/
|
|
86
192
|
getPointerPosition() {
|
|
193
|
+
const primary = this.getPrimaryPointer();
|
|
194
|
+
if (!primary) return Vec2.ZERO;
|
|
87
195
|
if (this.camera) {
|
|
88
|
-
const w = this.camera.screenToWorld(
|
|
89
|
-
this.pointerScreenPos.x,
|
|
90
|
-
this.pointerScreenPos.y
|
|
91
|
-
);
|
|
196
|
+
const w = this.camera.screenToWorld(primary.screenPos.x, primary.screenPos.y);
|
|
92
197
|
return new Vec2(w.x, w.y);
|
|
93
198
|
}
|
|
94
|
-
return
|
|
199
|
+
return primary.screenPos;
|
|
95
200
|
}
|
|
96
|
-
/**
|
|
201
|
+
/** Primary pointer's raw position in screen coordinates, or `Vec2.ZERO` when no pointer is tracked. */
|
|
97
202
|
getPointerScreenPosition() {
|
|
98
|
-
|
|
203
|
+
const primary = this.getPrimaryPointer();
|
|
204
|
+
return primary ? primary.screenPos : Vec2.ZERO;
|
|
99
205
|
}
|
|
100
|
-
/** Whether
|
|
206
|
+
/** Whether the primary pointer has any button held. */
|
|
101
207
|
isPointerDown() {
|
|
102
|
-
|
|
208
|
+
const primary = this.getPrimaryPointer();
|
|
209
|
+
return primary ? primary.isDown : false;
|
|
210
|
+
}
|
|
211
|
+
/** All currently-tracked pointers (one per active mouse, pen, or finger). */
|
|
212
|
+
getPointers() {
|
|
213
|
+
const out = [];
|
|
214
|
+
for (const p of this.pointers.values()) {
|
|
215
|
+
out.push(this.toPointerInfo(p));
|
|
216
|
+
}
|
|
217
|
+
return out;
|
|
218
|
+
}
|
|
219
|
+
/** Direct lookup by `pointerId`, or `undefined` if no pointer with that id is tracked. */
|
|
220
|
+
getPointer(id) {
|
|
221
|
+
const p = this.pointers.get(id);
|
|
222
|
+
return p ? this.toPointerInfo(p) : void 0;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Defensive snapshot of a tracked pointer. The runtime `MutablePointerInfo`
|
|
226
|
+
* holds a real `Set` for `buttons` — even though the `PointerInfo` type
|
|
227
|
+
* declares `ReadonlySet`, JS doesn't enforce that at runtime, so we copy the
|
|
228
|
+
* set on every public read. `Vec2` is convention-immutable across YAGE, so
|
|
229
|
+
* we share the same instance.
|
|
230
|
+
*/
|
|
231
|
+
toPointerInfo(pointer) {
|
|
232
|
+
return {
|
|
233
|
+
id: pointer.id,
|
|
234
|
+
screenPos: pointer.screenPos,
|
|
235
|
+
type: pointer.type,
|
|
236
|
+
isPrimary: pointer.isPrimary,
|
|
237
|
+
buttons: new Set(pointer.buttons),
|
|
238
|
+
isDown: pointer.isDown
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Subscribe to pointer-down events (button transitions from up → down on a
|
|
243
|
+
* tracked pointer). Returns a disposer that detaches the listener.
|
|
244
|
+
*/
|
|
245
|
+
onPointerDown(fn) {
|
|
246
|
+
this.pointerDownListeners.push(fn);
|
|
247
|
+
return () => {
|
|
248
|
+
const idx = this.pointerDownListeners.indexOf(fn);
|
|
249
|
+
if (idx !== -1) this.pointerDownListeners.splice(idx, 1);
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Subscribe to pointer-up events (button transitions from down → up, plus
|
|
254
|
+
* touch / pen lifecycle ends and `pointercancel`). Returns a disposer.
|
|
255
|
+
*/
|
|
256
|
+
onPointerUp(fn) {
|
|
257
|
+
this.pointerUpListeners.push(fn);
|
|
258
|
+
return () => {
|
|
259
|
+
const idx = this.pointerUpListeners.indexOf(fn);
|
|
260
|
+
if (idx !== -1) this.pointerUpListeners.splice(idx, 1);
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
/** Subscribe to pointer-move events. Returns a disposer. */
|
|
264
|
+
onPointerMove(fn) {
|
|
265
|
+
this.pointerMoveListeners.push(fn);
|
|
266
|
+
return () => {
|
|
267
|
+
const idx = this.pointerMoveListeners.indexOf(fn);
|
|
268
|
+
if (idx !== -1) this.pointerMoveListeners.splice(idx, 1);
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
// -- Consume primitives --
|
|
272
|
+
/**
|
|
273
|
+
* Mark a pointer as claimed for the rest of its event cycle (down → up).
|
|
274
|
+
* Subsequent action-map edges for this pointer (e.g. the `MouseLeft` edge a
|
|
275
|
+
* `pointerdown` would normally fire) are suppressed; `onPointerDown/Up/Move`
|
|
276
|
+
* listeners still fire because they are explicit user opt-ins.
|
|
277
|
+
*
|
|
278
|
+
* The mark clears automatically when the pointer's last button releases or
|
|
279
|
+
* on `pointercancel`. Call from a Pixi `pointerdown` handler that wants to
|
|
280
|
+
* own the event: `manager.consumePointer(e.pointerId)`.
|
|
281
|
+
*/
|
|
282
|
+
consumePointer(id) {
|
|
283
|
+
this.consumedPointers.add(id);
|
|
284
|
+
}
|
|
285
|
+
/** Whether the pointer is currently marked consumed. */
|
|
286
|
+
isPointerConsumed(id) {
|
|
287
|
+
return this.consumedPointers.has(id);
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Suppress wheel action-map edges (`WheelUp/Down/Left/Right`) for the rest
|
|
291
|
+
* of the current frame. `onWheel` listeners still fire.
|
|
292
|
+
*/
|
|
293
|
+
consumeWheel() {
|
|
294
|
+
this.consumedWheelThisFrame = true;
|
|
295
|
+
}
|
|
296
|
+
// -- Listener parity (keys, actions, wheel) --
|
|
297
|
+
/**
|
|
298
|
+
* Subscribe to key-down events. Pass a code (e.g. `"Space"`, `"GamepadA"`)
|
|
299
|
+
* to filter, or `"*"` for all keys. The listener fires on the same edge
|
|
300
|
+
* `isJustPressed` reports — for DOM-originated events that's the next
|
|
301
|
+
* `Phase.EarlyUpdate` after the browser dispatches; for synthetic injection
|
|
302
|
+
* (`fireKeyDown`) it's synchronous. Returns a disposer.
|
|
303
|
+
*/
|
|
304
|
+
onKeyDown(code, fn) {
|
|
305
|
+
if (code === "*") {
|
|
306
|
+
this.keyDownListenersAny.push(fn);
|
|
307
|
+
return () => {
|
|
308
|
+
const idx = this.keyDownListenersAny.indexOf(fn);
|
|
309
|
+
if (idx !== -1) this.keyDownListenersAny.splice(idx, 1);
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
let arr = this.keyDownListeners.get(code);
|
|
313
|
+
if (!arr) {
|
|
314
|
+
arr = [];
|
|
315
|
+
this.keyDownListeners.set(code, arr);
|
|
316
|
+
}
|
|
317
|
+
arr.push(fn);
|
|
318
|
+
return () => {
|
|
319
|
+
const list = this.keyDownListeners.get(code);
|
|
320
|
+
if (!list) return;
|
|
321
|
+
const idx = list.indexOf(fn);
|
|
322
|
+
if (idx !== -1) list.splice(idx, 1);
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
/** Subscribe to key-up events. See {@link onKeyDown}. */
|
|
326
|
+
onKeyUp(code, fn) {
|
|
327
|
+
if (code === "*") {
|
|
328
|
+
this.keyUpListenersAny.push(fn);
|
|
329
|
+
return () => {
|
|
330
|
+
const idx = this.keyUpListenersAny.indexOf(fn);
|
|
331
|
+
if (idx !== -1) this.keyUpListenersAny.splice(idx, 1);
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
let arr = this.keyUpListeners.get(code);
|
|
335
|
+
if (!arr) {
|
|
336
|
+
arr = [];
|
|
337
|
+
this.keyUpListeners.set(code, arr);
|
|
338
|
+
}
|
|
339
|
+
arr.push(fn);
|
|
340
|
+
return () => {
|
|
341
|
+
const list = this.keyUpListeners.get(code);
|
|
342
|
+
if (!list) return;
|
|
343
|
+
const idx = list.indexOf(fn);
|
|
344
|
+
if (idx !== -1) list.splice(idx, 1);
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Subscribe to action press edges (rising edge of any key bound to the
|
|
349
|
+
* action). Fires once per press. Returns a disposer.
|
|
350
|
+
*/
|
|
351
|
+
onAction(name, fn) {
|
|
352
|
+
let arr = this.actionListeners.get(name);
|
|
353
|
+
if (!arr) {
|
|
354
|
+
arr = [];
|
|
355
|
+
this.actionListeners.set(name, arr);
|
|
356
|
+
}
|
|
357
|
+
arr.push(fn);
|
|
358
|
+
return () => {
|
|
359
|
+
const list = this.actionListeners.get(name);
|
|
360
|
+
if (!list) return;
|
|
361
|
+
const idx = list.indexOf(fn);
|
|
362
|
+
if (idx !== -1) list.splice(idx, 1);
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
/** Subscribe to action release edges. Returns a disposer. */
|
|
366
|
+
onActionReleased(name, fn) {
|
|
367
|
+
let arr = this.actionReleasedListeners.get(name);
|
|
368
|
+
if (!arr) {
|
|
369
|
+
arr = [];
|
|
370
|
+
this.actionReleasedListeners.set(name, arr);
|
|
371
|
+
}
|
|
372
|
+
arr.push(fn);
|
|
373
|
+
return () => {
|
|
374
|
+
const list = this.actionReleasedListeners.get(name);
|
|
375
|
+
if (!list) return;
|
|
376
|
+
const idx = list.indexOf(fn);
|
|
377
|
+
if (idx !== -1) list.splice(idx, 1);
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Subscribe to scroll-wheel events. Receives raw `deltaX`/`deltaY` (already
|
|
382
|
+
* sign-flipped by `InputConfig.wheelInvertY` if set). Fires regardless of
|
|
383
|
+
* {@link consumeWheel} — it only gates action edges. Returns a disposer.
|
|
384
|
+
*/
|
|
385
|
+
onWheel(fn) {
|
|
386
|
+
this.wheelListeners.push(fn);
|
|
387
|
+
return () => {
|
|
388
|
+
const idx = this.wheelListeners.indexOf(fn);
|
|
389
|
+
if (idx !== -1) this.wheelListeners.splice(idx, 1);
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
getPrimaryPointer() {
|
|
393
|
+
if (this.primaryPointerId === null) return null;
|
|
394
|
+
return this.pointers.get(this.primaryPointerId) ?? null;
|
|
103
395
|
}
|
|
104
396
|
// -- Runtime action map management --
|
|
105
397
|
/** Replace the entire action map and store it as the default for {@link resetBindings}. */
|
|
@@ -295,9 +587,702 @@ var InputManager = class {
|
|
|
295
587
|
resolve(null);
|
|
296
588
|
}
|
|
297
589
|
}
|
|
298
|
-
|
|
590
|
+
/** Public wrapper for synthetic key-down injection. Applies sync. */
|
|
591
|
+
fireKeyDown(code) {
|
|
592
|
+
this._applyKeyDown(code);
|
|
593
|
+
}
|
|
594
|
+
/** Public wrapper for synthetic key-up injection. Applies sync. */
|
|
595
|
+
fireKeyUp(code) {
|
|
596
|
+
this._applyKeyUp(code);
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Public wrapper for synthetic pointer movement. Defaults to the primary
|
|
600
|
+
* mouse pointer (`id: 1`, `type: "mouse"`); pass `opts` to drive a specific
|
|
601
|
+
* touch / pen pointer.
|
|
602
|
+
*/
|
|
603
|
+
firePointerMove(screenX, screenY, opts) {
|
|
604
|
+
this._applyPointerMove(this.makeSyntheticInfo(screenX, screenY, -1, opts));
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Public wrapper for synthetic pointer-button presses. Defaults to button 0
|
|
608
|
+
* on the primary mouse pointer. Pass `opts` for touch / pen / non-primary
|
|
609
|
+
* pointers (e.g. `{ id: 5, type: "touch", isPrimary: false }`).
|
|
610
|
+
*/
|
|
611
|
+
firePointerDown(button = 0, opts) {
|
|
612
|
+
const id = opts?.id ?? 1;
|
|
613
|
+
const existing = this.pointers.get(id);
|
|
614
|
+
this._applyPointerDown(
|
|
615
|
+
this.makeSyntheticInfo(
|
|
616
|
+
existing?.screenPos.x ?? 0,
|
|
617
|
+
existing?.screenPos.y ?? 0,
|
|
618
|
+
button,
|
|
619
|
+
opts
|
|
620
|
+
)
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
/** Public wrapper for synthetic pointer-button releases. */
|
|
624
|
+
firePointerUp(button = 0, opts) {
|
|
625
|
+
const id = opts?.id ?? 1;
|
|
626
|
+
const existing = this.pointers.get(id);
|
|
627
|
+
const info = {
|
|
628
|
+
id,
|
|
629
|
+
screenX: existing?.screenPos.x ?? 0,
|
|
630
|
+
screenY: existing?.screenPos.y ?? 0,
|
|
631
|
+
type: existing?.type ?? "mouse",
|
|
632
|
+
isPrimary: existing?.isPrimary ?? id === 1,
|
|
633
|
+
button
|
|
634
|
+
};
|
|
635
|
+
this._applyPointerUp(info);
|
|
636
|
+
}
|
|
637
|
+
/** Public wrapper for synthetic wheel input. Applies sync, including
|
|
638
|
+
* action edges and `onWheel` listener notification — matching the DOM path
|
|
639
|
+
* so tests and inspector probes drive the full surface. */
|
|
640
|
+
fireWheel(dx, dy) {
|
|
641
|
+
for (const fn of [...this.wheelListeners]) fn(dx, dy);
|
|
642
|
+
this.applyWheelEdges(dx, dy);
|
|
643
|
+
}
|
|
644
|
+
makeSyntheticInfo(screenX, screenY, button, opts) {
|
|
645
|
+
const id = opts?.id ?? 1;
|
|
646
|
+
return {
|
|
647
|
+
id,
|
|
648
|
+
screenX,
|
|
649
|
+
screenY,
|
|
650
|
+
type: opts?.type ?? "mouse",
|
|
651
|
+
isPrimary: opts?.isPrimary ?? id === 1,
|
|
652
|
+
button
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Inject a synthetic gamepad button edge. Routes through the same internal
|
|
657
|
+
* path as real polling, so action queries (`isPressed`, `isJustPressed`),
|
|
658
|
+
* `listenForNextKey`, and rebinding all see the synthetic input.
|
|
659
|
+
*
|
|
660
|
+
* `code` should be a gamepad code string (e.g. `"GamepadA"`, `"GamepadLT"`).
|
|
661
|
+
* Used by inspector probes / deterministic tests in lieu of real polling.
|
|
662
|
+
*/
|
|
663
|
+
fireGamepadButton(code, pressed) {
|
|
664
|
+
const wasPressed = this.lastButtonState.get(code) ?? false;
|
|
665
|
+
if (pressed && !wasPressed) {
|
|
666
|
+
this._applyKeyDown(code);
|
|
667
|
+
this.lastButtonState.set(code, true);
|
|
668
|
+
} else if (!pressed && wasPressed) {
|
|
669
|
+
this._applyKeyUp(code);
|
|
670
|
+
this.lastButtonState.delete(code);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Inject a synthetic gamepad axis value. Stored separately from real-pad
|
|
675
|
+
* axis state and consulted by `getStick` / `getTrigger` only when no real
|
|
676
|
+
* pad is active — matching how a test fixture would use the API.
|
|
677
|
+
*
|
|
678
|
+
* Trigger axes additionally emit `GamepadLT`/`GamepadRT` button edges when
|
|
679
|
+
* crossing `triggerThreshold`, mirroring real-pad polling so synthetic
|
|
680
|
+
* inspector probes drive `isPressed` the same way as physical hardware.
|
|
681
|
+
*/
|
|
682
|
+
fireGamepadAxis(side, value) {
|
|
683
|
+
const safe = Number.isFinite(value) ? value : 0;
|
|
684
|
+
this.syntheticAxisState.set(side, safe);
|
|
685
|
+
if (side === "leftTrigger") {
|
|
686
|
+
this.fireGamepadButton("GamepadLT", safe >= this.triggerThreshold);
|
|
687
|
+
} else if (side === "rightTrigger") {
|
|
688
|
+
this.fireGamepadButton("GamepadRT", safe >= this.triggerThreshold);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
// -- Gamepad analog API --
|
|
692
|
+
/**
|
|
693
|
+
* Returns the deadzoned, magnitude-clamped stick vector for the given side.
|
|
694
|
+
*
|
|
695
|
+
* By default reads from the active pad (the most recently used controller,
|
|
696
|
+
* or the first connected one if nothing has been used yet). Pass
|
|
697
|
+
* `{ pad: index }` to read from a specific pad — useful for couch-co-op
|
|
698
|
+
* where each player's controller is addressed explicitly.
|
|
699
|
+
*
|
|
700
|
+
* Falls back to synthetic injection (`fireGamepadAxis`) when no pad is
|
|
701
|
+
* active — that's the test/probe path.
|
|
702
|
+
*/
|
|
703
|
+
getStick(side, opts) {
|
|
704
|
+
const { x: xKey, y: yKey } = STICK_AXIS_KEYS[side];
|
|
705
|
+
const padIdx = opts?.pad !== void 0 ? opts.pad : this.activePadIndex;
|
|
706
|
+
let x;
|
|
707
|
+
let y;
|
|
708
|
+
if (padIdx !== null) {
|
|
709
|
+
x = this.gamepadAxisState.get(`${padIdx}:${xKey}`) ?? 0;
|
|
710
|
+
y = this.gamepadAxisState.get(`${padIdx}:${yKey}`) ?? 0;
|
|
711
|
+
} else {
|
|
712
|
+
x = this.syntheticAxisState.get(xKey) ?? 0;
|
|
713
|
+
y = this.syntheticAxisState.get(yKey) ?? 0;
|
|
714
|
+
}
|
|
715
|
+
const mag = Math.hypot(x, y);
|
|
716
|
+
if (mag < this.stickDeadzone) return Vec2.ZERO;
|
|
717
|
+
if (mag === 0) return Vec2.ZERO;
|
|
718
|
+
const adjustedMag = Math.min(
|
|
719
|
+
1,
|
|
720
|
+
(mag - this.stickDeadzone) / (1 - this.stickDeadzone)
|
|
721
|
+
);
|
|
722
|
+
return new Vec2(x / mag * adjustedMag, y / mag * adjustedMag);
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Returns the deadzoned trigger value (0..1) for the given side.
|
|
726
|
+
* Reads from the active pad by default; use `{ pad: index }` for explicit
|
|
727
|
+
* per-pad reads. Falls back to synthetic state when no pad is active.
|
|
728
|
+
*/
|
|
729
|
+
getTrigger(side, opts) {
|
|
730
|
+
const key = TRIGGER_AXIS_KEYS[side];
|
|
731
|
+
const padIdx = opts?.pad !== void 0 ? opts.pad : this.activePadIndex;
|
|
732
|
+
const v = padIdx !== null ? this.gamepadAxisState.get(`${padIdx}:${key}`) ?? 0 : this.syntheticAxisState.get(key) ?? 0;
|
|
733
|
+
if (v < this.triggerDeadzone) return 0;
|
|
734
|
+
return Math.min(1, (v - this.triggerDeadzone) / (1 - this.triggerDeadzone));
|
|
735
|
+
}
|
|
736
|
+
// -- Gamepad enumeration / events --
|
|
737
|
+
/**
|
|
738
|
+
* Synchronously poll `navigator.getGamepads()` for currently-connected pads.
|
|
739
|
+
* Use this rather than the cached event-driven list when you need ground
|
|
740
|
+
* truth — `gamepadconnected` doesn't fire until the user presses a button.
|
|
741
|
+
*/
|
|
742
|
+
gamepads() {
|
|
743
|
+
if (typeof navigator === "undefined" || typeof navigator.getGamepads !== "function") {
|
|
744
|
+
return [];
|
|
745
|
+
}
|
|
746
|
+
const result = [];
|
|
747
|
+
for (const pad of navigator.getGamepads()) {
|
|
748
|
+
if (pad) result.push({ index: pad.index, id: pad.id });
|
|
749
|
+
}
|
|
750
|
+
return result;
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Subscribe to gamepad-connected events. Replays currently-known pads
|
|
754
|
+
* synchronously so callers don't need a separate `gamepads()` call.
|
|
755
|
+
* Returns a disposer.
|
|
756
|
+
*/
|
|
757
|
+
onGamepadConnected(fn) {
|
|
758
|
+
this.gamepadConnectListeners.push(fn);
|
|
759
|
+
for (const info of this.connectedPads.values()) fn(info);
|
|
760
|
+
return () => {
|
|
761
|
+
const idx = this.gamepadConnectListeners.indexOf(fn);
|
|
762
|
+
if (idx !== -1) this.gamepadConnectListeners.splice(idx, 1);
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
/** Subscribe to gamepad-disconnected events. Returns a disposer. */
|
|
766
|
+
onGamepadDisconnected(fn) {
|
|
767
|
+
this.gamepadDisconnectListeners.push(fn);
|
|
768
|
+
return () => {
|
|
769
|
+
const idx = this.gamepadDisconnectListeners.indexOf(fn);
|
|
770
|
+
if (idx !== -1) this.gamepadDisconnectListeners.splice(idx, 1);
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
// -- Active pad --
|
|
774
|
+
/**
|
|
775
|
+
* The pad whose analog input is read by default. Auto-promotes on input
|
|
776
|
+
* activity (button press or stick/trigger above deadzone) and on first
|
|
777
|
+
* connect. Returns `null` when no pad is connected.
|
|
778
|
+
*/
|
|
779
|
+
getActivePad() {
|
|
780
|
+
if (this.activePadIndex === null) return null;
|
|
781
|
+
return this.connectedPads.get(this.activePadIndex) ?? null;
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Manually set the active pad. Index must match a currently connected pad
|
|
785
|
+
* — pass an unknown index and the call is a no-op. Pass `null` to clear
|
|
786
|
+
* (analog reads will fall back to synthetic state if any).
|
|
787
|
+
*/
|
|
788
|
+
setActivePad(index) {
|
|
789
|
+
if (index !== null && !this.connectedPads.has(index)) return;
|
|
790
|
+
this.setActivePadInternal(index);
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Subscribe to active-pad changes. Replays the current active pad
|
|
794
|
+
* synchronously on subscribe so callers get the present state without a
|
|
795
|
+
* separate `getActivePad()` call. Returns a disposer.
|
|
796
|
+
*/
|
|
797
|
+
onActivePadChanged(fn) {
|
|
798
|
+
this.activePadListeners.push(fn);
|
|
799
|
+
fn(this.getActivePad());
|
|
800
|
+
return () => {
|
|
801
|
+
const idx = this.activePadListeners.indexOf(fn);
|
|
802
|
+
if (idx !== -1) this.activePadListeners.splice(idx, 1);
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
setActivePadInternal(index) {
|
|
806
|
+
if (this.activePadIndex === index) return;
|
|
807
|
+
this.activePadIndex = index;
|
|
808
|
+
const info = this.getActivePad();
|
|
809
|
+
for (const fn of this.activePadListeners) fn(info);
|
|
810
|
+
}
|
|
811
|
+
// -- Gamepad runtime config --
|
|
812
|
+
/** Enable or disable real gamepad polling. Synthetic injection still works when disabled. */
|
|
813
|
+
setPollingEnabled(enabled) {
|
|
814
|
+
this.pollingEnabled = enabled;
|
|
815
|
+
}
|
|
816
|
+
/** Whether real gamepad polling is currently enabled. */
|
|
817
|
+
isPollingEnabled() {
|
|
818
|
+
return this.pollingEnabled;
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Update analog deadzones at runtime. Either field may be omitted.
|
|
822
|
+
* Values are clamped to `[0, 0.999]` — capping below 1 keeps the rescaling
|
|
823
|
+
* denominator non-zero. Non-finite values are ignored.
|
|
824
|
+
*/
|
|
825
|
+
setDeadzones(opts) {
|
|
826
|
+
if (opts.stick !== void 0 && Number.isFinite(opts.stick)) {
|
|
827
|
+
this.stickDeadzone = Math.max(0, Math.min(0.999, opts.stick));
|
|
828
|
+
}
|
|
829
|
+
if (opts.trigger !== void 0 && Number.isFinite(opts.trigger)) {
|
|
830
|
+
this.triggerDeadzone = Math.max(0, Math.min(0.999, opts.trigger));
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Set the trigger button-edge threshold (default 0.5). Clamped to `[0, 1]`;
|
|
835
|
+
* non-finite values are ignored.
|
|
836
|
+
*/
|
|
837
|
+
setTriggerThreshold(value) {
|
|
838
|
+
if (!Number.isFinite(value)) return;
|
|
839
|
+
this.triggerThreshold = Math.max(0, Math.min(1, value));
|
|
840
|
+
}
|
|
841
|
+
// -- Internal: polling and connect/disconnect plumbing --
|
|
842
|
+
/**
|
|
843
|
+
* @internal Force-release held gamepad buttons and clear real-pad analog
|
|
844
|
+
* snapshots. Used on tab-hide (where `navigator.getGamepads()` returns
|
|
845
|
+
* stale data) and on disconnect when polling is paused. Synthetic axes
|
|
846
|
+
* live in their own field, so they're untouched.
|
|
847
|
+
*/
|
|
848
|
+
_releaseAllGamepadState() {
|
|
849
|
+
for (const code of [...this.lastButtonState.keys()]) {
|
|
850
|
+
this._applyKeyUp(code);
|
|
851
|
+
}
|
|
852
|
+
this.lastButtonState.clear();
|
|
853
|
+
this.gamepadAxisState.clear();
|
|
854
|
+
this.lastPadActivity.clear();
|
|
855
|
+
}
|
|
856
|
+
/** @internal Called by InputPlugin from `gamepadconnected` event or by
|
|
857
|
+
* polling when discovering a previously-unknown pad. Idempotent. */
|
|
858
|
+
_onGamepadConnected(info) {
|
|
859
|
+
if (this.connectedPads.has(info.index)) return;
|
|
860
|
+
this.connectedPads.set(info.index, info);
|
|
861
|
+
if (this.activePadIndex === null) {
|
|
862
|
+
this.setActivePadInternal(info.index);
|
|
863
|
+
}
|
|
864
|
+
for (const fn of this.gamepadConnectListeners) fn(info);
|
|
865
|
+
}
|
|
866
|
+
/** @internal Called by InputPlugin from `gamepaddisconnected` event or by
|
|
867
|
+
* polling when a pad vanishes silently. Idempotent. */
|
|
868
|
+
_onGamepadDisconnected(info) {
|
|
869
|
+
if (!this.connectedPads.has(info.index)) return;
|
|
870
|
+
this.connectedPads.delete(info.index);
|
|
871
|
+
for (const key of [...this.gamepadAxisState.keys()]) {
|
|
872
|
+
if (key.startsWith(`${info.index}:`)) this.gamepadAxisState.delete(key);
|
|
873
|
+
}
|
|
874
|
+
this.lastPadActivity.delete(info.index);
|
|
875
|
+
if (this.activePadIndex === info.index) {
|
|
876
|
+
const next = this.connectedPads.keys().next();
|
|
877
|
+
this.setActivePadInternal(next.done ? null : next.value);
|
|
878
|
+
}
|
|
879
|
+
if (this.pollingEnabled && typeof navigator !== "undefined" && typeof navigator.getGamepads === "function") {
|
|
880
|
+
this.reconcileButtonStateAcrossPads(navigator.getGamepads());
|
|
881
|
+
} else {
|
|
882
|
+
this._releaseAllGamepadState();
|
|
883
|
+
}
|
|
884
|
+
for (const fn of this.gamepadDisconnectListeners) fn(info);
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* @internal Poll real gamepads via `navigator.getGamepads()` and emit
|
|
888
|
+
* key-down/key-up edges for any aggregate state changes. Called by
|
|
889
|
+
* `InputPollSystem` once per frame.
|
|
890
|
+
*/
|
|
891
|
+
_pollGamepads() {
|
|
892
|
+
if (typeof navigator === "undefined" || typeof navigator.getGamepads !== "function") {
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
const pads = navigator.getGamepads();
|
|
896
|
+
const liveIndices = /* @__PURE__ */ new Set();
|
|
897
|
+
for (const pad of pads) {
|
|
898
|
+
if (!pad) continue;
|
|
899
|
+
liveIndices.add(pad.index);
|
|
900
|
+
if (!this.connectedPads.has(pad.index)) {
|
|
901
|
+
this._onGamepadConnected({ index: pad.index, id: pad.id });
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
for (const [, info] of [...this.connectedPads]) {
|
|
905
|
+
if (!liveIndices.has(info.index)) {
|
|
906
|
+
this._onGamepadDisconnected(info);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
for (const key of [...this.gamepadAxisState.keys()]) {
|
|
910
|
+
const colon = key.indexOf(":");
|
|
911
|
+
if (colon === -1) continue;
|
|
912
|
+
const idx = Number.parseInt(key.slice(0, colon), 10);
|
|
913
|
+
if (!liveIndices.has(idx)) this.gamepadAxisState.delete(key);
|
|
914
|
+
}
|
|
915
|
+
const currentActivity = /* @__PURE__ */ new Map();
|
|
916
|
+
for (const pad of pads) {
|
|
917
|
+
if (!pad) continue;
|
|
918
|
+
const standard = pad.mapping === "standard";
|
|
919
|
+
if (standard) {
|
|
920
|
+
for (let axIdx = 0; axIdx < STANDARD_AXIS_KEYS.length; axIdx++) {
|
|
921
|
+
const axisKey = STANDARD_AXIS_KEYS[axIdx];
|
|
922
|
+
if (!axisKey) continue;
|
|
923
|
+
const raw = pad.axes[axIdx] ?? 0;
|
|
924
|
+
this.gamepadAxisState.set(
|
|
925
|
+
`${pad.index}:${axisKey}`,
|
|
926
|
+
Number.isFinite(raw) ? raw : 0
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
const lt = pad.buttons[TRIGGER_LEFT_INDEX]?.value ?? 0;
|
|
930
|
+
const rt = pad.buttons[TRIGGER_RIGHT_INDEX]?.value ?? 0;
|
|
931
|
+
this.gamepadAxisState.set(
|
|
932
|
+
`${pad.index}:leftTrigger`,
|
|
933
|
+
Number.isFinite(lt) ? lt : 0
|
|
934
|
+
);
|
|
935
|
+
this.gamepadAxisState.set(
|
|
936
|
+
`${pad.index}:rightTrigger`,
|
|
937
|
+
Number.isFinite(rt) ? rt : 0
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
currentActivity.set(pad.index, this.padHasActivity(pad));
|
|
941
|
+
}
|
|
942
|
+
const activeStillActive = this.activePadIndex !== null && (currentActivity.get(this.activePadIndex) ?? false);
|
|
943
|
+
if (!activeStillActive) {
|
|
944
|
+
for (const [padIdx, isActive] of currentActivity) {
|
|
945
|
+
const wasActive = this.lastPadActivity.get(padIdx) ?? false;
|
|
946
|
+
if (isActive && !wasActive && padIdx !== this.activePadIndex) {
|
|
947
|
+
this.setActivePadInternal(padIdx);
|
|
948
|
+
break;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
for (const [padIdx, isActive] of currentActivity) {
|
|
953
|
+
this.lastPadActivity.set(padIdx, isActive);
|
|
954
|
+
}
|
|
955
|
+
this.reconcileButtonStateAcrossPads(pads);
|
|
956
|
+
}
|
|
957
|
+
/** Whether a pad has any input that should claim active-pad ownership. */
|
|
958
|
+
padHasActivity(pad) {
|
|
959
|
+
for (const btn of pad.buttons) {
|
|
960
|
+
if (btn?.pressed) return true;
|
|
961
|
+
}
|
|
962
|
+
if (pad.mapping === "standard") {
|
|
963
|
+
const lx = pad.axes[0] ?? 0;
|
|
964
|
+
const ly = pad.axes[1] ?? 0;
|
|
965
|
+
const rx = pad.axes[2] ?? 0;
|
|
966
|
+
const ry = pad.axes[3] ?? 0;
|
|
967
|
+
if (Math.hypot(lx, ly) > this.stickDeadzone) return true;
|
|
968
|
+
if (Math.hypot(rx, ry) > this.stickDeadzone) return true;
|
|
969
|
+
const lt = pad.buttons[TRIGGER_LEFT_INDEX]?.value ?? 0;
|
|
970
|
+
const rt = pad.buttons[TRIGGER_RIGHT_INDEX]?.value ?? 0;
|
|
971
|
+
if (lt > this.triggerDeadzone) return true;
|
|
972
|
+
if (rt > this.triggerDeadzone) return true;
|
|
973
|
+
}
|
|
974
|
+
return false;
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Aggregate "any pad pressed" per code across the supplied pad list and
|
|
978
|
+
* emit `_applyKeyDown`/`_applyKeyUp` edges. `lastButtonState` is updated
|
|
979
|
+
* unconditionally so listen-mode interception doesn't cause held-button
|
|
980
|
+
* re-fires on subsequent frames.
|
|
981
|
+
*/
|
|
982
|
+
reconcileButtonStateAcrossPads(pads) {
|
|
983
|
+
const codePressed = /* @__PURE__ */ new Map();
|
|
984
|
+
for (const pad of pads) {
|
|
985
|
+
if (!pad) continue;
|
|
986
|
+
const standard = pad.mapping === "standard";
|
|
987
|
+
const buttons = pad.buttons;
|
|
988
|
+
for (let btnIdx = 0; btnIdx < buttons.length; btnIdx++) {
|
|
989
|
+
const btn = buttons[btnIdx];
|
|
990
|
+
if (!btn) continue;
|
|
991
|
+
const standardCode = standard && btnIdx < STANDARD_BUTTON_CODES.length ? STANDARD_BUTTON_CODES[btnIdx] : void 0;
|
|
992
|
+
const code = standardCode ?? `GamepadButton${btnIdx}`;
|
|
993
|
+
const isTrigger = standard && (btnIdx === TRIGGER_LEFT_INDEX || btnIdx === TRIGGER_RIGHT_INDEX);
|
|
994
|
+
const isDown = isTrigger ? btn.value >= this.triggerThreshold : btn.pressed;
|
|
995
|
+
if (isDown) codePressed.set(code, true);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
const allCodes = /* @__PURE__ */ new Set([
|
|
999
|
+
...this.lastButtonState.keys(),
|
|
1000
|
+
...codePressed.keys()
|
|
1001
|
+
]);
|
|
1002
|
+
for (const code of allCodes) {
|
|
1003
|
+
const wasPressed = this.lastButtonState.get(code) ?? false;
|
|
1004
|
+
const isPressed = codePressed.get(code) ?? false;
|
|
1005
|
+
if (isPressed && !wasPressed) {
|
|
1006
|
+
this._applyKeyDown(code);
|
|
1007
|
+
} else if (!isPressed && wasPressed) {
|
|
1008
|
+
this._applyKeyUp(code);
|
|
1009
|
+
}
|
|
1010
|
+
if (isPressed) {
|
|
1011
|
+
this.lastButtonState.set(code, true);
|
|
1012
|
+
} else {
|
|
1013
|
+
this.lastButtonState.delete(code);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
/** Inject a one-frame synthetic action pulse. */
|
|
1018
|
+
fireAction(name) {
|
|
1019
|
+
if (!this.actionMap.has(name)) {
|
|
1020
|
+
throw new Error(`InputManager.fireAction(): unknown action "${name}".`);
|
|
1021
|
+
}
|
|
1022
|
+
this.syntheticPressedActions.add(name);
|
|
1023
|
+
this.syntheticActionStarts.set(name, this.elapsedMs);
|
|
1024
|
+
this.notifyActionListeners(this.actionListeners, name);
|
|
1025
|
+
}
|
|
1026
|
+
/** Release all synthetic and physical input state. */
|
|
1027
|
+
clearAll() {
|
|
1028
|
+
for (const code of [...this.pressedKeys]) {
|
|
1029
|
+
this._applyKeyUp(code);
|
|
1030
|
+
}
|
|
1031
|
+
this.justPressedKeys.clear();
|
|
1032
|
+
this.justReleasedKeys.clear();
|
|
1033
|
+
this.holdStart.clear();
|
|
1034
|
+
this.syntheticPressedActions.clear();
|
|
1035
|
+
this.syntheticActionStarts.clear();
|
|
1036
|
+
this.pointers.clear();
|
|
1037
|
+
this.primaryPointerId = null;
|
|
1038
|
+
this.mouseButtonAggregate.clear();
|
|
1039
|
+
this.consumedPointers.clear();
|
|
1040
|
+
this.consumedWheelThisFrame = false;
|
|
1041
|
+
this.inputQueue.length = 0;
|
|
1042
|
+
this.lastButtonState.clear();
|
|
1043
|
+
this.gamepadAxisState.clear();
|
|
1044
|
+
this.syntheticAxisState.clear();
|
|
1045
|
+
this.lastPadActivity.clear();
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Drop all tracked pointers and release the aggregate `MouseLeft/Middle/Right`
|
|
1049
|
+
* codes without touching keyboard or gamepad state. Useful for window-blur
|
|
1050
|
+
* / page-hide handling.
|
|
1051
|
+
*/
|
|
1052
|
+
clearPointerButtons() {
|
|
1053
|
+
for (const button of [...this.mouseButtonAggregate]) {
|
|
1054
|
+
const code = MOUSE_BUTTON_CODES[button];
|
|
1055
|
+
if (code) this._applyKeyUp(code);
|
|
1056
|
+
}
|
|
1057
|
+
this.mouseButtonAggregate.clear();
|
|
1058
|
+
this.pointers.clear();
|
|
1059
|
+
this.primaryPointerId = null;
|
|
1060
|
+
this.consumedPointers.clear();
|
|
1061
|
+
this.inputQueue = this.inputQueue.filter(
|
|
1062
|
+
(e) => e.kind !== "pointerDown" && e.kind !== "pointerUp" && e.kind !== "pointerCancel"
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
/** Snapshot of current held input state for inspector tooling. */
|
|
1066
|
+
snapshotState() {
|
|
1067
|
+
const cmp = /* @__PURE__ */ __name((a, b) => a < b ? -1 : a > b ? 1 : 0, "cmp");
|
|
1068
|
+
const keys = [...this.pressedKeys].sort(cmp);
|
|
1069
|
+
const nonGamepadKeys = keys.filter((k) => !k.startsWith("Gamepad"));
|
|
1070
|
+
const gamepadButtons = keys.filter((k) => k.startsWith("Gamepad"));
|
|
1071
|
+
const actions = this.getActionNames().filter((action) => this.isPressed(action)).sort(cmp);
|
|
1072
|
+
const aggregateButtons = [...this.mouseButtonAggregate].sort((a, b) => a - b);
|
|
1073
|
+
const pointers = [...this.pointers.values()].sort((a, b) => a.id - b.id).map((p) => ({
|
|
1074
|
+
id: p.id,
|
|
1075
|
+
x: p.screenPos.x,
|
|
1076
|
+
y: p.screenPos.y,
|
|
1077
|
+
type: p.type,
|
|
1078
|
+
isPrimary: p.isPrimary,
|
|
1079
|
+
buttons: [...p.buttons].sort((a, b) => a - b),
|
|
1080
|
+
down: p.isDown
|
|
1081
|
+
}));
|
|
1082
|
+
const primary = this.getPrimaryPointer();
|
|
1083
|
+
const realAxes = [...this.gamepadAxisState.entries()].filter(([, value]) => Math.abs(value) > 1e-3).map(([key, value]) => ({ key, value }));
|
|
1084
|
+
const syntheticAxes = [...this.syntheticAxisState.entries()].filter(([, value]) => Math.abs(value) > 1e-3).map(([key, value]) => ({ key: `synthetic:${key}`, value }));
|
|
1085
|
+
const axes = [...realAxes, ...syntheticAxes].sort((a, b) => cmp(a.key, b.key));
|
|
1086
|
+
return {
|
|
1087
|
+
keys: nonGamepadKeys,
|
|
1088
|
+
actions,
|
|
1089
|
+
mouse: {
|
|
1090
|
+
x: primary?.screenPos.x ?? 0,
|
|
1091
|
+
y: primary?.screenPos.y ?? 0,
|
|
1092
|
+
buttons: aggregateButtons,
|
|
1093
|
+
down: this.mouseButtonAggregate.size > 0
|
|
1094
|
+
},
|
|
1095
|
+
pointers,
|
|
1096
|
+
gamepad: {
|
|
1097
|
+
buttons: gamepadButtons,
|
|
1098
|
+
axes
|
|
1099
|
+
}
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
// -- Internal: DOM-handler enqueue path --
|
|
1103
|
+
/**
|
|
1104
|
+
* @internal Stash the renderer adapter so the drain step can call its
|
|
1105
|
+
* optional `hitTestUI(x, y)` for the auto-consume fallback. Called by
|
|
1106
|
+
* `InputPlugin.install`.
|
|
1107
|
+
*/
|
|
1108
|
+
_setRenderer(renderer) {
|
|
1109
|
+
this.renderer = renderer;
|
|
1110
|
+
}
|
|
299
1111
|
/** @internal */
|
|
300
|
-
|
|
1112
|
+
_enqueueKeyDown(code) {
|
|
1113
|
+
this.inputQueue.push({ kind: "keyDown", code });
|
|
1114
|
+
}
|
|
1115
|
+
/** @internal */
|
|
1116
|
+
_enqueueKeyUp(code) {
|
|
1117
|
+
this.inputQueue.push({ kind: "keyUp", code });
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* @internal Sync portion: upsert the pointer entry (existence, screenPos,
|
|
1121
|
+
* type, isPrimary, primaryPointerId) and notify pointerMoveListeners so
|
|
1122
|
+
* pointer-tracking UIs see live cursor positions. Move events do not carry
|
|
1123
|
+
* action-map edges, so they are not queued.
|
|
1124
|
+
*/
|
|
1125
|
+
_enqueuePointerMove(info) {
|
|
1126
|
+
const pointer = this.upsertPointer(info);
|
|
1127
|
+
pointer.screenPos = new Vec2(info.screenX, info.screenY);
|
|
1128
|
+
this.notifyPointerListeners(this.pointerMoveListeners, pointer);
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* @internal Sync portion: upsert pointer (existence, screenPos, type,
|
|
1132
|
+
* isPrimary, primaryPointerId) and notify pointerDownListeners. Button
|
|
1133
|
+
* mutation, action-map edges, and mouse-aggregate emit are deferred to the
|
|
1134
|
+
* next drain at `Phase.EarlyUpdate` so {@link consumePointer} (or the
|
|
1135
|
+
* renderer's UI hit-test) can suppress them, AND so a same-frame
|
|
1136
|
+
* down+up that arrives before drain still produces the correct
|
|
1137
|
+
* `MouseLeft` press/release edges (recomputing aggregate from live state
|
|
1138
|
+
* after sync mutation would silently drop the transient transition).
|
|
1139
|
+
*
|
|
1140
|
+
* Listeners therefore observe `pointer.buttons` BEFORE this event's edge is
|
|
1141
|
+
* applied. That's a documented tradeoff: the canonical event-button info
|
|
1142
|
+
* is in the `FederatedPointerEvent` / `PointerEvent` the user's Pixi
|
|
1143
|
+
* handler already receives, so the lossy `info.buttons` view rarely
|
|
1144
|
+
* matters in practice.
|
|
1145
|
+
*/
|
|
1146
|
+
_enqueuePointerDown(info) {
|
|
1147
|
+
const pointer = this.upsertPointer(info);
|
|
1148
|
+
pointer.screenPos = new Vec2(info.screenX, info.screenY);
|
|
1149
|
+
this.notifyPointerListeners(this.pointerDownListeners, pointer);
|
|
1150
|
+
this.inputQueue.push({ kind: "pointerDown", info });
|
|
1151
|
+
}
|
|
1152
|
+
/** @internal */
|
|
1153
|
+
_enqueuePointerUp(info) {
|
|
1154
|
+
const pointer = this.upsertPointer(info);
|
|
1155
|
+
pointer.screenPos = new Vec2(info.screenX, info.screenY);
|
|
1156
|
+
this.notifyPointerListeners(this.pointerUpListeners, pointer);
|
|
1157
|
+
this.inputQueue.push({ kind: "pointerUp", info });
|
|
1158
|
+
}
|
|
1159
|
+
/** @internal */
|
|
1160
|
+
_enqueuePointerCancel(id) {
|
|
1161
|
+
const pointer = this.pointers.get(id);
|
|
1162
|
+
if (pointer) {
|
|
1163
|
+
this.notifyPointerListeners(this.pointerUpListeners, pointer);
|
|
1164
|
+
}
|
|
1165
|
+
this.inputQueue.push({ kind: "pointerCancel", id });
|
|
1166
|
+
}
|
|
1167
|
+
/** @internal */
|
|
1168
|
+
_enqueueWheel(dx, dy) {
|
|
1169
|
+
for (const fn of [...this.wheelListeners]) fn(dx, dy);
|
|
1170
|
+
this.inputQueue.push({ kind: "wheel", dx, dy });
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* @internal Drain queued DOM events at `Phase.EarlyUpdate`. Each event
|
|
1174
|
+
* applies its deferred state (button mutations, action-map edges,
|
|
1175
|
+
* mouse-aggregate transitions). Consumed pointers are excluded from the
|
|
1176
|
+
* mouse aggregate so UI-claimed presses do not propagate to gameplay
|
|
1177
|
+
* actions. The renderer's optional `hitTestUI(x, y)` auto-claims a pointer
|
|
1178
|
+
* whose `pointerdown` lands on a UI-marked container.
|
|
1179
|
+
*/
|
|
1180
|
+
_drainInputQueue() {
|
|
1181
|
+
if (this.inputQueue.length === 0) return;
|
|
1182
|
+
const queue = this.inputQueue;
|
|
1183
|
+
this.inputQueue = [];
|
|
1184
|
+
for (const event of queue) {
|
|
1185
|
+
switch (event.kind) {
|
|
1186
|
+
case "keyDown":
|
|
1187
|
+
this._applyKeyDown(event.code);
|
|
1188
|
+
break;
|
|
1189
|
+
case "keyUp":
|
|
1190
|
+
this._applyKeyUp(event.code);
|
|
1191
|
+
break;
|
|
1192
|
+
case "pointerDown":
|
|
1193
|
+
this.drainPointerDown(event.info);
|
|
1194
|
+
break;
|
|
1195
|
+
case "pointerUp":
|
|
1196
|
+
this.drainPointerUp(event.info);
|
|
1197
|
+
break;
|
|
1198
|
+
case "pointerCancel":
|
|
1199
|
+
this.drainPointerCancel(event.id);
|
|
1200
|
+
break;
|
|
1201
|
+
case "wheel":
|
|
1202
|
+
this.applyWheelEdges(event.dx, event.dy);
|
|
1203
|
+
break;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
drainPointerDown(info) {
|
|
1208
|
+
if (!this.consumedPointers.has(info.id) && this.renderer?.hitTestUI?.(info.screenX, info.screenY)) {
|
|
1209
|
+
this.consumedPointers.add(info.id);
|
|
1210
|
+
}
|
|
1211
|
+
const pointer = this.pointers.get(info.id);
|
|
1212
|
+
if (!pointer) return;
|
|
1213
|
+
if (info.button >= 0 && info.button <= 2) {
|
|
1214
|
+
pointer.buttons.add(info.button);
|
|
1215
|
+
pointer.isDown = true;
|
|
1216
|
+
this.recomputeMouseAggregate(info.button);
|
|
1217
|
+
} else {
|
|
1218
|
+
pointer.isDown = pointer.buttons.size > 0;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
drainPointerUp(info) {
|
|
1222
|
+
const pointer = this.pointers.get(info.id);
|
|
1223
|
+
if (!pointer) return;
|
|
1224
|
+
if (info.button >= 0 && info.button <= 2) {
|
|
1225
|
+
pointer.buttons.delete(info.button);
|
|
1226
|
+
this.recomputeMouseAggregate(info.button);
|
|
1227
|
+
}
|
|
1228
|
+
pointer.isDown = pointer.buttons.size > 0;
|
|
1229
|
+
if (!pointer.isDown) {
|
|
1230
|
+
this.consumedPointers.delete(info.id);
|
|
1231
|
+
if (pointer.type !== "mouse") {
|
|
1232
|
+
this.removePointer(pointer.id);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
drainPointerCancel(id) {
|
|
1237
|
+
const pointer = this.pointers.get(id);
|
|
1238
|
+
if (!pointer) return;
|
|
1239
|
+
const heldButtons = [...pointer.buttons];
|
|
1240
|
+
pointer.buttons.clear();
|
|
1241
|
+
pointer.isDown = false;
|
|
1242
|
+
for (const button of heldButtons) {
|
|
1243
|
+
this.recomputeMouseAggregate(button);
|
|
1244
|
+
}
|
|
1245
|
+
this.consumedPointers.delete(id);
|
|
1246
|
+
if (pointer.type !== "mouse") {
|
|
1247
|
+
this.removePointer(id);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
applyWheelEdges(dx, dy) {
|
|
1251
|
+
if (this.consumedWheelThisFrame) return;
|
|
1252
|
+
if (Math.abs(dy) > 1e-3) {
|
|
1253
|
+
const code = dy < 0 ? "WheelUp" : "WheelDown";
|
|
1254
|
+
this.fireOneFrameEdge(code);
|
|
1255
|
+
}
|
|
1256
|
+
if (Math.abs(dx) > 1e-3) {
|
|
1257
|
+
const code = dx < 0 ? "WheelLeft" : "WheelRight";
|
|
1258
|
+
this.fireOneFrameEdge(code);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
* Add a code to `justPressedKeys` without entering `pressedKeys`. Used for
|
|
1263
|
+
* discrete edges (wheel ticks) that are never "held". Listeners and
|
|
1264
|
+
* `listenForNextKey` still fire as usual.
|
|
1265
|
+
*/
|
|
1266
|
+
fireOneFrameEdge(code) {
|
|
1267
|
+
if (this.listenResolve) {
|
|
1268
|
+
const resolve = this.listenResolve;
|
|
1269
|
+
this.listenResolve = null;
|
|
1270
|
+
resolve(code);
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
this.justPressedKeys.add(code);
|
|
1274
|
+
this.notifyKeyListeners(this.keyDownListeners, this.keyDownListenersAny, code);
|
|
1275
|
+
for (const action of this.actionsForCode(code)) {
|
|
1276
|
+
this.notifyActionListeners(this.actionListeners, action);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
// -- Internal: synthetic / sync apply path --
|
|
1280
|
+
/**
|
|
1281
|
+
* @internal Synthetic key-down. DOM-originated events must use
|
|
1282
|
+
* {@link _enqueueKeyDown} so `consumePointer` and the UI hit-test fallback
|
|
1283
|
+
* have a chance to run before action edges fire.
|
|
1284
|
+
*/
|
|
1285
|
+
_applyKeyDown(code) {
|
|
301
1286
|
if (this.listenResolve) {
|
|
302
1287
|
const resolve = this.listenResolve;
|
|
303
1288
|
this.listenResolve = null;
|
|
@@ -308,32 +1293,198 @@ var InputManager = class {
|
|
|
308
1293
|
this.pressedKeys.add(code);
|
|
309
1294
|
this.justPressedKeys.add(code);
|
|
310
1295
|
this.holdStart.set(code, this.elapsedMs);
|
|
1296
|
+
this.notifyKeyListeners(this.keyDownListeners, this.keyDownListenersAny, code);
|
|
1297
|
+
for (const action of this.actionsForCode(code)) {
|
|
1298
|
+
this.notifyActionListeners(this.actionListeners, action);
|
|
1299
|
+
}
|
|
311
1300
|
}
|
|
312
1301
|
}
|
|
313
|
-
/**
|
|
314
|
-
|
|
1302
|
+
/**
|
|
1303
|
+
* @internal Synthetic key-up. DOM-originated events must use
|
|
1304
|
+
* {@link _enqueueKeyUp}.
|
|
1305
|
+
*/
|
|
1306
|
+
_applyKeyUp(code) {
|
|
315
1307
|
if (this.pressedKeys.has(code)) {
|
|
316
1308
|
this.pressedKeys.delete(code);
|
|
317
1309
|
this.justReleasedKeys.add(code);
|
|
318
1310
|
this.holdStart.delete(code);
|
|
1311
|
+
this.notifyKeyListeners(this.keyUpListeners, this.keyUpListenersAny, code);
|
|
1312
|
+
for (const action of this.actionsForCode(code)) {
|
|
1313
|
+
this.notifyActionListeners(this.actionReleasedListeners, action);
|
|
1314
|
+
}
|
|
319
1315
|
}
|
|
320
1316
|
}
|
|
321
|
-
/**
|
|
322
|
-
|
|
323
|
-
|
|
1317
|
+
/**
|
|
1318
|
+
* @internal Synthetic pointer move. DOM-originated events must use
|
|
1319
|
+
* {@link _enqueuePointerMove}.
|
|
1320
|
+
*/
|
|
1321
|
+
_applyPointerMove(info) {
|
|
1322
|
+
const pointer = this.upsertPointer(info);
|
|
1323
|
+
pointer.screenPos = new Vec2(info.screenX, info.screenY);
|
|
1324
|
+
this.notifyPointerListeners(this.pointerMoveListeners, pointer);
|
|
324
1325
|
}
|
|
325
|
-
/**
|
|
326
|
-
|
|
327
|
-
|
|
1326
|
+
/**
|
|
1327
|
+
* @internal Synthetic pointer down. DOM-originated events must use
|
|
1328
|
+
* {@link _enqueuePointerDown}. This applies all state (button mutation,
|
|
1329
|
+
* mouse-aggregate emit, listener notify) synchronously.
|
|
1330
|
+
*/
|
|
1331
|
+
_applyPointerDown(info) {
|
|
1332
|
+
const pointer = this.upsertPointer(info);
|
|
1333
|
+
pointer.screenPos = new Vec2(info.screenX, info.screenY);
|
|
1334
|
+
if (info.button >= 0 && info.button <= 2) {
|
|
1335
|
+
pointer.buttons.add(info.button);
|
|
1336
|
+
pointer.isDown = true;
|
|
1337
|
+
this.recomputeMouseAggregate(info.button);
|
|
1338
|
+
} else {
|
|
1339
|
+
pointer.isDown = pointer.buttons.size > 0;
|
|
1340
|
+
}
|
|
1341
|
+
this.notifyPointerListeners(this.pointerDownListeners, pointer);
|
|
328
1342
|
}
|
|
329
|
-
/**
|
|
330
|
-
|
|
331
|
-
|
|
1343
|
+
/**
|
|
1344
|
+
* @internal Synthetic pointer up. DOM-originated events must use
|
|
1345
|
+
* {@link _enqueuePointerUp}.
|
|
1346
|
+
*/
|
|
1347
|
+
_applyPointerUp(info) {
|
|
1348
|
+
const pointer = this.upsertPointer(info);
|
|
1349
|
+
pointer.screenPos = new Vec2(info.screenX, info.screenY);
|
|
1350
|
+
if (info.button >= 0 && info.button <= 2) {
|
|
1351
|
+
pointer.buttons.delete(info.button);
|
|
1352
|
+
this.recomputeMouseAggregate(info.button);
|
|
1353
|
+
}
|
|
1354
|
+
pointer.isDown = pointer.buttons.size > 0;
|
|
1355
|
+
this.notifyPointerListeners(this.pointerUpListeners, pointer);
|
|
1356
|
+
if (!pointer.isDown) {
|
|
1357
|
+
this.consumedPointers.delete(info.id);
|
|
1358
|
+
if (pointer.type !== "mouse") {
|
|
1359
|
+
this.removePointer(pointer.id);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1364
|
+
* @internal Synthetic pointer cancel. Clears all buttons on the pointer,
|
|
1365
|
+
* fires up-listeners, and drops the entry (unless it's a mouse). Mirrors
|
|
1366
|
+
* the drain-time {@link drainPointerCancel} logic.
|
|
1367
|
+
*/
|
|
1368
|
+
_applyPointerCancel(id) {
|
|
1369
|
+
const pointer = this.pointers.get(id);
|
|
1370
|
+
if (!pointer) return;
|
|
1371
|
+
const heldButtons = [...pointer.buttons];
|
|
1372
|
+
pointer.buttons.clear();
|
|
1373
|
+
pointer.isDown = false;
|
|
1374
|
+
for (const button of heldButtons) {
|
|
1375
|
+
this.recomputeMouseAggregate(button);
|
|
1376
|
+
}
|
|
1377
|
+
this.notifyPointerListeners(this.pointerUpListeners, pointer);
|
|
1378
|
+
this.consumedPointers.delete(id);
|
|
1379
|
+
if (pointer.type !== "mouse") {
|
|
1380
|
+
this.removePointer(id);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
upsertPointer(info) {
|
|
1384
|
+
let pointer = this.pointers.get(info.id);
|
|
1385
|
+
if (!pointer) {
|
|
1386
|
+
pointer = {
|
|
1387
|
+
id: info.id,
|
|
1388
|
+
screenPos: new Vec2(info.screenX, info.screenY),
|
|
1389
|
+
type: info.type,
|
|
1390
|
+
isPrimary: info.isPrimary,
|
|
1391
|
+
buttons: /* @__PURE__ */ new Set(),
|
|
1392
|
+
isDown: false
|
|
1393
|
+
};
|
|
1394
|
+
this.pointers.set(info.id, pointer);
|
|
1395
|
+
} else {
|
|
1396
|
+
pointer.type = info.type;
|
|
1397
|
+
pointer.isPrimary = info.isPrimary;
|
|
1398
|
+
}
|
|
1399
|
+
if (info.isPrimary) {
|
|
1400
|
+
this.primaryPointerId = info.id;
|
|
1401
|
+
} else if (this.primaryPointerId === null) {
|
|
1402
|
+
this.primaryPointerId = info.id;
|
|
1403
|
+
}
|
|
1404
|
+
return pointer;
|
|
1405
|
+
}
|
|
1406
|
+
removePointer(id) {
|
|
1407
|
+
this.pointers.delete(id);
|
|
1408
|
+
if (this.primaryPointerId === id) {
|
|
1409
|
+
let next = null;
|
|
1410
|
+
for (const p of this.pointers.values()) {
|
|
1411
|
+
if (p.isPrimary) {
|
|
1412
|
+
next = p.id;
|
|
1413
|
+
break;
|
|
1414
|
+
}
|
|
1415
|
+
if (next === null) next = p.id;
|
|
1416
|
+
}
|
|
1417
|
+
this.primaryPointerId = next;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
/**
|
|
1421
|
+
* Recompute the `MouseLeft/Middle/Right` aggregate edge for `button`.
|
|
1422
|
+
* Consumed pointers are excluded so a UI-claimed press never propagates to
|
|
1423
|
+
* gameplay actions, even if a second non-UI pointer simultaneously holds
|
|
1424
|
+
* the same button.
|
|
1425
|
+
*/
|
|
1426
|
+
recomputeMouseAggregate(button) {
|
|
1427
|
+
const code = MOUSE_BUTTON_CODES[button];
|
|
1428
|
+
if (!code) return;
|
|
1429
|
+
let nowAny = false;
|
|
1430
|
+
for (const p of this.pointers.values()) {
|
|
1431
|
+
if (this.consumedPointers.has(p.id)) continue;
|
|
1432
|
+
if (p.buttons.has(button)) {
|
|
1433
|
+
nowAny = true;
|
|
1434
|
+
break;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
const wasAny = this.mouseButtonAggregate.has(button);
|
|
1438
|
+
if (nowAny && !wasAny) {
|
|
1439
|
+
this.mouseButtonAggregate.add(button);
|
|
1440
|
+
this._applyKeyDown(code);
|
|
1441
|
+
} else if (!nowAny && wasAny) {
|
|
1442
|
+
this.mouseButtonAggregate.delete(button);
|
|
1443
|
+
this._applyKeyUp(code);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
notifyPointerListeners(listeners, pointer) {
|
|
1447
|
+
if (listeners.length === 0) return;
|
|
1448
|
+
const info = this.toPointerInfo(pointer);
|
|
1449
|
+
for (const fn of [...listeners]) {
|
|
1450
|
+
fn(info);
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
notifyKeyListeners(perCode, anyList, code) {
|
|
1454
|
+
const list = perCode.get(code);
|
|
1455
|
+
if (list) {
|
|
1456
|
+
for (const fn of [...list]) fn(code);
|
|
1457
|
+
}
|
|
1458
|
+
if (anyList.length > 0) {
|
|
1459
|
+
for (const fn of [...anyList]) fn(code);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
notifyActionListeners(perAction, name) {
|
|
1463
|
+
const list = perAction.get(name);
|
|
1464
|
+
if (!list) return;
|
|
1465
|
+
for (const fn of [...list]) fn(name);
|
|
1466
|
+
}
|
|
1467
|
+
/**
|
|
1468
|
+
* Action names that include `code` in their bindings AND whose group is
|
|
1469
|
+
* currently enabled. Used for `onAction` / `onActionReleased` listener
|
|
1470
|
+
* fan-out so disabled-group suppression matches `isPressed` behavior.
|
|
1471
|
+
*/
|
|
1472
|
+
actionsForCode(code) {
|
|
1473
|
+
const result = [];
|
|
1474
|
+
for (const [action, keys] of this.actionMap) {
|
|
1475
|
+
if (keys.includes(code) && this.isActionEnabled(action)) {
|
|
1476
|
+
result.push(action);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
return result;
|
|
332
1480
|
}
|
|
333
1481
|
/** @internal Clear per-frame justPressed/justReleased flags. */
|
|
334
1482
|
_clearFrameState() {
|
|
335
1483
|
this.justPressedKeys.clear();
|
|
336
1484
|
this.justReleasedKeys.clear();
|
|
1485
|
+
this.syntheticPressedActions.clear();
|
|
1486
|
+
this.syntheticActionStarts.clear();
|
|
1487
|
+
this.consumedWheelThisFrame = false;
|
|
337
1488
|
}
|
|
338
1489
|
/** Set camera for pointer world-coord conversion. */
|
|
339
1490
|
setCamera(camera) {
|
|
@@ -351,12 +1502,33 @@ var InputManager = class {
|
|
|
351
1502
|
_advanceTime(dtMs) {
|
|
352
1503
|
this.elapsedMs += dtMs;
|
|
353
1504
|
}
|
|
1505
|
+
// -- Internal: sync-path aliases (back-compat with pre-0.5.x test callers) --
|
|
1506
|
+
/** @internal Sync alias — see {@link _applyKeyDown}. */
|
|
1507
|
+
_onKeyDown(code) {
|
|
1508
|
+
this._applyKeyDown(code);
|
|
1509
|
+
}
|
|
1510
|
+
/** @internal Sync alias — see {@link _applyKeyUp}. */
|
|
1511
|
+
_onKeyUp(code) {
|
|
1512
|
+
this._applyKeyUp(code);
|
|
1513
|
+
}
|
|
1514
|
+
/** @internal Sync alias — see {@link _applyPointerMove}. */
|
|
1515
|
+
_onPointerMove(info) {
|
|
1516
|
+
this._applyPointerMove(info);
|
|
1517
|
+
}
|
|
1518
|
+
/** @internal Sync alias — see {@link _applyPointerDown}. */
|
|
1519
|
+
_onPointerDown(info) {
|
|
1520
|
+
this._applyPointerDown(info);
|
|
1521
|
+
}
|
|
1522
|
+
/** @internal Sync alias — see {@link _applyPointerUp}. */
|
|
1523
|
+
_onPointerUp(info) {
|
|
1524
|
+
this._applyPointerUp(info);
|
|
1525
|
+
}
|
|
1526
|
+
/** @internal Sync alias — see {@link _applyPointerCancel}. */
|
|
1527
|
+
_onPointerCancel(id) {
|
|
1528
|
+
this._applyPointerCancel(id);
|
|
1529
|
+
}
|
|
354
1530
|
};
|
|
355
1531
|
|
|
356
|
-
// src/types.ts
|
|
357
|
-
import { ServiceKey } from "@yagejs/core";
|
|
358
|
-
var InputManagerKey = new ServiceKey("inputManager");
|
|
359
|
-
|
|
360
1532
|
// src/InputPollSystem.ts
|
|
361
1533
|
import { System, Phase } from "@yagejs/core";
|
|
362
1534
|
var InputPollSystem = class extends System {
|
|
@@ -366,7 +1538,12 @@ var InputPollSystem = class extends System {
|
|
|
366
1538
|
phase = Phase.EarlyUpdate;
|
|
367
1539
|
priority = -100;
|
|
368
1540
|
update(dt) {
|
|
369
|
-
this.use(InputManagerKey)
|
|
1541
|
+
const manager = this.use(InputManagerKey);
|
|
1542
|
+
manager._drainInputQueue();
|
|
1543
|
+
manager._advanceTime(dt);
|
|
1544
|
+
if (manager.isPollingEnabled()) {
|
|
1545
|
+
manager._pollGamepads();
|
|
1546
|
+
}
|
|
370
1547
|
}
|
|
371
1548
|
};
|
|
372
1549
|
|
|
@@ -421,11 +1598,6 @@ var InputDebugContributor = class {
|
|
|
421
1598
|
};
|
|
422
1599
|
|
|
423
1600
|
// src/InputPlugin.ts
|
|
424
|
-
var MOUSE_BUTTON_MAP = {
|
|
425
|
-
0: "MouseLeft",
|
|
426
|
-
1: "MouseMiddle",
|
|
427
|
-
2: "MouseRight"
|
|
428
|
-
};
|
|
429
1601
|
var InputPlugin = class {
|
|
430
1602
|
static {
|
|
431
1603
|
__name(this, "InputPlugin");
|
|
@@ -448,9 +1620,19 @@ var InputPlugin = class {
|
|
|
448
1620
|
if (this.config.groups) {
|
|
449
1621
|
this.manager.setGroups(this.config.groups);
|
|
450
1622
|
}
|
|
1623
|
+
if (this.config.deadzones) {
|
|
1624
|
+
this.manager.setDeadzones(this.config.deadzones);
|
|
1625
|
+
}
|
|
1626
|
+
if (this.config.triggerThreshold !== void 0) {
|
|
1627
|
+
this.manager.setTriggerThreshold(this.config.triggerThreshold);
|
|
1628
|
+
}
|
|
1629
|
+
if (this.config.pollGamepads === false) {
|
|
1630
|
+
this.manager.setPollingEnabled(false);
|
|
1631
|
+
}
|
|
451
1632
|
const rendererKey = this.config.rendererKey ?? RendererAdapterKey;
|
|
452
1633
|
const renderer = context.tryResolve(rendererKey);
|
|
453
1634
|
const pointerTarget = this.config.target ?? renderer?.canvas ?? document;
|
|
1635
|
+
this.manager._setRenderer(renderer ?? null);
|
|
454
1636
|
const coordinateElement = renderer?.canvas ?? this.config.target ?? null;
|
|
455
1637
|
const mapPointer = /* @__PURE__ */ __name((cssX, cssY) => renderer?.canvasToVirtual?.(cssX, cssY) ?? { x: cssX, y: cssY }, "mapPointer");
|
|
456
1638
|
const preventSet = new Set(this.config.preventDefaultKeys ?? []);
|
|
@@ -458,12 +1640,12 @@ var InputPlugin = class {
|
|
|
458
1640
|
const ke = e;
|
|
459
1641
|
if (ke.repeat) return;
|
|
460
1642
|
if (preventSet.has(ke.code)) ke.preventDefault();
|
|
461
|
-
this.manager.
|
|
1643
|
+
this.manager._enqueueKeyDown(ke.code);
|
|
462
1644
|
}, "onKeyDown");
|
|
463
1645
|
const onKeyUp = /* @__PURE__ */ __name((e) => {
|
|
464
1646
|
const ke = e;
|
|
465
1647
|
if (preventSet.has(ke.code)) ke.preventDefault();
|
|
466
|
-
this.manager.
|
|
1648
|
+
this.manager._enqueueKeyUp(ke.code);
|
|
467
1649
|
}, "onKeyUp");
|
|
468
1650
|
window.addEventListener("keydown", onKeyDown);
|
|
469
1651
|
window.addEventListener("keyup", onKeyUp);
|
|
@@ -471,8 +1653,7 @@ var InputPlugin = class {
|
|
|
471
1653
|
() => window.removeEventListener("keydown", onKeyDown),
|
|
472
1654
|
() => window.removeEventListener("keyup", onKeyUp)
|
|
473
1655
|
);
|
|
474
|
-
const
|
|
475
|
-
const pe = e;
|
|
1656
|
+
const buildInfo = /* @__PURE__ */ __name((pe) => {
|
|
476
1657
|
let cssX;
|
|
477
1658
|
let cssY;
|
|
478
1659
|
if (coordinateElement) {
|
|
@@ -484,36 +1665,96 @@ var InputPlugin = class {
|
|
|
484
1665
|
cssY = pe.clientY;
|
|
485
1666
|
}
|
|
486
1667
|
const mapped = mapPointer(cssX, cssY);
|
|
487
|
-
|
|
1668
|
+
const rawType = pe.pointerType;
|
|
1669
|
+
const type = rawType === "touch" || rawType === "pen" ? rawType : "mouse";
|
|
1670
|
+
return {
|
|
1671
|
+
id: pe.pointerId,
|
|
1672
|
+
screenX: mapped.x,
|
|
1673
|
+
screenY: mapped.y,
|
|
1674
|
+
type,
|
|
1675
|
+
isPrimary: pe.isPrimary,
|
|
1676
|
+
button: pe.button
|
|
1677
|
+
};
|
|
1678
|
+
}, "buildInfo");
|
|
1679
|
+
const onPointerMove = /* @__PURE__ */ __name((e) => {
|
|
1680
|
+
this.manager._enqueuePointerMove(buildInfo(e));
|
|
488
1681
|
}, "onPointerMove");
|
|
489
1682
|
const onPointerDown = /* @__PURE__ */ __name((e) => {
|
|
490
|
-
|
|
491
|
-
this.manager._onPointerDown();
|
|
492
|
-
const mouseKey = MOUSE_BUTTON_MAP[pe.button];
|
|
493
|
-
if (mouseKey) this.manager._onKeyDown(mouseKey);
|
|
1683
|
+
this.manager._enqueuePointerDown(buildInfo(e));
|
|
494
1684
|
}, "onPointerDown");
|
|
495
1685
|
const onPointerUp = /* @__PURE__ */ __name((e) => {
|
|
496
|
-
|
|
497
|
-
this.manager._onPointerUp();
|
|
498
|
-
const mouseKey = MOUSE_BUTTON_MAP[pe.button];
|
|
499
|
-
if (mouseKey) this.manager._onKeyUp(mouseKey);
|
|
1686
|
+
this.manager._enqueuePointerUp(buildInfo(e));
|
|
500
1687
|
}, "onPointerUp");
|
|
501
|
-
const onPointerCancel = /* @__PURE__ */ __name(() => {
|
|
502
|
-
|
|
503
|
-
this.manager.
|
|
504
|
-
this.manager._onKeyUp("MouseMiddle");
|
|
505
|
-
this.manager._onKeyUp("MouseRight");
|
|
1688
|
+
const onPointerCancel = /* @__PURE__ */ __name((e) => {
|
|
1689
|
+
const pe = e;
|
|
1690
|
+
this.manager._enqueuePointerCancel(pe.pointerId);
|
|
506
1691
|
}, "onPointerCancel");
|
|
1692
|
+
const onPointerLeave = /* @__PURE__ */ __name((e) => {
|
|
1693
|
+
const pe = e;
|
|
1694
|
+
this.manager._enqueuePointerCancel(pe.pointerId);
|
|
1695
|
+
}, "onPointerLeave");
|
|
507
1696
|
pointerTarget.addEventListener("pointerdown", onPointerDown);
|
|
508
1697
|
window.addEventListener("pointermove", onPointerMove);
|
|
509
1698
|
window.addEventListener("pointerup", onPointerUp);
|
|
510
1699
|
window.addEventListener("pointercancel", onPointerCancel);
|
|
1700
|
+
pointerTarget.addEventListener("pointerleave", onPointerLeave);
|
|
511
1701
|
this.cleanupFns.push(
|
|
512
1702
|
() => pointerTarget.removeEventListener("pointerdown", onPointerDown),
|
|
513
1703
|
() => window.removeEventListener("pointermove", onPointerMove),
|
|
514
1704
|
() => window.removeEventListener("pointerup", onPointerUp),
|
|
515
|
-
() => window.removeEventListener("pointercancel", onPointerCancel)
|
|
1705
|
+
() => window.removeEventListener("pointercancel", onPointerCancel),
|
|
1706
|
+
() => pointerTarget.removeEventListener("pointerleave", onPointerLeave)
|
|
1707
|
+
);
|
|
1708
|
+
const wheelInvertY = this.config.wheelInvertY === true;
|
|
1709
|
+
const preventDefaultWheel = this.config.preventDefaultWheel === true;
|
|
1710
|
+
const onWheel = /* @__PURE__ */ __name((e) => {
|
|
1711
|
+
const we = e;
|
|
1712
|
+
if (preventDefaultWheel) we.preventDefault();
|
|
1713
|
+
const dy = wheelInvertY ? -we.deltaY : we.deltaY;
|
|
1714
|
+
this.manager._enqueueWheel(we.deltaX, dy);
|
|
1715
|
+
}, "onWheel");
|
|
1716
|
+
pointerTarget.addEventListener(
|
|
1717
|
+
"wheel",
|
|
1718
|
+
onWheel,
|
|
1719
|
+
preventDefaultWheel ? { passive: false } : void 0
|
|
516
1720
|
);
|
|
1721
|
+
this.cleanupFns.push(
|
|
1722
|
+
() => pointerTarget.removeEventListener("wheel", onWheel)
|
|
1723
|
+
);
|
|
1724
|
+
const onGamepadConnected = /* @__PURE__ */ __name((e) => {
|
|
1725
|
+
const ge = e;
|
|
1726
|
+
if (!ge.gamepad) return;
|
|
1727
|
+
this.manager._onGamepadConnected({
|
|
1728
|
+
index: ge.gamepad.index,
|
|
1729
|
+
id: ge.gamepad.id
|
|
1730
|
+
});
|
|
1731
|
+
}, "onGamepadConnected");
|
|
1732
|
+
const onGamepadDisconnected = /* @__PURE__ */ __name((e) => {
|
|
1733
|
+
const ge = e;
|
|
1734
|
+
if (!ge.gamepad) return;
|
|
1735
|
+
this.manager._onGamepadDisconnected({
|
|
1736
|
+
index: ge.gamepad.index,
|
|
1737
|
+
id: ge.gamepad.id
|
|
1738
|
+
});
|
|
1739
|
+
}, "onGamepadDisconnected");
|
|
1740
|
+
window.addEventListener("gamepadconnected", onGamepadConnected);
|
|
1741
|
+
window.addEventListener("gamepaddisconnected", onGamepadDisconnected);
|
|
1742
|
+
this.cleanupFns.push(
|
|
1743
|
+
() => window.removeEventListener("gamepadconnected", onGamepadConnected),
|
|
1744
|
+
() => window.removeEventListener("gamepaddisconnected", onGamepadDisconnected)
|
|
1745
|
+
);
|
|
1746
|
+
if (typeof document !== "undefined") {
|
|
1747
|
+
const onVisibilityChange = /* @__PURE__ */ __name(() => {
|
|
1748
|
+
if (document.visibilityState === "hidden") {
|
|
1749
|
+
this.manager._releaseAllGamepadState();
|
|
1750
|
+
this.manager.clearPointerButtons();
|
|
1751
|
+
}
|
|
1752
|
+
}, "onVisibilityChange");
|
|
1753
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
1754
|
+
this.cleanupFns.push(
|
|
1755
|
+
() => document.removeEventListener("visibilitychange", onVisibilityChange)
|
|
1756
|
+
);
|
|
1757
|
+
}
|
|
517
1758
|
context.register(InputManagerKey, this.manager);
|
|
518
1759
|
}
|
|
519
1760
|
registerSystems(scheduler) {
|
|
@@ -649,10 +1890,33 @@ var KEY_DISPLAY_NAMES = {
|
|
|
649
1890
|
// Mouse buttons (synthetic codes from InputPlugin)
|
|
650
1891
|
MouseLeft: "Left Click",
|
|
651
1892
|
MouseMiddle: "Middle Click",
|
|
652
|
-
MouseRight: "Right Click"
|
|
1893
|
+
MouseRight: "Right Click",
|
|
1894
|
+
// Gamepad buttons (synthetic codes from InputPollSystem; standard mapping)
|
|
1895
|
+
GamepadA: "A",
|
|
1896
|
+
GamepadB: "B",
|
|
1897
|
+
GamepadX: "X",
|
|
1898
|
+
GamepadY: "Y",
|
|
1899
|
+
GamepadLB: "LB",
|
|
1900
|
+
GamepadRB: "RB",
|
|
1901
|
+
GamepadLT: "LT",
|
|
1902
|
+
GamepadRT: "RT",
|
|
1903
|
+
GamepadSelect: "Select",
|
|
1904
|
+
GamepadStart: "Start",
|
|
1905
|
+
GamepadLeftStick: "Left Stick",
|
|
1906
|
+
GamepadRightStick: "Right Stick",
|
|
1907
|
+
GamepadDPadUp: "D-Pad Up",
|
|
1908
|
+
GamepadDPadDown: "D-Pad Down",
|
|
1909
|
+
GamepadDPadLeft: "D-Pad Left",
|
|
1910
|
+
GamepadDPadRight: "D-Pad Right",
|
|
1911
|
+
GamepadHome: "Home"
|
|
653
1912
|
};
|
|
1913
|
+
var GAMEPAD_BUTTON_FALLBACK = /^GamepadButton(\d+)$/;
|
|
654
1914
|
function getKeyDisplayName(code) {
|
|
655
|
-
|
|
1915
|
+
const direct = KEY_DISPLAY_NAMES[code];
|
|
1916
|
+
if (direct !== void 0) return direct;
|
|
1917
|
+
const fallback = GAMEPAD_BUTTON_FALLBACK.exec(code);
|
|
1918
|
+
if (fallback) return `Gamepad Button ${fallback[1]}`;
|
|
1919
|
+
return code;
|
|
656
1920
|
}
|
|
657
1921
|
__name(getKeyDisplayName, "getKeyDisplayName");
|
|
658
1922
|
export {
|