@threlte/extras
Version:
Utilities, abstractions and plugins for your Threlte apps
153 lines (152 loc) • 6.81 kB
TypeScript
/**
* Controller remapping support for `useGamepad`.
*
* The browser's Gamepad API only guarantees a consistent layout when
* `Gamepad.mapping === "standard"`. Many controllers — notably Nintendo Switch
* Pro, Joy-Cons, and the Switch Online SNES/N64/Genesis pads — commonly report
* an empty `mapping` string, which means the raw button and axis indices come
* straight from the HID descriptor and do not match the W3C Standard Gamepad
* layout. This module provides a small, extensible table of controller-specific
* remaps so `useGamepad` can expose a consistent position-based API regardless
* of controller brand.
*
* The name of each button in the standard layout (`clusterBottom`, `clusterRight`,
* `clusterLeft`, `clusterTop`) refers to the physical position of the face
* button, not the letter printed on it. So on an Xbox pad `clusterBottom === A`,
* on a DualSense `clusterBottom === Cross`, and on a Nintendo pad
* `clusterBottom === B`.
*/
/**
* The standard button names exposed by `useGamepad` in non-XR mode, in the
* order of the W3C Standard Gamepad button indices.
*/
export declare const standardButtonNames: readonly ["clusterBottom", "clusterRight", "clusterLeft", "clusterTop", "leftBumper", "rightBumper", "leftTrigger", "rightTrigger", "select", "start", "leftStickButton", "rightStickButton", "directionalTop", "directionalBottom", "directionalLeft", "directionalRight", "center"];
export type StandardButtonName = (typeof standardButtonNames)[number];
/**
* Describes where to read a button's state from on a non-standard controller.
*
* - `{ button: n }` reads from `pad.buttons[n]`.
* - `{ axis, ... }` reads from `pad.axes[axis]`. Useful for analog triggers
* that some controllers expose as an axis in `[-1, 1]` rather than a button
* in `[0, 1]`. The axis value is remapped into `[0, 1]`:
* - when `range` is `"unit"` (default) the raw value is treated as a button
* value in `[0, 1]` (anything < 0 is clamped to 0).
* - when `range` is `"signed"` the axis is assumed to rest at `-1` and reach
* `1` when fully pressed, and is normalised to `(raw + 1) / 2`.
*
* `invert` flips the sign of the raw reading before normalisation.
* `pressThreshold` controls when the button reports `pressed: true`
* (default 0.5 on the normalised value).
*/
export type ButtonSource = {
button: number;
} | {
axis: number;
range?: 'unit' | 'signed';
invert?: boolean;
pressThreshold?: number;
};
/**
* Synthesizes directional button states (Top/Bottom/Left/Right) from a single
* axis that encodes a hat switch. Many Nintendo-style controllers report the
* D-pad this way on Chromium browsers, using `axes[9]` with eight quantized
* values around the circle.
*
* Each direction field holds the axis values at which that direction should
* be considered pressed. A diagonal (e.g. up-right) appears in both `up` and
* `right`. Idle is implied by the absence of a match, so no special neutral
* value is needed.
*/
export interface HatAxisMapping {
axis: number;
up: number[];
right: number[];
down: number[];
left: number[];
/** Absolute-value tolerance used when matching axis values. Default 0.1. */
tolerance?: number;
}
/** Describes how a stick is read from two axes. */
export interface StickAxisMapping {
xAxis: number;
yAxis: number;
invertX?: boolean;
invertY?: boolean;
}
/**
* A remap entry for a specific non-standard controller.
*
* Any field left undefined falls back to the W3C Standard Gamepad layout:
* `buttons[0..16]` for the standard button indices, `axes[0..1]` for the
* left stick, and `axes[2..3]` for the right stick.
*
* If `dpad` is set, the `directionalTop/Bottom/Left/Right` entries of
* `buttons` are ignored and directional state is synthesised from the hat
* axis instead.
*/
export interface GamepadMapping {
/** Human-readable name for debugging. */
name?: string;
buttons?: Partial<Record<StandardButtonName, ButtonSource>>;
leftStick?: StickAxisMapping;
rightStick?: StickAxisMapping;
dpad?: HatAxisMapping;
}
/**
* A dictionary of controller remaps keyed by a canonical `vvvv:pppp`
* signature (hex USB vendor and product IDs, lowercase) derived from
* `Gamepad.id`.
*/
export type GamepadMappings = Record<string, GamepadMapping>;
/**
* Parse a `Gamepad.id` string into a canonical `vvvv:pppp` signature.
*
* Chromium exposes `"Name (Vendor: VVVV Product: PPPP)"`, Firefox exposes
* `"VVVV-PPPP-Name"`. Returns `null` if neither pattern matches, in which
* case the caller cannot look up a remap and should fall back to the
* standard layout.
*/
export declare const parseGamepadSignature: (id: string) => string | null;
/**
* Built-in mappings for common non-standard controllers.
*
* Every browser/OS/connection combination can potentially report a controller
* differently, so these entries target the most widely reported configurations
* (Chromium browsers on recent macOS/Windows/Linux, USB and Bluetooth).
* Users with different setups can override any entry by passing `mappings` to
* `useGamepad`.
*
* Sources:
* - W3C Gamepad Standard Mapping (https://www.w3.org/TR/gamepad/#remapping)
* - SDL_GameControllerDB (https://github.com/mdqinc/SDL_GameControllerDB)
* - Chromium `device/gamepad/gamepad_standard_mappings_*.cc` (per-platform
* remapping tables shipped inside the browser itself)
*/
export declare const builtinMappings: GamepadMappings;
/**
* Look up a remap for the given `Gamepad`. Returns `null` when the pad already
* uses the Standard Gamepad layout, when its id cannot be parsed into a
* vendor/product signature, or when no mapping is registered for that pad.
*/
export declare const resolveMapping: (pad: Gamepad, userMappings: GamepadMappings | undefined, includeBuiltins: boolean) => GamepadMapping | null;
/**
* Read a single button's state using an optional remap entry. Falls back to
* `pad.buttons[defaultIndex]` when no remap is present. The synthesised return
* for axis-sourced buttons is structurally compatible with `GamepadButton`.
*/
export declare const readButton: (pad: Gamepad, source: ButtonSource | undefined, defaultIndex: number) => GamepadButton | undefined;
/**
* Read a stick's (x, y) using an optional remap. Falls back to the supplied
* default axis indices and no inversion when no remap is present.
*/
export declare const readStick: (pad: Gamepad, mapping: StickAxisMapping | undefined, defaultXAxis: number, defaultYAxis: number) => {
x: number;
y: number;
};
/** Compute the four directional states from a hat-axis mapping. */
export declare const readHatDirections: (pad: Gamepad, hat: HatAxisMapping) => {
up: boolean;
right: boolean;
down: boolean;
left: boolean;
};