three-gamepad-controls 0.24.1 → 0.24.3

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.
@@ -1,7 +1,7 @@
1
1
  import { GAMEPAD_AXIS } from "./core.js";
2
2
  import { DEFAULT_GAMEPAD_STICK_PIPELINE, resolveGamepadStickBinding } from "./gamepad-stick-processing.js";
3
3
  import { GamepadControls } from "./gamepad-controls.js";
4
- import { Euler } from "three";
4
+ import { Euler, Quaternion } from "three";
5
5
  //#region src/gamepad-pointer-lock-controls.ts
6
6
  const DEFAULT_POINTER_LOCK_OPTIONS = {
7
7
  moveSpeed: 5,
@@ -23,11 +23,17 @@ const DEFAULT_POINTER_LOCK_OPTIONS = {
23
23
  * Gamepad input is fully independent of pointer lock state. When the pointer
24
24
  * IS locked, mouse and gamepad look inputs are additive.
25
25
  * Bindings and speeds are configurable via {@link GamepadPointerLockControlsOptions}.
26
+ * Look dispatches `change` on the native controls after an actual orientation
27
+ * change; movement does not dispatch it or alter pointer lock state.
26
28
  */
27
29
  var GamepadPointerLockControls = class extends GamepadControls {
28
30
  #controls;
29
31
  #options;
30
32
  #euler;
33
+ /** Reusable orientation snapshot for sign-independent angular change detection. */
34
+ #previousQuaternion = new Quaternion();
35
+ /** Blocks recursive input application from native `change` listeners. */
36
+ #updating = false;
31
37
  /**
32
38
  * @param controls - A Three.js `PointerLockControls` instance.
33
39
  * @param options - Optional overrides for the default behavior.
@@ -45,7 +51,26 @@ var GamepadPointerLockControls = class extends GamepadControls {
45
51
  this.#euler = new Euler(0, 0, 0, "YXZ");
46
52
  }
47
53
  /**
54
+ * Polls and applies movement and look, ignoring updates from synchronous listeners.
55
+ * Gamepad input remains independent of the native pointer lock state.
56
+ *
57
+ * @param deltaTime - Seconds since the last frame.
58
+ */
59
+ update(deltaTime) {
60
+ if (this.#updating) return;
61
+ this.#updating = true;
62
+ try {
63
+ super.update(deltaTime);
64
+ } finally {
65
+ this.#updating = false;
66
+ }
67
+ }
68
+ /**
48
69
  * Maps the current gamepad state to `PointerLockControls` movement and look.
70
+ * Dispatches at most one native `change` after look changes orientation by more
71
+ * than `1e-7` radians, treating opposite quaternion signs as equivalent.
72
+ * Translation, zero look gain, and pitch-only input held at a clamp do not
73
+ * dispatch `change`; permitted yaw can still change orientation at a pitch limit.
49
74
  *
50
75
  * @param deltaTime - Seconds since the last frame.
51
76
  */
@@ -57,14 +82,16 @@ var GamepadPointerLockControls = class extends GamepadControls {
57
82
  if (move.y !== 0) this.#controls.moveForward(-move.y * moveSpeed * deltaTime);
58
83
  if (move.x !== 0) this.#controls.moveRight(move.x * moveSpeed * deltaTime);
59
84
  const look = input.stick(lookStick.xAxis, lookStick.yAxis, lookStick.pipeline);
60
- if (look.x !== 0 || look.y !== 0) {
85
+ const scale = lookSpeed * this.#controls.pointerSpeed * deltaTime * Math.PI;
86
+ if ((look.x !== 0 || look.y !== 0) && scale !== 0) {
61
87
  const camera = this.#controls.object;
62
- const scale = lookSpeed * this.#controls.pointerSpeed * deltaTime * Math.PI;
88
+ this.#previousQuaternion.copy(camera.quaternion);
63
89
  this.#euler.setFromQuaternion(camera.quaternion);
64
90
  this.#euler.y -= look.x * scale;
65
91
  this.#euler.x -= look.y * scale;
66
92
  this.#euler.x = Math.max(Math.PI / 2 - this.#controls.maxPolarAngle, Math.min(Math.PI / 2 - this.#controls.minPolarAngle, this.#euler.x));
67
93
  camera.quaternion.setFromEuler(this.#euler);
94
+ if (this.#previousQuaternion.angleTo(camera.quaternion) > 1e-7) this.#controls.dispatchEvent({ type: "change" });
68
95
  }
69
96
  }
70
97
  };
@@ -34,7 +34,7 @@ export type GamepadTrackballControlsOptions = GamepadControlsOptions & {
34
34
  */
35
35
  panStick: GamepadStickBindingOptions;
36
36
  /**
37
- * Dead zone threshold for analog trigger values.
37
+ * Each analog trigger must be strictly above this threshold before subtraction.
38
38
  * @default 0.1
39
39
  */
40
40
  buttonDeadzone: number;
@@ -54,6 +54,8 @@ export type GamepadTrackballControlsOptions = GamepadControlsOptions & {
54
54
  *
55
55
  * Call `update()` inside the render loop **before** `TrackballControls.update()`.
56
56
  * Bindings and speed multipliers are configurable via {@link GamepadTrackballControlsOptions}.
57
+ * The wrapper dispatches balanced gamepad `start` and `end` events on the native
58
+ * controls; the native update applies speeds and damping and dispatches `change`.
57
59
  */
58
60
  export declare class GamepadTrackballControls extends GamepadControls {
59
61
  #private;
@@ -64,10 +66,30 @@ export declare class GamepadTrackballControls extends GamepadControls {
64
66
  */
65
67
  constructor(controls: TrackballControls, options?: Partial<GamepadTrackballControlsOptions>);
66
68
  /**
67
- * Maps the current gamepad state to `TrackballControls` rotation, pan, and zoom.
69
+ * Polls the gamepad and queues input, ignoring updates from synchronous listeners.
70
+ * Pausing through `enabled` retains the owned interaction until input resumes,
71
+ * the gamepad disconnects, or the wrapper is disposed.
68
72
  *
69
73
  * @param deltaTime - Seconds since the last frame.
70
74
  */
75
+ update(deltaTime: number): void;
76
+ /**
77
+ * Reads each binding once, manages the gamepad session, and adds accepted deltas
78
+ * to native pointer vectors. Revalidates permissions after the `start` event.
79
+ *
80
+ * @param deltaTime - Seconds elapsed for this input frame.
81
+ */
71
82
  protected onUpdate(deltaTime: number): void;
83
+ /**
84
+ * Removes gamepad listeners and ends only this wrapper's active interaction.
85
+ * Repeated calls are safe; native pointer vectors and damping are preserved.
86
+ */
87
+ dispose(): void;
88
+ /**
89
+ * Ends the owned interaction before forwarding the active gamepad's disconnection.
90
+ *
91
+ * @param gamepad - The gamepad that just disconnected.
92
+ */
93
+ protected onGamepadDisconnected(gamepad: Gamepad): void;
72
94
  }
73
95
  //#endregion
@@ -25,10 +25,18 @@ const DEFAULT_TRACKBALL_OPTIONS = {
25
25
  *
26
26
  * Call `update()` inside the render loop **before** `TrackballControls.update()`.
27
27
  * Bindings and speed multipliers are configurable via {@link GamepadTrackballControlsOptions}.
28
+ * The wrapper dispatches balanced gamepad `start` and `end` events on the native
29
+ * controls; the native update applies speeds and damping and dispatches `change`.
28
30
  */
29
31
  var GamepadTrackballControls = class extends GamepadControls {
30
32
  #controls;
31
33
  #options;
34
+ /** Whether this wrapper owns an active gamepad interaction. */
35
+ #interacting = false;
36
+ /** Blocks recursive updates while a frame is being processed. */
37
+ #updating = false;
38
+ /** Blocks recursive updates while the owned interaction is ending. */
39
+ #ending = false;
32
40
  /**
33
41
  * @param controls - A Three.js `TrackballControls` instance.
34
42
  * @param options - Optional overrides for the default behavior.
@@ -45,76 +53,125 @@ var GamepadTrackballControls = class extends GamepadControls {
45
53
  };
46
54
  }
47
55
  /**
48
- * Maps the current gamepad state to `TrackballControls` rotation, pan, and zoom.
56
+ * Polls the gamepad and queues input, ignoring updates from synchronous listeners.
57
+ * Pausing through `enabled` retains the owned interaction until input resumes,
58
+ * the gamepad disconnects, or the wrapper is disposed.
49
59
  *
50
60
  * @param deltaTime - Seconds since the last frame.
51
61
  */
52
- onUpdate(deltaTime) {
53
- if (!this.#controls.enabled) return;
54
- const { rotateSpeed, panSpeed, zoomSpeed, rotateStick, panStick, buttonDeadzone, buttonZoomIn, buttonZoomOut } = this.#options;
55
- this.#queueRotation(deltaTime, rotateSpeed, rotateStick);
56
- this.#queuePan(deltaTime, panSpeed, panStick);
57
- this.#queueZoom(deltaTime, zoomSpeed, buttonDeadzone, buttonZoomIn, buttonZoomOut);
62
+ update(deltaTime) {
63
+ if (this.#updating || this.#ending) return;
64
+ this.#updating = true;
65
+ try {
66
+ super.update(deltaTime);
67
+ } finally {
68
+ this.#updating = false;
69
+ }
58
70
  }
59
71
  /**
60
- * Queues rotation input into TrackballControls' normalized move state.
72
+ * Reads each binding once, manages the gamepad session, and adds accepted deltas
73
+ * to native pointer vectors. Revalidates permissions after the `start` event.
61
74
  *
62
- * @param deltaTime - Seconds since the last frame.
63
- * @param rotateSpeed - User-configured rotation speed multiplier.
64
- * @param rotateStick - Resolved stick binding for rotation.
75
+ * @param deltaTime - Seconds elapsed for this input frame.
65
76
  */
66
- #queueRotation(deltaTime, rotateSpeed, rotateStick) {
77
+ onUpdate(deltaTime) {
78
+ if (!this.#controls.enabled) {
79
+ this.#endInteraction();
80
+ return;
81
+ }
82
+ const { rotateStick, panStick, buttonDeadzone, buttonZoomIn, buttonZoomOut } = this.#options;
83
+ const input = this.gamepadInput;
84
+ const rotate = input.stick(rotateStick.xAxis, rotateStick.yAxis, rotateStick.pipeline);
85
+ const pan = input.stick(panStick.xAxis, panStick.yAxis, panStick.pipeline);
86
+ const triggerIn = input.buttonValue(buttonZoomIn);
87
+ const triggerOut = input.buttonValue(buttonZoomOut);
88
+ const zoom = (triggerOut > buttonDeadzone ? triggerOut : 0) - (triggerIn > buttonDeadzone ? triggerIn : 0);
89
+ const frame = {
90
+ rotateX: rotate.x,
91
+ rotateY: rotate.y,
92
+ panX: pan.x,
93
+ panY: pan.y,
94
+ zoom
95
+ };
96
+ let actions = this.#acceptActions(frame, deltaTime);
97
+ if (actions === null) return;
98
+ if (!this.#interacting) {
99
+ this.#interacting = true;
100
+ this.#controls.dispatchEvent({ type: "start" });
101
+ actions = this.#acceptActions(frame, deltaTime);
102
+ if (actions === null) return;
103
+ }
67
104
  const controls = this.#controls;
68
- if (controls.noRotate) return;
69
- const rotate = this.gamepadInput.stick(rotateStick.xAxis, rotateStick.yAxis, rotateStick.pipeline);
70
- if (rotate.x === 0 && rotate.y === 0) return;
71
- const scale = rotateSpeed * deltaTime * Math.PI;
72
- controls._moveCurr.x += rotate.x * scale;
73
- controls._moveCurr.y += -rotate.y * scale;
105
+ controls._moveCurr.x += actions.rotateX;
106
+ controls._moveCurr.y -= actions.rotateY;
107
+ controls._panEnd.x += actions.panX;
108
+ controls._panEnd.y += actions.panY;
109
+ controls._zoomEnd.y += actions.zoom;
74
110
  }
75
111
  /**
76
- * Queues pan input into TrackballControls' normalized pan state.
77
- *
78
- * @param deltaTime - Seconds since the last frame.
79
- * @param panSpeed - User-configured pan speed multiplier.
80
- * @param panStick - Resolved stick binding for panning.
112
+ * Removes gamepad listeners and ends only this wrapper's active interaction.
113
+ * Repeated calls are safe; native pointer vectors and damping are preserved.
81
114
  */
82
- #queuePan(deltaTime, panSpeed, panStick) {
83
- const controls = this.#controls;
84
- if (controls.noPan) return;
85
- const pan = this.gamepadInput.stick(panStick.xAxis, panStick.yAxis, panStick.pipeline);
86
- if (pan.x === 0 && pan.y === 0) return;
87
- const scale = panSpeed * deltaTime * this.#getInputDampingFactor();
88
- controls._panEnd.x += pan.x * scale;
89
- controls._panEnd.y += pan.y * scale;
115
+ dispose() {
116
+ super.dispose();
117
+ this.#endInteraction();
90
118
  }
91
119
  /**
92
- * Queues trigger zoom input into TrackballControls' normalized zoom state.
120
+ * Ends the owned interaction before forwarding the active gamepad's disconnection.
93
121
  *
94
- * @param deltaTime - Seconds since the last frame.
95
- * @param zoomSpeed - User-configured zoom speed multiplier.
96
- * @param buttonDeadzone - Trigger dead zone threshold.
97
- * @param buttonZoomIn - Button index for zooming in.
98
- * @param buttonZoomOut - Button index for zooming out.
122
+ * @param gamepad - The gamepad that just disconnected.
99
123
  */
100
- #queueZoom(deltaTime, zoomSpeed, buttonDeadzone, buttonZoomIn, buttonZoomOut) {
101
- const controls = this.#controls;
102
- if (controls.noZoom) return;
103
- const input = this.gamepadInput;
104
- const triggerIn = input.buttonValue(buttonZoomIn);
105
- const triggerOut = input.buttonValue(buttonZoomOut);
106
- if (triggerIn <= buttonDeadzone && triggerOut <= buttonDeadzone) return;
107
- controls._zoomEnd.y += (triggerOut - triggerIn) * zoomSpeed * deltaTime * this.#getInputDampingFactor();
124
+ onGamepadDisconnected(gamepad) {
125
+ this.#endInteraction();
126
+ super.onGamepadDisconnected(gamepad);
108
127
  }
109
128
  /**
110
- * Compensates for TrackballControls reapplying queued pan and zoom deltas
111
- * while their input state catches up through damping.
129
+ * Resolves a cached frame against current permissions, wrapper speeds, and damping.
130
+ * Native speeds only gate acceptance here; Trackball applies them during its update.
131
+ * Native disable or lack of actionable input ends the owned session; a wrapper
132
+ * pause stops application while retaining it. Geometric limits do not erase intent.
112
133
  *
113
- * @returns Multiplier that matches TrackballControls' damping mode.
134
+ * @param input - Already processed sticks and independently filtered trigger difference.
135
+ * @param delta - Frame duration in seconds.
136
+ * @returns Accepted pointer-coordinate deltas, or `null` when application must stop.
114
137
  */
115
- #getInputDampingFactor() {
138
+ #acceptActions(input, delta) {
116
139
  const controls = this.#controls;
117
- return controls.staticMoving ? 1 : controls.dynamicDampingFactor;
140
+ if (!controls.enabled) {
141
+ this.#endInteraction();
142
+ return null;
143
+ }
144
+ if (!this.enabled || this.gamepad === null) return null;
145
+ const damping = controls.staticMoving ? 1 : controls.dynamicDampingFactor;
146
+ const rotate = !controls.noRotate && controls.rotateSpeed !== 0 ? this.#options.rotateSpeed * delta * Math.PI : 0;
147
+ const pan = !controls.noPan && controls.panSpeed !== 0 ? this.#options.panSpeed * delta * damping : 0;
148
+ const zoom = !controls.noZoom && controls.zoomSpeed !== 0 ? this.#options.zoomSpeed * delta * damping : 0;
149
+ const actions = {
150
+ rotateX: input.rotateX * rotate,
151
+ rotateY: input.rotateY * rotate,
152
+ panX: input.panX * pan,
153
+ panY: input.panY * pan,
154
+ zoom: input.zoom * zoom
155
+ };
156
+ if (!Object.values(actions).some((value) => value !== 0)) {
157
+ this.#endInteraction();
158
+ return null;
159
+ }
160
+ return actions;
161
+ }
162
+ /**
163
+ * Releases session ownership before dispatching the native `end` event once.
164
+ * Blocks recursive updates during finalization without clearing native pointer vectors.
165
+ */
166
+ #endInteraction() {
167
+ if (!this.#interacting) return;
168
+ this.#interacting = false;
169
+ this.#ending = true;
170
+ try {
171
+ this.#controls.dispatchEvent({ type: "end" });
172
+ } finally {
173
+ this.#ending = false;
174
+ }
118
175
  }
119
176
  };
120
177
  //#endregion