@simpludify/web 0.1.4 → 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 +10 -1
- package/dist/gamepad-bindings.d.ts +2 -100
- package/dist/gamepads.d.ts +11 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +559 -142
- package/dist/index.js.map +1 -1
- package/dist/input-capture.d.ts +33 -0
- package/dist/pointer.d.ts +12 -2
- package/dist/renderer.d.ts +9 -1
- package/dist/virtual-input-visual.d.ts +22 -0
- 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, 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,28 +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);
|
|
763
|
+
host.addEventListener("focusout", (event) => this.guard(() => {
|
|
764
|
+
if (!(event.relatedTarget instanceof Node) || !host.contains(event.relatedTarget) || blocksGameKeyboard(event.relatedTarget)) {
|
|
765
|
+
this.resetMouse();
|
|
766
|
+
this.routing.releaseVirtual();
|
|
767
|
+
}
|
|
768
|
+
}), options);
|
|
592
769
|
document.addEventListener("visibilitychange", () => {
|
|
593
770
|
if (document.hidden) this.release();
|
|
594
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();
|
|
595
804
|
}
|
|
596
805
|
surface(target) {
|
|
597
806
|
return target === this.host || target instanceof HTMLCanvasElement && target.parentElement === this.host;
|
|
@@ -617,10 +826,15 @@ var BrowserPointer = class {
|
|
|
617
826
|
}
|
|
618
827
|
}
|
|
619
828
|
release() {
|
|
829
|
+
this.resetMouse();
|
|
620
830
|
this.guard(() => this.routing.release());
|
|
621
831
|
}
|
|
622
832
|
dispose() {
|
|
623
833
|
this.abort.abort();
|
|
834
|
+
this.offInput?.();
|
|
835
|
+
this.resetMouse();
|
|
836
|
+
this.mouse?.dispose();
|
|
837
|
+
this.blockedMouse.clear();
|
|
624
838
|
this.guard(() => this.routing.dispose());
|
|
625
839
|
}
|
|
626
840
|
};
|
|
@@ -1907,6 +2121,108 @@ var TilemapVisual = class {
|
|
|
1907
2121
|
], "Tilemap presentation cleanup failed");
|
|
1908
2122
|
}
|
|
1909
2123
|
};
|
|
2124
|
+
// packages/web/src/virtual-input-visual.ts
|
|
2125
|
+
/** Screen-space presentation only. The existing BrowserPointer is the sole event adapter. */
|
|
2126
|
+
var VirtualInputVisual = class {
|
|
2127
|
+
input;
|
|
2128
|
+
release;
|
|
2129
|
+
display = new Container();
|
|
2130
|
+
entries = /* @__PURE__ */ new Map();
|
|
2131
|
+
dead = false;
|
|
2132
|
+
off;
|
|
2133
|
+
constructor(input, release) {
|
|
2134
|
+
this.input = input;
|
|
2135
|
+
this.release = release;
|
|
2136
|
+
this.display.eventMode = "none";
|
|
2137
|
+
this.display.sortableChildren = true;
|
|
2138
|
+
this.display.zIndex = .5;
|
|
2139
|
+
this.off = input.onDispose(() => this.dispose());
|
|
2140
|
+
}
|
|
2141
|
+
get diagnostics() {
|
|
2142
|
+
return {
|
|
2143
|
+
controls: this.entries.size,
|
|
2144
|
+
visible: [...this.entries.values()].filter((e) => e.graphics.visible).length
|
|
2145
|
+
};
|
|
2146
|
+
}
|
|
2147
|
+
update() {
|
|
2148
|
+
if (this.dead) return;
|
|
2149
|
+
const live = new Set(this.input.controls);
|
|
2150
|
+
for (const [control, entry] of this.entries) if (!live.has(control)) {
|
|
2151
|
+
this.entries.delete(control);
|
|
2152
|
+
this.release(entry.graphics);
|
|
2153
|
+
}
|
|
2154
|
+
this.display.visible = this.input.visible;
|
|
2155
|
+
for (const control of live) {
|
|
2156
|
+
let entry = this.entries.get(control);
|
|
2157
|
+
if (!entry) {
|
|
2158
|
+
entry = {
|
|
2159
|
+
graphics: new Graphics(),
|
|
2160
|
+
signature: ""
|
|
2161
|
+
};
|
|
2162
|
+
entry.graphics.zIndex = control.kind === "button" ? 1 : 0;
|
|
2163
|
+
this.entries.set(control, entry);
|
|
2164
|
+
this.display.addChild(entry.graphics);
|
|
2165
|
+
}
|
|
2166
|
+
entry.graphics.visible = control.visible;
|
|
2167
|
+
if (!control.visible || !this.input.visible) continue;
|
|
2168
|
+
const center = control.center, knob = control.knob, radius = control.radius;
|
|
2169
|
+
const signature = JSON.stringify([
|
|
2170
|
+
center,
|
|
2171
|
+
knob,
|
|
2172
|
+
radius,
|
|
2173
|
+
control.color,
|
|
2174
|
+
control.enabled,
|
|
2175
|
+
this.input.enabled,
|
|
2176
|
+
control.pressed,
|
|
2177
|
+
control.kind
|
|
2178
|
+
]);
|
|
2179
|
+
if (signature === entry.signature) continue;
|
|
2180
|
+
entry.signature = signature;
|
|
2181
|
+
const g = entry.graphics;
|
|
2182
|
+
g.clear();
|
|
2183
|
+
g.alpha = control.enabled && this.input.enabled ? 1 : .35;
|
|
2184
|
+
g.circle(center.x, center.y, radius).fill({
|
|
2185
|
+
color: control.color,
|
|
2186
|
+
alpha: .12
|
|
2187
|
+
}).stroke({
|
|
2188
|
+
color: control.color,
|
|
2189
|
+
alpha: .75,
|
|
2190
|
+
width: 2
|
|
2191
|
+
});
|
|
2192
|
+
if (control.kind === "stick") {
|
|
2193
|
+
g.moveTo(center.x, center.y).lineTo(knob.x, knob.y).stroke({
|
|
2194
|
+
color: control.color,
|
|
2195
|
+
alpha: .5,
|
|
2196
|
+
width: 3
|
|
2197
|
+
});
|
|
2198
|
+
g.circle(knob.x, knob.y, radius * .36).fill({
|
|
2199
|
+
color: control.color,
|
|
2200
|
+
alpha: control.pressed ? .85 : .45
|
|
2201
|
+
});
|
|
2202
|
+
} else {
|
|
2203
|
+
if (control.pressed) g.circle(center.x, center.y, radius * .7).fill({
|
|
2204
|
+
color: control.color,
|
|
2205
|
+
alpha: .65
|
|
2206
|
+
});
|
|
2207
|
+
if (control.directional && control.pressed) g.moveTo(center.x, center.y).lineTo(knob.x, knob.y).stroke({
|
|
2208
|
+
color: control.color,
|
|
2209
|
+
width: 4
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
dispose() {
|
|
2215
|
+
if (this.dead) return;
|
|
2216
|
+
this.dead = true;
|
|
2217
|
+
const entries = [...this.entries.values()];
|
|
2218
|
+
this.entries.clear();
|
|
2219
|
+
cleanupAll([
|
|
2220
|
+
this.off,
|
|
2221
|
+
...entries.map((e) => () => this.release(e.graphics)),
|
|
2222
|
+
() => this.display.destroy({ children: true })
|
|
2223
|
+
], "Virtual input presentation cleanup failed");
|
|
2224
|
+
}
|
|
2225
|
+
};
|
|
1910
2226
|
// packages/web/src/renderer.ts
|
|
1911
2227
|
var PixiRenderer = class {
|
|
1912
2228
|
host;
|
|
@@ -1915,6 +2231,46 @@ var PixiRenderer = class {
|
|
|
1915
2231
|
app = new Application();
|
|
1916
2232
|
pendingUITextKeys = /* @__PURE__ */ new Set();
|
|
1917
2233
|
canvasPresentations = /* @__PURE__ */ new Set();
|
|
2234
|
+
virtualPresentations = /* @__PURE__ */ new Set();
|
|
2235
|
+
get virtualInputDiagnostics() {
|
|
2236
|
+
let controls = 0, visible = 0;
|
|
2237
|
+
for (const visual of this.virtualPresentations) {
|
|
2238
|
+
controls += visual.diagnostics.controls;
|
|
2239
|
+
visible += visual.diagnostics.visible;
|
|
2240
|
+
}
|
|
2241
|
+
return {
|
|
2242
|
+
presentations: this.virtualPresentations.size,
|
|
2243
|
+
controls,
|
|
2244
|
+
visible
|
|
2245
|
+
};
|
|
2246
|
+
}
|
|
2247
|
+
prepareVirtualInput(input) {
|
|
2248
|
+
if (!this.ready || this.dead) throw new EngineError("RENDERER_NOT_READY", "Renderer is not available");
|
|
2249
|
+
const visual = new VirtualInputVisual(input, (graphics) => {
|
|
2250
|
+
const context = graphics.context.uid;
|
|
2251
|
+
graphics.destroy({ context: true });
|
|
2252
|
+
releaseUIGraphics(this.app.renderer, graphics, context);
|
|
2253
|
+
});
|
|
2254
|
+
this.virtualPresentations.add(visual);
|
|
2255
|
+
const off = input.onDispose(() => {
|
|
2256
|
+
this.virtualPresentations.delete(visual);
|
|
2257
|
+
});
|
|
2258
|
+
let disposed = false;
|
|
2259
|
+
return {
|
|
2260
|
+
update: () => {
|
|
2261
|
+
if (disposed || input.disposed) return;
|
|
2262
|
+
if (!visual.display.parent) this.app.stage.addChild(visual.display);
|
|
2263
|
+
visual.update();
|
|
2264
|
+
},
|
|
2265
|
+
dispose: () => {
|
|
2266
|
+
if (disposed) return;
|
|
2267
|
+
disposed = true;
|
|
2268
|
+
off();
|
|
2269
|
+
this.virtualPresentations.delete(visual);
|
|
2270
|
+
visual.dispose();
|
|
2271
|
+
}
|
|
2272
|
+
};
|
|
2273
|
+
}
|
|
1918
2274
|
get canvasDiagnostics() {
|
|
1919
2275
|
let nodes = 0, visibleNodes = 0, buttons = 0, syncs = 0, textMetrics = 0;
|
|
1920
2276
|
for (const p of this.canvasPresentations) {
|
|
@@ -2508,6 +2864,10 @@ var PixiRenderer = class {
|
|
|
2508
2864
|
this.scenes.clear();
|
|
2509
2865
|
cleanupAll([
|
|
2510
2866
|
...scenes.map((scene) => scene.dispose),
|
|
2867
|
+
...[...this.virtualPresentations].map((p) => () => {
|
|
2868
|
+
this.virtualPresentations.delete(p);
|
|
2869
|
+
p.dispose();
|
|
2870
|
+
}),
|
|
2511
2871
|
...[...this.canvasPresentations].map((p) => () => {
|
|
2512
2872
|
this.canvasPresentations.delete(p);
|
|
2513
2873
|
p.dispose();
|
|
@@ -2763,8 +3123,44 @@ var Game = class {
|
|
|
2763
3123
|
gamepads;
|
|
2764
3124
|
renderer;
|
|
2765
3125
|
keyboard;
|
|
3126
|
+
captures;
|
|
3127
|
+
captureInput(options = {}) {
|
|
3128
|
+
this.assertAlive();
|
|
3129
|
+
return this.captures.start(options);
|
|
3130
|
+
}
|
|
2766
3131
|
pointers;
|
|
2767
3132
|
pointerChannels = /* @__PURE__ */ new WeakMap();
|
|
3133
|
+
virtualInputs = /* @__PURE__ */ new WeakMap();
|
|
3134
|
+
virtualPresentation;
|
|
3135
|
+
get virtualInput() {
|
|
3136
|
+
this.assertAlive();
|
|
3137
|
+
return this.virtualFor(this.engine.scene);
|
|
3138
|
+
}
|
|
3139
|
+
virtualFor(scene) {
|
|
3140
|
+
let input = this.virtualInputs.get(scene);
|
|
3141
|
+
if (!input) {
|
|
3142
|
+
input = new VirtualInput(this.channelFor(scene), this.engine.input);
|
|
3143
|
+
this.virtualInputs.set(scene, input);
|
|
3144
|
+
const owned = input;
|
|
3145
|
+
input.onDispose(() => {
|
|
3146
|
+
if (this.virtualInputs.get(scene) === owned) this.virtualInputs.delete(scene);
|
|
3147
|
+
if (this.engine.scene === scene) {
|
|
3148
|
+
this.virtualPresentation?.dispose();
|
|
3149
|
+
this.virtualPresentation = void 0;
|
|
3150
|
+
}
|
|
3151
|
+
});
|
|
3152
|
+
}
|
|
3153
|
+
return input;
|
|
3154
|
+
}
|
|
3155
|
+
prepareVirtual(scene) {
|
|
3156
|
+
const input = this.virtualInputs.get(scene);
|
|
3157
|
+
if (!input || input.controls.length === 0) return;
|
|
3158
|
+
const renderer = this.renderer;
|
|
3159
|
+
if (!renderer.prepareVirtualInput) throw new EngineError("RENDERER_CAPABILITY", "Renderer does not support virtual input presentation");
|
|
3160
|
+
const presentation = renderer.prepareVirtualInput(input);
|
|
3161
|
+
input.onDispose(() => presentation.dispose());
|
|
3162
|
+
return presentation;
|
|
3163
|
+
}
|
|
2768
3164
|
canvas;
|
|
2769
3165
|
canvasPresentation;
|
|
2770
3166
|
defaultCanvas;
|
|
@@ -2874,8 +3270,12 @@ var Game = class {
|
|
|
2874
3270
|
request: (callback) => requestAnimationFrame(callback),
|
|
2875
3271
|
cancel: (handle) => cancelAnimationFrame(handle)
|
|
2876
3272
|
};
|
|
3273
|
+
this.captures = new InputCaptureManager(this.element, this.engine.input, () => this.engine.scene, () => this.pointers.release(), this.report);
|
|
2877
3274
|
this.keyboard = new BrowserInput(this.element, this.engine.input);
|
|
2878
|
-
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
|
+
});
|
|
2879
3279
|
this.pointers = new BrowserPointer(this.element, (event) => {
|
|
2880
3280
|
const rect = this.element.getBoundingClientRect();
|
|
2881
3281
|
if (rect.width <= 0 || rect.height <= 0) throw new EngineError("VIEWPORT_UNAVAILABLE", "Game has no visible bounds");
|
|
@@ -2885,7 +3285,7 @@ var Game = class {
|
|
|
2885
3285
|
world: this.screenToWorld(screen),
|
|
2886
3286
|
view: this.view
|
|
2887
3287
|
};
|
|
2888
|
-
}, this.report, () => this.
|
|
3288
|
+
}, this.report, () => this.updateProjection(), this.engine.input);
|
|
2889
3289
|
document.addEventListener("visibilitychange", () => {
|
|
2890
3290
|
this.previousTime = void 0;
|
|
2891
3291
|
this.fixed?.reset();
|
|
@@ -3040,6 +3440,7 @@ var Game = class {
|
|
|
3040
3440
|
const scene = new Scene(id), nodes = [], controller = new AbortController();
|
|
3041
3441
|
let canvas;
|
|
3042
3442
|
const pointer = this.channelFor(scene);
|
|
3443
|
+
const virtualFor = () => this.virtualFor(scene);
|
|
3043
3444
|
this.staging = scene;
|
|
3044
3445
|
scene.onDispose(() => controller.abort());
|
|
3045
3446
|
try {
|
|
@@ -3055,6 +3456,9 @@ var Game = class {
|
|
|
3055
3456
|
add: (actor) => scene.add(actor),
|
|
3056
3457
|
ui: (...items) => nodes.push(...items),
|
|
3057
3458
|
pointer,
|
|
3459
|
+
get virtualInput() {
|
|
3460
|
+
return virtualFor();
|
|
3461
|
+
},
|
|
3058
3462
|
canvasUI: (root, options) => {
|
|
3059
3463
|
if (canvas) throw new EngineError("UI_OWNERSHIP", "A Scene has one Canvas UI root");
|
|
3060
3464
|
canvas = new CanvasUI(scene, root, options);
|
|
@@ -3093,6 +3497,7 @@ var Game = class {
|
|
|
3093
3497
|
async present(scene, nodes, canvas) {
|
|
3094
3498
|
let prepared, ui, committed = false;
|
|
3095
3499
|
let canvasPresentation;
|
|
3500
|
+
let virtualPresentation;
|
|
3096
3501
|
try {
|
|
3097
3502
|
const preparing = this.renderer.prepare(scene);
|
|
3098
3503
|
preparing.then((value) => {
|
|
@@ -3113,6 +3518,7 @@ var Game = class {
|
|
|
3113
3518
|
canvasPresentation = renderer.prepareCanvasUI(canvas, this.pointers.routing);
|
|
3114
3519
|
scene.onDispose(() => canvasPresentation?.dispose());
|
|
3115
3520
|
}
|
|
3521
|
+
virtualPresentation = this.prepareVirtual(scene);
|
|
3116
3522
|
scene.markReady();
|
|
3117
3523
|
this.errorBox.hidden = true;
|
|
3118
3524
|
try {
|
|
@@ -3123,18 +3529,20 @@ var Game = class {
|
|
|
3123
3529
|
this.report(error);
|
|
3124
3530
|
}
|
|
3125
3531
|
this.assertAlive();
|
|
3126
|
-
const oldPrepared = this.prepared, oldUI = this.ui, oldCanvas = this.canvasPresentation;
|
|
3532
|
+
const oldPrepared = this.prepared, oldUI = this.ui, oldCanvas = this.canvasPresentation, oldVirtual = this.virtualPresentation;
|
|
3127
3533
|
this.prepared = prepared;
|
|
3128
3534
|
this.ui = ui;
|
|
3129
3535
|
this.canvas = canvas;
|
|
3130
3536
|
this.canvasPresentation = canvasPresentation;
|
|
3537
|
+
this.virtualPresentation = virtualPresentation;
|
|
3131
3538
|
this.pointers.routing.setScene(this.channelFor(scene), canvas);
|
|
3132
3539
|
committed = true;
|
|
3133
3540
|
this.element.append(ui.element);
|
|
3134
3541
|
for (const cleanup of [
|
|
3135
3542
|
() => oldPrepared?.dispose(),
|
|
3136
3543
|
() => oldUI?.dispose(),
|
|
3137
|
-
() => oldCanvas?.dispose()
|
|
3544
|
+
() => oldCanvas?.dispose(),
|
|
3545
|
+
() => oldVirtual?.dispose()
|
|
3138
3546
|
]) try {
|
|
3139
3547
|
cleanup();
|
|
3140
3548
|
} catch (error) {
|
|
@@ -3150,7 +3558,8 @@ var Game = class {
|
|
|
3150
3558
|
if (!committed) for (const cleanup of [
|
|
3151
3559
|
() => prepared?.dispose(),
|
|
3152
3560
|
() => ui?.dispose(),
|
|
3153
|
-
() => canvasPresentation?.dispose()
|
|
3561
|
+
() => canvasPresentation?.dispose(),
|
|
3562
|
+
() => virtualPresentation?.dispose()
|
|
3154
3563
|
]) try {
|
|
3155
3564
|
cleanup();
|
|
3156
3565
|
} catch (cleanupError) {
|
|
@@ -3262,6 +3671,7 @@ var Game = class {
|
|
|
3262
3671
|
width: this.width,
|
|
3263
3672
|
height: this.height
|
|
3264
3673
|
};
|
|
3674
|
+
this.virtualInputs.get(this.engine.scene)?.refresh(viewport);
|
|
3265
3675
|
const previous = this.projection.viewport;
|
|
3266
3676
|
const c = this.camera;
|
|
3267
3677
|
if (!this.projectionDirty && previous.x === viewport.x && previous.y === viewport.y && previous.width === viewport.width && previous.height === viewport.height && this.cameraX === c.x && this.cameraY === c.y && this.cameraZoom === c.zoom) return;
|
|
@@ -3306,6 +3716,8 @@ var Game = class {
|
|
|
3306
3716
|
}
|
|
3307
3717
|
render() {
|
|
3308
3718
|
this.updateProjection();
|
|
3719
|
+
this.virtualPresentation ??= this.prepareVirtual(this.engine.scene);
|
|
3720
|
+
this.virtualPresentation?.update();
|
|
3309
3721
|
this.prepared?.render(this.projection);
|
|
3310
3722
|
}
|
|
3311
3723
|
tick = (time) => {
|
|
@@ -3319,12 +3731,14 @@ var Game = class {
|
|
|
3319
3731
|
} else {
|
|
3320
3732
|
const elapsed = this.previousTime === void 0 ? 0 : Math.max(0, (time - this.previousTime) / 1e3);
|
|
3321
3733
|
this.previousTime = time;
|
|
3734
|
+
if (this.virtualInputs.has(this.engine.scene)) this.updateProjection();
|
|
3322
3735
|
if (this.playback) this.playback.advance(elapsed, this.options.fixedStep?.maxSteps ?? 8);
|
|
3323
3736
|
else if (this.fixed) this.fixed.advance(elapsed);
|
|
3324
3737
|
else this.engine.step(Math.min(this.maxDelta, elapsed));
|
|
3325
3738
|
if (!this.dead && !this.engine.scene.disposed) this.engine.scene.advancePresentation(Math.min(this.maxDelta, elapsed));
|
|
3326
3739
|
if (this.engine.paused) this.canvas?.stopMotion();
|
|
3327
3740
|
else this.canvas?.advance(elapsed);
|
|
3741
|
+
this.virtualInputs.get(this.engine.scene)?.advance(Math.min(this.maxDelta, elapsed));
|
|
3328
3742
|
if (this.ratio !== (this.options.pixelRatio ?? Math.min(window.devicePixelRatio || 1, 2))) this.resize();
|
|
3329
3743
|
this.render();
|
|
3330
3744
|
}
|
|
@@ -3350,10 +3764,12 @@ var Game = class {
|
|
|
3350
3764
|
for (const cleanup of [
|
|
3351
3765
|
() => this.stopReplay(),
|
|
3352
3766
|
() => this.staging?.dispose(),
|
|
3767
|
+
() => this.captures.dispose(),
|
|
3353
3768
|
() => this.pointers.dispose(),
|
|
3354
3769
|
() => this.gamepads.dispose(),
|
|
3355
3770
|
() => this.keyboard.dispose(),
|
|
3356
3771
|
() => this.canvasPresentation?.dispose(),
|
|
3772
|
+
() => this.virtualPresentation?.dispose(),
|
|
3357
3773
|
() => this.canvas?.dispose(),
|
|
3358
3774
|
() => this.observer.disconnect(),
|
|
3359
3775
|
() => this.ui?.dispose(),
|
|
@@ -3371,6 +3787,7 @@ var Game = class {
|
|
|
3371
3787
|
this.staging = void 0;
|
|
3372
3788
|
this.canvas = void 0;
|
|
3373
3789
|
this.canvasPresentation = void 0;
|
|
3790
|
+
this.virtualPresentation = void 0;
|
|
3374
3791
|
this.defaultCanvas = void 0;
|
|
3375
3792
|
this.defaultUI.length = 0;
|
|
3376
3793
|
this.scenes.clear();
|