@simpludify/core 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/foundation/version.d.ts +1 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +1316 -36
- package/dist/index.js.map +1 -1
- package/dist/interaction/action-bindings.d.ts +18 -2
- package/dist/interaction/actions.d.ts +42 -8
- package/dist/interaction/gamepad-bindings.d.ts +100 -0
- package/dist/interaction/input-capture.d.ts +38 -0
- package/dist/interaction/input.d.ts +22 -0
- package/dist/interaction/physical-input.d.ts +32 -0
- package/dist/interaction/pointer-routing.d.ts +11 -0
- package/dist/interaction/pointer-target.d.ts +14 -0
- package/dist/interaction/virtual-control.d.ts +140 -0
- package/dist/interaction/virtual-input.d.ts +49 -0
- package/dist/interaction/virtual-regions.d.ts +68 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1583,7 +1583,7 @@ var RandomManager = class {
|
|
|
1583
1583
|
};
|
|
1584
1584
|
// packages/core/src/foundation/version.ts
|
|
1585
1585
|
/** Source workspace version; checked against package.json by the build validator. */
|
|
1586
|
-
var ENGINE_VERSION = "0.1.
|
|
1586
|
+
var ENGINE_VERSION = "0.1.6";
|
|
1587
1587
|
// packages/core/src/scene/actor.ts
|
|
1588
1588
|
var inputPhase$1 = ComponentPhase.Input;
|
|
1589
1589
|
var updatePhase$1 = ComponentPhase.Update;
|
|
@@ -2814,7 +2814,7 @@ function boxCircle(box, circle, out, tolerance) {
|
|
|
2814
2814
|
out.normalY = nx * box.sin + ny * box.cos;
|
|
2815
2815
|
return true;
|
|
2816
2816
|
}
|
|
2817
|
-
function axis(a, b, x, y, out, tolerance) {
|
|
2817
|
+
function axis$1(a, b, x, y, out, tolerance) {
|
|
2818
2818
|
const distance = (b.x - a.x) * x + (b.y - a.y) * y;
|
|
2819
2819
|
const depth = a.hx * Math.abs(a.cos * x + a.sin * y) + a.hy * Math.abs(-a.sin * x + a.cos * y) + (b.hx * Math.abs(b.cos * x + b.sin * y) + b.hy * Math.abs(-b.sin * x + b.cos * y)) - Math.abs(distance);
|
|
2820
2820
|
if (depth < -tolerance) return false;
|
|
@@ -2845,7 +2845,7 @@ function contactManifold(a, b, out, tolerance = 0) {
|
|
|
2845
2845
|
}
|
|
2846
2846
|
if (b.collider.shape === CollisionShape.Circle) return boxCircle(ag, bg, out, tolerance);
|
|
2847
2847
|
out.depth = Infinity;
|
|
2848
|
-
return axis(ag, bg, ag.cos, ag.sin, out, tolerance) && axis(ag, bg, -ag.sin, ag.cos, out, tolerance) && axis(ag, bg, bg.cos, bg.sin, out, tolerance) && axis(ag, bg, -bg.sin, bg.cos, out, tolerance);
|
|
2848
|
+
return axis$1(ag, bg, ag.cos, ag.sin, out, tolerance) && axis$1(ag, bg, -ag.sin, ag.cos, out, tolerance) && axis$1(ag, bg, bg.cos, bg.sin, out, tolerance) && axis$1(ag, bg, -bg.sin, bg.cos, out, tolerance);
|
|
2849
2849
|
}
|
|
2850
2850
|
// packages/core/src/game/collision-response.ts
|
|
2851
2851
|
/** Translate along the minimum separation axis, preserving tangential movement intent. */
|
|
@@ -4080,6 +4080,135 @@ var TileGeometry = class TileGeometry {
|
|
|
4080
4080
|
for (let dy = -radius; dy <= radius; dy++) for (let dx = Math.max(-radius, -dy - radius); dx <= Math.min(radius, -dy + radius); dx++) yield tile(x + dx, y + dy);
|
|
4081
4081
|
}
|
|
4082
4082
|
};
|
|
4083
|
+
// packages/core/src/interaction/gamepad-bindings.ts
|
|
4084
|
+
/** Standard mapping physical positions, not product-specific printed labels. */
|
|
4085
|
+
var GamepadButton = Object.freeze({
|
|
4086
|
+
South: 0,
|
|
4087
|
+
East: 1,
|
|
4088
|
+
West: 2,
|
|
4089
|
+
North: 3,
|
|
4090
|
+
LeftShoulder: 4,
|
|
4091
|
+
RightShoulder: 5,
|
|
4092
|
+
LeftTrigger: 6,
|
|
4093
|
+
RightTrigger: 7,
|
|
4094
|
+
Select: 8,
|
|
4095
|
+
Start: 9,
|
|
4096
|
+
LeftStick: 10,
|
|
4097
|
+
RightStick: 11,
|
|
4098
|
+
DPadUp: 12,
|
|
4099
|
+
DPadDown: 13,
|
|
4100
|
+
DPadLeft: 14,
|
|
4101
|
+
DPadRight: 15,
|
|
4102
|
+
Home: 16
|
|
4103
|
+
});
|
|
4104
|
+
var GamepadAxis = Object.freeze({
|
|
4105
|
+
LeftX: 0,
|
|
4106
|
+
LeftY: 1,
|
|
4107
|
+
RightX: 2,
|
|
4108
|
+
RightY: 3
|
|
4109
|
+
});
|
|
4110
|
+
var axis = (index) => Object.freeze({ axis: index });
|
|
4111
|
+
/** Ready-to-use data; game Actions retain application-defined names. */
|
|
4112
|
+
var GamepadBindings = Object.freeze({
|
|
4113
|
+
LeftStick: Object.freeze({
|
|
4114
|
+
x: axis(0),
|
|
4115
|
+
y: axis(1)
|
|
4116
|
+
}),
|
|
4117
|
+
RightStick: Object.freeze({
|
|
4118
|
+
x: axis(2),
|
|
4119
|
+
y: axis(3)
|
|
4120
|
+
}),
|
|
4121
|
+
DPad: Object.freeze({
|
|
4122
|
+
x: Object.freeze({
|
|
4123
|
+
negative: Object.freeze([14]),
|
|
4124
|
+
positive: Object.freeze([15])
|
|
4125
|
+
}),
|
|
4126
|
+
y: Object.freeze({
|
|
4127
|
+
negative: Object.freeze([12]),
|
|
4128
|
+
positive: Object.freeze([13])
|
|
4129
|
+
})
|
|
4130
|
+
}),
|
|
4131
|
+
Move: Object.freeze({
|
|
4132
|
+
x: Object.freeze({
|
|
4133
|
+
axis: 0,
|
|
4134
|
+
negative: Object.freeze([14]),
|
|
4135
|
+
positive: Object.freeze([15])
|
|
4136
|
+
}),
|
|
4137
|
+
y: Object.freeze({
|
|
4138
|
+
axis: 1,
|
|
4139
|
+
negative: Object.freeze([12]),
|
|
4140
|
+
positive: Object.freeze([13])
|
|
4141
|
+
})
|
|
4142
|
+
})
|
|
4143
|
+
});
|
|
4144
|
+
var fail = () => {
|
|
4145
|
+
throw new EngineError("INVALID_GAMEPAD_BINDING", "Invalid gamepad binding");
|
|
4146
|
+
};
|
|
4147
|
+
function gamepadIndex(value) {
|
|
4148
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) return fail();
|
|
4149
|
+
return value;
|
|
4150
|
+
}
|
|
4151
|
+
function gamepadDeadZone(value) {
|
|
4152
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value >= 1) return fail();
|
|
4153
|
+
return value;
|
|
4154
|
+
}
|
|
4155
|
+
function record(value, keys) {
|
|
4156
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((k) => !keys.includes(k))) return fail();
|
|
4157
|
+
return Object.fromEntries(Object.entries(value));
|
|
4158
|
+
}
|
|
4159
|
+
function axisBinding(value) {
|
|
4160
|
+
const v = record(value, [
|
|
4161
|
+
"axis",
|
|
4162
|
+
"invert",
|
|
4163
|
+
"negative",
|
|
4164
|
+
"positive",
|
|
4165
|
+
"deadZone"
|
|
4166
|
+
]);
|
|
4167
|
+
const indices = (value) => {
|
|
4168
|
+
if (value === void 0) return Object.freeze([]);
|
|
4169
|
+
if (!Array.isArray(value)) return fail();
|
|
4170
|
+
return Object.freeze([...new Set(value.map(gamepadIndex))]);
|
|
4171
|
+
};
|
|
4172
|
+
if (v.invert !== void 0 && typeof v.invert !== "boolean") return fail();
|
|
4173
|
+
return Object.freeze({
|
|
4174
|
+
...v.axis === void 0 ? {} : { axis: gamepadIndex(v.axis) },
|
|
4175
|
+
...v.invert === void 0 ? {} : { invert: v.invert },
|
|
4176
|
+
negative: indices(v.negative),
|
|
4177
|
+
positive: indices(v.positive),
|
|
4178
|
+
...v.deadZone === void 0 ? {} : { deadZone: gamepadDeadZone(v.deadZone) }
|
|
4179
|
+
});
|
|
4180
|
+
}
|
|
4181
|
+
function checkedGamepadBindings(actions, value) {
|
|
4182
|
+
const v = record(value, Object.keys(actions));
|
|
4183
|
+
return Object.freeze(Object.entries(v).map(([name, binding]) => {
|
|
4184
|
+
if (!Object.hasOwn(actions, name)) return fail();
|
|
4185
|
+
const action = actions[name];
|
|
4186
|
+
if (!action) return fail();
|
|
4187
|
+
let result;
|
|
4188
|
+
if (action.kind === "button") {
|
|
4189
|
+
const b = record(binding, ["buttons"]);
|
|
4190
|
+
if (b.buttons !== void 0 && !Array.isArray(b.buttons)) return fail();
|
|
4191
|
+
result = Object.freeze({ buttons: Object.freeze((b.buttons ?? []).map((item) => {
|
|
4192
|
+
if (typeof item === "number") return gamepadIndex(item);
|
|
4193
|
+
const b = record(item, ["button", "threshold"]), threshold = b.threshold ?? .5;
|
|
4194
|
+
if (typeof threshold !== "number" || !Number.isFinite(threshold) || threshold <= 0 || threshold > 1) return fail();
|
|
4195
|
+
return Object.freeze({
|
|
4196
|
+
button: gamepadIndex(b.button),
|
|
4197
|
+
threshold
|
|
4198
|
+
});
|
|
4199
|
+
})) });
|
|
4200
|
+
} else if (action.kind === "axis1d") result = axisBinding(binding);
|
|
4201
|
+
else {
|
|
4202
|
+
const keys = action.kind === "axis2d" ? ["x", "y"] : [
|
|
4203
|
+
"x",
|
|
4204
|
+
"y",
|
|
4205
|
+
"z"
|
|
4206
|
+
], b = record(binding, keys);
|
|
4207
|
+
result = Object.freeze(Object.fromEntries(keys.map((k) => [k, axisBinding(b[k] ?? {})])));
|
|
4208
|
+
}
|
|
4209
|
+
return Object.freeze([action, result]);
|
|
4210
|
+
}));
|
|
4211
|
+
}
|
|
4083
4212
|
// packages/core/src/interaction/key-code.ts
|
|
4084
4213
|
/** Physical UI Events code values plus F13–F24; Unidentified is not bindable. */
|
|
4085
4214
|
var KeyCode = {
|
|
@@ -4275,6 +4404,27 @@ function parseKeyCode(value) {
|
|
|
4275
4404
|
if (!isKeyCode(value)) throw new EngineError("INVALID_KEY_CODE", "Unknown physical keyboard code", { value });
|
|
4276
4405
|
return value;
|
|
4277
4406
|
}
|
|
4407
|
+
// packages/core/src/interaction/physical-input.ts
|
|
4408
|
+
var InputDevice = Object.freeze({
|
|
4409
|
+
Keyboard: "keyboard",
|
|
4410
|
+
Mouse: "mouse",
|
|
4411
|
+
Gamepad: "gamepad"
|
|
4412
|
+
});
|
|
4413
|
+
/** Standard PointerEvent.button indices; these are not the buttons bit mask. */
|
|
4414
|
+
var MouseButton = Object.freeze({
|
|
4415
|
+
Left: 0,
|
|
4416
|
+
Middle: 1,
|
|
4417
|
+
Right: 2,
|
|
4418
|
+
Back: 3,
|
|
4419
|
+
Forward: 4
|
|
4420
|
+
});
|
|
4421
|
+
function isMouseButton(value) {
|
|
4422
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 4;
|
|
4423
|
+
}
|
|
4424
|
+
function parseMouseButton(value) {
|
|
4425
|
+
if (!isMouseButton(value)) throw new EngineError("INVALID_MOUSE_BUTTON", "Expected a standard mouse button index (0–4)");
|
|
4426
|
+
return value;
|
|
4427
|
+
}
|
|
4278
4428
|
// packages/core/src/interaction/action-bindings.ts
|
|
4279
4429
|
function checkedBinding(action, value) {
|
|
4280
4430
|
const record = (v, allowed) => {
|
|
@@ -4288,43 +4438,123 @@ function checkedBinding(action, value) {
|
|
|
4288
4438
|
if (!Array.isArray(v)) throw new EngineError("INVALID_BINDING", "Key list must be an array");
|
|
4289
4439
|
return Object.freeze([...new Set(v.map(parseKeyCode))]);
|
|
4290
4440
|
};
|
|
4291
|
-
const
|
|
4292
|
-
|
|
4441
|
+
const mice = (v) => {
|
|
4442
|
+
if (!Array.isArray(v)) throw new EngineError("INVALID_BINDING", "Mouse button list must be an array");
|
|
4443
|
+
return Object.freeze([...new Set(v.map(parseMouseButton))]);
|
|
4444
|
+
};
|
|
4445
|
+
const axis = (v, root = false) => {
|
|
4446
|
+
const r = record(v ?? {}, [
|
|
4447
|
+
"negative",
|
|
4448
|
+
"positive",
|
|
4449
|
+
"mouseNegative",
|
|
4450
|
+
"mousePositive",
|
|
4451
|
+
...root ? ["gamepad"] : []
|
|
4452
|
+
]);
|
|
4293
4453
|
return Object.freeze({
|
|
4294
4454
|
negative: codes(r.negative),
|
|
4295
|
-
positive: codes(r.positive)
|
|
4455
|
+
positive: codes(r.positive),
|
|
4456
|
+
...r.mouseNegative === void 0 ? {} : { mouseNegative: mice(r.mouseNegative) },
|
|
4457
|
+
...r.mousePositive === void 0 ? {} : { mousePositive: mice(r.mousePositive) }
|
|
4296
4458
|
});
|
|
4297
4459
|
};
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4460
|
+
const r = record(value, action.kind === "button" ? [
|
|
4461
|
+
"keys",
|
|
4462
|
+
"mouse",
|
|
4463
|
+
"gamepad"
|
|
4464
|
+
] : action.kind === "axis1d" ? [
|
|
4465
|
+
"negative",
|
|
4466
|
+
"positive",
|
|
4467
|
+
"mouseNegative",
|
|
4468
|
+
"mousePositive",
|
|
4469
|
+
"gamepad"
|
|
4470
|
+
] : action.kind === "axis2d" ? [
|
|
4301
4471
|
"x",
|
|
4302
4472
|
"y",
|
|
4303
|
-
"
|
|
4473
|
+
"gamepad"
|
|
4474
|
+
] : [
|
|
4475
|
+
"x",
|
|
4476
|
+
"y",
|
|
4477
|
+
"z",
|
|
4478
|
+
"gamepad"
|
|
4304
4479
|
]);
|
|
4305
|
-
|
|
4480
|
+
let gamepad;
|
|
4481
|
+
if (r.gamepad !== void 0) {
|
|
4482
|
+
const checked = checkedGamepadBindings({ [action.name]: action }, { [action.name]: r.gamepad })[0];
|
|
4483
|
+
if (!checked) throw new EngineError("INVALID_BINDING", "Missing gamepad binding");
|
|
4484
|
+
gamepad = checked[1];
|
|
4485
|
+
}
|
|
4486
|
+
const base = action.kind === "button" ? {
|
|
4487
|
+
keys: codes(r.keys),
|
|
4488
|
+
...r.mouse === void 0 ? {} : { mouse: mice(r.mouse) }
|
|
4489
|
+
} : action.kind === "axis1d" ? axis(value, true) : action.kind === "axis2d" ? {
|
|
4306
4490
|
x: axis(r.x),
|
|
4307
4491
|
y: axis(r.y)
|
|
4308
4492
|
} : {
|
|
4309
4493
|
x: axis(r.x),
|
|
4310
4494
|
y: axis(r.y),
|
|
4311
4495
|
z: axis(r.z)
|
|
4496
|
+
};
|
|
4497
|
+
return Object.freeze({
|
|
4498
|
+
...base,
|
|
4499
|
+
...gamepad === void 0 ? {} : { gamepad }
|
|
4312
4500
|
});
|
|
4313
4501
|
}
|
|
4314
4502
|
function keyLists(binding, path = "") {
|
|
4315
4503
|
const lists = [];
|
|
4316
|
-
for (const [key, value] of Object.entries(binding)) if (
|
|
4504
|
+
for (const [key, value] of Object.entries(binding)) if (key === "keys" || key === "negative" || key === "positive") lists.push({
|
|
4317
4505
|
path: path + key,
|
|
4318
4506
|
keys: value
|
|
4319
4507
|
});
|
|
4320
|
-
else lists.push(...keyLists(value, `${path}${key}.`));
|
|
4508
|
+
else if (key === "x" || key === "y" || key === "z") lists.push(...keyLists(value, `${path}${key}.`));
|
|
4321
4509
|
return lists;
|
|
4322
4510
|
}
|
|
4511
|
+
function mouseLists(binding, path = "") {
|
|
4512
|
+
const lists = [];
|
|
4513
|
+
for (const [key, value] of Object.entries(binding)) if (key === "mouse" || key === "mouseNegative" || key === "mousePositive") lists.push({
|
|
4514
|
+
path: path + key,
|
|
4515
|
+
buttons: value
|
|
4516
|
+
});
|
|
4517
|
+
else if (key === "x" || key === "y" || key === "z") lists.push(...mouseLists(value, `${path}${key}.`));
|
|
4518
|
+
return lists;
|
|
4519
|
+
}
|
|
4520
|
+
function gamepadControls(binding, path = "gamepad") {
|
|
4521
|
+
const result = [];
|
|
4522
|
+
for (const [name, value] of Object.entries(binding)) if (name === "axis" && typeof value === "number") result.push({
|
|
4523
|
+
kind: "axis",
|
|
4524
|
+
index: value,
|
|
4525
|
+
path: `${path}.axis`
|
|
4526
|
+
});
|
|
4527
|
+
else if (name === "buttons" || name === "negative" || name === "positive") for (const item of value) result.push({
|
|
4528
|
+
kind: "button",
|
|
4529
|
+
index: typeof item === "number" ? item : item.button,
|
|
4530
|
+
path: `${path}.${name}`
|
|
4531
|
+
});
|
|
4532
|
+
else if (name === "x" || name === "y" || name === "z") result.push(...gamepadControls(value, `${path}.${name}`));
|
|
4533
|
+
return result;
|
|
4534
|
+
}
|
|
4535
|
+
function editGamepad(current, patch, remove) {
|
|
4536
|
+
if (Array.isArray(patch)) {
|
|
4537
|
+
const old = Array.isArray(current) ? current : [];
|
|
4538
|
+
const control = (v) => v && typeof v === "object" && "button" in v ? v.button : v;
|
|
4539
|
+
return remove ? old.filter((v) => !patch.some((p) => control(p) === control(v))) : [...new Map([...old, ...patch].map((v) => [JSON.stringify(v), v])).values()];
|
|
4540
|
+
}
|
|
4541
|
+
if (patch && typeof patch === "object") {
|
|
4542
|
+
const result = current && typeof current === "object" ? { ...current } : {};
|
|
4543
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
4544
|
+
const edited = editGamepad(result[key], value, remove);
|
|
4545
|
+
if (edited === void 0) delete result[key];
|
|
4546
|
+
else result[key] = edited;
|
|
4547
|
+
}
|
|
4548
|
+
return result;
|
|
4549
|
+
}
|
|
4550
|
+
return remove ? current === patch ? void 0 : current : patch;
|
|
4551
|
+
}
|
|
4323
4552
|
function editBinding(current, patch, remove) {
|
|
4324
4553
|
const result = { ...current };
|
|
4325
4554
|
for (const [name, value] of Object.entries(patch)) {
|
|
4326
4555
|
const old = current[name];
|
|
4327
|
-
if (
|
|
4556
|
+
if (name === "gamepad") result[name] = editGamepad(old, value, remove);
|
|
4557
|
+
else if (Array.isArray(value)) result[name] = remove ? (old ?? []).filter((k) => !value.includes(k)) : [.../* @__PURE__ */ new Set([...old ?? [], ...value])];
|
|
4328
4558
|
else result[name] = editBinding(old, value, remove);
|
|
4329
4559
|
}
|
|
4330
4560
|
return result;
|
|
@@ -4522,6 +4752,9 @@ var ActionController = class {
|
|
|
4522
4752
|
bus = new EventBus();
|
|
4523
4753
|
keyboardAllowed = true;
|
|
4524
4754
|
blockedKeys = /* @__PURE__ */ new Set();
|
|
4755
|
+
mouseAllowed = true;
|
|
4756
|
+
blockedMouse = /* @__PURE__ */ new Set();
|
|
4757
|
+
bindingGeneration = 0;
|
|
4525
4758
|
pending = [];
|
|
4526
4759
|
transitions = [];
|
|
4527
4760
|
constructor(actions, options) {
|
|
@@ -4529,6 +4762,8 @@ var ActionController = class {
|
|
|
4529
4762
|
this.options = options;
|
|
4530
4763
|
if (options.keyboard !== void 0 && typeof options.keyboard !== "boolean") throw new EngineError("INVALID_VALUE", "Keyboard enabled must be boolean");
|
|
4531
4764
|
this.keyboardAllowed = options.keyboard ?? true;
|
|
4765
|
+
if (options.mouse !== void 0 && typeof options.mouse !== "boolean") throw new EngineError("INVALID_VALUE", "Mouse enabled must be boolean");
|
|
4766
|
+
this.mouseAllowed = options.mouse ?? true;
|
|
4532
4767
|
for (const action of Object.values(actions)) {
|
|
4533
4768
|
const binding = checkedBinding(action, options.bindings && Object.hasOwn(options.bindings, action.name) ? options.bindings[action.name] ?? {} : {});
|
|
4534
4769
|
const value = neutral(action);
|
|
@@ -4575,6 +4810,24 @@ var ActionController = class {
|
|
|
4575
4810
|
get sourceCount() {
|
|
4576
4811
|
return this.sources.size;
|
|
4577
4812
|
}
|
|
4813
|
+
get mouseEnabled() {
|
|
4814
|
+
return this.mouseAllowed;
|
|
4815
|
+
}
|
|
4816
|
+
set mouseEnabled(value) {
|
|
4817
|
+
if (this.disposed) throw new EngineError("OWNER_DISPOSED", "Action controller has been disposed");
|
|
4818
|
+
if (typeof value !== "boolean") throw new EngineError("INVALID_VALUE", "Mouse enabled must be boolean");
|
|
4819
|
+
if (value === this.mouseAllowed) return;
|
|
4820
|
+
this.mouseAllowed = value;
|
|
4821
|
+
this.blockedMouse.clear();
|
|
4822
|
+
if (value) {
|
|
4823
|
+
for (const entry of this.entries.values()) for (const list of mouseLists(entry.binding)) for (const button of list.buttons) if (this.options.input.mouseDown(button)) this.blockedMouse.add(button);
|
|
4824
|
+
}
|
|
4825
|
+
for (const action of this.entries.keys()) this.recompute(action);
|
|
4826
|
+
}
|
|
4827
|
+
/** @internal Used by existing host adapters to follow common binding settings. */
|
|
4828
|
+
get bindingRevision() {
|
|
4829
|
+
return this.bindingGeneration;
|
|
4830
|
+
}
|
|
4578
4831
|
onDispose(callback) {
|
|
4579
4832
|
return this.lifetime.onDispose(callback);
|
|
4580
4833
|
}
|
|
@@ -4653,13 +4906,13 @@ var ActionController = class {
|
|
|
4653
4906
|
}
|
|
4654
4907
|
return source;
|
|
4655
4908
|
}
|
|
4656
|
-
/** @internal */ write(source, action, value) {
|
|
4909
|
+
/** @internal */ write(source, action, value, edges = true) {
|
|
4657
4910
|
this.options.input.assertWrite(isNeutral(value));
|
|
4658
4911
|
this.entry(action);
|
|
4659
4912
|
const values = this.sources.get(source);
|
|
4660
4913
|
if (!values) throw new EngineError("OWNER_DISPOSED", "Action source has been disposed");
|
|
4661
4914
|
values.set(action, value);
|
|
4662
|
-
this.recompute(action);
|
|
4915
|
+
this.recompute(action, edges);
|
|
4663
4916
|
}
|
|
4664
4917
|
/** @internal */ clearSource(source, remove) {
|
|
4665
4918
|
const values = this.sources.get(source);
|
|
@@ -4670,11 +4923,11 @@ var ActionController = class {
|
|
|
4670
4923
|
if (!this.disposed) for (const action of actions) this.recompute(action);
|
|
4671
4924
|
}
|
|
4672
4925
|
keyboard(action) {
|
|
4673
|
-
if (!this.keyboardAllowed) return neutral(action);
|
|
4674
4926
|
const binding = this.entry(action).binding;
|
|
4675
|
-
const held = (keys) => keys?.some((k) => !this.blockedKeys.has(k) && this.options.input.keyDown(k)) ?? false;
|
|
4676
|
-
const
|
|
4677
|
-
|
|
4927
|
+
const held = (keys) => this.keyboardAllowed && (keys?.some((k) => !this.blockedKeys.has(k) && this.options.input.keyDown(k)) ?? false);
|
|
4928
|
+
const mouse = (buttons) => this.mouseAllowed && (buttons?.some((b) => !this.blockedMouse.has(b) && this.options.input.mouseDown(b)) ?? false);
|
|
4929
|
+
const axis = (b) => Number(held(b?.positive) || mouse(b?.mousePositive)) - Number(held(b?.negative) || mouse(b?.mouseNegative));
|
|
4930
|
+
if (action.kind === "button") return held(binding.keys) || mouse(binding.mouse);
|
|
4678
4931
|
if (action.kind === "axis1d") return axis(binding);
|
|
4679
4932
|
const b = binding;
|
|
4680
4933
|
return action.kind === "axis2d" ? {
|
|
@@ -4727,11 +4980,15 @@ var ActionController = class {
|
|
|
4727
4980
|
}
|
|
4728
4981
|
/** @internal */ keyChanged() {
|
|
4729
4982
|
for (const key of this.blockedKeys) if (!this.options.input.keyDown(key)) this.blockedKeys.delete(key);
|
|
4983
|
+
for (const button of this.blockedMouse) if (!this.options.input.mouseDown(button)) this.blockedMouse.delete(button);
|
|
4730
4984
|
if (this.active || !this.disposed && this.options.scene.inputOwner === this.options.input && (this.options.scene.state === SceneState.Preparing || this.options.scene.state === SceneState.Ready)) for (const action of this.entries.keys()) this.recompute(action);
|
|
4731
4985
|
}
|
|
4732
4986
|
/** @internal */ accepts(code) {
|
|
4733
4987
|
return this.active && this.keyboardAllowed && [...this.entries.values()].some((e) => keyLists(e.binding).some((list) => list.keys.includes(code)));
|
|
4734
4988
|
}
|
|
4989
|
+
/** @internal */ acceptsMouse(button) {
|
|
4990
|
+
return this.active && this.mouseAllowed && [...this.entries.values()].some((e) => mouseLists(e.binding).some((list) => list.buttons.includes(button)));
|
|
4991
|
+
}
|
|
4735
4992
|
/** @internal */ capture(override) {
|
|
4736
4993
|
if (!this.active) return;
|
|
4737
4994
|
const tokens = override ? new Map([...this.entries.keys()].map((a) => [a.name, a])) : void 0;
|
|
@@ -4773,16 +5030,25 @@ var ActionController = class {
|
|
|
4773
5030
|
/** Clears held contributions and unconsumed edges; no synthetic button release. */
|
|
4774
5031
|
reset() {
|
|
4775
5032
|
this.blockedKeys.clear();
|
|
5033
|
+
this.blockedMouse.clear();
|
|
4776
5034
|
for (const values of this.sources.values()) values.clear();
|
|
4777
5035
|
this.pending = [];
|
|
4778
5036
|
for (const [action, entry] of this.entries) entry.value = neutral(action);
|
|
4779
5037
|
}
|
|
5038
|
+
/** @internal Flush device transitions without destroying explicit program values. */
|
|
5039
|
+
resetPhysical() {
|
|
5040
|
+
this.blockedKeys.clear();
|
|
5041
|
+
this.blockedMouse.clear();
|
|
5042
|
+
this.pending = [];
|
|
5043
|
+
for (const action of this.entries.keys()) this.recompute(action, false);
|
|
5044
|
+
}
|
|
4780
5045
|
getBinding(action) {
|
|
4781
5046
|
return this.entry(action).binding;
|
|
4782
5047
|
}
|
|
4783
5048
|
rebind(action, binding) {
|
|
4784
5049
|
const checked = checkedBinding(action, binding), entry = this.entry(action);
|
|
4785
5050
|
entry.binding = checked;
|
|
5051
|
+
this.bindingGeneration++;
|
|
4786
5052
|
this.pending = this.pending.filter((event) => event.action !== action);
|
|
4787
5053
|
this.recompute(action, false);
|
|
4788
5054
|
}
|
|
@@ -4795,7 +5061,13 @@ var ActionController = class {
|
|
|
4795
5061
|
resetBindings(action) {
|
|
4796
5062
|
for (const token of action ? [action] : this.entries.keys()) this.rebind(token, this.entry(token).defaults);
|
|
4797
5063
|
}
|
|
4798
|
-
conflicts() {
|
|
5064
|
+
conflicts(device = "keyboard") {
|
|
5065
|
+
if (![
|
|
5066
|
+
"keyboard",
|
|
5067
|
+
"mouse",
|
|
5068
|
+
"gamepad",
|
|
5069
|
+
"all"
|
|
5070
|
+
].includes(device)) throw new EngineError("INVALID_BINDING", "Unknown conflict device");
|
|
4799
5071
|
const keys = /* @__PURE__ */ new Map();
|
|
4800
5072
|
for (const [action, entry] of this.entries) for (const list of keyLists(entry.binding)) for (const key of list.keys) {
|
|
4801
5073
|
const group = keys.get(key) ?? [];
|
|
@@ -4805,10 +5077,54 @@ var ActionController = class {
|
|
|
4805
5077
|
});
|
|
4806
5078
|
keys.set(key, group);
|
|
4807
5079
|
}
|
|
4808
|
-
|
|
5080
|
+
const keyboard = [...keys].filter(([, bindings]) => bindings.length > 1).map(([key, bindings]) => ({
|
|
4809
5081
|
key,
|
|
4810
5082
|
bindings
|
|
4811
5083
|
}));
|
|
5084
|
+
if (device === "keyboard") return keyboard;
|
|
5085
|
+
const mice = /* @__PURE__ */ new Map();
|
|
5086
|
+
const pads = /* @__PURE__ */ new Map();
|
|
5087
|
+
for (const [action, entry] of this.entries) {
|
|
5088
|
+
for (const list of mouseLists(entry.binding)) for (const button of list.buttons) {
|
|
5089
|
+
const uses = mice.get(button) ?? [];
|
|
5090
|
+
uses.push({
|
|
5091
|
+
action,
|
|
5092
|
+
path: list.path
|
|
5093
|
+
});
|
|
5094
|
+
mice.set(button, uses);
|
|
5095
|
+
}
|
|
5096
|
+
const pad = entry.binding.gamepad;
|
|
5097
|
+
if (pad) for (const input of gamepadControls(pad)) {
|
|
5098
|
+
const key = `${input.kind}:${input.index}`;
|
|
5099
|
+
const group = pads.get(key) ?? {
|
|
5100
|
+
kind: input.kind,
|
|
5101
|
+
index: input.index,
|
|
5102
|
+
bindings: []
|
|
5103
|
+
};
|
|
5104
|
+
group.bindings.push({
|
|
5105
|
+
action,
|
|
5106
|
+
path: input.path
|
|
5107
|
+
});
|
|
5108
|
+
pads.set(key, group);
|
|
5109
|
+
}
|
|
5110
|
+
}
|
|
5111
|
+
const mouse = [...mice].filter(([, uses]) => uses.length > 1).map(([button, bindings]) => ({
|
|
5112
|
+
device: "mouse",
|
|
5113
|
+
button,
|
|
5114
|
+
bindings
|
|
5115
|
+
}));
|
|
5116
|
+
const gamepad = [...pads.values()].filter((group) => group.bindings.length > 1).map((group) => ({
|
|
5117
|
+
device: "gamepad",
|
|
5118
|
+
...group
|
|
5119
|
+
}));
|
|
5120
|
+
return device === "mouse" ? mouse : device === "gamepad" ? gamepad : [
|
|
5121
|
+
...keyboard.map((group) => ({
|
|
5122
|
+
device: "keyboard",
|
|
5123
|
+
...group
|
|
5124
|
+
})),
|
|
5125
|
+
...mouse,
|
|
5126
|
+
...gamepad
|
|
5127
|
+
];
|
|
4812
5128
|
}
|
|
4813
5129
|
exportBindings() {
|
|
4814
5130
|
return {
|
|
@@ -4847,6 +5163,10 @@ var ActionSource = class extends OwnedInputSource {
|
|
|
4847
5163
|
const checked = checkedActionValue(action, value);
|
|
4848
5164
|
if (this.writable(isNeutral(checked))) this.controller.write(this, action, checked);
|
|
4849
5165
|
}
|
|
5166
|
+
/** @internal Reset one device binding without synthesizing logical edges. */
|
|
5167
|
+
resetBinding(action) {
|
|
5168
|
+
if (this.writable(true)) this.controller.write(this, action, neutral(action), false);
|
|
5169
|
+
}
|
|
4850
5170
|
release(remove) {
|
|
4851
5171
|
this.controller.clearSource(this, remove);
|
|
4852
5172
|
}
|
|
@@ -4869,16 +5189,38 @@ var KeyboardSource = class extends OwnedInputSource {
|
|
|
4869
5189
|
this.input.releaseKeyboard(this, remove);
|
|
4870
5190
|
}
|
|
4871
5191
|
};
|
|
5192
|
+
/** Physical mouse contribution; game state is read through ActionController. */
|
|
5193
|
+
var MouseSource = class extends OwnedInputSource {
|
|
5194
|
+
input;
|
|
5195
|
+
/** @internal */ constructor(input) {
|
|
5196
|
+
super();
|
|
5197
|
+
this.input = input;
|
|
5198
|
+
}
|
|
5199
|
+
set(button, pressed) {
|
|
5200
|
+
parseMouseButton(button);
|
|
5201
|
+
if (typeof pressed !== "boolean") throw new EngineError("INVALID_VALUE", "Mouse state must be boolean");
|
|
5202
|
+
if (this.writable(!pressed)) this.input.writeMouse(this, button, pressed);
|
|
5203
|
+
}
|
|
5204
|
+
release(remove) {
|
|
5205
|
+
this.input.releaseMouse(this, remove);
|
|
5206
|
+
}
|
|
5207
|
+
};
|
|
4872
5208
|
/** Engine-owned routing and update boundary. No raw game-state/alternate movement API. */
|
|
4873
5209
|
var InputSystem = class {
|
|
4874
5210
|
actions = /* @__PURE__ */ new Set();
|
|
4875
5211
|
adapters = /* @__PURE__ */ new Set();
|
|
4876
5212
|
keyboards = /* @__PURE__ */ new Map();
|
|
5213
|
+
mice = /* @__PURE__ */ new Map();
|
|
5214
|
+
captureCancel;
|
|
4877
5215
|
dead = false;
|
|
4878
5216
|
playback = false;
|
|
4879
5217
|
frames;
|
|
5218
|
+
/** @internal Device hosts may reject live gestures during Replay playback. */
|
|
5219
|
+
get liveInput() {
|
|
5220
|
+
return !this.dead && !this.playback && !this.captureCancel;
|
|
5221
|
+
}
|
|
4880
5222
|
get sourceCount() {
|
|
4881
|
-
let count = this.keyboards.size;
|
|
5223
|
+
let count = this.keyboards.size + this.mice.size;
|
|
4882
5224
|
for (const actions of this.actions) count += actions.sourceCount;
|
|
4883
5225
|
return count;
|
|
4884
5226
|
}
|
|
@@ -4898,10 +5240,19 @@ var InputSystem = class {
|
|
|
4898
5240
|
};
|
|
4899
5241
|
}
|
|
4900
5242
|
acceptsKey(code) {
|
|
4901
|
-
if (this.
|
|
5243
|
+
if (!this.liveInput) return false;
|
|
4902
5244
|
for (const actions of this.actions) if (actions.accepts(code)) return true;
|
|
4903
5245
|
return false;
|
|
4904
5246
|
}
|
|
5247
|
+
acceptsMouse(button) {
|
|
5248
|
+
if (!this.liveInput) return false;
|
|
5249
|
+
for (const actions of this.actions) if (actions.acceptsMouse(button)) return true;
|
|
5250
|
+
return false;
|
|
5251
|
+
}
|
|
5252
|
+
/** @internal */ mouseDown(button) {
|
|
5253
|
+
for (const buttons of this.mice.values()) if (buttons.has(button)) return true;
|
|
5254
|
+
return false;
|
|
5255
|
+
}
|
|
4905
5256
|
notify() {
|
|
4906
5257
|
for (const actions of this.actions) actions.keyChanged();
|
|
4907
5258
|
}
|
|
@@ -4945,10 +5296,42 @@ var InputSystem = class {
|
|
|
4945
5296
|
}
|
|
4946
5297
|
return source;
|
|
4947
5298
|
}
|
|
5299
|
+
createMouseSource(owner) {
|
|
5300
|
+
if (this.dead || owner?.disposed) throw new EngineError("OWNER_DISPOSED", "Input owner has been disposed");
|
|
5301
|
+
const source = new MouseSource(this);
|
|
5302
|
+
this.mice.set(source, /* @__PURE__ */ new Set());
|
|
5303
|
+
try {
|
|
5304
|
+
if (owner) source.own(owner);
|
|
5305
|
+
} catch (error) {
|
|
5306
|
+
source.dispose();
|
|
5307
|
+
throw error;
|
|
5308
|
+
}
|
|
5309
|
+
return source;
|
|
5310
|
+
}
|
|
5311
|
+
/** @internal */ writeMouse(source, button, pressed) {
|
|
5312
|
+
this.assertWrite(!pressed);
|
|
5313
|
+
const buttons = this.mice.get(source);
|
|
5314
|
+
if (!buttons || this.dead) throw new EngineError("OWNER_DISPOSED", "Input has been disposed");
|
|
5315
|
+
if (this.captureCancel && pressed) return;
|
|
5316
|
+
if (buttons.has(button) === pressed) return;
|
|
5317
|
+
const before = this.mouseDown(button);
|
|
5318
|
+
if (pressed) buttons.add(button);
|
|
5319
|
+
else buttons.delete(button);
|
|
5320
|
+
if (before !== this.mouseDown(button)) this.notify();
|
|
5321
|
+
}
|
|
5322
|
+
/** @internal */ releaseMouse(source, remove) {
|
|
5323
|
+
const buttons = this.mice.get(source);
|
|
5324
|
+
if (!buttons) return;
|
|
5325
|
+
const changed = buttons.size > 0;
|
|
5326
|
+
buttons.clear();
|
|
5327
|
+
if (remove) this.mice.delete(source);
|
|
5328
|
+
if (changed && !this.dead) this.notify();
|
|
5329
|
+
}
|
|
4948
5330
|
/** @internal */ writeKey(source, code, pressed) {
|
|
4949
5331
|
this.assertWrite(!pressed);
|
|
4950
5332
|
const keys = this.keyboards.get(source);
|
|
4951
5333
|
if (!keys || this.dead) throw new EngineError("OWNER_DISPOSED", "Input has been disposed");
|
|
5334
|
+
if (this.captureCancel && pressed) return;
|
|
4952
5335
|
if (keys.has(code) === pressed) return;
|
|
4953
5336
|
const before = this.keyDown(code);
|
|
4954
5337
|
if (pressed) keys.add(code);
|
|
@@ -4987,11 +5370,41 @@ var InputSystem = class {
|
|
|
4987
5370
|
this.playback = false;
|
|
4988
5371
|
this.clear();
|
|
4989
5372
|
}
|
|
4990
|
-
|
|
5373
|
+
/** @internal Host capture gate. Program ActionSource values remain explicit. */
|
|
5374
|
+
suspendLiveInput(cancel) {
|
|
5375
|
+
if (!this.liveInput) throw new EngineError("INPUT_CAPTURE_UNAVAILABLE", "Live input is unavailable or another capture is active");
|
|
5376
|
+
this.captureCancel = cancel;
|
|
5377
|
+
try {
|
|
5378
|
+
this.resetPhysical();
|
|
5379
|
+
} catch (error) {
|
|
5380
|
+
this.captureCancel = void 0;
|
|
5381
|
+
throw error;
|
|
5382
|
+
}
|
|
5383
|
+
return () => {
|
|
5384
|
+
if (this.captureCancel !== cancel) return;
|
|
5385
|
+
this.captureCancel = void 0;
|
|
5386
|
+
this.resetPhysical();
|
|
5387
|
+
};
|
|
5388
|
+
}
|
|
5389
|
+
resetPhysical() {
|
|
4991
5390
|
for (const keys of this.keyboards.values()) keys.clear();
|
|
4992
|
-
|
|
5391
|
+
for (const buttons of this.mice.values()) buttons.clear();
|
|
5392
|
+
cleanupAll([...[...this.adapters].map((adapter) => () => {
|
|
4993
5393
|
if (this.adapters.has(adapter)) adapter.reset();
|
|
4994
|
-
})], "
|
|
5394
|
+
}), ...[...this.actions].map((actions) => () => actions.resetPhysical())], "Physical input reset failed");
|
|
5395
|
+
}
|
|
5396
|
+
clear() {
|
|
5397
|
+
const cancel = this.captureCancel;
|
|
5398
|
+
this.captureCancel = void 0;
|
|
5399
|
+
for (const keys of this.keyboards.values()) keys.clear();
|
|
5400
|
+
for (const buttons of this.mice.values()) buttons.clear();
|
|
5401
|
+
cleanupAll([
|
|
5402
|
+
() => cancel?.(),
|
|
5403
|
+
...[...this.actions].map((actions) => () => actions.reset()),
|
|
5404
|
+
...[...this.adapters].map((adapter) => () => {
|
|
5405
|
+
if (this.adapters.has(adapter)) adapter.reset();
|
|
5406
|
+
})
|
|
5407
|
+
], "Input reset failed");
|
|
4995
5408
|
}
|
|
4996
5409
|
/** Device diagnostics only; game state is ActionController.snapshot(). */
|
|
4997
5410
|
snapshot() {
|
|
@@ -5002,19 +5415,123 @@ var InputSystem = class {
|
|
|
5002
5415
|
dispose() {
|
|
5003
5416
|
if (this.dead) return;
|
|
5004
5417
|
this.dead = true;
|
|
5418
|
+
const cancel = this.captureCancel;
|
|
5419
|
+
this.captureCancel = void 0;
|
|
5005
5420
|
try {
|
|
5006
5421
|
cleanupAll([
|
|
5422
|
+
() => cancel?.(),
|
|
5007
5423
|
...[...this.adapters].map((a) => () => a.dispose()),
|
|
5008
5424
|
...[...this.actions].map((a) => () => a.dispose()),
|
|
5009
|
-
...[...this.keyboards.keys()].map((s) => () => s.dispose())
|
|
5425
|
+
...[...this.keyboards.keys()].map((s) => () => s.dispose()),
|
|
5426
|
+
...[...this.mice.keys()].map((s) => () => s.dispose())
|
|
5010
5427
|
], "Input cleanup failed");
|
|
5011
5428
|
} finally {
|
|
5012
5429
|
this.adapters.clear();
|
|
5013
5430
|
this.actions.clear();
|
|
5014
5431
|
this.keyboards.clear();
|
|
5432
|
+
this.mice.clear();
|
|
5015
5433
|
}
|
|
5016
5434
|
}
|
|
5017
5435
|
};
|
|
5436
|
+
// packages/core/src/interaction/input-capture.ts
|
|
5437
|
+
var InputCaptureState = Object.freeze({
|
|
5438
|
+
Arming: "arming",
|
|
5439
|
+
Listening: "listening",
|
|
5440
|
+
Completed: "completed",
|
|
5441
|
+
Cancelled: "cancelled"
|
|
5442
|
+
});
|
|
5443
|
+
/** A portable single-input session. Hosts supply neutral arming and physical events. */
|
|
5444
|
+
var InputCapture = class {
|
|
5445
|
+
life = new Lifetime();
|
|
5446
|
+
status = InputCaptureState.Arming;
|
|
5447
|
+
devices;
|
|
5448
|
+
gamepadIndex;
|
|
5449
|
+
result;
|
|
5450
|
+
settle = () => {};
|
|
5451
|
+
constructor(options = {}) {
|
|
5452
|
+
if (!options || typeof options !== "object" || Array.isArray(options) || Object.keys(options).some((key) => ![
|
|
5453
|
+
"devices",
|
|
5454
|
+
"gamepadIndex",
|
|
5455
|
+
"owner"
|
|
5456
|
+
].includes(key))) throw new EngineError("INVALID_INPUT_CAPTURE", "Invalid input capture options");
|
|
5457
|
+
const devices = options.devices ?? Object.values(InputDevice);
|
|
5458
|
+
if (!Array.isArray(devices) || !devices.length || devices.some((device) => !Object.values(InputDevice).includes(device))) throw new EngineError("INVALID_INPUT_CAPTURE", "Select at least one known capture device");
|
|
5459
|
+
this.devices = Object.freeze([...new Set(devices)]);
|
|
5460
|
+
this.gamepadIndex = options.gamepadIndex === void 0 ? void 0 : gamepadIndex(options.gamepadIndex);
|
|
5461
|
+
if (options.owner?.disposed) throw new EngineError("OWNER_DISPOSED", "Capture owner has been disposed");
|
|
5462
|
+
this.result = new Promise((resolve) => {
|
|
5463
|
+
this.settle = resolve;
|
|
5464
|
+
});
|
|
5465
|
+
if (options.owner) this.life.onDispose(options.owner.onDispose(() => this.cancel()));
|
|
5466
|
+
}
|
|
5467
|
+
get state() {
|
|
5468
|
+
return this.status;
|
|
5469
|
+
}
|
|
5470
|
+
get disposed() {
|
|
5471
|
+
return this.life.disposed;
|
|
5472
|
+
}
|
|
5473
|
+
onDispose(callback) {
|
|
5474
|
+
return this.life.onDispose(callback);
|
|
5475
|
+
}
|
|
5476
|
+
/** @internal Hosts call only after all eligible preheld buttons/keys are neutral. */
|
|
5477
|
+
arm() {
|
|
5478
|
+
if (this.status === InputCaptureState.Arming) this.status = InputCaptureState.Listening;
|
|
5479
|
+
}
|
|
5480
|
+
/** @internal Offer one fresh physical down, never a repeat or opening gesture. */
|
|
5481
|
+
accept(input) {
|
|
5482
|
+
if (this.status !== InputCaptureState.Listening) return false;
|
|
5483
|
+
if (!input || typeof input !== "object" || !Object.values(InputDevice).includes(input.device)) throw new EngineError("INVALID_INPUT_CAPTURE", "Unknown physical input device");
|
|
5484
|
+
if (!this.devices.includes(input.device)) return false;
|
|
5485
|
+
let descriptor, binding;
|
|
5486
|
+
if (input.device === InputDevice.Keyboard) {
|
|
5487
|
+
const code = parseKeyCode(input.code);
|
|
5488
|
+
descriptor = {
|
|
5489
|
+
device: InputDevice.Keyboard,
|
|
5490
|
+
code
|
|
5491
|
+
};
|
|
5492
|
+
binding = Object.freeze({ keys: Object.freeze([code]) });
|
|
5493
|
+
} else if (input.device === InputDevice.Mouse) {
|
|
5494
|
+
const button = parseMouseButton(input.button);
|
|
5495
|
+
descriptor = {
|
|
5496
|
+
device: InputDevice.Mouse,
|
|
5497
|
+
button
|
|
5498
|
+
};
|
|
5499
|
+
binding = Object.freeze({ mouse: Object.freeze([button]) });
|
|
5500
|
+
} else {
|
|
5501
|
+
const index = gamepadIndex(input.index), button = gamepadIndex(input.button);
|
|
5502
|
+
if (this.gamepadIndex !== void 0 && this.gamepadIndex !== index) return false;
|
|
5503
|
+
if (typeof input.id !== "string" || typeof input.mapping !== "string") throw new EngineError("INVALID_INPUT_CAPTURE", "Gamepad capture requires device facts");
|
|
5504
|
+
descriptor = {
|
|
5505
|
+
device: InputDevice.Gamepad,
|
|
5506
|
+
index,
|
|
5507
|
+
button,
|
|
5508
|
+
id: input.id,
|
|
5509
|
+
mapping: input.mapping
|
|
5510
|
+
};
|
|
5511
|
+
binding = Object.freeze({ gamepad: Object.freeze({ buttons: Object.freeze([button]) }) });
|
|
5512
|
+
}
|
|
5513
|
+
this.finish(InputCaptureState.Completed, Object.freeze({
|
|
5514
|
+
...descriptor,
|
|
5515
|
+
binding
|
|
5516
|
+
}));
|
|
5517
|
+
return true;
|
|
5518
|
+
}
|
|
5519
|
+
finish(state, input) {
|
|
5520
|
+
if (this.disposed) return;
|
|
5521
|
+
this.status = state;
|
|
5522
|
+
try {
|
|
5523
|
+
this.life.dispose();
|
|
5524
|
+
} finally {
|
|
5525
|
+
this.settle(input);
|
|
5526
|
+
}
|
|
5527
|
+
}
|
|
5528
|
+
cancel() {
|
|
5529
|
+
this.finish(InputCaptureState.Cancelled, void 0);
|
|
5530
|
+
}
|
|
5531
|
+
dispose() {
|
|
5532
|
+
this.cancel();
|
|
5533
|
+
}
|
|
5534
|
+
};
|
|
5018
5535
|
// packages/core/src/ui/canvas-layout.ts
|
|
5019
5536
|
var finite = (v) => {
|
|
5020
5537
|
if (!Number.isFinite(v) || v < 0) throw new EngineError("INVALID_UI", "UI layout overflow");
|
|
@@ -5309,6 +5826,18 @@ var ScenePointer = class {
|
|
|
5309
5826
|
lifetime = new Lifetime();
|
|
5310
5827
|
events = new EventBus();
|
|
5311
5828
|
removeScene;
|
|
5829
|
+
targets;
|
|
5830
|
+
/** @internal Exactly one Scene virtual-input provider shares the existing router. */
|
|
5831
|
+
attachTargets(provider) {
|
|
5832
|
+
if (this.disposed || this.targets) throw new EngineError("INVALID_VIRTUAL_INPUT", "Scene already has virtual input or is disposed");
|
|
5833
|
+
this.targets = provider;
|
|
5834
|
+
return () => {
|
|
5835
|
+
if (this.targets === provider) this.targets = void 0;
|
|
5836
|
+
};
|
|
5837
|
+
}
|
|
5838
|
+
/** @internal */ pickTarget(data) {
|
|
5839
|
+
return this.targets?.disposed ? void 0 : this.targets?.pick(data);
|
|
5840
|
+
}
|
|
5312
5841
|
constructor(scene) {
|
|
5313
5842
|
this.scene = scene;
|
|
5314
5843
|
this.removeScene = scene.onDispose(() => this.dispose());
|
|
@@ -5331,6 +5860,7 @@ var ScenePointer = class {
|
|
|
5331
5860
|
/** @internal Device-independent routing state; DOM is only an adapter. */
|
|
5332
5861
|
var PointerRouting = class {
|
|
5333
5862
|
released;
|
|
5863
|
+
targetOwners = /* @__PURE__ */ new Map();
|
|
5334
5864
|
controlOwners = /* @__PURE__ */ new Map();
|
|
5335
5865
|
scrollOwners = /* @__PURE__ */ new Map();
|
|
5336
5866
|
suppressed = /* @__PURE__ */ new Set();
|
|
@@ -5355,7 +5885,8 @@ var PointerRouting = class {
|
|
|
5355
5885
|
keyboardNodes: this.keys.size,
|
|
5356
5886
|
deliveries: this.deliveries,
|
|
5357
5887
|
scrollDrags: this.scrollOwners.size,
|
|
5358
|
-
controlDrags: this.controlOwners.size
|
|
5888
|
+
controlDrags: this.controlOwners.size,
|
|
5889
|
+
virtualDrags: this.targetOwners.size
|
|
5359
5890
|
};
|
|
5360
5891
|
}
|
|
5361
5892
|
setScene(channel, ui) {
|
|
@@ -5381,6 +5912,14 @@ var PointerRouting = class {
|
|
|
5381
5912
|
}
|
|
5382
5913
|
}
|
|
5383
5914
|
emit(gesture, type, info) {
|
|
5915
|
+
if (gesture.target) {
|
|
5916
|
+
const phase = type === PointerDown ? "down" : type === PointerMove ? "move" : type === PointerUp ? "up" : type === PointerCancel ? "cancel" : void 0;
|
|
5917
|
+
if (phase) {
|
|
5918
|
+
this.deliveries++;
|
|
5919
|
+
gesture.target.deliver(phase, info);
|
|
5920
|
+
}
|
|
5921
|
+
return;
|
|
5922
|
+
}
|
|
5384
5923
|
if (gesture.channel.disposed || gesture.node?.disposed) return;
|
|
5385
5924
|
this.deliveries++;
|
|
5386
5925
|
(gesture.node?.events ?? gesture.channel.events).emit(type, info);
|
|
@@ -5388,7 +5927,7 @@ var PointerRouting = class {
|
|
|
5388
5927
|
inside(g, data) {
|
|
5389
5928
|
return Object.freeze({
|
|
5390
5929
|
...data,
|
|
5391
|
-
inside: g.scroll && g.ui ? g.ui.containsNode(g.scroll, data.screen.x / g.ui.scale, data.screen.y / g.ui.scale) : g.node ? g.ui?.hitTest(data.screen.x / g.ui.scale, data.screen.y / g.ui.scale) === g.node : data.inside
|
|
5930
|
+
inside: g.target ? g.target.contains(data) : g.scroll && g.ui ? g.ui.containsNode(g.scroll, data.screen.x / g.ui.scale, data.screen.y / g.ui.scale) : g.node ? g.ui?.hitTest(data.screen.x / g.ui.scale, data.screen.y / g.ui.scale) === g.node : data.inside
|
|
5392
5931
|
});
|
|
5393
5932
|
}
|
|
5394
5933
|
down(data) {
|
|
@@ -5398,6 +5937,11 @@ var PointerRouting = class {
|
|
|
5398
5937
|
this.suppressed.delete(data.pointerId);
|
|
5399
5938
|
const pick = this.ui?.pick(data.screen.x / this.ui.scale, data.screen.y / this.ui.scale);
|
|
5400
5939
|
const node = pick?.node, bar = pick?.bar;
|
|
5940
|
+
const target = !node && data.button === 0 ? this.channel.pickTarget(data) : void 0;
|
|
5941
|
+
if (target && this.targetOwners.has(target)) {
|
|
5942
|
+
this.suppressed.add(data.pointerId);
|
|
5943
|
+
return;
|
|
5944
|
+
}
|
|
5401
5945
|
if (bar && data.button === 0 && (!bar.enabled || this.scrollOwners.has(bar.node))) {
|
|
5402
5946
|
this.suppressed.add(data.pointerId);
|
|
5403
5947
|
return node;
|
|
@@ -5406,20 +5950,24 @@ var PointerRouting = class {
|
|
|
5406
5950
|
this.suppressed.add(data.pointerId);
|
|
5407
5951
|
return node;
|
|
5408
5952
|
}
|
|
5409
|
-
if (!node && !data.inside) return;
|
|
5953
|
+
if (!node && !target && !data.inside) return;
|
|
5410
5954
|
const g = {
|
|
5411
5955
|
channel: this.channel,
|
|
5412
5956
|
last: data,
|
|
5413
5957
|
primary: data.button === 0,
|
|
5414
5958
|
start: data,
|
|
5415
5959
|
scale: this.ui?.scale ?? 1,
|
|
5416
|
-
candidates: this.ui?.scrollTargets(data.screen.x / this.ui.scale, data.screen.y / this.ui.scale) ?? [],
|
|
5960
|
+
candidates: !target ? this.ui?.scrollTargets(data.screen.x / this.ui.scale, data.screen.y / this.ui.scale) ?? [] : [],
|
|
5417
5961
|
candidateRevisions: [],
|
|
5418
5962
|
vx: 0,
|
|
5419
5963
|
vy: 0,
|
|
5420
5964
|
...node ? {
|
|
5421
5965
|
node,
|
|
5422
5966
|
ui: this.ui
|
|
5967
|
+
} : {},
|
|
5968
|
+
...target ? {
|
|
5969
|
+
target,
|
|
5970
|
+
targetRevision: target.revision
|
|
5423
5971
|
} : {}
|
|
5424
5972
|
};
|
|
5425
5973
|
g.candidateRevisions = g.candidates.map((n) => n.scrollRevision);
|
|
@@ -5441,6 +5989,17 @@ var PointerRouting = class {
|
|
|
5441
5989
|
if (node?.control) g.controlRevision = node.control.revision;
|
|
5442
5990
|
this.gestures.set(data.pointerId, g);
|
|
5443
5991
|
if (node) g.off = node.onDispose(() => this.cancel(data.pointerId));
|
|
5992
|
+
if (target) {
|
|
5993
|
+
this.targetOwners.set(target, data.pointerId);
|
|
5994
|
+
const off = target.subscribe(() => {
|
|
5995
|
+
if (!target.available || target.revision !== g.targetRevision) this.cancel(data.pointerId);
|
|
5996
|
+
});
|
|
5997
|
+
const disposed = target.onDispose(() => this.cancel(data.pointerId));
|
|
5998
|
+
g.off = () => {
|
|
5999
|
+
off();
|
|
6000
|
+
disposed();
|
|
6001
|
+
};
|
|
6002
|
+
}
|
|
5444
6003
|
cleanupAll([
|
|
5445
6004
|
() => {
|
|
5446
6005
|
const port = node?.control;
|
|
@@ -5489,6 +6048,11 @@ var PointerRouting = class {
|
|
|
5489
6048
|
owns(id) {
|
|
5490
6049
|
return this.gestures.has(id) || this.suppressed.has(id);
|
|
5491
6050
|
}
|
|
6051
|
+
/** @internal Mouse bindings observe world ownership without claiming a second gesture. */
|
|
6052
|
+
worldOwns(id) {
|
|
6053
|
+
const gesture = this.gestures.get(id);
|
|
6054
|
+
return Boolean(gesture && !gesture.node && !gesture.target && !gesture.scroll && !gesture.candidates.length);
|
|
6055
|
+
}
|
|
5492
6056
|
move(data) {
|
|
5493
6057
|
this.validate(data);
|
|
5494
6058
|
if (this.suppressed.has(data.pointerId)) return;
|
|
@@ -5498,7 +6062,7 @@ var PointerRouting = class {
|
|
|
5498
6062
|
const dx = Math.abs(data.screen.x - g.start.screen.x), dy = Math.abs(data.screen.y - g.start.screen.y);
|
|
5499
6063
|
if (dx >= g.control.style.dragThreshold && dx > dy) g.controlLocked = true;
|
|
5500
6064
|
}
|
|
5501
|
-
if (!g.scroll && g.primary && !g.controlLocked) this.claimScroll(g, data);
|
|
6065
|
+
if (!g.target && !g.scroll && g.primary && !g.controlLocked) this.claimScroll(g, data);
|
|
5502
6066
|
if (this.gestures.get(data.pointerId) !== g) return;
|
|
5503
6067
|
if (g.bar && g.scroll && g.ui) {
|
|
5504
6068
|
const bar = canvasScrollbars(g.scroll).find((b) => b.axis === g.bar?.axis);
|
|
@@ -5639,6 +6203,7 @@ var PointerRouting = class {
|
|
|
5639
6203
|
g = this.gestures.get(data.pointerId);
|
|
5640
6204
|
if (!g) return;
|
|
5641
6205
|
this.gestures.delete(data.pointerId);
|
|
6206
|
+
if (g.target) this.targetOwners.delete(g.target);
|
|
5642
6207
|
if (g.control) this.controlOwners.delete(g.control);
|
|
5643
6208
|
if (g.scroll) {
|
|
5644
6209
|
this.scrollOwners.delete(g.scroll);
|
|
@@ -5656,7 +6221,7 @@ var PointerRouting = class {
|
|
|
5656
6221
|
if (!g.node || g.node.available) this.emit(g, PointerUp, info);
|
|
5657
6222
|
},
|
|
5658
6223
|
() => {
|
|
5659
|
-
if (!g.scroll && g.primary && info.inside && (!g.node || g.node.available)) this.emit(g, PointerClick, info);
|
|
6224
|
+
if (!g.target && !g.scroll && g.primary && info.inside && (!g.node || g.node.available)) this.emit(g, PointerClick, info);
|
|
5660
6225
|
},
|
|
5661
6226
|
() => {
|
|
5662
6227
|
if (!g.scroll && g.primary && info.inside && g.node?.available) g.node.activate("pointer");
|
|
@@ -5668,6 +6233,7 @@ var PointerRouting = class {
|
|
|
5668
6233
|
const g = this.gestures.get(id);
|
|
5669
6234
|
if (!g) return;
|
|
5670
6235
|
this.gestures.delete(id);
|
|
6236
|
+
if (g.target) this.targetOwners.delete(g.target);
|
|
5671
6237
|
if (g.control) this.controlOwners.delete(g.control);
|
|
5672
6238
|
if (g.scroll) {
|
|
5673
6239
|
this.scrollOwners.delete(g.scroll);
|
|
@@ -5738,6 +6304,10 @@ var PointerRouting = class {
|
|
|
5738
6304
|
...[...this.keys.keys()].map((n) => () => this.releaseKeys(n))
|
|
5739
6305
|
], "Pointer release failed");
|
|
5740
6306
|
}
|
|
6307
|
+
/** @internal Host focus changes interrupt virtual devices without changing Canvas editing rules. */
|
|
6308
|
+
releaseVirtual() {
|
|
6309
|
+
cleanupAll([...this.gestures].filter(([, g]) => g.target).map(([id]) => () => this.cancel(id)), "Virtual pointer release failed");
|
|
6310
|
+
}
|
|
5741
6311
|
dispose() {
|
|
5742
6312
|
try {
|
|
5743
6313
|
this.reset();
|
|
@@ -5753,6 +6323,716 @@ var PointerRouting = class {
|
|
|
5753
6323
|
cleanupAll([...off, () => this.release()], "Pointer routing cleanup failed");
|
|
5754
6324
|
}
|
|
5755
6325
|
};
|
|
6326
|
+
// packages/core/src/interaction/virtual-regions.ts
|
|
6327
|
+
var VirtualRegions = Object.freeze({
|
|
6328
|
+
/** A shared edge is not an overlap. Use separate rectangles to leave a center gap. */
|
|
6329
|
+
Left: Object.freeze({
|
|
6330
|
+
kind: "rect",
|
|
6331
|
+
x: 0,
|
|
6332
|
+
y: 0,
|
|
6333
|
+
width: .5,
|
|
6334
|
+
height: 1
|
|
6335
|
+
}),
|
|
6336
|
+
Right: Object.freeze({
|
|
6337
|
+
kind: "rect",
|
|
6338
|
+
x: .5,
|
|
6339
|
+
y: 0,
|
|
6340
|
+
width: .5,
|
|
6341
|
+
height: 1
|
|
6342
|
+
})
|
|
6343
|
+
});
|
|
6344
|
+
/** @internal */
|
|
6345
|
+
function virtualFinite(value, field) {
|
|
6346
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new EngineError("INVALID_VIRTUAL_INPUT", `${field} must be finite`);
|
|
6347
|
+
return value;
|
|
6348
|
+
}
|
|
6349
|
+
/** @internal */
|
|
6350
|
+
function virtualBounds(value) {
|
|
6351
|
+
const result = {
|
|
6352
|
+
x: virtualFinite(value.x, "x"),
|
|
6353
|
+
y: virtualFinite(value.y, "y"),
|
|
6354
|
+
width: virtualFinite(value.width, "width"),
|
|
6355
|
+
height: virtualFinite(value.height, "height")
|
|
6356
|
+
};
|
|
6357
|
+
if (result.width < 0 || result.height < 0 || !Number.isFinite(result.x + result.width) || !Number.isFinite(result.y + result.height)) throw new EngineError("INVALID_VIRTUAL_INPUT", "Region extents must be finite and nonnegative");
|
|
6358
|
+
return Object.freeze(result);
|
|
6359
|
+
}
|
|
6360
|
+
/** @internal */
|
|
6361
|
+
function virtualPosition(value, viewport) {
|
|
6362
|
+
const space = value.space ?? "viewport";
|
|
6363
|
+
if (space !== "viewport" && space !== "screen") throw new EngineError("INVALID_VIRTUAL_INPUT", "Unknown virtual coordinate space");
|
|
6364
|
+
const x = virtualFinite(value.x, "position.x"), y = virtualFinite(value.y, "position.y");
|
|
6365
|
+
return Object.freeze(space === "screen" ? {
|
|
6366
|
+
x,
|
|
6367
|
+
y
|
|
6368
|
+
} : {
|
|
6369
|
+
x: virtualFinite(viewport.x + x * viewport.width, "resolved x"),
|
|
6370
|
+
y: virtualFinite(viewport.y + y * viewport.height, "resolved y")
|
|
6371
|
+
});
|
|
6372
|
+
}
|
|
6373
|
+
/** @internal Copy configuration before retaining it. Predicate functions are deliberately live. */
|
|
6374
|
+
function checkedVirtualRegion(region) {
|
|
6375
|
+
if (!region || ![
|
|
6376
|
+
"rect",
|
|
6377
|
+
"circle",
|
|
6378
|
+
"custom"
|
|
6379
|
+
].includes(region.kind)) throw new EngineError("INVALID_VIRTUAL_INPUT", "Unknown activation region");
|
|
6380
|
+
const copy = Object.freeze({ ...region });
|
|
6381
|
+
if (copy.kind === "custom") {
|
|
6382
|
+
if (typeof copy.bounds !== "function" || typeof copy.contains !== "function") throw new EngineError("INVALID_VIRTUAL_INPUT", "Custom regions require bounds and contains");
|
|
6383
|
+
} else resolveVirtualRegion(copy, {
|
|
6384
|
+
x: 0,
|
|
6385
|
+
y: 0,
|
|
6386
|
+
width: 1,
|
|
6387
|
+
height: 1
|
|
6388
|
+
});
|
|
6389
|
+
return copy;
|
|
6390
|
+
}
|
|
6391
|
+
/** @internal */
|
|
6392
|
+
function resolveVirtualRegion(region, viewport) {
|
|
6393
|
+
if (region.kind === "custom") {
|
|
6394
|
+
if (typeof region.bounds !== "function" || typeof region.contains !== "function") throw new EngineError("INVALID_VIRTUAL_INPUT", "Custom regions require bounds and contains functions");
|
|
6395
|
+
return {
|
|
6396
|
+
kind: "custom",
|
|
6397
|
+
bounds: virtualBounds(region.bounds(viewport)),
|
|
6398
|
+
test: region.contains
|
|
6399
|
+
};
|
|
6400
|
+
}
|
|
6401
|
+
const center = virtualPosition(region, viewport);
|
|
6402
|
+
if (region.kind === "rect") {
|
|
6403
|
+
const relative = (region.space ?? "viewport") === "viewport";
|
|
6404
|
+
const width = virtualFinite(region.width, "region.width"), height = virtualFinite(region.height, "region.height");
|
|
6405
|
+
if (width < 0 || height < 0) throw new EngineError("INVALID_VIRTUAL_INPUT", "Region sizes must be nonnegative");
|
|
6406
|
+
return {
|
|
6407
|
+
kind: "rect",
|
|
6408
|
+
bounds: virtualBounds({
|
|
6409
|
+
...center,
|
|
6410
|
+
width: width * (relative ? viewport.width : 1),
|
|
6411
|
+
height: height * (relative ? viewport.height : 1)
|
|
6412
|
+
})
|
|
6413
|
+
};
|
|
6414
|
+
}
|
|
6415
|
+
const radius = virtualFinite(region.radius, "region.radius") * ((region.space ?? "viewport") === "viewport" ? Math.min(viewport.width, viewport.height) : 1);
|
|
6416
|
+
if (radius < 0) throw new EngineError("INVALID_VIRTUAL_INPUT", "Region radius must be nonnegative");
|
|
6417
|
+
return {
|
|
6418
|
+
kind: "circle",
|
|
6419
|
+
center,
|
|
6420
|
+
radius,
|
|
6421
|
+
bounds: virtualBounds({
|
|
6422
|
+
x: center.x - radius,
|
|
6423
|
+
y: center.y - radius,
|
|
6424
|
+
width: radius * 2,
|
|
6425
|
+
height: radius * 2
|
|
6426
|
+
})
|
|
6427
|
+
};
|
|
6428
|
+
}
|
|
6429
|
+
/** @internal Rectangles are half-open; circles exclude their circumference. */
|
|
6430
|
+
function virtualContains(region, point, viewport) {
|
|
6431
|
+
const b = region.bounds;
|
|
6432
|
+
if (point.x < b.x || point.y < b.y || point.x >= b.x + b.width || point.y >= b.y + b.height) return false;
|
|
6433
|
+
if (region.kind === "circle") return Math.hypot(point.x - region.center.x, point.y - region.center.y) < region.radius;
|
|
6434
|
+
if (region.kind === "custom") {
|
|
6435
|
+
const value = region.test(point, viewport);
|
|
6436
|
+
if (typeof value !== "boolean") throw new EngineError("INVALID_VIRTUAL_INPUT", "Custom region predicate must return boolean");
|
|
6437
|
+
return value;
|
|
6438
|
+
}
|
|
6439
|
+
return true;
|
|
6440
|
+
}
|
|
6441
|
+
/** @internal Exact built-in intersection; custom bounds are conservatively ambiguous. */
|
|
6442
|
+
function virtualOverlap(a, b) {
|
|
6443
|
+
const x = a.bounds, y = b.bounds;
|
|
6444
|
+
if (Math.min(x.x + x.width, y.x + y.width) <= Math.max(x.x, y.x) || Math.min(x.y + x.height, y.y + y.height) <= Math.max(x.y, y.y)) return false;
|
|
6445
|
+
if (a.kind === "circle" && b.kind === "circle") return Math.hypot(a.center.x - b.center.x, a.center.y - b.center.y) < a.radius + b.radius;
|
|
6446
|
+
const circle = a.kind === "circle" ? a : b.kind === "circle" ? b : void 0;
|
|
6447
|
+
const rect = a.kind === "circle" ? b : a;
|
|
6448
|
+
if (circle && rect.kind === "rect") {
|
|
6449
|
+
const c = circle.center, r = rect.bounds;
|
|
6450
|
+
return Math.hypot(c.x - Math.max(r.x, Math.min(r.x + r.width, c.x)), c.y - Math.max(r.y, Math.min(r.y + r.height, c.y))) < circle.radius;
|
|
6451
|
+
}
|
|
6452
|
+
return true;
|
|
6453
|
+
}
|
|
6454
|
+
// packages/core/src/interaction/virtual-control.ts
|
|
6455
|
+
var VirtualStickMode = {
|
|
6456
|
+
Fixed: "fixed",
|
|
6457
|
+
Floating: "floating",
|
|
6458
|
+
Follow: "follow"
|
|
6459
|
+
};
|
|
6460
|
+
var screenPoint$1 = (x, y) => ({
|
|
6461
|
+
space: "screen",
|
|
6462
|
+
x: virtualFinite(x, "screen.x"),
|
|
6463
|
+
y: virtualFinite(y, "screen.y")
|
|
6464
|
+
});
|
|
6465
|
+
var zero = Object.freeze({
|
|
6466
|
+
x: 0,
|
|
6467
|
+
y: 0
|
|
6468
|
+
});
|
|
6469
|
+
var Start = defineEvent("VirtualStart");
|
|
6470
|
+
var Change = defineEvent("VirtualChange");
|
|
6471
|
+
var End = defineEvent("VirtualEnd");
|
|
6472
|
+
var Cancel = defineEvent("VirtualCancel");
|
|
6473
|
+
var Hold = defineEvent("VirtualHold");
|
|
6474
|
+
function flag(value) {
|
|
6475
|
+
if (typeof value !== "boolean") throw new EngineError("INVALID_VIRTUAL_INPUT", "Virtual control flags must be boolean");
|
|
6476
|
+
return value;
|
|
6477
|
+
}
|
|
6478
|
+
function zone(value) {
|
|
6479
|
+
virtualFinite(value, "deadZone");
|
|
6480
|
+
if (value < 0 || value >= 1) throw new EngineError("INVALID_VIRTUAL_INPUT", "Dead zone must satisfy 0 <= value < 1");
|
|
6481
|
+
return value;
|
|
6482
|
+
}
|
|
6483
|
+
function mode(value) {
|
|
6484
|
+
if (!Object.values(VirtualStickMode).includes(value)) throw new EngineError("INVALID_VIRTUAL_INPUT", "Unknown virtual stick mode");
|
|
6485
|
+
return value;
|
|
6486
|
+
}
|
|
6487
|
+
/** @internal Validate before allocating ActionSources or registering a target. */
|
|
6488
|
+
function checkVirtualOptions(options) {
|
|
6489
|
+
checkedVirtualRegion(options.region);
|
|
6490
|
+
positive(options.radius ?? 64, "radius");
|
|
6491
|
+
zone(options.deadZone ?? .15);
|
|
6492
|
+
flag(options.enabled ?? true);
|
|
6493
|
+
flag(options.visible ?? true);
|
|
6494
|
+
flag(options.direction ?? false);
|
|
6495
|
+
mode(options.mode ?? VirtualStickMode.Fixed);
|
|
6496
|
+
if (virtualFinite(options.holdDelay ?? .5, "holdDelay") < 0) throw new EngineError("INVALID_VIRTUAL_INPUT", "Hold delay must be nonnegative");
|
|
6497
|
+
if (options.position) {
|
|
6498
|
+
virtualFinite(options.position.x, "position.x");
|
|
6499
|
+
virtualFinite(options.position.y, "position.y");
|
|
6500
|
+
if (options.position.space !== void 0 && options.position.space !== "screen" && options.position.space !== "viewport") throw new EngineError("INVALID_VIRTUAL_INPUT", "Unknown position space");
|
|
6501
|
+
}
|
|
6502
|
+
const color = options.color ?? 6736821;
|
|
6503
|
+
if (!Number.isInteger(color) || color < 0 || color > 16777215) throw new EngineError("INVALID_VIRTUAL_INPUT", "Virtual color must be RGB");
|
|
6504
|
+
}
|
|
6505
|
+
/** Portable gesture state. Routing, Action composition and rendering retain their existing owners. */
|
|
6506
|
+
var VirtualControl = class {
|
|
6507
|
+
kind;
|
|
6508
|
+
host;
|
|
6509
|
+
binding;
|
|
6510
|
+
life = new Lifetime();
|
|
6511
|
+
events = new EventBus();
|
|
6512
|
+
observers = /* @__PURE__ */ new Set();
|
|
6513
|
+
activation;
|
|
6514
|
+
activeFlag;
|
|
6515
|
+
shown;
|
|
6516
|
+
size;
|
|
6517
|
+
deadZoneValue;
|
|
6518
|
+
version = 0;
|
|
6519
|
+
gesture;
|
|
6520
|
+
vector = zero;
|
|
6521
|
+
dragMode;
|
|
6522
|
+
position;
|
|
6523
|
+
holdDelay;
|
|
6524
|
+
color;
|
|
6525
|
+
directional;
|
|
6526
|
+
constructor(kind, options, host, binding) {
|
|
6527
|
+
this.kind = kind;
|
|
6528
|
+
this.host = host;
|
|
6529
|
+
this.binding = binding;
|
|
6530
|
+
checkVirtualOptions(options);
|
|
6531
|
+
this.activation = checkedVirtualRegion(options.region);
|
|
6532
|
+
this.position = options.position ? Object.freeze({ ...options.position }) : void 0;
|
|
6533
|
+
this.activeFlag = options.enabled ?? true;
|
|
6534
|
+
this.shown = options.visible ?? true;
|
|
6535
|
+
this.size = options.radius ?? (kind === "stick" ? 64 : 28);
|
|
6536
|
+
this.deadZoneValue = options.deadZone ?? .15;
|
|
6537
|
+
this.dragMode = mode(options.mode ?? VirtualStickMode.Fixed);
|
|
6538
|
+
this.holdDelay = options.holdDelay ?? .5;
|
|
6539
|
+
this.color = options.color ?? 6736821;
|
|
6540
|
+
this.directional = kind === "stick" || (options.direction ?? false);
|
|
6541
|
+
this.life.onDispose(binding.onDispose(() => this.dispose()));
|
|
6542
|
+
}
|
|
6543
|
+
get disposed() {
|
|
6544
|
+
return this.life.disposed;
|
|
6545
|
+
}
|
|
6546
|
+
get available() {
|
|
6547
|
+
return !this.disposed && this.activeFlag && this.host.available() && this.binding.active();
|
|
6548
|
+
}
|
|
6549
|
+
get revision() {
|
|
6550
|
+
return this.version;
|
|
6551
|
+
}
|
|
6552
|
+
get enabled() {
|
|
6553
|
+
return this.activeFlag;
|
|
6554
|
+
}
|
|
6555
|
+
set enabled(value) {
|
|
6556
|
+
this.alive();
|
|
6557
|
+
const next = flag(value);
|
|
6558
|
+
if (next === this.activeFlag) return;
|
|
6559
|
+
this.activeFlag = next;
|
|
6560
|
+
this.interrupt();
|
|
6561
|
+
}
|
|
6562
|
+
get visible() {
|
|
6563
|
+
return this.shown;
|
|
6564
|
+
}
|
|
6565
|
+
set visible(value) {
|
|
6566
|
+
this.alive();
|
|
6567
|
+
this.shown = flag(value);
|
|
6568
|
+
}
|
|
6569
|
+
get radius() {
|
|
6570
|
+
return this.size;
|
|
6571
|
+
}
|
|
6572
|
+
set radius(value) {
|
|
6573
|
+
this.alive();
|
|
6574
|
+
const next = positive(value, "radius");
|
|
6575
|
+
if (next === this.size) return;
|
|
6576
|
+
this.size = next;
|
|
6577
|
+
this.interrupt();
|
|
6578
|
+
}
|
|
6579
|
+
get deadZone() {
|
|
6580
|
+
return this.deadZoneValue;
|
|
6581
|
+
}
|
|
6582
|
+
set deadZone(value) {
|
|
6583
|
+
this.alive();
|
|
6584
|
+
const next = zone(value);
|
|
6585
|
+
if (next === this.deadZoneValue) return;
|
|
6586
|
+
this.deadZoneValue = next;
|
|
6587
|
+
this.interrupt();
|
|
6588
|
+
}
|
|
6589
|
+
get region() {
|
|
6590
|
+
return this.activation;
|
|
6591
|
+
}
|
|
6592
|
+
set region(value) {
|
|
6593
|
+
this.alive();
|
|
6594
|
+
const next = checkedVirtualRegion(value);
|
|
6595
|
+
this.host.region(this, next);
|
|
6596
|
+
this.activation = next;
|
|
6597
|
+
this.interrupt();
|
|
6598
|
+
}
|
|
6599
|
+
get stickMode() {
|
|
6600
|
+
return this.dragMode;
|
|
6601
|
+
}
|
|
6602
|
+
set stickMode(value) {
|
|
6603
|
+
this.alive();
|
|
6604
|
+
const next = mode(value);
|
|
6605
|
+
if (next === this.dragMode) return;
|
|
6606
|
+
this.dragMode = next;
|
|
6607
|
+
this.interrupt();
|
|
6608
|
+
}
|
|
6609
|
+
get pointerId() {
|
|
6610
|
+
return this.gesture?.last.pointerId;
|
|
6611
|
+
}
|
|
6612
|
+
get pressed() {
|
|
6613
|
+
return this.gesture !== void 0;
|
|
6614
|
+
}
|
|
6615
|
+
get elapsed() {
|
|
6616
|
+
return this.gesture?.elapsed ?? 0;
|
|
6617
|
+
}
|
|
6618
|
+
get direction() {
|
|
6619
|
+
return this.vector;
|
|
6620
|
+
}
|
|
6621
|
+
get center() {
|
|
6622
|
+
const p = this.gesture?.center ?? this.host.center(this);
|
|
6623
|
+
return Object.freeze(screenPoint$1(p.x, p.y));
|
|
6624
|
+
}
|
|
6625
|
+
get knob() {
|
|
6626
|
+
const center = this.center, last = this.gesture?.last.screen;
|
|
6627
|
+
if (!last || !this.directional) return center;
|
|
6628
|
+
const origin = this.kind === "button" ? this.gesture?.origin ?? center : center;
|
|
6629
|
+
const dx = last.x - origin.x, dy = last.y - origin.y, length = Math.hypot(dx, dy);
|
|
6630
|
+
const scale = length > this.size ? this.size / length : 1;
|
|
6631
|
+
return Object.freeze(screenPoint$1(center.x + dx * scale, center.y + dy * scale));
|
|
6632
|
+
}
|
|
6633
|
+
onDispose(callback) {
|
|
6634
|
+
return this.life.onDispose(callback);
|
|
6635
|
+
}
|
|
6636
|
+
/** @internal Geometry/availability invalidation, not a per-frame observer. */
|
|
6637
|
+
subscribe(callback) {
|
|
6638
|
+
this.alive();
|
|
6639
|
+
this.observers.add(callback);
|
|
6640
|
+
return () => {
|
|
6641
|
+
this.observers.delete(callback);
|
|
6642
|
+
};
|
|
6643
|
+
}
|
|
6644
|
+
onStart(callback, owner) {
|
|
6645
|
+
return this.events.on(Start, callback, owner);
|
|
6646
|
+
}
|
|
6647
|
+
onChange(callback, owner) {
|
|
6648
|
+
return this.events.on(Change, callback, owner);
|
|
6649
|
+
}
|
|
6650
|
+
onEnd(callback, owner) {
|
|
6651
|
+
return this.events.on(End, callback, owner);
|
|
6652
|
+
}
|
|
6653
|
+
onCancel(callback, owner) {
|
|
6654
|
+
return this.events.on(Cancel, callback, owner);
|
|
6655
|
+
}
|
|
6656
|
+
onHold(callback, owner) {
|
|
6657
|
+
return this.events.on(Hold, callback, owner);
|
|
6658
|
+
}
|
|
6659
|
+
/** Release/cancel only; the captured physical ID stays suppressed until its release. */
|
|
6660
|
+
cancel() {
|
|
6661
|
+
this.alive();
|
|
6662
|
+
this.interrupt();
|
|
6663
|
+
}
|
|
6664
|
+
/** @internal */
|
|
6665
|
+
invalidate() {
|
|
6666
|
+
this.interrupt();
|
|
6667
|
+
}
|
|
6668
|
+
alive() {
|
|
6669
|
+
if (this.disposed) throw new EngineError("OWNER_DISPOSED", "Virtual control has been disposed");
|
|
6670
|
+
}
|
|
6671
|
+
interrupt() {
|
|
6672
|
+
this.version++;
|
|
6673
|
+
cleanupAll([...[...this.observers], () => {
|
|
6674
|
+
if (this.gesture) this.finish(true);
|
|
6675
|
+
else this.binding.clear();
|
|
6676
|
+
}], "Virtual gesture interruption failed");
|
|
6677
|
+
}
|
|
6678
|
+
contains(data) {
|
|
6679
|
+
return this.host.contains(this, data);
|
|
6680
|
+
}
|
|
6681
|
+
snapshot(g) {
|
|
6682
|
+
return Object.freeze({
|
|
6683
|
+
control: this,
|
|
6684
|
+
pointerId: g.last.pointerId,
|
|
6685
|
+
pointerType: g.last.pointerType,
|
|
6686
|
+
origin: g.origin,
|
|
6687
|
+
position: Object.freeze(screenPoint$1(g.last.screen.x, g.last.screen.y)),
|
|
6688
|
+
center: g.center,
|
|
6689
|
+
direction: this.vector,
|
|
6690
|
+
inside: this.contains(g.last),
|
|
6691
|
+
elapsed: g.elapsed
|
|
6692
|
+
});
|
|
6693
|
+
}
|
|
6694
|
+
update(g) {
|
|
6695
|
+
if (!this.directional) {
|
|
6696
|
+
this.vector = zero;
|
|
6697
|
+
return;
|
|
6698
|
+
}
|
|
6699
|
+
const start = this.kind === "button" ? g.origin : g.center;
|
|
6700
|
+
const dx = g.last.screen.x - start.x, dy = g.last.screen.y - start.y;
|
|
6701
|
+
const length = Math.hypot(dx, dy);
|
|
6702
|
+
if (this.kind === "stick" && this.dragMode === VirtualStickMode.Follow && length > this.size) g.center = Object.freeze(screenPoint$1(g.last.screen.x - dx * this.size / length, g.last.screen.y - dy * this.size / length));
|
|
6703
|
+
const magnitude = Math.min(1, length / this.size);
|
|
6704
|
+
const output = magnitude <= this.deadZoneValue ? 0 : (magnitude - this.deadZoneValue) / (1 - this.deadZoneValue);
|
|
6705
|
+
this.vector = output === 0 ? zero : Object.freeze({
|
|
6706
|
+
x: dx / length * output,
|
|
6707
|
+
y: dy / length * output
|
|
6708
|
+
});
|
|
6709
|
+
}
|
|
6710
|
+
deliver(type, data) {
|
|
6711
|
+
if (type === "cancel") {
|
|
6712
|
+
if (this.gesture?.last.pointerId === data.pointerId) this.finish(true);
|
|
6713
|
+
return;
|
|
6714
|
+
}
|
|
6715
|
+
if (type === "down") {
|
|
6716
|
+
if (!this.available || this.gesture) return;
|
|
6717
|
+
const center = this.kind === "stick" && this.dragMode !== VirtualStickMode.Fixed ? data.screen : this.host.center(this);
|
|
6718
|
+
const g = {
|
|
6719
|
+
last: data,
|
|
6720
|
+
origin: Object.freeze(screenPoint$1(data.screen.x, data.screen.y)),
|
|
6721
|
+
center: Object.freeze(screenPoint$1(center.x, center.y)),
|
|
6722
|
+
elapsed: 0,
|
|
6723
|
+
held: false
|
|
6724
|
+
};
|
|
6725
|
+
this.gesture = g;
|
|
6726
|
+
this.update(g);
|
|
6727
|
+
this.binding.write(this.vector, true);
|
|
6728
|
+
const event = this.snapshot(g);
|
|
6729
|
+
cleanupAll([() => this.events.emit(Start, event), () => {
|
|
6730
|
+
if (this.gesture === g) this.events.emit(Change, event);
|
|
6731
|
+
}], "Virtual start handlers failed");
|
|
6732
|
+
return;
|
|
6733
|
+
}
|
|
6734
|
+
const g = this.gesture;
|
|
6735
|
+
if (!g || g.last.pointerId !== data.pointerId) return;
|
|
6736
|
+
if (!this.available) {
|
|
6737
|
+
this.interrupt();
|
|
6738
|
+
return;
|
|
6739
|
+
}
|
|
6740
|
+
if (data.screen.x !== g.last.screen.x || data.screen.y !== g.last.screen.y) {
|
|
6741
|
+
g.last = data;
|
|
6742
|
+
this.update(g);
|
|
6743
|
+
this.binding.write(this.vector, true);
|
|
6744
|
+
this.events.emit(Change, this.snapshot(g));
|
|
6745
|
+
}
|
|
6746
|
+
if (type === "up" && this.gesture === g) {
|
|
6747
|
+
g.last = data;
|
|
6748
|
+
this.finish(false);
|
|
6749
|
+
}
|
|
6750
|
+
}
|
|
6751
|
+
/** @internal Explicit presentation clock; no browser/OS timer in Core. */
|
|
6752
|
+
advance(seconds) {
|
|
6753
|
+
const g = this.gesture;
|
|
6754
|
+
if (!g) return;
|
|
6755
|
+
if (!this.available) {
|
|
6756
|
+
this.interrupt();
|
|
6757
|
+
return;
|
|
6758
|
+
}
|
|
6759
|
+
g.elapsed += seconds;
|
|
6760
|
+
if (!g.held && g.elapsed >= this.holdDelay) {
|
|
6761
|
+
g.held = true;
|
|
6762
|
+
this.events.emit(Hold, this.snapshot(g));
|
|
6763
|
+
}
|
|
6764
|
+
}
|
|
6765
|
+
finish(cancelled) {
|
|
6766
|
+
const g = this.gesture;
|
|
6767
|
+
if (!g) return;
|
|
6768
|
+
let event;
|
|
6769
|
+
try {
|
|
6770
|
+
event = this.snapshot(g);
|
|
6771
|
+
} finally {
|
|
6772
|
+
this.gesture = void 0;
|
|
6773
|
+
this.vector = zero;
|
|
6774
|
+
this.binding.clear();
|
|
6775
|
+
}
|
|
6776
|
+
this.events.emit(cancelled ? Cancel : End, event);
|
|
6777
|
+
}
|
|
6778
|
+
dispose() {
|
|
6779
|
+
if (this.disposed) return;
|
|
6780
|
+
cleanupAll([
|
|
6781
|
+
() => this.life.dispose(),
|
|
6782
|
+
() => this.interrupt(),
|
|
6783
|
+
() => this.binding.dispose(),
|
|
6784
|
+
() => this.events.dispose(),
|
|
6785
|
+
() => this.observers.clear(),
|
|
6786
|
+
() => this.host.remove(this)
|
|
6787
|
+
], "Virtual control cleanup failed");
|
|
6788
|
+
}
|
|
6789
|
+
};
|
|
6790
|
+
var VirtualStick = class extends VirtualControl {
|
|
6791
|
+
/** @internal Use VirtualInput.stick. */
|
|
6792
|
+
constructor(options, host, binding) {
|
|
6793
|
+
super("stick", options, host, binding);
|
|
6794
|
+
}
|
|
6795
|
+
get value() {
|
|
6796
|
+
return this.direction;
|
|
6797
|
+
}
|
|
6798
|
+
get mode() {
|
|
6799
|
+
return this.stickMode;
|
|
6800
|
+
}
|
|
6801
|
+
set mode(value) {
|
|
6802
|
+
this.stickMode = value;
|
|
6803
|
+
}
|
|
6804
|
+
};
|
|
6805
|
+
var VirtualButton = class extends VirtualControl {
|
|
6806
|
+
/** @internal Use VirtualInput.button. */
|
|
6807
|
+
constructor(options, host, binding) {
|
|
6808
|
+
super("button", options, host, binding);
|
|
6809
|
+
}
|
|
6810
|
+
get value() {
|
|
6811
|
+
return this.pressed;
|
|
6812
|
+
}
|
|
6813
|
+
/** Confirmation on normal release, including a drag outside the activation region; never on cancel. */
|
|
6814
|
+
onCommit(callback, owner) {
|
|
6815
|
+
return this.onEnd(callback, owner);
|
|
6816
|
+
}
|
|
6817
|
+
};
|
|
6818
|
+
// packages/core/src/interaction/virtual-input.ts
|
|
6819
|
+
/** Scene-scoped virtual devices; all gameplay values flow through existing ActionSources. */
|
|
6820
|
+
var VirtualInput = class {
|
|
6821
|
+
pointer;
|
|
6822
|
+
input;
|
|
6823
|
+
life = new Lifetime();
|
|
6824
|
+
list = Object.freeze([]);
|
|
6825
|
+
regions = /* @__PURE__ */ new Map();
|
|
6826
|
+
anchors = /* @__PURE__ */ new Map();
|
|
6827
|
+
view = Object.freeze({
|
|
6828
|
+
x: 0,
|
|
6829
|
+
y: 0,
|
|
6830
|
+
width: 800,
|
|
6831
|
+
height: 450
|
|
6832
|
+
});
|
|
6833
|
+
active = true;
|
|
6834
|
+
shown = true;
|
|
6835
|
+
dirty = true;
|
|
6836
|
+
host;
|
|
6837
|
+
constructor(pointer, input) {
|
|
6838
|
+
this.pointer = pointer;
|
|
6839
|
+
this.input = input;
|
|
6840
|
+
if (pointer.disposed) throw new EngineError("OWNER_DISPOSED", "Scene pointer has been disposed");
|
|
6841
|
+
this.host = {
|
|
6842
|
+
available: () => !this.disposed && this.active && input.liveInput,
|
|
6843
|
+
center: (control) => this.anchors.get(control) ?? {
|
|
6844
|
+
x: 0,
|
|
6845
|
+
y: 0
|
|
6846
|
+
},
|
|
6847
|
+
contains: (control, data) => {
|
|
6848
|
+
const region = this.regions.get(control);
|
|
6849
|
+
return region ? virtualContains(region, data.screen, this.view) : false;
|
|
6850
|
+
},
|
|
6851
|
+
region: (control, region) => {
|
|
6852
|
+
const regions = this.resolve(this.view, control, region);
|
|
6853
|
+
this.regions = regions;
|
|
6854
|
+
this.anchors = this.centers(regions, this.view);
|
|
6855
|
+
this.dirty = true;
|
|
6856
|
+
},
|
|
6857
|
+
remove: (control) => {
|
|
6858
|
+
this.list = Object.freeze(this.list.filter((c) => c !== control));
|
|
6859
|
+
this.regions.delete(control);
|
|
6860
|
+
this.anchors.delete(control);
|
|
6861
|
+
}
|
|
6862
|
+
};
|
|
6863
|
+
try {
|
|
6864
|
+
this.life.onDispose(pointer.attachTargets(this));
|
|
6865
|
+
this.life.onDispose(input.addAdapter(this));
|
|
6866
|
+
this.life.onDispose(pointer.onDispose(() => this.dispose()));
|
|
6867
|
+
} catch (error) {
|
|
6868
|
+
this.life.dispose();
|
|
6869
|
+
throw error;
|
|
6870
|
+
}
|
|
6871
|
+
}
|
|
6872
|
+
get disposed() {
|
|
6873
|
+
return this.life.disposed;
|
|
6874
|
+
}
|
|
6875
|
+
get controls() {
|
|
6876
|
+
return this.list;
|
|
6877
|
+
}
|
|
6878
|
+
get viewport() {
|
|
6879
|
+
return this.view;
|
|
6880
|
+
}
|
|
6881
|
+
get enabled() {
|
|
6882
|
+
return this.active;
|
|
6883
|
+
}
|
|
6884
|
+
set enabled(value) {
|
|
6885
|
+
this.alive();
|
|
6886
|
+
this.flag(value);
|
|
6887
|
+
if (this.active === value) return;
|
|
6888
|
+
this.active = value;
|
|
6889
|
+
this.reset();
|
|
6890
|
+
}
|
|
6891
|
+
get visible() {
|
|
6892
|
+
return this.shown;
|
|
6893
|
+
}
|
|
6894
|
+
set visible(value) {
|
|
6895
|
+
this.alive();
|
|
6896
|
+
this.flag(value);
|
|
6897
|
+
this.shown = value;
|
|
6898
|
+
}
|
|
6899
|
+
onDispose(callback) {
|
|
6900
|
+
return this.life.onDispose(callback);
|
|
6901
|
+
}
|
|
6902
|
+
flag(value) {
|
|
6903
|
+
if (typeof value !== "boolean") throw new EngineError("INVALID_VIRTUAL_INPUT", "Virtual input flags must be boolean");
|
|
6904
|
+
}
|
|
6905
|
+
alive() {
|
|
6906
|
+
if (this.disposed) throw new EngineError("OWNER_DISPOSED", "Virtual input has been disposed");
|
|
6907
|
+
}
|
|
6908
|
+
validate(controller, action, kind, options) {
|
|
6909
|
+
this.alive();
|
|
6910
|
+
checkVirtualOptions(options);
|
|
6911
|
+
if (!this.input.hasController(controller) || controller.reader(action).scene !== this.pointer.scene || action.kind !== kind) throw new EngineError("INVALID_ACTION", "Virtual input requires a matching Action kind and a controller in its own Scene/InputSystem");
|
|
6912
|
+
const region = resolveVirtualRegion(options.region, this.view);
|
|
6913
|
+
const peers = this.resolve(this.view);
|
|
6914
|
+
for (const control of this.list) {
|
|
6915
|
+
const other = peers.get(control);
|
|
6916
|
+
if (other && control.kind === (kind === "axis2d" ? "stick" : "button") && virtualOverlap(region, other)) throw new EngineError("VIRTUAL_REGION_OVERLAP", "Same-priority virtual activation regions overlap");
|
|
6917
|
+
}
|
|
6918
|
+
if (options.position) virtualPosition(options.position, this.view);
|
|
6919
|
+
}
|
|
6920
|
+
binding(controller, action) {
|
|
6921
|
+
const source = controller.createSource();
|
|
6922
|
+
return {
|
|
6923
|
+
get disposed() {
|
|
6924
|
+
return source.disposed;
|
|
6925
|
+
},
|
|
6926
|
+
onDispose: (callback) => controller.onDispose(callback),
|
|
6927
|
+
clear: () => source.clear(),
|
|
6928
|
+
dispose: () => source.dispose(),
|
|
6929
|
+
active: () => controller.active,
|
|
6930
|
+
write: (direction, pressed) => source.set(action, action.kind === "axis2d" ? direction : pressed)
|
|
6931
|
+
};
|
|
6932
|
+
}
|
|
6933
|
+
stick(controller, action, options) {
|
|
6934
|
+
this.validate(controller, action, "axis2d", options);
|
|
6935
|
+
const binding = this.binding(controller, action);
|
|
6936
|
+
try {
|
|
6937
|
+
return this.add(new VirtualStick(options, this.host, binding));
|
|
6938
|
+
} catch (error) {
|
|
6939
|
+
binding.dispose();
|
|
6940
|
+
throw error;
|
|
6941
|
+
}
|
|
6942
|
+
}
|
|
6943
|
+
button(controller, action, options) {
|
|
6944
|
+
this.validate(controller, action, "button", options);
|
|
6945
|
+
const binding = this.binding(controller, action);
|
|
6946
|
+
try {
|
|
6947
|
+
return this.add(new VirtualButton(options, this.host, binding));
|
|
6948
|
+
} catch (error) {
|
|
6949
|
+
binding.dispose();
|
|
6950
|
+
throw error;
|
|
6951
|
+
}
|
|
6952
|
+
}
|
|
6953
|
+
add(control) {
|
|
6954
|
+
this.list = Object.freeze([...this.list, control]);
|
|
6955
|
+
this.dirty = true;
|
|
6956
|
+
try {
|
|
6957
|
+
this.refresh();
|
|
6958
|
+
} catch (error) {
|
|
6959
|
+
control.dispose();
|
|
6960
|
+
throw error;
|
|
6961
|
+
}
|
|
6962
|
+
return control;
|
|
6963
|
+
}
|
|
6964
|
+
resolve(view, replaced, region) {
|
|
6965
|
+
const result = /* @__PURE__ */ new Map();
|
|
6966
|
+
for (const control of this.list) result.set(control, resolveVirtualRegion(control === replaced && region ? region : control.region, view));
|
|
6967
|
+
for (let i = 0; i < this.list.length; i++) {
|
|
6968
|
+
const a = this.list[i];
|
|
6969
|
+
if (!a) continue;
|
|
6970
|
+
for (let j = i + 1; j < this.list.length; j++) {
|
|
6971
|
+
const b = this.list[j], x = result.get(a), y = b ? result.get(b) : void 0;
|
|
6972
|
+
if (b && x && y && a.kind === b.kind && virtualOverlap(x, y)) throw new EngineError("VIRTUAL_REGION_OVERLAP", "Same-priority virtual activation regions overlap", {
|
|
6973
|
+
first: i,
|
|
6974
|
+
second: j
|
|
6975
|
+
});
|
|
6976
|
+
}
|
|
6977
|
+
}
|
|
6978
|
+
return result;
|
|
6979
|
+
}
|
|
6980
|
+
centers(regions, view) {
|
|
6981
|
+
return new Map([...regions].map(([control, region]) => {
|
|
6982
|
+
const b = region.bounds;
|
|
6983
|
+
return [control, control.position ? virtualPosition(control.position, view) : Object.freeze({
|
|
6984
|
+
x: b.x + b.width / 2,
|
|
6985
|
+
y: b.y + b.height / 2
|
|
6986
|
+
})];
|
|
6987
|
+
}));
|
|
6988
|
+
}
|
|
6989
|
+
/** Resolve after Canvas layout and before routing/rendering. Layout changes cancel active gestures. */
|
|
6990
|
+
refresh(viewport = this.view) {
|
|
6991
|
+
this.alive();
|
|
6992
|
+
const view = virtualBounds(viewport);
|
|
6993
|
+
const resized = Object.keys(view).some((key) => view[key] !== this.view[key]);
|
|
6994
|
+
if (!this.dirty && !resized && !this.list.some((c) => c.region.kind === "custom")) return;
|
|
6995
|
+
const regions = this.resolve(view), centers = this.centers(regions, view);
|
|
6996
|
+
const changed = this.list.filter((c) => resized || JSON.stringify(regions.get(c)) !== JSON.stringify(this.regions.get(c)) || JSON.stringify(centers.get(c)) !== JSON.stringify(this.anchors.get(c)));
|
|
6997
|
+
this.view = view;
|
|
6998
|
+
this.regions = regions;
|
|
6999
|
+
this.anchors = centers;
|
|
7000
|
+
this.dirty = false;
|
|
7001
|
+
cleanupAll(changed.map((c) => () => c.invalidate()), "Virtual layout cancellation failed");
|
|
7002
|
+
}
|
|
7003
|
+
pick(data) {
|
|
7004
|
+
if (this.disposed || !this.active || !this.input.liveInput || data.button !== 0) return;
|
|
7005
|
+
this.refresh();
|
|
7006
|
+
for (const kind of ["button", "stick"]) for (const control of this.list) if (control.kind === kind && control.available && control.contains(data)) return control;
|
|
7007
|
+
}
|
|
7008
|
+
sample() {
|
|
7009
|
+
if (this.disposed) return;
|
|
7010
|
+
this.refresh();
|
|
7011
|
+
cleanupAll(this.list.filter((c) => c.pressed && !c.available).map((c) => () => c.invalidate()), "Virtual availability cancellation failed");
|
|
7012
|
+
}
|
|
7013
|
+
advance(seconds) {
|
|
7014
|
+
this.alive();
|
|
7015
|
+
if (virtualFinite(seconds, "seconds") < 0) throw new EngineError("INVALID_VIRTUAL_INPUT", "Presentation seconds must be nonnegative");
|
|
7016
|
+
cleanupAll(this.list.map((c) => () => {
|
|
7017
|
+
if (!c.disposed) c.advance(seconds);
|
|
7018
|
+
}), "Virtual hold callbacks failed");
|
|
7019
|
+
}
|
|
7020
|
+
reset() {
|
|
7021
|
+
cleanupAll(this.list.map((c) => () => c.invalidate()), "Virtual input reset failed");
|
|
7022
|
+
}
|
|
7023
|
+
dispose() {
|
|
7024
|
+
if (this.disposed) return;
|
|
7025
|
+
cleanupAll([
|
|
7026
|
+
() => this.life.dispose(),
|
|
7027
|
+
...this.list.map((c) => () => c.dispose()),
|
|
7028
|
+
() => {
|
|
7029
|
+
this.regions.clear();
|
|
7030
|
+
this.anchors.clear();
|
|
7031
|
+
this.list = Object.freeze([]);
|
|
7032
|
+
}
|
|
7033
|
+
], "Virtual input cleanup failed");
|
|
7034
|
+
}
|
|
7035
|
+
};
|
|
5756
7036
|
// packages/core/src/runtime/engine.ts
|
|
5757
7037
|
var Engine = class {
|
|
5758
7038
|
input = new InputSystem();
|
|
@@ -6320,7 +7600,7 @@ var ReplayPlayer = class {
|
|
|
6320
7600
|
this.engine = engine;
|
|
6321
7601
|
this.data = parseReplayData(data);
|
|
6322
7602
|
this.bound = bindings(engine, options.controllers);
|
|
6323
|
-
if (this.data.engineVersion !== "0.1.
|
|
7603
|
+
if (this.data.engineVersion !== "0.1.6" || engine.random.seed !== this.data.random.seed) throw new EngineError("REPLAY_INCOMPATIBLE", "Engine version or initial seed does not match Replay");
|
|
6324
7604
|
if (!compatible(this.bound.map(([id, c]) => ({
|
|
6325
7605
|
id,
|
|
6326
7606
|
actions: c.schema()
|
|
@@ -8218,6 +9498,6 @@ var CanvasUI = class {
|
|
|
8218
9498
|
], "Canvas UI cleanup failed");
|
|
8219
9499
|
}
|
|
8220
9500
|
};
|
|
8221
|
-
export { Action, ActionController, ActionMovement, ActionSource, Actor, ActorState, Aspect, BodyType, Camera, CameraFollow, Cancellation, CanvasButton, CanvasCheckbox, CanvasCheckboxNode, CanvasColumn, CanvasHoldButton, CanvasInputNode, CanvasLabel, CanvasNode, CanvasNumberInput, CanvasNumberInputNode, CanvasPanel, CanvasRow, CanvasScroll, CanvasSelect, CanvasSelectNode, CanvasSlider, CanvasSliderNode, CanvasTextEditorNode, CanvasTextInput, CanvasTextInputNode, CanvasUI, Character, CharacterActor, Collider, CollisionBody, CollisionResponse, CollisionShape, CollisionWorld, Component, ComponentPhase, ContactEnter, ContactExit, ContactStay, DEFAULT_RANDOM_STREAM, ENGINE_VERSION, Easing, Engine, EngineError, EventBus, FixedStepRunner, GeometryTilemap, GeometryTilemapLayer, Grid, HexOrientation, InputSystem, KeyCode, KeyboardBindings, KeyboardSource, Lifetime, Motion, PointerCancel, PointerClick, PointerDown, PointerEnter, PointerLeave, PointerMove, PointerRouting, PointerUp, RANDOM_ALGORITHM, RandomManager, RandomStream, Renderable, ReplayPlayer, ReplayRecorder, Scene, ScenePointer, SceneState, Shape, SpriteAnimation, SpriteAnimationState, SpriteFit, SpriteSheet, TileContactEnter, TileContactExit, TileContactStay, TileGeometry, Tilemap, TilemapLayer, Transform, Tween, TweenBinding, TweenPlayback, TweenScheduler, actorTween, canvasControlText, canvasHoverHighlight, canvasNumericRange, canvasNumericValue, canvasOpacity, canvasScrollbars, canvasSliderGeometry, canvasTween, cleanupAll, containsScreenPoint, containsUI, createViewTransform, defaultCanvasStyle, defineActions, defineComponent, defineEvent, deriveStreamSeed, imageSize, intersectUI, isKeyCode, layoutCanvas, nativeSession, parseKeyCode, parseReplayData, positive, screenPoint, screenToPhysical, screenToWorld, scrollbarContains, sliderTween, spriteFrame, spriteFrameBounds, tweenFinite, worldPoint, worldToScreen, wrapCanvasText };
|
|
9501
|
+
export { Action, ActionController, ActionMovement, ActionSource, Actor, ActorState, Aspect, BodyType, Camera, CameraFollow, Cancellation, CanvasButton, CanvasCheckbox, CanvasCheckboxNode, CanvasColumn, CanvasHoldButton, CanvasInputNode, CanvasLabel, CanvasNode, CanvasNumberInput, CanvasNumberInputNode, CanvasPanel, CanvasRow, CanvasScroll, CanvasSelect, CanvasSelectNode, CanvasSlider, CanvasSliderNode, CanvasTextEditorNode, CanvasTextInput, CanvasTextInputNode, CanvasUI, Character, CharacterActor, Collider, CollisionBody, CollisionResponse, CollisionShape, CollisionWorld, Component, ComponentPhase, ContactEnter, ContactExit, ContactStay, DEFAULT_RANDOM_STREAM, ENGINE_VERSION, Easing, Engine, EngineError, EventBus, FixedStepRunner, GamepadAxis, GamepadBindings, GamepadButton, GeometryTilemap, GeometryTilemapLayer, Grid, HexOrientation, InputCapture, InputCaptureState, InputDevice, InputSystem, KeyCode, KeyboardBindings, KeyboardSource, Lifetime, Motion, MouseButton, MouseSource, PointerCancel, PointerClick, PointerDown, PointerEnter, PointerLeave, PointerMove, PointerRouting, PointerUp, RANDOM_ALGORITHM, RandomManager, RandomStream, Renderable, ReplayPlayer, ReplayRecorder, Scene, ScenePointer, SceneState, Shape, SpriteAnimation, SpriteAnimationState, SpriteFit, SpriteSheet, TileContactEnter, TileContactExit, TileContactStay, TileGeometry, Tilemap, TilemapLayer, Transform, Tween, TweenBinding, TweenPlayback, TweenScheduler, VirtualButton, VirtualControl, VirtualInput, VirtualRegions, VirtualStick, VirtualStickMode, actorTween, canvasControlText, canvasHoverHighlight, canvasNumericRange, canvasNumericValue, canvasOpacity, canvasScrollbars, canvasSliderGeometry, canvasTween, checkedGamepadBindings, cleanupAll, containsScreenPoint, containsUI, createViewTransform, defaultCanvasStyle, defineActions, defineComponent, defineEvent, deriveStreamSeed, gamepadDeadZone, gamepadIndex, imageSize, intersectUI, isKeyCode, isMouseButton, layoutCanvas, nativeSession, parseKeyCode, parseMouseButton, parseReplayData, positive, screenPoint, screenToPhysical, screenToWorld, scrollbarContains, sliderTween, spriteFrame, spriteFrameBounds, tweenFinite, worldPoint, worldToScreen, wrapCanvasText };
|
|
8222
9502
|
|
|
8223
9503
|
//# sourceMappingURL=index.js.map
|