three-gamepad-controls 0.25.5 → 0.25.7

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.
@@ -51,6 +51,8 @@ export type GamepadFirstPersonControlsOptions = GamepadControlsOptions & {
51
51
  * and climbing along Y, independent of camera pitch and roll.
52
52
  * Combined gamepad movement is limited to unit magnitude before speed and
53
53
  * forward height gains, preserving smaller analog intentions unchanged.
54
+ * Applied backward movement suppresses only the automatic target for the next
55
+ * native update; native inertia and manual input remain intact.
54
56
  * Bindings and speeds are configurable via {@link GamepadFirstPersonControlsOptions}.
55
57
  */
56
58
  export declare class GamepadFirstPersonControls extends GamepadControls {
@@ -61,6 +63,20 @@ export declare class GamepadFirstPersonControls extends GamepadControls {
61
63
  * Any property not provided falls back to its default value.
62
64
  */
63
65
  constructor(controls: FirstPersonControls, options?: Partial<GamepadFirstPersonControlsOptions>);
66
+ /**
67
+ * Starts a fresh input frame, dropping unconsumed auto-forward suppression.
68
+ * Recursive updates are ignored; failed input frames retain no suppression.
69
+ *
70
+ * @param deltaTime - Seconds since the last frame.
71
+ */
72
+ update(deltaTime: number): void;
73
+ /**
74
+ * Releases this instance's update adapter without disposing the native control.
75
+ * Later application replacements are preserved. Disposal is terminal.
76
+ */
77
+ dispose(): void;
78
+ /** Releases pending suppression before notifying disconnection listeners. */
79
+ protected onGamepadDisconnected(gamepad: Gamepad): void;
64
80
  /**
65
81
  * Maps the current gamepad state to `FirstPersonControls` translation and look.
66
82
  *
@@ -30,11 +30,21 @@ const LOOK_SPEED_SCALE = 36e3;
30
30
  * and climbing along Y, independent of camera pitch and roll.
31
31
  * Combined gamepad movement is limited to unit magnitude before speed and
32
32
  * forward height gains, preserving smaller analog intentions unchanged.
33
+ * Applied backward movement suppresses only the automatic target for the next
34
+ * native update; native inertia and manual input remain intact.
33
35
  * Bindings and speeds are configurable via {@link GamepadFirstPersonControlsOptions}.
34
36
  */
35
37
  var GamepadFirstPersonControls = class extends GamepadControls {
36
38
  #controls;
37
39
  #options;
40
+ #originalUpdate;
41
+ #originalUpdateDescriptor;
42
+ #nativeUpdate;
43
+ #backwardObject = null;
44
+ #backwardGamepad = null;
45
+ #updating = false;
46
+ #nativeUpdating = false;
47
+ #disposed = false;
38
48
  #lookDirection;
39
49
  #spherical;
40
50
  #targetPosition;
@@ -55,6 +65,111 @@ var GamepadFirstPersonControls = class extends GamepadControls {
55
65
  this.#lookDirection = new Vector3();
56
66
  this.#spherical = new Spherical();
57
67
  this.#targetPosition = new Vector3();
68
+ this.#originalUpdate = controls.update;
69
+ this.#originalUpdateDescriptor = Object.getOwnPropertyDescriptor(controls, "update");
70
+ this.#nativeUpdate = (deltaTime) => this.#updateNative(deltaTime);
71
+ controls.update = this.#nativeUpdate;
72
+ }
73
+ /**
74
+ * Starts a fresh input frame, dropping unconsumed auto-forward suppression.
75
+ * Recursive updates are ignored; failed input frames retain no suppression.
76
+ *
77
+ * @param deltaTime - Seconds since the last frame.
78
+ */
79
+ update(deltaTime) {
80
+ if (this.#updating || this.#nativeUpdating || this.#disposed) return;
81
+ this.#clearBackwardInput();
82
+ this.#updating = true;
83
+ try {
84
+ super.update(deltaTime);
85
+ } catch (error) {
86
+ this.#clearBackwardInput();
87
+ throw error;
88
+ } finally {
89
+ this.#updating = false;
90
+ }
91
+ }
92
+ /**
93
+ * Releases this instance's update adapter without disposing the native control.
94
+ * Later application replacements are preserved. Disposal is terminal.
95
+ */
96
+ dispose() {
97
+ if (this.#disposed) return;
98
+ this.#disposed = true;
99
+ this.#clearBackwardInput();
100
+ super.dispose();
101
+ if (this.#controls.update === this.#nativeUpdate) {
102
+ if (this.#originalUpdateDescriptor === void 0) Reflect.deleteProperty(this.#controls, "update");
103
+ else Object.defineProperty(this.#controls, "update", this.#originalUpdateDescriptor);
104
+ }
105
+ }
106
+ /** Releases pending suppression before notifying disconnection listeners. */
107
+ onGamepadDisconnected(gamepad) {
108
+ this.#clearBackwardInput();
109
+ super.onGamepadDisconnected(gamepad);
110
+ }
111
+ /** Clears the destination and snapshot that received backward movement. */
112
+ #clearBackwardInput() {
113
+ this.#backwardObject = null;
114
+ this.#backwardGamepad = null;
115
+ }
116
+ /** Revalidates ownership and permissions after synchronous callbacks. */
117
+ #canApplyInput(gamepad, object) {
118
+ return !this.#disposed && this.enabled && this.#controls.enabled && this.gamepadInput.gamepad === gamepad && this.#controls.object === object;
119
+ }
120
+ /**
121
+ * Consumes one backward frame. Masking only autoForward lets the original
122
+ * update compute manual targets and damp its existing velocity unchanged.
123
+ * A temporary setter preserves assignments, including explicit false writes;
124
+ * conditional restoration also preserves application property replacements.
125
+ */
126
+ #updateNative(deltaTime) {
127
+ if (this.#nativeUpdating || this.#updating) return;
128
+ const controls = this.#controls;
129
+ const gamepad = this.#backwardGamepad;
130
+ const object = this.#backwardObject;
131
+ this.#clearBackwardInput();
132
+ this.#nativeUpdating = true;
133
+ try {
134
+ if (gamepad === null || deltaTime === 0 || !this.#canApplyInput(gamepad, object)) {
135
+ this.#originalUpdate.call(controls, deltaTime);
136
+ return;
137
+ }
138
+ const descriptor = Object.getOwnPropertyDescriptor(controls, "autoForward");
139
+ let value = controls.autoForward;
140
+ let assigned = false;
141
+ let suppress = true;
142
+ const enumerable = descriptor?.enumerable ?? true;
143
+ const get = () => suppress && this.#canApplyInput(gamepad, object) ? false : value;
144
+ const set = (next) => {
145
+ assigned = true;
146
+ value = next;
147
+ descriptor?.set?.call(controls, next);
148
+ };
149
+ Object.defineProperty(controls, "autoForward", {
150
+ configurable: true,
151
+ enumerable,
152
+ get,
153
+ set
154
+ });
155
+ try {
156
+ this.#originalUpdate.call(controls, deltaTime);
157
+ } finally {
158
+ suppress = false;
159
+ const current = Object.getOwnPropertyDescriptor(controls, "autoForward");
160
+ if (current?.get === get && current.set === set && current.configurable === true && current.enumerable === enumerable) {
161
+ if (descriptor === void 0) {
162
+ Reflect.deleteProperty(controls, "autoForward");
163
+ if (assigned) controls.autoForward = value;
164
+ } else Object.defineProperty(controls, "autoForward", "value" in descriptor ? {
165
+ ...descriptor,
166
+ value
167
+ } : descriptor);
168
+ }
169
+ }
170
+ } finally {
171
+ this.#nativeUpdating = false;
172
+ }
58
173
  }
59
174
  /**
60
175
  * Maps the current gamepad state to `FirstPersonControls` translation and look.
@@ -64,8 +179,15 @@ var GamepadFirstPersonControls = class extends GamepadControls {
64
179
  onUpdate(deltaTime) {
65
180
  if (!this.#controls.enabled) return;
66
181
  const { moveSpeed, lookSpeed, moveStick, lookStick, buttonDeadzone, buttonMoveUp, buttonMoveDown } = this.#options;
67
- this.#applyMovement(deltaTime, moveSpeed, moveStick, buttonDeadzone, buttonMoveUp, buttonMoveDown);
68
- this.#applyLook(deltaTime, lookSpeed, lookStick);
182
+ const input = this.gamepadInput;
183
+ const gamepad = input.gamepad;
184
+ const object = this.#controls.object;
185
+ const move = input.stick(moveStick.xAxis, moveStick.yAxis, moveStick.pipeline);
186
+ if (!this.#canApplyInput(gamepad, object)) return;
187
+ const look = input.stick(lookStick.xAxis, lookStick.yAxis, lookStick.pipeline);
188
+ if (!this.#canApplyInput(gamepad, object)) return;
189
+ this.#applyMovement(deltaTime, moveSpeed, move, buttonDeadzone, buttonMoveUp, buttonMoveDown);
190
+ this.#applyLook(deltaTime, lookSpeed, look);
69
191
  }
70
192
  /**
71
193
  * Applies translation in the native keyboard frame, using yaw before look.
@@ -75,15 +197,14 @@ var GamepadFirstPersonControls = class extends GamepadControls {
75
197
  *
76
198
  * @param deltaTime - Seconds since the last frame.
77
199
  * @param moveSpeed - User-configured movement speed multiplier.
78
- * @param moveStick - Resolved movement stick binding.
200
+ * @param move - Processed movement stick input.
79
201
  * @param buttonDeadzone - Analog button dead zone threshold.
80
202
  * @param buttonMoveUp - Button index for upward movement.
81
203
  * @param buttonMoveDown - Button index for downward movement.
82
204
  */
83
- #applyMovement(deltaTime, moveSpeed, moveStick, buttonDeadzone, buttonMoveUp, buttonMoveDown) {
205
+ #applyMovement(deltaTime, moveSpeed, move, buttonDeadzone, buttonMoveUp, buttonMoveDown) {
84
206
  const controls = this.#controls;
85
207
  const input = this.gamepadInput;
86
- const move = input.stick(moveStick.xAxis, moveStick.yAxis, moveStick.pipeline);
87
208
  const up = input.buttonValue(buttonMoveUp);
88
209
  const down = input.buttonValue(buttonMoveDown);
89
210
  let strafe = move.x;
@@ -108,6 +229,10 @@ var GamepadFirstPersonControls = class extends GamepadControls {
108
229
  const strafeDistance = strafe * moveMult;
109
230
  controls.object.position.x += sinYaw * forwardDistance - cosYaw * strafeDistance;
110
231
  controls.object.position.z += cosYaw * forwardDistance + sinYaw * strafeDistance;
232
+ if (forwardDistance < 0) {
233
+ this.#backwardObject = controls.object;
234
+ this.#backwardGamepad = input.gamepad;
235
+ }
111
236
  }
112
237
  if (climb !== 0) controls.object.position.y += climb * moveMult;
113
238
  }
@@ -116,10 +241,10 @@ var GamepadFirstPersonControls = class extends GamepadControls {
116
241
  *
117
242
  * @param deltaTime - Seconds since the last frame.
118
243
  * @param lookSpeed - User-configured look speed multiplier.
119
- * @param lookStick - Resolved camera-look stick binding.
244
+ * @param look - Processed camera-look stick input.
120
245
  */
121
- #applyLook(deltaTime, lookSpeed, lookStick) {
122
- const { x: lookX, y: lookY } = this.gamepadInput.stick(lookStick.xAxis, lookStick.yAxis, lookStick.pipeline);
246
+ #applyLook(deltaTime, lookSpeed, look) {
247
+ const { x: lookX, y: lookY } = look;
123
248
  if (lookX === 0 && lookY === 0) return;
124
249
  const controls = this.#controls;
125
250
  const actualLookSpeed = controls.lookSpeed * lookSpeed * deltaTime * LOOK_SPEED_SCALE;
@@ -7,8 +7,8 @@ import { MapControls } from "three/addons/controls/MapControls.js";
7
7
  * mouse conventions. All options from {@link GamepadOrbitControlsOptions} are available.
8
8
  *
9
9
  * Call `update()` inside the render loop **before** `MapControls.update()`.
10
- * Gamepad interaction events, pause semantics, and combined native/wrapper speeds
11
- * are inherited from {@link GamepadOrbitControls}.
10
+ * Gamepad interaction events, pause semantics, combined native/wrapper speeds,
11
+ * and automatic-rotation suspension are inherited from {@link GamepadOrbitControls}.
12
12
  */
13
13
  export declare class GamepadMapControls extends GamepadOrbitControls {
14
14
  /**
@@ -18,8 +18,8 @@ const DEFAULT_MAP_OPTIONS = {
18
18
  * mouse conventions. All options from {@link GamepadOrbitControlsOptions} are available.
19
19
  *
20
20
  * Call `update()` inside the render loop **before** `MapControls.update()`.
21
- * Gamepad interaction events, pause semantics, and combined native/wrapper speeds
22
- * are inherited from {@link GamepadOrbitControls}.
21
+ * Gamepad interaction events, pause semantics, combined native/wrapper speeds,
22
+ * and automatic-rotation suspension are inherited from {@link GamepadOrbitControls}.
23
23
  */
24
24
  var GamepadMapControls = class extends GamepadOrbitControls {
25
25
  /**
@@ -55,6 +55,8 @@ export type GamepadOrbitControlsOptions = GamepadControlsOptions & {
55
55
  * Bindings and speeds are configurable via {@link GamepadOrbitControlsOptions}.
56
56
  * The wrapper dispatches balanced gamepad `start` and `end` events on the native
57
57
  * controls; native operations and damping remain responsible for `change`.
58
+ * Accepted input suspends automatic rotation through every native update in the
59
+ * frame, preserving autoRotate configuration, pointer state, and residual damping.
58
60
  */
59
61
  export declare class GamepadOrbitControls extends GamepadControls {
60
62
  #private;
@@ -81,7 +83,8 @@ export declare class GamepadOrbitControls extends GamepadControls {
81
83
  protected onUpdate(deltaTime: number): void;
82
84
  /**
83
85
  * Removes gamepad listeners and ends only this wrapper's active interaction.
84
- * Repeated calls are safe; the native controls and their damping are preserved.
86
+ * Restores the owned native update adapter even when an end listener throws.
87
+ * Repeated calls are safe and disposal is terminal; native damping is preserved.
85
88
  */
86
89
  dispose(): void;
87
90
  /**
@@ -27,10 +27,18 @@ const DEFAULT_ORBIT_OPTIONS = {
27
27
  * Bindings and speeds are configurable via {@link GamepadOrbitControlsOptions}.
28
28
  * The wrapper dispatches balanced gamepad `start` and `end` events on the native
29
29
  * controls; native operations and damping remain responsible for `change`.
30
+ * Accepted input suspends automatic rotation through every native update in the
31
+ * frame, preserving autoRotate configuration, pointer state, and residual damping.
30
32
  */
31
33
  var GamepadOrbitControls = class extends GamepadControls {
32
34
  #controls;
33
35
  #options;
36
+ #originalUpdate;
37
+ #originalUpdateDescriptor;
38
+ #nativeUpdate;
39
+ #autoRotationFrame = null;
40
+ #nativeUpdating = false;
41
+ #disposed = false;
34
42
  /** Whether this wrapper owns an active gamepad interaction. */
35
43
  #interacting = false;
36
44
  /** Blocks recursive updates while a frame is being processed. */
@@ -51,6 +59,10 @@ var GamepadOrbitControls = class extends GamepadControls {
51
59
  rotateStick: resolveGamepadStickBinding(DEFAULT_ORBIT_OPTIONS.rotateStick, options?.rotateStick),
52
60
  panStick: resolveGamepadStickBinding(DEFAULT_ORBIT_OPTIONS.panStick, options?.panStick)
53
61
  };
62
+ this.#originalUpdate = controls.update;
63
+ this.#originalUpdateDescriptor = Object.getOwnPropertyDescriptor(controls, "update");
64
+ this.#nativeUpdate = (delta) => this.#updateNative(delta);
65
+ controls.update = this.#nativeUpdate;
54
66
  }
55
67
  /**
56
68
  * Polls the gamepad and applies input, ignoring updates from synchronous listeners.
@@ -60,10 +72,14 @@ var GamepadOrbitControls = class extends GamepadControls {
60
72
  * @param deltaTime - Seconds since the last frame.
61
73
  */
62
74
  update(deltaTime) {
63
- if (this.#updating || this.#ending) return;
75
+ if (this.#updating || this.#ending || this.#nativeUpdating || this.#disposed) return;
76
+ this.#autoRotationFrame = null;
64
77
  this.#updating = true;
65
78
  try {
66
79
  super.update(deltaTime);
80
+ } catch (error) {
81
+ this.#autoRotationFrame = null;
82
+ throw error;
67
83
  } finally {
68
84
  this.#updating = false;
69
85
  }
@@ -81,59 +97,77 @@ var GamepadOrbitControls = class extends GamepadControls {
81
97
  }
82
98
  const { rotateStick, panStick, buttonDeadzone, buttonDollyIn, buttonDollyOut } = this.#options;
83
99
  const input = this.gamepadInput;
100
+ const gamepad = input.gamepad;
101
+ const object = this.#controls.object;
84
102
  const rotate = input.stick(rotateStick.xAxis, rotateStick.yAxis, rotateStick.pipeline);
85
103
  const pan = input.stick(panStick.xAxis, panStick.yAxis, panStick.pipeline);
86
104
  const triggerIn = input.buttonValue(buttonDollyIn);
87
105
  const triggerOut = input.buttonValue(buttonDollyOut);
88
106
  const frame = {
89
- rotateX: rotate.x,
90
- rotateY: rotate.y,
91
- panX: pan.x,
92
- panY: pan.y,
93
- dollyIn: triggerIn > buttonDeadzone ? triggerIn : 0,
94
- dollyOut: triggerOut > buttonDeadzone ? triggerOut : 0
107
+ gamepad,
108
+ object,
109
+ delta: deltaTime,
110
+ input: {
111
+ rotateX: rotate.x,
112
+ rotateY: rotate.y,
113
+ panX: pan.x,
114
+ panY: pan.y,
115
+ dollyIn: triggerIn > buttonDeadzone ? triggerIn : 0,
116
+ dollyOut: triggerOut > buttonDeadzone ? triggerOut : 0
117
+ }
95
118
  };
96
- let actions = this.#acceptActions(frame, deltaTime);
119
+ let actions = this.#acceptActions(frame);
97
120
  if (actions === null) return;
98
121
  if (!this.#interacting) {
99
122
  this.#interacting = true;
100
123
  this.#controls.dispatchEvent({ type: "start" });
101
- actions = this.#acceptActions(frame, deltaTime);
124
+ actions = this.#acceptActions(frame);
102
125
  if (actions === null) return;
103
126
  }
104
127
  const controls = this.#controls;
105
128
  if (actions.rotateX !== 0) {
106
129
  controls.rotateLeft(actions.rotateX);
107
- actions = this.#acceptActions(frame, deltaTime);
130
+ actions = this.#acceptActions(frame);
108
131
  if (actions === null) return;
109
132
  }
110
133
  if (actions.rotateY !== 0) {
111
134
  controls.rotateUp(actions.rotateY);
112
- actions = this.#acceptActions(frame, deltaTime);
135
+ actions = this.#acceptActions(frame);
113
136
  if (actions === null) return;
114
137
  }
115
138
  if (actions.panX !== 0 || actions.panY !== 0) {
116
139
  controls.pan(actions.panX, actions.panY);
117
- actions = this.#acceptActions(frame, deltaTime);
140
+ actions = this.#acceptActions(frame);
118
141
  if (actions === null) return;
119
142
  }
120
143
  if (actions.dollyIn !== 0) {
121
144
  controls.dollyIn(1 / (1 + actions.dollyIn));
122
- actions = this.#acceptActions(frame, deltaTime);
145
+ actions = this.#acceptActions(frame);
123
146
  if (actions === null) return;
124
147
  }
125
148
  if (actions.dollyOut !== 0) {
126
149
  controls.dollyOut(1 / (1 + actions.dollyOut));
127
- this.#acceptActions(frame, deltaTime);
150
+ this.#acceptActions(frame);
128
151
  }
129
152
  }
130
153
  /**
131
154
  * Removes gamepad listeners and ends only this wrapper's active interaction.
132
- * Repeated calls are safe; the native controls and their damping are preserved.
155
+ * Restores the owned native update adapter even when an end listener throws.
156
+ * Repeated calls are safe and disposal is terminal; native damping is preserved.
133
157
  */
134
158
  dispose() {
159
+ if (this.#disposed) return;
160
+ this.#disposed = true;
161
+ this.#autoRotationFrame = null;
135
162
  super.dispose();
136
- this.#endInteraction();
163
+ try {
164
+ this.#endInteraction();
165
+ } finally {
166
+ if (this.#controls.update === this.#nativeUpdate) {
167
+ if (this.#originalUpdateDescriptor === void 0) Reflect.deleteProperty(this.#controls, "update");
168
+ else Object.defineProperty(this.#controls, "update", this.#originalUpdateDescriptor);
169
+ }
170
+ }
137
171
  }
138
172
  /**
139
173
  * Ends the owned interaction before forwarding the active gamepad's disconnection.
@@ -149,17 +183,29 @@ var GamepadOrbitControls = class extends GamepadControls {
149
183
  * Native disable or lack of actionable input ends the owned session; a wrapper
150
184
  * pause stops application while retaining it. Geometric limits do not erase intent.
151
185
  *
152
- * @param input - Already processed sticks and filtered triggers for this frame.
153
- * @param delta - Frame duration in seconds.
186
+ * @param frame - Cached input and the destination that owns it.
154
187
  * @returns Accepted frame deltas, or `null` when input application must stop.
155
188
  */
156
- #acceptActions(input, delta) {
157
- const controls = this.#controls;
158
- if (!controls.enabled) {
189
+ #acceptActions(frame) {
190
+ if (!this.#controls.enabled) {
159
191
  this.#endInteraction();
160
192
  return null;
161
193
  }
162
- if (!this.enabled || this.gamepad === null) return null;
194
+ if (!this.#ownsFrame(frame)) {
195
+ this.#autoRotationFrame = null;
196
+ return null;
197
+ }
198
+ const actions = this.#resolveActions(frame.input, frame.delta);
199
+ if (actions === null) {
200
+ this.#endInteraction();
201
+ return null;
202
+ }
203
+ this.#autoRotationFrame = frame;
204
+ return actions;
205
+ }
206
+ /** Resolves current permissions and gains without events or pipeline reads. */
207
+ #resolveActions(input, delta) {
208
+ const controls = this.#controls;
163
209
  const rotate = controls.enableRotate ? controls.rotateSpeed * this.#options.rotateSpeed * delta * Math.PI : 0;
164
210
  const pan = controls.enablePan ? controls.panSpeed * this.#options.panSpeed * delta * 500 : 0;
165
211
  const zoom = controls.enableZoom ? controls.zoomSpeed * this.#options.zoomSpeed * delta : 0;
@@ -171,17 +217,68 @@ var GamepadOrbitControls = class extends GamepadControls {
171
217
  dollyIn: input.dollyIn * zoom,
172
218
  dollyOut: input.dollyOut * zoom
173
219
  };
174
- if (!Object.values(actions).some((value) => value !== 0)) {
175
- this.#endInteraction();
176
- return null;
177
- }
220
+ if (!Object.values(actions).some((value) => value !== 0)) return null;
178
221
  return actions;
179
222
  }
223
+ /** Checks the live wrapper, selected snapshot, and camera after callbacks. */
224
+ #ownsFrame(frame) {
225
+ return !this.#disposed && this.enabled && this.gamepad === frame.gamepad && this.#controls.object === frame.object;
226
+ }
227
+ /** Releases invalid intent without ending a paused interaction or repolling. */
228
+ #suppressAutoRotation() {
229
+ const frame = this.#autoRotationFrame;
230
+ if (frame === null) return false;
231
+ if (!this.#controls.enabled || !this.#ownsFrame(frame) || this.#resolveActions(frame.input, frame.delta) === null) {
232
+ this.#autoRotationFrame = null;
233
+ return false;
234
+ }
235
+ return true;
236
+ }
237
+ /**
238
+ * Masks only the automatic angle for this call, retaining intent for subsequent
239
+ * internal and external updates. Native pointer gating and damping stay intact.
240
+ * Restores owned descriptors and makes retained masks inert even on failure.
241
+ */
242
+ #updateNative(delta) {
243
+ if (this.#nativeUpdating) return false;
244
+ const controls = this.#controls;
245
+ this.#nativeUpdating = true;
246
+ try {
247
+ if (!this.#suppressAutoRotation()) return this.#originalUpdate.call(controls, delta);
248
+ const descriptor = Object.getOwnPropertyDescriptor(controls, "_getAutoRotationAngle");
249
+ const original = controls._getAutoRotationAngle;
250
+ const enumerable = descriptor?.enumerable ?? false;
251
+ let masking = true;
252
+ const angle = (delta) => masking && this.#suppressAutoRotation() ? 0 : original.call(controls, delta);
253
+ Object.defineProperty(controls, "_getAutoRotationAngle", {
254
+ configurable: true,
255
+ enumerable,
256
+ writable: true,
257
+ value: angle
258
+ });
259
+ try {
260
+ return this.#originalUpdate.call(controls, delta);
261
+ } finally {
262
+ masking = false;
263
+ const current = Object.getOwnPropertyDescriptor(controls, "_getAutoRotationAngle");
264
+ if (current?.value === angle && current.configurable === true && current.enumerable === enumerable && current.writable === true) {
265
+ if (descriptor === void 0) Reflect.deleteProperty(controls, "_getAutoRotationAngle");
266
+ else Object.defineProperty(controls, "_getAutoRotationAngle", descriptor);
267
+ }
268
+ }
269
+ } catch (error) {
270
+ this.#autoRotationFrame = null;
271
+ throw error;
272
+ } finally {
273
+ this.#nativeUpdating = false;
274
+ }
275
+ }
180
276
  /**
181
277
  * Releases session ownership before dispatching the native `end` event once.
182
278
  * Blocks recursive updates during finalization without clearing native input or damping.
183
279
  */
184
280
  #endInteraction() {
281
+ this.#autoRotationFrame = null;
185
282
  if (!this.#interacting) return;
186
283
  this.#interacting = false;
187
284
  this.#ending = true;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "three-gamepad-controls",
3
3
  "description": "Gamepad support for Three.js controls.",
4
- "version": "0.25.5",
4
+ "version": "0.25.7",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/luckasnix/three-gamepad-controls#readme",
7
7
  "author": {