three-gamepad-controls 0.20.0 → 0.22.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 CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  Gamepad support for [Three.js](https://threejs.org) controls, built on top of [Web Gamepad API](https://developer.mozilla.org/en-US/docs/Web/API/Gamepad_API).
4
4
 
5
+ > [!WARNING]
6
+ > This library has not reached version 1.0.0 and should not yet be considered stable. Until 1.0.0, minor releases may include breaking changes.
7
+
5
8
  ## Architecture
6
9
 
7
10
  ![Layered architecture showing the shared gamepad input foundation branching into direct application input and Three.js control integrations.](./assets/architecture-diagram.webp "Three.js Gamepad Controls layered architecture")
@@ -154,9 +154,9 @@ var GamepadArcballControls = class extends GamepadControls {
154
154
  const controls = this.#controls;
155
155
  controls.updateMatrixState();
156
156
  this.#previousUp.copy(controls.object.up);
157
- const changed = this.#applyTransform(controls.rotate(axis, angle));
158
- if (changed) controls.object.up.copy(this.#previousUp).applyAxisAngle(axis, -angle);
159
- return changed;
157
+ this.#applyTransform(controls.rotate(axis, angle));
158
+ controls.object.up.copy(this.#previousUp).applyAxisAngle(axis, -angle);
159
+ return true;
160
160
  }
161
161
  /**
162
162
  * Applies gamepad pan by converting stick input to Arcball trackball points.
@@ -174,7 +174,8 @@ var GamepadArcballControls = class extends GamepadControls {
174
174
  controls.updateMatrixState();
175
175
  this.#panStart.set(0, 0, 0);
176
176
  this.#panEnd.set(panX * distance, panY * distance, 0);
177
- return this.#applyTransform(controls.pan(this.#panStart, this.#panEnd));
177
+ this.#applyTransform(controls.pan(this.#panStart, this.#panEnd));
178
+ return true;
178
179
  }
179
180
  /**
180
181
  * Applies trigger-driven zoom around Arcball's gizmo center.
@@ -191,7 +192,10 @@ var GamepadArcballControls = class extends GamepadControls {
191
192
  const size = controls.scaleFactor ** (zoom * zoomSpeed * deltaTime * ZOOM_NOTCHES_PER_SECOND);
192
193
  if (!Number.isFinite(size) || size <= 0 || size === 1) return false;
193
194
  controls.updateMatrixState();
194
- return this.#applyTransform(controls.scale(size, controls._gizmos.position));
195
+ const transformation = controls.scale(size, controls._gizmos.position);
196
+ if (transformation === void 0) return false;
197
+ this.#applyTransform(transformation);
198
+ return true;
195
199
  }
196
200
  /**
197
201
  * Applies shoulder-button rotation around the current camera view axis.
@@ -209,9 +213,9 @@ var GamepadArcballControls = class extends GamepadControls {
209
213
  controls.updateMatrixState();
210
214
  controls.object.getWorldDirection(controls._rotationAxis);
211
215
  this.#previousUp.copy(controls.object.up);
212
- const changed = this.#applyTransform(controls.zRotate(controls._gizmos.position, angle));
213
- if (changed) controls.object.up.copy(this.#previousUp).applyAxisAngle(controls._rotationAxis, angle);
214
- return changed;
216
+ this.#applyTransform(controls.zRotate(controls._gizmos.position, angle));
217
+ controls.object.up.copy(this.#previousUp).applyAxisAngle(controls._rotationAxis, angle);
218
+ return true;
215
219
  }
216
220
  /**
217
221
  * Focuses ArcballControls on the given point when one was consumed.
@@ -230,14 +234,11 @@ var GamepadArcballControls = class extends GamepadControls {
230
234
  /**
231
235
  * Applies a transformation returned by an Arcball runtime helper.
232
236
  *
233
- * @param transformation - Arcball transformation matrices, if any.
234
- * @returns `true` when a transformation was applied.
237
+ * @param transformation - Arcball transformation matrices to apply.
235
238
  */
236
239
  #applyTransform(transformation) {
237
- if (transformation === void 0) return false;
238
240
  this.#controls.applyTransformMatrix(transformation);
239
241
  this.#controls.updateMatrixState();
240
- return true;
241
242
  }
242
243
  /**
243
244
  * Consumes a focus-button press and resolves the viewport center hit point.
@@ -80,12 +80,13 @@ var GamepadDragControls = class extends GamepadControls {
80
80
  this.#clearHover();
81
81
  return;
82
82
  }
83
- if (this.#selected !== null) {
83
+ const selected = this.#selected;
84
+ if (selected !== null) {
84
85
  if (selectStarted) {
85
86
  this.#releaseSelected();
86
87
  return;
87
88
  }
88
- this.#updateSelected(deltaTime);
89
+ this.#updateSelected(selected, deltaTime);
89
90
  return;
90
91
  }
91
92
  const hit = this.#intersectCenter();
@@ -114,17 +115,16 @@ var GamepadDragControls = class extends GamepadControls {
114
115
  /**
115
116
  * Updates the selected object from gamepad drag and rotation input.
116
117
  *
118
+ * @param selected - Object selected for the current update.
117
119
  * @param deltaTime - Seconds since the last frame.
118
120
  */
119
- #updateSelected(deltaTime) {
120
- const selected = this.#selected;
121
- if (selected === null) return;
121
+ #updateSelected(selected, deltaTime) {
122
122
  const { dragSpeed, rotateSpeed, dragStick, rotateStick } = this.#options;
123
123
  const input = this.gamepadInput;
124
124
  const drag = input.stick(dragStick.xAxis, dragStick.yAxis, dragStick.pipeline);
125
125
  const rotate = input.stick(rotateStick.xAxis, rotateStick.yAxis, rotateStick.pipeline);
126
- const dragged = this.#applyDrag(deltaTime, drag.x, drag.y, dragSpeed);
127
- const rotated = this.#applyRotation(deltaTime, rotate.x, rotate.y, rotateSpeed);
126
+ const dragged = this.#applyDrag(selected, deltaTime, drag.x, drag.y, dragSpeed);
127
+ const rotated = this.#applyRotation(selected, deltaTime, rotate.x, rotate.y, rotateSpeed);
128
128
  if (dragged || rotated) this.#controls.dispatchEvent({
129
129
  type: "drag",
130
130
  object: selected
@@ -133,34 +133,35 @@ var GamepadDragControls = class extends GamepadControls {
133
133
  /**
134
134
  * Moves the selected object in the camera-facing plane.
135
135
  *
136
+ * @param selected - Object selected for the current update.
136
137
  * @param deltaTime - Seconds since the last frame.
137
138
  * @param dragX - Horizontal drag input after dead zone processing.
138
139
  * @param dragY - Vertical drag input after dead zone processing.
139
140
  * @param dragSpeed - User-configured drag speed multiplier.
140
141
  * @returns `true` when the selected object moved.
141
142
  */
142
- #applyDrag(deltaTime, dragX, dragY, dragSpeed) {
143
- if (this.#selected === null || dragX === 0 && dragY === 0) return false;
143
+ #applyDrag(selected, deltaTime, dragX, dragY, dragSpeed) {
144
+ if (dragX === 0 && dragY === 0) return false;
144
145
  this.#updateCameraAxes();
145
146
  this.#updateViewSizeAtSelectedDepth();
146
147
  const scale = dragSpeed * deltaTime;
147
148
  this.#selectedWorldPosition.addScaledVector(this.#cameraRight, dragX * this.#viewSize.x * scale);
148
149
  this.#selectedWorldPosition.addScaledVector(this.#cameraUp, -dragY * this.#viewSize.y * scale);
149
- this.#applySelectedWorldPosition();
150
+ this.#applySelectedWorldPosition(selected);
150
151
  return true;
151
152
  }
152
153
  /**
153
154
  * Rotates the selected object around camera-relative world axes.
154
155
  *
156
+ * @param selected - Object selected for the current update.
155
157
  * @param deltaTime - Seconds since the last frame.
156
158
  * @param rotateX - Horizontal rotation input after dead zone processing.
157
159
  * @param rotateY - Vertical rotation input after dead zone processing.
158
160
  * @param rotateSpeed - User-configured rotation speed multiplier.
159
161
  * @returns `true` when the selected object rotated.
160
162
  */
161
- #applyRotation(deltaTime, rotateX, rotateY, rotateSpeed) {
162
- const selected = this.#selected;
163
- if (selected === null || rotateX === 0 && rotateY === 0) return false;
163
+ #applyRotation(selected, deltaTime, rotateX, rotateY, rotateSpeed) {
164
+ if (rotateX === 0 && rotateY === 0) return false;
164
165
  this.#updateCameraAxes();
165
166
  const scale = this.#controls.rotateSpeed * rotateSpeed * deltaTime * Math.PI;
166
167
  if (rotateX !== 0) selected.rotateOnWorldAxis(this.#cameraUp, rotateX * scale);
@@ -252,9 +253,7 @@ var GamepadDragControls = class extends GamepadControls {
252
253
  }
253
254
  return group;
254
255
  }
255
- #applySelectedWorldPosition() {
256
- const selected = this.#selected;
257
- if (selected === null) return;
256
+ #applySelectedWorldPosition(selected) {
258
257
  if (selected.parent === null) {
259
258
  selected.position.copy(this.#selectedWorldPosition);
260
259
  selected.updateMatrixWorld();
@@ -10,6 +10,10 @@ type GamepadStickProcessingMode = "axial" | "radial";
10
10
  * Built-in response curve applied to stick components or magnitude.
11
11
  */
12
12
  type GamepadResponseCurve = "linear" | "quadratic" | "cubic";
13
+ /**
14
+ * Stick component selection used by inversion processing.
15
+ */
16
+ type GamepadStickInversionAxis = "x" | "y" | "both";
13
17
  /**
14
18
  * Two-dimensional gamepad stick value.
15
19
  */
@@ -23,6 +27,10 @@ type GamepadStick = {
23
27
  */
24
28
  y: number;
25
29
  };
30
+ /**
31
+ * Pure custom transformation applied to a gamepad stick.
32
+ */
33
+ type GamepadStickTransform = (value: Readonly<GamepadStick>) => GamepadStick;
26
34
  /**
27
35
  * Pure, stateless transformation applied to a gamepad stick.
28
36
  *
@@ -39,26 +47,21 @@ type GamepadStickProcessor = {
39
47
  process(value: Readonly<GamepadStick>): GamepadStick;
40
48
  };
41
49
  /**
42
- * Ordered, reusable sequence of stateless stick processors.
50
+ * Options for {@link gamepadStickPipeline}.
43
51
  */
44
- type GamepadStickPipeline = GamepadStickProcessor & {
52
+ type GamepadStickPipelineOptions = {
45
53
  /**
46
- * Processors in execution order.
54
+ * Default geometry for processing methods that support axial and radial modes.
55
+ * @default "axial"
47
56
  */
48
- readonly processors: readonly GamepadStickProcessor[];
57
+ mode?: GamepadStickProcessingMode;
49
58
  };
50
59
  /**
51
- * Options for {@link createGamepadDeadzoneProcessor}.
60
+ * Options for {@link GamepadStickPipeline.deadzone}.
52
61
  */
53
- type GamepadDeadzoneProcessorOptions = {
54
- /**
55
- * Dead zone threshold.
56
- * @default 0.1
57
- */
58
- threshold?: number;
62
+ type GamepadDeadzoneOptions = {
59
63
  /**
60
- * Whether to process each component or the vector magnitude.
61
- * @default "axial"
64
+ * Geometry for this dead zone, overriding the pipeline default.
62
65
  */
63
66
  mode?: GamepadStickProcessingMode;
64
67
  /**
@@ -68,32 +71,58 @@ type GamepadDeadzoneProcessorOptions = {
68
71
  rescale?: boolean;
69
72
  };
70
73
  /**
71
- * Options for {@link createGamepadResponseCurveProcessor}.
74
+ * Options for {@link GamepadStickPipeline.responseCurve}.
72
75
  */
73
- type GamepadResponseCurveProcessorOptions = {
74
- /**
75
- * Curve used to transform values in the normalized input range.
76
- */
77
- curve: GamepadResponseCurve;
76
+ type GamepadResponseCurveOptions = {
78
77
  /**
79
- * Whether to process each component or the vector magnitude.
78
+ * Geometry for this response curve, overriding the pipeline default.
80
79
  */
81
- mode: GamepadStickProcessingMode;
80
+ mode?: GamepadStickProcessingMode;
82
81
  };
83
82
  /**
84
- * Options for {@link createGamepadInversionProcessor}.
83
+ * Ordered, reusable sequence of stateless stick processors.
84
+ *
85
+ * Every configuration method returns a new frozen pipeline and leaves the
86
+ * current pipeline unchanged.
85
87
  */
86
- type GamepadInversionProcessorOptions = {
88
+ type GamepadStickPipeline = GamepadStickProcessor & {
87
89
  /**
88
- * Whether to invert the horizontal component.
89
- * @default false
90
+ * Appends an axial or radial dead zone.
91
+ *
92
+ * @param threshold - Dead zone threshold.
93
+ * @param options - Optional dead zone configuration.
94
+ * @returns New pipeline containing the dead zone.
90
95
  */
91
- invertX?: boolean;
96
+ deadzone(threshold?: number, options?: GamepadDeadzoneOptions): GamepadStickPipeline;
92
97
  /**
93
- * Whether to invert the vertical component.
94
- * @default false
98
+ * Appends an axial or radial response curve.
99
+ *
100
+ * @param curve - Response curve used to transform normalized magnitudes.
101
+ * @param options - Optional response curve configuration.
102
+ * @returns New pipeline containing the response curve.
103
+ */
104
+ responseCurve(curve: GamepadResponseCurve, options?: GamepadResponseCurveOptions): GamepadStickPipeline;
105
+ /**
106
+ * Appends component inversion.
107
+ *
108
+ * @param axis - Component or components to invert.
109
+ * @returns New pipeline containing the inversion.
110
+ */
111
+ invert(axis: GamepadStickInversionAxis): GamepadStickPipeline;
112
+ /**
113
+ * Appends a custom stick transformation.
114
+ *
115
+ * @param operation - Pure transformation applied to the current value.
116
+ * @returns New pipeline containing the transformation.
117
+ */
118
+ transform(operation: GamepadStickTransform): GamepadStickPipeline;
119
+ /**
120
+ * Appends a reusable stick processor, including another pipeline.
121
+ *
122
+ * @param processor - Processor to execute next.
123
+ * @returns New pipeline containing the processor.
95
124
  */
96
- invertY?: boolean;
125
+ pipe(processor: GamepadStickProcessor): GamepadStickPipeline;
97
126
  };
98
127
  /**
99
128
  * Partial binding for a two-dimensional stick action.
@@ -119,36 +148,16 @@ type GamepadStickBindingOptions = {
119
148
  */
120
149
  type GamepadStickBinding = Required<GamepadStickBindingOptions>;
121
150
  /**
122
- * Creates an ordered pipeline from stateless stick processors.
151
+ * Creates an immutable, fluent stick processing pipeline.
123
152
  *
124
- * The processor list is copied once. Processor errors are not caught, and a
125
- * processor's result is passed directly to the next processor.
126
- *
127
- * @param processors - Processors in execution order.
128
- * @returns Reusable stick pipeline.
129
- */
130
- declare const createGamepadStickPipeline: (...processors: readonly GamepadStickProcessor[]) => GamepadStickPipeline;
131
- /**
132
- * Creates an axial or radial dead zone processor.
133
- *
134
- * @param options - Optional dead zone configuration.
135
- * @returns Stateless dead zone processor.
136
- */
137
- declare const createGamepadDeadzoneProcessor: (options?: GamepadDeadzoneProcessorOptions) => GamepadStickProcessor;
138
- /**
139
- * Creates an axial or radial response curve processor.
140
- *
141
- * @param options - Response curve configuration.
142
- * @returns Stateless response curve processor.
143
- */
144
- declare const createGamepadResponseCurveProcessor: (options: GamepadResponseCurveProcessorOptions) => GamepadStickProcessor;
145
- /**
146
- * Creates a processor that independently inverts stick components.
153
+ * The configured mode becomes the default for dead zones and response curves.
154
+ * Each fluent method returns a new pipeline, so intermediate pipelines remain
155
+ * reusable. Processor errors are not caught.
147
156
  *
148
- * @param options - Optional inversion configuration.
149
- * @returns Stateless inversion processor.
157
+ * @param options - Optional pipeline defaults.
158
+ * @returns Frozen stick pipeline.
150
159
  */
151
- declare const createGamepadInversionProcessor: (options?: GamepadInversionProcessorOptions) => GamepadStickProcessor;
160
+ declare const gamepadStickPipeline: (options?: GamepadStickPipelineOptions) => GamepadStickPipeline;
152
161
  /**
153
162
  * Historical stick processing used when no custom pipeline is supplied.
154
163
  */
@@ -162,4 +171,4 @@ declare const DEFAULT_GAMEPAD_STICK_PIPELINE: GamepadStickPipeline;
162
171
  */
163
172
  declare const resolveGamepadStickBinding: (defaults: GamepadStickBinding, options?: GamepadStickBindingOptions) => GamepadStickBinding;
164
173
  //#endregion
165
- export { DEFAULT_GAMEPAD_STICK_PIPELINE, GamepadDeadzoneProcessorOptions, GamepadInversionProcessorOptions, GamepadResponseCurve, GamepadResponseCurveProcessorOptions, GamepadStick, GamepadStickBinding, GamepadStickBindingOptions, GamepadStickPipeline, GamepadStickProcessingMode, GamepadStickProcessor, createGamepadDeadzoneProcessor, createGamepadInversionProcessor, createGamepadResponseCurveProcessor, createGamepadStickPipeline, resolveGamepadStickBinding };
174
+ export { DEFAULT_GAMEPAD_STICK_PIPELINE, GamepadDeadzoneOptions, GamepadResponseCurve, GamepadResponseCurveOptions, GamepadStick, GamepadStickBinding, GamepadStickBindingOptions, GamepadStickInversionAxis, GamepadStickPipeline, GamepadStickPipelineOptions, GamepadStickProcessingMode, GamepadStickProcessor, GamepadStickTransform, gamepadStickPipeline, resolveGamepadStickBinding };
@@ -1,13 +1,6 @@
1
1
  //#region src/gamepad-stick-processing.ts
2
- const DEFAULT_DEADZONE_PROCESSOR_OPTIONS = {
3
- threshold: .1,
4
- mode: "axial",
5
- rescale: false
6
- };
7
- const DEFAULT_INVERSION_PROCESSOR_OPTIONS = {
8
- invertX: false,
9
- invertY: false
10
- };
2
+ const DEFAULT_GAMEPAD_STICK_PROCESSING_MODE = "axial";
3
+ const DEFAULT_GAMEPAD_DEADZONE_THRESHOLD = .1;
11
4
  /**
12
5
  * Returns a canonical stick result and preserves the original value when
13
6
  * processing produced no change.
@@ -65,13 +58,13 @@ const rescaleGamepadDeadzoneMagnitude = (magnitude, threshold) => {
65
58
  return Math.min((magnitude - threshold) / (1 - threshold), 1);
66
59
  };
67
60
  /**
68
- * Applies a built-in curve to a normalized magnitude.
61
+ * Applies a built-in response curve to a normalized magnitude.
69
62
  *
70
63
  * Magnitudes above `1` remain unchanged so response curves do not implicitly
71
64
  * clamp or normalize custom processor output.
72
65
  *
73
66
  * @param magnitude - Non-negative component or vector magnitude.
74
- * @param curve - Curve to apply.
67
+ * @param curve - Response curve to apply.
75
68
  * @returns Curved magnitude.
76
69
  */
77
70
  const applyGamepadResponseCurve = (magnitude, curve) => {
@@ -83,36 +76,14 @@ const applyGamepadResponseCurve = (magnitude, curve) => {
83
76
  }
84
77
  };
85
78
  /**
86
- * Creates an ordered pipeline from stateless stick processors.
87
- *
88
- * The processor list is copied once. Processor errors are not caught, and a
89
- * processor's result is passed directly to the next processor.
90
- *
91
- * @param processors - Processors in execution order.
92
- * @returns Reusable stick pipeline.
93
- */
94
- const createGamepadStickPipeline = (...processors) => {
95
- const pipelineProcessors = Object.freeze([...processors]);
96
- return Object.freeze({
97
- processors: pipelineProcessors,
98
- process(value) {
99
- let processed = value;
100
- for (const processor of pipelineProcessors) processed = processor.process(processed);
101
- return processed;
102
- }
103
- });
104
- };
105
- /**
106
79
  * Creates an axial or radial dead zone processor.
107
80
  *
108
- * @param options - Optional dead zone configuration.
109
- * @returns Stateless dead zone processor.
81
+ * @param threshold - Dead zone threshold.
82
+ * @param mode - Whether to process components or vector magnitude.
83
+ * @param rescale - Whether to remap values outside the dead zone.
84
+ * @returns Frozen dead zone processor.
110
85
  */
111
- const createGamepadDeadzoneProcessor = (options) => {
112
- const { threshold, mode, rescale } = {
113
- ...DEFAULT_DEADZONE_PROCESSOR_OPTIONS,
114
- ...options
115
- };
86
+ const createDeadzoneProcessor = (threshold, mode, rescale) => {
116
87
  const mapMagnitude = (magnitude) => {
117
88
  if (magnitude < threshold) return 0;
118
89
  return rescale ? rescaleGamepadDeadzoneMagnitude(magnitude, threshold) : magnitude;
@@ -125,11 +96,11 @@ const createGamepadDeadzoneProcessor = (options) => {
125
96
  /**
126
97
  * Creates an axial or radial response curve processor.
127
98
  *
128
- * @param options - Response curve configuration.
129
- * @returns Stateless response curve processor.
99
+ * @param curve - Response curve applied to normalized magnitudes.
100
+ * @param mode - Whether to process components or vector magnitude.
101
+ * @returns Frozen response curve processor.
130
102
  */
131
- const createGamepadResponseCurveProcessor = (options) => {
132
- const { curve, mode } = options;
103
+ const createResponseCurveProcessor = (curve, mode) => {
133
104
  const mapMagnitude = (magnitude) => applyGamepadResponseCurve(magnitude, curve);
134
105
  return Object.freeze({ process(value) {
135
106
  if (mode === "axial") return createGamepadStickResult(value, mapGamepadSignedMagnitude(value.x, mapMagnitude), mapGamepadSignedMagnitude(value.y, mapMagnitude));
@@ -137,27 +108,79 @@ const createGamepadResponseCurveProcessor = (options) => {
137
108
  } });
138
109
  };
139
110
  /**
140
- * Creates a processor that independently inverts stick components.
111
+ * Creates a processor that inverts selected stick components.
141
112
  *
142
- * @param options - Optional inversion configuration.
143
- * @returns Stateless inversion processor.
113
+ * @param axis - Component or components to invert.
114
+ * @returns Frozen inversion processor.
144
115
  */
145
- const createGamepadInversionProcessor = (options) => {
146
- const { invertX, invertY } = {
147
- ...DEFAULT_INVERSION_PROCESSOR_OPTIONS,
148
- ...options
149
- };
116
+ const createInversionProcessor = (axis) => {
117
+ const invertX = axis === "x" || axis === "both";
118
+ const invertY = axis === "y" || axis === "both";
150
119
  return Object.freeze({ process(value) {
151
- if (!invertX && !invertY) return createGamepadStickResult(value, value.x, value.y);
152
120
  const x = value.x === 0 ? 0 : invertX ? -value.x : value.x;
153
121
  const y = value.y === 0 ? 0 : invertY ? -value.y : value.y;
154
122
  return createGamepadStickResult(value, x, y);
155
123
  } });
156
124
  };
157
125
  /**
126
+ * Creates a frozen custom transformation processor.
127
+ *
128
+ * @param operation - Transformation applied to the current pipeline value.
129
+ * @returns Frozen custom processor.
130
+ */
131
+ const createTransformProcessor = (operation) => {
132
+ return Object.freeze({ process: operation });
133
+ };
134
+ /**
135
+ * Creates an immutable pipeline over a private processor sequence.
136
+ *
137
+ * @param mode - Default processing mode for new built-in stages.
138
+ * @param processors - Processors in execution order.
139
+ * @returns Frozen stick pipeline.
140
+ */
141
+ const createPipeline = (mode, processors) => {
142
+ const pipelineProcessors = Object.freeze([...processors]);
143
+ const append = (processor) => createPipeline(mode, [...pipelineProcessors, processor]);
144
+ return Object.freeze({
145
+ process(value) {
146
+ let processed = value;
147
+ for (const processor of pipelineProcessors) processed = processor.process(processed);
148
+ return processed;
149
+ },
150
+ deadzone(threshold = DEFAULT_GAMEPAD_DEADZONE_THRESHOLD, options) {
151
+ return append(createDeadzoneProcessor(threshold, options?.mode ?? mode, options?.rescale ?? false));
152
+ },
153
+ responseCurve(curve, options) {
154
+ return append(createResponseCurveProcessor(curve, options?.mode ?? mode));
155
+ },
156
+ invert(axis) {
157
+ return append(createInversionProcessor(axis));
158
+ },
159
+ transform(operation) {
160
+ return append(createTransformProcessor(operation));
161
+ },
162
+ pipe(processor) {
163
+ return append(processor);
164
+ }
165
+ });
166
+ };
167
+ /**
168
+ * Creates an immutable, fluent stick processing pipeline.
169
+ *
170
+ * The configured mode becomes the default for dead zones and response curves.
171
+ * Each fluent method returns a new pipeline, so intermediate pipelines remain
172
+ * reusable. Processor errors are not caught.
173
+ *
174
+ * @param options - Optional pipeline defaults.
175
+ * @returns Frozen stick pipeline.
176
+ */
177
+ const gamepadStickPipeline = (options) => {
178
+ return createPipeline(options?.mode ?? DEFAULT_GAMEPAD_STICK_PROCESSING_MODE, []);
179
+ };
180
+ /**
158
181
  * Historical stick processing used when no custom pipeline is supplied.
159
182
  */
160
- const DEFAULT_GAMEPAD_STICK_PIPELINE = createGamepadStickPipeline(createGamepadDeadzoneProcessor());
183
+ const DEFAULT_GAMEPAD_STICK_PIPELINE = gamepadStickPipeline().deadzone();
161
184
  /**
162
185
  * Resolves a partial stick binding over an action-specific default.
163
186
  *
@@ -173,4 +196,4 @@ const resolveGamepadStickBinding = (defaults, options) => {
173
196
  };
174
197
  };
175
198
  //#endregion
176
- export { DEFAULT_GAMEPAD_STICK_PIPELINE, createGamepadDeadzoneProcessor, createGamepadInversionProcessor, createGamepadResponseCurveProcessor, createGamepadStickPipeline, resolveGamepadStickBinding };
199
+ export { DEFAULT_GAMEPAD_STICK_PIPELINE, gamepadStickPipeline, resolveGamepadStickBinding };
@@ -162,11 +162,13 @@ var GamepadTransformControls = class extends GamepadControls {
162
162
  this.#handleModeAndAxisButtons(startedButtons);
163
163
  if (startedButtons.has(this.#options.buttonReset)) this.#resetActiveTransform();
164
164
  const controls = this.#controls;
165
- if (!controls.enabled || controls.object === void 0) {
165
+ const object = controls.object;
166
+ if (!controls.enabled || object === void 0) {
166
167
  this.#endTransform(true);
167
168
  return;
168
169
  }
169
- if (this.#ensureValidAxis() === null) {
170
+ const axis = this.#ensureValidAxis();
171
+ if (axis === null) {
170
172
  this.#endTransform(true);
171
173
  return;
172
174
  }
@@ -176,8 +178,8 @@ var GamepadTransformControls = class extends GamepadControls {
176
178
  this.#endTransform(false);
177
179
  return;
178
180
  }
179
- if (!this.#isTransforming && !this.#startTransform()) return;
180
- if (this.#applyCurrentTransform(deltaTime, transform.x, transform.y)) {
181
+ if (!this.#isTransforming) this.#startTransform(object);
182
+ if (this.#applyCurrentTransform(object, axis, deltaTime, transform.x, transform.y)) {
181
183
  controls.dispatchEvent({ type: "change" });
182
184
  controls.dispatchEvent({ type: "objectChange" });
183
185
  }
@@ -270,7 +272,7 @@ var GamepadTransformControls = class extends GamepadControls {
270
272
  const current = this.#activeAxisByMode[this.#controls.mode];
271
273
  const currentIndex = current === null ? -1 : axes.indexOf(current);
272
274
  const nextIndex = currentIndex === -1 ? 0 : (currentIndex + direction + axes.length) % axes.length;
273
- this.#activeAxisByMode[this.#controls.mode] = axes[nextIndex] ?? null;
275
+ this.#activeAxisByMode[this.#controls.mode] = axes[nextIndex];
274
276
  this.#ensureValidAxis();
275
277
  }
276
278
  /**
@@ -349,12 +351,10 @@ var GamepadTransformControls = class extends GamepadControls {
349
351
  /**
350
352
  * Starts a TransformControls drag interaction for the active object and axis.
351
353
  *
352
- * @returns `true` when a transform interaction was started.
354
+ * @param object - Object attached to TransformControls for this update.
353
355
  */
354
- #startTransform() {
356
+ #startTransform(object) {
355
357
  const controls = this.#controls;
356
- const object = controls.object;
357
- if (object === void 0 || controls.axis === null) return false;
358
358
  this.#captureTransformStart(object);
359
359
  controls.dragging = true;
360
360
  this.#isTransforming = true;
@@ -362,7 +362,6 @@ var GamepadTransformControls = class extends GamepadControls {
362
362
  type: "mouseDown",
363
363
  mode: controls.mode
364
364
  });
365
- return true;
366
365
  }
367
366
  /**
368
367
  * Ends an active TransformControls drag interaction.
@@ -435,35 +434,36 @@ var GamepadTransformControls = class extends GamepadControls {
435
434
  /**
436
435
  * Dispatches the current stick input to the active TransformControls mode.
437
436
  *
437
+ * @param object - Object attached to TransformControls for this update.
438
+ * @param axis - Valid axis selected for the active mode.
438
439
  * @param deltaTime - Seconds since the last frame.
439
440
  * @param transformX - Horizontal transform input after dead zone processing.
440
441
  * @param transformY - Vertical transform input after dead zone processing.
441
442
  * @returns `true` when the attached object changed.
442
443
  */
443
- #applyCurrentTransform(deltaTime, transformX, transformY) {
444
+ #applyCurrentTransform(object, axis, deltaTime, transformX, transformY) {
444
445
  switch (this.#controls.mode) {
445
- case "translate": return this.#applyTranslate(deltaTime, transformX, transformY);
446
- case "rotate": return this.#applyRotate(deltaTime, transformX, transformY);
447
- case "scale": return this.#applyScale(deltaTime, transformX, transformY);
446
+ case "translate": return this.#applyTranslate(object, axis, deltaTime, transformX, transformY);
447
+ case "rotate": return this.#applyRotate(object, axis, deltaTime, transformX, transformY);
448
+ case "scale": return this.#applyScale(object, axis, deltaTime, transformX, transformY);
448
449
  }
449
450
  }
450
451
  /**
451
452
  * Applies translation in the selected axis, plane, or screen-facing plane.
452
453
  *
454
+ * @param object - Object attached to TransformControls for this update.
455
+ * @param axis - Valid translation axis selected for this update.
453
456
  * @param deltaTime - Seconds since the last frame.
454
457
  * @param transformX - Horizontal transform input after dead zone processing.
455
458
  * @param transformY - Vertical transform input after dead zone processing.
456
459
  * @returns `true` when the attached object moved.
457
460
  */
458
- #applyTranslate(deltaTime, transformX, transformY) {
461
+ #applyTranslate(object, axis, deltaTime, transformX, transformY) {
459
462
  const controls = this.#controls;
460
- const object = controls.object;
461
- const axis = controls.axis;
462
- if (object === void 0 || axis === null) return false;
463
463
  this.#updateCameraState(object);
464
464
  this.#worldDelta.set(0, 0, 0);
465
- const scale = controls.axis === "XYZ" ? "world" : controls.space;
466
- const speed = controls.axis === "XYZ" ? this.#options.translateSpeed * deltaTime : (this.#viewSize.x + this.#viewSize.y) / 2 * this.#options.translateSpeed * deltaTime;
465
+ const scale = axis === "XYZ" ? "world" : controls.space;
466
+ const speed = axis === "XYZ" ? this.#options.translateSpeed * deltaTime : (this.#viewSize.x + this.#viewSize.y) / 2 * this.#options.translateSpeed * deltaTime;
467
467
  if (axis === "XYZ") {
468
468
  this.#worldDelta.addScaledVector(this.#cameraRight, transformX * this.#viewSize.x * speed);
469
469
  this.#worldDelta.addScaledVector(this.#cameraUp, -transformY * this.#viewSize.y * speed);
@@ -542,16 +542,15 @@ var GamepadTransformControls = class extends GamepadControls {
542
542
  /**
543
543
  * Applies rotation for the selected axis or free-rotation mode.
544
544
  *
545
+ * @param object - Object attached to TransformControls for this update.
546
+ * @param axis - Valid rotation axis selected for this update.
545
547
  * @param deltaTime - Seconds since the last frame.
546
548
  * @param transformX - Horizontal transform input after dead zone processing.
547
549
  * @param transformY - Vertical transform input after dead zone processing.
548
550
  * @returns `true` when the attached object rotated.
549
551
  */
550
- #applyRotate(deltaTime, transformX, transformY) {
552
+ #applyRotate(object, axis, deltaTime, transformX, transformY) {
551
553
  const controls = this.#controls;
552
- const object = controls.object;
553
- const axis = controls.axis;
554
- if (object === void 0 || axis === null) return false;
555
554
  this.#updateCameraState(object);
556
555
  const angleScale = this.#options.rotateSpeed * deltaTime * Math.PI;
557
556
  if (axis === "XYZE") {
@@ -564,11 +563,11 @@ var GamepadTransformControls = class extends GamepadControls {
564
563
  if (axis === "E") {
565
564
  this.#axisWorld.copy(this.#cameraForward).normalize();
566
565
  input = this.#getDominantInput(transformX, -transformY);
567
- } else if (axis === "X" || axis === "Y" || axis === "Z") {
566
+ } else {
568
567
  const space = controls.space;
569
568
  this.#getTransformAxisWorld(axis, space, this.#axisWorld);
570
569
  input = this.#getRotationAxisInput(this.#axisWorld, transformX, transformY);
571
- } else return false;
570
+ }
572
571
  this.#rotationAmount += input * angleScale;
573
572
  const angle = this.#snapRotation(this.#rotationAmount);
574
573
  if (axis !== "E" && controls.space === "local") {
@@ -611,16 +610,15 @@ var GamepadTransformControls = class extends GamepadControls {
611
610
  /**
612
611
  * Applies scale along the selected axis or uniformly across all axes.
613
612
  *
613
+ * @param object - Object attached to TransformControls for this update.
614
+ * @param axis - Valid scale axis selected for this update.
614
615
  * @param deltaTime - Seconds since the last frame.
615
616
  * @param transformX - Horizontal transform input after dead zone processing.
616
617
  * @param transformY - Vertical transform input after dead zone processing.
617
618
  * @returns `true` when the attached object scaled.
618
619
  */
619
- #applyScale(deltaTime, transformX, transformY) {
620
+ #applyScale(object, axis, deltaTime, transformX, transformY) {
620
621
  const controls = this.#controls;
621
- const object = controls.object;
622
- const axis = controls.axis;
623
- if (object === void 0 || axis === null) return false;
624
622
  this.#updateCameraState(object);
625
623
  const input = axis === "XYZ" ? this.#getDominantInput(transformX, -transformY) : this.#getProjectedScaleInput(axis, transformX, transformY);
626
624
  const factor = Math.exp(input * this.#options.scaleSpeed * deltaTime);
@@ -647,7 +645,6 @@ var GamepadTransformControls = class extends GamepadControls {
647
645
  * @returns Signed scale input for the current frame.
648
646
  */
649
647
  #getProjectedScaleInput(axis, transformX, transformY) {
650
- if (axis !== "X" && axis !== "Y" && axis !== "Z") return this.#getDominantInput(transformX, -transformY);
651
648
  this.#getTransformAxisWorld(axis, "local", this.#axisWorld);
652
649
  return this.#getProjectedAxisInput(this.#axisWorld, transformX, transformY, this.#getDominantInput(transformX, -transformY));
653
650
  }
@@ -775,9 +772,7 @@ var GamepadTransformControls = class extends GamepadControls {
775
772
  * @param target - Vector to adjust in place.
776
773
  */
777
774
  #divideByParentScale(target) {
778
- target.x = this.#parentScale.x === 0 ? 0 : target.x / this.#parentScale.x;
779
- target.y = this.#parentScale.y === 0 ? 0 : target.y / this.#parentScale.y;
780
- target.z = this.#parentScale.z === 0 ? 0 : target.z / this.#parentScale.z;
775
+ target.divide(this.#parentScale);
781
776
  }
782
777
  /**
783
778
  * Writes a unit axis vector into a target vector.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { GAMEPAD_AXIS, GAMEPAD_BUTTON, GamepadAxisKey, GamepadAxisValue, GamepadButtonKey, GamepadButtonValue, MAX_GAMEPAD_INDEX, MIN_GAMEPAD_INDEX } from "./core.js";
2
- import { DEFAULT_GAMEPAD_STICK_PIPELINE, GamepadDeadzoneProcessorOptions, GamepadInversionProcessorOptions, GamepadResponseCurve, GamepadResponseCurveProcessorOptions, GamepadStick, GamepadStickBinding, GamepadStickBindingOptions, GamepadStickPipeline, GamepadStickProcessingMode, GamepadStickProcessor, createGamepadDeadzoneProcessor, createGamepadInversionProcessor, createGamepadResponseCurveProcessor, createGamepadStickPipeline, resolveGamepadStickBinding } from "./gamepad-stick-processing.js";
2
+ import { DEFAULT_GAMEPAD_STICK_PIPELINE, GamepadDeadzoneOptions, GamepadResponseCurve, GamepadResponseCurveOptions, GamepadStick, GamepadStickBinding, GamepadStickBindingOptions, GamepadStickInversionAxis, GamepadStickPipeline, GamepadStickPipelineOptions, GamepadStickProcessingMode, GamepadStickProcessor, GamepadStickTransform, gamepadStickPipeline, resolveGamepadStickBinding } from "./gamepad-stick-processing.js";
3
3
  import { GamepadAxisOptions, GamepadInput, GamepadInputEventMap, GamepadInputOptions } from "./gamepad-input.js";
4
4
  import { GamepadControls, GamepadControlsEventMap, GamepadControlsOptions } from "./gamepad-controls.js";
5
5
  import { GamepadArcballControls, GamepadArcballControlsOptions } from "./gamepad-arcball-controls.js";
@@ -11,4 +11,4 @@ import { GamepadMapControls } from "./gamepad-map-controls.js";
11
11
  import { GamepadPointerLockControls, GamepadPointerLockControlsOptions } from "./gamepad-pointer-lock-controls.js";
12
12
  import { GamepadTrackballControls, GamepadTrackballControlsOptions } from "./gamepad-trackball-controls.js";
13
13
  import { GamepadTransformControls, GamepadTransformControlsOptions } from "./gamepad-transform-controls.js";
14
- export { DEFAULT_GAMEPAD_STICK_PIPELINE, GAMEPAD_AXIS, GAMEPAD_BUTTON, GamepadArcballControls, GamepadArcballControlsOptions, GamepadAxisKey, GamepadAxisOptions, GamepadAxisValue, GamepadButtonKey, GamepadButtonValue, GamepadControls, GamepadControlsEventMap, GamepadControlsOptions, GamepadDeadzoneProcessorOptions, GamepadDragControls, GamepadDragControlsOptions, GamepadFirstPersonControls, GamepadFirstPersonControlsOptions, GamepadFlyControls, GamepadFlyControlsOptions, GamepadInput, GamepadInputEventMap, GamepadInputOptions, GamepadInversionProcessorOptions, GamepadMapControls, GamepadOrbitControls, GamepadOrbitControlsOptions, GamepadPointerLockControls, GamepadPointerLockControlsOptions, GamepadResponseCurve, GamepadResponseCurveProcessorOptions, GamepadStick, GamepadStickBinding, GamepadStickBindingOptions, GamepadStickPipeline, GamepadStickProcessingMode, GamepadStickProcessor, GamepadTrackballControls, GamepadTrackballControlsOptions, GamepadTransformControls, GamepadTransformControlsOptions, MAX_GAMEPAD_INDEX, MIN_GAMEPAD_INDEX, createGamepadDeadzoneProcessor, createGamepadInversionProcessor, createGamepadResponseCurveProcessor, createGamepadStickPipeline, resolveGamepadStickBinding };
14
+ export { DEFAULT_GAMEPAD_STICK_PIPELINE, GAMEPAD_AXIS, GAMEPAD_BUTTON, GamepadArcballControls, GamepadArcballControlsOptions, GamepadAxisKey, GamepadAxisOptions, GamepadAxisValue, GamepadButtonKey, GamepadButtonValue, GamepadControls, GamepadControlsEventMap, GamepadControlsOptions, GamepadDeadzoneOptions, GamepadDragControls, GamepadDragControlsOptions, GamepadFirstPersonControls, GamepadFirstPersonControlsOptions, GamepadFlyControls, GamepadFlyControlsOptions, GamepadInput, GamepadInputEventMap, GamepadInputOptions, GamepadMapControls, GamepadOrbitControls, GamepadOrbitControlsOptions, GamepadPointerLockControls, GamepadPointerLockControlsOptions, GamepadResponseCurve, GamepadResponseCurveOptions, GamepadStick, GamepadStickBinding, GamepadStickBindingOptions, GamepadStickInversionAxis, GamepadStickPipeline, GamepadStickPipelineOptions, GamepadStickProcessingMode, GamepadStickProcessor, GamepadStickTransform, GamepadTrackballControls, GamepadTrackballControlsOptions, GamepadTransformControls, GamepadTransformControlsOptions, MAX_GAMEPAD_INDEX, MIN_GAMEPAD_INDEX, gamepadStickPipeline, resolveGamepadStickBinding };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { GAMEPAD_AXIS, GAMEPAD_BUTTON, MAX_GAMEPAD_INDEX, MIN_GAMEPAD_INDEX } from "./core.js";
2
- import { DEFAULT_GAMEPAD_STICK_PIPELINE, createGamepadDeadzoneProcessor, createGamepadInversionProcessor, createGamepadResponseCurveProcessor, createGamepadStickPipeline, resolveGamepadStickBinding } from "./gamepad-stick-processing.js";
2
+ import { DEFAULT_GAMEPAD_STICK_PIPELINE, gamepadStickPipeline, resolveGamepadStickBinding } from "./gamepad-stick-processing.js";
3
3
  import { GamepadInput } from "./gamepad-input.js";
4
4
  import { GamepadControls } from "./gamepad-controls.js";
5
5
  import { GamepadArcballControls } from "./gamepad-arcball-controls.js";
@@ -11,4 +11,4 @@ import { GamepadMapControls } from "./gamepad-map-controls.js";
11
11
  import { GamepadPointerLockControls } from "./gamepad-pointer-lock-controls.js";
12
12
  import { GamepadTrackballControls } from "./gamepad-trackball-controls.js";
13
13
  import { GamepadTransformControls } from "./gamepad-transform-controls.js";
14
- export { DEFAULT_GAMEPAD_STICK_PIPELINE, GAMEPAD_AXIS, GAMEPAD_BUTTON, GamepadArcballControls, GamepadControls, GamepadDragControls, GamepadFirstPersonControls, GamepadFlyControls, GamepadInput, GamepadMapControls, GamepadOrbitControls, GamepadPointerLockControls, GamepadTrackballControls, GamepadTransformControls, MAX_GAMEPAD_INDEX, MIN_GAMEPAD_INDEX, createGamepadDeadzoneProcessor, createGamepadInversionProcessor, createGamepadResponseCurveProcessor, createGamepadStickPipeline, resolveGamepadStickBinding };
14
+ export { DEFAULT_GAMEPAD_STICK_PIPELINE, GAMEPAD_AXIS, GAMEPAD_BUTTON, GamepadArcballControls, GamepadControls, GamepadDragControls, GamepadFirstPersonControls, GamepadFlyControls, GamepadInput, GamepadMapControls, GamepadOrbitControls, GamepadPointerLockControls, GamepadTrackballControls, GamepadTransformControls, MAX_GAMEPAD_INDEX, MIN_GAMEPAD_INDEX, gamepadStickPipeline, resolveGamepadStickBinding };
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.20.0",
4
+ "version": "0.22.0",
5
5
  "homepage": "https://github.com/luckasnix/three-gamepad-controls#readme",
6
6
  "author": {
7
7
  "name": "Kasnix",
@@ -46,16 +46,19 @@
46
46
  }
47
47
  },
48
48
  "devDependencies": {
49
- "@biomejs/biome": "2.5.4",
49
+ "@biomejs/biome": "2.5.6",
50
50
  "@commitlint/cli": "21.2.1",
51
51
  "@commitlint/config-conventional": "21.2.0",
52
52
  "@commitlint/types": "21.2.0",
53
53
  "@types/node": "24.13.3",
54
54
  "@types/three": "0.184.0",
55
+ "@vitest/coverage-v8": "4.1.10",
55
56
  "husky": "9.1.7",
57
+ "jsdom": "29.1.1",
56
58
  "three": "0.184.0",
57
- "tsdown": "0.22.8",
58
- "typescript": "7.0.2"
59
+ "tsdown": "0.22.14",
60
+ "typescript": "7.0.2",
61
+ "vitest": "4.1.10"
59
62
  },
60
63
  "peerDependencies": {
61
64
  "@types/three": ">=0.184.0",
@@ -70,6 +73,9 @@
70
73
  "lint:write": "biome lint --write",
71
74
  "check-all": "biome check",
72
75
  "write-all": "biome check --write",
73
- "check-ci": "biome ci"
76
+ "check-ci": "biome ci",
77
+ "test": "vitest run",
78
+ "test:watch": "vitest",
79
+ "test:coverage": "vitest run --coverage"
74
80
  }
75
81
  }