@forgeax/engine-input 0.1.31 → 0.1.32
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/dist/composite-backend.d.ts +24 -0
- package/dist/composite-backend.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +204 -30
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/composite-backend.test.ts +55 -0
- package/src/composite-backend.ts +276 -33
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgeax/engine-input",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.32",
|
|
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.
|
|
35
|
-
"@forgeax/engine-plugin": "0.1.
|
|
34
|
+
"@forgeax/engine-ecs": "0.1.32",
|
|
35
|
+
"@forgeax/engine-plugin": "0.1.32"
|
|
36
36
|
},
|
|
37
37
|
"forgeax": {
|
|
38
38
|
"metrics": {
|
|
@@ -92,6 +92,24 @@ describe('CompositeInputBackend merge semantics', () => {
|
|
|
92
92
|
expect(c.sample().downKeys.has('w')).toBe(true); // still held next frame
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
+
it('accepts logical keys and physical codes for both keyboard projections', () => {
|
|
96
|
+
const inner = fakeInner();
|
|
97
|
+
const c = makeCompositeBackend(inner);
|
|
98
|
+
c.press('w');
|
|
99
|
+
let sample = c.sample();
|
|
100
|
+
expect(sample.downKeys.has('w')).toBe(true);
|
|
101
|
+
expect(sample.downCodes?.has('KeyW')).toBe(true);
|
|
102
|
+
c.release('w');
|
|
103
|
+
sample = c.sample();
|
|
104
|
+
expect(sample.upKeys.has('w')).toBe(true);
|
|
105
|
+
expect(sample.upCodes?.has('KeyW')).toBe(true);
|
|
106
|
+
|
|
107
|
+
c.press('KeyW');
|
|
108
|
+
sample = c.sample();
|
|
109
|
+
expect(sample.downKeys.has('w')).toBe(true);
|
|
110
|
+
expect(sample.downCodes?.has('KeyW')).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
|
|
95
113
|
it('release produces an up-edge that lives exactly ONE frame', () => {
|
|
96
114
|
const inner = fakeInner();
|
|
97
115
|
const c = makeCompositeBackend(inner);
|
|
@@ -275,6 +293,43 @@ describe('CompositeInputBackend merge semantics', () => {
|
|
|
275
293
|
expect(sample.releasedButtons).toBeUndefined();
|
|
276
294
|
});
|
|
277
295
|
|
|
296
|
+
it('revokeInjectedLease fences late injections until a new lease opens', () => {
|
|
297
|
+
const inner = fakeInner();
|
|
298
|
+
const c = makeCompositeBackend(inner);
|
|
299
|
+
c.press('w');
|
|
300
|
+
expect(c.sample().downCodes?.has('KeyW')).toBe(true);
|
|
301
|
+
c.revokeInjectedLease();
|
|
302
|
+
c.press('a');
|
|
303
|
+
expect(c.sample().downKeys.has('a')).toBe(false);
|
|
304
|
+
c.beginInjectedLease();
|
|
305
|
+
c.press('a');
|
|
306
|
+
expect(c.sample().downCodes?.has('KeyA')).toBe(true);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('keeps an old execution lease read-only after a later lease opens', () => {
|
|
310
|
+
const inner = fakeInner();
|
|
311
|
+
const c = makeCompositeBackend(inner);
|
|
312
|
+
const oldLease = c.createInjectedLease();
|
|
313
|
+
oldLease.press('KeyW');
|
|
314
|
+
expect(c.sample().downCodes?.has('KeyW')).toBe(true);
|
|
315
|
+
|
|
316
|
+
const currentLease = c.createInjectedLease();
|
|
317
|
+
oldLease.revokeInjectedLease();
|
|
318
|
+
oldLease.press('KeyA');
|
|
319
|
+
oldLease.setButton(0, true);
|
|
320
|
+
oldLease.addMovement(10, 20);
|
|
321
|
+
oldLease.addWheel(3);
|
|
322
|
+
expect(c.sample().downKeys.has('a')).toBe(false);
|
|
323
|
+
expect(c.sample().buttons[0]).toBe(false);
|
|
324
|
+
expect(c.sample().movementX).toBe(0);
|
|
325
|
+
expect(c.sample().wheelDelta).toBe(0);
|
|
326
|
+
|
|
327
|
+
currentLease.press('KeyD');
|
|
328
|
+
const sample = c.sample();
|
|
329
|
+
expect(sample.downKeys.has('d')).toBe(true);
|
|
330
|
+
expect(sample.downKeys.has('a')).toBe(false);
|
|
331
|
+
});
|
|
332
|
+
|
|
278
333
|
it('forwards setPointerLockAllowed and detach to inner', () => {
|
|
279
334
|
let allowed: boolean | undefined;
|
|
280
335
|
let detached = false;
|
package/src/composite-backend.ts
CHANGED
|
@@ -64,22 +64,113 @@ export interface CompositeInputBackend extends InputBackend {
|
|
|
64
64
|
* emit one clean release edge so a previously observed hold is closed.
|
|
65
65
|
*/
|
|
66
66
|
clearInjected(): void;
|
|
67
|
+
/** Open a fresh synthetic-input lease after a previous revoke boundary. */
|
|
68
|
+
beginInjectedLease(): void;
|
|
69
|
+
/** Revoke the current lease and fence later injections until reopened. */
|
|
70
|
+
revokeInjectedLease(): void;
|
|
71
|
+
/**
|
|
72
|
+
* Create the one execution-scoped injection view. Creating a lease
|
|
73
|
+
* invalidates every previously-created view; an invalid view remains a
|
|
74
|
+
* no-op even after a later lease is opened.
|
|
75
|
+
*/
|
|
76
|
+
createInjectedLease(): CompositeInputLease;
|
|
67
77
|
/** Toggle the yield-to-human gate at runtime. */
|
|
68
78
|
setYieldToHuman(yield_: boolean): void;
|
|
69
79
|
}
|
|
70
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Synthetic input exposed to one eval/execution. It deliberately has no
|
|
83
|
+
* `beginInjectedLease` method: a script can release its own lease, but it
|
|
84
|
+
* cannot reopen an old reference after the host has moved to another job.
|
|
85
|
+
*/
|
|
86
|
+
export interface CompositeInputLease extends InputBackend {
|
|
87
|
+
press(key: string): void;
|
|
88
|
+
release(key: string): void;
|
|
89
|
+
setButton(slot: 0 | 1 | 2, down: boolean): void;
|
|
90
|
+
addMovement(dx: number, dy: number): void;
|
|
91
|
+
addWheel(notches: number): void;
|
|
92
|
+
clearInjected(): void;
|
|
93
|
+
revokeInjectedLease(): void;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// The public injection surface accepts either the logical KeyboardEvent.key
|
|
97
|
+
// (`w`, ` `, `ArrowUp`) or the physical KeyboardEvent.code (`KeyW`, `Space`).
|
|
98
|
+
// Keep both projections populated so code-first control drives games that read
|
|
99
|
+
// either keyboard.down(...) or keyboard.downCode(...).
|
|
100
|
+
const CODE_TO_KEY: Readonly<Record<string, string>> = {
|
|
101
|
+
Space: ' ',
|
|
102
|
+
ShiftLeft: 'Shift',
|
|
103
|
+
ShiftRight: 'Shift',
|
|
104
|
+
ControlLeft: 'Control',
|
|
105
|
+
ControlRight: 'Control',
|
|
106
|
+
AltLeft: 'Alt',
|
|
107
|
+
AltRight: 'Alt',
|
|
108
|
+
MetaLeft: 'Meta',
|
|
109
|
+
MetaRight: 'Meta',
|
|
110
|
+
BracketLeft: '[',
|
|
111
|
+
BracketRight: ']',
|
|
112
|
+
Backslash: '\\',
|
|
113
|
+
Semicolon: ';',
|
|
114
|
+
Quote: "'",
|
|
115
|
+
Comma: ',',
|
|
116
|
+
Period: '.',
|
|
117
|
+
Slash: '/',
|
|
118
|
+
Minus: '-',
|
|
119
|
+
Equal: '=',
|
|
120
|
+
Backquote: '`',
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
function keyForCode(code: string): string | undefined {
|
|
124
|
+
if (CODE_TO_KEY[code] !== undefined) return CODE_TO_KEY[code];
|
|
125
|
+
if (/^Key[A-Z]$/.test(code)) return code.slice(3).toLowerCase();
|
|
126
|
+
if (/^Digit[0-9]$/.test(code)) return code.slice(5);
|
|
127
|
+
if (/^Numpad[0-9]$/.test(code)) return code.slice(6);
|
|
128
|
+
if (/^F(?:[1-9]|1[0-2])$/.test(code)) return code;
|
|
129
|
+
if (/^Arrow(?:Up|Down|Left|Right)$/.test(code)) return code;
|
|
130
|
+
if (/^(?:Escape|Enter|Tab|Backspace|Delete|Home|End|PageUp|PageDown|Insert)$/.test(code))
|
|
131
|
+
return code;
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function codeForKey(key: string): string | undefined {
|
|
136
|
+
if (/^[a-zA-Z]$/.test(key)) return `Key${key.toUpperCase()}`;
|
|
137
|
+
if (/^[0-9]$/.test(key)) return `Digit${key}`;
|
|
138
|
+
if (key === ' ') return 'Space';
|
|
139
|
+
if (
|
|
140
|
+
/^(?:Arrow(?:Up|Down|Left|Right)|Escape|Enter|Tab|Backspace|Delete|Home|End|PageUp|PageDown|Insert)$/.test(
|
|
141
|
+
key,
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
return key;
|
|
145
|
+
if (/^F(?:[1-9]|1[0-2])$/.test(key)) return key;
|
|
146
|
+
const entry = Object.entries(CODE_TO_KEY).find(([, value]) => value === key);
|
|
147
|
+
return entry?.[0];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function normalizeKey(value: string): { readonly key: string; readonly code: string | undefined } {
|
|
151
|
+
const logical = keyForCode(value);
|
|
152
|
+
return logical === undefined
|
|
153
|
+
? { key: value, code: codeForKey(value) }
|
|
154
|
+
: { key: logical, code: value };
|
|
155
|
+
}
|
|
156
|
+
|
|
71
157
|
export function makeCompositeBackend(
|
|
72
158
|
inner: InputBackend,
|
|
73
159
|
options?: CompositeBackendOptions,
|
|
74
160
|
): CompositeInputBackend {
|
|
75
161
|
let yieldToHuman = options?.yieldToHuman ?? true;
|
|
162
|
+
let leaseRevoked = false;
|
|
163
|
+
let leaseGeneration = 0;
|
|
76
164
|
|
|
77
165
|
// Injected held-state (survives across frames).
|
|
78
166
|
const heldKeys = new Set<string>();
|
|
167
|
+
const heldCodes = new Set<string>();
|
|
79
168
|
const buttons: [boolean, boolean, boolean] = [false, false, false];
|
|
80
169
|
// Injected per-frame accumulators (drained on each sample()).
|
|
81
170
|
const upEdges = new Set<string>();
|
|
171
|
+
const upCodeEdges = new Set<string>();
|
|
82
172
|
const pressedKeys = new Set<string>();
|
|
173
|
+
const pressedCodes = new Set<string>();
|
|
83
174
|
const pressedButtons: [boolean, boolean, boolean] = [false, false, false];
|
|
84
175
|
const releasedButtons: [boolean, boolean, boolean] = [false, false, false];
|
|
85
176
|
let mvx = 0;
|
|
@@ -89,8 +180,11 @@ export function makeCompositeBackend(
|
|
|
89
180
|
function injectionActive(): boolean {
|
|
90
181
|
return (
|
|
91
182
|
heldKeys.size > 0 ||
|
|
183
|
+
heldCodes.size > 0 ||
|
|
92
184
|
upEdges.size > 0 ||
|
|
185
|
+
upCodeEdges.size > 0 ||
|
|
93
186
|
pressedKeys.size > 0 ||
|
|
187
|
+
pressedCodes.size > 0 ||
|
|
94
188
|
buttons.some((b) => b) ||
|
|
95
189
|
pressedButtons.some((b) => b) ||
|
|
96
190
|
releasedButtons.some((b) => b)
|
|
@@ -111,26 +205,79 @@ export function makeCompositeBackend(
|
|
|
111
205
|
// is also pressing. A yielded injected release is likewise a no-op: emitting its
|
|
112
206
|
// up-edge would incorrectly release the human-held key in the scan system.
|
|
113
207
|
const downKeys = new Set(base.downKeys);
|
|
208
|
+
const downCodes =
|
|
209
|
+
base.downCodes === undefined && heldCodes.size === 0
|
|
210
|
+
? undefined
|
|
211
|
+
: new Set(base.downCodes ?? []);
|
|
114
212
|
for (const k of heldKeys) {
|
|
115
|
-
|
|
213
|
+
const code = codeForKey(k);
|
|
214
|
+
if (
|
|
215
|
+
yieldToHuman &&
|
|
216
|
+
(base.downKeys.has(k) || (code !== undefined && base.downCodes?.has(code) === true))
|
|
217
|
+
)
|
|
218
|
+
continue; // human owns this exact key
|
|
116
219
|
downKeys.add(k);
|
|
117
220
|
}
|
|
221
|
+
for (const code of heldCodes) {
|
|
222
|
+
const key = keyForCode(code);
|
|
223
|
+
if (
|
|
224
|
+
yieldToHuman &&
|
|
225
|
+
(base.downCodes?.has(code) === true || (key !== undefined && base.downKeys.has(key)))
|
|
226
|
+
)
|
|
227
|
+
continue;
|
|
228
|
+
downCodes?.add(code);
|
|
229
|
+
}
|
|
118
230
|
|
|
119
231
|
// upKeys: union of human up-edges and injected up-edges (one-frame life), except
|
|
120
232
|
// injected edges for keys that the human still owns under the per-key yield gate.
|
|
121
233
|
const mergedUp = new Set(base.upKeys);
|
|
122
234
|
for (const k of upEdges) {
|
|
123
|
-
|
|
235
|
+
const code = codeForKey(k);
|
|
236
|
+
if (
|
|
237
|
+
yieldToHuman &&
|
|
238
|
+
(base.downKeys.has(k) || (code !== undefined && base.downCodes?.has(code) === true))
|
|
239
|
+
)
|
|
240
|
+
continue;
|
|
124
241
|
mergedUp.add(k);
|
|
125
242
|
}
|
|
243
|
+
const mergedUpCodes =
|
|
244
|
+
base.upCodes === undefined && upCodeEdges.size === 0
|
|
245
|
+
? undefined
|
|
246
|
+
: new Set(base.upCodes ?? []);
|
|
247
|
+
for (const code of upCodeEdges) {
|
|
248
|
+
const key = keyForCode(code);
|
|
249
|
+
if (
|
|
250
|
+
yieldToHuman &&
|
|
251
|
+
(base.downCodes?.has(code) === true || (key !== undefined && base.downKeys.has(key)))
|
|
252
|
+
)
|
|
253
|
+
continue;
|
|
254
|
+
mergedUpCodes?.add(code);
|
|
255
|
+
}
|
|
126
256
|
|
|
127
257
|
const mergedPressed =
|
|
128
258
|
base.pressedKeys === undefined && pressedKeys.size === 0
|
|
129
259
|
? undefined
|
|
130
260
|
: new Set([...(base.pressedKeys ?? []), ...pressedKeys]);
|
|
131
|
-
if (
|
|
261
|
+
if (
|
|
262
|
+
yieldToHuman &&
|
|
263
|
+
(base.downKeys.size > 0 || (base.downCodes?.size ?? 0) > 0) &&
|
|
264
|
+
mergedPressed !== undefined
|
|
265
|
+
) {
|
|
132
266
|
for (const key of pressedKeys) {
|
|
133
|
-
|
|
267
|
+
const code = codeForKey(key);
|
|
268
|
+
if (base.downKeys.has(key) || (code !== undefined && base.downCodes?.has(code) === true))
|
|
269
|
+
mergedPressed.delete(key);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const mergedPressedCodes =
|
|
273
|
+
base.pressedCodes === undefined && pressedCodes.size === 0
|
|
274
|
+
? undefined
|
|
275
|
+
: new Set(base.pressedCodes ?? []);
|
|
276
|
+
if (yieldToHuman && mergedPressedCodes !== undefined) {
|
|
277
|
+
for (const code of pressedCodes) {
|
|
278
|
+
const key = keyForCode(code);
|
|
279
|
+
if (base.downCodes?.has(code) === true || (key !== undefined && base.downKeys.has(key)))
|
|
280
|
+
mergedPressedCodes.delete(code);
|
|
134
281
|
}
|
|
135
282
|
}
|
|
136
283
|
|
|
@@ -166,8 +313,10 @@ export function makeCompositeBackend(
|
|
|
166
313
|
...base, // carry inner optional fields (pointers/gamepads/gestures/...) untouched
|
|
167
314
|
downKeys,
|
|
168
315
|
upKeys: mergedUp,
|
|
316
|
+
...(downCodes === undefined ? {} : { downCodes }),
|
|
317
|
+
...(mergedUpCodes === undefined ? {} : { upCodes: mergedUpCodes }),
|
|
169
318
|
...(mergedPressed === undefined ? {} : { pressedKeys: mergedPressed }),
|
|
170
|
-
...(
|
|
319
|
+
...(mergedPressedCodes === undefined ? {} : { pressedCodes: mergedPressedCodes }),
|
|
171
320
|
buttons: mergedButtons,
|
|
172
321
|
...(mergedPressedButtons === undefined ? {} : { pressedButtons: mergedPressedButtons }),
|
|
173
322
|
...(mergedReleasedButtons === undefined ? {} : { releasedButtons: mergedReleasedButtons }),
|
|
@@ -180,7 +329,9 @@ export function makeCompositeBackend(
|
|
|
180
329
|
|
|
181
330
|
// Drain injected per-frame accumulators (mirrors inner's own drain).
|
|
182
331
|
upEdges.clear();
|
|
332
|
+
upCodeEdges.clear();
|
|
183
333
|
pressedKeys.clear();
|
|
334
|
+
pressedCodes.clear();
|
|
184
335
|
pressedButtons[0] = false;
|
|
185
336
|
pressedButtons[1] = false;
|
|
186
337
|
pressedButtons[2] = false;
|
|
@@ -201,54 +352,146 @@ export function makeCompositeBackend(
|
|
|
201
352
|
? { setPointerLockAllowed: (allowed: boolean) => inner.setPointerLockAllowed?.(allowed) }
|
|
202
353
|
: {};
|
|
203
354
|
|
|
355
|
+
const clearInjected = (): void => {
|
|
356
|
+
// Clearing is a lease boundary: discard every pending frame edge first,
|
|
357
|
+
// then emit only the releases required to close state that was held
|
|
358
|
+
// across the boundary. This prevents a press/release queued before a
|
|
359
|
+
// revoke from becoming a ghost edge for the next consumer.
|
|
360
|
+
upEdges.clear();
|
|
361
|
+
upCodeEdges.clear();
|
|
362
|
+
pressedKeys.clear();
|
|
363
|
+
pressedCodes.clear();
|
|
364
|
+
for (const k of heldKeys) upEdges.add(k);
|
|
365
|
+
for (const code of heldCodes) upCodeEdges.add(code);
|
|
366
|
+
heldKeys.clear();
|
|
367
|
+
heldCodes.clear();
|
|
368
|
+
pressedButtons[0] = false;
|
|
369
|
+
pressedButtons[1] = false;
|
|
370
|
+
pressedButtons[2] = false;
|
|
371
|
+
releasedButtons[0] = false;
|
|
372
|
+
releasedButtons[1] = false;
|
|
373
|
+
releasedButtons[2] = false;
|
|
374
|
+
if (buttons[0]) releasedButtons[0] = true;
|
|
375
|
+
if (buttons[1]) releasedButtons[1] = true;
|
|
376
|
+
if (buttons[2]) releasedButtons[2] = true;
|
|
377
|
+
buttons[0] = buttons[1] = buttons[2] = false;
|
|
378
|
+
mvx = 0;
|
|
379
|
+
mvy = 0;
|
|
380
|
+
wheel = 0;
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const beginInjectedLease = (): void => {
|
|
384
|
+
// A new boundary must invalidate old references. Merely flipping a global
|
|
385
|
+
// boolean would let an async script keep writing after a later job opens.
|
|
386
|
+
leaseGeneration += 1;
|
|
387
|
+
leaseRevoked = false;
|
|
388
|
+
clearInjected();
|
|
389
|
+
};
|
|
390
|
+
const revokeInjectedLease = (): void => {
|
|
391
|
+
leaseGeneration += 1;
|
|
392
|
+
leaseRevoked = true;
|
|
393
|
+
clearInjected();
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
const createInjectedLease = (): CompositeInputLease => {
|
|
397
|
+
beginInjectedLease();
|
|
398
|
+
const generation = leaseGeneration;
|
|
399
|
+
const active = (): boolean => !leaseRevoked && generation === leaseGeneration;
|
|
400
|
+
return {
|
|
401
|
+
sample,
|
|
402
|
+
detach: () => {},
|
|
403
|
+
press(key) {
|
|
404
|
+
if (!active()) return;
|
|
405
|
+
const normalized = normalizeKey(key);
|
|
406
|
+
if (!heldKeys.has(normalized.key)) pressedKeys.add(normalized.key);
|
|
407
|
+
heldKeys.add(normalized.key);
|
|
408
|
+
if (normalized.code !== undefined) {
|
|
409
|
+
if (!heldCodes.has(normalized.code)) pressedCodes.add(normalized.code);
|
|
410
|
+
heldCodes.add(normalized.code);
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
release(key) {
|
|
414
|
+
if (!active()) return;
|
|
415
|
+
const normalized = normalizeKey(key);
|
|
416
|
+
if (heldKeys.delete(normalized.key)) upEdges.add(normalized.key);
|
|
417
|
+
if (normalized.code !== undefined && heldCodes.delete(normalized.code))
|
|
418
|
+
upCodeEdges.add(normalized.code);
|
|
419
|
+
},
|
|
420
|
+
setButton(slot, down) {
|
|
421
|
+
if (!active()) return;
|
|
422
|
+
if (down && !buttons[slot]) pressedButtons[slot] = true;
|
|
423
|
+
if (!down && buttons[slot]) releasedButtons[slot] = true;
|
|
424
|
+
buttons[slot] = down;
|
|
425
|
+
},
|
|
426
|
+
addMovement(dx, dy) {
|
|
427
|
+
if (!active()) return;
|
|
428
|
+
mvx += dx;
|
|
429
|
+
mvy += dy;
|
|
430
|
+
},
|
|
431
|
+
addWheel(notches) {
|
|
432
|
+
if (!active()) return;
|
|
433
|
+
wheel += notches;
|
|
434
|
+
},
|
|
435
|
+
clearInjected() {
|
|
436
|
+
if (active()) clearInjected();
|
|
437
|
+
},
|
|
438
|
+
revokeInjectedLease() {
|
|
439
|
+
if (active()) revokeInjectedLease();
|
|
440
|
+
},
|
|
441
|
+
};
|
|
442
|
+
};
|
|
443
|
+
|
|
204
444
|
return {
|
|
205
445
|
sample,
|
|
206
446
|
...lockGate,
|
|
207
|
-
// Teardown belongs to the human backend --
|
|
208
|
-
|
|
447
|
+
// Teardown belongs to the human backend -- revoke the synthetic lease
|
|
448
|
+
// first, then forward the physical listener teardown. `clear()` is also
|
|
449
|
+
// exposed so an owner can revoke input without detaching the browser.
|
|
450
|
+
clear: () => {
|
|
451
|
+
clearInjected();
|
|
452
|
+
inner.clear?.();
|
|
453
|
+
},
|
|
454
|
+
detach: () => {
|
|
455
|
+
clearInjected();
|
|
456
|
+
inner.detach();
|
|
457
|
+
},
|
|
209
458
|
|
|
210
459
|
press(key) {
|
|
211
|
-
if (
|
|
212
|
-
|
|
460
|
+
if (leaseRevoked) return;
|
|
461
|
+
const normalized = normalizeKey(key);
|
|
462
|
+
if (!heldKeys.has(normalized.key)) pressedKeys.add(normalized.key);
|
|
463
|
+
heldKeys.add(normalized.key);
|
|
464
|
+
if (normalized.code !== undefined) {
|
|
465
|
+
if (!heldCodes.has(normalized.code)) pressedCodes.add(normalized.code);
|
|
466
|
+
heldCodes.add(normalized.code);
|
|
467
|
+
}
|
|
213
468
|
},
|
|
214
469
|
release(key) {
|
|
215
|
-
if (
|
|
470
|
+
if (leaseRevoked) return;
|
|
471
|
+
const normalized = normalizeKey(key);
|
|
472
|
+
if (heldKeys.delete(normalized.key)) upEdges.add(normalized.key); // held -> emit one up-edge
|
|
473
|
+
if (normalized.code !== undefined && heldCodes.delete(normalized.code))
|
|
474
|
+
upCodeEdges.add(normalized.code);
|
|
216
475
|
},
|
|
217
476
|
setButton(slot, down) {
|
|
477
|
+
if (leaseRevoked) return;
|
|
218
478
|
if (down && !buttons[slot]) pressedButtons[slot] = true;
|
|
219
479
|
if (!down && buttons[slot]) releasedButtons[slot] = true;
|
|
220
480
|
buttons[slot] = down;
|
|
221
481
|
},
|
|
222
482
|
addMovement(dx, dy) {
|
|
483
|
+
if (leaseRevoked) return;
|
|
223
484
|
mvx += dx;
|
|
224
485
|
mvy += dy;
|
|
225
486
|
},
|
|
226
487
|
addWheel(notches) {
|
|
488
|
+
if (leaseRevoked) return;
|
|
227
489
|
wheel += notches;
|
|
228
490
|
},
|
|
229
|
-
clearInjected
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
// revoke from becoming a ghost edge for the next consumer.
|
|
234
|
-
upEdges.clear();
|
|
235
|
-
pressedKeys.clear();
|
|
236
|
-
for (const k of heldKeys) upEdges.add(k); // clean release edge for each
|
|
237
|
-
heldKeys.clear();
|
|
238
|
-
pressedButtons[0] = false;
|
|
239
|
-
pressedButtons[1] = false;
|
|
240
|
-
pressedButtons[2] = false;
|
|
241
|
-
releasedButtons[0] = false;
|
|
242
|
-
releasedButtons[1] = false;
|
|
243
|
-
releasedButtons[2] = false;
|
|
244
|
-
if (buttons[0]) releasedButtons[0] = true;
|
|
245
|
-
if (buttons[1]) releasedButtons[1] = true;
|
|
246
|
-
if (buttons[2]) releasedButtons[2] = true;
|
|
247
|
-
buttons[0] = buttons[1] = buttons[2] = false;
|
|
248
|
-
mvx = 0;
|
|
249
|
-
mvy = 0;
|
|
250
|
-
wheel = 0;
|
|
251
|
-
},
|
|
491
|
+
clearInjected,
|
|
492
|
+
beginInjectedLease,
|
|
493
|
+
revokeInjectedLease,
|
|
494
|
+
createInjectedLease,
|
|
252
495
|
setYieldToHuman(yield_) {
|
|
253
496
|
yieldToHuman = yield_;
|
|
254
497
|
},
|