@forgeax/engine-input 0.1.20 → 0.1.23

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/README.md CHANGED
@@ -93,6 +93,13 @@ detach();
93
93
  | `snap.keyboard.justPressedCode(code)` | `boolean` | physical code 上一帧未按、本帧按下(一帧寿命) |
94
94
  | `snap.keyboard.upCode(code)` | `boolean` | physical code 上一帧按下、本帧释放(一帧寿命) |
95
95
 
96
+ 键盘和鼠标的 press/release 是 backend 在事件到达时记录的边沿,而不
97
+ 是扫描时用 held 状态反推。因而一次 `keydown → keyup` 或
98
+ `pointerdown → pointerup` 若发生在两个帧首扫描之间,仍会在下一份
99
+ `InputSnapshot` 中同时留下 press 与 release;扫描后各边沿只保留一帧。
100
+ `InputFrameStartScan` 每帧只调用一次 `sample()`,因此用户系统不会看到
101
+ 半更新的输入状态,也不应自行读取 DOM 事件。
102
+
96
103
  ### mouse(5 读点)
97
104
 
98
105
  | 调用 | 返回 | 语义 |
@@ -218,6 +225,14 @@ Five gesture recognizers running in the backend closure (C-3 single legal cross-
218
225
  | `setPointerLockAllowed(false)` immediate release | When `gameGate` transitions from `true` to `false` and a lock is active, the backend immediately releases: W3C path calls `exitPointerLock()`, provider path calls `exitLock()` + clears `providerLocked`. This solves the "mode switches to top-down while locked" boundary. |
219
226
  | lockProvider error → onLockError | W3C `requestPointerLock` promise rejection and provider `requestLock` throw/reject both route through `onLockError({ path, cause })`. Provider failure also rolls back `providerLocked = false`. The callback is wired by `attachInputAuto` into `AppError({ code: 'app-pointer-lock-failed' })` on the app's `onError` fan-out. |
220
227
 
228
+ Pointer Lock is attempted from the trusted canvas click when the two explicit
229
+ game/host gates allow it; `document.hasFocus()` is not a third gate because a
230
+ browser may report a transient false value during the same activation. Both a
231
+ rejected W3C request and the browser `pointerlockerror` event are converted to
232
+ the same `onLockError` diagnostic and are caught before they can become an
233
+ unhandled Promise rejection. Lock state remains a boolean readpoint in the
234
+ snapshot; DOM error events never cross the consumer boundary.
235
+
221
236
  ## Pointer-lock contract: PointerLockProvider / lockProvider / setPointerLockAllowed / pointerLocked
222
237
 
223
238
  Four anchors cover the full pointer-lock surface across the input-backend boundary.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=browser-backend-edge-latch.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-backend-edge-latch.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/browser-backend-edge-latch.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=browser-edge-latch.browser.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-edge-latch.browser.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/browser-edge-latch.browser.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=pointer-lock.browser.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pointer-lock.browser.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/pointer-lock.browser.test.ts"],"names":[],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"browser-backend.d.ts","sourceRoot":"","sources":["../src/browser-backend.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAGV,YAAY,EAIZ,WAAW,EAEX,qBAAqB,EACtB,MAAM,kBAAkB,CAAC;AAQ1B,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAI1D;AAED;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC;IACvB;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,OAAO,CAAC;IAC5C;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE;QAAE,WAAW,CAAC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,EAAE,CAAA;KAAE,CAAC;IAC5D;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,qBAAqB,EAAE,CAAC;IAC7D;;;;;;;OAOG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IAClD;;;;;;;OAOG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5B;;;;;;OAMG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,mBAAmB,CAAC;IAC5C;;;;;;OAMG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,KAAK,GAAG,UAAU,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC;CACvF;AAED;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,WAAW,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,4BAA4B;IAC5B,QAAQ,IAAI,IAAI,CAAC;CAClB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,iBAAiB,EACzB,OAAO,GAAE,0BAA+B,GACvC,CAAC,MAAM,IAAI,CAAC,GAAG;IAAE,OAAO,EAAE,YAAY,CAAA;CAAE,CAysB1C"}
1
+ {"version":3,"file":"browser-backend.d.ts","sourceRoot":"","sources":["../src/browser-backend.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAGV,YAAY,EAIZ,WAAW,EAEX,qBAAqB,EACtB,MAAM,kBAAkB,CAAC;AAQ1B,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAI1D;AAED;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC;IACvB;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,OAAO,CAAC;IAC5C;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE;QAAE,WAAW,CAAC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,EAAE,CAAA;KAAE,CAAC;IAC5D;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,qBAAqB,EAAE,CAAC;IAC7D;;;;;;;OAOG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IAClD;;;;;;;OAOG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5B;;;;;;OAMG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,mBAAmB,CAAC;IAC5C;;;;;;OAMG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,KAAK,GAAG,UAAU,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC;CACvF;AAED;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,WAAW,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,4BAA4B;IAC5B,QAAQ,IAAI,IAAI,CAAC;CAClB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,iBAAiB,EACzB,OAAO,GAAE,0BAA+B,GACvC,CAAC,MAAM,IAAI,CAAC,GAAG;IAAE,OAAO,EAAE,YAAY,CAAA;CAAE,CA6vB1C"}
@@ -25,7 +25,10 @@ export interface CompositeInputBackend extends InputBackend {
25
25
  addMovement(dx: number, dy: number): void;
26
26
  /** Accumulate injected wheel notches (drained on next sample). */
27
27
  addWheel(notches: number): void;
28
- /** Drop all injected state; currently-held keys emit a clean up-edge once. */
28
+ /**
29
+ * Drop all injected state and pending frame edges; currently-held keys/buttons
30
+ * emit one clean release edge so a previously observed hold is closed.
31
+ */
29
32
  clearInjected(): void;
30
33
  /** Toggle the yield-to-human gate at runtime. */
31
34
  setYieldToHuman(yield_: boolean): void;
@@ -1 +1 @@
1
- {"version":3,"file":"composite-backend.d.ts","sourceRoot":"","sources":["../src/composite-backend.ts"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EAAE,YAAY,EAAsB,MAAM,kBAAkB,CAAC;AAEzE,sDAAsD;AACtD,MAAM,WAAW,uBAAuB;IACtC;;;;;OAKG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC;CACjC;AAED;;;;GAIG;AACH,MAAM,WAAW,qBAAsB,SAAQ,YAAY;IACzD,uDAAuD;IACvD,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,iFAAiF;IACjF,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,4DAA4D;IAC5D,SAAS,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC;IAChD,gFAAgF;IAChF,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1C,kEAAkE;IAClE,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,8EAA8E;IAC9E,aAAa,IAAI,IAAI,CAAC;IACtB,iDAAiD;IACjD,eAAe,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;CACxC;AAED,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,YAAY,EACnB,OAAO,CAAC,EAAE,uBAAuB,GAChC,qBAAqB,CAsHvB"}
1
+ {"version":3,"file":"composite-backend.d.ts","sourceRoot":"","sources":["../src/composite-backend.ts"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EAAE,YAAY,EAAsB,MAAM,kBAAkB,CAAC;AAEzE,sDAAsD;AACtD,MAAM,WAAW,uBAAuB;IACtC;;;;;OAKG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC;CACjC;AAED;;;;GAIG;AACH,MAAM,WAAW,qBAAsB,SAAQ,YAAY;IACzD,uDAAuD;IACvD,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,iFAAiF;IACjF,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,4DAA4D;IAC5D,SAAS,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC;IAChD,gFAAgF;IAChF,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1C,kEAAkE;IAClE,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC;;;OAGG;IACH,aAAa,IAAI,IAAI,CAAC;IACtB,iDAAiD;IACjD,eAAe,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;CACxC;AAED,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,YAAY,EACnB,OAAO,CAAC,EAAE,uBAAuB,GAChC,qBAAqB,CAsLvB"}
@@ -23,8 +23,8 @@ export declare const INPUT_BACKEND_KEY: "InputBackend";
23
23
  * Module-level `defineSystem` with the real fn body — no factory, no closure.
24
24
  * Each `world.update()` tick:
25
25
  * 1. reads the {@link InputBackend} from `INPUT_BACKEND_KEY` and calls
26
- * `backend.sample()` to drain the per-frame accumulator (movement delta +
27
- * up-edge set);
26
+ * `backend.sample()` once to atomically drain the per-frame accumulators
27
+ * (movement delta plus key/code/button press and release edge sets);
28
28
  * 2. derives a fresh `InputSnapshot` via `snapshotFromSample`;
29
29
  * 3. writes it under `INPUT_SNAPSHOT_RESOURCE_KEY` via
30
30
  * `world.insertResource` -- idempotent overwrite, charter P4
package/dist/index.mjs CHANGED
@@ -596,7 +596,11 @@ function attachBrowserInputBackend(canvas, options = {}) {
596
596
  const upEdges = /* @__PURE__ */ new Set();
597
597
  const heldCodes = /* @__PURE__ */ new Set();
598
598
  const upCodeEdges = /* @__PURE__ */ new Set();
599
+ const pressedKeys = /* @__PURE__ */ new Set();
600
+ const pressedCodes = /* @__PURE__ */ new Set();
599
601
  const buttons = [false, false, false];
602
+ const pressedButtons = [false, false, false];
603
+ const releasedButtons = [false, false, false];
600
604
  let mvx = 0;
601
605
  let mvy = 0;
602
606
  let wheelAccum = 0;
@@ -605,12 +609,18 @@ function attachBrowserInputBackend(canvas, options = {}) {
605
609
  let w3cLocked = false;
606
610
  let providerLocked = false;
607
611
  let gameGate = true;
612
+ function emitLockError(path, cause) {
613
+ try {
614
+ options.onLockError?.({ path, cause });
615
+ } catch {
616
+ }
617
+ }
608
618
  function releaseProviderLock() {
609
619
  if (providerLocked && options.lockProvider?.exitLock) {
610
620
  try {
611
621
  options.lockProvider.exitLock();
612
622
  } catch (err) {
613
- if (options.onLockError) options.onLockError({ path: "provider", cause: err });
623
+ emitLockError("provider", err);
614
624
  }
615
625
  }
616
626
  providerLocked = false;
@@ -679,11 +689,11 @@ function attachBrowserInputBackend(canvas, options = {}) {
679
689
  clear();
680
690
  return;
681
691
  }
692
+ if (!heldKeys.has(ev.key)) pressedKeys.add(ev.key);
682
693
  heldKeys.add(ev.key);
683
- upEdges.delete(ev.key);
684
694
  if (ev.code) {
695
+ if (!heldCodes.has(ev.code)) pressedCodes.add(ev.code);
685
696
  heldCodes.add(ev.code);
686
- upCodeEdges.delete(ev.code);
687
697
  }
688
698
  if (ev.key === "Escape" && providerLocked) {
689
699
  releaseProviderLock();
@@ -707,6 +717,7 @@ function attachBrowserInputBackend(canvas, options = {}) {
707
717
  }
708
718
  if (ev.pointerType === "mouse") {
709
719
  if (ev.button === 0 || ev.button === 1 || ev.button === 2) {
720
+ if (!buttons[ev.button]) pressedButtons[ev.button] = true;
710
721
  buttons[ev.button] = true;
711
722
  }
712
723
  }
@@ -762,6 +773,7 @@ function attachBrowserInputBackend(canvas, options = {}) {
762
773
  if (isUiEvent(ev)) return;
763
774
  if (ev.pointerType === "mouse") {
764
775
  if (ev.button === 0 || ev.button === 1 || ev.button === 2) {
776
+ if (buttons[ev.button]) releasedButtons[ev.button] = true;
765
777
  buttons[ev.button] = false;
766
778
  }
767
779
  }
@@ -843,6 +855,14 @@ function attachBrowserInputBackend(canvas, options = {}) {
843
855
  function onBlur() {
844
856
  upEdges.clear();
845
857
  upCodeEdges.clear();
858
+ pressedKeys.clear();
859
+ pressedCodes.clear();
860
+ pressedButtons[0] = false;
861
+ pressedButtons[1] = false;
862
+ pressedButtons[2] = false;
863
+ releasedButtons[0] = false;
864
+ releasedButtons[1] = false;
865
+ releasedButtons[2] = false;
846
866
  heldKeys.clear();
847
867
  heldCodes.clear();
848
868
  buttons[0] = false;
@@ -895,6 +915,10 @@ function attachBrowserInputBackend(canvas, options = {}) {
895
915
  if (wasLocked && !w3cLocked && !providerLocked) onBlur();
896
916
  }
897
917
  safeAdd(doc, "pointerlockchange", onPointerLockChange);
918
+ function onPointerLockError(event) {
919
+ emitLockError("w3c", event);
920
+ }
921
+ safeAdd(doc, "pointerlockerror", onPointerLockError);
898
922
  let _prevTouchAction;
899
923
  if (canvas.style) {
900
924
  _prevTouchAction = canvas.style.touchAction;
@@ -903,7 +927,6 @@ function attachBrowserInputBackend(canvas, options = {}) {
903
927
  function onCanvasClick() {
904
928
  if (!gameGate) return;
905
929
  if (options.pointerLockAllowed && !options.pointerLockAllowed()) return;
906
- if (typeof doc.hasFocus === "function" && !doc.hasFocus()) return;
907
930
  if (options.lockProvider) {
908
931
  providerLocked = true;
909
932
  try {
@@ -911,28 +934,26 @@ function attachBrowserInputBackend(canvas, options = {}) {
911
934
  if (result && typeof result.catch === "function") {
912
935
  result.catch((cause) => {
913
936
  providerLocked = false;
914
- if (options.onLockError) {
915
- options.onLockError({ path: "provider", cause });
916
- }
937
+ emitLockError("provider", cause);
917
938
  });
918
939
  }
919
940
  } catch (cause) {
920
941
  providerLocked = false;
921
- if (options.onLockError) {
922
- options.onLockError({ path: "provider", cause });
923
- }
942
+ emitLockError("provider", cause);
924
943
  }
925
944
  return;
926
945
  }
927
946
  const fn = canvas.requestPointerLock;
928
947
  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
- });
948
+ try {
949
+ const r = fn.call(canvas);
950
+ if (r && typeof r.catch === "function") {
951
+ r.catch((cause) => {
952
+ emitLockError("w3c", cause);
953
+ });
954
+ }
955
+ } catch (cause) {
956
+ emitLockError("w3c", cause);
936
957
  }
937
958
  }
938
959
  safeAdd(canvas, "click", onCanvasClick);
@@ -1007,7 +1028,11 @@ function attachBrowserInputBackend(canvas, options = {}) {
1007
1028
  upKeys: new Set(upEdges),
1008
1029
  downCodes: new Set(heldCodes),
1009
1030
  upCodes: new Set(upCodeEdges),
1031
+ pressedKeys: new Set(pressedKeys),
1032
+ pressedCodes: new Set(pressedCodes),
1010
1033
  buttons: [buttons[0], buttons[1], buttons[2]],
1034
+ pressedButtons: [pressedButtons[0], pressedButtons[1], pressedButtons[2]],
1035
+ releasedButtons: [releasedButtons[0], releasedButtons[1], releasedButtons[2]],
1011
1036
  movementX: mvx,
1012
1037
  movementY: mvy,
1013
1038
  ...mouseX !== void 0 && mouseY !== void 0 ? { mouseX, mouseY } : {},
@@ -1025,6 +1050,14 @@ function attachBrowserInputBackend(canvas, options = {}) {
1025
1050
  };
1026
1051
  upEdges.clear();
1027
1052
  upCodeEdges.clear();
1053
+ pressedKeys.clear();
1054
+ pressedCodes.clear();
1055
+ pressedButtons[0] = false;
1056
+ pressedButtons[1] = false;
1057
+ pressedButtons[2] = false;
1058
+ releasedButtons[0] = false;
1059
+ releasedButtons[1] = false;
1060
+ releasedButtons[2] = false;
1028
1061
  mvx = 0;
1029
1062
  mvy = 0;
1030
1063
  wheelAccum = 0;
@@ -1044,8 +1077,10 @@ function attachBrowserInputBackend(canvas, options = {}) {
1044
1077
  function clear() {
1045
1078
  heldKeys.clear();
1046
1079
  upEdges.clear();
1080
+ pressedKeys.clear();
1047
1081
  heldCodes.clear();
1048
1082
  upCodeEdges.clear();
1083
+ pressedCodes.clear();
1049
1084
  pointerMap.clear();
1050
1085
  phaseQueue.length = 0;
1051
1086
  prevGamepadFrame.clear();
@@ -1054,6 +1089,12 @@ function attachBrowserInputBackend(canvas, options = {}) {
1054
1089
  buttons[0] = false;
1055
1090
  buttons[1] = false;
1056
1091
  buttons[2] = false;
1092
+ pressedButtons[0] = false;
1093
+ pressedButtons[1] = false;
1094
+ pressedButtons[2] = false;
1095
+ releasedButtons[0] = false;
1096
+ releasedButtons[1] = false;
1097
+ releasedButtons[2] = false;
1057
1098
  mvx = 0;
1058
1099
  mvy = 0;
1059
1100
  wheelAccum = 0;
@@ -1076,6 +1117,7 @@ function attachBrowserInputBackend(canvas, options = {}) {
1076
1117
  safeRemove(canvas, "wheel", onWheel);
1077
1118
  safeRemove(doc, "visibilitychange", onVisibilityChange);
1078
1119
  safeRemove(doc, "pointerlockchange", onPointerLockChange);
1120
+ safeRemove(doc, "pointerlockerror", onPointerLockError);
1079
1121
  safeRemove(canvas, "click", onCanvasClick);
1080
1122
  clear();
1081
1123
  if (canvas.style && _prevTouchAction !== void 0) {
@@ -1156,35 +1198,33 @@ function buildGamepadReader(slot) {
1156
1198
  function snapshotFromSample(sample, actionStates, inputMap, previousSnapshot) {
1157
1199
  const heldKeys = new Set(sample.downKeys);
1158
1200
  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
- }
1201
+ const justPressedKeys = sample.pressedKeys === void 0 ? new Set(
1202
+ [...heldKeys].filter(
1203
+ (key) => previousSnapshot === void 0 || !previousSnapshot.keyboard.down(key)
1204
+ )
1205
+ ) : new Set(sample.pressedKeys);
1165
1206
  const heldCodes = new Set(sample.downCodes ?? []);
1166
1207
  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
- }
1208
+ const justPressedCodes = sample.pressedCodes === void 0 ? new Set(
1209
+ [...heldCodes].filter(
1210
+ (code) => previousSnapshot === void 0 || !previousSnapshot.keyboard.downCode(code)
1211
+ )
1212
+ ) : new Set(sample.pressedCodes);
1173
1213
  const buttons = [
1174
1214
  sample.buttons[0],
1175
1215
  sample.buttons[1],
1176
1216
  sample.buttons[2]
1177
1217
  ];
1178
- const justPressedButtons = [
1218
+ const justPressedButtons = sample.pressedButtons === void 0 ? [
1179
1219
  buttons[0] && (previousSnapshot === void 0 || !previousSnapshot.mouse.button(0)),
1180
1220
  buttons[1] && (previousSnapshot === void 0 || !previousSnapshot.mouse.button(1)),
1181
1221
  buttons[2] && (previousSnapshot === void 0 || !previousSnapshot.mouse.button(2))
1182
- ];
1183
- const justReleasedButtons = [
1222
+ ] : [sample.pressedButtons[0], sample.pressedButtons[1], sample.pressedButtons[2]];
1223
+ const justReleasedButtons = sample.releasedButtons === void 0 ? [
1184
1224
  sample.focusReset !== true && previousSnapshot?.mouse.button(0) === true && !buttons[0],
1185
1225
  sample.focusReset !== true && previousSnapshot?.mouse.button(1) === true && !buttons[1],
1186
1226
  sample.focusReset !== true && previousSnapshot?.mouse.button(2) === true && !buttons[2]
1187
- ];
1227
+ ] : [sample.releasedButtons[0], sample.releasedButtons[1], sample.releasedButtons[2]];
1188
1228
  const movementDelta = Object.freeze({ x: sample.movementX, y: sample.movementY });
1189
1229
  const mousePosition = sample.mouseX === void 0 || sample.mouseY === void 0 ? void 0 : Object.freeze({ x: sample.mouseX, y: sample.mouseY });
1190
1230
  const wheelDelta = sample.wheelDelta;
@@ -1371,11 +1411,14 @@ function makeCompositeBackend(inner, options) {
1371
1411
  const heldKeys = /* @__PURE__ */ new Set();
1372
1412
  const buttons = [false, false, false];
1373
1413
  const upEdges = /* @__PURE__ */ new Set();
1414
+ const pressedKeys = /* @__PURE__ */ new Set();
1415
+ const pressedButtons = [false, false, false];
1416
+ const releasedButtons = [false, false, false];
1374
1417
  let mvx = 0;
1375
1418
  let mvy = 0;
1376
1419
  let wheel = 0;
1377
1420
  function injectionActive() {
1378
- return heldKeys.size > 0 || upEdges.size > 0 || buttons.some((b) => b);
1421
+ return heldKeys.size > 0 || upEdges.size > 0 || pressedKeys.size > 0 || buttons.some((b) => b) || pressedButtons.some((b) => b) || releasedButtons.some((b) => b);
1379
1422
  }
1380
1423
  function sample() {
1381
1424
  const base = inner.sample();
@@ -1389,18 +1432,38 @@ function makeCompositeBackend(inner, options) {
1389
1432
  if (yieldToHuman && base.downKeys.has(k)) continue;
1390
1433
  mergedUp.add(k);
1391
1434
  }
1435
+ const mergedPressed = base.pressedKeys === void 0 && pressedKeys.size === 0 ? void 0 : /* @__PURE__ */ new Set([...base.pressedKeys ?? [], ...pressedKeys]);
1436
+ if (yieldToHuman && base.downKeys.size > 0 && mergedPressed !== void 0) {
1437
+ for (const key of pressedKeys) {
1438
+ if (base.downKeys.has(key)) mergedPressed.delete(key);
1439
+ }
1440
+ }
1392
1441
  const mergedButtons = [
1393
1442
  base.buttons[0] || buttons[0],
1394
1443
  base.buttons[1] || buttons[1],
1395
1444
  base.buttons[2] || buttons[2]
1396
1445
  ];
1446
+ const mergedPressedButtons = base.pressedButtons === void 0 && !pressedButtons.some(Boolean) ? void 0 : [
1447
+ (base.pressedButtons?.[0] ?? false) || pressedButtons[0],
1448
+ (base.pressedButtons?.[1] ?? false) || pressedButtons[1],
1449
+ (base.pressedButtons?.[2] ?? false) || pressedButtons[2]
1450
+ ];
1451
+ const mergedReleasedButtons = base.releasedButtons === void 0 && !releasedButtons.some(Boolean) ? void 0 : [
1452
+ (base.releasedButtons?.[0] ?? false) || releasedButtons[0],
1453
+ (base.releasedButtons?.[1] ?? false) || releasedButtons[1],
1454
+ (base.releasedButtons?.[2] ?? false) || releasedButtons[2]
1455
+ ];
1397
1456
  const focused = base.focused || injectionActive();
1398
1457
  const out = {
1399
1458
  ...base,
1400
1459
  // carry inner optional fields (pointers/gamepads/gestures/...) untouched
1401
1460
  downKeys,
1402
1461
  upKeys: mergedUp,
1462
+ ...mergedPressed === void 0 ? {} : { pressedKeys: mergedPressed },
1463
+ ...base.pressedCodes === void 0 ? {} : { pressedCodes: base.pressedCodes },
1403
1464
  buttons: mergedButtons,
1465
+ ...mergedPressedButtons === void 0 ? {} : { pressedButtons: mergedPressedButtons },
1466
+ ...mergedReleasedButtons === void 0 ? {} : { releasedButtons: mergedReleasedButtons },
1404
1467
  movementX: base.movementX + mvx,
1405
1468
  movementY: base.movementY + mvy,
1406
1469
  wheelDelta: base.wheelDelta + wheel,
@@ -1409,6 +1472,13 @@ function makeCompositeBackend(inner, options) {
1409
1472
  // AI never fabricates a lock
1410
1473
  };
1411
1474
  upEdges.clear();
1475
+ pressedKeys.clear();
1476
+ pressedButtons[0] = false;
1477
+ pressedButtons[1] = false;
1478
+ pressedButtons[2] = false;
1479
+ releasedButtons[0] = false;
1480
+ releasedButtons[1] = false;
1481
+ releasedButtons[2] = false;
1412
1482
  mvx = 0;
1413
1483
  mvy = 0;
1414
1484
  wheel = 0;
@@ -1421,13 +1491,15 @@ function makeCompositeBackend(inner, options) {
1421
1491
  // Teardown belongs to the human backend -- forward it.
1422
1492
  detach: () => inner.detach(),
1423
1493
  press(key) {
1494
+ if (!heldKeys.has(key)) pressedKeys.add(key);
1424
1495
  heldKeys.add(key);
1425
- upEdges.delete(key);
1426
1496
  },
1427
1497
  release(key) {
1428
1498
  if (heldKeys.delete(key)) upEdges.add(key);
1429
1499
  },
1430
1500
  setButton(slot, down) {
1501
+ if (down && !buttons[slot]) pressedButtons[slot] = true;
1502
+ if (!down && buttons[slot]) releasedButtons[slot] = true;
1431
1503
  buttons[slot] = down;
1432
1504
  },
1433
1505
  addMovement(dx, dy) {
@@ -1438,8 +1510,19 @@ function makeCompositeBackend(inner, options) {
1438
1510
  wheel += notches;
1439
1511
  },
1440
1512
  clearInjected() {
1513
+ upEdges.clear();
1514
+ pressedKeys.clear();
1441
1515
  for (const k of heldKeys) upEdges.add(k);
1442
1516
  heldKeys.clear();
1517
+ pressedButtons[0] = false;
1518
+ pressedButtons[1] = false;
1519
+ pressedButtons[2] = false;
1520
+ releasedButtons[0] = false;
1521
+ releasedButtons[1] = false;
1522
+ releasedButtons[2] = false;
1523
+ if (buttons[0]) releasedButtons[0] = true;
1524
+ if (buttons[1]) releasedButtons[1] = true;
1525
+ if (buttons[2]) releasedButtons[2] = true;
1443
1526
  buttons[0] = buttons[1] = buttons[2] = false;
1444
1527
  mvx = 0;
1445
1528
  mvy = 0;