@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.
Files changed (107) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +312 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/browser-backend-dual-instance.test.d.ts +2 -0
  5. package/dist/__tests__/browser-backend-dual-instance.test.d.ts.map +1 -0
  6. package/dist/__tests__/browser-backend-focus-loss.test.d.ts +2 -0
  7. package/dist/__tests__/browser-backend-focus-loss.test.d.ts.map +1 -0
  8. package/dist/__tests__/browser-backend-lock-error.test.d.ts +2 -0
  9. package/dist/__tests__/browser-backend-lock-error.test.d.ts.map +1 -0
  10. package/dist/__tests__/browser-backend-lock-gate.test.d.ts +2 -0
  11. package/dist/__tests__/browser-backend-lock-gate.test.d.ts.map +1 -0
  12. package/dist/__tests__/browser-backend-lock-release.test.d.ts +2 -0
  13. package/dist/__tests__/browser-backend-lock-release.test.d.ts.map +1 -0
  14. package/dist/__tests__/browser-backend-lock-state.test.d.ts +2 -0
  15. package/dist/__tests__/browser-backend-lock-state.test.d.ts.map +1 -0
  16. package/dist/__tests__/canvas-input-boundary.test.d.ts +2 -0
  17. package/dist/__tests__/canvas-input-boundary.test.d.ts.map +1 -0
  18. package/dist/__tests__/canvas-input-owner.test-d.d.ts +2 -0
  19. package/dist/__tests__/canvas-input-owner.test-d.d.ts.map +1 -0
  20. package/dist/__tests__/composite-backend.test.d.ts +2 -0
  21. package/dist/__tests__/composite-backend.test.d.ts.map +1 -0
  22. package/dist/__tests__/controller-platform-owner.test-d.d.ts +2 -0
  23. package/dist/__tests__/controller-platform-owner.test-d.d.ts.map +1 -0
  24. package/dist/__tests__/input-snapshot.test-d.d.ts +2 -0
  25. package/dist/__tests__/input-snapshot.test-d.d.ts.map +1 -0
  26. package/dist/__tests__/input-snapshot.test.d.ts +2 -0
  27. package/dist/__tests__/input-snapshot.test.d.ts.map +1 -0
  28. package/dist/__tests__/swipe-direction-owner.test-d.d.ts +2 -0
  29. package/dist/__tests__/swipe-direction-owner.test-d.d.ts.map +1 -0
  30. package/dist/__tests__/ui-input-matrix.test.d.ts +2 -0
  31. package/dist/__tests__/ui-input-matrix.test.d.ts.map +1 -0
  32. package/dist/__tests__/ui-ownership-edge.test.d.ts +2 -0
  33. package/dist/__tests__/ui-ownership-edge.test.d.ts.map +1 -0
  34. package/dist/__tests__/ui-ownership.test.d.ts +2 -0
  35. package/dist/__tests__/ui-ownership.test.d.ts.map +1 -0
  36. package/dist/__tests__/ui-reset-idempotence.test.d.ts +2 -0
  37. package/dist/__tests__/ui-reset-idempotence.test.d.ts.map +1 -0
  38. package/dist/__tests__/ui-reset.test.d.ts +2 -0
  39. package/dist/__tests__/ui-reset.test.d.ts.map +1 -0
  40. package/dist/action-state.d.ts +146 -0
  41. package/dist/action-state.d.ts.map +1 -0
  42. package/dist/browser-backend.d.ts +121 -0
  43. package/dist/browser-backend.d.ts.map +1 -0
  44. package/dist/canvas-input-boundary.d.ts +13 -0
  45. package/dist/canvas-input-boundary.d.ts.map +1 -0
  46. package/dist/composite-backend.d.ts +34 -0
  47. package/dist/composite-backend.d.ts.map +1 -0
  48. package/dist/controller-db-data.d.ts +5 -0
  49. package/dist/controller-db-data.d.ts.map +1 -0
  50. package/dist/controller-db-data.mjs +12 -0
  51. package/dist/controller-db-data.mjs.map +1 -0
  52. package/dist/controller-db.d.ts +70 -0
  53. package/dist/controller-db.d.ts.map +1 -0
  54. package/dist/controller-db.mjs +119 -0
  55. package/dist/controller-db.mjs.map +1 -0
  56. package/dist/frame-start-scan-system.d.ts +37 -0
  57. package/dist/frame-start-scan-system.d.ts.map +1 -0
  58. package/dist/gamepad-frame.d.ts +42 -0
  59. package/dist/gamepad-frame.d.ts.map +1 -0
  60. package/dist/gesture-recognizer.d.ts +145 -0
  61. package/dist/gesture-recognizer.d.ts.map +1 -0
  62. package/dist/index.d.ts +13 -0
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.mjs +1502 -0
  65. package/dist/index.mjs.map +1 -0
  66. package/dist/input-snapshot.d.ts +420 -0
  67. package/dist/input-snapshot.d.ts.map +1 -0
  68. package/dist/plugin-service.d.ts +11 -0
  69. package/dist/plugin-service.d.ts.map +1 -0
  70. package/dist/ui-ownership.d.ts +17 -0
  71. package/dist/ui-ownership.d.ts.map +1 -0
  72. package/dist/virtual-joystick.d.ts +39 -0
  73. package/dist/virtual-joystick.d.ts.map +1 -0
  74. package/package.json +72 -0
  75. package/src/__tests__/browser-backend-dual-instance.test.ts +379 -0
  76. package/src/__tests__/browser-backend-focus-loss.test.ts +239 -0
  77. package/src/__tests__/browser-backend-lock-error.test.ts +425 -0
  78. package/src/__tests__/browser-backend-lock-gate.test.ts +283 -0
  79. package/src/__tests__/browser-backend-lock-release.test.ts +641 -0
  80. package/src/__tests__/browser-backend-lock-state.test.ts +604 -0
  81. package/src/__tests__/canvas-input-boundary.test.ts +65 -0
  82. package/src/__tests__/canvas-input-owner.test-d.ts +39 -0
  83. package/src/__tests__/composite-backend.test.ts +299 -0
  84. package/src/__tests__/controller-platform-owner.test-d.ts +46 -0
  85. package/src/__tests__/input-snapshot.test-d.ts +78 -0
  86. package/src/__tests__/input-snapshot.test.ts +96 -0
  87. package/src/__tests__/swipe-direction-owner.test-d.ts +45 -0
  88. package/src/__tests__/ui-input-matrix.test.ts +31 -0
  89. package/src/__tests__/ui-ownership-edge.test.ts +33 -0
  90. package/src/__tests__/ui-ownership.test.ts +35 -0
  91. package/src/__tests__/ui-reset-idempotence.test.ts +17 -0
  92. package/src/__tests__/ui-reset.test.ts +25 -0
  93. package/src/action-state.ts +380 -0
  94. package/src/browser-backend.ts +884 -0
  95. package/src/canvas-input-boundary.ts +81 -0
  96. package/src/composite-backend.ts +189 -0
  97. package/src/controller-db-data.ts +25 -0
  98. package/src/controller-db.ts +218 -0
  99. package/src/frame-start-scan-system.ts +93 -0
  100. package/src/gamepad-frame.ts +259 -0
  101. package/src/gesture-recognizer.ts +495 -0
  102. package/src/index.ts +101 -0
  103. package/src/input-snapshot.ts +732 -0
  104. package/src/plugin-service.ts +31 -0
  105. package/src/ui-ownership.ts +61 -0
  106. package/src/virtual-joystick.ts +103 -0
  107. 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
+ }