three-gamepad-controls 0.25.1 → 0.25.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.
@@ -9,7 +9,8 @@ import { DragControls } from "three/addons/controls/DragControls.js";
9
9
  */
10
10
  export type GamepadDragControlsOptions = GamepadControlsOptions & {
11
11
  /**
12
- * Screen-relative translation speed multiplier.
12
+ * Screen-relative translation speed multiplier. At full processed input,
13
+ * `1` moves one effective viewport width or height per second on each axis.
13
14
  * @default 1.0
14
15
  */
15
16
  dragSpeed: number;
@@ -1,6 +1,7 @@
1
1
  import { GAMEPAD_AXIS, GAMEPAD_BUTTON } 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 { getCameraViewSize } from "./three-utils.js";
4
5
  import { Matrix4, Vector2, Vector3 } from "three";
5
6
  //#region src/gamepad-drag-controls.ts
6
7
  const DEFAULT_DRAG_OPTIONS = {
@@ -32,8 +33,6 @@ var GamepadDragControls = class extends GamepadControls {
32
33
  #parentInverse;
33
34
  #selectedWorldPosition;
34
35
  #selectedLocalPosition;
35
- #cameraWorldPosition;
36
- #cameraForward;
37
36
  #cameraRight;
38
37
  #cameraUp;
39
38
  #cameraToSelected;
@@ -59,8 +58,6 @@ var GamepadDragControls = class extends GamepadControls {
59
58
  this.#parentInverse = new Matrix4();
60
59
  this.#selectedWorldPosition = new Vector3();
61
60
  this.#selectedLocalPosition = new Vector3();
62
- this.#cameraWorldPosition = new Vector3();
63
- this.#cameraForward = new Vector3();
64
61
  this.#cameraRight = new Vector3();
65
62
  this.#cameraUp = new Vector3();
66
63
  this.#cameraToSelected = new Vector3();
@@ -151,8 +148,7 @@ var GamepadDragControls = class extends GamepadControls {
151
148
  */
152
149
  #applyDrag(selected, deltaTime, dragX, dragY, dragSpeed) {
153
150
  if (dragX === 0 && dragY === 0) return false;
154
- this.#updateCameraAxes();
155
- this.#updateViewSizeAtSelectedDepth();
151
+ this.#updateDragCameraState();
156
152
  const scale = dragSpeed * deltaTime;
157
153
  this.#selectedWorldPosition.addScaledVector(this.#cameraRight, dragX * this.#viewSize.x * scale);
158
154
  this.#selectedWorldPosition.addScaledVector(this.#cameraUp, -dragY * this.#viewSize.y * scale);
@@ -185,6 +181,7 @@ var GamepadDragControls = class extends GamepadControls {
185
181
  #intersectCenter() {
186
182
  const controls = this.#controls;
187
183
  this.#intersections.length = 0;
184
+ controls.object.updateWorldMatrix(true, false);
188
185
  controls.raycaster.setFromCamera(this.#centerNdc, controls.object);
189
186
  controls.raycaster.intersectObjects(controls.objects, controls.recursive, this.#intersections);
190
187
  return this.#intersections[0];
@@ -276,42 +273,17 @@ var GamepadDragControls = class extends GamepadControls {
276
273
  }
277
274
  #updateCameraAxes() {
278
275
  const camera = this.#controls.object;
276
+ camera.updateWorldMatrix(true, false);
279
277
  this.#cameraRight.set(1, 0, 0).applyQuaternion(camera.quaternion).normalize();
280
278
  this.#cameraUp.set(0, 1, 0).applyQuaternion(camera.quaternion).normalize();
281
- camera.getWorldDirection(this.#cameraForward).normalize();
282
279
  }
283
- #updateViewSizeAtSelectedDepth() {
280
+ #updateDragCameraState() {
284
281
  const camera = this.#controls.object;
285
- if (this.#isOrthographicCamera(camera)) {
286
- this.#viewSize.set(Math.abs(camera.right - camera.left) / camera.zoom, Math.abs(camera.top - camera.bottom) / camera.zoom);
287
- return;
288
- }
289
- if (this.#isPerspectiveCamera(camera)) {
290
- camera.getWorldPosition(this.#cameraWorldPosition);
291
- const depth = Math.max(Number.EPSILON, this.#cameraToSelected.copy(this.#selectedWorldPosition).sub(this.#cameraWorldPosition).dot(this.#cameraForward));
292
- const height = 2 * Math.tan(camera.fov * Math.PI / 360) * depth;
293
- this.#viewSize.set(height * camera.aspect, height);
294
- return;
295
- }
296
- this.#viewSize.set(1, 1);
297
- }
298
- /**
299
- * Narrows a Three.js camera to `PerspectiveCamera`.
300
- *
301
- * @param camera - Camera to inspect.
302
- * @returns `true` when the camera is perspective.
303
- */
304
- #isPerspectiveCamera(camera) {
305
- return camera.isPerspectiveCamera === true;
306
- }
307
- /**
308
- * Narrows a Three.js camera to `OrthographicCamera`.
309
- *
310
- * @param camera - Camera to inspect.
311
- * @returns `true` when the camera is orthographic.
312
- */
313
- #isOrthographicCamera(camera) {
314
- return camera.isOrthographicCamera === true;
282
+ camera.updateWorldMatrix(true, false);
283
+ const view = camera.matrixWorldInverse.elements;
284
+ this.#cameraRight.set(view[0], view[4], view[8]).normalize();
285
+ this.#cameraUp.set(view[1], view[5], view[9]).normalize();
286
+ getCameraViewSize(camera, this.#selectedWorldPosition, this.#viewSize, this.#cameraToSelected);
315
287
  }
316
288
  };
317
289
  //#endregion
@@ -10,6 +10,7 @@ import { FirstPersonControls } from "three/addons/controls/FirstPersonControls.j
10
10
  export type GamepadFirstPersonControlsOptions = GamepadControlsOptions & {
11
11
  /**
12
12
  * Multiplier on `FirstPersonControls.movementSpeed` for translation.
13
+ * Applied after normalizing combined stick and trigger movement.
13
14
  * @default 1.0
14
15
  */
15
16
  moveSpeed: number;
@@ -49,6 +50,8 @@ export type GamepadFirstPersonControlsOptions = GamepadControlsOptions & {
49
50
  * `gamepadControls.update(delta)` before `controls.update(delta)` each frame.
50
51
  * Movement follows the native keyboard frame: yaw-based translation in XZ
51
52
  * and climbing along Y, independent of camera pitch and roll.
53
+ * Combined gamepad movement is limited to unit magnitude before speed and
54
+ * forward height gains, preserving smaller analog intentions unchanged.
52
55
  * Bindings and speeds are configurable via {@link GamepadFirstPersonControlsOptions}.
53
56
  */
54
57
  export declare class GamepadFirstPersonControls extends GamepadControls {
@@ -28,6 +28,8 @@ const LOOK_SPEED_SCALE = 36e3;
28
28
  * `gamepadControls.update(delta)` before `controls.update(delta)` each frame.
29
29
  * Movement follows the native keyboard frame: yaw-based translation in XZ
30
30
  * and climbing along Y, independent of camera pitch and roll.
31
+ * Combined gamepad movement is limited to unit magnitude before speed and
32
+ * forward height gains, preserving smaller analog intentions unchanged.
31
33
  * Bindings and speeds are configurable via {@link GamepadFirstPersonControlsOptions}.
32
34
  */
33
35
  var GamepadFirstPersonControls = class extends GamepadControls {
@@ -67,8 +69,9 @@ var GamepadFirstPersonControls = class extends GamepadControls {
67
69
  }
68
70
  /**
69
71
  * Applies translation in the native keyboard frame, using yaw before look.
70
- * Forward height gain uses the initial Y position; triggers are filtered
71
- * independently before combining their proportional Y displacement.
72
+ * Triggers are filtered independently before the combined movement intent
73
+ * is limited to unit magnitude. Speed and forward height gains follow this
74
+ * normalization; height gain uses the initial Y position before climbing.
72
75
  *
73
76
  * @param deltaTime - Seconds since the last frame.
74
77
  * @param moveSpeed - User-configured movement speed multiplier.
@@ -80,25 +83,32 @@ var GamepadFirstPersonControls = class extends GamepadControls {
80
83
  #applyMovement(deltaTime, moveSpeed, moveStick, buttonDeadzone, buttonMoveUp, buttonMoveDown) {
81
84
  const controls = this.#controls;
82
85
  const input = this.gamepadInput;
83
- const moveMult = deltaTime * controls.movementSpeed * moveSpeed;
84
86
  const move = input.stick(moveStick.xAxis, moveStick.yAxis, moveStick.pipeline);
85
- if (move.x !== 0 || move.y !== 0) {
87
+ const up = input.buttonValue(buttonMoveUp);
88
+ const down = input.buttonValue(buttonMoveDown);
89
+ let strafe = move.x;
90
+ let forward = -move.y;
91
+ let climb = (up > buttonDeadzone ? up : 0) - (down > buttonDeadzone ? down : 0);
92
+ const magnitude = Math.hypot(strafe, forward, climb);
93
+ if (magnitude > 1) {
94
+ strafe /= magnitude;
95
+ forward /= magnitude;
96
+ climb /= magnitude;
97
+ }
98
+ const moveMult = deltaTime * controls.movementSpeed * moveSpeed;
99
+ if (strafe !== 0 || forward !== 0) {
86
100
  const yaw = MathUtils.degToRad(this.#getOrientation().lon);
87
101
  const sinYaw = Math.sin(yaw);
88
102
  const cosYaw = Math.cos(yaw);
89
- const forward = -move.y;
90
103
  let forwardDistance = forward * moveMult;
91
104
  if (forward > 0 && controls.heightSpeed) {
92
105
  const heightDelta = MathUtils.clamp(controls.object.position.y, controls.heightMin, controls.heightMax) - controls.heightMin;
93
106
  forwardDistance += forward * deltaTime * heightDelta * controls.heightCoef * moveSpeed;
94
107
  }
95
- const strafeDistance = move.x * moveMult;
108
+ const strafeDistance = strafe * moveMult;
96
109
  controls.object.position.x += sinYaw * forwardDistance - cosYaw * strafeDistance;
97
110
  controls.object.position.z += cosYaw * forwardDistance + sinYaw * strafeDistance;
98
111
  }
99
- const up = input.buttonValue(buttonMoveUp);
100
- const down = input.buttonValue(buttonMoveDown);
101
- const climb = (up > buttonDeadzone ? up : 0) - (down > buttonDeadzone ? down : 0);
102
112
  if (climb !== 0) controls.object.position.y += climb * moveMult;
103
113
  }
104
114
  /**
@@ -9,7 +9,9 @@ import { TransformControls } from "three/addons/controls/TransformControls.js";
9
9
  */
10
10
  export type GamepadTransformControlsOptions = GamepadControlsOptions & {
11
11
  /**
12
- * Screen-relative translation speed multiplier.
12
+ * Screen-relative translation speed multiplier. `XYZ` uses effective viewport
13
+ * width and height per second; constrained axes use their mean in world units
14
+ * per second. Both are multiplied by the processed stick input.
13
15
  * @default 1.0
14
16
  */
15
17
  translateSpeed: number;
@@ -1,6 +1,7 @@
1
1
  import { GAMEPAD_AXIS, GAMEPAD_BUTTON } 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 { getCameraViewSize } from "./three-utils.js";
4
5
  import { Matrix4, Quaternion, Vector2, Vector3 } from "three";
5
6
  //#region src/gamepad-transform-controls.ts
6
7
  const DEFAULT_TRANSFORM_OPTIONS = {
@@ -73,6 +74,7 @@ var GamepadTransformControls = class extends GamepadControls {
73
74
  #viewSize;
74
75
  #parentInverse;
75
76
  #cameraWorldPosition;
77
+ #cameraSpacePosition;
76
78
  #cameraForward;
77
79
  #cameraRight;
78
80
  #cameraUp;
@@ -135,6 +137,7 @@ var GamepadTransformControls = class extends GamepadControls {
135
137
  this.#viewSize = new Vector2();
136
138
  this.#parentInverse = new Matrix4();
137
139
  this.#cameraWorldPosition = new Vector3();
140
+ this.#cameraSpacePosition = new Vector3();
138
141
  this.#cameraForward = new Vector3();
139
142
  this.#cameraRight = new Vector3();
140
143
  this.#cameraUp = new Vector3();
@@ -847,7 +850,7 @@ var GamepadTransformControls = class extends GamepadControls {
847
850
  */
848
851
  #updateCameraState(object) {
849
852
  const camera = this.#controls.camera;
850
- camera.updateMatrixWorld();
853
+ camera.updateWorldMatrix(true, false);
851
854
  object.updateWorldMatrix(true, false);
852
855
  this.#cameraRight.setFromMatrixColumn(camera.matrixWorld, 0).normalize();
853
856
  this.#cameraUp.setFromMatrixColumn(camera.matrixWorld, 1).normalize();
@@ -855,25 +858,7 @@ var GamepadTransformControls = class extends GamepadControls {
855
858
  camera.getWorldPosition(this.#cameraWorldPosition);
856
859
  this.#worldPosition.setFromMatrixPosition(object.matrixWorld);
857
860
  this.#eye.copy(this.#cameraWorldPosition).sub(this.#worldPosition).normalize();
858
- this.#updateViewSizeAtObjectDepth(camera);
859
- }
860
- /**
861
- * Computes world-space viewport size at the attached object's depth.
862
- *
863
- * @param camera - TransformControls camera.
864
- */
865
- #updateViewSizeAtObjectDepth(camera) {
866
- if (this.#isOrthographicCamera(camera)) {
867
- this.#viewSize.set(Math.abs(camera.right - camera.left) / camera.zoom, Math.abs(camera.top - camera.bottom) / camera.zoom);
868
- return;
869
- }
870
- if (this.#isPerspectiveCamera(camera)) {
871
- const depth = Math.max(Number.EPSILON, this.#worldPosition.subVectors(this.#worldPosition, this.#cameraWorldPosition).dot(this.#cameraForward));
872
- const height = 2 * Math.tan(camera.fov * Math.PI / 360) * depth;
873
- this.#viewSize.set(height * camera.aspect, height);
874
- return;
875
- }
876
- this.#viewSize.set(1, 1);
861
+ getCameraViewSize(camera, this.#worldPosition, this.#viewSize, this.#cameraSpacePosition);
877
862
  }
878
863
  /**
879
864
  * Resolves a transform axis letter to a normalized world-space direction.
@@ -1078,24 +1063,6 @@ var GamepadTransformControls = class extends GamepadControls {
1078
1063
  for (const button of buttons) if (input.wasPressed(button)) startedButtons.add(button);
1079
1064
  return startedButtons;
1080
1065
  }
1081
- /**
1082
- * Narrows a Three.js camera to `PerspectiveCamera`.
1083
- *
1084
- * @param camera - Camera to inspect.
1085
- * @returns `true` when the camera is perspective.
1086
- */
1087
- #isPerspectiveCamera(camera) {
1088
- return camera.isPerspectiveCamera === true;
1089
- }
1090
- /**
1091
- * Narrows a Three.js camera to `OrthographicCamera`.
1092
- *
1093
- * @param camera - Camera to inspect.
1094
- * @returns `true` when the camera is orthographic.
1095
- */
1096
- #isOrthographicCamera(camera) {
1097
- return camera.isOrthographicCamera === true;
1098
- }
1099
1066
  };
1100
1067
  //#endregion
1101
1068
  export { GamepadTransformControls };
@@ -0,0 +1,27 @@
1
+ //#region src/three-utils.ts
2
+ /**
3
+ * Reads the effective viewport dimensions at a world-space point.
4
+ *
5
+ * The caller updates camera world and projection matrices and owns both reusable
6
+ * vectors. View offsets change dimensions, not the point's accumulated position.
7
+ *
8
+ * @param camera - Camera whose current projection is used.
9
+ * @param worldPosition - World-space point, left unchanged.
10
+ * @param target - Receives viewport width and height in world units.
11
+ * @param cameraPosition - Scratch vector for camera-space depth.
12
+ * @returns The target vector, or a unit viewport for an unknown camera type.
13
+ */
14
+ const getCameraViewSize = (camera, worldPosition, target, cameraPosition) => {
15
+ if (camera.isOrthographicCamera === true) {
16
+ const projection = camera.projectionMatrix.elements;
17
+ return target.set(Math.abs(2 / projection[0]), Math.abs(2 / projection[5]));
18
+ }
19
+ if (camera.isPerspectiveCamera === true) {
20
+ cameraPosition.copy(worldPosition).applyMatrix4(camera.matrixWorldInverse);
21
+ const depth = Math.max(Number.EPSILON, -cameraPosition.z);
22
+ return camera.getViewSize(depth, target);
23
+ }
24
+ return target.set(1, 1);
25
+ };
26
+ //#endregion
27
+ export { getCameraViewSize };
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.1",
4
+ "version": "0.25.3",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/luckasnix/three-gamepad-controls#readme",
7
7
  "author": {