three-gamepad-controls 0.10.6 → 0.11.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.
@@ -0,0 +1,270 @@
1
+ import { GamepadManager } from "./gamepad-manager.js";
2
+ import { applyGamepadDeadzone, getGamepadButtonPressed, getGamepadButtonValue } from "./utils.js";
3
+ import { EventDispatcher } from "three";
4
+ //#region src/gamepad-input.ts
5
+ const DEFAULT_GAMEPAD_INPUT_OPTIONS = { deadzone: .1 };
6
+ /**
7
+ * Gamepad input state reader for gameplay, menus, and custom actions.
8
+ *
9
+ * Call {@link update} once per frame before reading button transitions or axes.
10
+ */
11
+ var GamepadInput = class extends EventDispatcher {
12
+ /**
13
+ * When `false`, input polling is paused.
14
+ * @default true
15
+ */
16
+ enabled = true;
17
+ #manager;
18
+ #options;
19
+ #pressedButtons;
20
+ #previousPressedButtons;
21
+ /**
22
+ * Bound browser connection listener kept so it can be removed in {@link dispose}.
23
+ */
24
+ #onGamepadConnected;
25
+ /**
26
+ * Bound browser disconnection listener kept so it can be removed in {@link dispose}.
27
+ */
28
+ #onGamepadDisconnected;
29
+ #gamepad = null;
30
+ /**
31
+ * Creates a gamepad input reader.
32
+ *
33
+ * @param options - Optional overrides for the default input behavior.
34
+ */
35
+ constructor(options) {
36
+ super();
37
+ this.#manager = new GamepadManager();
38
+ this.#options = {
39
+ ...DEFAULT_GAMEPAD_INPUT_OPTIONS,
40
+ ...options
41
+ };
42
+ this.#pressedButtons = /* @__PURE__ */ new Set();
43
+ this.#previousPressedButtons = /* @__PURE__ */ new Set();
44
+ this.#onGamepadConnected = this.#handleGamepadConnectedEvent.bind(this);
45
+ this.#onGamepadDisconnected = this.#handleGamepadDisconnectedEvent.bind(this);
46
+ window.addEventListener("gamepadconnected", this.#onGamepadConnected);
47
+ window.addEventListener("gamepaddisconnected", this.#onGamepadDisconnected);
48
+ }
49
+ /**
50
+ * Forwards a browser connection event to the active-gamepad adoption logic.
51
+ *
52
+ * @param event - Browser event containing the connected gamepad snapshot.
53
+ */
54
+ #handleGamepadConnectedEvent(event) {
55
+ this.#handleGamepadConnected(event.gamepad);
56
+ }
57
+ /**
58
+ * Forwards a browser disconnection event to active-gamepad cleanup.
59
+ *
60
+ * @param event - Browser event containing the disconnected gamepad snapshot.
61
+ */
62
+ #handleGamepadDisconnectedEvent(event) {
63
+ this.#handleGamepadDisconnected(event.gamepad);
64
+ }
65
+ /**
66
+ * The currently active gamepad, or `null` if no gamepad is connected.
67
+ *
68
+ * @returns The active gamepad snapshot, or `null`.
69
+ */
70
+ get gamepad() {
71
+ return this.#gamepad;
72
+ }
73
+ /**
74
+ * Whether a gamepad is currently active.
75
+ *
76
+ * @returns `true` when a gamepad is active, otherwise `false`.
77
+ */
78
+ get connected() {
79
+ return this.#gamepad !== null;
80
+ }
81
+ /**
82
+ * Mapping reported by the active gamepad, or `null` when none is active.
83
+ *
84
+ * @returns The active gamepad mapping, or `null`.
85
+ */
86
+ get mapping() {
87
+ return this.#gamepad?.mapping ?? null;
88
+ }
89
+ /**
90
+ * Raw active gamepad snapshot, or `null` if no gamepad is connected.
91
+ *
92
+ * @returns The raw active gamepad snapshot, or `null`.
93
+ */
94
+ get rawGamepad() {
95
+ return this.#gamepad;
96
+ }
97
+ /**
98
+ * Polls the gamepad and refreshes current and previous button state.
99
+ */
100
+ update() {
101
+ if (!this.enabled) return;
102
+ const { gamepad, connected, disconnected } = this.#manager.update();
103
+ if (connected !== null) {
104
+ this.#gamepad = gamepad;
105
+ this.#syncButtonState({ seedPrevious: true });
106
+ this.dispatchEvent({
107
+ type: "connected",
108
+ gamepad: connected
109
+ });
110
+ return;
111
+ }
112
+ if (disconnected !== null) {
113
+ this.#gamepad = null;
114
+ this.#clearButtonState();
115
+ this.dispatchEvent({
116
+ type: "disconnected",
117
+ gamepad: disconnected
118
+ });
119
+ return;
120
+ }
121
+ this.#gamepad = gamepad;
122
+ this.#syncButtonState({ seedPrevious: false });
123
+ }
124
+ /**
125
+ * Removes all window-level event listeners attached by this input reader.
126
+ */
127
+ dispose() {
128
+ window.removeEventListener("gamepadconnected", this.#onGamepadConnected);
129
+ window.removeEventListener("gamepaddisconnected", this.#onGamepadDisconnected);
130
+ this.#manager.activeGamepad = null;
131
+ this.#gamepad = null;
132
+ this.#clearButtonState();
133
+ this.enabled = false;
134
+ }
135
+ /**
136
+ * Returns whether a button is currently pressed.
137
+ *
138
+ * @param button - Button index to inspect.
139
+ * @returns `true` when the button is currently pressed, otherwise `false`.
140
+ */
141
+ isPressed(button) {
142
+ return this.#pressedButtons.has(button);
143
+ }
144
+ /**
145
+ * Returns whether a button was pressed during the latest update.
146
+ *
147
+ * @param button - Button index to inspect.
148
+ * @returns `true` only on the frame where the button transitions to pressed.
149
+ */
150
+ wasPressed(button) {
151
+ return this.#pressedButtons.has(button) && !this.#previousPressedButtons.has(button);
152
+ }
153
+ /**
154
+ * Returns whether a button was released during the latest update.
155
+ *
156
+ * @param button - Button index to inspect.
157
+ * @returns `true` only on the frame where the button transitions to released.
158
+ */
159
+ wasReleased(button) {
160
+ return !this.#pressedButtons.has(button) && this.#previousPressedButtons.has(button);
161
+ }
162
+ /**
163
+ * Returns the current analog value for a button.
164
+ *
165
+ * @param button - Button index to inspect.
166
+ * @returns The button value, `1` for pressed digital buttons, or `0`.
167
+ */
168
+ buttonValue(button) {
169
+ if (this.#gamepad === null) return 0;
170
+ return getGamepadButtonValue(this.#gamepad, button);
171
+ }
172
+ /**
173
+ * Returns the current value of an axis after dead zone processing.
174
+ *
175
+ * @param axis - Axis index to inspect.
176
+ * @param options - Optional per-read axis options.
177
+ * @returns Axis value after dead zone processing, or `0` when unavailable.
178
+ */
179
+ axis(axis, options) {
180
+ return applyGamepadDeadzone(this.#gamepad?.axes[axis] ?? 0, this.#getDeadzone(options));
181
+ }
182
+ /**
183
+ * Returns a two-axis stick after dead zone processing.
184
+ *
185
+ * @param xAxis - Horizontal axis index.
186
+ * @param yAxis - Vertical axis index.
187
+ * @param options - Optional per-read axis options.
188
+ * @returns Object containing processed `x` and `y` values.
189
+ */
190
+ stick(xAxis, yAxis, options) {
191
+ return {
192
+ x: this.axis(xAxis, options),
193
+ y: this.axis(yAxis, options)
194
+ };
195
+ }
196
+ /**
197
+ * Handles a browser connection event and adopts the gamepad when possible.
198
+ *
199
+ * Button state is seeded as both current and previous so an already-held
200
+ * button does not produce a synthetic `wasPressed` transition on connect.
201
+ *
202
+ * @param gamepad - Browser-provided connected gamepad snapshot.
203
+ */
204
+ #handleGamepadConnected(gamepad) {
205
+ if (!this.#manager.connect(gamepad)) return;
206
+ this.#gamepad = this.#manager.activeGamepad;
207
+ this.#syncButtonState({ seedPrevious: true });
208
+ this.dispatchEvent({
209
+ type: "connected",
210
+ gamepad
211
+ });
212
+ }
213
+ /**
214
+ * Handles a browser disconnection event for the active gamepad.
215
+ *
216
+ * Button state is cleared instead of diffed so disconnecting a controller
217
+ * does not produce synthetic `wasReleased` transitions.
218
+ *
219
+ * @param gamepad - Browser-provided disconnected gamepad snapshot.
220
+ */
221
+ #handleGamepadDisconnected(gamepad) {
222
+ const disconnectedGamepad = this.#manager.disconnect(gamepad);
223
+ if (disconnectedGamepad === null) return;
224
+ this.#gamepad = null;
225
+ this.#clearButtonState();
226
+ this.dispatchEvent({
227
+ type: "disconnected",
228
+ gamepad: disconnectedGamepad
229
+ });
230
+ }
231
+ /**
232
+ * Refreshes current and previous pressed-button sets from the active snapshot.
233
+ *
234
+ * When `seedPrevious` is `true`, the refreshed current state is copied into
235
+ * the previous state. This intentionally suppresses transition events on the
236
+ * first frame after adopting a gamepad.
237
+ *
238
+ * @param options - Button state synchronization options.
239
+ * @param options.seedPrevious - Whether to seed previous state from current state.
240
+ */
241
+ #syncButtonState({ seedPrevious }) {
242
+ this.#previousPressedButtons.clear();
243
+ for (const button of this.#pressedButtons) this.#previousPressedButtons.add(button);
244
+ this.#pressedButtons.clear();
245
+ if (this.#gamepad !== null) {
246
+ for (let index = 0; index < this.#gamepad.buttons.length; index += 1) if (getGamepadButtonPressed(this.#gamepad, index)) this.#pressedButtons.add(index);
247
+ }
248
+ if (!seedPrevious) return;
249
+ this.#previousPressedButtons.clear();
250
+ for (const button of this.#pressedButtons) this.#previousPressedButtons.add(button);
251
+ }
252
+ /**
253
+ * Clears all stored button state.
254
+ */
255
+ #clearButtonState() {
256
+ this.#pressedButtons.clear();
257
+ this.#previousPressedButtons.clear();
258
+ }
259
+ /**
260
+ * Resolves the dead zone for a single axis or stick read.
261
+ *
262
+ * @param options - Optional per-read axis options.
263
+ * @returns The per-read dead zone when provided, otherwise the instance default.
264
+ */
265
+ #getDeadzone(options) {
266
+ return options?.deadzone ?? this.#options.deadzone;
267
+ }
268
+ };
269
+ //#endregion
270
+ export { GamepadInput };
@@ -51,6 +51,16 @@ var GamepadTrackballControls = class extends GamepadControls {
51
51
  this.#queuePan(deltaTime, gamepad, panSpeed, deadzone, axisPanX, axisPanY);
52
52
  this.#queueZoom(deltaTime, gamepad, zoomSpeed, deadzone, buttonZoomIn, buttonZoomOut);
53
53
  }
54
+ /**
55
+ * Queues rotation input into TrackballControls' normalized move state.
56
+ *
57
+ * @param deltaTime - Seconds since the last frame.
58
+ * @param gamepad - Fresh gamepad snapshot to read from.
59
+ * @param rotateSpeed - User-configured rotation speed multiplier.
60
+ * @param deadzone - Axis dead zone threshold.
61
+ * @param axisRotateX - Axis index for horizontal rotation.
62
+ * @param axisRotateY - Axis index for vertical rotation.
63
+ */
54
64
  #queueRotation(deltaTime, gamepad, rotateSpeed, deadzone, axisRotateX, axisRotateY) {
55
65
  const controls = this.#controls;
56
66
  if (controls.noRotate) {
@@ -65,6 +75,16 @@ var GamepadTrackballControls = class extends GamepadControls {
65
75
  controls._moveCurr.x += rotX * scale;
66
76
  controls._moveCurr.y += -rotY * scale;
67
77
  }
78
+ /**
79
+ * Queues pan input into TrackballControls' normalized pan state.
80
+ *
81
+ * @param deltaTime - Seconds since the last frame.
82
+ * @param gamepad - Fresh gamepad snapshot to read from.
83
+ * @param panSpeed - User-configured pan speed multiplier.
84
+ * @param deadzone - Axis dead zone threshold.
85
+ * @param axisPanX - Axis index for horizontal panning.
86
+ * @param axisPanY - Axis index for vertical panning.
87
+ */
68
88
  #queuePan(deltaTime, gamepad, panSpeed, deadzone, axisPanX, axisPanY) {
69
89
  const controls = this.#controls;
70
90
  if (controls.noPan) {
@@ -78,6 +98,16 @@ var GamepadTrackballControls = class extends GamepadControls {
78
98
  controls._panEnd.x += panX * scale;
79
99
  controls._panEnd.y += panY * scale;
80
100
  }
101
+ /**
102
+ * Queues trigger zoom input into TrackballControls' normalized zoom state.
103
+ *
104
+ * @param deltaTime - Seconds since the last frame.
105
+ * @param gamepad - Fresh gamepad snapshot to read from.
106
+ * @param zoomSpeed - User-configured zoom speed multiplier.
107
+ * @param deadzone - Trigger dead zone threshold.
108
+ * @param buttonZoomIn - Button index for zooming in.
109
+ * @param buttonZoomOut - Button index for zooming out.
110
+ */
81
111
  #queueZoom(deltaTime, gamepad, zoomSpeed, deadzone, buttonZoomIn, buttonZoomOut) {
82
112
  const controls = this.#controls;
83
113
  if (controls.noZoom) {
@@ -92,6 +122,8 @@ var GamepadTrackballControls = class extends GamepadControls {
92
122
  /**
93
123
  * Compensates for TrackballControls reapplying queued pan and zoom deltas
94
124
  * while their input state catches up through damping.
125
+ *
126
+ * @returns Multiplier that matches TrackballControls' damping mode.
95
127
  */
96
128
  #getInputDampingFactor() {
97
129
  const controls = this.#controls;