@simpludify/web 0.1.5 → 0.1.6
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 +3 -3
- package/dist/game.d.ts +3 -1
- package/dist/gamepad-bindings.d.ts +2 -100
- package/dist/gamepads.d.ts +11 -2
- package/dist/index.js +361 -140
- package/dist/index.js.map +1 -1
- package/dist/input-capture.d.ts +33 -0
- package/dist/pointer.d.ts +12 -2
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import "./style.css";
|
|
2
|
-
import { ActionController, Aspect, Camera, CanvasButton as CanvasButton$1, CanvasUI, Engine, EngineError, EventBus, FixedStepRunner, GeometryTilemapLayer, Lifetime, PointerRouting, Renderable, ReplayPlayer, Scene, ScenePointer, SceneState, Shape, TilemapLayer, Transform, VirtualInput, canvasControlText, canvasHoverHighlight, canvasOpacity, canvasScrollbars, canvasSliderGeometry, cleanupAll, containsScreenPoint, createViewTransform, defineEvent, imageSize, intersectUI, isKeyCode, positive, screenPoint, screenToWorld, spriteFrameBounds, worldToScreen, wrapCanvasText } from "@simpludify/core";
|
|
2
|
+
import { ActionController, Aspect, Camera, CanvasButton as CanvasButton$1, CanvasUI, Engine, EngineError, EventBus, FixedStepRunner, GamepadAxis, GamepadBindings, GamepadButton, GeometryTilemapLayer, InputCapture, InputCaptureState, InputDevice, Lifetime, MouseButton, PointerRouting, Renderable, ReplayPlayer, Scene, ScenePointer, SceneState, Shape, TilemapLayer, Transform, VirtualInput, canvasControlText, canvasHoverHighlight, canvasOpacity, canvasScrollbars, canvasSliderGeometry, checkedGamepadBindings, cleanupAll, containsScreenPoint, createViewTransform, defineEvent, gamepadDeadZone, gamepadIndex, imageSize, intersectUI, isKeyCode, isMouseButton, positive, screenPoint, screenToWorld, spriteFrameBounds, worldToScreen, wrapCanvasText } from "@simpludify/core";
|
|
3
3
|
import { Application, CanvasTextMetrics, Container, Graphics, Rectangle, RendererType, Sprite, Text, TextStyle, Texture } from "pixi.js";
|
|
4
4
|
export * from "@simpludify/core";
|
|
5
5
|
// packages/web/src/canvas-node.ts
|
|
@@ -17,135 +17,6 @@ function CanvasButton(text, click, style = {}) {
|
|
|
17
17
|
return click(signal);
|
|
18
18
|
}, style);
|
|
19
19
|
}
|
|
20
|
-
// packages/web/src/gamepad-bindings.ts
|
|
21
|
-
/** Standard mapping physical positions, not product-specific printed labels. */
|
|
22
|
-
var GamepadButton = Object.freeze({
|
|
23
|
-
South: 0,
|
|
24
|
-
East: 1,
|
|
25
|
-
West: 2,
|
|
26
|
-
North: 3,
|
|
27
|
-
LeftShoulder: 4,
|
|
28
|
-
RightShoulder: 5,
|
|
29
|
-
LeftTrigger: 6,
|
|
30
|
-
RightTrigger: 7,
|
|
31
|
-
Select: 8,
|
|
32
|
-
Start: 9,
|
|
33
|
-
LeftStick: 10,
|
|
34
|
-
RightStick: 11,
|
|
35
|
-
DPadUp: 12,
|
|
36
|
-
DPadDown: 13,
|
|
37
|
-
DPadLeft: 14,
|
|
38
|
-
DPadRight: 15,
|
|
39
|
-
Home: 16
|
|
40
|
-
});
|
|
41
|
-
var GamepadAxis = Object.freeze({
|
|
42
|
-
LeftX: 0,
|
|
43
|
-
LeftY: 1,
|
|
44
|
-
RightX: 2,
|
|
45
|
-
RightY: 3
|
|
46
|
-
});
|
|
47
|
-
var axis = (index) => Object.freeze({ axis: index });
|
|
48
|
-
/** Ready-to-use data; game Actions retain application-defined names. */
|
|
49
|
-
var GamepadBindings = Object.freeze({
|
|
50
|
-
LeftStick: Object.freeze({
|
|
51
|
-
x: axis(0),
|
|
52
|
-
y: axis(1)
|
|
53
|
-
}),
|
|
54
|
-
RightStick: Object.freeze({
|
|
55
|
-
x: axis(2),
|
|
56
|
-
y: axis(3)
|
|
57
|
-
}),
|
|
58
|
-
DPad: Object.freeze({
|
|
59
|
-
x: Object.freeze({
|
|
60
|
-
negative: Object.freeze([14]),
|
|
61
|
-
positive: Object.freeze([15])
|
|
62
|
-
}),
|
|
63
|
-
y: Object.freeze({
|
|
64
|
-
negative: Object.freeze([12]),
|
|
65
|
-
positive: Object.freeze([13])
|
|
66
|
-
})
|
|
67
|
-
}),
|
|
68
|
-
Move: Object.freeze({
|
|
69
|
-
x: Object.freeze({
|
|
70
|
-
axis: 0,
|
|
71
|
-
negative: Object.freeze([14]),
|
|
72
|
-
positive: Object.freeze([15])
|
|
73
|
-
}),
|
|
74
|
-
y: Object.freeze({
|
|
75
|
-
axis: 1,
|
|
76
|
-
negative: Object.freeze([12]),
|
|
77
|
-
positive: Object.freeze([13])
|
|
78
|
-
})
|
|
79
|
-
})
|
|
80
|
-
});
|
|
81
|
-
var fail = () => {
|
|
82
|
-
throw new EngineError("INVALID_GAMEPAD_BINDING", "Invalid gamepad binding");
|
|
83
|
-
};
|
|
84
|
-
function gamepadIndex(value) {
|
|
85
|
-
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) return fail();
|
|
86
|
-
return value;
|
|
87
|
-
}
|
|
88
|
-
function gamepadDeadZone(value) {
|
|
89
|
-
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value >= 1) return fail();
|
|
90
|
-
return value;
|
|
91
|
-
}
|
|
92
|
-
function record(value, keys) {
|
|
93
|
-
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((k) => !keys.includes(k))) return fail();
|
|
94
|
-
return Object.fromEntries(Object.entries(value));
|
|
95
|
-
}
|
|
96
|
-
function axisBinding(value) {
|
|
97
|
-
const v = record(value, [
|
|
98
|
-
"axis",
|
|
99
|
-
"invert",
|
|
100
|
-
"negative",
|
|
101
|
-
"positive",
|
|
102
|
-
"deadZone"
|
|
103
|
-
]);
|
|
104
|
-
const indices = (value) => {
|
|
105
|
-
if (value === void 0) return Object.freeze([]);
|
|
106
|
-
if (!Array.isArray(value)) return fail();
|
|
107
|
-
return Object.freeze([...new Set(value.map(gamepadIndex))]);
|
|
108
|
-
};
|
|
109
|
-
if (v.invert !== void 0 && typeof v.invert !== "boolean") return fail();
|
|
110
|
-
return Object.freeze({
|
|
111
|
-
...v.axis === void 0 ? {} : { axis: gamepadIndex(v.axis) },
|
|
112
|
-
invert: v.invert ?? false,
|
|
113
|
-
negative: indices(v.negative),
|
|
114
|
-
positive: indices(v.positive),
|
|
115
|
-
...v.deadZone === void 0 ? {} : { deadZone: gamepadDeadZone(v.deadZone) }
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
function checkedGamepadBindings(actions, value) {
|
|
119
|
-
const v = record(value, Object.keys(actions));
|
|
120
|
-
return Object.freeze(Object.entries(v).map(([name, binding]) => {
|
|
121
|
-
if (!Object.hasOwn(actions, name)) return fail();
|
|
122
|
-
const action = actions[name];
|
|
123
|
-
if (!action) return fail();
|
|
124
|
-
let result;
|
|
125
|
-
if (action.kind === "button") {
|
|
126
|
-
const b = record(binding, ["buttons"]);
|
|
127
|
-
if (b.buttons !== void 0 && !Array.isArray(b.buttons)) return fail();
|
|
128
|
-
result = Object.freeze({ buttons: Object.freeze((b.buttons ?? []).map((item) => {
|
|
129
|
-
if (typeof item === "number") return gamepadIndex(item);
|
|
130
|
-
const b = record(item, ["button", "threshold"]), threshold = b.threshold ?? .5;
|
|
131
|
-
if (typeof threshold !== "number" || !Number.isFinite(threshold) || threshold <= 0 || threshold > 1) return fail();
|
|
132
|
-
return Object.freeze({
|
|
133
|
-
button: gamepadIndex(b.button),
|
|
134
|
-
threshold
|
|
135
|
-
});
|
|
136
|
-
})) });
|
|
137
|
-
} else if (action.kind === "axis1d") result = axisBinding(binding);
|
|
138
|
-
else {
|
|
139
|
-
const keys = action.kind === "axis2d" ? ["x", "y"] : [
|
|
140
|
-
"x",
|
|
141
|
-
"y",
|
|
142
|
-
"z"
|
|
143
|
-
], b = record(binding, keys);
|
|
144
|
-
result = Object.freeze(Object.fromEntries(keys.map((k) => [k, axisBinding(b[k] ?? {})])));
|
|
145
|
-
}
|
|
146
|
-
return Object.freeze([action, result]);
|
|
147
|
-
}));
|
|
148
|
-
}
|
|
149
20
|
// packages/web/src/gamepad-layout.ts
|
|
150
21
|
var positions = Object.fromEntries(Object.entries(GamepadButton).map(([name, index]) => [index, name]));
|
|
151
22
|
var faces = Object.freeze({
|
|
@@ -230,6 +101,9 @@ var GamepadSource = class {
|
|
|
230
101
|
signature;
|
|
231
102
|
zone;
|
|
232
103
|
mapping;
|
|
104
|
+
following;
|
|
105
|
+
bindingRevision;
|
|
106
|
+
waiting = /* @__PURE__ */ new Set();
|
|
233
107
|
constructor(input, controller, options) {
|
|
234
108
|
this.input = input;
|
|
235
109
|
this.controller = controller;
|
|
@@ -238,7 +112,9 @@ var GamepadSource = class {
|
|
|
238
112
|
this.zone = gamepadDeadZone(options.deadZone ?? .15);
|
|
239
113
|
this.mapping = options.mapping ?? "standard";
|
|
240
114
|
if (this.mapping !== "standard" && this.mapping !== "raw") throw new EngineError("INVALID_GAMEPAD_BINDING", "Unknown gamepad mapping policy");
|
|
241
|
-
this.
|
|
115
|
+
this.following = options.bindings === void 0;
|
|
116
|
+
this.bindingRevision = controller.bindingRevision;
|
|
117
|
+
this.bindings = this.following ? this.commonBindings() : checkedGamepadBindings(controller.actions, options.bindings);
|
|
242
118
|
this.source = controller.createSource();
|
|
243
119
|
this.life.onDispose(() => this.source.dispose());
|
|
244
120
|
this.life.onDispose(controller.onDispose(() => this.dispose()));
|
|
@@ -277,19 +153,42 @@ var GamepadSource = class {
|
|
|
277
153
|
}
|
|
278
154
|
rebind(bindings) {
|
|
279
155
|
this.alive();
|
|
280
|
-
const checked = checkedGamepadBindings(this.controller.actions, bindings);
|
|
156
|
+
const checked = bindings === void 0 ? this.commonBindings() : checkedGamepadBindings(this.controller.actions, bindings);
|
|
281
157
|
this.clear();
|
|
282
158
|
this.bindings = checked;
|
|
159
|
+
this.following = bindings === void 0;
|
|
160
|
+
this.bindingRevision = this.controller.bindingRevision;
|
|
161
|
+
}
|
|
162
|
+
commonBindings() {
|
|
163
|
+
return Object.freeze(Object.values(this.controller.actions).flatMap((action) => {
|
|
164
|
+
const binding = this.controller.getBinding(action).gamepad;
|
|
165
|
+
return binding ? [Object.freeze([action, binding])] : [];
|
|
166
|
+
}));
|
|
167
|
+
}
|
|
168
|
+
syncBindings() {
|
|
169
|
+
if (!this.following || this.bindingRevision === this.controller.bindingRevision) return;
|
|
170
|
+
const next = this.commonBindings();
|
|
171
|
+
const before = new Map(this.bindings), after = new Map(next);
|
|
172
|
+
for (const action of /* @__PURE__ */ new Set([...before.keys(), ...after.keys()])) {
|
|
173
|
+
if (JSON.stringify(before.get(action)) === JSON.stringify(after.get(action))) continue;
|
|
174
|
+
this.source.resetBinding(action);
|
|
175
|
+
if (after.has(action)) this.waiting.add(action);
|
|
176
|
+
else this.waiting.delete(action);
|
|
177
|
+
}
|
|
178
|
+
this.bindings = next;
|
|
179
|
+
this.bindingRevision = this.controller.bindingRevision;
|
|
283
180
|
}
|
|
284
181
|
clear() {
|
|
285
182
|
this.source.clear();
|
|
286
183
|
this.armed = false;
|
|
184
|
+
this.waiting.clear();
|
|
287
185
|
}
|
|
288
186
|
alive() {
|
|
289
187
|
if (this.disposed) throw new EngineError("OWNER_DISPOSED", "Gamepad source has been disposed");
|
|
290
188
|
}
|
|
291
189
|
/** @internal */ sample(pad, allowed) {
|
|
292
190
|
if (this.disposed) return;
|
|
191
|
+
this.syncBindings();
|
|
293
192
|
if (!allowed || !this.enabled || !this.controller.active || !pad || this.mapping === "standard" && pad.mapping !== "standard") {
|
|
294
193
|
this.clear();
|
|
295
194
|
this.signature = void 0;
|
|
@@ -310,7 +209,13 @@ var GamepadSource = class {
|
|
|
310
209
|
if (next.every(([, value]) => neutral(value))) this.armed = true;
|
|
311
210
|
return;
|
|
312
211
|
}
|
|
313
|
-
for (const [action, value] of next)
|
|
212
|
+
for (const [action, value] of next) {
|
|
213
|
+
if (this.waiting.has(action)) {
|
|
214
|
+
if (!neutral(value)) continue;
|
|
215
|
+
this.waiting.delete(action);
|
|
216
|
+
}
|
|
217
|
+
this.source.set(action, value);
|
|
218
|
+
}
|
|
314
219
|
}
|
|
315
220
|
/** @internal */ onDispose(callback) {
|
|
316
221
|
return this.life.onDispose(callback);
|
|
@@ -331,6 +236,7 @@ var GamepadInput = class {
|
|
|
331
236
|
readable = false;
|
|
332
237
|
poll;
|
|
333
238
|
active;
|
|
239
|
+
observe;
|
|
334
240
|
constructor(input, options = {}) {
|
|
335
241
|
this.input = input;
|
|
336
242
|
this.life.onDispose(() => this.changes.dispose());
|
|
@@ -338,6 +244,7 @@ var GamepadInput = class {
|
|
|
338
244
|
throw new Error("Gamepad API unavailable");
|
|
339
245
|
})());
|
|
340
246
|
this.active = options.active ?? (() => true);
|
|
247
|
+
this.observe = options.observe;
|
|
341
248
|
this.life.onDispose(input.addAdapter(this));
|
|
342
249
|
const events = options.events ?? (typeof window === "undefined" ? void 0 : window), abort = new AbortController();
|
|
343
250
|
this.life.onDispose(() => abort.abort());
|
|
@@ -412,7 +319,8 @@ var GamepadInput = class {
|
|
|
412
319
|
axisCount: p.axes.length,
|
|
413
320
|
inferredLayout: inferredGamepadLayout(p.id)
|
|
414
321
|
})));
|
|
415
|
-
|
|
322
|
+
this.observe?.(pads);
|
|
323
|
+
const allowed = this.active() && this.input.liveInput;
|
|
416
324
|
for (const source of this.sources) if (source.disposed) this.sources.delete(source);
|
|
417
325
|
else source.sample(pads.get(source.index), allowed);
|
|
418
326
|
this.publish(listed, wasReadable !== this.readable);
|
|
@@ -520,35 +428,286 @@ var BrowserInput = class {
|
|
|
520
428
|
this.clear();
|
|
521
429
|
}
|
|
522
430
|
};
|
|
431
|
+
// packages/web/src/input-capture.ts
|
|
432
|
+
var mouseMasks$1 = [
|
|
433
|
+
[0, 1],
|
|
434
|
+
[1, 4],
|
|
435
|
+
[2, 2],
|
|
436
|
+
[3, 8],
|
|
437
|
+
[4, 16]
|
|
438
|
+
];
|
|
439
|
+
/** @internal One Game-owned observer; existing adapters retain gameplay ownership. */
|
|
440
|
+
var InputCaptureManager = class {
|
|
441
|
+
host;
|
|
442
|
+
input;
|
|
443
|
+
scene;
|
|
444
|
+
releasePointers;
|
|
445
|
+
report;
|
|
446
|
+
abort = new AbortController();
|
|
447
|
+
current;
|
|
448
|
+
keys = /* @__PURE__ */ new Set();
|
|
449
|
+
blockedKeys = /* @__PURE__ */ new Set();
|
|
450
|
+
blockedPointers = /* @__PURE__ */ new Set();
|
|
451
|
+
blockedAuxiliary = /* @__PURE__ */ new Set();
|
|
452
|
+
mouseButtons = 0;
|
|
453
|
+
needsPadSample = false;
|
|
454
|
+
pads = /* @__PURE__ */ new Map();
|
|
455
|
+
readyPads = /* @__PURE__ */ new Set();
|
|
456
|
+
dead = false;
|
|
457
|
+
constructor(host, input, scene, releasePointers, report) {
|
|
458
|
+
this.host = host;
|
|
459
|
+
this.input = input;
|
|
460
|
+
this.scene = scene;
|
|
461
|
+
this.releasePointers = releasePointers;
|
|
462
|
+
this.report = report;
|
|
463
|
+
const options = {
|
|
464
|
+
capture: true,
|
|
465
|
+
signal: this.abort.signal
|
|
466
|
+
};
|
|
467
|
+
window.addEventListener("keydown", (event) => this.guard(() => this.keyDown(event)), options);
|
|
468
|
+
window.addEventListener("keyup", (event) => this.guard(() => {
|
|
469
|
+
if (isKeyCode(event.code)) {
|
|
470
|
+
this.keys.delete(event.code);
|
|
471
|
+
this.blockedKeys.delete(event.code);
|
|
472
|
+
}
|
|
473
|
+
this.arm();
|
|
474
|
+
}), options);
|
|
475
|
+
for (const kind of [
|
|
476
|
+
"pointerdown",
|
|
477
|
+
"pointermove",
|
|
478
|
+
"pointerup",
|
|
479
|
+
"pointercancel"
|
|
480
|
+
]) window.addEventListener(kind, (event) => this.guard(() => this.pointer(event)), options);
|
|
481
|
+
for (const kind of ["auxclick", "contextmenu"]) window.addEventListener(kind, (event) => {
|
|
482
|
+
if (this.surface(event.target) && isMouseButton(event.button) && this.blockedAuxiliary.has(event.button)) {
|
|
483
|
+
this.consume(event);
|
|
484
|
+
if (kind === "auxclick") this.blockedAuxiliary.delete(event.button);
|
|
485
|
+
}
|
|
486
|
+
}, options);
|
|
487
|
+
window.addEventListener("blur", (event) => {
|
|
488
|
+
if (event.target === window) this.interrupt();
|
|
489
|
+
}, options);
|
|
490
|
+
document.addEventListener("visibilitychange", () => {
|
|
491
|
+
if (document.hidden) this.interrupt();
|
|
492
|
+
}, options);
|
|
493
|
+
host.addEventListener("focusout", (event) => {
|
|
494
|
+
if (!(event.relatedTarget instanceof Node) || !host.contains(event.relatedTarget) || blocksGameKeyboard(event.relatedTarget)) this.interrupt();
|
|
495
|
+
}, options);
|
|
496
|
+
host.addEventListener("focusin", (event) => {
|
|
497
|
+
if (blocksGameKeyboard(event.target)) this.interrupt();
|
|
498
|
+
}, options);
|
|
499
|
+
}
|
|
500
|
+
start(options = {}) {
|
|
501
|
+
if (this.dead) throw new EngineError("GAME_DISPOSED", "Capture host has been disposed");
|
|
502
|
+
if (this.current) throw new EngineError("INPUT_CAPTURE_ACTIVE", "Cancel the active capture before starting another");
|
|
503
|
+
const scene = this.scene();
|
|
504
|
+
if (scene.state !== SceneState.Active || document.hidden || !document.hasFocus()) throw new EngineError("INPUT_CAPTURE_UNAVAILABLE", "Capture requires an active Scene and focused visible document");
|
|
505
|
+
const capture = new InputCapture(options);
|
|
506
|
+
let release;
|
|
507
|
+
try {
|
|
508
|
+
capture.onDispose(scene.onDispose(() => capture.cancel()));
|
|
509
|
+
capture.onDispose(() => {
|
|
510
|
+
if (this.current === capture) this.current = void 0;
|
|
511
|
+
this.pads.clear();
|
|
512
|
+
this.readyPads.clear();
|
|
513
|
+
this.needsPadSample = false;
|
|
514
|
+
release?.();
|
|
515
|
+
});
|
|
516
|
+
this.current = capture;
|
|
517
|
+
this.needsPadSample = capture.devices.includes(InputDevice.Gamepad);
|
|
518
|
+
for (const key of this.keys) this.blockedKeys.add(key);
|
|
519
|
+
for (const [button, mask] of mouseMasks$1) if (this.mouseButtons & mask) this.blockedAuxiliary.add(button);
|
|
520
|
+
this.releasePointers();
|
|
521
|
+
if (capture.disposed) return capture;
|
|
522
|
+
release = this.input.suspendLiveInput(() => capture.cancel());
|
|
523
|
+
if (capture.disposed) {
|
|
524
|
+
release();
|
|
525
|
+
return capture;
|
|
526
|
+
}
|
|
527
|
+
this.host.focus({ preventScroll: true });
|
|
528
|
+
this.arm();
|
|
529
|
+
return capture;
|
|
530
|
+
} catch (error) {
|
|
531
|
+
capture.cancel();
|
|
532
|
+
throw error;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
arm() {
|
|
536
|
+
const capture = this.current;
|
|
537
|
+
if (!capture || capture.state !== InputCaptureState.Arming || this.needsPadSample) return;
|
|
538
|
+
if (capture.devices.includes(InputDevice.Keyboard) && this.keys.size) return;
|
|
539
|
+
if (capture.devices.includes(InputDevice.Mouse) && this.mouseButtons) return;
|
|
540
|
+
if (capture.devices.includes(InputDevice.Gamepad) && [...this.pads.values()].some((pad) => (capture.gamepadIndex === void 0 || capture.gamepadIndex === pad.index) && pad.buttons.size)) return;
|
|
541
|
+
capture.arm();
|
|
542
|
+
}
|
|
543
|
+
keyDown(event) {
|
|
544
|
+
if (event.isComposing || event.keyCode === 229 || !isKeyCode(event.code)) return;
|
|
545
|
+
const previous = this.keys.has(event.code);
|
|
546
|
+
this.keys.add(event.code);
|
|
547
|
+
if (!(event.target instanceof Node) || !this.host.contains(event.target)) return;
|
|
548
|
+
if (blocksGameKeyboard(event.target)) return;
|
|
549
|
+
if (event.target instanceof HTMLElement && event.target.closest("button")) return;
|
|
550
|
+
const capture = this.current;
|
|
551
|
+
if (!capture && !this.blockedKeys.has(event.code)) return;
|
|
552
|
+
this.blockedKeys.add(event.code);
|
|
553
|
+
this.consume(event);
|
|
554
|
+
if (capture && !previous && !event.repeat) capture.accept({
|
|
555
|
+
device: InputDevice.Keyboard,
|
|
556
|
+
code: event.code
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
surface(target) {
|
|
560
|
+
return target === this.host || target instanceof HTMLCanvasElement && target.parentElement === this.host;
|
|
561
|
+
}
|
|
562
|
+
pointer(event) {
|
|
563
|
+
const capture = this.current, mouse = event.pointerType === "mouse", old = this.mouseButtons;
|
|
564
|
+
if (mouse) this.mouseButtons = event.type === "pointercancel" ? 0 : event.buttons;
|
|
565
|
+
if (event.type === "pointerdown" && !capture) {
|
|
566
|
+
this.blockedPointers.delete(event.pointerId);
|
|
567
|
+
if (isMouseButton(event.button)) this.blockedAuxiliary.delete(event.button);
|
|
568
|
+
}
|
|
569
|
+
const blocked = this.blockedPointers.has(event.pointerId);
|
|
570
|
+
if (capture && this.surface(event.target)) {
|
|
571
|
+
if (event.type === "pointerdown") this.host.focus({ preventScroll: true });
|
|
572
|
+
this.blockedPointers.add(event.pointerId);
|
|
573
|
+
this.consume(event);
|
|
574
|
+
if (mouse && isMouseButton(event.button)) {
|
|
575
|
+
const mask = mouseMasks$1.find(([button]) => button === event.button)?.[1] ?? 0;
|
|
576
|
+
if (event.buttons & mask && !(old & mask)) {
|
|
577
|
+
this.blockedAuxiliary.add(event.button);
|
|
578
|
+
capture.accept({
|
|
579
|
+
device: InputDevice.Mouse,
|
|
580
|
+
button: event.button
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
} else if (blocked) this.consume(event);
|
|
585
|
+
if (event.type === "pointerup" || event.type === "pointercancel" || mouse && event.buttons === 0) this.blockedPointers.delete(event.pointerId);
|
|
586
|
+
this.arm();
|
|
587
|
+
}
|
|
588
|
+
/** Same validated gamepad poll as gameplay; no navigator polling in this manager. */
|
|
589
|
+
sample(pads) {
|
|
590
|
+
const capture = this.current;
|
|
591
|
+
if (!capture?.devices.includes(InputDevice.Gamepad)) return;
|
|
592
|
+
const before = this.pads, first = this.needsPadSample, next = /* @__PURE__ */ new Map();
|
|
593
|
+
for (const pad of pads.values()) {
|
|
594
|
+
const buttons = /* @__PURE__ */ new Set();
|
|
595
|
+
pad.buttons.forEach((button, index) => {
|
|
596
|
+
if (Number.isFinite(button?.value) && button.value >= .5) buttons.add(index);
|
|
597
|
+
});
|
|
598
|
+
const signature = JSON.stringify([
|
|
599
|
+
pad.id,
|
|
600
|
+
pad.mapping,
|
|
601
|
+
pad.buttons.length
|
|
602
|
+
]);
|
|
603
|
+
const entry = {
|
|
604
|
+
index: pad.index,
|
|
605
|
+
id: pad.id,
|
|
606
|
+
mapping: pad.mapping,
|
|
607
|
+
signature,
|
|
608
|
+
buttons
|
|
609
|
+
};
|
|
610
|
+
next.set(pad.index, entry);
|
|
611
|
+
if (first || before.get(pad.index)?.signature !== signature) this.readyPads.delete(pad.index);
|
|
612
|
+
if (!buttons.size) this.readyPads.add(pad.index);
|
|
613
|
+
}
|
|
614
|
+
for (const index of this.readyPads) if (!next.has(index)) this.readyPads.delete(index);
|
|
615
|
+
this.pads = next;
|
|
616
|
+
this.needsPadSample = false;
|
|
617
|
+
this.arm();
|
|
618
|
+
if (first || capture.state !== InputCaptureState.Listening) return;
|
|
619
|
+
for (const pad of [...next.values()].sort((a, b) => a.index - b.index)) {
|
|
620
|
+
if (!this.readyPads.has(pad.index) || before.get(pad.index)?.signature !== pad.signature) continue;
|
|
621
|
+
for (const button of pad.buttons) if (!before.get(pad.index)?.buttons.has(button) && capture.accept({
|
|
622
|
+
device: InputDevice.Gamepad,
|
|
623
|
+
index: pad.index,
|
|
624
|
+
id: pad.id,
|
|
625
|
+
mapping: pad.mapping,
|
|
626
|
+
button
|
|
627
|
+
})) return;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
consume(event) {
|
|
631
|
+
event.preventDefault();
|
|
632
|
+
event.stopImmediatePropagation();
|
|
633
|
+
}
|
|
634
|
+
guard(action) {
|
|
635
|
+
try {
|
|
636
|
+
action();
|
|
637
|
+
} catch (error) {
|
|
638
|
+
this.report(error);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
interrupt() {
|
|
642
|
+
this.guard(() => this.current?.cancel());
|
|
643
|
+
this.keys.clear();
|
|
644
|
+
this.blockedKeys.clear();
|
|
645
|
+
this.blockedPointers.clear();
|
|
646
|
+
this.blockedAuxiliary.clear();
|
|
647
|
+
this.mouseButtons = 0;
|
|
648
|
+
}
|
|
649
|
+
dispose() {
|
|
650
|
+
if (this.dead) return;
|
|
651
|
+
this.dead = true;
|
|
652
|
+
this.abort.abort();
|
|
653
|
+
this.interrupt();
|
|
654
|
+
}
|
|
655
|
+
};
|
|
523
656
|
// packages/web/src/pointer.ts
|
|
657
|
+
var mouseMasks = [
|
|
658
|
+
[MouseButton.Left, 1],
|
|
659
|
+
[MouseButton.Middle, 4],
|
|
660
|
+
[MouseButton.Right, 2],
|
|
661
|
+
[MouseButton.Back, 8],
|
|
662
|
+
[MouseButton.Forward, 16]
|
|
663
|
+
];
|
|
524
664
|
/** One DOM capture-phase adapter per Game; Pixi federated events never route these widgets. */
|
|
525
665
|
var BrowserPointer = class {
|
|
526
666
|
host;
|
|
527
667
|
project;
|
|
528
668
|
report;
|
|
529
669
|
flush;
|
|
670
|
+
input;
|
|
530
671
|
routing;
|
|
531
672
|
abort = new AbortController();
|
|
532
|
-
|
|
673
|
+
mouse;
|
|
674
|
+
mouseStates = /* @__PURE__ */ new Map();
|
|
675
|
+
blockedMouse = /* @__PURE__ */ new Set();
|
|
676
|
+
auxiliary = /* @__PURE__ */ new Set();
|
|
677
|
+
offInput;
|
|
678
|
+
constructor(host, project, report, flush, input) {
|
|
533
679
|
this.host = host;
|
|
534
680
|
this.project = project;
|
|
535
681
|
this.report = report;
|
|
536
682
|
this.flush = flush;
|
|
683
|
+
this.input = input;
|
|
537
684
|
this.routing = new PointerRouting((id) => {
|
|
538
685
|
if (host.hasPointerCapture(id)) host.releasePointerCapture(id);
|
|
539
686
|
});
|
|
687
|
+
this.mouse = input?.createMouseSource();
|
|
688
|
+
this.offInput = input?.addAdapter({
|
|
689
|
+
sample: () => {},
|
|
690
|
+
reset: () => this.resetMouse(),
|
|
691
|
+
dispose: () => this.dispose()
|
|
692
|
+
});
|
|
540
693
|
const options = {
|
|
541
694
|
signal: this.abort.signal,
|
|
542
695
|
capture: true
|
|
543
696
|
};
|
|
544
697
|
host.addEventListener("pointerdown", (e) => this.guard(() => {
|
|
545
698
|
if (!this.surface(e.target)) return;
|
|
699
|
+
if (e.pointerType === "mouse") {
|
|
700
|
+
this.releaseMouse(e.pointerId);
|
|
701
|
+
this.blockedMouse.delete(e.pointerId);
|
|
702
|
+
if (isMouseButton(e.button)) this.auxiliary.delete(e.button);
|
|
703
|
+
}
|
|
546
704
|
this.flush();
|
|
547
705
|
const data = this.data(e);
|
|
548
706
|
try {
|
|
549
707
|
host.setPointerCapture(e.pointerId);
|
|
550
708
|
} catch {}
|
|
551
709
|
const target = this.routing.down(data);
|
|
710
|
+
this.updateMouse(e);
|
|
552
711
|
if (this.routing.has(e.pointerId)) {
|
|
553
712
|
e.preventDefault();
|
|
554
713
|
if (target) focusGameSurface(host);
|
|
@@ -570,31 +729,78 @@ var BrowserPointer = class {
|
|
|
570
729
|
});
|
|
571
730
|
window.addEventListener("pointermove", (e) => this.guard(() => {
|
|
572
731
|
if (!this.routing.owns(e.pointerId) && !this.surface(e.target)) {
|
|
732
|
+
if (e.pointerType === "mouse") this.releaseMouse(e.pointerId);
|
|
573
733
|
if (this.routing.hasHover(e.pointerId)) this.routing.leave(this.data(e));
|
|
574
734
|
return;
|
|
575
735
|
}
|
|
576
736
|
this.flush();
|
|
737
|
+
this.updateMouse(e);
|
|
577
738
|
this.routing.move(this.data(e));
|
|
578
739
|
}), options);
|
|
579
740
|
window.addEventListener("pointerup", (e) => this.guard(() => {
|
|
741
|
+
if (e.pointerType === "mouse") this.releaseMouse(e.pointerId);
|
|
580
742
|
if (this.routing.owns(e.pointerId)) {
|
|
581
743
|
e.preventDefault();
|
|
582
744
|
this.flush();
|
|
583
745
|
this.routing.up(this.data(e));
|
|
584
746
|
}
|
|
585
747
|
}), options);
|
|
586
|
-
window.addEventListener("pointercancel", (e) => this.guard(() =>
|
|
587
|
-
|
|
748
|
+
window.addEventListener("pointercancel", (e) => this.guard(() => {
|
|
749
|
+
this.releaseMouse(e.pointerId);
|
|
750
|
+
this.blockedMouse.delete(e.pointerId);
|
|
751
|
+
this.routing.cancel(e.pointerId, false);
|
|
752
|
+
}), options);
|
|
753
|
+
host.addEventListener("lostpointercapture", (e) => this.guard(() => {
|
|
754
|
+
this.releaseMouse(e.pointerId);
|
|
755
|
+
if (e.pointerType === "mouse" && e.buttons !== 0) this.blockedMouse.add(e.pointerId);
|
|
756
|
+
else this.blockedMouse.delete(e.pointerId);
|
|
757
|
+
this.routing.cancel(e.pointerId);
|
|
758
|
+
}), options);
|
|
588
759
|
host.addEventListener("pointerleave", (e) => this.guard(() => this.routing.leave(this.data(e))), options);
|
|
589
760
|
window.addEventListener("blur", (event) => {
|
|
590
761
|
if (event.target === window) this.release();
|
|
591
762
|
}, options);
|
|
592
763
|
host.addEventListener("focusout", (event) => this.guard(() => {
|
|
593
|
-
if (!(event.relatedTarget instanceof Node) || !host.contains(event.relatedTarget) || blocksGameKeyboard(event.relatedTarget))
|
|
764
|
+
if (!(event.relatedTarget instanceof Node) || !host.contains(event.relatedTarget) || blocksGameKeyboard(event.relatedTarget)) {
|
|
765
|
+
this.resetMouse();
|
|
766
|
+
this.routing.releaseVirtual();
|
|
767
|
+
}
|
|
594
768
|
}), options);
|
|
595
769
|
document.addEventListener("visibilitychange", () => {
|
|
596
770
|
if (document.hidden) this.release();
|
|
597
771
|
}, options);
|
|
772
|
+
for (const kind of ["auxclick", "contextmenu"]) host.addEventListener(kind, (event) => {
|
|
773
|
+
if (this.surface(event.target) && isMouseButton(event.button) && this.auxiliary.has(event.button) && input?.acceptsMouse(event.button)) {
|
|
774
|
+
event.preventDefault();
|
|
775
|
+
if (kind === "auxclick") this.auxiliary.delete(event.button);
|
|
776
|
+
}
|
|
777
|
+
}, options);
|
|
778
|
+
}
|
|
779
|
+
applyMouse() {
|
|
780
|
+
let buttons = 0;
|
|
781
|
+
for (const state of this.mouseStates.values()) buttons |= state;
|
|
782
|
+
for (const [button, mask] of mouseMasks) this.mouse?.set(button, (buttons & mask) !== 0);
|
|
783
|
+
}
|
|
784
|
+
releaseMouse(id) {
|
|
785
|
+
if (this.mouseStates.delete(id)) this.applyMouse();
|
|
786
|
+
}
|
|
787
|
+
updateMouse(event) {
|
|
788
|
+
if (event.pointerType !== "mouse" || !this.mouse) return;
|
|
789
|
+
if (event.buttons === 0) this.blockedMouse.delete(event.pointerId);
|
|
790
|
+
const world = this.routing.worldOwns(event.pointerId) && !this.blockedMouse.has(event.pointerId) && this.input?.liveInput;
|
|
791
|
+
if (world) this.mouseStates.set(event.pointerId, event.buttons);
|
|
792
|
+
else this.mouseStates.delete(event.pointerId);
|
|
793
|
+
if (isMouseButton(event.button)) {
|
|
794
|
+
if (world && this.input?.acceptsMouse(event.button)) this.auxiliary.add(event.button);
|
|
795
|
+
else this.auxiliary.delete(event.button);
|
|
796
|
+
}
|
|
797
|
+
this.applyMouse();
|
|
798
|
+
}
|
|
799
|
+
resetMouse() {
|
|
800
|
+
for (const id of this.mouseStates.keys()) this.blockedMouse.add(id);
|
|
801
|
+
this.mouseStates.clear();
|
|
802
|
+
this.auxiliary.clear();
|
|
803
|
+
this.mouse?.clear();
|
|
598
804
|
}
|
|
599
805
|
surface(target) {
|
|
600
806
|
return target === this.host || target instanceof HTMLCanvasElement && target.parentElement === this.host;
|
|
@@ -620,10 +826,15 @@ var BrowserPointer = class {
|
|
|
620
826
|
}
|
|
621
827
|
}
|
|
622
828
|
release() {
|
|
829
|
+
this.resetMouse();
|
|
623
830
|
this.guard(() => this.routing.release());
|
|
624
831
|
}
|
|
625
832
|
dispose() {
|
|
626
833
|
this.abort.abort();
|
|
834
|
+
this.offInput?.();
|
|
835
|
+
this.resetMouse();
|
|
836
|
+
this.mouse?.dispose();
|
|
837
|
+
this.blockedMouse.clear();
|
|
627
838
|
this.guard(() => this.routing.dispose());
|
|
628
839
|
}
|
|
629
840
|
};
|
|
@@ -2912,6 +3123,11 @@ var Game = class {
|
|
|
2912
3123
|
gamepads;
|
|
2913
3124
|
renderer;
|
|
2914
3125
|
keyboard;
|
|
3126
|
+
captures;
|
|
3127
|
+
captureInput(options = {}) {
|
|
3128
|
+
this.assertAlive();
|
|
3129
|
+
return this.captures.start(options);
|
|
3130
|
+
}
|
|
2915
3131
|
pointers;
|
|
2916
3132
|
pointerChannels = /* @__PURE__ */ new WeakMap();
|
|
2917
3133
|
virtualInputs = /* @__PURE__ */ new WeakMap();
|
|
@@ -3054,8 +3270,12 @@ var Game = class {
|
|
|
3054
3270
|
request: (callback) => requestAnimationFrame(callback),
|
|
3055
3271
|
cancel: (handle) => cancelAnimationFrame(handle)
|
|
3056
3272
|
};
|
|
3273
|
+
this.captures = new InputCaptureManager(this.element, this.engine.input, () => this.engine.scene, () => this.pointers.release(), this.report);
|
|
3057
3274
|
this.keyboard = new BrowserInput(this.element, this.engine.input);
|
|
3058
|
-
this.gamepads = new GamepadInput(this.engine.input, {
|
|
3275
|
+
this.gamepads = new GamepadInput(this.engine.input, {
|
|
3276
|
+
observe: (pads) => this.captures.sample(pads),
|
|
3277
|
+
active: () => !document.hidden && document.hasFocus() && this.element.contains(document.activeElement) && !blocksGameKeyboard(document.activeElement)
|
|
3278
|
+
});
|
|
3059
3279
|
this.pointers = new BrowserPointer(this.element, (event) => {
|
|
3060
3280
|
const rect = this.element.getBoundingClientRect();
|
|
3061
3281
|
if (rect.width <= 0 || rect.height <= 0) throw new EngineError("VIEWPORT_UNAVAILABLE", "Game has no visible bounds");
|
|
@@ -3065,7 +3285,7 @@ var Game = class {
|
|
|
3065
3285
|
world: this.screenToWorld(screen),
|
|
3066
3286
|
view: this.view
|
|
3067
3287
|
};
|
|
3068
|
-
}, this.report, () => this.updateProjection());
|
|
3288
|
+
}, this.report, () => this.updateProjection(), this.engine.input);
|
|
3069
3289
|
document.addEventListener("visibilitychange", () => {
|
|
3070
3290
|
this.previousTime = void 0;
|
|
3071
3291
|
this.fixed?.reset();
|
|
@@ -3544,6 +3764,7 @@ var Game = class {
|
|
|
3544
3764
|
for (const cleanup of [
|
|
3545
3765
|
() => this.stopReplay(),
|
|
3546
3766
|
() => this.staging?.dispose(),
|
|
3767
|
+
() => this.captures.dispose(),
|
|
3547
3768
|
() => this.pointers.dispose(),
|
|
3548
3769
|
() => this.gamepads.dispose(),
|
|
3549
3770
|
() => this.keyboard.dispose(),
|