@forgeax/engine-input 0.0.0-dev.8d955ade1c79

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
package/dist/index.mjs ADDED
@@ -0,0 +1,1502 @@
1
+ import { defineSystemSet, defineSystem } from '@forgeax/engine-ecs';
2
+
3
+ // src/action-state.ts
4
+ var INPUT_MAP_KEY = "InputMap";
5
+ var DEFAULT_DEADZONE = 0.2;
6
+ function inverseLerp(a, b, val) {
7
+ if (val <= a) return 0;
8
+ if (val >= b) return 1;
9
+ return (val - a) / (b - a);
10
+ }
11
+ function applyDeadzone(rawAbs, deadzone) {
12
+ return rawAbs < deadzone ? 0 : inverseLerp(deadzone, 1, rawAbs);
13
+ }
14
+ function bindingContribution(binding, sample) {
15
+ switch (binding.type) {
16
+ case "key": {
17
+ const down = sample.downKeys.has(binding.key);
18
+ return { rawAbs: down ? 1 : 0, pressed: down };
19
+ }
20
+ case "mouseButton": {
21
+ const down = sample.buttons[binding.button] === true;
22
+ return { rawAbs: down ? 1 : 0, pressed: down };
23
+ }
24
+ case "gamepadButton": {
25
+ const slots = sample.gamepads ?? [];
26
+ let anyPressed = false;
27
+ for (const slot of slots) {
28
+ if (!slot.standardMapping) continue;
29
+ if (slot.pressed.has(binding.button)) {
30
+ anyPressed = true;
31
+ break;
32
+ }
33
+ }
34
+ return { rawAbs: anyPressed ? 1 : 0, pressed: anyPressed };
35
+ }
36
+ case "gamepadAxis": {
37
+ const slots = sample.gamepads ?? [];
38
+ let maxRawAbs = 0;
39
+ for (const slot of slots) {
40
+ if (!slot.standardMapping) continue;
41
+ const rawAxis = slot.axes[binding.axis];
42
+ const sign = binding.sign;
43
+ if (sign === void 0) {
44
+ const abs = Math.abs(rawAxis);
45
+ if (abs > maxRawAbs) maxRawAbs = abs;
46
+ } else if (sign === 1) {
47
+ const v = Math.max(0, rawAxis);
48
+ if (v > maxRawAbs) maxRawAbs = v;
49
+ } else {
50
+ const v = Math.max(0, -rawAxis);
51
+ if (v > maxRawAbs) maxRawAbs = v;
52
+ }
53
+ }
54
+ return { rawAbs: maxRawAbs, pressed: false };
55
+ }
56
+ }
57
+ }
58
+ function deriveActionStates(sample, inputMap, prevActionStates) {
59
+ const deduped = /* @__PURE__ */ new Map();
60
+ for (const config of inputMap) {
61
+ deduped.set(config.action, config);
62
+ }
63
+ const prevByAction = /* @__PURE__ */ new Map();
64
+ if (prevActionStates) {
65
+ for (const s of prevActionStates) {
66
+ prevByAction.set(s.action, s.pressed);
67
+ }
68
+ }
69
+ const results = [];
70
+ for (const config of deduped.values()) {
71
+ const deadzone = config.deadzone ?? DEFAULT_DEADZONE;
72
+ let aggregatedPressed = false;
73
+ let aggregatedStrength = 0;
74
+ let aggregatedRaw = 0;
75
+ for (const binding of config.bindings) {
76
+ const { rawAbs } = bindingContribution(binding, sample);
77
+ if (rawAbs > aggregatedRaw) aggregatedRaw = rawAbs;
78
+ if (rawAbs >= deadzone) {
79
+ aggregatedPressed = true;
80
+ const str = applyDeadzone(rawAbs, deadzone);
81
+ if (str > aggregatedStrength) aggregatedStrength = str;
82
+ }
83
+ }
84
+ const prevPressed = prevByAction.get(config.action) ?? false;
85
+ const justPressed = aggregatedPressed && !prevPressed;
86
+ const justReleased = sample.focusReset !== true && !aggregatedPressed && prevPressed;
87
+ results.push({
88
+ action: config.action,
89
+ pressed: aggregatedPressed,
90
+ justPressed,
91
+ justReleased,
92
+ strength: aggregatedStrength,
93
+ raw: aggregatedRaw
94
+ });
95
+ }
96
+ return results;
97
+ }
98
+ function getAxis(_inputMap, actionStates, neg, pos) {
99
+ const posState = actionStates.find((s) => s.action === pos);
100
+ const negState = actionStates.find((s) => s.action === neg);
101
+ const posStrength = posState?.strength ?? 0;
102
+ const negStrength = negState?.strength ?? 0;
103
+ if (neg === pos) return 0;
104
+ return posStrength - negStrength;
105
+ }
106
+ function getVector(inputMap, actionStates, negX, posX, negY, posY, opts) {
107
+ const posXState = actionStates.find((s) => s.action === posX);
108
+ const negXState = actionStates.find((s) => s.action === negX);
109
+ const posYState = actionStates.find((s) => s.action === posY);
110
+ const negYState = actionStates.find((s) => s.action === negY);
111
+ const x = (posXState?.raw ?? 0) - (negXState?.raw ?? 0);
112
+ const y = (posYState?.raw ?? 0) - (negYState?.raw ?? 0);
113
+ const deadzone = opts?.deadzone ?? computeAverageDeadzone(inputMap, negX, posX, negY, posY);
114
+ const length = Math.sqrt(x * x + y * y);
115
+ if (length <= deadzone) {
116
+ return { x: 0, y: 0 };
117
+ }
118
+ if (length > 1) {
119
+ return { x: x / length, y: y / length };
120
+ }
121
+ const factor = inverseLerp(deadzone, 1, length) / length;
122
+ return { x: x * factor, y: y * factor };
123
+ }
124
+ function computeAverageDeadzone(inputMap, negX, posX, negY, posY) {
125
+ const map = /* @__PURE__ */ new Map();
126
+ for (const c of inputMap) {
127
+ map.set(c.action, c.deadzone ?? DEFAULT_DEADZONE);
128
+ }
129
+ const dzNegX = map.get(negX) ?? DEFAULT_DEADZONE;
130
+ const dzPosX = map.get(posX) ?? DEFAULT_DEADZONE;
131
+ const dzNegY = map.get(negY) ?? DEFAULT_DEADZONE;
132
+ const dzPosY = map.get(posY) ?? DEFAULT_DEADZONE;
133
+ return (dzNegX + dzPosX + dzNegY + dzPosY) / 4;
134
+ }
135
+
136
+ // src/gamepad-frame.ts
137
+ var STANDARD_BUTTON_COUNT = 17;
138
+ var STANDARD_BUTTON_INDEX = {
139
+ a: 0,
140
+ b: 1,
141
+ x: 2,
142
+ y: 3,
143
+ leftshoulder: 4,
144
+ rightshoulder: 5,
145
+ lefttrigger: 6,
146
+ righttrigger: 7,
147
+ back: 8,
148
+ start: 9,
149
+ leftstick: 10,
150
+ rightstick: 11,
151
+ dpup: 12,
152
+ dpdown: 13,
153
+ dpleft: 14,
154
+ dpright: 15,
155
+ guide: 16
156
+ };
157
+ var STANDARD_AXIS_INDEX = {
158
+ leftx: 0,
159
+ lefty: 1,
160
+ rightx: 2,
161
+ righty: 3
162
+ };
163
+ var TRIGGER_PRESS_THRESHOLD = 0.5;
164
+ function remapToStandardLayout(gp, tokens) {
165
+ const pressed = /* @__PURE__ */ new Set();
166
+ const buttonValues = /* @__PURE__ */ new Map();
167
+ const axes = [0, 0, 0, 0];
168
+ for (const [name, token] of Object.entries(tokens)) {
169
+ const stdButton = STANDARD_BUTTON_INDEX[name];
170
+ const stdAxis = STANDARD_AXIS_INDEX[name];
171
+ if (stdButton !== void 0) {
172
+ if (token.kind === "button") {
173
+ const raw = gp.buttons[token.index];
174
+ if (raw) {
175
+ buttonValues.set(stdButton, raw.value);
176
+ if (raw.pressed) pressed.add(stdButton);
177
+ }
178
+ } else if (token.kind === "axis") {
179
+ const value = gp.axes[token.index] ?? 0;
180
+ buttonValues.set(stdButton, value);
181
+ if (value > TRIGGER_PRESS_THRESHOLD) pressed.add(stdButton);
182
+ }
183
+ continue;
184
+ }
185
+ if (stdAxis !== void 0 && token.kind === "axis") {
186
+ const raw = gp.axes[token.index] ?? 0;
187
+ axes[stdAxis] = token.half === "-" ? -raw : raw;
188
+ }
189
+ }
190
+ return { pressed, buttonValues, axes };
191
+ }
192
+ function emptySlot(index) {
193
+ return {
194
+ index,
195
+ standardMapping: false,
196
+ pressed: /* @__PURE__ */ new Set(),
197
+ justPressed: /* @__PURE__ */ new Set(),
198
+ justReleased: /* @__PURE__ */ new Set(),
199
+ buttonValues: /* @__PURE__ */ new Map(),
200
+ axes: [0, 0, 0, 0]
201
+ };
202
+ }
203
+ function edges(prevPressed, curPressed) {
204
+ const justPressed = /* @__PURE__ */ new Set();
205
+ const justReleased = /* @__PURE__ */ new Set();
206
+ for (const b of curPressed) {
207
+ if (!prevPressed.has(b)) justPressed.add(b);
208
+ }
209
+ for (const b of prevPressed) {
210
+ if (!curPressed.has(b)) justReleased.add(b);
211
+ }
212
+ return { justPressed, justReleased };
213
+ }
214
+ function diffGamepadFrame(prev, curGamepads, remapLookup) {
215
+ const results = [];
216
+ const prevIndices = new Set(prev.keys());
217
+ const curIndices = /* @__PURE__ */ new Set();
218
+ for (const gp of curGamepads) {
219
+ curIndices.add(gp.index);
220
+ const prevPressed = prev.get(gp.index)?.pressed ?? /* @__PURE__ */ new Set();
221
+ results.push(
222
+ gp.mapping === "standard" ? diffStandardSlot(gp, prevPressed) : diffNonStandardSlot(gp, prevPressed, remapLookup)
223
+ );
224
+ }
225
+ for (const idx of prevIndices) {
226
+ if (!curIndices.has(idx)) {
227
+ results.push(emptySlot(idx));
228
+ }
229
+ }
230
+ return results;
231
+ }
232
+ function diffStandardSlot(gp, prevPressed) {
233
+ const curPressed = /* @__PURE__ */ new Set();
234
+ const buttonValues = /* @__PURE__ */ new Map();
235
+ const btnCount = Math.min(gp.buttons.length, STANDARD_BUTTON_COUNT);
236
+ for (let b = 0; b < btnCount; b++) {
237
+ const btn = gp.buttons[b];
238
+ if (!btn) continue;
239
+ buttonValues.set(b, btn.value);
240
+ if (btn.pressed) curPressed.add(b);
241
+ }
242
+ const { justPressed, justReleased } = edges(prevPressed, curPressed);
243
+ const axes = [
244
+ gp.axes[0] ?? 0,
245
+ gp.axes[1] ?? 0,
246
+ gp.axes[2] ?? 0,
247
+ gp.axes[3] ?? 0
248
+ ];
249
+ return {
250
+ index: gp.index,
251
+ standardMapping: true,
252
+ pressed: curPressed,
253
+ justPressed,
254
+ justReleased,
255
+ buttonValues,
256
+ axes
257
+ };
258
+ }
259
+ function diffNonStandardSlot(gp, prevPressed, remapLookup) {
260
+ const tokens = remapLookup ? remapLookup(gp.id) : null;
261
+ if (!tokens) return emptySlot(gp.index);
262
+ const { pressed, buttonValues, axes } = remapToStandardLayout(gp, tokens);
263
+ const { justPressed, justReleased } = edges(prevPressed, pressed);
264
+ return {
265
+ index: gp.index,
266
+ standardMapping: true,
267
+ pressed,
268
+ justPressed,
269
+ justReleased,
270
+ buttonValues,
271
+ axes
272
+ };
273
+ }
274
+
275
+ // src/gesture-recognizer.ts
276
+ var LONG_PRESS_DURATION_MS = 500;
277
+ var LONG_PRESS_SLOP = 10;
278
+ var DOUBLE_TAP_INTERVAL_MS = 350;
279
+ var DOUBLE_TAP_DISTANCE = 10;
280
+ var SWIPE_VELOCITY_THRESHOLD = 0.5;
281
+ var SWIPE_WINDOW_MS = 100;
282
+ var SWIPE_DIRECTIONS = ["left", "right", "up", "down"];
283
+ var IDENTITY_GESTURE = Object.freeze({ pinchScale: 1, rotationAngle: 0 });
284
+ function createRecognizerState() {
285
+ return {
286
+ pinch: null,
287
+ gesture: IDENTITY_GESTURE,
288
+ longPresses: /* @__PURE__ */ new Map(),
289
+ lastTap: null,
290
+ swipeHistory: /* @__PURE__ */ new Map()
291
+ };
292
+ }
293
+ function distance(ax, ay, bx, by) {
294
+ return Math.hypot(bx - ax, by - ay);
295
+ }
296
+ function angleOf(ax, ay, bx, by) {
297
+ return Math.atan2(by - ay, bx - ax);
298
+ }
299
+ function normalizeAngle(a) {
300
+ let r = a;
301
+ while (r > Math.PI) r -= 2 * Math.PI;
302
+ while (r <= -Math.PI) r += 2 * Math.PI;
303
+ return r;
304
+ }
305
+ function dominantDirection(dx, dy) {
306
+ if (Math.abs(dx) >= Math.abs(dy)) return dx >= 0 ? SWIPE_DIRECTIONS[1] : SWIPE_DIRECTIONS[0];
307
+ return dy >= 0 ? SWIPE_DIRECTIONS[3] : SWIPE_DIRECTIONS[2];
308
+ }
309
+ function narrowGesturePointerType(pt) {
310
+ switch (pt) {
311
+ case "mouse":
312
+ return "mouse";
313
+ case "pen":
314
+ return "pen";
315
+ case "touch":
316
+ return "touch";
317
+ }
318
+ }
319
+ function emitPinchLifecycle(ctx, phase, tracker, now) {
320
+ const pointerIds = [tracker.pointerA, tracker.pointerB];
321
+ const pointerType = narrowGesturePointerType(tracker.pointerType);
322
+ ctx.events.push({ kind: `pinch-${phase}`, pointerIds, pointerType, timestamp: now });
323
+ ctx.events.push({ kind: `rotate-${phase}`, pointerIds, pointerType, timestamp: now });
324
+ }
325
+ function handleDown(ctx, ev, now) {
326
+ ctx.longPresses.set(ev.pointerId, {
327
+ downTime: now,
328
+ x: ev.x,
329
+ y: ev.y,
330
+ pointerType: ev.pointerType,
331
+ armed: true,
332
+ fired: false
333
+ });
334
+ ctx.swipeHistory.set(ev.pointerId, [{ t: now, x: ev.x, y: ev.y }]);
335
+ }
336
+ function handleMove(ctx, ev, now) {
337
+ const lp = ctx.longPresses.get(ev.pointerId);
338
+ if (lp?.armed && distance(lp.x, lp.y, ev.x, ev.y) > LONG_PRESS_SLOP) {
339
+ ctx.longPresses.set(ev.pointerId, { ...lp, armed: false });
340
+ }
341
+ const hist = ctx.swipeHistory.get(ev.pointerId);
342
+ if (hist) {
343
+ hist.push({ t: now, x: ev.x, y: ev.y });
344
+ pruneSwipeWindow(hist, now);
345
+ }
346
+ }
347
+ function pruneSwipeWindow(hist, now) {
348
+ const cutoff = now - SWIPE_WINDOW_MS;
349
+ while (hist.length > 1 && hist[0] !== void 0 && hist[0].t < cutoff) {
350
+ hist.shift();
351
+ }
352
+ }
353
+ function detectSwipe(ctx, ev, now) {
354
+ const hist = ctx.swipeHistory.get(ev.pointerId);
355
+ if (!hist || hist.length === 0) return;
356
+ const cutoff = now - SWIPE_WINDOW_MS;
357
+ const oldest = hist.find((s) => s.t >= cutoff) ?? hist[0];
358
+ if (!oldest) return;
359
+ const dt = now - oldest.t;
360
+ if (dt <= 0) return;
361
+ const dist = distance(oldest.x, oldest.y, ev.x, ev.y);
362
+ if (dist / dt < SWIPE_VELOCITY_THRESHOLD) return;
363
+ ctx.events.push({
364
+ kind: "swipe",
365
+ pointerId: ev.pointerId,
366
+ direction: dominantDirection(ev.x - oldest.x, ev.y - oldest.y),
367
+ pointerType: narrowGesturePointerType(ev.pointerType),
368
+ timestamp: now
369
+ });
370
+ }
371
+ function detectDoubleTap(ctx, ev, now) {
372
+ const prev = ctx.lastTap;
373
+ const withinTime = prev !== null && now - prev.time <= DOUBLE_TAP_INTERVAL_MS;
374
+ const withinDist = prev !== null && distance(prev.x, prev.y, ev.x, ev.y) <= DOUBLE_TAP_DISTANCE;
375
+ if (prev !== null && withinTime && withinDist) {
376
+ ctx.events.push({
377
+ kind: "double-tap",
378
+ pointerId: ev.pointerId,
379
+ x: ev.x,
380
+ y: ev.y,
381
+ pointerType: narrowGesturePointerType(ev.pointerType),
382
+ timestamp: now
383
+ });
384
+ ctx.lastTap = null;
385
+ return;
386
+ }
387
+ ctx.lastTap = { time: now, x: ev.x, y: ev.y };
388
+ }
389
+ function endPinchIfMember(ctx, pointerId, phase, now) {
390
+ const pinch = ctx.pinch;
391
+ if (!pinch || pinch.pointerA !== pointerId && pinch.pointerB !== pointerId) return;
392
+ emitPinchLifecycle(ctx, phase, pinch, now);
393
+ ctx.pinch = null;
394
+ if (phase === "cancel") {
395
+ ctx.gesture = { pinchScale: 1, rotationAngle: 0 };
396
+ }
397
+ }
398
+ function handleUp(ctx, ev, now) {
399
+ detectSwipe(ctx, ev, now);
400
+ detectDoubleTap(ctx, ev, now);
401
+ ctx.longPresses.delete(ev.pointerId);
402
+ ctx.swipeHistory.delete(ev.pointerId);
403
+ endPinchIfMember(ctx, ev.pointerId, "end", now);
404
+ }
405
+ function handleCancel(ctx, ev, now) {
406
+ ctx.longPresses.delete(ev.pointerId);
407
+ ctx.swipeHistory.delete(ev.pointerId);
408
+ endPinchIfMember(ctx, ev.pointerId, "cancel", now);
409
+ }
410
+ function tryLockPair(ctx, pointerMap, now) {
411
+ if (ctx.pinch !== null || pointerMap.size < 2) return;
412
+ const ids = [...pointerMap.keys()].sort((a2, b2) => a2 - b2);
413
+ const idA = ids[0];
414
+ const idB = ids[1];
415
+ if (idA === void 0 || idB === void 0) return;
416
+ const a = pointerMap.get(idA);
417
+ const b = pointerMap.get(idB);
418
+ if (!a || !b) return;
419
+ ctx.pinch = {
420
+ pointerA: idA,
421
+ pointerB: idB,
422
+ initialDistance: distance(a.x, a.y, b.x, b.y) || 1,
423
+ lastAngle: angleOf(a.x, a.y, b.x, b.y),
424
+ pointerType: a.pointerType
425
+ };
426
+ ctx.gesture = { pinchScale: 1, rotationAngle: 0 };
427
+ emitPinchLifecycle(ctx, "begin", ctx.pinch, now);
428
+ }
429
+ function updatePinchContinuous(ctx, pointerMap) {
430
+ const pinch = ctx.pinch;
431
+ if (!pinch) return;
432
+ const a = pointerMap.get(pinch.pointerA);
433
+ const b = pointerMap.get(pinch.pointerB);
434
+ if (!a || !b) return;
435
+ const curDist = distance(a.x, a.y, b.x, b.y);
436
+ const curAngle = angleOf(a.x, a.y, b.x, b.y);
437
+ const delta = normalizeAngle(curAngle - pinch.lastAngle);
438
+ ctx.gesture = {
439
+ pinchScale: curDist / pinch.initialDistance,
440
+ rotationAngle: ctx.gesture.rotationAngle + delta
441
+ };
442
+ ctx.pinch = { ...pinch, lastAngle: curAngle };
443
+ }
444
+ function fireLongPresses(ctx, pointerMap, now) {
445
+ const pinch = ctx.pinch;
446
+ for (const [id, lp] of ctx.longPresses) {
447
+ if (!lp.armed || lp.fired) continue;
448
+ if (now - lp.downTime < LONG_PRESS_DURATION_MS) continue;
449
+ if (!pointerMap.has(id)) continue;
450
+ if (pinch && (pinch.pointerA === id || pinch.pointerB === id)) continue;
451
+ ctx.events.push({
452
+ kind: "long-press",
453
+ pointerId: id,
454
+ x: lp.x,
455
+ y: lp.y,
456
+ pointerType: narrowGesturePointerType(lp.pointerType),
457
+ timestamp: now
458
+ });
459
+ ctx.longPresses.set(id, { ...lp, fired: true });
460
+ }
461
+ }
462
+ function processGestureFrame(phaseQueue, pointerMap, prevState, now) {
463
+ const ctx = {
464
+ pinch: prevState.pinch,
465
+ gesture: { ...prevState.gesture },
466
+ longPresses: new Map(prevState.longPresses),
467
+ lastTap: prevState.lastTap,
468
+ swipeHistory: cloneSwipeHistory(prevState.swipeHistory),
469
+ events: []
470
+ };
471
+ for (const ev of phaseQueue) {
472
+ switch (ev.phase) {
473
+ case "down":
474
+ handleDown(ctx, ev, now);
475
+ break;
476
+ case "move":
477
+ handleMove(ctx, ev, now);
478
+ break;
479
+ case "up":
480
+ handleUp(ctx, ev, now);
481
+ break;
482
+ case "cancel":
483
+ handleCancel(ctx, ev, now);
484
+ break;
485
+ }
486
+ }
487
+ tryLockPair(ctx, pointerMap, now);
488
+ updatePinchContinuous(ctx, pointerMap);
489
+ fireLongPresses(ctx, pointerMap, now);
490
+ const gesture = Object.freeze({
491
+ pinchScale: ctx.gesture.pinchScale,
492
+ rotationAngle: ctx.gesture.rotationAngle
493
+ });
494
+ const newState = {
495
+ pinch: ctx.pinch,
496
+ gesture,
497
+ longPresses: ctx.longPresses,
498
+ lastTap: ctx.lastTap,
499
+ swipeHistory: ctx.swipeHistory
500
+ };
501
+ return { newState, gestureState: gesture, gestureEvents: ctx.events };
502
+ }
503
+ function cloneSwipeHistory(src) {
504
+ const out = /* @__PURE__ */ new Map();
505
+ for (const [id, samples] of src) out.set(id, [...samples]);
506
+ return out;
507
+ }
508
+
509
+ // src/ui-ownership.ts
510
+ function contains(root, value) {
511
+ const NodeCtor = globalThis.Node;
512
+ return NodeCtor && value instanceof NodeCtor ? value === root || root.contains(value) : false;
513
+ }
514
+ function resolveUiOwnership(event, host) {
515
+ const path = event.composedPath;
516
+ if (typeof path === "function") {
517
+ const composed = path.call(event);
518
+ return {
519
+ owned: composed.some((item) => contains(host, item)) || composed.length === 0 && contains(host, event.target),
520
+ source: composed.length === 0 ? "target-fallback" : "composed-path"
521
+ };
522
+ }
523
+ if ("target" in event) {
524
+ return { owned: contains(host, event.target), source: "target-fallback" };
525
+ }
526
+ return { owned: false, source: "unknown" };
527
+ }
528
+ function isUiOwnedEvent(event, host) {
529
+ return resolveUiOwnership(event, host).owned;
530
+ }
531
+ function createUiInputResetBoundary(options) {
532
+ let disposed = false;
533
+ const reset = () => {
534
+ if (!disposed) options.clear();
535
+ };
536
+ const onAbort = () => reset();
537
+ options.signal?.addEventListener("abort", onAbort, { once: true });
538
+ return {
539
+ reset,
540
+ dispose: () => {
541
+ if (disposed) return;
542
+ disposed = true;
543
+ options.signal?.removeEventListener("abort", onAbort);
544
+ }
545
+ };
546
+ }
547
+
548
+ // src/virtual-joystick.ts
549
+ function computeVec(ptrX, ptrY, originX, originY, radius, deadzone) {
550
+ const rawX = (ptrX - originX) / radius;
551
+ const rawY = (ptrY - originY) / radius;
552
+ const mag = Math.sqrt(rawX * rawX + rawY * rawY);
553
+ if (mag < deadzone) return { x: 0, y: 0 };
554
+ if (mag > 1) {
555
+ return { x: rawX / mag, y: rawY / mag };
556
+ }
557
+ return { x: rawX, y: rawY };
558
+ }
559
+ function deriveVirtualAxes(configs, pointerMap, bindState) {
560
+ const results = [];
561
+ for (const cfg of configs) {
562
+ const bs = bindState.get(cfg.name);
563
+ if (bs?.pointerId !== void 0) {
564
+ const ptr = pointerMap.get(bs.pointerId);
565
+ if (ptr) {
566
+ const vec = computeVec(ptr.x, ptr.y, bs.originX, bs.originY, cfg.radius, cfg.deadzone);
567
+ results.push({ name: cfg.name, x: vec.x, y: vec.y });
568
+ } else {
569
+ bs.pointerId = void 0;
570
+ results.push({ name: cfg.name, x: 0, y: 0 });
571
+ }
572
+ } else {
573
+ results.push({ name: cfg.name, x: 0, y: 0 });
574
+ }
575
+ }
576
+ return results;
577
+ }
578
+ function handleVirtualJoystickUnbind(bindState, pointerId) {
579
+ for (const bs of bindState.values()) {
580
+ if (bs.pointerId === pointerId) {
581
+ bs.pointerId = void 0;
582
+ }
583
+ }
584
+ }
585
+
586
+ // src/browser-backend.ts
587
+ function coercePointerType(raw) {
588
+ if (raw === "pen") return "pen";
589
+ if (raw === "touch") return "touch";
590
+ return "mouse";
591
+ }
592
+ function attachBrowserInputBackend(canvas, options = {}) {
593
+ const doc = options.document ?? globalThis.document;
594
+ const win = options.window ?? globalThis.window;
595
+ const heldKeys = /* @__PURE__ */ new Set();
596
+ const upEdges = /* @__PURE__ */ new Set();
597
+ const heldCodes = /* @__PURE__ */ new Set();
598
+ const upCodeEdges = /* @__PURE__ */ new Set();
599
+ const buttons = [false, false, false];
600
+ let mvx = 0;
601
+ let mvy = 0;
602
+ let wheelAccum = 0;
603
+ let detached = false;
604
+ const isUiEvent = (event) => options.uiRoot !== void 0 && isUiOwnedEvent(event, options.uiRoot);
605
+ let w3cLocked = false;
606
+ let providerLocked = false;
607
+ let gameGate = true;
608
+ function releaseProviderLock() {
609
+ if (providerLocked && options.lockProvider?.exitLock) {
610
+ try {
611
+ options.lockProvider.exitLock();
612
+ } catch (err) {
613
+ if (options.onLockError) options.onLockError({ path: "provider", cause: err });
614
+ }
615
+ }
616
+ providerLocked = false;
617
+ }
618
+ const nav = options.navigator ?? globalThis.navigator;
619
+ const gamepadAvailable = typeof nav?.getGamepads === "function";
620
+ const pointerAvailable = typeof globalThis.PointerEvent !== "undefined";
621
+ const caps = { gamepad: gamepadAvailable, pointer: pointerAvailable };
622
+ let prevGamepadFrame = /* @__PURE__ */ new Map();
623
+ let gamepadBaselinePending = false;
624
+ let focusResetPending = false;
625
+ let controllerDb;
626
+ let dbLoadStarted = false;
627
+ const guidCache = /* @__PURE__ */ new Map();
628
+ let controllerDbApi;
629
+ function kickOffDbLoad() {
630
+ if (dbLoadStarted) return;
631
+ dbLoadStarted = true;
632
+ const loadApi = controllerDbApi ? Promise.resolve(controllerDbApi) : import('@forgeax/engine-input/controller-db').then((m) => {
633
+ controllerDbApi = m;
634
+ return m;
635
+ });
636
+ const loadText = options.loadControllerDb ? options.loadControllerDb() : import('@forgeax/engine-input/controller-db-data').then((m) => m.loadBundledControllerDb());
637
+ Promise.all([loadApi, loadText]).then(([api, txt]) => {
638
+ controllerDb = api.parseControllerDb(txt);
639
+ }).catch(() => {
640
+ dbLoadStarted = false;
641
+ });
642
+ }
643
+ function remapLookup(gamepadId) {
644
+ if (!controllerDb || !controllerDbApi) return null;
645
+ let guid = guidCache.get(gamepadId);
646
+ if (!guidCache.has(gamepadId)) {
647
+ guid = controllerDbApi.extractGuidFromGamepadId(gamepadId);
648
+ guidCache.set(gamepadId, guid);
649
+ }
650
+ if (!guid) return null;
651
+ const ua = typeof globalThis.navigator?.userAgent === "string" ? globalThis.navigator.userAgent : "";
652
+ const platform = controllerDbApi.platformFromUserAgent(ua);
653
+ const entry = controllerDbApi.selectBestMappingEntry(controllerDb, guid, platform);
654
+ return entry ? entry.tokens : null;
655
+ }
656
+ const pointerMap = /* @__PURE__ */ new Map();
657
+ const phaseQueue = [];
658
+ let mouseX;
659
+ let mouseY;
660
+ const vjConfigs = options.virtualJoysticks ?? [];
661
+ const vjBindState = /* @__PURE__ */ new Map();
662
+ const now = options.now ?? (() => performance.now());
663
+ let recognizerState = createRecognizerState();
664
+ function computePointerCoords(ev) {
665
+ const rect = canvas.getBoundingClientRect?.();
666
+ if (!rect) return { x: ev.clientX, y: ev.clientY };
667
+ const scaleX = canvas.width / rect.width;
668
+ const scaleY = canvas.height / rect.height;
669
+ return {
670
+ x: (ev.clientX - rect.left) * scaleX,
671
+ y: (ev.clientY - rect.top) * scaleY
672
+ };
673
+ }
674
+ function isFocused() {
675
+ return typeof doc?.hasFocus === "function" ? doc.hasFocus() : true;
676
+ }
677
+ function onKeyDown(ev) {
678
+ if (isUiEvent(ev)) {
679
+ clear();
680
+ return;
681
+ }
682
+ heldKeys.add(ev.key);
683
+ upEdges.delete(ev.key);
684
+ if (ev.code) {
685
+ heldCodes.add(ev.code);
686
+ upCodeEdges.delete(ev.code);
687
+ }
688
+ if (ev.key === "Escape" && providerLocked) {
689
+ releaseProviderLock();
690
+ }
691
+ }
692
+ function onKeyUp(ev) {
693
+ if (isUiEvent(ev)) return;
694
+ heldKeys.delete(ev.key);
695
+ if (isFocused()) {
696
+ upEdges.add(ev.key);
697
+ }
698
+ if (ev.code) {
699
+ heldCodes.delete(ev.code);
700
+ if (isFocused()) upCodeEdges.add(ev.code);
701
+ }
702
+ }
703
+ function onPointerDown(ev) {
704
+ if (isUiEvent(ev)) {
705
+ clear();
706
+ return;
707
+ }
708
+ if (ev.pointerType === "mouse") {
709
+ if (ev.button === 0 || ev.button === 1 || ev.button === 2) {
710
+ buttons[ev.button] = true;
711
+ }
712
+ }
713
+ if (!w3cLocked && !providerLocked && typeof canvas.setPointerCapture === "function") {
714
+ try {
715
+ canvas.setPointerCapture(ev.pointerId);
716
+ } catch {
717
+ }
718
+ }
719
+ const coords = computePointerCoords(ev);
720
+ if (ev.pointerType === "mouse") {
721
+ mouseX = coords.x;
722
+ mouseY = coords.y;
723
+ }
724
+ pointerMap.set(ev.pointerId, {
725
+ x: coords.x,
726
+ y: coords.y,
727
+ pressure: ev.pressure,
728
+ pointerType: coercePointerType(ev.pointerType),
729
+ prevX: coords.x,
730
+ prevY: coords.y
731
+ });
732
+ phaseQueue.push({
733
+ pointerId: ev.pointerId,
734
+ phase: "down",
735
+ x: coords.x,
736
+ y: coords.y,
737
+ pressure: ev.pressure,
738
+ pointerType: coercePointerType(ev.pointerType)
739
+ });
740
+ if (vjConfigs.length > 0) {
741
+ for (const cfg of vjConfigs) {
742
+ const { region } = cfg;
743
+ if (coords.x >= region.x && coords.x <= region.x + region.width && coords.y >= region.y && coords.y <= region.y + region.height) {
744
+ const existing = vjBindState.get(cfg.name);
745
+ if (!existing?.pointerId) {
746
+ const originX = cfg.mode === "fixed" ? cfg.anchor?.x ?? region.x + region.width / 2 : coords.x;
747
+ const originY = cfg.mode === "fixed" ? cfg.anchor?.y ?? region.y + region.height / 2 : coords.y;
748
+ if (existing) {
749
+ existing.pointerId = ev.pointerId;
750
+ existing.originX = originX;
751
+ existing.originY = originY;
752
+ } else {
753
+ vjBindState.set(cfg.name, { pointerId: ev.pointerId, originX, originY });
754
+ }
755
+ break;
756
+ }
757
+ }
758
+ }
759
+ }
760
+ }
761
+ function onPointerUp(ev) {
762
+ if (isUiEvent(ev)) return;
763
+ if (ev.pointerType === "mouse") {
764
+ if (ev.button === 0 || ev.button === 1 || ev.button === 2) {
765
+ buttons[ev.button] = false;
766
+ }
767
+ }
768
+ const entry = pointerMap.get(ev.pointerId);
769
+ if (entry) {
770
+ phaseQueue.push({
771
+ pointerId: ev.pointerId,
772
+ phase: "up",
773
+ x: entry.x,
774
+ y: entry.y,
775
+ pressure: ev.pressure,
776
+ pointerType: coercePointerType(ev.pointerType)
777
+ });
778
+ pointerMap.delete(ev.pointerId);
779
+ }
780
+ if (vjBindState.size > 0) {
781
+ handleVirtualJoystickUnbind(vjBindState, ev.pointerId);
782
+ }
783
+ }
784
+ function onPointerMove(ev) {
785
+ if (isUiEvent(ev)) return;
786
+ if (ev.pointerType === "mouse") {
787
+ mvx += ev.movementX;
788
+ mvy += ev.movementY;
789
+ }
790
+ const coords = computePointerCoords(ev);
791
+ if (ev.pointerType === "mouse") {
792
+ mouseX = coords.x;
793
+ mouseY = coords.y;
794
+ }
795
+ const entry = pointerMap.get(ev.pointerId);
796
+ if (entry) {
797
+ entry.x = coords.x;
798
+ entry.y = coords.y;
799
+ entry.pressure = ev.pressure;
800
+ phaseQueue.push({
801
+ pointerId: ev.pointerId,
802
+ phase: "move",
803
+ x: coords.x,
804
+ y: coords.y,
805
+ pressure: ev.pressure,
806
+ pointerType: coercePointerType(ev.pointerType)
807
+ });
808
+ }
809
+ }
810
+ function onPointerCancel(ev) {
811
+ if (isUiEvent(ev)) return;
812
+ const entry = pointerMap.get(ev.pointerId);
813
+ if (entry) {
814
+ phaseQueue.push({
815
+ pointerId: ev.pointerId,
816
+ phase: "cancel",
817
+ x: entry.x,
818
+ y: entry.y,
819
+ pressure: ev.pressure,
820
+ pointerType: coercePointerType(ev.pointerType)
821
+ });
822
+ pointerMap.delete(ev.pointerId);
823
+ }
824
+ if (vjBindState.size > 0) {
825
+ handleVirtualJoystickUnbind(vjBindState, ev.pointerId);
826
+ }
827
+ }
828
+ function onVisibilityChange() {
829
+ if (doc.visibilityState === "hidden") {
830
+ onBlur();
831
+ }
832
+ }
833
+ function onWheel(ev) {
834
+ if (isUiEvent(ev)) {
835
+ clear();
836
+ return;
837
+ }
838
+ const dy = ev.deltaY;
839
+ if (typeof dy === "number" && dy !== 0) {
840
+ wheelAccum += dy > 0 ? 1 : -1;
841
+ }
842
+ }
843
+ function onBlur() {
844
+ upEdges.clear();
845
+ upCodeEdges.clear();
846
+ heldKeys.clear();
847
+ heldCodes.clear();
848
+ buttons[0] = false;
849
+ buttons[1] = false;
850
+ buttons[2] = false;
851
+ mvx = 0;
852
+ mvy = 0;
853
+ wheelAccum = 0;
854
+ if (typeof doc.exitPointerLock === "function" && doc.pointerLockElement === canvas) {
855
+ doc.exitPointerLock();
856
+ }
857
+ w3cLocked = false;
858
+ releaseProviderLock();
859
+ if (pointerMap.size > 0) {
860
+ for (const [id, entry] of pointerMap) {
861
+ phaseQueue.push({
862
+ pointerId: id,
863
+ phase: "cancel",
864
+ x: entry.x,
865
+ y: entry.y,
866
+ pressure: entry.pressure,
867
+ pointerType: entry.pointerType
868
+ });
869
+ }
870
+ pointerMap.clear();
871
+ }
872
+ prevGamepadFrame.clear();
873
+ gamepadBaselinePending = true;
874
+ focusResetPending = true;
875
+ vjBindState.clear();
876
+ }
877
+ const safeAdd = (target, kind, handler) => {
878
+ target?.addEventListener?.(kind, handler);
879
+ };
880
+ const safeRemove = (target, kind, handler) => {
881
+ target?.removeEventListener?.(kind, handler);
882
+ };
883
+ safeAdd(win, "keydown", onKeyDown);
884
+ safeAdd(win, "keyup", onKeyUp);
885
+ safeAdd(win, "blur", onBlur);
886
+ safeAdd(canvas, "pointerdown", onPointerDown);
887
+ safeAdd(canvas, "pointerup", onPointerUp);
888
+ safeAdd(canvas, "pointermove", onPointerMove);
889
+ safeAdd(canvas, "pointercancel", onPointerCancel);
890
+ safeAdd(canvas, "wheel", onWheel);
891
+ safeAdd(doc, "visibilitychange", onVisibilityChange);
892
+ function onPointerLockChange() {
893
+ const wasLocked = w3cLocked;
894
+ w3cLocked = doc.pointerLockElement === canvas;
895
+ if (wasLocked && !w3cLocked && !providerLocked) onBlur();
896
+ }
897
+ safeAdd(doc, "pointerlockchange", onPointerLockChange);
898
+ let _prevTouchAction;
899
+ if (canvas.style) {
900
+ _prevTouchAction = canvas.style.touchAction;
901
+ canvas.style.touchAction = "none";
902
+ }
903
+ function onCanvasClick() {
904
+ if (!gameGate) return;
905
+ if (options.pointerLockAllowed && !options.pointerLockAllowed()) return;
906
+ if (typeof doc.hasFocus === "function" && !doc.hasFocus()) return;
907
+ if (options.lockProvider) {
908
+ providerLocked = true;
909
+ try {
910
+ const result = options.lockProvider.requestLock();
911
+ if (result && typeof result.catch === "function") {
912
+ result.catch((cause) => {
913
+ providerLocked = false;
914
+ if (options.onLockError) {
915
+ options.onLockError({ path: "provider", cause });
916
+ }
917
+ });
918
+ }
919
+ } catch (cause) {
920
+ providerLocked = false;
921
+ if (options.onLockError) {
922
+ options.onLockError({ path: "provider", cause });
923
+ }
924
+ }
925
+ return;
926
+ }
927
+ const fn = canvas.requestPointerLock;
928
+ if (typeof fn !== "function") return;
929
+ const r = fn.call(canvas);
930
+ if (r && typeof r.catch === "function") {
931
+ r.catch((cause) => {
932
+ if (options.onLockError) {
933
+ options.onLockError({ path: "w3c", cause });
934
+ }
935
+ });
936
+ }
937
+ }
938
+ safeAdd(canvas, "click", onCanvasClick);
939
+ function sample() {
940
+ let gamepads;
941
+ if (gamepadAvailable) {
942
+ try {
943
+ const rawGamepads = nav.getGamepads?.() ?? [];
944
+ const valid = [];
945
+ let hasNonStandard = false;
946
+ for (const gp of rawGamepads) {
947
+ if (!gp?.connected) continue;
948
+ valid.push(gp);
949
+ if (gp.mapping !== "standard") hasNonStandard = true;
950
+ }
951
+ if (hasNonStandard) kickOffDbLoad();
952
+ gamepads = diffGamepadFrame(prevGamepadFrame, valid, remapLookup);
953
+ if (gamepadBaselinePending) {
954
+ gamepads = gamepads.map((slot) => ({
955
+ ...slot,
956
+ justPressed: /* @__PURE__ */ new Set(),
957
+ justReleased: /* @__PURE__ */ new Set()
958
+ }));
959
+ gamepadBaselinePending = false;
960
+ }
961
+ const nextFrame = /* @__PURE__ */ new Map();
962
+ for (const slot of gamepads) {
963
+ if (slot.pressed.size > 0 || slot.standardMapping) {
964
+ nextFrame.set(slot.index, slot);
965
+ }
966
+ }
967
+ prevGamepadFrame = nextFrame;
968
+ } catch {
969
+ }
970
+ }
971
+ let pointers;
972
+ if (pointerMap.size > 0) {
973
+ pointers = [];
974
+ for (const [id, entry] of pointerMap) {
975
+ const deltaX = entry.x - entry.prevX;
976
+ const deltaY = entry.y - entry.prevY;
977
+ pointers.push({
978
+ pointerId: id,
979
+ x: entry.x,
980
+ y: entry.y,
981
+ pressure: entry.pressure,
982
+ pointerType: entry.pointerType,
983
+ active: true,
984
+ delta: Object.freeze({ x: deltaX, y: deltaY })
985
+ });
986
+ entry.prevX = entry.x;
987
+ entry.prevY = entry.y;
988
+ }
989
+ }
990
+ const pointerEvents = phaseQueue.length > 0 ? [...phaseQueue] : void 0;
991
+ let virtualAxes;
992
+ if (vjConfigs.length > 0) {
993
+ virtualAxes = deriveVirtualAxes(vjConfigs, pointerMap, vjBindState);
994
+ }
995
+ const gestureResult = processGestureFrame(
996
+ phaseQueue,
997
+ pointerMap,
998
+ recognizerState,
999
+ now()
1000
+ );
1001
+ recognizerState = gestureResult.newState;
1002
+ const gestureEvents = gestureResult.gestureEvents.length > 0 ? gestureResult.gestureEvents : void 0;
1003
+ const gs = gestureResult.gestureState;
1004
+ const gestures = gs.pinchScale !== 1 || gs.rotationAngle !== 0 ? gs : void 0;
1005
+ const out = {
1006
+ downKeys: new Set(heldKeys),
1007
+ upKeys: new Set(upEdges),
1008
+ downCodes: new Set(heldCodes),
1009
+ upCodes: new Set(upCodeEdges),
1010
+ buttons: [buttons[0], buttons[1], buttons[2]],
1011
+ movementX: mvx,
1012
+ movementY: mvy,
1013
+ ...mouseX !== void 0 && mouseY !== void 0 ? { mouseX, mouseY } : {},
1014
+ wheelDelta: wheelAccum,
1015
+ focused: isFocused(),
1016
+ capabilities: caps,
1017
+ pointerLocked: w3cLocked || providerLocked,
1018
+ ...gamepads ? { gamepads } : {},
1019
+ ...pointers ? { pointers } : {},
1020
+ ...pointerEvents ? { pointerEvents } : {},
1021
+ ...virtualAxes ? { virtualAxes } : {},
1022
+ ...gestures ? { gestures } : {},
1023
+ ...gestureEvents ? { gestureEvents } : {},
1024
+ ...focusResetPending ? { focusReset: true } : {}
1025
+ };
1026
+ upEdges.clear();
1027
+ upCodeEdges.clear();
1028
+ mvx = 0;
1029
+ mvy = 0;
1030
+ wheelAccum = 0;
1031
+ phaseQueue.length = 0;
1032
+ focusResetPending = false;
1033
+ return out;
1034
+ }
1035
+ function setPointerLockAllowed(allowed) {
1036
+ gameGate = allowed;
1037
+ if (!allowed) {
1038
+ if (typeof doc.exitPointerLock === "function" && doc.pointerLockElement === canvas) {
1039
+ doc.exitPointerLock();
1040
+ }
1041
+ releaseProviderLock();
1042
+ }
1043
+ }
1044
+ function clear() {
1045
+ heldKeys.clear();
1046
+ upEdges.clear();
1047
+ heldCodes.clear();
1048
+ upCodeEdges.clear();
1049
+ pointerMap.clear();
1050
+ phaseQueue.length = 0;
1051
+ prevGamepadFrame.clear();
1052
+ vjBindState.clear();
1053
+ recognizerState = createRecognizerState();
1054
+ buttons[0] = false;
1055
+ buttons[1] = false;
1056
+ buttons[2] = false;
1057
+ mvx = 0;
1058
+ mvy = 0;
1059
+ wheelAccum = 0;
1060
+ if (typeof doc?.exitPointerLock === "function" && doc.pointerLockElement === canvas) {
1061
+ doc.exitPointerLock();
1062
+ }
1063
+ w3cLocked = false;
1064
+ releaseProviderLock();
1065
+ }
1066
+ function detach() {
1067
+ if (detached) return;
1068
+ detached = true;
1069
+ safeRemove(win, "keydown", onKeyDown);
1070
+ safeRemove(win, "keyup", onKeyUp);
1071
+ safeRemove(win, "blur", onBlur);
1072
+ safeRemove(canvas, "pointerdown", onPointerDown);
1073
+ safeRemove(canvas, "pointerup", onPointerUp);
1074
+ safeRemove(canvas, "pointermove", onPointerMove);
1075
+ safeRemove(canvas, "pointercancel", onPointerCancel);
1076
+ safeRemove(canvas, "wheel", onWheel);
1077
+ safeRemove(doc, "visibilitychange", onVisibilityChange);
1078
+ safeRemove(doc, "pointerlockchange", onPointerLockChange);
1079
+ safeRemove(canvas, "click", onCanvasClick);
1080
+ clear();
1081
+ if (canvas.style && _prevTouchAction !== void 0) {
1082
+ canvas.style.touchAction = _prevTouchAction;
1083
+ }
1084
+ }
1085
+ const backend = { sample, clear, detach, setPointerLockAllowed };
1086
+ const handle = Object.assign(detach, { backend });
1087
+ return handle;
1088
+ }
1089
+
1090
+ // src/input-snapshot.ts
1091
+ function createEmptyInputBackendSample() {
1092
+ return {
1093
+ downKeys: /* @__PURE__ */ new Set(),
1094
+ upKeys: /* @__PURE__ */ new Set(),
1095
+ buttons: [false, false, false],
1096
+ movementX: 0,
1097
+ movementY: 0,
1098
+ wheelDelta: 0,
1099
+ focused: true,
1100
+ pointerLocked: false
1101
+ };
1102
+ }
1103
+ function emptyGamepadReader() {
1104
+ return Object.freeze({
1105
+ connected: false,
1106
+ standardMapping: false,
1107
+ button(_b) {
1108
+ return false;
1109
+ },
1110
+ buttonValue(_b) {
1111
+ return 0;
1112
+ },
1113
+ justPressed(_b) {
1114
+ return false;
1115
+ },
1116
+ justReleased(_b) {
1117
+ return false;
1118
+ },
1119
+ axis(_a) {
1120
+ return 0;
1121
+ }
1122
+ });
1123
+ }
1124
+ function buildGamepadReader(slot) {
1125
+ if (!slot.standardMapping) {
1126
+ return Object.freeze({
1127
+ connected: true,
1128
+ standardMapping: false,
1129
+ button: emptyGamepadReader().button,
1130
+ buttonValue: emptyGamepadReader().buttonValue,
1131
+ justPressed: emptyGamepadReader().justPressed,
1132
+ justReleased: emptyGamepadReader().justReleased,
1133
+ axis: emptyGamepadReader().axis
1134
+ });
1135
+ }
1136
+ return Object.freeze({
1137
+ connected: true,
1138
+ standardMapping: true,
1139
+ button(b) {
1140
+ return slot.pressed.has(b);
1141
+ },
1142
+ buttonValue(b) {
1143
+ return slot.buttonValues.get(b) ?? 0;
1144
+ },
1145
+ justPressed(b) {
1146
+ return slot.justPressed.has(b);
1147
+ },
1148
+ justReleased(b) {
1149
+ return slot.justReleased.has(b);
1150
+ },
1151
+ axis(a) {
1152
+ return slot.axes[a];
1153
+ }
1154
+ });
1155
+ }
1156
+ function snapshotFromSample(sample, actionStates, inputMap, previousSnapshot) {
1157
+ const heldKeys = new Set(sample.downKeys);
1158
+ const upEdges = new Set(sample.upKeys);
1159
+ const justPressedKeys = /* @__PURE__ */ new Set();
1160
+ for (const key of heldKeys) {
1161
+ if (previousSnapshot === void 0 || !previousSnapshot.keyboard.down(key)) {
1162
+ justPressedKeys.add(key);
1163
+ }
1164
+ }
1165
+ const heldCodes = new Set(sample.downCodes ?? []);
1166
+ const upCodeEdges = new Set(sample.upCodes ?? []);
1167
+ const justPressedCodes = /* @__PURE__ */ new Set();
1168
+ for (const code of heldCodes) {
1169
+ if (previousSnapshot === void 0 || !previousSnapshot.keyboard.downCode(code)) {
1170
+ justPressedCodes.add(code);
1171
+ }
1172
+ }
1173
+ const buttons = [
1174
+ sample.buttons[0],
1175
+ sample.buttons[1],
1176
+ sample.buttons[2]
1177
+ ];
1178
+ const justPressedButtons = [
1179
+ buttons[0] && (previousSnapshot === void 0 || !previousSnapshot.mouse.button(0)),
1180
+ buttons[1] && (previousSnapshot === void 0 || !previousSnapshot.mouse.button(1)),
1181
+ buttons[2] && (previousSnapshot === void 0 || !previousSnapshot.mouse.button(2))
1182
+ ];
1183
+ const justReleasedButtons = [
1184
+ sample.focusReset !== true && previousSnapshot?.mouse.button(0) === true && !buttons[0],
1185
+ sample.focusReset !== true && previousSnapshot?.mouse.button(1) === true && !buttons[1],
1186
+ sample.focusReset !== true && previousSnapshot?.mouse.button(2) === true && !buttons[2]
1187
+ ];
1188
+ const movementDelta = Object.freeze({ x: sample.movementX, y: sample.movementY });
1189
+ const mousePosition = sample.mouseX === void 0 || sample.mouseY === void 0 ? void 0 : Object.freeze({ x: sample.mouseX, y: sample.mouseY });
1190
+ const wheelDelta = sample.wheelDelta;
1191
+ const gamepadSlots = sample.gamepads ?? [];
1192
+ const caps = sample.capabilities ?? Object.freeze({ gamepad: false, pointer: false });
1193
+ const pointerEvts = sample.pointerEvents ?? [];
1194
+ const gesture = sample.gestures ?? IDENTITY_GESTURE;
1195
+ const gestureEvts = sample.gestureEvents ?? [];
1196
+ const _virtualAxes = sample.virtualAxes ?? [];
1197
+ const virtualAxesMap = /* @__PURE__ */ new Map();
1198
+ for (const va of _virtualAxes) {
1199
+ virtualAxesMap.set(va.name, va);
1200
+ }
1201
+ const gamepadSlotMap = /* @__PURE__ */ new Map();
1202
+ for (const slot of gamepadSlots) {
1203
+ gamepadSlotMap.set(slot.index, slot);
1204
+ }
1205
+ const actionMap = /* @__PURE__ */ new Map();
1206
+ if (actionStates) {
1207
+ for (const a of actionStates) {
1208
+ actionMap.set(a.action, a);
1209
+ }
1210
+ }
1211
+ function emptyActionReader() {
1212
+ return Object.freeze({
1213
+ isPressed: () => false,
1214
+ justPressed: () => false,
1215
+ justReleased: () => false,
1216
+ strength: 0
1217
+ });
1218
+ }
1219
+ const snapshot = {
1220
+ keyboard: {
1221
+ down(key) {
1222
+ return heldKeys.has(key);
1223
+ },
1224
+ justPressed(key) {
1225
+ return justPressedKeys.has(key);
1226
+ },
1227
+ up(key) {
1228
+ return upEdges.has(key);
1229
+ },
1230
+ downCode(code) {
1231
+ return heldCodes.has(code);
1232
+ },
1233
+ justPressedCode(code) {
1234
+ return justPressedCodes.has(code);
1235
+ },
1236
+ upCode(code) {
1237
+ return upCodeEdges.has(code);
1238
+ }
1239
+ },
1240
+ mouse: {
1241
+ position: mousePosition,
1242
+ movementDelta,
1243
+ pointerLocked: sample.pointerLocked,
1244
+ button(i) {
1245
+ return buttons[i] === true;
1246
+ },
1247
+ justPressed(i) {
1248
+ return justPressedButtons[i] === true;
1249
+ },
1250
+ justReleased(i) {
1251
+ return justReleasedButtons[i] === true;
1252
+ },
1253
+ wheelDelta
1254
+ },
1255
+ gamepad(i) {
1256
+ const slot = gamepadSlotMap.get(i);
1257
+ if (!slot) return emptyGamepadReader();
1258
+ return buildGamepadReader(slot);
1259
+ },
1260
+ capabilities: caps,
1261
+ pointer(id) {
1262
+ const entry = (sample.pointers ?? []).find((p) => p.pointerId === id);
1263
+ if (!entry) {
1264
+ return Object.freeze({
1265
+ active: false,
1266
+ pointerId: -1,
1267
+ x: 0,
1268
+ y: 0,
1269
+ pressure: 0,
1270
+ pointerType: "mouse",
1271
+ delta: Object.freeze({ x: 0, y: 0 })
1272
+ });
1273
+ }
1274
+ return Object.freeze({
1275
+ ...entry,
1276
+ delta: Object.freeze({ x: entry.delta.x, y: entry.delta.y })
1277
+ });
1278
+ },
1279
+ virtualAxis(name) {
1280
+ const va = virtualAxesMap.get(name);
1281
+ if (!va) return Object.freeze({ x: 0, y: 0 });
1282
+ return Object.freeze({ x: va.x, y: va.y });
1283
+ },
1284
+ action(name) {
1285
+ const s = actionMap.get(name);
1286
+ if (!s) return emptyActionReader();
1287
+ return Object.freeze({
1288
+ isPressed: () => s.pressed,
1289
+ justPressed: () => s.justPressed,
1290
+ justReleased: () => s.justReleased,
1291
+ strength: s.strength
1292
+ });
1293
+ },
1294
+ getAxis(neg, pos) {
1295
+ if (!actionStates || !inputMap) return 0;
1296
+ return getAxis(inputMap, actionStates, neg, pos);
1297
+ },
1298
+ getVector(negX, posX, negY, posY, opts) {
1299
+ if (!actionStates || !inputMap) return { x: 0, y: 0 };
1300
+ return getVector(inputMap, actionStates, negX, posX, negY, posY, opts);
1301
+ },
1302
+ pointerEvents: pointerEvts,
1303
+ gesture,
1304
+ gestureEvents: gestureEvts
1305
+ };
1306
+ snapshot._actionStates = actionStates;
1307
+ snapshot._inputMap = inputMap;
1308
+ return Object.freeze(snapshot);
1309
+ }
1310
+ function readActionStatesForEdgeDiff(snap) {
1311
+ return snap._actionStates;
1312
+ }
1313
+ function createInputSnapshot() {
1314
+ return snapshotFromSample(createEmptyInputBackendSample());
1315
+ }
1316
+ var INPUT_SNAPSHOT_RESOURCE_KEY = "InputSnapshot";
1317
+
1318
+ // src/canvas-input-boundary.ts
1319
+ function createCanvasInputBoundary(source) {
1320
+ let active = "editor";
1321
+ let gamePointerLockAllowed = false;
1322
+ source.setPointerLockAllowed?.(false);
1323
+ const empty = () => createEmptyInputBackendSample();
1324
+ const clear = () => {
1325
+ source.clear?.();
1326
+ source.setPointerLockAllowed?.(false);
1327
+ };
1328
+ const routed = (consumer) => ({
1329
+ sample: () => consumer === active ? source.sample() : empty(),
1330
+ setPointerLockAllowed: (allowed) => {
1331
+ if (consumer === "game") {
1332
+ gamePointerLockAllowed = allowed;
1333
+ source.setPointerLockAllowed?.(active === "game" && allowed);
1334
+ } else {
1335
+ source.setPointerLockAllowed?.(false);
1336
+ }
1337
+ },
1338
+ clear: () => {
1339
+ if (consumer === active) clear();
1340
+ },
1341
+ detach: () => source.detach()
1342
+ });
1343
+ const grantGame = () => {
1344
+ if (active === "game") return;
1345
+ clear();
1346
+ active = "game";
1347
+ source.setPointerLockAllowed?.(gamePointerLockAllowed);
1348
+ };
1349
+ const revokeGame = () => {
1350
+ clear();
1351
+ active = "editor";
1352
+ };
1353
+ return {
1354
+ editor: routed("editor"),
1355
+ game: routed("game"),
1356
+ owner: () => active,
1357
+ grantGame,
1358
+ revokeGame,
1359
+ handleUiEvent: (event, host) => {
1360
+ if (!isUiOwnedEvent(event, host)) return false;
1361
+ clear();
1362
+ return true;
1363
+ },
1364
+ detach: () => source.detach()
1365
+ };
1366
+ }
1367
+
1368
+ // src/composite-backend.ts
1369
+ function makeCompositeBackend(inner, options) {
1370
+ let yieldToHuman = options?.yieldToHuman ?? true;
1371
+ const heldKeys = /* @__PURE__ */ new Set();
1372
+ const buttons = [false, false, false];
1373
+ const upEdges = /* @__PURE__ */ new Set();
1374
+ let mvx = 0;
1375
+ let mvy = 0;
1376
+ let wheel = 0;
1377
+ function injectionActive() {
1378
+ return heldKeys.size > 0 || upEdges.size > 0 || buttons.some((b) => b);
1379
+ }
1380
+ function sample() {
1381
+ const base = inner.sample();
1382
+ const downKeys = new Set(base.downKeys);
1383
+ for (const k of heldKeys) {
1384
+ if (yieldToHuman && base.downKeys.has(k)) continue;
1385
+ downKeys.add(k);
1386
+ }
1387
+ const mergedUp = new Set(base.upKeys);
1388
+ for (const k of upEdges) {
1389
+ if (yieldToHuman && base.downKeys.has(k)) continue;
1390
+ mergedUp.add(k);
1391
+ }
1392
+ const mergedButtons = [
1393
+ base.buttons[0] || buttons[0],
1394
+ base.buttons[1] || buttons[1],
1395
+ base.buttons[2] || buttons[2]
1396
+ ];
1397
+ const focused = base.focused || injectionActive();
1398
+ const out = {
1399
+ ...base,
1400
+ // carry inner optional fields (pointers/gamepads/gestures/...) untouched
1401
+ downKeys,
1402
+ upKeys: mergedUp,
1403
+ buttons: mergedButtons,
1404
+ movementX: base.movementX + mvx,
1405
+ movementY: base.movementY + mvy,
1406
+ wheelDelta: base.wheelDelta + wheel,
1407
+ focused,
1408
+ pointerLocked: base.pointerLocked
1409
+ // AI never fabricates a lock
1410
+ };
1411
+ upEdges.clear();
1412
+ mvx = 0;
1413
+ mvy = 0;
1414
+ wheel = 0;
1415
+ return out;
1416
+ }
1417
+ const lockGate = inner.setPointerLockAllowed ? { setPointerLockAllowed: (allowed) => inner.setPointerLockAllowed?.(allowed) } : {};
1418
+ return {
1419
+ sample,
1420
+ ...lockGate,
1421
+ // Teardown belongs to the human backend -- forward it.
1422
+ detach: () => inner.detach(),
1423
+ press(key) {
1424
+ heldKeys.add(key);
1425
+ upEdges.delete(key);
1426
+ },
1427
+ release(key) {
1428
+ if (heldKeys.delete(key)) upEdges.add(key);
1429
+ },
1430
+ setButton(slot, down) {
1431
+ buttons[slot] = down;
1432
+ },
1433
+ addMovement(dx, dy) {
1434
+ mvx += dx;
1435
+ mvy += dy;
1436
+ },
1437
+ addWheel(notches) {
1438
+ wheel += notches;
1439
+ },
1440
+ clearInjected() {
1441
+ for (const k of heldKeys) upEdges.add(k);
1442
+ heldKeys.clear();
1443
+ buttons[0] = buttons[1] = buttons[2] = false;
1444
+ mvx = 0;
1445
+ mvy = 0;
1446
+ wheel = 0;
1447
+ },
1448
+ setYieldToHuman(yield_) {
1449
+ yieldToHuman = yield_;
1450
+ }
1451
+ };
1452
+ }
1453
+ var FRAME_START_SCAN_SYSTEM_NAME = "input-frame-start-scan";
1454
+ var InputSet = defineSystemSet({ name: "input" });
1455
+ var INPUT_BACKEND_KEY = "InputBackend";
1456
+ var InputFrameStartScan = defineSystem({
1457
+ name: FRAME_START_SCAN_SYSTEM_NAME,
1458
+ queries: [],
1459
+ fn: (world) => {
1460
+ const backend = world.getResource(INPUT_BACKEND_KEY);
1461
+ const sample = backend.sample();
1462
+ let actionStates;
1463
+ let inputMap;
1464
+ if (world.hasResource(INPUT_MAP_KEY)) {
1465
+ inputMap = world.getResource(INPUT_MAP_KEY);
1466
+ let prevActionStates;
1467
+ if (world.hasResource(INPUT_SNAPSHOT_RESOURCE_KEY)) {
1468
+ const prevSnap = world.getResource(INPUT_SNAPSHOT_RESOURCE_KEY);
1469
+ prevActionStates = readActionStatesForEdgeDiff(prevSnap);
1470
+ }
1471
+ actionStates = deriveActionStates(sample, inputMap, prevActionStates);
1472
+ }
1473
+ const previousSnapshot = world.hasResource(INPUT_SNAPSHOT_RESOURCE_KEY) ? world.getResource(INPUT_SNAPSHOT_RESOURCE_KEY) : void 0;
1474
+ const snapshot = snapshotFromSample(sample, actionStates, inputMap, previousSnapshot);
1475
+ world.insertResource(INPUT_SNAPSHOT_RESOURCE_KEY, snapshot);
1476
+ }
1477
+ });
1478
+
1479
+ // src/plugin-service.ts
1480
+ function inputBackendPlugin(backend) {
1481
+ return {
1482
+ name: "input-backend",
1483
+ provide: "input",
1484
+ apply(ctx) {
1485
+ ctx.provide("input", backend);
1486
+ }
1487
+ };
1488
+ }
1489
+ function ownedInputBackendPlugin(backend, dispose) {
1490
+ return {
1491
+ name: "input-backend",
1492
+ provide: "input",
1493
+ apply(ctx) {
1494
+ ctx.provide("input", backend);
1495
+ ctx.effect(() => dispose, "input/backend");
1496
+ }
1497
+ };
1498
+ }
1499
+
1500
+ export { DOUBLE_TAP_DISTANCE, DOUBLE_TAP_INTERVAL_MS, FRAME_START_SCAN_SYSTEM_NAME, IDENTITY_GESTURE, INPUT_BACKEND_KEY, INPUT_MAP_KEY, INPUT_SNAPSHOT_RESOURCE_KEY, InputFrameStartScan, InputSet, LONG_PRESS_DURATION_MS, LONG_PRESS_SLOP, SWIPE_VELOCITY_THRESHOLD, SWIPE_WINDOW_MS, attachBrowserInputBackend, createCanvasInputBoundary, createInputSnapshot, createUiInputResetBoundary, deriveActionStates, getAxis, getVector, inputBackendPlugin, isUiOwnedEvent, makeCompositeBackend, ownedInputBackendPlugin, resolveUiOwnership, snapshotFromSample };
1501
+ //# sourceMappingURL=index.mjs.map
1502
+ //# sourceMappingURL=index.mjs.map