@gjsify/gamepad 0.1.9
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/lib/esm/axis-mapping.js +35 -0
- package/lib/esm/button-mapping.js +92 -0
- package/lib/esm/gamepad-button.js +16 -0
- package/lib/esm/gamepad-event.js +14 -0
- package/lib/esm/gamepad-manager.js +223 -0
- package/lib/esm/gamepad.js +26 -0
- package/lib/esm/haptic-actuator.js +27 -0
- package/lib/esm/index.js +23 -0
- package/lib/esm/register.js +10 -0
- package/lib/types/axis-mapping.d.ts +32 -0
- package/lib/types/button-mapping.d.ts +55 -0
- package/lib/types/gamepad-button.d.ts +11 -0
- package/lib/types/gamepad-event.d.ts +17 -0
- package/lib/types/gamepad-manager.d.ts +37 -0
- package/lib/types/gamepad.d.ts +45 -0
- package/lib/types/gamepad.spec.d.ts +2 -0
- package/lib/types/haptic-actuator.d.ts +16 -0
- package/lib/types/index.d.ts +9 -0
- package/lib/types/register.d.ts +1 -0
- package/package.json +50 -0
- package/src/axis-mapping.ts +50 -0
- package/src/button-mapping.ts +87 -0
- package/src/gamepad-button.ts +22 -0
- package/src/gamepad-event.ts +29 -0
- package/src/gamepad-manager.ts +299 -0
- package/src/gamepad.spec.ts +191 -0
- package/src/gamepad.ts +66 -0
- package/src/haptic-actuator.ts +49 -0
- package/src/index.ts +18 -0
- package/src/register.ts +21 -0
- package/src/test.mts +5 -0
- package/tsconfig.json +33 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type Manette from '@girs/manette-0.2';
|
|
2
|
+
import type { GamepadHapticActuator, GamepadHapticEffectType, GamepadEffectParameters, GamepadHapticsResult } from './gamepad.js';
|
|
3
|
+
/**
|
|
4
|
+
* Wraps libmanette's rumble support as a W3C GamepadHapticActuator.
|
|
5
|
+
*
|
|
6
|
+
* libmanette supports dual-rumble with strong/weak magnitude control
|
|
7
|
+
* via `Device.rumble(strong_magnitude, weak_magnitude, milliseconds)`.
|
|
8
|
+
* Magnitudes are in the range 0–65535 (uint16).
|
|
9
|
+
*/
|
|
10
|
+
export declare class ManetteHapticActuator implements GamepadHapticActuator {
|
|
11
|
+
readonly effects: readonly GamepadHapticEffectType[];
|
|
12
|
+
private _device;
|
|
13
|
+
constructor(device: Manette.Device);
|
|
14
|
+
playEffect(type: GamepadHapticEffectType, params?: GamepadEffectParameters): Promise<GamepadHapticsResult>;
|
|
15
|
+
reset(): Promise<GamepadHapticsResult>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { GamepadButton } from './gamepad-button.js';
|
|
2
|
+
export { Gamepad } from './gamepad.js';
|
|
3
|
+
export type { GamepadMappingType, GamepadHapticActuator, GamepadHapticEffectType, GamepadHapticsResult, GamepadEffectParameters, } from './gamepad.js';
|
|
4
|
+
export { GamepadEvent } from './gamepad-event.js';
|
|
5
|
+
export type { GamepadEventInit } from './gamepad-event.js';
|
|
6
|
+
export { GamepadManager } from './gamepad-manager.js';
|
|
7
|
+
export { ManetteHapticActuator } from './haptic-actuator.js';
|
|
8
|
+
export { MANETTE_TO_W3C_BUTTON, ManetteButton, W3CButton, W3C_BUTTON_COUNT } from './button-mapping.js';
|
|
9
|
+
export { MANETTE_TO_W3C_AXIS, ManetteAxis, W3CAxis, W3C_AXIS_COUNT, TRIGGER_PRESS_THRESHOLD } from './axis-mapping.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gjsify/gamepad",
|
|
3
|
+
"version": "0.1.9",
|
|
4
|
+
"description": "Gamepad Web API for GJS using libmanette as backend",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"module": "lib/esm/index.js",
|
|
7
|
+
"types": "lib/types/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/types/index.d.ts",
|
|
11
|
+
"default": "./lib/esm/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./register": {
|
|
14
|
+
"types": "./lib/types/register.d.ts",
|
|
15
|
+
"default": "./lib/esm/register.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"sideEffects": [
|
|
19
|
+
"./lib/esm/register.js"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"clear": "rm -rf lib tsconfig.tsbuildinfo tsconfig.types.tsbuildinfo test.gjs.mjs || exit 0",
|
|
23
|
+
"check": "tsc --noEmit",
|
|
24
|
+
"build": "yarn build:gjsify && yarn build:types",
|
|
25
|
+
"build:gjsify": "gjsify build --library 'src/**/*.{ts,js}' --exclude 'src/**/*.spec.{mts,ts}' 'src/test.{mts,ts}'",
|
|
26
|
+
"build:types": "tsc",
|
|
27
|
+
"build:test": "yarn build:test:gjs",
|
|
28
|
+
"build:test:gjs": "gjsify build src/test.mts --app gjs --outfile test.gjs.mjs",
|
|
29
|
+
"test": "yarn build:gjsify && yarn build:test && yarn test:gjs",
|
|
30
|
+
"test:gjs": "gjs -m test.gjs.mjs"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"gjs",
|
|
34
|
+
"gamepad",
|
|
35
|
+
"libmanette",
|
|
36
|
+
"controller"
|
|
37
|
+
],
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@gjsify/dom-events": "^0.1.9"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@girs/gjs": "^4.0.0-rc.2",
|
|
43
|
+
"@girs/glib-2.0": "^2.88.0-4.0.0-rc.2",
|
|
44
|
+
"@girs/manette-0.2": "^0.2.13-4.0.0-rc.2",
|
|
45
|
+
"@gjsify/cli": "^0.1.9",
|
|
46
|
+
"@gjsify/unit": "^0.1.9",
|
|
47
|
+
"@types/node": "^25.6.0",
|
|
48
|
+
"typescript": "^6.0.2"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Gamepad Web API — axis mapping from libmanette to W3C standard gamepad layout
|
|
2
|
+
// Reference: https://w3c.github.io/gamepad/#remapping
|
|
3
|
+
//
|
|
4
|
+
// NOTE: Unlike buttons (which use Linux BTN_* hardware codes), libmanette 0.2
|
|
5
|
+
// axis events use SDL logical indices: 0=leftx, 1=lefty, 2=rightx, 3=righty,
|
|
6
|
+
// 4=lefttrigger, 5=righttrigger. The hardware ABS_* code is available via
|
|
7
|
+
// event.get_hardware_code() but the main axis index from get_absolute() is
|
|
8
|
+
// the SDL-mapped logical index.
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* SDL logical axis indices as reported by Manette.Event.get_absolute().
|
|
12
|
+
* These are the SDL gamepad mapping indices, NOT Linux ABS_* hardware codes.
|
|
13
|
+
*/
|
|
14
|
+
export const ManetteAxis = {
|
|
15
|
+
LEFT_X: 0, // SDL leftx
|
|
16
|
+
LEFT_Y: 1, // SDL lefty
|
|
17
|
+
RIGHT_X: 2, // SDL rightx
|
|
18
|
+
RIGHT_Y: 3, // SDL righty
|
|
19
|
+
LEFT_TRIGGER: 4, // SDL lefttrigger
|
|
20
|
+
RIGHT_TRIGGER: 5, // SDL righttrigger
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* W3C standard gamepad axis indices.
|
|
25
|
+
* https://w3c.github.io/gamepad/#remapping
|
|
26
|
+
*/
|
|
27
|
+
export const W3CAxis = {
|
|
28
|
+
LEFT_STICK_X: 0,
|
|
29
|
+
LEFT_STICK_Y: 1,
|
|
30
|
+
RIGHT_STICK_X: 2,
|
|
31
|
+
RIGHT_STICK_Y: 3,
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
/** Total number of axes in the W3C standard mapping. */
|
|
35
|
+
export const W3C_AXIS_COUNT = 4;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Maps SDL logical axis → W3C axis index.
|
|
39
|
+
* Stick axes (0–3) map 1:1. Trigger axes (4–5) are NOT in this map —
|
|
40
|
+
* they are handled separately in gamepad-manager as buttons[6]/buttons[7].
|
|
41
|
+
*/
|
|
42
|
+
export const MANETTE_TO_W3C_AXIS: ReadonlyMap<number, number> = new Map([
|
|
43
|
+
[ManetteAxis.LEFT_X, W3CAxis.LEFT_STICK_X],
|
|
44
|
+
[ManetteAxis.LEFT_Y, W3CAxis.LEFT_STICK_Y],
|
|
45
|
+
[ManetteAxis.RIGHT_X, W3CAxis.RIGHT_STICK_X],
|
|
46
|
+
[ManetteAxis.RIGHT_Y, W3CAxis.RIGHT_STICK_Y],
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
/** Threshold above which an analog trigger is considered "pressed". */
|
|
50
|
+
export const TRIGGER_PRESS_THRESHOLD = 0.5;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Gamepad Web API — button mapping from libmanette to W3C standard gamepad layout
|
|
2
|
+
// Reference: https://w3c.github.io/gamepad/#remapping
|
|
3
|
+
//
|
|
4
|
+
// IMPORTANT: libmanette 0.2 delivers Linux input event codes (BTN_*) in
|
|
5
|
+
// Event.get_button(), NOT the Manette.Button enum values from the docs.
|
|
6
|
+
// The enum (SOUTH=5, EAST=7, …) describes the semantic meaning, but the
|
|
7
|
+
// actual values transmitted over signals are the kernel BTN_* constants.
|
|
8
|
+
// Reference: linux/input-event-codes.h
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Linux BTN_* input event codes as reported by Manette.Event.get_button().
|
|
12
|
+
* These are the actual values libmanette 0.2 passes in button-press/release signals.
|
|
13
|
+
*/
|
|
14
|
+
export const LinuxButton = {
|
|
15
|
+
BTN_SOUTH: 304, // 0x130 — A (Xbox), B (Nintendo), Cross (PlayStation)
|
|
16
|
+
BTN_EAST: 305, // 0x131 — B (Xbox), A (Nintendo), Circle (PlayStation)
|
|
17
|
+
BTN_C: 306, // 0x132
|
|
18
|
+
BTN_NORTH: 307, // 0x133 — Y (Xbox), X (Nintendo), Triangle (PlayStation)
|
|
19
|
+
BTN_WEST: 308, // 0x134 — X (Xbox), Y (Nintendo), Square (PlayStation)
|
|
20
|
+
BTN_Z: 309, // 0x135
|
|
21
|
+
BTN_TL: 310, // 0x136 — Left shoulder (L, LB)
|
|
22
|
+
BTN_TR: 311, // 0x137 — Right shoulder (R, RB)
|
|
23
|
+
BTN_TL2: 312, // 0x138 — Left trigger (LT, L2)
|
|
24
|
+
BTN_TR2: 313, // 0x139 — Right trigger (RT, R2)
|
|
25
|
+
BTN_SELECT: 314, // 0x13a — Select / Back / Share
|
|
26
|
+
BTN_START: 315, // 0x13b — Start / Menu / Options
|
|
27
|
+
BTN_MODE: 316, // 0x13c — Home / Guide / PS
|
|
28
|
+
BTN_THUMBL: 317, // 0x13d — Left stick click (L3)
|
|
29
|
+
BTN_THUMBR: 318, // 0x13e — Right stick click (R3)
|
|
30
|
+
BTN_DPAD_UP: 544, // 0x220
|
|
31
|
+
BTN_DPAD_DOWN: 545, // 0x221
|
|
32
|
+
BTN_DPAD_LEFT: 546, // 0x222
|
|
33
|
+
BTN_DPAD_RIGHT:547, // 0x223
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* W3C standard gamepad button indices.
|
|
38
|
+
* https://w3c.github.io/gamepad/#remapping
|
|
39
|
+
*/
|
|
40
|
+
export const W3CButton = {
|
|
41
|
+
FACE_1: 0, // A (Xbox), B (Nintendo), Cross (PlayStation)
|
|
42
|
+
FACE_2: 1, // B (Xbox), A (Nintendo), Circle (PlayStation)
|
|
43
|
+
FACE_3: 2, // X (Xbox), Y (Nintendo), Square (PlayStation)
|
|
44
|
+
FACE_4: 3, // Y (Xbox), X (Nintendo), Triangle (PlayStation)
|
|
45
|
+
LEFT_BUMPER: 4,
|
|
46
|
+
RIGHT_BUMPER: 5,
|
|
47
|
+
LEFT_TRIGGER: 6, // Analog trigger — also populated from axis events
|
|
48
|
+
RIGHT_TRIGGER: 7, // Analog trigger — also populated from axis events
|
|
49
|
+
SELECT: 8,
|
|
50
|
+
START: 9,
|
|
51
|
+
LEFT_STICK: 10,
|
|
52
|
+
RIGHT_STICK: 11,
|
|
53
|
+
DPAD_UP: 12,
|
|
54
|
+
DPAD_DOWN: 13,
|
|
55
|
+
DPAD_LEFT: 14,
|
|
56
|
+
DPAD_RIGHT: 15,
|
|
57
|
+
HOME: 16,
|
|
58
|
+
} as const;
|
|
59
|
+
|
|
60
|
+
/** Total number of buttons in the W3C standard mapping. */
|
|
61
|
+
export const W3C_BUTTON_COUNT = 17;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Maps Linux BTN_* code → W3C standard button index.
|
|
65
|
+
*/
|
|
66
|
+
export const MANETTE_TO_W3C_BUTTON: ReadonlyMap<number, number> = new Map([
|
|
67
|
+
[LinuxButton.BTN_SOUTH, W3CButton.FACE_1],
|
|
68
|
+
[LinuxButton.BTN_EAST, W3CButton.FACE_2],
|
|
69
|
+
[LinuxButton.BTN_WEST, W3CButton.FACE_3],
|
|
70
|
+
[LinuxButton.BTN_NORTH, W3CButton.FACE_4],
|
|
71
|
+
[LinuxButton.BTN_TL, W3CButton.LEFT_BUMPER],
|
|
72
|
+
[LinuxButton.BTN_TR, W3CButton.RIGHT_BUMPER],
|
|
73
|
+
[LinuxButton.BTN_TL2, W3CButton.LEFT_TRIGGER],
|
|
74
|
+
[LinuxButton.BTN_TR2, W3CButton.RIGHT_TRIGGER],
|
|
75
|
+
[LinuxButton.BTN_SELECT, W3CButton.SELECT],
|
|
76
|
+
[LinuxButton.BTN_START, W3CButton.START],
|
|
77
|
+
[LinuxButton.BTN_THUMBL, W3CButton.LEFT_STICK],
|
|
78
|
+
[LinuxButton.BTN_THUMBR, W3CButton.RIGHT_STICK],
|
|
79
|
+
[LinuxButton.BTN_DPAD_UP, W3CButton.DPAD_UP],
|
|
80
|
+
[LinuxButton.BTN_DPAD_DOWN, W3CButton.DPAD_DOWN],
|
|
81
|
+
[LinuxButton.BTN_DPAD_LEFT, W3CButton.DPAD_LEFT],
|
|
82
|
+
[LinuxButton.BTN_DPAD_RIGHT, W3CButton.DPAD_RIGHT],
|
|
83
|
+
[LinuxButton.BTN_MODE, W3CButton.HOME],
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
// Keep the old name exported for backward compat in tests/examples
|
|
87
|
+
export { LinuxButton as ManetteButton };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Gamepad Web API — GamepadButton
|
|
2
|
+
// Reference: https://w3c.github.io/gamepad/#dom-gamepadbutton
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Represents the state of a single button on a gamepad.
|
|
6
|
+
* https://w3c.github.io/gamepad/#dom-gamepadbutton
|
|
7
|
+
*/
|
|
8
|
+
export class GamepadButton {
|
|
9
|
+
pressed: boolean;
|
|
10
|
+
touched: boolean;
|
|
11
|
+
value: number;
|
|
12
|
+
|
|
13
|
+
constructor(pressed = false, touched = false, value = 0) {
|
|
14
|
+
this.pressed = pressed;
|
|
15
|
+
this.touched = touched;
|
|
16
|
+
this.value = value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get [Symbol.toStringTag]() {
|
|
20
|
+
return 'GamepadButton';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Gamepad Web API — GamepadEvent
|
|
2
|
+
// Reference: https://w3c.github.io/gamepad/#dom-gamepadevent
|
|
3
|
+
|
|
4
|
+
import { Event } from '@gjsify/dom-events';
|
|
5
|
+
import type { Gamepad } from './gamepad.js';
|
|
6
|
+
|
|
7
|
+
export interface GamepadEventInit {
|
|
8
|
+
gamepad: Gamepad;
|
|
9
|
+
bubbles?: boolean;
|
|
10
|
+
cancelable?: boolean;
|
|
11
|
+
composed?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Fired on the Window when a gamepad is connected or disconnected.
|
|
16
|
+
* https://w3c.github.io/gamepad/#dom-gamepadevent
|
|
17
|
+
*/
|
|
18
|
+
export class GamepadEvent extends Event {
|
|
19
|
+
readonly gamepad: Gamepad;
|
|
20
|
+
|
|
21
|
+
constructor(type: string, eventInitDict: GamepadEventInit) {
|
|
22
|
+
super(type, eventInitDict);
|
|
23
|
+
this.gamepad = eventInitDict.gamepad;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get [Symbol.toStringTag]() {
|
|
27
|
+
return 'GamepadEvent';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
// Gamepad Web API — GamepadManager
|
|
2
|
+
// Bridges libmanette's event-driven model to the W3C polling-based Gamepad API.
|
|
3
|
+
// Reference: https://w3c.github.io/gamepad/
|
|
4
|
+
// Reimplemented for GJS using libmanette (gi://Manette)
|
|
5
|
+
|
|
6
|
+
import type Manette from '@girs/manette-0.2';
|
|
7
|
+
import { GamepadButton } from './gamepad-button.js';
|
|
8
|
+
import { Gamepad } from './gamepad.js';
|
|
9
|
+
import { GamepadEvent } from './gamepad-event.js';
|
|
10
|
+
import { ManetteHapticActuator } from './haptic-actuator.js';
|
|
11
|
+
import { MANETTE_TO_W3C_BUTTON, W3C_BUTTON_COUNT } from './button-mapping.js';
|
|
12
|
+
import { MANETTE_TO_W3C_AXIS, ManetteAxis, W3C_AXIS_COUNT, TRIGGER_PRESS_THRESHOLD } from './axis-mapping.js';
|
|
13
|
+
import { W3CButton } from './button-mapping.js';
|
|
14
|
+
|
|
15
|
+
/** Internal mutable state for a single connected gamepad. */
|
|
16
|
+
interface DeviceState {
|
|
17
|
+
device: Manette.Device;
|
|
18
|
+
index: number;
|
|
19
|
+
connected: boolean;
|
|
20
|
+
timestamp: number;
|
|
21
|
+
buttons: Float64Array;
|
|
22
|
+
buttonsPressed: boolean[];
|
|
23
|
+
axes: Float64Array;
|
|
24
|
+
hapticActuator: ManetteHapticActuator;
|
|
25
|
+
signalIds: number[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const MAX_GAMEPADS = 4;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Singleton manager that wraps Manette.Monitor and maintains gamepad state.
|
|
32
|
+
*
|
|
33
|
+
* libmanette fires GObject signals on button/axis changes. This manager
|
|
34
|
+
* caches the latest state so that `getGamepads()` can return a snapshot
|
|
35
|
+
* matching the W3C Gamepad API's polling model.
|
|
36
|
+
*/
|
|
37
|
+
export class GamepadManager {
|
|
38
|
+
private _monitor: Manette.Monitor | null = null;
|
|
39
|
+
private _slots: (DeviceState | null)[] = new Array(MAX_GAMEPADS).fill(null);
|
|
40
|
+
private _monitorSignalIds: number[] = [];
|
|
41
|
+
private _ManetteModule: typeof Manette | null = null;
|
|
42
|
+
private _initPromise: Promise<void> | null = null;
|
|
43
|
+
private _initialized = false;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Lazily initialize the Manette.Monitor.
|
|
47
|
+
* Called on first `getGamepads()` invocation.
|
|
48
|
+
*/
|
|
49
|
+
private _ensureInit(): void {
|
|
50
|
+
if (this._initialized) return;
|
|
51
|
+
if (this._initPromise) return;
|
|
52
|
+
|
|
53
|
+
this._initPromise = this._init();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private async _init(): Promise<void> {
|
|
57
|
+
try {
|
|
58
|
+
const mod = await import('gi://Manette');
|
|
59
|
+
this._ManetteModule = mod.default;
|
|
60
|
+
} catch {
|
|
61
|
+
// libmanette not available — getGamepads() will return empty array
|
|
62
|
+
this._initialized = true;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const monitor = new this._ManetteModule.Monitor();
|
|
67
|
+
this._monitor = monitor;
|
|
68
|
+
|
|
69
|
+
// Enumerate already-connected devices
|
|
70
|
+
const iter = monitor.iterate();
|
|
71
|
+
let result = iter.next();
|
|
72
|
+
while (result[0]) {
|
|
73
|
+
const device = result[1];
|
|
74
|
+
if (device) {
|
|
75
|
+
this._onDeviceConnected(device);
|
|
76
|
+
}
|
|
77
|
+
result = iter.next();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Listen for future connect/disconnect
|
|
81
|
+
this._monitorSignalIds.push(
|
|
82
|
+
monitor.connect('device-connected', (_monitor: Manette.Monitor, device: Manette.Device) => {
|
|
83
|
+
this._onDeviceConnected(device);
|
|
84
|
+
}),
|
|
85
|
+
monitor.connect('device-disconnected', (_monitor: Manette.Monitor, device: Manette.Device) => {
|
|
86
|
+
this._onDeviceDisconnected(device);
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
this._initialized = true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private _onDeviceConnected(device: Manette.Device): void {
|
|
94
|
+
// Find a free slot
|
|
95
|
+
let slotIndex = -1;
|
|
96
|
+
for (let i = 0; i < MAX_GAMEPADS; i++) {
|
|
97
|
+
if (this._slots[i] === null) {
|
|
98
|
+
slotIndex = i;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (slotIndex === -1) return; // All slots occupied
|
|
103
|
+
|
|
104
|
+
const state: DeviceState = {
|
|
105
|
+
device,
|
|
106
|
+
index: slotIndex,
|
|
107
|
+
connected: true,
|
|
108
|
+
timestamp: performance.now(),
|
|
109
|
+
buttons: new Float64Array(W3C_BUTTON_COUNT),
|
|
110
|
+
buttonsPressed: new Array(W3C_BUTTON_COUNT).fill(false),
|
|
111
|
+
axes: new Float64Array(W3C_AXIS_COUNT),
|
|
112
|
+
hapticActuator: new ManetteHapticActuator(device),
|
|
113
|
+
signalIds: [],
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Wire up device signals
|
|
117
|
+
state.signalIds.push(
|
|
118
|
+
device.connect('button-press-event', (_device: Manette.Device, event: Manette.Event) => {
|
|
119
|
+
this._onButtonPress(state, event);
|
|
120
|
+
}),
|
|
121
|
+
device.connect('button-release-event', (_device: Manette.Device, event: Manette.Event) => {
|
|
122
|
+
this._onButtonRelease(state, event);
|
|
123
|
+
}),
|
|
124
|
+
device.connect('absolute-axis-event', (_device: Manette.Device, event: Manette.Event) => {
|
|
125
|
+
this._onAxisChange(state, event);
|
|
126
|
+
}),
|
|
127
|
+
device.connect('hat-axis-event', (_device: Manette.Device, event: Manette.Event) => {
|
|
128
|
+
this._onHatChange(state, event);
|
|
129
|
+
}),
|
|
130
|
+
device.connect('disconnected', () => {
|
|
131
|
+
this._onDeviceDisconnected(device);
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
this._slots[slotIndex] = state;
|
|
136
|
+
|
|
137
|
+
// Dispatch gamepadconnected event
|
|
138
|
+
const snapshot = this._createSnapshot(state);
|
|
139
|
+
if (snapshot) {
|
|
140
|
+
globalThis.dispatchEvent?.(new GamepadEvent('gamepadconnected', { gamepad: snapshot }) as unknown as Event);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private _onDeviceDisconnected(device: Manette.Device): void {
|
|
145
|
+
const state = this._findStateByDevice(device);
|
|
146
|
+
if (!state) return;
|
|
147
|
+
|
|
148
|
+
// Disconnect all device signals
|
|
149
|
+
for (const id of state.signalIds) {
|
|
150
|
+
device.disconnect(id);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
state.connected = false;
|
|
154
|
+
const snapshot = this._createSnapshot(state);
|
|
155
|
+
this._slots[state.index] = null;
|
|
156
|
+
|
|
157
|
+
// Dispatch gamepaddisconnected event
|
|
158
|
+
if (snapshot) {
|
|
159
|
+
globalThis.dispatchEvent?.(new GamepadEvent('gamepaddisconnected', { gamepad: snapshot }) as unknown as Event);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private _onButtonPress(state: DeviceState, event: Manette.Event): void {
|
|
164
|
+
const [ok, button] = event.get_button();
|
|
165
|
+
if (!ok) return;
|
|
166
|
+
|
|
167
|
+
const w3cIdx = MANETTE_TO_W3C_BUTTON.get(button);
|
|
168
|
+
if (w3cIdx === undefined) return;
|
|
169
|
+
|
|
170
|
+
state.buttons[w3cIdx] = 1.0;
|
|
171
|
+
state.buttonsPressed[w3cIdx] = true;
|
|
172
|
+
state.timestamp = performance.now();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private _onButtonRelease(state: DeviceState, event: Manette.Event): void {
|
|
176
|
+
const [ok, button] = event.get_button();
|
|
177
|
+
if (!ok) return;
|
|
178
|
+
|
|
179
|
+
const w3cIdx = MANETTE_TO_W3C_BUTTON.get(button);
|
|
180
|
+
if (w3cIdx === undefined) return;
|
|
181
|
+
|
|
182
|
+
state.buttons[w3cIdx] = 0.0;
|
|
183
|
+
state.buttonsPressed[w3cIdx] = false;
|
|
184
|
+
state.timestamp = performance.now();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private _onAxisChange(state: DeviceState, event: Manette.Event): void {
|
|
188
|
+
const [ok, axis, value] = event.get_absolute();
|
|
189
|
+
if (!ok) return;
|
|
190
|
+
|
|
191
|
+
const w3cAxisIdx = MANETTE_TO_W3C_AXIS.get(axis);
|
|
192
|
+
if (w3cAxisIdx !== undefined) {
|
|
193
|
+
// Stick axis → axes array
|
|
194
|
+
state.axes[w3cAxisIdx] = value;
|
|
195
|
+
} else if (axis === ManetteAxis.LEFT_TRIGGER) {
|
|
196
|
+
// Left trigger (SDL idx 4) → buttons[6] with analog value
|
|
197
|
+
const normalized = (value + 1) / 2; // libmanette: -1..1 → 0..1
|
|
198
|
+
state.buttons[W3CButton.LEFT_TRIGGER] = normalized;
|
|
199
|
+
state.buttonsPressed[W3CButton.LEFT_TRIGGER] = normalized > TRIGGER_PRESS_THRESHOLD;
|
|
200
|
+
} else if (axis === ManetteAxis.RIGHT_TRIGGER) {
|
|
201
|
+
// Right trigger (SDL idx 5) → buttons[7] with analog value
|
|
202
|
+
const normalized = (value + 1) / 2;
|
|
203
|
+
state.buttons[W3CButton.RIGHT_TRIGGER] = normalized;
|
|
204
|
+
state.buttonsPressed[W3CButton.RIGHT_TRIGGER] = normalized > TRIGGER_PRESS_THRESHOLD;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
state.timestamp = performance.now();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private _onHatChange(state: DeviceState, event: Manette.Event): void {
|
|
211
|
+
const [ok, hatAxis, hatValue] = event.get_hat();
|
|
212
|
+
if (!ok) return;
|
|
213
|
+
|
|
214
|
+
// Hat axes: 0 = horizontal (left/right), 1 = vertical (up/down)
|
|
215
|
+
// Values: -1, 0, 1
|
|
216
|
+
if (hatAxis === 0) {
|
|
217
|
+
// Horizontal: negative = left, positive = right
|
|
218
|
+
state.buttonsPressed[W3CButton.DPAD_LEFT] = hatValue < 0;
|
|
219
|
+
state.buttons[W3CButton.DPAD_LEFT] = hatValue < 0 ? 1.0 : 0.0;
|
|
220
|
+
state.buttonsPressed[W3CButton.DPAD_RIGHT] = hatValue > 0;
|
|
221
|
+
state.buttons[W3CButton.DPAD_RIGHT] = hatValue > 0 ? 1.0 : 0.0;
|
|
222
|
+
} else if (hatAxis === 1) {
|
|
223
|
+
// Vertical: negative = up, positive = down
|
|
224
|
+
state.buttonsPressed[W3CButton.DPAD_UP] = hatValue < 0;
|
|
225
|
+
state.buttons[W3CButton.DPAD_UP] = hatValue < 0 ? 1.0 : 0.0;
|
|
226
|
+
state.buttonsPressed[W3CButton.DPAD_DOWN] = hatValue > 0;
|
|
227
|
+
state.buttons[W3CButton.DPAD_DOWN] = hatValue > 0 ? 1.0 : 0.0;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
state.timestamp = performance.now();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private _findStateByDevice(device: Manette.Device): DeviceState | null {
|
|
234
|
+
for (const state of this._slots) {
|
|
235
|
+
if (state && state.device === device) return state;
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private _createSnapshot(state: DeviceState): Gamepad {
|
|
241
|
+
const buttons: GamepadButton[] = [];
|
|
242
|
+
for (let i = 0; i < W3C_BUTTON_COUNT; i++) {
|
|
243
|
+
buttons.push(new GamepadButton(
|
|
244
|
+
state.buttonsPressed[i],
|
|
245
|
+
state.buttonsPressed[i] || state.buttons[i] > 0,
|
|
246
|
+
state.buttons[i],
|
|
247
|
+
));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return new Gamepad({
|
|
251
|
+
id: state.device.get_name() ?? `Gamepad (${state.device.get_guid()})`,
|
|
252
|
+
index: state.index,
|
|
253
|
+
connected: state.connected,
|
|
254
|
+
timestamp: state.timestamp,
|
|
255
|
+
mapping: 'standard',
|
|
256
|
+
axes: Array.from(state.axes),
|
|
257
|
+
buttons,
|
|
258
|
+
vibrationActuator: state.hapticActuator,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Returns a snapshot array matching the W3C `navigator.getGamepads()` contract.
|
|
264
|
+
* Each non-null entry is a frozen Gamepad object with current state.
|
|
265
|
+
*/
|
|
266
|
+
getGamepads(): (Gamepad | null)[] {
|
|
267
|
+
this._ensureInit();
|
|
268
|
+
|
|
269
|
+
const result: (Gamepad | null)[] = [];
|
|
270
|
+
for (let i = 0; i < MAX_GAMEPADS; i++) {
|
|
271
|
+
const state = this._slots[i];
|
|
272
|
+
result.push(state ? this._createSnapshot(state) : null);
|
|
273
|
+
}
|
|
274
|
+
return result;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Cleanup — disconnect all signal handlers. */
|
|
278
|
+
dispose(): void {
|
|
279
|
+
for (const state of this._slots) {
|
|
280
|
+
if (state) {
|
|
281
|
+
for (const id of state.signalIds) {
|
|
282
|
+
state.device.disconnect(id);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
this._slots.fill(null);
|
|
287
|
+
|
|
288
|
+
if (this._monitor) {
|
|
289
|
+
for (const id of this._monitorSignalIds) {
|
|
290
|
+
this._monitor.disconnect(id);
|
|
291
|
+
}
|
|
292
|
+
this._monitorSignalIds = [];
|
|
293
|
+
this._monitor = null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
this._initialized = false;
|
|
297
|
+
this._initPromise = null;
|
|
298
|
+
}
|
|
299
|
+
}
|