UNPKG

@threlte/extras

Version:

Utilities, abstractions and plugins for your Threlte apps

153 lines (152 loc) 6.81 kB
/** * 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; };