web-input-system 0.0.0-stage → 0.4.0

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/index.js ADDED
@@ -0,0 +1,885 @@
1
+ // src/asset.ts
2
+ function defineInputAsset(def) {
3
+ return { def };
4
+ }
5
+
6
+ // src/composites.ts
7
+ function axis1D(opts) {
8
+ return { kind: "axis1d", ...opts };
9
+ }
10
+ function vector2D(opts) {
11
+ return { kind: "vector2d", normalize: opts.normalize ?? true, ...opts };
12
+ }
13
+ function isComposite(binding) {
14
+ return typeof binding === "object" && binding !== null && (binding.kind === "axis1d" || binding.kind === "vector2d");
15
+ }
16
+
17
+ // src/interactions.ts
18
+ function press() {
19
+ return { kind: "press" };
20
+ }
21
+ function hold(duration) {
22
+ return { kind: "hold", duration };
23
+ }
24
+ function tap(maxDuration = 0.25) {
25
+ return { kind: "tap", maxDuration };
26
+ }
27
+ function doubleTap(maxInterval = 0.3, maxDuration = 0.25) {
28
+ return { kind: "doubleTap", maxInterval, maxDuration };
29
+ }
30
+ function createInteraction(def, hooks) {
31
+ switch (def.kind) {
32
+ case "press":
33
+ return null;
34
+ case "hold":
35
+ return new HoldInteraction(def.duration, hooks);
36
+ case "tap":
37
+ return new TapInteraction(def.maxDuration, hooks);
38
+ case "doubleTap":
39
+ return new DoubleTapInteraction(def.maxInterval, def.maxDuration, hooks);
40
+ }
41
+ }
42
+ var HoldInteraction = class {
43
+ constructor(duration, hooks) {
44
+ this.duration = duration;
45
+ this.hooks = hooks;
46
+ }
47
+ duration;
48
+ hooks;
49
+ phase = "idle";
50
+ timer = 0;
51
+ onPress() {
52
+ this.phase = "holding";
53
+ this.timer = 0;
54
+ this.hooks.started();
55
+ }
56
+ onRelease() {
57
+ if (this.phase === "idle") return;
58
+ this.phase = "idle";
59
+ this.hooks.canceled();
60
+ }
61
+ tick(dt) {
62
+ if (this.phase !== "holding") return;
63
+ this.timer += dt;
64
+ if (this.timer >= this.duration) {
65
+ this.phase = "done";
66
+ this.hooks.performed();
67
+ }
68
+ }
69
+ reset() {
70
+ this.phase = "idle";
71
+ this.timer = 0;
72
+ }
73
+ };
74
+ var TapInteraction = class {
75
+ constructor(maxDuration, hooks) {
76
+ this.maxDuration = maxDuration;
77
+ this.hooks = hooks;
78
+ }
79
+ maxDuration;
80
+ hooks;
81
+ phase = "idle";
82
+ timer = 0;
83
+ onPress() {
84
+ this.phase = "pressing";
85
+ this.timer = 0;
86
+ this.hooks.started();
87
+ }
88
+ onRelease() {
89
+ if (this.phase !== "pressing") return;
90
+ this.phase = "idle";
91
+ this.hooks.performed();
92
+ this.hooks.canceled();
93
+ }
94
+ tick(dt) {
95
+ if (this.phase !== "pressing") return;
96
+ this.timer += dt;
97
+ if (this.timer > this.maxDuration) {
98
+ this.phase = "idle";
99
+ this.hooks.canceled();
100
+ }
101
+ }
102
+ reset() {
103
+ this.phase = "idle";
104
+ this.timer = 0;
105
+ }
106
+ };
107
+ var DoubleTapInteraction = class {
108
+ constructor(maxInterval, maxDuration, hooks) {
109
+ this.maxInterval = maxInterval;
110
+ this.maxDuration = maxDuration;
111
+ this.hooks = hooks;
112
+ }
113
+ maxInterval;
114
+ maxDuration;
115
+ hooks;
116
+ phase = "idle";
117
+ timer = 0;
118
+ onPress() {
119
+ if (this.phase === "idle") {
120
+ this.phase = "press1";
121
+ this.timer = 0;
122
+ this.hooks.started();
123
+ } else if (this.phase === "wait2") {
124
+ this.phase = "press2";
125
+ this.timer = 0;
126
+ }
127
+ }
128
+ onRelease() {
129
+ if (this.phase === "press1") {
130
+ if (this.timer <= this.maxDuration) {
131
+ this.phase = "wait2";
132
+ this.timer = 0;
133
+ } else {
134
+ this.cancel();
135
+ }
136
+ } else if (this.phase === "press2") {
137
+ if (this.timer <= this.maxDuration) {
138
+ this.phase = "idle";
139
+ this.hooks.performed();
140
+ this.hooks.canceled();
141
+ } else {
142
+ this.cancel();
143
+ }
144
+ }
145
+ }
146
+ tick(dt) {
147
+ if (this.phase === "idle") return;
148
+ this.timer += dt;
149
+ const expired = this.phase === "press1" && this.timer > this.maxDuration || this.phase === "wait2" && this.timer > this.maxInterval || this.phase === "press2" && this.timer > this.maxDuration;
150
+ if (expired) this.cancel();
151
+ }
152
+ reset() {
153
+ this.phase = "idle";
154
+ this.timer = 0;
155
+ }
156
+ cancel() {
157
+ this.phase = "idle";
158
+ this.hooks.canceled();
159
+ }
160
+ };
161
+
162
+ // src/processors.ts
163
+ function scale(factor) {
164
+ return {
165
+ number: (v) => v * factor,
166
+ vector2: (v) => ({ x: v.x * factor, y: v.y * factor })
167
+ };
168
+ }
169
+ function invert() {
170
+ return {
171
+ number: (v) => -v,
172
+ vector2: (v) => ({ x: -v.x, y: -v.y })
173
+ };
174
+ }
175
+ function invertX() {
176
+ return { vector2: (v) => ({ x: -v.x, y: v.y }) };
177
+ }
178
+ function invertY() {
179
+ return { vector2: (v) => ({ x: v.x, y: -v.y }) };
180
+ }
181
+ function deadzone(min = 0.125, max = 0.925) {
182
+ return {
183
+ number: (v) => {
184
+ const a = Math.abs(v);
185
+ if (a < min) return 0;
186
+ if (a >= max) return Math.sign(v);
187
+ return Math.sign(v) * ((a - min) / (max - min));
188
+ }
189
+ };
190
+ }
191
+ function clamp(min, max) {
192
+ return { number: (v) => Math.min(max, Math.max(min, v)) };
193
+ }
194
+ function normalize() {
195
+ return {
196
+ vector2: (v) => {
197
+ const len = Math.hypot(v.x, v.y);
198
+ return len > 0 ? { x: v.x / len, y: v.y / len } : v;
199
+ }
200
+ };
201
+ }
202
+
203
+ // src/action.ts
204
+ var InputAction = class {
205
+ /** Action name in 'map.action' form, for logging and debugging. */
206
+ name;
207
+ /** Current phase ('waiting' = never triggered or fully reset). */
208
+ get phase() {
209
+ return this._phase;
210
+ }
211
+ _phase = "waiting";
212
+ runtime;
213
+ listeners = {
214
+ started: /* @__PURE__ */ new Set(),
215
+ performed: /* @__PURE__ */ new Set(),
216
+ canceled: /* @__PURE__ */ new Set()
217
+ };
218
+ constructor(name, runtime) {
219
+ this.name = name;
220
+ this.runtime = runtime;
221
+ }
222
+ /** Subscribe to performed (value produced/changed). Returns an unsubscribe fn. */
223
+ performed(cb) {
224
+ return this.subscribe("performed", cb);
225
+ }
226
+ /** Subscribe to started (first frame leaving the default value). Returns an unsubscribe fn. */
227
+ started(cb) {
228
+ return this.subscribe("started", cb);
229
+ }
230
+ /** Subscribe to canceled (release / back to default / map deactivated). Returns an unsubscribe fn. */
231
+ canceled(cb) {
232
+ return this.subscribe("canceled", cb);
233
+ }
234
+ subscribe(phase, cb) {
235
+ const set = this.listeners[phase];
236
+ set.add(cb);
237
+ return () => set.delete(cb);
238
+ }
239
+ /** Poll the current value (type default — false / 0 / {0,0} — while the map is inactive). */
240
+ readValue() {
241
+ return this.runtime.readValue();
242
+ }
243
+ /** Whether the action is currently actuated (button: held; value: non-default). */
244
+ isPressed() {
245
+ return this.runtime.isPressed();
246
+ }
247
+ /** [internal] Called by the manager during dispatch. */
248
+ _emit(phase, value) {
249
+ this._phase = phase;
250
+ const ctx = {
251
+ action: this.name,
252
+ value,
253
+ phase,
254
+ time: typeof performance !== "undefined" ? performance.now() : 0
255
+ };
256
+ for (const cb of this.listeners[phase]) cb(ctx);
257
+ }
258
+ /** [internal] Reset phase without dispatching (used by dispose). */
259
+ _reset() {
260
+ this._phase = "waiting";
261
+ }
262
+ };
263
+
264
+ // src/actionMap.ts
265
+ var InputActionMap = class {
266
+ name;
267
+ /** [internal] Notifies the manager to re-evaluate dispatch on toggle. */
268
+ onToggle;
269
+ _enabled = true;
270
+ constructor(name, onToggle) {
271
+ this.name = name;
272
+ this.onToggle = onToggle;
273
+ }
274
+ get enabled() {
275
+ return this._enabled;
276
+ }
277
+ enable() {
278
+ if (this._enabled) return;
279
+ this._enabled = true;
280
+ this.onToggle();
281
+ }
282
+ disable() {
283
+ if (!this._enabled) return;
284
+ this._enabled = false;
285
+ this.onToggle();
286
+ }
287
+ };
288
+
289
+ // src/types.ts
290
+ var MOUSE_LEFT_BUTTON = "<Mouse>/leftButton";
291
+ var MOUSE_MIDDLE_BUTTON = "<Mouse>/middleButton";
292
+ var MOUSE_RIGHT_BUTTON = "<Mouse>/rightButton";
293
+ var MOUSE_DELTA = "<Mouse>/delta";
294
+ var MOUSE_WHEEL_Y = "<Mouse>/wheelY";
295
+ function mouseButtonPath(button) {
296
+ switch (button) {
297
+ case 0:
298
+ return MOUSE_LEFT_BUTTON;
299
+ case 1:
300
+ return MOUSE_MIDDLE_BUTTON;
301
+ case 2:
302
+ return MOUSE_RIGHT_BUTTON;
303
+ default:
304
+ return `<Mouse>/button${button}`;
305
+ }
306
+ }
307
+ function isDeltaControl(path) {
308
+ return path === MOUSE_DELTA || path === MOUSE_WHEEL_Y;
309
+ }
310
+
311
+ // src/manager.ts
312
+ var InputManager = class {
313
+ /** Auto-tracked from pointerlockchange; hosts may also override manually. */
314
+ isPointerLocked = false;
315
+ maps = /* @__PURE__ */ new Map();
316
+ actions = /* @__PURE__ */ new Map();
317
+ /** control path → actions bound to it (dispatch entry point). */
318
+ controlIndex = /* @__PURE__ */ new Map();
319
+ /** All bound keyboard codes, computed once (preventDefault lookup). */
320
+ boundKeyCodes = /* @__PURE__ */ new Set();
321
+ controls = /* @__PURE__ */ new Map();
322
+ contextGetter;
323
+ contextValue;
324
+ gateMouse;
325
+ doPreventDefault;
326
+ disposers = [];
327
+ /** Active rebind capture session (at most one at a time). */
328
+ capture = null;
329
+ /** 'map/action' → replacement bindings (the serializable overrides table). */
330
+ overrides = /* @__PURE__ */ new Map();
331
+ constructor(asset, options = {}) {
332
+ this.contextGetter = options.context;
333
+ this.gateMouse = options.gateMouseByPointerLock ?? false;
334
+ this.doPreventDefault = options.preventDefault ?? true;
335
+ for (const [mapName, mapDef] of Object.entries(asset.def.maps)) {
336
+ const runtimeMap = {
337
+ map: new InputActionMap(mapName, () => this.syncAll()),
338
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
339
+ def: mapDef
340
+ };
341
+ this.maps.set(mapName, runtimeMap);
342
+ for (const [actionName, actionDef] of Object.entries(mapDef.actions)) {
343
+ const valueType = actionDef.type === "button" ? "button" : actionDef.value;
344
+ const ra = {
345
+ action: null,
346
+ mapName,
347
+ defaultBindings: actionDef.bindings ?? [],
348
+ bindings: actionDef.bindings ?? [],
349
+ valueType,
350
+ processors: actionDef.type === "value" ? actionDef.processors ?? [] : [],
351
+ interaction: null,
352
+ held: false,
353
+ dispatched: false,
354
+ lastValue: defaultValue(valueType)
355
+ };
356
+ ra.action = new InputAction(`${mapName}.${actionName}`, {
357
+ readValue: () => this.readActionValue(ra),
358
+ isPressed: () => this.isActionPressed(ra)
359
+ });
360
+ const interactions = actionDef.type === "button" ? actionDef.interactions : void 0;
361
+ if (interactions && interactions.length > 0) {
362
+ if (interactions.length > 1) {
363
+ throw new Error(
364
+ `InputManager: '${mapName}/${actionName}' declares ${interactions.length} interactions; only one per action is supported`
365
+ );
366
+ }
367
+ ra.interaction = createInteraction(interactions[0], {
368
+ started: () => {
369
+ ra.dispatched = true;
370
+ ra.action._emit("started", true);
371
+ },
372
+ performed: () => {
373
+ ra.action._emit("performed", true);
374
+ },
375
+ canceled: () => {
376
+ ra.dispatched = false;
377
+ ra.action._emit("canceled", false);
378
+ }
379
+ });
380
+ }
381
+ this.actions.set(`${mapName}/${actionName}`, ra);
382
+ }
383
+ }
384
+ this.rebuildIndexes();
385
+ const target = options.target ?? (typeof window !== "undefined" ? window : void 0);
386
+ if (target) this.attach(target);
387
+ }
388
+ // ==================== Lookup ====================
389
+ /** Get an action; map/action names and the value type are inferred from the asset. */
390
+ action(mapName, actionName) {
391
+ const ra = this.actions.get(`${mapName}/${actionName}`);
392
+ if (!ra) throw new Error(`InputManager: unknown action '${mapName}/${actionName}'`);
393
+ return ra.action;
394
+ }
395
+ /** Get an action map (enable/disable). */
396
+ map(name) {
397
+ const rm = this.maps.get(name);
398
+ if (!rm) throw new Error(`InputManager: unknown action map '${name}'`);
399
+ return rm.map;
400
+ }
401
+ /** Raw control held-state polling, bypassing map gating (hold-to-row etc.). */
402
+ isControlDown(path) {
403
+ return this.control(path).pressed;
404
+ }
405
+ /** Push-style context (alternative to the `context` sampler, which wins if both exist). */
406
+ setContext(ctx) {
407
+ this.contextValue = ctx;
408
+ this.syncAll();
409
+ }
410
+ /** Call at the end of every frame: advances interaction timers, then resets delta accumulators. */
411
+ update(dt) {
412
+ var _a;
413
+ for (const ra of this.actions.values()) (_a = ra.interaction) == null ? void 0 : _a.tick(dt);
414
+ const delta = this.controls.get(MOUSE_DELTA);
415
+ if (delta) {
416
+ delta.x = 0;
417
+ delta.y = 0;
418
+ }
419
+ const wheel = this.controls.get(MOUSE_WHEEL_Y);
420
+ if (wheel) {
421
+ wheel.x = 0;
422
+ wheel.y = 0;
423
+ }
424
+ }
425
+ // ==================== Rebinding ====================
426
+ /** Effective bindings of an action (overrides applied). */
427
+ getBindings(mapName, actionName) {
428
+ return this.requireAction(mapName, actionName).bindings;
429
+ }
430
+ /**
431
+ * Replace an action's bindings at runtime (recorded in the overrides table).
432
+ * For composite bindings, read current bindings with getBindings, swap the
433
+ * control path of one part, and pass the updated array back.
434
+ */
435
+ setBindings(mapName, actionName, bindings) {
436
+ this.setBindingsInternal(`${mapName}/${actionName}`, bindings);
437
+ }
438
+ /** Restore asset defaults: one action, or everything when called without arguments. */
439
+ resetBindings(mapName, actionName) {
440
+ if (mapName !== void 0 && actionName !== void 0) {
441
+ const ra = this.actions.get(`${mapName}/${actionName}`);
442
+ if (ra) ra.bindings = ra.defaultBindings;
443
+ this.overrides.delete(`${mapName}/${actionName}`);
444
+ } else {
445
+ for (const [key, ra] of this.actions) ra.bindings = ra.defaultBindings;
446
+ this.overrides.clear();
447
+ }
448
+ this.rebuildIndexes();
449
+ this.syncAll();
450
+ }
451
+ /** Export the overrides table (JSON-serializable; persistence is the host's job). */
452
+ exportOverrides() {
453
+ const out = {};
454
+ for (const [key, bindings] of this.overrides) out[key] = bindings;
455
+ return out;
456
+ }
457
+ /**
458
+ * Apply a previously exported overrides table. Unknown action paths are
459
+ * skipped (overrides may come from an older version of the asset).
460
+ */
461
+ applyOverrides(overrides) {
462
+ for (const [key, bindings] of Object.entries(overrides)) {
463
+ const ra = this.actions.get(key);
464
+ if (!ra) continue;
465
+ ra.bindings = bindings;
466
+ this.overrides.set(key, bindings);
467
+ }
468
+ this.rebuildIndexes();
469
+ this.syncAll();
470
+ }
471
+ /** Conflict detection: which actions currently bind this control. */
472
+ findActionsByControl(path) {
473
+ const out = [];
474
+ for (const ra of this.controlIndex.get(path) ?? []) {
475
+ out.push({ map: ra.mapName, action: ra.action.name.split(".").pop() });
476
+ }
477
+ return out;
478
+ }
479
+ /**
480
+ * Capture the next discrete input (keyboard key or mouse button) and make it
481
+ * the action's only binding. Normal dispatch is suppressed while capturing;
482
+ * blur cancels the session. Returns a function to cancel manually.
483
+ */
484
+ startRebind(mapName, actionName, callbacks, options = {}) {
485
+ var _a, _b;
486
+ this.requireAction(mapName, actionName);
487
+ (_b = (_a = this.capture) == null ? void 0 : _a.onCancel) == null ? void 0 : _b.call(_a);
488
+ const session = {
489
+ mapName,
490
+ actionName,
491
+ cancelOn: options.cancelOn ?? ["Escape"],
492
+ onComplete: callbacks.onComplete,
493
+ onCancel: callbacks.onCancel
494
+ };
495
+ this.capture = session;
496
+ return () => {
497
+ var _a2;
498
+ if (this.capture === session) {
499
+ this.capture = null;
500
+ (_a2 = session.onCancel) == null ? void 0 : _a2.call(session);
501
+ }
502
+ };
503
+ }
504
+ /** True while a rebind capture is waiting for input. */
505
+ get isRebinding() {
506
+ return this.capture !== null;
507
+ }
508
+ requireAction(mapName, actionName) {
509
+ const ra = this.actions.get(`${mapName}/${actionName}`);
510
+ if (!ra) throw new Error(`InputManager: unknown action '${mapName}/${actionName}'`);
511
+ return ra;
512
+ }
513
+ setBindingsInternal(key, bindings) {
514
+ const ra = this.actions.get(key);
515
+ if (!ra) throw new Error(`InputManager: unknown action '${key}'`);
516
+ ra.bindings = bindings;
517
+ this.overrides.set(key, bindings);
518
+ this.rebuildIndexes();
519
+ this.syncAll();
520
+ }
521
+ /** Rebuild control→action index and bound-key set from effective bindings. */
522
+ rebuildIndexes() {
523
+ this.controlIndex.clear();
524
+ this.boundKeyCodes.clear();
525
+ for (const ra of this.actions.values()) {
526
+ for (const path of flattenControlPaths(ra.bindings)) {
527
+ const list = this.controlIndex.get(path);
528
+ if (list) list.push(ra);
529
+ else this.controlIndex.set(path, [ra]);
530
+ if (!path.startsWith("<")) this.boundKeyCodes.add(path);
531
+ }
532
+ }
533
+ }
534
+ /** Feed a control press into the capture session. Returns true when consumed. */
535
+ handleCapture(path) {
536
+ var _a;
537
+ const session = this.capture;
538
+ if (!session) return false;
539
+ this.capture = null;
540
+ if (session.cancelOn.includes(path)) {
541
+ (_a = session.onCancel) == null ? void 0 : _a.call(session);
542
+ return true;
543
+ }
544
+ this.setBindingsInternal(`${session.mapName}/${session.actionName}`, [path]);
545
+ session.onComplete(path);
546
+ return true;
547
+ }
548
+ // ==================== Evaluation ====================
549
+ getCtx() {
550
+ return this.contextGetter ? this.contextGetter() : this.contextValue;
551
+ }
552
+ isMapActive(mapName) {
553
+ const rm = this.maps.get(mapName);
554
+ if (!rm || !rm.map.enabled) return false;
555
+ const predicate = rm.def.active;
556
+ return predicate ? predicate(this.getCtx()) : true;
557
+ }
558
+ control(path) {
559
+ let st = this.controls.get(path);
560
+ if (!st) {
561
+ st = { pressed: false, x: 0, y: 0 };
562
+ this.controls.set(path, st);
563
+ }
564
+ return st;
565
+ }
566
+ /** override: delta dispatch substitutes the event delta for the frame accumulator (prevents double-counting). */
567
+ evaluate(ra, override) {
568
+ let raw;
569
+ switch (ra.valueType) {
570
+ case "button":
571
+ raw = ra.bindings.some((b) => this.bindingPressed(b));
572
+ break;
573
+ case "axis":
574
+ raw = ra.bindings.reduce((sum, b) => sum + this.bindingAxis(b, override), 0);
575
+ break;
576
+ case "vector2": {
577
+ let x = 0;
578
+ let y = 0;
579
+ for (const b of ra.bindings) {
580
+ const v = this.bindingVector(b, override);
581
+ x += v.x;
582
+ y += v.y;
583
+ }
584
+ raw = { x, y };
585
+ break;
586
+ }
587
+ }
588
+ return applyProcessors(ra.processors, ra.valueType, raw);
589
+ }
590
+ bindingPressed(b) {
591
+ if (isComposite(b)) {
592
+ return b.kind === "axis1d" ? this.control(b.negative).pressed || this.control(b.positive).pressed : this.control(b.up).pressed || this.control(b.down).pressed || this.control(b.left).pressed || this.control(b.right).pressed;
593
+ }
594
+ const st = this.control(b);
595
+ return isDeltaControl(b) ? st.x !== 0 || st.y !== 0 : st.pressed;
596
+ }
597
+ bindingAxis(b, override) {
598
+ if (isComposite(b)) {
599
+ if (b.kind !== "axis1d") return 0;
600
+ return (this.control(b.positive).pressed ? 1 : 0) - (this.control(b.negative).pressed ? 1 : 0);
601
+ }
602
+ if (b === MOUSE_WHEEL_Y) return override && override.path === b ? override.y : this.control(b).y;
603
+ if (b === MOUSE_DELTA) return override && override.path === b ? override.x : this.control(b).x;
604
+ return this.control(b).pressed ? 1 : 0;
605
+ }
606
+ bindingVector(b, override) {
607
+ if (isComposite(b)) {
608
+ if (b.kind !== "vector2d") return { x: 0, y: 0 };
609
+ return compositeVector(b, (p) => this.control(p).pressed);
610
+ }
611
+ if (b === MOUSE_DELTA) {
612
+ if (override && override.path === b) return { x: override.x, y: override.y };
613
+ const st = this.control(b);
614
+ return { x: st.x, y: st.y };
615
+ }
616
+ if (b === MOUSE_WHEEL_Y) {
617
+ const y = override && override.path === b ? override.y : this.control(b).y;
618
+ return { x: 0, y };
619
+ }
620
+ return { x: 0, y: this.control(b).pressed ? 1 : 0 };
621
+ }
622
+ readActionValue(ra) {
623
+ if (!this.isMapActive(ra.mapName)) return defaultValue(ra.valueType);
624
+ return this.evaluate(ra);
625
+ }
626
+ isActionPressed(ra) {
627
+ if (!this.isMapActive(ra.mapName)) return false;
628
+ const v = this.evaluate(ra);
629
+ return ra.valueType === "button" ? v : !isDefaultValue(ra.valueType, v);
630
+ }
631
+ // ==================== Dispatch ====================
632
+ /** Discrete controls (keyboard / mouse buttons): three-phase state machine. */
633
+ syncAction(ra) {
634
+ if (!this.isMapActive(ra.mapName)) {
635
+ if (ra.interaction) {
636
+ ra.held = false;
637
+ ra.interaction.reset();
638
+ }
639
+ if (ra.dispatched) {
640
+ ra.dispatched = false;
641
+ ra.action._emit("canceled", defaultValue(ra.valueType));
642
+ }
643
+ return;
644
+ }
645
+ const value = this.evaluate(ra);
646
+ if (ra.valueType === "button") {
647
+ if (ra.interaction) {
648
+ const pressed = value;
649
+ if (pressed !== ra.held) {
650
+ ra.held = pressed;
651
+ if (pressed) ra.interaction.onPress();
652
+ else ra.interaction.onRelease();
653
+ }
654
+ return;
655
+ }
656
+ if (value && !ra.dispatched) {
657
+ ra.dispatched = true;
658
+ ra.action._emit("started", value);
659
+ ra.action._emit("performed", value);
660
+ } else if (!value && ra.dispatched) {
661
+ ra.dispatched = false;
662
+ ra.action._emit("canceled", value);
663
+ }
664
+ return;
665
+ }
666
+ if (isDefaultValue(ra.valueType, value)) {
667
+ if (ra.dispatched) {
668
+ ra.dispatched = false;
669
+ ra.action._emit("canceled", value);
670
+ }
671
+ } else {
672
+ if (!ra.dispatched) {
673
+ ra.dispatched = true;
674
+ ra.action._emit("started", value);
675
+ ra.action._emit("performed", value);
676
+ } else if (!valueEquals(ra.lastValue, value)) {
677
+ ra.action._emit("performed", value);
678
+ }
679
+ }
680
+ ra.lastValue = value;
681
+ }
682
+ syncControls(paths) {
683
+ const seen = /* @__PURE__ */ new Set();
684
+ for (const path of paths) {
685
+ for (const ra of this.controlIndex.get(path) ?? []) {
686
+ if (seen.has(ra)) continue;
687
+ seen.add(ra);
688
+ this.syncAction(ra);
689
+ }
690
+ }
691
+ }
692
+ syncAll() {
693
+ for (const ra of this.actions.values()) this.syncAction(ra);
694
+ }
695
+ /** Delta controls (mouse motion / wheel): performed per event, no started/canceled lifecycle. */
696
+ dispatchDelta(path, x, y) {
697
+ const st = this.control(path);
698
+ st.x += x;
699
+ st.y += y;
700
+ for (const ra of this.controlIndex.get(path) ?? []) {
701
+ if (!this.isMapActive(ra.mapName)) continue;
702
+ const value = this.evaluate(ra, { path, x, y });
703
+ if (!isDefaultValue(ra.valueType, value)) {
704
+ ra.action._emit("performed", value);
705
+ ra.lastValue = value;
706
+ }
707
+ }
708
+ }
709
+ mouseGateOpen() {
710
+ return !this.gateMouse || this.isPointerLocked;
711
+ }
712
+ // ==================== DOM wiring ====================
713
+ attach(target) {
714
+ const doc = target.document;
715
+ const onKeyDown = (e) => {
716
+ const el = e.target;
717
+ if (el && (el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.isContentEditable)) return;
718
+ if (e.repeat) return;
719
+ if (this.handleCapture(e.code)) return;
720
+ this.control(e.code).pressed = true;
721
+ if (this.doPreventDefault && this.boundKeyCodes.has(e.code)) e.preventDefault();
722
+ this.syncControls([e.code]);
723
+ };
724
+ const onKeyUp = (e) => {
725
+ this.control(e.code).pressed = false;
726
+ this.syncControls([e.code]);
727
+ };
728
+ const onMouseMove = (e) => {
729
+ if (!this.mouseGateOpen()) return;
730
+ this.dispatchDelta(MOUSE_DELTA, e.movementX, e.movementY);
731
+ };
732
+ const onWheel = (e) => {
733
+ if (!this.mouseGateOpen()) return;
734
+ this.dispatchDelta(MOUSE_WHEEL_Y, 0, e.deltaY);
735
+ if (this.doPreventDefault && (this.controlIndex.get(MOUSE_WHEEL_Y) ?? []).some((ra) => this.isMapActive(ra.mapName))) {
736
+ e.preventDefault();
737
+ }
738
+ };
739
+ const onMouseDown = (e) => {
740
+ const path = mouseButtonPath(e.button);
741
+ this.control(path).pressed = true;
742
+ if (this.handleCapture(path)) return;
743
+ if (this.mouseGateOpen()) this.syncControls([path]);
744
+ };
745
+ const onMouseUp = (e) => {
746
+ const path = mouseButtonPath(e.button);
747
+ this.control(path).pressed = false;
748
+ if (this.mouseGateOpen()) this.syncControls([path]);
749
+ };
750
+ const clearAll = () => {
751
+ var _a;
752
+ for (const st of this.controls.values()) {
753
+ st.pressed = false;
754
+ }
755
+ if (this.capture) {
756
+ const session = this.capture;
757
+ this.capture = null;
758
+ (_a = session.onCancel) == null ? void 0 : _a.call(session);
759
+ }
760
+ this.syncAll();
761
+ };
762
+ const onBlur = () => clearAll();
763
+ const onVisibilityChange = () => {
764
+ if (doc.hidden) clearAll();
765
+ };
766
+ const onPointerLockChange = () => {
767
+ this.isPointerLocked = doc.pointerLockElement != null;
768
+ this.syncAll();
769
+ };
770
+ target.addEventListener("keydown", onKeyDown);
771
+ target.addEventListener("keyup", onKeyUp);
772
+ target.addEventListener("mousemove", onMouseMove);
773
+ target.addEventListener("wheel", onWheel, { passive: false });
774
+ target.addEventListener("mousedown", onMouseDown);
775
+ target.addEventListener("mouseup", onMouseUp);
776
+ target.addEventListener("blur", onBlur);
777
+ doc.addEventListener("visibilitychange", onVisibilityChange);
778
+ doc.addEventListener("pointerlockchange", onPointerLockChange);
779
+ this.disposers.push(
780
+ () => target.removeEventListener("keydown", onKeyDown),
781
+ () => target.removeEventListener("keyup", onKeyUp),
782
+ () => target.removeEventListener("mousemove", onMouseMove),
783
+ () => target.removeEventListener("wheel", onWheel),
784
+ () => target.removeEventListener("mousedown", onMouseDown),
785
+ () => target.removeEventListener("mouseup", onMouseUp),
786
+ () => target.removeEventListener("blur", onBlur),
787
+ () => doc.removeEventListener("visibilitychange", onVisibilityChange),
788
+ () => doc.removeEventListener("pointerlockchange", onPointerLockChange)
789
+ );
790
+ }
791
+ dispose() {
792
+ this.disposers.forEach((d) => d());
793
+ this.disposers.length = 0;
794
+ for (const ra of this.actions.values()) {
795
+ ra.dispatched = false;
796
+ ra.action._reset();
797
+ }
798
+ this.controls.clear();
799
+ }
800
+ };
801
+ function createInput(asset, options) {
802
+ return new InputManager(asset, options);
803
+ }
804
+ function flattenControlPaths(bindings) {
805
+ const out = [];
806
+ for (const b of bindings) {
807
+ if (isComposite(b)) {
808
+ if (b.kind === "axis1d") out.push(b.negative, b.positive);
809
+ else out.push(b.up, b.down, b.left, b.right);
810
+ } else {
811
+ out.push(b);
812
+ }
813
+ }
814
+ return out;
815
+ }
816
+ function compositeVector(b, pressed) {
817
+ let x = (pressed(b.right) ? 1 : 0) - (pressed(b.left) ? 1 : 0);
818
+ let y = (pressed(b.up) ? 1 : 0) - (pressed(b.down) ? 1 : 0);
819
+ if (b.normalize && x !== 0 && y !== 0) {
820
+ const inv = 1 / Math.hypot(x, y);
821
+ x *= inv;
822
+ y *= inv;
823
+ }
824
+ return { x, y };
825
+ }
826
+ function defaultValue(type) {
827
+ return type === "button" ? false : type === "axis" ? 0 : { x: 0, y: 0 };
828
+ }
829
+ function applyProcessors(processors, type, value) {
830
+ if (processors.length === 0) return value;
831
+ if (type === "axis") {
832
+ let v = value;
833
+ for (const p of processors) if (p.number) v = p.number(v);
834
+ return v;
835
+ }
836
+ if (type === "vector2") {
837
+ let v = value;
838
+ for (const p of processors) if (p.vector2) v = p.vector2(v);
839
+ return v;
840
+ }
841
+ return value;
842
+ }
843
+ function isDefaultValue(type, v) {
844
+ if (type === "button") return !v;
845
+ if (type === "axis") return v === 0;
846
+ const vec = v;
847
+ return vec.x === 0 && vec.y === 0;
848
+ }
849
+ function valueEquals(a, b) {
850
+ if (typeof a === "object" && typeof b === "object" && a !== null && b !== null) {
851
+ const va = a;
852
+ const vb = b;
853
+ return va.x === vb.x && va.y === vb.y;
854
+ }
855
+ return a === b;
856
+ }
857
+ export {
858
+ InputAction,
859
+ InputActionMap,
860
+ InputManager,
861
+ MOUSE_DELTA,
862
+ MOUSE_LEFT_BUTTON,
863
+ MOUSE_MIDDLE_BUTTON,
864
+ MOUSE_RIGHT_BUTTON,
865
+ MOUSE_WHEEL_Y,
866
+ axis1D,
867
+ clamp,
868
+ createInput,
869
+ deadzone,
870
+ defineInputAsset,
871
+ doubleTap,
872
+ hold,
873
+ invert,
874
+ invertX,
875
+ invertY,
876
+ isComposite,
877
+ isDeltaControl,
878
+ mouseButtonPath,
879
+ normalize,
880
+ press,
881
+ scale,
882
+ tap,
883
+ vector2D
884
+ };
885
+ //# sourceMappingURL=index.js.map