three-gamepad-controls 0.7.0 → 0.9.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.
- package/README.md +2 -0
- package/dist/gamepad-arcball-controls.d.ts +107 -0
- package/dist/gamepad-arcball-controls.js +205 -0
- package/dist/gamepad-drag-controls.d.ts +87 -0
- package/dist/gamepad-drag-controls.js +260 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,7 +37,9 @@ bun add three-gamepad-controls
|
|
|
37
37
|
## Documentation
|
|
38
38
|
|
|
39
39
|
- [Core](./docs/core.md) — The fundamental building blocks.
|
|
40
|
+
- [GamepadArcballControls](./docs/gamepad-arcball-controls.md) - Gamepad support for `ArcballControls`.
|
|
40
41
|
- [GamepadControls](./docs/gamepad-controls.md) — Abstract base class for custom gamepad controls.
|
|
42
|
+
- [GamepadDragControls](./docs/gamepad-drag-controls.md) - Gamepad support for `DragControls`.
|
|
41
43
|
- [GamepadFirstPersonControls](./docs/gamepad-first-person-controls.md) — Gamepad support for `FirstPersonControls`.
|
|
42
44
|
- [GamepadFlyControls](./docs/gamepad-fly-controls.md) — Gamepad support for `FlyControls`.
|
|
43
45
|
- [GamepadMapControls](./docs/gamepad-map-controls.md) — Gamepad support for `MapControls`.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { GamepadControls } from "./gamepad-controls.js";
|
|
2
|
+
import { ArcballControls } from "three/addons/controls/ArcballControls.js";
|
|
3
|
+
|
|
4
|
+
//#region src/gamepad-arcball-controls.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Configuration for {@link GamepadArcballControls}.
|
|
7
|
+
*
|
|
8
|
+
* Every property has a sensible default, so you only need to pass the properties you want to override.
|
|
9
|
+
*/
|
|
10
|
+
type GamepadArcballControlsOptions = {
|
|
11
|
+
/**
|
|
12
|
+
* Multiplier on `ArcballControls.rotateSpeed` for rotation.
|
|
13
|
+
* @default 1.0
|
|
14
|
+
*/
|
|
15
|
+
rotateSpeed: number;
|
|
16
|
+
/**
|
|
17
|
+
* Multiplier on panning speed.
|
|
18
|
+
* @default 1.0
|
|
19
|
+
*/
|
|
20
|
+
panSpeed: number;
|
|
21
|
+
/**
|
|
22
|
+
* Multiplier on zooming speed.
|
|
23
|
+
* @default 1.0
|
|
24
|
+
*/
|
|
25
|
+
zoomSpeed: number;
|
|
26
|
+
/**
|
|
27
|
+
* Multiplier on z-rotation speed.
|
|
28
|
+
* @default 1.0
|
|
29
|
+
*/
|
|
30
|
+
zRotateSpeed: number;
|
|
31
|
+
/**
|
|
32
|
+
* Axis dead zone threshold in the range `[0, 1]`.
|
|
33
|
+
* @default 0.1
|
|
34
|
+
*/
|
|
35
|
+
deadzone: number;
|
|
36
|
+
/**
|
|
37
|
+
* Axis index for **horizontal** arcball rotation.
|
|
38
|
+
* @default 0 - Left stick X
|
|
39
|
+
*/
|
|
40
|
+
axisRotateX: number;
|
|
41
|
+
/**
|
|
42
|
+
* Axis index for **vertical** arcball rotation.
|
|
43
|
+
* @default 1 - Left stick Y
|
|
44
|
+
*/
|
|
45
|
+
axisRotateY: number;
|
|
46
|
+
/**
|
|
47
|
+
* Axis index for **horizontal** panning.
|
|
48
|
+
* @default 2 - Right stick X
|
|
49
|
+
*/
|
|
50
|
+
axisPanX: number;
|
|
51
|
+
/**
|
|
52
|
+
* Axis index for **vertical** panning.
|
|
53
|
+
* @default 3 - Right stick Y
|
|
54
|
+
*/
|
|
55
|
+
axisPanY: number;
|
|
56
|
+
/**
|
|
57
|
+
* Button index for zooming **in** (analog trigger value used for proportional zoom).
|
|
58
|
+
* @default 6 - Left trigger
|
|
59
|
+
*/
|
|
60
|
+
buttonZoomIn: number;
|
|
61
|
+
/**
|
|
62
|
+
* Button index for zooming **out** (analog trigger value used for proportional zoom).
|
|
63
|
+
* @default 7 - Right trigger
|
|
64
|
+
*/
|
|
65
|
+
buttonZoomOut: number;
|
|
66
|
+
/**
|
|
67
|
+
* Button index for rotating counterclockwise around the camera view axis.
|
|
68
|
+
* @default 4 - Left shoulder
|
|
69
|
+
*/
|
|
70
|
+
buttonZRotateLeft: number;
|
|
71
|
+
/**
|
|
72
|
+
* Button index for rotating clockwise around the camera view axis.
|
|
73
|
+
* @default 5 - Right shoulder
|
|
74
|
+
*/
|
|
75
|
+
buttonZRotateRight: number;
|
|
76
|
+
/**
|
|
77
|
+
* Button index for focusing the point at the center of the view.
|
|
78
|
+
* @default 0 - South face button
|
|
79
|
+
*/
|
|
80
|
+
buttonFocus: number;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Adds gamepad support to Three.js `ArcballControls`.
|
|
84
|
+
*
|
|
85
|
+
* Call `update()` inside the render loop to poll gamepad input and apply
|
|
86
|
+
* Arcball transformations. The wrapped `ArcballControls.update()` is only
|
|
87
|
+
* needed after manual camera or target changes, matching Arcball's native API.
|
|
88
|
+
*/
|
|
89
|
+
declare class GamepadArcballControls extends GamepadControls {
|
|
90
|
+
#private;
|
|
91
|
+
/**
|
|
92
|
+
* @param controls - A Three.js `ArcballControls` instance.
|
|
93
|
+
* @param options - Optional overrides for the default behavior.
|
|
94
|
+
* Any property not provided falls back to its default value.
|
|
95
|
+
*/
|
|
96
|
+
constructor(controls: ArcballControls, options?: Partial<GamepadArcballControlsOptions>);
|
|
97
|
+
/**
|
|
98
|
+
* Maps the current gamepad state to `ArcballControls` rotation, pan, zoom,
|
|
99
|
+
* z-rotation, and center focus.
|
|
100
|
+
*
|
|
101
|
+
* @param deltaTime - Seconds since the last frame.
|
|
102
|
+
* @param gamepad - Fresh gamepad snapshot provided by the base class.
|
|
103
|
+
*/
|
|
104
|
+
protected onUpdate(deltaTime: number, gamepad: Gamepad): void;
|
|
105
|
+
}
|
|
106
|
+
//#endregion
|
|
107
|
+
export { GamepadArcballControls, GamepadArcballControlsOptions };
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { GAMEPAD_AXIS, GAMEPAD_BUTTON } from "./core.js";
|
|
2
|
+
import { GamepadControls } from "./gamepad-controls.js";
|
|
3
|
+
import { Vector2, Vector3 } from "three";
|
|
4
|
+
//#region src/gamepad-arcball-controls.ts
|
|
5
|
+
/**
|
|
6
|
+
* Default options merged in the constructor when no explicit configuration is provided.
|
|
7
|
+
*/
|
|
8
|
+
const DEFAULT_ARCBALL_OPTIONS = {
|
|
9
|
+
rotateSpeed: 1,
|
|
10
|
+
panSpeed: 1,
|
|
11
|
+
zoomSpeed: 1,
|
|
12
|
+
zRotateSpeed: 1,
|
|
13
|
+
deadzone: .1,
|
|
14
|
+
axisRotateX: GAMEPAD_AXIS.LeftX,
|
|
15
|
+
axisRotateY: GAMEPAD_AXIS.LeftY,
|
|
16
|
+
axisPanX: GAMEPAD_AXIS.RightX,
|
|
17
|
+
axisPanY: GAMEPAD_AXIS.RightY,
|
|
18
|
+
buttonZoomIn: GAMEPAD_BUTTON.LeftTrigger,
|
|
19
|
+
buttonZoomOut: GAMEPAD_BUTTON.RightTrigger,
|
|
20
|
+
buttonZRotateLeft: GAMEPAD_BUTTON.LeftShoulder,
|
|
21
|
+
buttonZRotateRight: GAMEPAD_BUTTON.RightShoulder,
|
|
22
|
+
buttonFocus: GAMEPAD_BUTTON.South
|
|
23
|
+
};
|
|
24
|
+
const ZOOM_NOTCHES_PER_SECOND = 8;
|
|
25
|
+
/**
|
|
26
|
+
* Adds gamepad support to Three.js `ArcballControls`.
|
|
27
|
+
*
|
|
28
|
+
* Call `update()` inside the render loop to poll gamepad input and apply
|
|
29
|
+
* Arcball transformations. The wrapped `ArcballControls.update()` is only
|
|
30
|
+
* needed after manual camera or target changes, matching Arcball's native API.
|
|
31
|
+
*/
|
|
32
|
+
var GamepadArcballControls = class extends GamepadControls {
|
|
33
|
+
#controls;
|
|
34
|
+
#options;
|
|
35
|
+
#centerNdc;
|
|
36
|
+
#panStart;
|
|
37
|
+
#panEnd;
|
|
38
|
+
#rotationAxis;
|
|
39
|
+
#cameraForward;
|
|
40
|
+
#cameraRight;
|
|
41
|
+
#previousUp;
|
|
42
|
+
#focusButtonPressed = false;
|
|
43
|
+
#wasInteracting = false;
|
|
44
|
+
/**
|
|
45
|
+
* @param controls - A Three.js `ArcballControls` instance.
|
|
46
|
+
* @param options - Optional overrides for the default behavior.
|
|
47
|
+
* Any property not provided falls back to its default value.
|
|
48
|
+
*/
|
|
49
|
+
constructor(controls, options) {
|
|
50
|
+
super();
|
|
51
|
+
this.#controls = controls;
|
|
52
|
+
this.#options = {
|
|
53
|
+
...DEFAULT_ARCBALL_OPTIONS,
|
|
54
|
+
...options
|
|
55
|
+
};
|
|
56
|
+
this.#centerNdc = new Vector2(0, 0);
|
|
57
|
+
this.#panStart = new Vector3();
|
|
58
|
+
this.#panEnd = new Vector3();
|
|
59
|
+
this.#rotationAxis = new Vector3();
|
|
60
|
+
this.#cameraForward = new Vector3();
|
|
61
|
+
this.#cameraRight = new Vector3();
|
|
62
|
+
this.#previousUp = new Vector3();
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Maps the current gamepad state to `ArcballControls` rotation, pan, zoom,
|
|
66
|
+
* z-rotation, and center focus.
|
|
67
|
+
*
|
|
68
|
+
* @param deltaTime - Seconds since the last frame.
|
|
69
|
+
* @param gamepad - Fresh gamepad snapshot provided by the base class.
|
|
70
|
+
*/
|
|
71
|
+
onUpdate(deltaTime, gamepad) {
|
|
72
|
+
const controls = this.#controls;
|
|
73
|
+
const { rotateSpeed, panSpeed, zoomSpeed, zRotateSpeed, deadzone, axisRotateX, axisRotateY, axisPanX, axisPanY, buttonZoomIn, buttonZoomOut, buttonZRotateLeft, buttonZRotateRight, buttonFocus } = this.#options;
|
|
74
|
+
const focusPoint = this.#consumeFocusPoint(gamepad, buttonFocus);
|
|
75
|
+
if (!controls.enabled) {
|
|
76
|
+
this.#endInteraction();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const rotateX = controls.enableRotate ? this.#applyDeadzone(gamepad.axes[axisRotateX] ?? 0, deadzone) : 0;
|
|
80
|
+
const rotateY = controls.enableRotate ? this.#applyDeadzone(gamepad.axes[axisRotateY] ?? 0, deadzone) : 0;
|
|
81
|
+
const panX = controls.enablePan ? this.#applyDeadzone(gamepad.axes[axisPanX] ?? 0, deadzone) : 0;
|
|
82
|
+
const panY = controls.enablePan ? this.#applyDeadzone(gamepad.axes[axisPanY] ?? 0, deadzone) : 0;
|
|
83
|
+
const zoom = controls.enableZoom ? this.#getButtonValue(gamepad, buttonZoomIn) - this.#getButtonValue(gamepad, buttonZoomOut) : 0;
|
|
84
|
+
const zRotation = controls.enableRotate ? this.#getButtonValue(gamepad, buttonZRotateLeft) - this.#getButtonValue(gamepad, buttonZRotateRight) : 0;
|
|
85
|
+
const activeInput = rotateX !== 0 || rotateY !== 0 || panX !== 0 || panY !== 0 || Math.abs(zoom) > deadzone || Math.abs(zRotation) > deadzone;
|
|
86
|
+
if (!activeInput && focusPoint === null) {
|
|
87
|
+
this.#endInteraction();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (!this.#wasInteracting) controls.dispatchEvent({ type: "start" });
|
|
91
|
+
let changed = false;
|
|
92
|
+
changed = this.#applyRotation(deltaTime, rotateX, rotateY, rotateSpeed) || changed;
|
|
93
|
+
changed = this.#applyPan(deltaTime, panX, panY, panSpeed) || changed;
|
|
94
|
+
changed = this.#applyZoom(deltaTime, zoom, zoomSpeed, deadzone) || changed;
|
|
95
|
+
changed = this.#applyZRotation(deltaTime, zRotation, zRotateSpeed, deadzone) || changed;
|
|
96
|
+
changed = this.#applyFocus(focusPoint) || changed;
|
|
97
|
+
if (changed) {
|
|
98
|
+
controls.update();
|
|
99
|
+
controls.updateMatrixState();
|
|
100
|
+
controls.dispatchEvent({ type: "change" });
|
|
101
|
+
}
|
|
102
|
+
this.#wasInteracting = activeInput;
|
|
103
|
+
if (!activeInput) this.#endInteraction();
|
|
104
|
+
}
|
|
105
|
+
#applyRotation(deltaTime, rotateX, rotateY, rotateSpeed) {
|
|
106
|
+
if (rotateX === 0 && rotateY === 0) return false;
|
|
107
|
+
const controls = this.#controls;
|
|
108
|
+
const amount = controls.rotateSpeed * rotateSpeed * deltaTime * Math.PI;
|
|
109
|
+
let changed = false;
|
|
110
|
+
if (rotateX !== 0) {
|
|
111
|
+
this.#rotationAxis.copy(controls.object.up).normalize();
|
|
112
|
+
changed = this.#applyRotationAroundAxis(this.#rotationAxis, rotateX * amount) || changed;
|
|
113
|
+
}
|
|
114
|
+
if (rotateY !== 0) {
|
|
115
|
+
controls.object.getWorldDirection(this.#cameraForward);
|
|
116
|
+
this.#cameraRight.crossVectors(this.#cameraForward, controls.object.up).normalize();
|
|
117
|
+
changed = this.#applyRotationAroundAxis(this.#cameraRight, -rotateY * amount) || changed;
|
|
118
|
+
}
|
|
119
|
+
return changed;
|
|
120
|
+
}
|
|
121
|
+
#applyRotationAroundAxis(axis, angle) {
|
|
122
|
+
if (axis.lengthSq() === 0 || angle === 0) return false;
|
|
123
|
+
const controls = this.#controls;
|
|
124
|
+
controls.updateMatrixState();
|
|
125
|
+
this.#previousUp.copy(controls.object.up);
|
|
126
|
+
const changed = this.#applyTransform(controls.rotate(axis, angle));
|
|
127
|
+
if (changed) controls.object.up.copy(this.#previousUp).applyAxisAngle(axis, -angle);
|
|
128
|
+
return changed;
|
|
129
|
+
}
|
|
130
|
+
#applyPan(deltaTime, panX, panY, panSpeed) {
|
|
131
|
+
if (panX === 0 && panY === 0) return false;
|
|
132
|
+
const controls = this.#controls;
|
|
133
|
+
const distance = controls._tbRadius * panSpeed * deltaTime;
|
|
134
|
+
controls.updateMatrixState();
|
|
135
|
+
this.#panStart.set(0, 0, 0);
|
|
136
|
+
this.#panEnd.set(panX * distance, panY * distance, 0);
|
|
137
|
+
return this.#applyTransform(controls.pan(this.#panStart, this.#panEnd));
|
|
138
|
+
}
|
|
139
|
+
#applyZoom(deltaTime, zoom, zoomSpeed, deadzone) {
|
|
140
|
+
if (Math.abs(zoom) <= deadzone || this.#controls.scaleFactor <= 0) return false;
|
|
141
|
+
const controls = this.#controls;
|
|
142
|
+
const size = controls.scaleFactor ** (zoom * zoomSpeed * deltaTime * ZOOM_NOTCHES_PER_SECOND);
|
|
143
|
+
if (!Number.isFinite(size) || size <= 0 || size === 1) return false;
|
|
144
|
+
controls.updateMatrixState();
|
|
145
|
+
return this.#applyTransform(controls.scale(size, controls._gizmos.position));
|
|
146
|
+
}
|
|
147
|
+
#applyZRotation(deltaTime, zRotation, zRotateSpeed, deadzone) {
|
|
148
|
+
if (Math.abs(zRotation) <= deadzone) return false;
|
|
149
|
+
const controls = this.#controls;
|
|
150
|
+
const angle = zRotation * zRotateSpeed * deltaTime * Math.PI;
|
|
151
|
+
controls.updateMatrixState();
|
|
152
|
+
controls.object.getWorldDirection(controls._rotationAxis);
|
|
153
|
+
this.#previousUp.copy(controls.object.up);
|
|
154
|
+
const changed = this.#applyTransform(controls.zRotate(controls._gizmos.position, angle));
|
|
155
|
+
if (changed) controls.object.up.copy(this.#previousUp).applyAxisAngle(controls._rotationAxis, angle);
|
|
156
|
+
return changed;
|
|
157
|
+
}
|
|
158
|
+
#applyFocus(point) {
|
|
159
|
+
if (point === null) return false;
|
|
160
|
+
const controls = this.#controls;
|
|
161
|
+
controls.updateMatrixState();
|
|
162
|
+
controls.focus(point, controls.scaleFactor);
|
|
163
|
+
controls.updateMatrixState();
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
#applyTransform(transformation) {
|
|
167
|
+
if (transformation === void 0) return false;
|
|
168
|
+
this.#controls.applyTransformMatrix(transformation);
|
|
169
|
+
this.#controls.updateMatrixState();
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
#consumeFocusPoint(gamepad, buttonFocus) {
|
|
173
|
+
const controls = this.#controls;
|
|
174
|
+
const focusPressed = this.#getButtonPressed(gamepad, buttonFocus);
|
|
175
|
+
const shouldFocus = focusPressed && !this.#focusButtonPressed;
|
|
176
|
+
this.#focusButtonPressed = focusPressed;
|
|
177
|
+
if (!shouldFocus || !controls.enabled || !controls.enablePan || !controls.enableFocus || controls.scene === null) return null;
|
|
178
|
+
return controls.unprojectOnObj(this.#centerNdc, controls.object);
|
|
179
|
+
}
|
|
180
|
+
#endInteraction() {
|
|
181
|
+
if (!this.#wasInteracting) return;
|
|
182
|
+
this.#controls.dispatchEvent({ type: "end" });
|
|
183
|
+
this.#wasInteracting = false;
|
|
184
|
+
}
|
|
185
|
+
#getButtonValue(gamepad, button) {
|
|
186
|
+
const gamepadButton = gamepad.buttons[button];
|
|
187
|
+
if (gamepadButton === void 0) return 0;
|
|
188
|
+
if (gamepadButton.value !== 0) return gamepadButton.value;
|
|
189
|
+
return gamepadButton.pressed ? 1 : 0;
|
|
190
|
+
}
|
|
191
|
+
#getButtonPressed(gamepad, button) {
|
|
192
|
+
return gamepad.buttons[button]?.pressed ?? false;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Returns `value` unchanged, or `0` if below the dead zone `threshold`.
|
|
196
|
+
*
|
|
197
|
+
* @param value - Raw axis or trigger value, typically in `[-1, 1]`.
|
|
198
|
+
* @param threshold - Dead zone size; values below this magnitude are zeroed.
|
|
199
|
+
*/
|
|
200
|
+
#applyDeadzone(value, threshold) {
|
|
201
|
+
return Math.abs(value) < threshold ? 0 : value;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
//#endregion
|
|
205
|
+
export { GamepadArcballControls };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { GamepadControls } from "./gamepad-controls.js";
|
|
2
|
+
import { DragControls } from "three/addons/controls/DragControls.js";
|
|
3
|
+
|
|
4
|
+
//#region src/gamepad-drag-controls.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Configuration for {@link GamepadDragControls}.
|
|
7
|
+
*
|
|
8
|
+
* Every property has a sensible default, so you only need to pass the properties you want to override.
|
|
9
|
+
*/
|
|
10
|
+
type GamepadDragControlsOptions = {
|
|
11
|
+
/**
|
|
12
|
+
* Screen-relative translation speed multiplier.
|
|
13
|
+
* @default 1.0
|
|
14
|
+
*/
|
|
15
|
+
dragSpeed: number;
|
|
16
|
+
/**
|
|
17
|
+
* Multiplier on `DragControls.rotateSpeed` for rotation.
|
|
18
|
+
* @default 1.0
|
|
19
|
+
*/
|
|
20
|
+
rotateSpeed: number;
|
|
21
|
+
/**
|
|
22
|
+
* Axis dead zone threshold in the range `[0, 1]`.
|
|
23
|
+
* @default 0.1
|
|
24
|
+
*/
|
|
25
|
+
deadzone: number;
|
|
26
|
+
/**
|
|
27
|
+
* Axis index for **horizontal** dragging.
|
|
28
|
+
* @default 0 - Left stick X
|
|
29
|
+
*/
|
|
30
|
+
axisDragX: number;
|
|
31
|
+
/**
|
|
32
|
+
* Axis index for **vertical** dragging.
|
|
33
|
+
* @default 1 - Left stick Y
|
|
34
|
+
*/
|
|
35
|
+
axisDragY: number;
|
|
36
|
+
/**
|
|
37
|
+
* Axis index for **horizontal** object rotation.
|
|
38
|
+
* @default 2 - Right stick X
|
|
39
|
+
*/
|
|
40
|
+
axisRotateX: number;
|
|
41
|
+
/**
|
|
42
|
+
* Axis index for **vertical** object rotation.
|
|
43
|
+
* @default 3 - Right stick Y
|
|
44
|
+
*/
|
|
45
|
+
axisRotateY: number;
|
|
46
|
+
/**
|
|
47
|
+
* Button index for grabbing and dropping the object under the center reticle.
|
|
48
|
+
* @default 0 - South face button
|
|
49
|
+
*/
|
|
50
|
+
buttonSelect: number;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Adds gamepad support to Three.js `DragControls`.
|
|
54
|
+
*
|
|
55
|
+
* The center of the viewport acts as a logical reticle. Press the select button
|
|
56
|
+
* once to grab the centered object and again to drop it.
|
|
57
|
+
*/
|
|
58
|
+
declare class GamepadDragControls extends GamepadControls {
|
|
59
|
+
#private;
|
|
60
|
+
/**
|
|
61
|
+
* @param controls - A Three.js `DragControls` instance.
|
|
62
|
+
* @param options - Optional overrides for the default behavior.
|
|
63
|
+
* Any property not provided falls back to its default value.
|
|
64
|
+
*/
|
|
65
|
+
constructor(controls: DragControls, options?: Partial<GamepadDragControlsOptions>);
|
|
66
|
+
/**
|
|
67
|
+
* Maps the current gamepad state to `DragControls` hover, grab/drop, drag,
|
|
68
|
+
* and rotate behavior.
|
|
69
|
+
*
|
|
70
|
+
* @param deltaTime - Seconds since the last frame.
|
|
71
|
+
* @param gamepad - Fresh gamepad snapshot provided by the base class.
|
|
72
|
+
*/
|
|
73
|
+
protected onUpdate(deltaTime: number, gamepad: Gamepad): void;
|
|
74
|
+
/**
|
|
75
|
+
* Releases any selected object and removes hover state before disposing the
|
|
76
|
+
* gamepad lifecycle listeners.
|
|
77
|
+
*/
|
|
78
|
+
dispose(): void;
|
|
79
|
+
/**
|
|
80
|
+
* Releases the selected object if the active gamepad disconnects mid-drag.
|
|
81
|
+
*
|
|
82
|
+
* @param gamepad - The gamepad that just disconnected.
|
|
83
|
+
*/
|
|
84
|
+
protected onGamepadDisconnected(gamepad: Gamepad): void;
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
export { GamepadDragControls, GamepadDragControlsOptions };
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { GAMEPAD_AXIS, GAMEPAD_BUTTON } from "./core.js";
|
|
2
|
+
import { GamepadControls } from "./gamepad-controls.js";
|
|
3
|
+
import { Matrix4, Vector2, Vector3 } from "three";
|
|
4
|
+
//#region src/gamepad-drag-controls.ts
|
|
5
|
+
/**
|
|
6
|
+
* Default options merged in the constructor when no explicit configuration is provided.
|
|
7
|
+
*/
|
|
8
|
+
const DEFAULT_DRAG_OPTIONS = {
|
|
9
|
+
dragSpeed: 1,
|
|
10
|
+
rotateSpeed: 1,
|
|
11
|
+
deadzone: .1,
|
|
12
|
+
axisDragX: GAMEPAD_AXIS.LeftX,
|
|
13
|
+
axisDragY: GAMEPAD_AXIS.LeftY,
|
|
14
|
+
axisRotateX: GAMEPAD_AXIS.RightX,
|
|
15
|
+
axisRotateY: GAMEPAD_AXIS.RightY,
|
|
16
|
+
buttonSelect: GAMEPAD_BUTTON.South
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Adds gamepad support to Three.js `DragControls`.
|
|
20
|
+
*
|
|
21
|
+
* The center of the viewport acts as a logical reticle. Press the select button
|
|
22
|
+
* once to grab the centered object and again to drop it.
|
|
23
|
+
*/
|
|
24
|
+
var GamepadDragControls = class extends GamepadControls {
|
|
25
|
+
#controls;
|
|
26
|
+
#options;
|
|
27
|
+
#centerNdc;
|
|
28
|
+
#intersections;
|
|
29
|
+
#parentInverse;
|
|
30
|
+
#selectedWorldPosition;
|
|
31
|
+
#selectedLocalPosition;
|
|
32
|
+
#cameraWorldPosition;
|
|
33
|
+
#cameraForward;
|
|
34
|
+
#cameraRight;
|
|
35
|
+
#cameraUp;
|
|
36
|
+
#cameraToSelected;
|
|
37
|
+
#viewSize;
|
|
38
|
+
#hovered = null;
|
|
39
|
+
#selected = null;
|
|
40
|
+
#selectButtonPressed = false;
|
|
41
|
+
/**
|
|
42
|
+
* @param controls - A Three.js `DragControls` instance.
|
|
43
|
+
* @param options - Optional overrides for the default behavior.
|
|
44
|
+
* Any property not provided falls back to its default value.
|
|
45
|
+
*/
|
|
46
|
+
constructor(controls, options) {
|
|
47
|
+
super();
|
|
48
|
+
this.#controls = controls;
|
|
49
|
+
this.#options = {
|
|
50
|
+
...DEFAULT_DRAG_OPTIONS,
|
|
51
|
+
...options
|
|
52
|
+
};
|
|
53
|
+
this.#centerNdc = new Vector2(0, 0);
|
|
54
|
+
this.#intersections = [];
|
|
55
|
+
this.#parentInverse = new Matrix4();
|
|
56
|
+
this.#selectedWorldPosition = new Vector3();
|
|
57
|
+
this.#selectedLocalPosition = new Vector3();
|
|
58
|
+
this.#cameraWorldPosition = new Vector3();
|
|
59
|
+
this.#cameraForward = new Vector3();
|
|
60
|
+
this.#cameraRight = new Vector3();
|
|
61
|
+
this.#cameraUp = new Vector3();
|
|
62
|
+
this.#cameraToSelected = new Vector3();
|
|
63
|
+
this.#viewSize = new Vector2();
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Maps the current gamepad state to `DragControls` hover, grab/drop, drag,
|
|
67
|
+
* and rotate behavior.
|
|
68
|
+
*
|
|
69
|
+
* @param deltaTime - Seconds since the last frame.
|
|
70
|
+
* @param gamepad - Fresh gamepad snapshot provided by the base class.
|
|
71
|
+
*/
|
|
72
|
+
onUpdate(deltaTime, gamepad) {
|
|
73
|
+
const controls = this.#controls;
|
|
74
|
+
const selectPressed = this.#getButtonPressed(gamepad, this.#options.buttonSelect);
|
|
75
|
+
const selectStarted = selectPressed && !this.#selectButtonPressed;
|
|
76
|
+
this.#selectButtonPressed = selectPressed;
|
|
77
|
+
if (!controls.enabled) {
|
|
78
|
+
this.#releaseSelected();
|
|
79
|
+
this.#clearHover();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (this.#selected !== null) {
|
|
83
|
+
if (selectStarted) {
|
|
84
|
+
this.#releaseSelected();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
this.#updateSelected(deltaTime, gamepad);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const hit = this.#intersectCenter();
|
|
91
|
+
this.#updateHover(hit?.object ?? null);
|
|
92
|
+
if (selectStarted && hit !== void 0) this.#grabObject(hit.object);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Releases any selected object and removes hover state before disposing the
|
|
96
|
+
* gamepad lifecycle listeners.
|
|
97
|
+
*/
|
|
98
|
+
dispose() {
|
|
99
|
+
this.#releaseSelected();
|
|
100
|
+
this.#clearHover();
|
|
101
|
+
super.dispose();
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Releases the selected object if the active gamepad disconnects mid-drag.
|
|
105
|
+
*
|
|
106
|
+
* @param gamepad - The gamepad that just disconnected.
|
|
107
|
+
*/
|
|
108
|
+
onGamepadDisconnected(gamepad) {
|
|
109
|
+
this.#releaseSelected();
|
|
110
|
+
this.#clearHover();
|
|
111
|
+
super.onGamepadDisconnected(gamepad);
|
|
112
|
+
}
|
|
113
|
+
#updateSelected(deltaTime, gamepad) {
|
|
114
|
+
const selected = this.#selected;
|
|
115
|
+
if (selected === null) return;
|
|
116
|
+
const { dragSpeed, rotateSpeed, deadzone, axisDragX, axisDragY, axisRotateX, axisRotateY } = this.#options;
|
|
117
|
+
const dragX = this.#applyDeadzone(gamepad.axes[axisDragX] ?? 0, deadzone);
|
|
118
|
+
const dragY = this.#applyDeadzone(gamepad.axes[axisDragY] ?? 0, deadzone);
|
|
119
|
+
const rotateX = this.#applyDeadzone(gamepad.axes[axisRotateX] ?? 0, deadzone);
|
|
120
|
+
const rotateY = this.#applyDeadzone(gamepad.axes[axisRotateY] ?? 0, deadzone);
|
|
121
|
+
const dragged = this.#applyDrag(deltaTime, dragX, dragY, dragSpeed);
|
|
122
|
+
const rotated = this.#applyRotation(deltaTime, rotateX, rotateY, rotateSpeed);
|
|
123
|
+
if (dragged || rotated) this.#controls.dispatchEvent({
|
|
124
|
+
type: "drag",
|
|
125
|
+
object: selected
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
#applyDrag(deltaTime, dragX, dragY, dragSpeed) {
|
|
129
|
+
if (this.#selected === null || dragX === 0 && dragY === 0) return false;
|
|
130
|
+
this.#updateCameraAxes();
|
|
131
|
+
this.#updateViewSizeAtSelectedDepth();
|
|
132
|
+
const scale = dragSpeed * deltaTime;
|
|
133
|
+
this.#selectedWorldPosition.addScaledVector(this.#cameraRight, dragX * this.#viewSize.x * scale);
|
|
134
|
+
this.#selectedWorldPosition.addScaledVector(this.#cameraUp, -dragY * this.#viewSize.y * scale);
|
|
135
|
+
this.#applySelectedWorldPosition();
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
#applyRotation(deltaTime, rotateX, rotateY, rotateSpeed) {
|
|
139
|
+
const selected = this.#selected;
|
|
140
|
+
if (selected === null || rotateX === 0 && rotateY === 0) return false;
|
|
141
|
+
this.#updateCameraAxes();
|
|
142
|
+
const scale = this.#controls.rotateSpeed * rotateSpeed * deltaTime * Math.PI;
|
|
143
|
+
if (rotateX !== 0) selected.rotateOnWorldAxis(this.#cameraUp, rotateX * scale);
|
|
144
|
+
if (rotateY !== 0) selected.rotateOnWorldAxis(this.#cameraRight, rotateY * scale);
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
#intersectCenter() {
|
|
148
|
+
const controls = this.#controls;
|
|
149
|
+
this.#intersections.length = 0;
|
|
150
|
+
controls.raycaster.setFromCamera(this.#centerNdc, controls.object);
|
|
151
|
+
controls.raycaster.intersectObjects(controls.objects, controls.recursive, this.#intersections);
|
|
152
|
+
return this.#intersections[0];
|
|
153
|
+
}
|
|
154
|
+
#updateHover(object) {
|
|
155
|
+
if (this.#hovered === object) return;
|
|
156
|
+
this.#clearHover();
|
|
157
|
+
if (object === null) return;
|
|
158
|
+
this.#hovered = object;
|
|
159
|
+
this.#controls.dispatchEvent({
|
|
160
|
+
type: "hoveron",
|
|
161
|
+
object
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
#clearHover() {
|
|
165
|
+
if (this.#hovered === null) return;
|
|
166
|
+
const object = this.#hovered;
|
|
167
|
+
this.#hovered = null;
|
|
168
|
+
this.#controls.dispatchEvent({
|
|
169
|
+
type: "hoveroff",
|
|
170
|
+
object
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
#grabObject(object) {
|
|
174
|
+
const selected = this.#getSelectedObject(object);
|
|
175
|
+
selected.updateWorldMatrix(true, false);
|
|
176
|
+
this.#selected = selected;
|
|
177
|
+
this.#selectedWorldPosition.setFromMatrixPosition(selected.matrixWorld);
|
|
178
|
+
this.#controls.dispatchEvent({
|
|
179
|
+
type: "dragstart",
|
|
180
|
+
object: selected
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
#releaseSelected() {
|
|
184
|
+
if (this.#selected === null) return;
|
|
185
|
+
const selected = this.#selected;
|
|
186
|
+
this.#selected = null;
|
|
187
|
+
this.#controls.dispatchEvent({
|
|
188
|
+
type: "dragend",
|
|
189
|
+
object: selected
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
#getSelectedObject(object) {
|
|
193
|
+
if (!this.#controls.transformGroup) return object;
|
|
194
|
+
return this.#findOutermostGroup(object) ?? object;
|
|
195
|
+
}
|
|
196
|
+
#findOutermostGroup(object) {
|
|
197
|
+
let group = null;
|
|
198
|
+
let current = object;
|
|
199
|
+
while (current !== null) {
|
|
200
|
+
if (current.isGroup === true) group = current;
|
|
201
|
+
current = current.parent;
|
|
202
|
+
}
|
|
203
|
+
return group;
|
|
204
|
+
}
|
|
205
|
+
#applySelectedWorldPosition() {
|
|
206
|
+
const selected = this.#selected;
|
|
207
|
+
if (selected === null) return;
|
|
208
|
+
if (selected.parent === null) {
|
|
209
|
+
selected.position.copy(this.#selectedWorldPosition);
|
|
210
|
+
selected.updateMatrixWorld();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
selected.parent.updateWorldMatrix(true, false);
|
|
214
|
+
this.#parentInverse.copy(selected.parent.matrixWorld).invert();
|
|
215
|
+
this.#selectedLocalPosition.copy(this.#selectedWorldPosition).applyMatrix4(this.#parentInverse);
|
|
216
|
+
selected.position.copy(this.#selectedLocalPosition);
|
|
217
|
+
selected.updateMatrixWorld();
|
|
218
|
+
}
|
|
219
|
+
#updateCameraAxes() {
|
|
220
|
+
const camera = this.#controls.object;
|
|
221
|
+
this.#cameraRight.set(1, 0, 0).applyQuaternion(camera.quaternion).normalize();
|
|
222
|
+
this.#cameraUp.set(0, 1, 0).applyQuaternion(camera.quaternion).normalize();
|
|
223
|
+
camera.getWorldDirection(this.#cameraForward).normalize();
|
|
224
|
+
}
|
|
225
|
+
#updateViewSizeAtSelectedDepth() {
|
|
226
|
+
const camera = this.#controls.object;
|
|
227
|
+
if (this.#isOrthographicCamera(camera)) {
|
|
228
|
+
this.#viewSize.set(Math.abs(camera.right - camera.left) / camera.zoom, Math.abs(camera.top - camera.bottom) / camera.zoom);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (this.#isPerspectiveCamera(camera)) {
|
|
232
|
+
camera.getWorldPosition(this.#cameraWorldPosition);
|
|
233
|
+
const depth = Math.max(Number.EPSILON, this.#cameraToSelected.copy(this.#selectedWorldPosition).sub(this.#cameraWorldPosition).dot(this.#cameraForward));
|
|
234
|
+
const height = 2 * Math.tan(camera.fov * Math.PI / 360) * depth;
|
|
235
|
+
this.#viewSize.set(height * camera.aspect, height);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
this.#viewSize.set(1, 1);
|
|
239
|
+
}
|
|
240
|
+
#isPerspectiveCamera(camera) {
|
|
241
|
+
return camera.isPerspectiveCamera === true;
|
|
242
|
+
}
|
|
243
|
+
#isOrthographicCamera(camera) {
|
|
244
|
+
return camera.isOrthographicCamera === true;
|
|
245
|
+
}
|
|
246
|
+
#getButtonPressed(gamepad, button) {
|
|
247
|
+
return gamepad.buttons[button]?.pressed ?? false;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Returns `value` unchanged, or `0` if below the dead zone `threshold`.
|
|
251
|
+
*
|
|
252
|
+
* @param value - Raw axis value, typically in `[-1, 1]`.
|
|
253
|
+
* @param threshold - Dead zone size; values below this magnitude are zeroed.
|
|
254
|
+
*/
|
|
255
|
+
#applyDeadzone(value, threshold) {
|
|
256
|
+
return Math.abs(value) < threshold ? 0 : value;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
//#endregion
|
|
260
|
+
export { GamepadDragControls };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { GAMEPAD_AXIS, GAMEPAD_BUTTON, GamepadAxisKey, GamepadAxisValue, GamepadButtonKey, GamepadButtonValue } from "./core.js";
|
|
2
2
|
import { GamepadControls, GamepadControlsEventMap } from "./gamepad-controls.js";
|
|
3
|
+
import { GamepadArcballControls, GamepadArcballControlsOptions } from "./gamepad-arcball-controls.js";
|
|
4
|
+
import { GamepadDragControls, GamepadDragControlsOptions } from "./gamepad-drag-controls.js";
|
|
3
5
|
import { GamepadFirstPersonControls, GamepadFirstPersonControlsOptions } from "./gamepad-first-person-controls.js";
|
|
4
6
|
import { GamepadFlyControls, GamepadFlyControlsOptions } from "./gamepad-fly-controls.js";
|
|
5
7
|
import { GamepadOrbitControls, GamepadOrbitControlsOptions } from "./gamepad-orbit-controls.js";
|
|
6
8
|
import { GamepadMapControls } from "./gamepad-map-controls.js";
|
|
7
9
|
import { GamepadPointerLockControls, GamepadPointerLockControlsOptions } from "./gamepad-pointer-lock-controls.js";
|
|
8
10
|
import { GamepadTrackballControls, GamepadTrackballControlsOptions } from "./gamepad-trackball-controls.js";
|
|
9
|
-
export { GAMEPAD_AXIS, GAMEPAD_BUTTON, GamepadAxisKey, GamepadAxisValue, GamepadButtonKey, GamepadButtonValue, GamepadControls, GamepadControlsEventMap, GamepadFirstPersonControls, GamepadFirstPersonControlsOptions, GamepadFlyControls, GamepadFlyControlsOptions, GamepadMapControls, GamepadOrbitControls, GamepadOrbitControlsOptions, GamepadPointerLockControls, GamepadPointerLockControlsOptions, GamepadTrackballControls, GamepadTrackballControlsOptions };
|
|
11
|
+
export { GAMEPAD_AXIS, GAMEPAD_BUTTON, GamepadArcballControls, GamepadArcballControlsOptions, GamepadAxisKey, GamepadAxisValue, GamepadButtonKey, GamepadButtonValue, GamepadControls, GamepadControlsEventMap, GamepadDragControls, GamepadDragControlsOptions, GamepadFirstPersonControls, GamepadFirstPersonControlsOptions, GamepadFlyControls, GamepadFlyControlsOptions, GamepadMapControls, GamepadOrbitControls, GamepadOrbitControlsOptions, GamepadPointerLockControls, GamepadPointerLockControlsOptions, GamepadTrackballControls, GamepadTrackballControlsOptions };
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { GAMEPAD_AXIS, GAMEPAD_BUTTON } from "./core.js";
|
|
2
2
|
import { GamepadControls } from "./gamepad-controls.js";
|
|
3
|
+
import { GamepadArcballControls } from "./gamepad-arcball-controls.js";
|
|
4
|
+
import { GamepadDragControls } from "./gamepad-drag-controls.js";
|
|
3
5
|
import { GamepadFirstPersonControls } from "./gamepad-first-person-controls.js";
|
|
4
6
|
import { GamepadFlyControls } from "./gamepad-fly-controls.js";
|
|
5
7
|
import { GamepadOrbitControls } from "./gamepad-orbit-controls.js";
|
|
6
8
|
import { GamepadMapControls } from "./gamepad-map-controls.js";
|
|
7
9
|
import { GamepadPointerLockControls } from "./gamepad-pointer-lock-controls.js";
|
|
8
10
|
import { GamepadTrackballControls } from "./gamepad-trackball-controls.js";
|
|
9
|
-
export { GAMEPAD_AXIS, GAMEPAD_BUTTON, GamepadControls, GamepadFirstPersonControls, GamepadFlyControls, GamepadMapControls, GamepadOrbitControls, GamepadPointerLockControls, GamepadTrackballControls };
|
|
11
|
+
export { GAMEPAD_AXIS, GAMEPAD_BUTTON, GamepadArcballControls, GamepadControls, GamepadDragControls, GamepadFirstPersonControls, GamepadFlyControls, GamepadMapControls, GamepadOrbitControls, GamepadPointerLockControls, GamepadTrackballControls };
|
package/package.json
CHANGED