@forgeax/engine-input 0.1.2
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/LICENSE +202 -0
- package/README.md +312 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/browser-backend-dual-instance.test.d.ts +2 -0
- package/dist/__tests__/browser-backend-dual-instance.test.d.ts.map +1 -0
- package/dist/__tests__/browser-backend-focus-loss.test.d.ts +2 -0
- package/dist/__tests__/browser-backend-focus-loss.test.d.ts.map +1 -0
- package/dist/__tests__/browser-backend-lock-error.test.d.ts +2 -0
- package/dist/__tests__/browser-backend-lock-error.test.d.ts.map +1 -0
- package/dist/__tests__/browser-backend-lock-gate.test.d.ts +2 -0
- package/dist/__tests__/browser-backend-lock-gate.test.d.ts.map +1 -0
- package/dist/__tests__/browser-backend-lock-release.test.d.ts +2 -0
- package/dist/__tests__/browser-backend-lock-release.test.d.ts.map +1 -0
- package/dist/__tests__/browser-backend-lock-state.test.d.ts +2 -0
- package/dist/__tests__/browser-backend-lock-state.test.d.ts.map +1 -0
- package/dist/__tests__/canvas-input-boundary.test.d.ts +2 -0
- package/dist/__tests__/canvas-input-boundary.test.d.ts.map +1 -0
- package/dist/__tests__/canvas-input-owner.test-d.d.ts +2 -0
- package/dist/__tests__/canvas-input-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/composite-backend.test.d.ts +2 -0
- package/dist/__tests__/composite-backend.test.d.ts.map +1 -0
- package/dist/__tests__/controller-platform-owner.test-d.d.ts +2 -0
- package/dist/__tests__/controller-platform-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/input-snapshot.test-d.d.ts +2 -0
- package/dist/__tests__/input-snapshot.test-d.d.ts.map +1 -0
- package/dist/__tests__/input-snapshot.test.d.ts +2 -0
- package/dist/__tests__/input-snapshot.test.d.ts.map +1 -0
- package/dist/__tests__/swipe-direction-owner.test-d.d.ts +2 -0
- package/dist/__tests__/swipe-direction-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/ui-input-matrix.test.d.ts +2 -0
- package/dist/__tests__/ui-input-matrix.test.d.ts.map +1 -0
- package/dist/__tests__/ui-ownership-edge.test.d.ts +2 -0
- package/dist/__tests__/ui-ownership-edge.test.d.ts.map +1 -0
- package/dist/__tests__/ui-ownership.test.d.ts +2 -0
- package/dist/__tests__/ui-ownership.test.d.ts.map +1 -0
- package/dist/__tests__/ui-reset-idempotence.test.d.ts +2 -0
- package/dist/__tests__/ui-reset-idempotence.test.d.ts.map +1 -0
- package/dist/__tests__/ui-reset.test.d.ts +2 -0
- package/dist/__tests__/ui-reset.test.d.ts.map +1 -0
- package/dist/action-state.d.ts +146 -0
- package/dist/action-state.d.ts.map +1 -0
- package/dist/browser-backend.d.ts +121 -0
- package/dist/browser-backend.d.ts.map +1 -0
- package/dist/canvas-input-boundary.d.ts +13 -0
- package/dist/canvas-input-boundary.d.ts.map +1 -0
- package/dist/composite-backend.d.ts +34 -0
- package/dist/composite-backend.d.ts.map +1 -0
- package/dist/controller-db-data.d.ts +5 -0
- package/dist/controller-db-data.d.ts.map +1 -0
- package/dist/controller-db-data.mjs +12 -0
- package/dist/controller-db-data.mjs.map +1 -0
- package/dist/controller-db.d.ts +70 -0
- package/dist/controller-db.d.ts.map +1 -0
- package/dist/controller-db.mjs +119 -0
- package/dist/controller-db.mjs.map +1 -0
- package/dist/frame-start-scan-system.d.ts +37 -0
- package/dist/frame-start-scan-system.d.ts.map +1 -0
- package/dist/gamepad-frame.d.ts +42 -0
- package/dist/gamepad-frame.d.ts.map +1 -0
- package/dist/gesture-recognizer.d.ts +145 -0
- package/dist/gesture-recognizer.d.ts.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +1502 -0
- package/dist/index.mjs.map +1 -0
- package/dist/input-snapshot.d.ts +420 -0
- package/dist/input-snapshot.d.ts.map +1 -0
- package/dist/plugin-service.d.ts +11 -0
- package/dist/plugin-service.d.ts.map +1 -0
- package/dist/ui-ownership.d.ts +17 -0
- package/dist/ui-ownership.d.ts.map +1 -0
- package/dist/virtual-joystick.d.ts +39 -0
- package/dist/virtual-joystick.d.ts.map +1 -0
- package/package.json +72 -0
- package/src/__tests__/browser-backend-dual-instance.test.ts +379 -0
- package/src/__tests__/browser-backend-focus-loss.test.ts +239 -0
- package/src/__tests__/browser-backend-lock-error.test.ts +425 -0
- package/src/__tests__/browser-backend-lock-gate.test.ts +283 -0
- package/src/__tests__/browser-backend-lock-release.test.ts +641 -0
- package/src/__tests__/browser-backend-lock-state.test.ts +604 -0
- package/src/__tests__/canvas-input-boundary.test.ts +65 -0
- package/src/__tests__/canvas-input-owner.test-d.ts +39 -0
- package/src/__tests__/composite-backend.test.ts +299 -0
- package/src/__tests__/controller-platform-owner.test-d.ts +46 -0
- package/src/__tests__/input-snapshot.test-d.ts +78 -0
- package/src/__tests__/input-snapshot.test.ts +96 -0
- package/src/__tests__/swipe-direction-owner.test-d.ts +45 -0
- package/src/__tests__/ui-input-matrix.test.ts +31 -0
- package/src/__tests__/ui-ownership-edge.test.ts +33 -0
- package/src/__tests__/ui-ownership.test.ts +35 -0
- package/src/__tests__/ui-reset-idempotence.test.ts +17 -0
- package/src/__tests__/ui-reset.test.ts +25 -0
- package/src/action-state.ts +380 -0
- package/src/browser-backend.ts +884 -0
- package/src/canvas-input-boundary.ts +81 -0
- package/src/composite-backend.ts +189 -0
- package/src/controller-db-data.ts +25 -0
- package/src/controller-db.ts +218 -0
- package/src/frame-start-scan-system.ts +93 -0
- package/src/gamepad-frame.ts +259 -0
- package/src/gesture-recognizer.ts +495 -0
- package/src/index.ts +101 -0
- package/src/input-snapshot.ts +732 -0
- package/src/plugin-service.ts +31 -0
- package/src/ui-ownership.ts +61 -0
- package/src/virtual-joystick.ts +103 -0
- package/vendor/gamecontrollerdb.txt +2114 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// gamepad-frame.ts -- pure functions for per-frame gamepad diff from raw
|
|
2
|
+
// navigator.getGamepads() output. No DOM dependency; only called by
|
|
3
|
+
// browser-backend.ts @internal.
|
|
4
|
+
//
|
|
5
|
+
// D-1: edge diff (justPressed / justReleased) derived via set-delta from
|
|
6
|
+
// prev vs cur pressed sets — same pattern as Bevy button_input.rs.
|
|
7
|
+
// D-1: slot diff (connect / disconnect) derived via set difference from
|
|
8
|
+
// prev vs cur slot index sets.
|
|
9
|
+
// OOS-4: no deadzone applied — axes pass through raw.
|
|
10
|
+
//
|
|
11
|
+
// M3 D-1 (option A): non-standard-layout gamepads are normalized at the
|
|
12
|
+
// acquisition layer. When a caller-supplied remapLookup returns SDL
|
|
13
|
+
// gamecontrollerdb.txt mapping tokens for the gamepad's id, raw HID
|
|
14
|
+
// button/axis indices are re-projected onto the standard layout and the
|
|
15
|
+
// slot is reported with standardMapping=true. No lookup / no match keeps
|
|
16
|
+
// the Feat1 empty-signal behaviour (graceful degradation).
|
|
17
|
+
|
|
18
|
+
import type { MappingTokens } from './controller-db';
|
|
19
|
+
import type { GamepadSlotSample } from './input-snapshot';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Minimal raw Gamepad shape from browser API (no DOM types here).
|
|
23
|
+
*
|
|
24
|
+
* M3 F-2 seam fix: `id` mirrors the browser `Gamepad.id` string so the
|
|
25
|
+
* remap lookup can derive the SDL GUID keyed by device identity (not by
|
|
26
|
+
* slot index, which is unstable across reconnects).
|
|
27
|
+
*/
|
|
28
|
+
export interface RawGamepadStub {
|
|
29
|
+
readonly index: number;
|
|
30
|
+
readonly id: string;
|
|
31
|
+
readonly connected: boolean;
|
|
32
|
+
readonly mapping: string;
|
|
33
|
+
readonly buttons: readonly { readonly value: number; readonly pressed: boolean }[];
|
|
34
|
+
readonly axes: readonly number[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Standard layout button count (17: 0-16) and axis count (4: 0-3).
|
|
39
|
+
* Non-standard gamepads without a DB match report standardMapping=false
|
|
40
|
+
* and empty readpoints.
|
|
41
|
+
*/
|
|
42
|
+
const STANDARD_BUTTON_COUNT = 17;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* SDL logical name -> standard-layout button index (W3C Gamepad standard
|
|
46
|
+
* mapping, KB source section 4.3). `guide` occupies the optional 17th slot.
|
|
47
|
+
*/
|
|
48
|
+
const STANDARD_BUTTON_INDEX: Readonly<Record<string, number>> = {
|
|
49
|
+
a: 0,
|
|
50
|
+
b: 1,
|
|
51
|
+
x: 2,
|
|
52
|
+
y: 3,
|
|
53
|
+
leftshoulder: 4,
|
|
54
|
+
rightshoulder: 5,
|
|
55
|
+
lefttrigger: 6,
|
|
56
|
+
righttrigger: 7,
|
|
57
|
+
back: 8,
|
|
58
|
+
start: 9,
|
|
59
|
+
leftstick: 10,
|
|
60
|
+
rightstick: 11,
|
|
61
|
+
dpup: 12,
|
|
62
|
+
dpdown: 13,
|
|
63
|
+
dpleft: 14,
|
|
64
|
+
dpright: 15,
|
|
65
|
+
guide: 16,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** SDL logical name -> standard-layout axis index (KB source section 4.3). */
|
|
69
|
+
const STANDARD_AXIS_INDEX: Readonly<Record<string, number>> = {
|
|
70
|
+
leftx: 0,
|
|
71
|
+
lefty: 1,
|
|
72
|
+
rightx: 2,
|
|
73
|
+
righty: 3,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** Analog trigger threshold: a trigger mapped to an axis counts as pressed above this. */
|
|
77
|
+
const TRIGGER_PRESS_THRESHOLD = 0.5;
|
|
78
|
+
|
|
79
|
+
interface RemappedReadpoints {
|
|
80
|
+
readonly pressed: Set<number>;
|
|
81
|
+
readonly buttonValues: Map<number, number>;
|
|
82
|
+
readonly axes: [number, number, number, number];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Re-project a non-standard raw gamepad onto the standard layout using SDL
|
|
87
|
+
* mapping tokens. Browser getGamepads() exposes only buttons[] and axes[]
|
|
88
|
+
* (no hats), so hat tokens (dpad-as-hat) cannot be resolved from raw data
|
|
89
|
+
* and are skipped — a documented graceful degradation for the rare hat-only
|
|
90
|
+
* dpad devices under the web Gamepad API.
|
|
91
|
+
*/
|
|
92
|
+
function remapToStandardLayout(gp: RawGamepadStub, tokens: MappingTokens): RemappedReadpoints {
|
|
93
|
+
const pressed = new Set<number>();
|
|
94
|
+
const buttonValues = new Map<number, number>();
|
|
95
|
+
const axes: [number, number, number, number] = [0, 0, 0, 0];
|
|
96
|
+
|
|
97
|
+
for (const [name, token] of Object.entries(tokens)) {
|
|
98
|
+
const stdButton = STANDARD_BUTTON_INDEX[name];
|
|
99
|
+
const stdAxis = STANDARD_AXIS_INDEX[name];
|
|
100
|
+
|
|
101
|
+
if (stdButton !== undefined) {
|
|
102
|
+
if (token.kind === 'button') {
|
|
103
|
+
const raw = gp.buttons[token.index];
|
|
104
|
+
if (raw) {
|
|
105
|
+
buttonValues.set(stdButton, raw.value);
|
|
106
|
+
if (raw.pressed) pressed.add(stdButton);
|
|
107
|
+
}
|
|
108
|
+
} else if (token.kind === 'axis') {
|
|
109
|
+
// Trigger (or button) mapped to a raw axis: value = raw axis reading.
|
|
110
|
+
const value = gp.axes[token.index] ?? 0;
|
|
111
|
+
buttonValues.set(stdButton, value);
|
|
112
|
+
if (value > TRIGGER_PRESS_THRESHOLD) pressed.add(stdButton);
|
|
113
|
+
}
|
|
114
|
+
// hat -> button (dpad): no hat source in the browser API; skip.
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (stdAxis !== undefined && token.kind === 'axis') {
|
|
119
|
+
const raw = gp.axes[token.index] ?? 0;
|
|
120
|
+
axes[stdAxis] = token.half === '-' ? -raw : raw;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { pressed, buttonValues, axes };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Build an empty-signal slot sample (non-standard-no-match / disconnected). */
|
|
128
|
+
function emptySlot(index: number): GamepadSlotSample {
|
|
129
|
+
return {
|
|
130
|
+
index,
|
|
131
|
+
standardMapping: false,
|
|
132
|
+
pressed: new Set(),
|
|
133
|
+
justPressed: new Set(),
|
|
134
|
+
justReleased: new Set(),
|
|
135
|
+
buttonValues: new Map(),
|
|
136
|
+
axes: [0, 0, 0, 0],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Derive justPressed / justReleased edge sets from prev vs cur pressed sets. */
|
|
141
|
+
function edges(
|
|
142
|
+
prevPressed: ReadonlySet<number>,
|
|
143
|
+
curPressed: ReadonlySet<number>,
|
|
144
|
+
): { justPressed: Set<number>; justReleased: Set<number> } {
|
|
145
|
+
const justPressed = new Set<number>();
|
|
146
|
+
const justReleased = new Set<number>();
|
|
147
|
+
for (const b of curPressed) {
|
|
148
|
+
if (!prevPressed.has(b)) justPressed.add(b);
|
|
149
|
+
}
|
|
150
|
+
for (const b of prevPressed) {
|
|
151
|
+
if (!curPressed.has(b)) justReleased.add(b);
|
|
152
|
+
}
|
|
153
|
+
return { justPressed, justReleased };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Diff prev vs cur frames for all gamepad slots. Returns a flat
|
|
158
|
+
* GamepadSlotSample[] array keyed by gamepad.index.
|
|
159
|
+
*
|
|
160
|
+
* - Standard-mapping slots produce GamepadSlotSample with edge sets filled
|
|
161
|
+
* from prev vs cur pressed-set delta.
|
|
162
|
+
* - Non-standard slots: when `remapLookup` returns mapping tokens for the
|
|
163
|
+
* gamepad id, raw HID indices are re-projected onto the standard layout
|
|
164
|
+
* and the slot reports standardMapping=true (D-1 semantic redefinition).
|
|
165
|
+
* With no lookup or no match, the slot reports standardMapping=false with
|
|
166
|
+
* empty readpoints and connected=true (Feat1 behaviour, AC-04).
|
|
167
|
+
* - Disconnected slots (in prev but not in cur) produce a slot with
|
|
168
|
+
* standardMapping=false and all readpoints empty.
|
|
169
|
+
* - Null entries in the browser getGamepads() array are skipped by the
|
|
170
|
+
* caller before calling this function.
|
|
171
|
+
*
|
|
172
|
+
* @param remapLookup - Optional acquisition-layer normalizer. Given a
|
|
173
|
+
* browser Gamepad.id, returns SDL mapping tokens or null. Only consulted
|
|
174
|
+
* for non-standard-mapping gamepads (D-1 option A).
|
|
175
|
+
*/
|
|
176
|
+
export function diffGamepadFrame(
|
|
177
|
+
prev: ReadonlyMap<number, GamepadSlotSample>,
|
|
178
|
+
curGamepads: readonly RawGamepadStub[],
|
|
179
|
+
remapLookup?: (gamepadId: string) => MappingTokens | null,
|
|
180
|
+
): GamepadSlotSample[] {
|
|
181
|
+
const results: GamepadSlotSample[] = [];
|
|
182
|
+
const prevIndices = new Set(prev.keys());
|
|
183
|
+
const curIndices = new Set<number>();
|
|
184
|
+
|
|
185
|
+
for (const gp of curGamepads) {
|
|
186
|
+
curIndices.add(gp.index);
|
|
187
|
+
const prevPressed = prev.get(gp.index)?.pressed ?? new Set<number>();
|
|
188
|
+
results.push(
|
|
189
|
+
gp.mapping === 'standard'
|
|
190
|
+
? diffStandardSlot(gp, prevPressed)
|
|
191
|
+
: diffNonStandardSlot(gp, prevPressed, remapLookup),
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Disconnected slots: emit empty-signal entries for slots that were in prev
|
|
196
|
+
// but not in cur, so the snapshot reader reports connected=false.
|
|
197
|
+
for (const idx of prevIndices) {
|
|
198
|
+
if (!curIndices.has(idx)) {
|
|
199
|
+
results.push(emptySlot(idx));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return results;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Diff a standard-mapping gamepad slot: raw indices pass through 1:1. */
|
|
207
|
+
function diffStandardSlot(gp: RawGamepadStub, prevPressed: ReadonlySet<number>): GamepadSlotSample {
|
|
208
|
+
const curPressed = new Set<number>();
|
|
209
|
+
const buttonValues = new Map<number, number>();
|
|
210
|
+
const btnCount = Math.min(gp.buttons.length, STANDARD_BUTTON_COUNT);
|
|
211
|
+
for (let b = 0; b < btnCount; b++) {
|
|
212
|
+
const btn = gp.buttons[b];
|
|
213
|
+
if (!btn) continue;
|
|
214
|
+
buttonValues.set(b, btn.value);
|
|
215
|
+
if (btn.pressed) curPressed.add(b);
|
|
216
|
+
}
|
|
217
|
+
const { justPressed, justReleased } = edges(prevPressed, curPressed);
|
|
218
|
+
// Axes: raw values, no deadzone (OOS-4).
|
|
219
|
+
const axes: [number, number, number, number] = [
|
|
220
|
+
gp.axes[0] ?? 0,
|
|
221
|
+
gp.axes[1] ?? 0,
|
|
222
|
+
gp.axes[2] ?? 0,
|
|
223
|
+
gp.axes[3] ?? 0,
|
|
224
|
+
];
|
|
225
|
+
return {
|
|
226
|
+
index: gp.index,
|
|
227
|
+
standardMapping: true,
|
|
228
|
+
pressed: curPressed,
|
|
229
|
+
justPressed,
|
|
230
|
+
justReleased,
|
|
231
|
+
buttonValues,
|
|
232
|
+
axes,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Diff a non-standard-mapping gamepad slot (D-1 option A): attempt an
|
|
238
|
+
* acquisition-layer remap via the SDL DB. No lookup / no match falls back
|
|
239
|
+
* to the Feat1 empty signal with connected=true (AC-04).
|
|
240
|
+
*/
|
|
241
|
+
function diffNonStandardSlot(
|
|
242
|
+
gp: RawGamepadStub,
|
|
243
|
+
prevPressed: ReadonlySet<number>,
|
|
244
|
+
remapLookup?: (gamepadId: string) => MappingTokens | null,
|
|
245
|
+
): GamepadSlotSample {
|
|
246
|
+
const tokens = remapLookup ? remapLookup(gp.id) : null;
|
|
247
|
+
if (!tokens) return emptySlot(gp.index);
|
|
248
|
+
const { pressed, buttonValues, axes } = remapToStandardLayout(gp, tokens);
|
|
249
|
+
const { justPressed, justReleased } = edges(prevPressed, pressed);
|
|
250
|
+
return {
|
|
251
|
+
index: gp.index,
|
|
252
|
+
standardMapping: true,
|
|
253
|
+
pressed,
|
|
254
|
+
justPressed,
|
|
255
|
+
justReleased,
|
|
256
|
+
buttonValues,
|
|
257
|
+
axes,
|
|
258
|
+
};
|
|
259
|
+
}
|