@forgeax/engine-input 0.1.19 → 0.1.21

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.
@@ -307,12 +307,14 @@ export interface InputSnapshot {
307
307
  export interface InputBackend {
308
308
  /**
309
309
  * Produce one frame's worth of input data and reset the per-frame
310
- * accumulators (movement delta + up-edge set). Held-key state and
311
- * held-button state survive across calls.
310
+ * accumulators (movement delta + edge sets). Held-key state and
311
+ * held-button state survive across calls. Browser producers latch edge
312
+ * transitions when events arrive, so a down+up sequence between two scans
313
+ * remains observable instead of being lost to the held-state projection.
312
314
  *
313
315
  * Contract notes:
314
316
  * - `downKeys` is the held-key set as of the call (held across frames)
315
- * - `upKeys` is the up-edge set since the previous sample (cleared
317
+ * - `upKeys` is the release-edge set since the previous sample (cleared
316
318
  * after the call so the edge appears in exactly one frame)
317
319
  * - `buttons` is the held-button slot tuple `[b0, b1, b2]`
318
320
  * - `movementX` / `movementY` are the accumulated PointerLock delta
@@ -349,7 +351,15 @@ export interface InputBackendSample {
349
351
  readonly downCodes?: ReadonlySet<string>;
350
352
  /** One-frame physical code release edges, when the producer has them. */
351
353
  readonly upCodes?: ReadonlySet<string>;
354
+ /** Logical key press edges latched since the previous frame-start scan. */
355
+ readonly pressedKeys?: ReadonlySet<string>;
356
+ /** Physical code press edges latched since the previous frame-start scan. */
357
+ readonly pressedCodes?: ReadonlySet<string>;
352
358
  readonly buttons: readonly [boolean, boolean, boolean];
359
+ /** Mouse button press edges latched since the previous frame-start scan. */
360
+ readonly pressedButtons?: readonly [boolean, boolean, boolean];
361
+ /** Mouse button release edges latched since the previous frame-start scan. */
362
+ readonly releasedButtons?: readonly [boolean, boolean, boolean];
353
363
  readonly movementX: number;
354
364
  readonly movementY: number;
355
365
  /** Latest canvas-pixel mouse position; absent for non-pointer backends. */
@@ -1 +1 @@
1
- {"version":3,"file":"input-snapshot.d.ts","sourceRoot":"","sources":["../src/input-snapshot.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"input-snapshot.d.ts","sourceRoot":"","sources":["../src/input-snapshot.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgeax/engine-input",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -31,8 +31,8 @@
31
31
  "LICENSE"
32
32
  ],
33
33
  "dependencies": {
34
- "@forgeax/engine-ecs": "0.1.19",
35
- "@forgeax/engine-plugin": "0.1.19"
34
+ "@forgeax/engine-ecs": "0.1.21",
35
+ "@forgeax/engine-plugin": "0.1.21"
36
36
  },
37
37
  "forgeax": {
38
38
  "metrics": {
@@ -0,0 +1,154 @@
1
+ import { Update, World } from '@forgeax/engine-ecs';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { attachBrowserInputBackend } from '../browser-backend';
4
+ import { INPUT_BACKEND_KEY, InputFrameStartScan } from '../frame-start-scan-system';
5
+ import { INPUT_SNAPSHOT_RESOURCE_KEY, snapshotFromSample } from '../input-snapshot';
6
+
7
+ type Listener = (event: Event) => void;
8
+
9
+ function buildEnvironment(): {
10
+ readonly backend: ReturnType<typeof attachBrowserInputBackend>['backend'];
11
+ fire(target: 'canvas' | 'document' | 'window', kind: string, event: Event): void;
12
+ } {
13
+ const listeners = new Map<string, Map<string, Set<Listener>>>();
14
+ const target = (name: string) => ({
15
+ addEventListener(kind: string, listener: Listener): void {
16
+ let byKind = listeners.get(name);
17
+ if (byKind === undefined) {
18
+ byKind = new Map();
19
+ listeners.set(name, byKind);
20
+ }
21
+ let handlers = byKind.get(kind);
22
+ if (handlers === undefined) {
23
+ handlers = new Set();
24
+ byKind.set(kind, handlers);
25
+ }
26
+ handlers.add(listener);
27
+ },
28
+ removeEventListener(kind: string, listener: Listener): void {
29
+ listeners.get(name)?.get(kind)?.delete(listener);
30
+ },
31
+ });
32
+ const canvas = {
33
+ ...target('canvas'),
34
+ width: 800,
35
+ height: 600,
36
+ style: { touchAction: '' },
37
+ getBoundingClientRect: () => ({ left: 0, top: 0, width: 800, height: 600 }) as DOMRect,
38
+ requestPointerLock: () => {},
39
+ setPointerCapture: () => {},
40
+ } as unknown as HTMLCanvasElement;
41
+ const document = {
42
+ ...target('document'),
43
+ hasFocus: () => true,
44
+ visibilityState: 'visible',
45
+ pointerLockElement: null,
46
+ exitPointerLock: () => {},
47
+ } as unknown as Document;
48
+ const window = target('window') as unknown as Window;
49
+ const handle = attachBrowserInputBackend(canvas, { document, window });
50
+ return {
51
+ backend: handle.backend,
52
+ fire(targetName, kind, event) {
53
+ for (const listener of listeners.get(targetName)?.get(kind) ?? []) listener(event);
54
+ },
55
+ };
56
+ }
57
+
58
+ function keyEvent(key: string, code: string, repeat = false): Event {
59
+ return { key, code, repeat } as unknown as Event;
60
+ }
61
+
62
+ function pointerEvent(type: 'pointerdown' | 'pointerup', button: 0 | 1 | 2): Event {
63
+ return {
64
+ pointerId: 1,
65
+ pointerType: 'mouse',
66
+ button,
67
+ clientX: 100,
68
+ clientY: 100,
69
+ pressure: type === 'pointerdown' ? 0.5 : 0,
70
+ movementX: 0,
71
+ movementY: 0,
72
+ } as unknown as Event;
73
+ }
74
+
75
+ describe('browser input edge latch', () => {
76
+ it('preserves key and mouse down/up transitions that arrive between scans', () => {
77
+ const env = buildEnvironment();
78
+ env.fire('window', 'keydown', keyEvent('a', 'KeyA'));
79
+ env.fire('window', 'keyup', keyEvent('a', 'KeyA'));
80
+ env.fire('canvas', 'pointerdown', pointerEvent('pointerdown', 0));
81
+ env.fire('canvas', 'pointerup', pointerEvent('pointerup', 0));
82
+
83
+ const sample = env.backend.sample();
84
+ expect(sample.downKeys.has('a')).toBe(false);
85
+ expect(sample.upKeys.has('a')).toBe(true);
86
+ expect(sample.pressedKeys?.has('a')).toBe(true);
87
+ expect(sample.downCodes?.has('KeyA')).toBe(false);
88
+ expect(sample.upCodes?.has('KeyA')).toBe(true);
89
+ expect(sample.pressedCodes?.has('KeyA')).toBe(true);
90
+ expect(sample.buttons).toEqual([false, false, false]);
91
+ expect(sample.pressedButtons).toEqual([true, false, false]);
92
+ expect(sample.releasedButtons).toEqual([true, false, false]);
93
+
94
+ const next = env.backend.sample();
95
+ expect(next.upKeys.size).toBe(0);
96
+ expect(next.pressedKeys?.size).toBe(0);
97
+ expect(next.upCodes?.size).toBe(0);
98
+ expect(next.pressedCodes?.size).toBe(0);
99
+ expect(next.pressedButtons).toEqual([false, false, false]);
100
+ expect(next.releasedButtons).toEqual([false, false, false]);
101
+ });
102
+
103
+ it('does not synthesize a second press for repeated keydown events', () => {
104
+ const env = buildEnvironment();
105
+ env.fire('window', 'keydown', keyEvent('w', 'KeyW'));
106
+ expect(env.backend.sample().pressedKeys?.has('w')).toBe(true);
107
+
108
+ env.fire('window', 'keydown', keyEvent('w', 'KeyW', true));
109
+ const repeated = env.backend.sample();
110
+ expect(repeated.downKeys.has('w')).toBe(true);
111
+ expect(repeated.pressedKeys?.has('w')).toBe(false);
112
+ });
113
+
114
+ it('lets snapshot and frame-start scan consume the latched edges exactly once', () => {
115
+ const sample = {
116
+ downKeys: new Set<string>(),
117
+ upKeys: new Set(['a']),
118
+ downCodes: new Set<string>(),
119
+ upCodes: new Set(['KeyA']),
120
+ pressedKeys: new Set(['a']),
121
+ pressedCodes: new Set(['KeyA']),
122
+ buttons: [false, false, false] as const,
123
+ pressedButtons: [true, false, false] as const,
124
+ releasedButtons: [true, false, false] as const,
125
+ movementX: 0,
126
+ movementY: 0,
127
+ wheelDelta: 0,
128
+ focused: true,
129
+ pointerLocked: false,
130
+ };
131
+ const direct = snapshotFromSample(sample);
132
+ expect(direct.keyboard.justPressed('a')).toBe(true);
133
+ expect(direct.keyboard.justPressedCode('KeyA')).toBe(true);
134
+ expect(direct.mouse.justPressed(0)).toBe(true);
135
+ expect(direct.mouse.justReleased(0)).toBe(true);
136
+ expect(direct.mouse.button(0)).toBe(false);
137
+
138
+ let calls = 0;
139
+ const world = new World();
140
+ world.insertResource(INPUT_BACKEND_KEY, {
141
+ sample: () => {
142
+ calls += 1;
143
+ return sample;
144
+ },
145
+ detach: () => {},
146
+ });
147
+ world.addSystem(Update, InputFrameStartScan).unwrap();
148
+ world.update(0).unwrap();
149
+ const scanned = world.getResource<typeof direct>(INPUT_SNAPSHOT_RESOURCE_KEY);
150
+ expect(calls).toBe(1);
151
+ expect(scanned.keyboard.justPressed('a')).toBe(true);
152
+ expect(scanned.mouse.justReleased(0)).toBe(true);
153
+ });
154
+ });
@@ -179,7 +179,11 @@ describe('browser-backend focus loss', () => {
179
179
  const cancelled = env.backend.sample();
180
180
  expect(cancelled.downKeys.size).toBe(0);
181
181
  expect(cancelled.downCodes?.size).toBe(0);
182
+ expect(cancelled.pressedKeys?.size).toBe(0);
183
+ expect(cancelled.pressedCodes?.size).toBe(0);
182
184
  expect(cancelled.buttons).toEqual([false, false, false]);
185
+ expect(cancelled.pressedButtons).toEqual([false, false, false]);
186
+ expect(cancelled.releasedButtons).toEqual([false, false, false]);
183
187
  expect(cancelled.focusReset).toBe(true);
184
188
  expect(cancelled.pointerLocked).toBe(false);
185
189
  expect(cancelled.pointers).toBeUndefined();
@@ -1,7 +1,7 @@
1
- // browser-backend-lock-error.test.ts -- M1 w4: onLockError unit tests.
1
+ // browser-backend-lock-error.test.ts -- M3 pointer-lock failure unit tests.
2
2
  //
3
- // Covers: W3C rejection → onLockError (no longer silently swallowed),
4
- // provider reject → onLockError + providerLocked rollback.
3
+ // Covers: W3C rejection / pointerlockerror → onLockError (no longer silently
4
+ // swallowed), provider reject → onLockError + providerLocked rollback.
5
5
  //
6
6
  // charter awareness:
7
7
  // P3 explicit failure -- lock failures must produce structured signals,
@@ -84,14 +84,28 @@ describe('browser-backend-lock-error.test.ts (w4)', () => {
84
84
  }
85
85
 
86
86
  // Wait for microtask to resolve promise rejection.
87
- return vi.waitFor(
88
- () => {
89
- expect(lockErrorCalls.length).toBe(1);
90
- expect(lockErrorCalls[0]?.path).toBe('w3c');
91
- expect(lockErrorCalls[0]?.cause).toBe(rejectionError);
92
- },
93
- { timeout: 200 },
94
- );
87
+ return vi
88
+ .waitFor(
89
+ () => {
90
+ expect(lockErrorCalls.length).toBe(1);
91
+ expect(lockErrorCalls[0]?.path).toBe('w3c');
92
+ expect(lockErrorCalls[0]?.cause).toBe(rejectionError);
93
+ },
94
+ { timeout: 200 },
95
+ )
96
+ .then(async () => {
97
+ const browserEvent = new Event('pointerlockerror');
98
+ const errorHandlers = listeners.get('document')?.get('pointerlockerror');
99
+ for (const handler of errorHandlers ?? []) handler(browserEvent);
100
+ await vi.waitFor(
101
+ () => {
102
+ expect(lockErrorCalls.length).toBe(2);
103
+ expect(lockErrorCalls[1]?.path).toBe('w3c');
104
+ expect(lockErrorCalls[1]?.cause).toBe(browserEvent);
105
+ },
106
+ { timeout: 200 },
107
+ );
108
+ });
95
109
  });
96
110
 
97
111
  it('W3C rejection without onLockError provided does NOT throw (backend is resilient)', () => {
@@ -17,6 +17,7 @@ import { attachBrowserInputBackend } from '../browser-backend';
17
17
  function buildEnvForClickTest(opts?: {
18
18
  hostPredicate?: (() => boolean) | undefined;
19
19
  lockProvider?: { requestLock: () => void; exitLock: () => void } | undefined;
20
+ focus?: boolean | undefined;
20
21
  }): {
21
22
  backend: ReturnType<typeof attachBrowserInputBackend>['backend'];
22
23
  fireClick(): void;
@@ -68,7 +69,7 @@ function buildEnvForClickTest(opts?: {
68
69
  const doc = {
69
70
  ...makeTarget('document'),
70
71
  hasFocus() {
71
- return true;
72
+ return opts?.focus ?? true;
72
73
  },
73
74
  visibilityState: 'visible',
74
75
  get pointerLockElement() {
@@ -144,6 +145,12 @@ describe('browser-backend-lock-gate.test.ts (w2)', () => {
144
145
  expect(env.backend.sample().pointerLocked).toBe(false); // pointerlockchange not fired yet
145
146
  });
146
147
 
148
+ it('does not add a silent focus gate to a trusted click', () => {
149
+ const env = buildEnvForClickTest({ focus: false, hostPredicate: () => true });
150
+ env.fireClick();
151
+ expect(env.requestPointerLockCalls.count).toBe(1);
152
+ });
153
+
147
154
  it('gameGate=true (default), hostPredicate=false -> no lock', () => {
148
155
  const env = buildEnvForClickTest({
149
156
  hostPredicate: () => false,
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { userEvent } from 'vitest/browser';
3
+ import { attachBrowserInputBackend } from '../browser-backend';
4
+
5
+ describe('browser edge latch (real Chromium)', () => {
6
+ it('keeps trusted low-frequency down/up edges until the next sample', async () => {
7
+ const canvas = document.createElement('canvas');
8
+ canvas.width = 320;
9
+ canvas.height = 200;
10
+ canvas.tabIndex = 0;
11
+ document.body.append(canvas);
12
+ const handle = attachBrowserInputBackend(canvas, {
13
+ pointerLockAllowed: () => false,
14
+ });
15
+ try {
16
+ // This is a real Playwright user click. The gate is disabled so the
17
+ // fixture does not request a browser pointer lock as a side effect.
18
+ await userEvent.click(canvas);
19
+ handle.backend.sample();
20
+
21
+ // A single user-level key interaction produces both DOM events before
22
+ // the deliberately delayed frame-start sample.
23
+ await userEvent.keyboard('a');
24
+ const sample = handle.backend.sample();
25
+ expect(sample.downKeys.has('a')).toBe(false);
26
+ expect(sample.upKeys.has('a')).toBe(true);
27
+ expect(sample.pressedKeys?.has('a')).toBe(true);
28
+ expect(sample.downCodes?.has('KeyA')).toBe(false);
29
+ expect(sample.upCodes?.has('KeyA')).toBe(true);
30
+ expect(sample.pressedCodes?.has('KeyA')).toBe(true);
31
+
32
+ const next = handle.backend.sample();
33
+ expect(next.pressedKeys?.size).toBe(0);
34
+ expect(next.upKeys.size).toBe(0);
35
+ expect(next.pressedCodes?.size).toBe(0);
36
+ expect(next.upCodes?.size).toBe(0);
37
+ } finally {
38
+ handle();
39
+ canvas.remove();
40
+ }
41
+ });
42
+ });
@@ -257,6 +257,24 @@ describe('CompositeInputBackend merge semantics', () => {
257
257
  expect(s.upKeys.has('a')).toBe(true);
258
258
  });
259
259
 
260
+ it('clearInjected drops pending press/release latches without ghost edges', () => {
261
+ const inner = fakeInner();
262
+ const c = makeCompositeBackend(inner);
263
+ c.press('w');
264
+ c.setButton(0, true);
265
+ c.release('w');
266
+ c.setButton(0, false);
267
+ c.clearInjected();
268
+
269
+ const sample = c.sample();
270
+ expect(sample.downKeys.size).toBe(0);
271
+ expect(sample.upKeys.size).toBe(0);
272
+ expect(sample.pressedKeys).toBeUndefined();
273
+ expect(sample.buttons).toEqual([false, false, false]);
274
+ expect(sample.pressedButtons).toBeUndefined();
275
+ expect(sample.releasedButtons).toBeUndefined();
276
+ });
277
+
260
278
  it('forwards setPointerLockAllowed and detach to inner', () => {
261
279
  let allowed: boolean | undefined;
262
280
  let detached = false;
@@ -47,6 +47,25 @@ describe('keyboard snapshot edges', () => {
47
47
  expect(snap.keyboard.downCode('KeyA')).toBe(true);
48
48
  expect(snap.keyboard.justPressedCode('KeyA')).toBe(true);
49
49
  });
50
+
51
+ it('uses producer-latched press edges even when held state is already released', () => {
52
+ const snap = snapshotFromSample({
53
+ ...sample([], ['a'], [], ['KeyA']),
54
+ pressedKeys: new Set(['a']),
55
+ pressedCodes: new Set(['KeyA']),
56
+ });
57
+
58
+ expect(snap.keyboard.down('a')).toBe(false);
59
+ expect(snap.keyboard.up('a')).toBe(true);
60
+ expect(snap.keyboard.justPressed('a')).toBe(true);
61
+ expect(snap.keyboard.downCode('KeyA')).toBe(false);
62
+ expect(snap.keyboard.upCode('KeyA')).toBe(true);
63
+ expect(snap.keyboard.justPressedCode('KeyA')).toBe(true);
64
+
65
+ const next = snapshotFromSample(sample([]), undefined, undefined, snap);
66
+ expect(next.keyboard.justPressed('a')).toBe(false);
67
+ expect(next.keyboard.up('a')).toBe(false);
68
+ });
50
69
  });
51
70
 
52
71
  describe('empty backend sample ownership', () => {
@@ -93,4 +112,20 @@ describe('mouse snapshot edges', () => {
93
112
  expect(released.mouse.button(0)).toBe(false);
94
113
  expect(released.mouse.justReleased(0)).toBe(true);
95
114
  });
115
+
116
+ it('preserves a producer-latched button tap between frame-start scans', () => {
117
+ const tap = snapshotFromSample({
118
+ ...sample([]),
119
+ pressedButtons: [true, false, false],
120
+ releasedButtons: [true, false, false],
121
+ });
122
+
123
+ expect(tap.mouse.button(0)).toBe(false);
124
+ expect(tap.mouse.justPressed(0)).toBe(true);
125
+ expect(tap.mouse.justReleased(0)).toBe(true);
126
+
127
+ const next = snapshotFromSample(sample([]), undefined, undefined, tap);
128
+ expect(next.mouse.justPressed(0)).toBe(false);
129
+ expect(next.mouse.justReleased(0)).toBe(false);
130
+ });
96
131
  });
@@ -0,0 +1,59 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { userEvent } from 'vitest/browser';
3
+ import { attachBrowserInputBackend } from '../browser-backend';
4
+
5
+ describe('pointer lock recovery (real Chromium)', () => {
6
+ it('attempts a trusted click even when hasFocus is false and contains rejection/error events', async () => {
7
+ const canvas = document.createElement('canvas');
8
+ canvas.dataset.testid = 'pointer-lock-target';
9
+ document.body.append(canvas);
10
+ const originalFocus = Object.getOwnPropertyDescriptor(document, 'hasFocus');
11
+ const originalRequest = Object.getOwnPropertyDescriptor(canvas, 'requestPointerLock');
12
+ const errors: Array<{ path: 'w3c' | 'provider'; cause: unknown }> = [];
13
+ const unhandled: unknown[] = [];
14
+ let requestCalls = 0;
15
+ const rejection = new DOMException('Pointer lock denied', 'NotAllowedError');
16
+ Object.defineProperty(document, 'hasFocus', {
17
+ configurable: true,
18
+ value: () => false,
19
+ });
20
+ Object.defineProperty(canvas, 'requestPointerLock', {
21
+ configurable: true,
22
+ value: () => {
23
+ requestCalls += 1;
24
+ return Promise.reject(rejection);
25
+ },
26
+ });
27
+ const onUnhandled = (event: PromiseRejectionEvent): void => {
28
+ event.preventDefault();
29
+ unhandled.push(event.reason);
30
+ };
31
+ window.addEventListener('unhandledrejection', onUnhandled);
32
+ const handle = attachBrowserInputBackend(canvas, {
33
+ onLockError: (detail) => errors.push(detail),
34
+ });
35
+ try {
36
+ // userEvent delegates to Playwright's locator click, preserving the
37
+ // trusted user-activation boundary required by Pointer Lock.
38
+ await userEvent.click(canvas);
39
+ await new Promise((resolve) => setTimeout(resolve, 0));
40
+ expect(requestCalls).toBe(1);
41
+ expect(errors[0]).toMatchObject({ path: 'w3c', cause: rejection });
42
+ expect(unhandled).toEqual([]);
43
+
44
+ const browserEvent = new Event('pointerlockerror');
45
+ document.dispatchEvent(browserEvent);
46
+ expect(errors[1]).toMatchObject({ path: 'w3c', cause: browserEvent });
47
+ expect(handle.backend.sample().pointerLocked).toBe(false);
48
+ } finally {
49
+ handle();
50
+ window.removeEventListener('unhandledrejection', onUnhandled);
51
+ if (originalFocus === undefined) delete (document as { hasFocus?: unknown }).hasFocus;
52
+ else Object.defineProperty(document, 'hasFocus', originalFocus);
53
+ if (originalRequest === undefined)
54
+ delete (canvas as { requestPointerLock?: unknown }).requestPointerLock;
55
+ else Object.defineProperty(canvas, 'requestPointerLock', originalRequest);
56
+ canvas.remove();
57
+ }
58
+ });
59
+ });