@yagejs/input 0.4.0 → 0.6.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.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  InputManagerKey,
3
3
  __name
4
- } from "./chunk-CYWOKPMI.js";
4
+ } from "./chunk-O2U7FZ7Q.js";
5
5
 
6
6
  // src/InputPlugin.ts
7
7
  import { RendererAdapterKey } from "@yagejs/core";
@@ -9,6 +9,42 @@ import { DebugRegistryKey } from "@yagejs/debug/api";
9
9
 
10
10
  // src/InputManager.ts
11
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
+ ];
12
48
  var InputManager = class {
13
49
  static {
14
50
  __name(this, "InputManager");
@@ -24,11 +60,64 @@ var InputManager = class {
24
60
  groups = /* @__PURE__ */ new Map();
25
61
  actionGroups = /* @__PURE__ */ new Map();
26
62
  disabledGroups = /* @__PURE__ */ new Set();
27
- pointerScreenPos = Vec2.ZERO;
28
- pointerDownState = false;
29
- pressedMouseButtons = /* @__PURE__ */ new Set();
30
- gamepadButtons = /* @__PURE__ */ new Map();
31
- gamepadAxes = /* @__PURE__ */ new Map();
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;
32
121
  camera = null;
33
122
  elapsedMs = 0;
34
123
  listenResolve = null;
@@ -93,24 +182,216 @@ var InputManager = class {
93
182
  return new Vec2(x, y);
94
183
  }
95
184
  // -- Pointer --
96
- /** Pointer position in world coordinates (via Camera), or screen coords if no camera. */
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
+ */
97
192
  getPointerPosition() {
193
+ const primary = this.getPrimaryPointer();
194
+ if (!primary) return Vec2.ZERO;
98
195
  if (this.camera) {
99
- const w = this.camera.screenToWorld(
100
- this.pointerScreenPos.x,
101
- this.pointerScreenPos.y
102
- );
196
+ const w = this.camera.screenToWorld(primary.screenPos.x, primary.screenPos.y);
103
197
  return new Vec2(w.x, w.y);
104
198
  }
105
- return this.pointerScreenPos;
199
+ return primary.screenPos;
106
200
  }
107
- /** Raw pointer position in screen coordinates. */
201
+ /** Primary pointer's raw position in screen coordinates, or `Vec2.ZERO` when no pointer is tracked. */
108
202
  getPointerScreenPosition() {
109
- return this.pointerScreenPos;
203
+ const primary = this.getPrimaryPointer();
204
+ return primary ? primary.screenPos : Vec2.ZERO;
110
205
  }
111
- /** Whether any pointer button is currently held. */
206
+ /** Whether the primary pointer has any button held. */
112
207
  isPointerDown() {
113
- return this.pointerDownState;
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;
114
395
  }
115
396
  // -- Runtime action map management --
116
397
  /** Replace the entire action map and store it as the default for {@link resetBindings}. */
@@ -306,44 +587,432 @@ var InputManager = class {
306
587
  resolve(null);
307
588
  }
308
589
  }
309
- /** Public wrapper for synthetic key-down injection. */
590
+ /** Public wrapper for synthetic key-down injection. Applies sync. */
310
591
  fireKeyDown(code) {
311
- this._onKeyDown(code);
592
+ this._applyKeyDown(code);
312
593
  }
313
- /** Public wrapper for synthetic key-up injection. */
594
+ /** Public wrapper for synthetic key-up injection. Applies sync. */
314
595
  fireKeyUp(code) {
315
- this._onKeyUp(code);
596
+ this._applyKeyUp(code);
316
597
  }
317
- /** Public wrapper for synthetic pointer movement. */
318
- firePointerMove(screenX, screenY) {
319
- this._onPointerMove(screenX, screenY);
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));
320
605
  }
321
- /** Public wrapper for synthetic pointer-button presses. */
322
- firePointerDown(button = 0) {
323
- this._onPointerDown();
324
- this.pressedMouseButtons.add(button);
325
- if (button === 0) this._onKeyDown("MouseLeft");
326
- if (button === 1) this._onKeyDown("MouseMiddle");
327
- if (button === 2) this._onKeyDown("MouseRight");
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
+ );
328
622
  }
329
623
  /** Public wrapper for synthetic pointer-button releases. */
330
- firePointerUp(button = 0) {
331
- this.pressedMouseButtons.delete(button);
332
- this.pointerDownState = this.pressedMouseButtons.size > 0;
333
- if (!this.pointerDownState) {
334
- this._onPointerUp();
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;
335
714
  }
336
- if (button === 0) this._onKeyUp("MouseLeft");
337
- if (button === 1) this._onKeyUp("MouseMiddle");
338
- if (button === 2) this._onKeyUp("MouseRight");
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);
339
723
  }
340
- /** Store synthetic gamepad button state. */
341
- fireGamepadButton(idx, pressed) {
342
- this.gamepadButtons.set(idx, pressed);
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;
343
975
  }
344
- /** Store synthetic gamepad axis state. */
345
- fireGamepadAxis(idx, value) {
346
- this.gamepadAxes.set(idx, value);
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
+ }
347
1016
  }
348
1017
  /** Inject a one-frame synthetic action pulse. */
349
1018
  fireAction(name) {
@@ -352,58 +1021,268 @@ var InputManager = class {
352
1021
  }
353
1022
  this.syntheticPressedActions.add(name);
354
1023
  this.syntheticActionStarts.set(name, this.elapsedMs);
1024
+ this.notifyActionListeners(this.actionListeners, name);
355
1025
  }
356
1026
  /** Release all synthetic and physical input state. */
357
1027
  clearAll() {
358
1028
  for (const code of [...this.pressedKeys]) {
359
- this._onKeyUp(code);
1029
+ this._applyKeyUp(code);
360
1030
  }
361
1031
  this.justPressedKeys.clear();
362
1032
  this.justReleasedKeys.clear();
363
1033
  this.holdStart.clear();
364
1034
  this.syntheticPressedActions.clear();
365
1035
  this.syntheticActionStarts.clear();
366
- this.pressedMouseButtons.clear();
367
- this.pointerDownState = false;
368
- this.gamepadButtons.clear();
369
- this.gamepadAxes.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();
370
1046
  }
371
- /** Release any pressed pointer buttons without touching keyboard state. */
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
+ */
372
1052
  clearPointerButtons() {
373
- for (const button of [...this.pressedMouseButtons]) {
374
- if (button === 0) this._onKeyUp("MouseLeft");
375
- if (button === 1) this._onKeyUp("MouseMiddle");
376
- if (button === 2) this._onKeyUp("MouseRight");
1053
+ for (const button of [...this.mouseButtonAggregate]) {
1054
+ const code = MOUSE_BUTTON_CODES[button];
1055
+ if (code) this._applyKeyUp(code);
377
1056
  }
378
- this.pressedMouseButtons.clear();
379
- this.pointerDownState = false;
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
+ );
380
1064
  }
381
1065
  /** Snapshot of current held input state for inspector tooling. */
382
1066
  snapshotState() {
383
1067
  const cmp = /* @__PURE__ */ __name((a, b) => a < b ? -1 : a > b ? 1 : 0, "cmp");
384
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"));
385
1071
  const actions = this.getActionNames().filter((action) => this.isPressed(action)).sort(cmp);
386
- const buttons = [...this.pressedMouseButtons].sort((a, b) => a - b);
387
- const pressedButtons = [...this.gamepadButtons.entries()].filter(([, pressed]) => pressed).map(([idx]) => idx).sort((a, b) => a - b);
388
- const axes = [...this.gamepadAxes.entries()].filter(([, value]) => value !== 0).sort(([a], [b]) => a - b).map(([index, value]) => ({ index, value }));
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));
389
1086
  return {
390
- keys,
1087
+ keys: nonGamepadKeys,
391
1088
  actions,
392
1089
  mouse: {
393
- x: this.pointerScreenPos.x,
394
- y: this.pointerScreenPos.y,
395
- buttons,
396
- down: this.pointerDownState
1090
+ x: primary?.screenPos.x ?? 0,
1091
+ y: primary?.screenPos.y ?? 0,
1092
+ buttons: aggregateButtons,
1093
+ down: this.mouseButtonAggregate.size > 0
397
1094
  },
1095
+ pointers,
398
1096
  gamepad: {
399
- buttons: pressedButtons,
1097
+ buttons: gamepadButtons,
400
1098
  axes
401
1099
  }
402
1100
  };
403
1101
  }
404
- // -- Internal methods (called by InputPlugin / Systems) --
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
+ }
405
1111
  /** @internal */
406
- _onKeyDown(code) {
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) {
407
1286
  if (this.listenResolve) {
408
1287
  const resolve = this.listenResolve;
409
1288
  this.listenResolve = null;
@@ -414,27 +1293,190 @@ var InputManager = class {
414
1293
  this.pressedKeys.add(code);
415
1294
  this.justPressedKeys.add(code);
416
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
+ }
417
1300
  }
418
1301
  }
419
- /** @internal */
420
- _onKeyUp(code) {
1302
+ /**
1303
+ * @internal Synthetic key-up. DOM-originated events must use
1304
+ * {@link _enqueueKeyUp}.
1305
+ */
1306
+ _applyKeyUp(code) {
421
1307
  if (this.pressedKeys.has(code)) {
422
1308
  this.pressedKeys.delete(code);
423
1309
  this.justReleasedKeys.add(code);
424
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
+ }
425
1315
  }
426
1316
  }
427
- /** @internal */
428
- _onPointerMove(screenX, screenY) {
429
- this.pointerScreenPos = new Vec2(screenX, screenY);
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);
430
1325
  }
431
- /** @internal */
432
- _onPointerDown() {
433
- this.pointerDownState = true;
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);
434
1342
  }
435
- /** @internal */
436
- _onPointerUp() {
437
- this.pointerDownState = false;
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;
438
1480
  }
439
1481
  /** @internal Clear per-frame justPressed/justReleased flags. */
440
1482
  _clearFrameState() {
@@ -442,6 +1484,7 @@ var InputManager = class {
442
1484
  this.justReleasedKeys.clear();
443
1485
  this.syntheticPressedActions.clear();
444
1486
  this.syntheticActionStarts.clear();
1487
+ this.consumedWheelThisFrame = false;
445
1488
  }
446
1489
  /** Set camera for pointer world-coord conversion. */
447
1490
  setCamera(camera) {
@@ -459,6 +1502,31 @@ var InputManager = class {
459
1502
  _advanceTime(dtMs) {
460
1503
  this.elapsedMs += dtMs;
461
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
+ }
462
1530
  };
463
1531
 
464
1532
  // src/InputPollSystem.ts
@@ -470,7 +1538,12 @@ var InputPollSystem = class extends System {
470
1538
  phase = Phase.EarlyUpdate;
471
1539
  priority = -100;
472
1540
  update(dt) {
473
- this.use(InputManagerKey)._advanceTime(dt);
1541
+ const manager = this.use(InputManagerKey);
1542
+ manager._drainInputQueue();
1543
+ manager._advanceTime(dt);
1544
+ if (manager.isPollingEnabled()) {
1545
+ manager._pollGamepads();
1546
+ }
474
1547
  }
475
1548
  };
476
1549
 
@@ -525,11 +1598,6 @@ var InputDebugContributor = class {
525
1598
  };
526
1599
 
527
1600
  // src/InputPlugin.ts
528
- var MOUSE_BUTTON_MAP = {
529
- 0: "MouseLeft",
530
- 1: "MouseMiddle",
531
- 2: "MouseRight"
532
- };
533
1601
  var InputPlugin = class {
534
1602
  static {
535
1603
  __name(this, "InputPlugin");
@@ -552,9 +1620,19 @@ var InputPlugin = class {
552
1620
  if (this.config.groups) {
553
1621
  this.manager.setGroups(this.config.groups);
554
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
+ }
555
1632
  const rendererKey = this.config.rendererKey ?? RendererAdapterKey;
556
1633
  const renderer = context.tryResolve(rendererKey);
557
1634
  const pointerTarget = this.config.target ?? renderer?.canvas ?? document;
1635
+ this.manager._setRenderer(renderer ?? null);
558
1636
  const coordinateElement = renderer?.canvas ?? this.config.target ?? null;
559
1637
  const mapPointer = /* @__PURE__ */ __name((cssX, cssY) => renderer?.canvasToVirtual?.(cssX, cssY) ?? { x: cssX, y: cssY }, "mapPointer");
560
1638
  const preventSet = new Set(this.config.preventDefaultKeys ?? []);
@@ -562,12 +1640,12 @@ var InputPlugin = class {
562
1640
  const ke = e;
563
1641
  if (ke.repeat) return;
564
1642
  if (preventSet.has(ke.code)) ke.preventDefault();
565
- this.manager._onKeyDown(ke.code);
1643
+ this.manager._enqueueKeyDown(ke.code);
566
1644
  }, "onKeyDown");
567
1645
  const onKeyUp = /* @__PURE__ */ __name((e) => {
568
1646
  const ke = e;
569
1647
  if (preventSet.has(ke.code)) ke.preventDefault();
570
- this.manager._onKeyUp(ke.code);
1648
+ this.manager._enqueueKeyUp(ke.code);
571
1649
  }, "onKeyUp");
572
1650
  window.addEventListener("keydown", onKeyDown);
573
1651
  window.addEventListener("keyup", onKeyUp);
@@ -575,8 +1653,7 @@ var InputPlugin = class {
575
1653
  () => window.removeEventListener("keydown", onKeyDown),
576
1654
  () => window.removeEventListener("keyup", onKeyUp)
577
1655
  );
578
- const onPointerMove = /* @__PURE__ */ __name((e) => {
579
- const pe = e;
1656
+ const buildInfo = /* @__PURE__ */ __name((pe) => {
580
1657
  let cssX;
581
1658
  let cssY;
582
1659
  if (coordinateElement) {
@@ -588,39 +1665,96 @@ var InputPlugin = class {
588
1665
  cssY = pe.clientY;
589
1666
  }
590
1667
  const mapped = mapPointer(cssX, cssY);
591
- this.manager._onPointerMove(mapped.x, mapped.y);
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));
592
1681
  }, "onPointerMove");
593
1682
  const onPointerDown = /* @__PURE__ */ __name((e) => {
594
- const pe = e;
595
- const button = pe.button;
596
- if (button in MOUSE_BUTTON_MAP) {
597
- this.manager.firePointerDown(button);
598
- } else {
599
- this.manager._onPointerDown();
600
- }
1683
+ this.manager._enqueuePointerDown(buildInfo(e));
601
1684
  }, "onPointerDown");
602
1685
  const onPointerUp = /* @__PURE__ */ __name((e) => {
603
- const pe = e;
604
- const button = pe.button;
605
- if (button in MOUSE_BUTTON_MAP) {
606
- this.manager.firePointerUp(button);
607
- } else {
608
- this.manager._onPointerUp();
609
- }
1686
+ this.manager._enqueuePointerUp(buildInfo(e));
610
1687
  }, "onPointerUp");
611
- const onPointerCancel = /* @__PURE__ */ __name(() => {
612
- this.manager.clearPointerButtons();
1688
+ const onPointerCancel = /* @__PURE__ */ __name((e) => {
1689
+ const pe = e;
1690
+ this.manager._enqueuePointerCancel(pe.pointerId);
613
1691
  }, "onPointerCancel");
1692
+ const onPointerLeave = /* @__PURE__ */ __name((e) => {
1693
+ const pe = e;
1694
+ this.manager._enqueuePointerCancel(pe.pointerId);
1695
+ }, "onPointerLeave");
614
1696
  pointerTarget.addEventListener("pointerdown", onPointerDown);
615
1697
  window.addEventListener("pointermove", onPointerMove);
616
1698
  window.addEventListener("pointerup", onPointerUp);
617
1699
  window.addEventListener("pointercancel", onPointerCancel);
1700
+ pointerTarget.addEventListener("pointerleave", onPointerLeave);
618
1701
  this.cleanupFns.push(
619
1702
  () => pointerTarget.removeEventListener("pointerdown", onPointerDown),
620
1703
  () => window.removeEventListener("pointermove", onPointerMove),
621
1704
  () => window.removeEventListener("pointerup", onPointerUp),
622
- () => 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
623
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
+ }
624
1758
  context.register(InputManagerKey, this.manager);
625
1759
  }
626
1760
  registerSystems(scheduler) {
@@ -756,10 +1890,33 @@ var KEY_DISPLAY_NAMES = {
756
1890
  // Mouse buttons (synthetic codes from InputPlugin)
757
1891
  MouseLeft: "Left Click",
758
1892
  MouseMiddle: "Middle Click",
759
- 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"
760
1912
  };
1913
+ var GAMEPAD_BUTTON_FALLBACK = /^GamepadButton(\d+)$/;
761
1914
  function getKeyDisplayName(code) {
762
- return KEY_DISPLAY_NAMES[code] ?? code;
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;
763
1920
  }
764
1921
  __name(getKeyDisplayName, "getKeyDisplayName");
765
1922
  export {