@simpludify/core 0.1.4 → 0.1.5
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 +5 -0
- package/dist/index.js +771 -9
- package/dist/index.js.map +1 -1
- package/dist/interaction/input.d.ts +2 -0
- package/dist/interaction/pointer-routing.d.ts +9 -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.5";
|
|
1587
1587
|
// packages/core/src/scene/actor.ts
|
|
1588
1588
|
var inputPhase$1 = ComponentPhase.Input;
|
|
1589
1589
|
var updatePhase$1 = ComponentPhase.Update;
|
|
@@ -4877,6 +4877,10 @@ var InputSystem = class {
|
|
|
4877
4877
|
dead = false;
|
|
4878
4878
|
playback = false;
|
|
4879
4879
|
frames;
|
|
4880
|
+
/** @internal Device hosts may reject live gestures during Replay playback. */
|
|
4881
|
+
get liveInput() {
|
|
4882
|
+
return !this.dead && !this.playback;
|
|
4883
|
+
}
|
|
4880
4884
|
get sourceCount() {
|
|
4881
4885
|
let count = this.keyboards.size;
|
|
4882
4886
|
for (const actions of this.actions) count += actions.sourceCount;
|
|
@@ -5309,6 +5313,18 @@ var ScenePointer = class {
|
|
|
5309
5313
|
lifetime = new Lifetime();
|
|
5310
5314
|
events = new EventBus();
|
|
5311
5315
|
removeScene;
|
|
5316
|
+
targets;
|
|
5317
|
+
/** @internal Exactly one Scene virtual-input provider shares the existing router. */
|
|
5318
|
+
attachTargets(provider) {
|
|
5319
|
+
if (this.disposed || this.targets) throw new EngineError("INVALID_VIRTUAL_INPUT", "Scene already has virtual input or is disposed");
|
|
5320
|
+
this.targets = provider;
|
|
5321
|
+
return () => {
|
|
5322
|
+
if (this.targets === provider) this.targets = void 0;
|
|
5323
|
+
};
|
|
5324
|
+
}
|
|
5325
|
+
/** @internal */ pickTarget(data) {
|
|
5326
|
+
return this.targets?.disposed ? void 0 : this.targets?.pick(data);
|
|
5327
|
+
}
|
|
5312
5328
|
constructor(scene) {
|
|
5313
5329
|
this.scene = scene;
|
|
5314
5330
|
this.removeScene = scene.onDispose(() => this.dispose());
|
|
@@ -5331,6 +5347,7 @@ var ScenePointer = class {
|
|
|
5331
5347
|
/** @internal Device-independent routing state; DOM is only an adapter. */
|
|
5332
5348
|
var PointerRouting = class {
|
|
5333
5349
|
released;
|
|
5350
|
+
targetOwners = /* @__PURE__ */ new Map();
|
|
5334
5351
|
controlOwners = /* @__PURE__ */ new Map();
|
|
5335
5352
|
scrollOwners = /* @__PURE__ */ new Map();
|
|
5336
5353
|
suppressed = /* @__PURE__ */ new Set();
|
|
@@ -5355,7 +5372,8 @@ var PointerRouting = class {
|
|
|
5355
5372
|
keyboardNodes: this.keys.size,
|
|
5356
5373
|
deliveries: this.deliveries,
|
|
5357
5374
|
scrollDrags: this.scrollOwners.size,
|
|
5358
|
-
controlDrags: this.controlOwners.size
|
|
5375
|
+
controlDrags: this.controlOwners.size,
|
|
5376
|
+
virtualDrags: this.targetOwners.size
|
|
5359
5377
|
};
|
|
5360
5378
|
}
|
|
5361
5379
|
setScene(channel, ui) {
|
|
@@ -5381,6 +5399,14 @@ var PointerRouting = class {
|
|
|
5381
5399
|
}
|
|
5382
5400
|
}
|
|
5383
5401
|
emit(gesture, type, info) {
|
|
5402
|
+
if (gesture.target) {
|
|
5403
|
+
const phase = type === PointerDown ? "down" : type === PointerMove ? "move" : type === PointerUp ? "up" : type === PointerCancel ? "cancel" : void 0;
|
|
5404
|
+
if (phase) {
|
|
5405
|
+
this.deliveries++;
|
|
5406
|
+
gesture.target.deliver(phase, info);
|
|
5407
|
+
}
|
|
5408
|
+
return;
|
|
5409
|
+
}
|
|
5384
5410
|
if (gesture.channel.disposed || gesture.node?.disposed) return;
|
|
5385
5411
|
this.deliveries++;
|
|
5386
5412
|
(gesture.node?.events ?? gesture.channel.events).emit(type, info);
|
|
@@ -5388,7 +5414,7 @@ var PointerRouting = class {
|
|
|
5388
5414
|
inside(g, data) {
|
|
5389
5415
|
return Object.freeze({
|
|
5390
5416
|
...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
|
|
5417
|
+
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
5418
|
});
|
|
5393
5419
|
}
|
|
5394
5420
|
down(data) {
|
|
@@ -5398,6 +5424,11 @@ var PointerRouting = class {
|
|
|
5398
5424
|
this.suppressed.delete(data.pointerId);
|
|
5399
5425
|
const pick = this.ui?.pick(data.screen.x / this.ui.scale, data.screen.y / this.ui.scale);
|
|
5400
5426
|
const node = pick?.node, bar = pick?.bar;
|
|
5427
|
+
const target = !node && data.button === 0 ? this.channel.pickTarget(data) : void 0;
|
|
5428
|
+
if (target && this.targetOwners.has(target)) {
|
|
5429
|
+
this.suppressed.add(data.pointerId);
|
|
5430
|
+
return;
|
|
5431
|
+
}
|
|
5401
5432
|
if (bar && data.button === 0 && (!bar.enabled || this.scrollOwners.has(bar.node))) {
|
|
5402
5433
|
this.suppressed.add(data.pointerId);
|
|
5403
5434
|
return node;
|
|
@@ -5406,20 +5437,24 @@ var PointerRouting = class {
|
|
|
5406
5437
|
this.suppressed.add(data.pointerId);
|
|
5407
5438
|
return node;
|
|
5408
5439
|
}
|
|
5409
|
-
if (!node && !data.inside) return;
|
|
5440
|
+
if (!node && !target && !data.inside) return;
|
|
5410
5441
|
const g = {
|
|
5411
5442
|
channel: this.channel,
|
|
5412
5443
|
last: data,
|
|
5413
5444
|
primary: data.button === 0,
|
|
5414
5445
|
start: data,
|
|
5415
5446
|
scale: this.ui?.scale ?? 1,
|
|
5416
|
-
candidates: this.ui?.scrollTargets(data.screen.x / this.ui.scale, data.screen.y / this.ui.scale) ?? [],
|
|
5447
|
+
candidates: !target ? this.ui?.scrollTargets(data.screen.x / this.ui.scale, data.screen.y / this.ui.scale) ?? [] : [],
|
|
5417
5448
|
candidateRevisions: [],
|
|
5418
5449
|
vx: 0,
|
|
5419
5450
|
vy: 0,
|
|
5420
5451
|
...node ? {
|
|
5421
5452
|
node,
|
|
5422
5453
|
ui: this.ui
|
|
5454
|
+
} : {},
|
|
5455
|
+
...target ? {
|
|
5456
|
+
target,
|
|
5457
|
+
targetRevision: target.revision
|
|
5423
5458
|
} : {}
|
|
5424
5459
|
};
|
|
5425
5460
|
g.candidateRevisions = g.candidates.map((n) => n.scrollRevision);
|
|
@@ -5441,6 +5476,17 @@ var PointerRouting = class {
|
|
|
5441
5476
|
if (node?.control) g.controlRevision = node.control.revision;
|
|
5442
5477
|
this.gestures.set(data.pointerId, g);
|
|
5443
5478
|
if (node) g.off = node.onDispose(() => this.cancel(data.pointerId));
|
|
5479
|
+
if (target) {
|
|
5480
|
+
this.targetOwners.set(target, data.pointerId);
|
|
5481
|
+
const off = target.subscribe(() => {
|
|
5482
|
+
if (!target.available || target.revision !== g.targetRevision) this.cancel(data.pointerId);
|
|
5483
|
+
});
|
|
5484
|
+
const disposed = target.onDispose(() => this.cancel(data.pointerId));
|
|
5485
|
+
g.off = () => {
|
|
5486
|
+
off();
|
|
5487
|
+
disposed();
|
|
5488
|
+
};
|
|
5489
|
+
}
|
|
5444
5490
|
cleanupAll([
|
|
5445
5491
|
() => {
|
|
5446
5492
|
const port = node?.control;
|
|
@@ -5498,7 +5544,7 @@ var PointerRouting = class {
|
|
|
5498
5544
|
const dx = Math.abs(data.screen.x - g.start.screen.x), dy = Math.abs(data.screen.y - g.start.screen.y);
|
|
5499
5545
|
if (dx >= g.control.style.dragThreshold && dx > dy) g.controlLocked = true;
|
|
5500
5546
|
}
|
|
5501
|
-
if (!g.scroll && g.primary && !g.controlLocked) this.claimScroll(g, data);
|
|
5547
|
+
if (!g.target && !g.scroll && g.primary && !g.controlLocked) this.claimScroll(g, data);
|
|
5502
5548
|
if (this.gestures.get(data.pointerId) !== g) return;
|
|
5503
5549
|
if (g.bar && g.scroll && g.ui) {
|
|
5504
5550
|
const bar = canvasScrollbars(g.scroll).find((b) => b.axis === g.bar?.axis);
|
|
@@ -5639,6 +5685,7 @@ var PointerRouting = class {
|
|
|
5639
5685
|
g = this.gestures.get(data.pointerId);
|
|
5640
5686
|
if (!g) return;
|
|
5641
5687
|
this.gestures.delete(data.pointerId);
|
|
5688
|
+
if (g.target) this.targetOwners.delete(g.target);
|
|
5642
5689
|
if (g.control) this.controlOwners.delete(g.control);
|
|
5643
5690
|
if (g.scroll) {
|
|
5644
5691
|
this.scrollOwners.delete(g.scroll);
|
|
@@ -5656,7 +5703,7 @@ var PointerRouting = class {
|
|
|
5656
5703
|
if (!g.node || g.node.available) this.emit(g, PointerUp, info);
|
|
5657
5704
|
},
|
|
5658
5705
|
() => {
|
|
5659
|
-
if (!g.scroll && g.primary && info.inside && (!g.node || g.node.available)) this.emit(g, PointerClick, info);
|
|
5706
|
+
if (!g.target && !g.scroll && g.primary && info.inside && (!g.node || g.node.available)) this.emit(g, PointerClick, info);
|
|
5660
5707
|
},
|
|
5661
5708
|
() => {
|
|
5662
5709
|
if (!g.scroll && g.primary && info.inside && g.node?.available) g.node.activate("pointer");
|
|
@@ -5668,6 +5715,7 @@ var PointerRouting = class {
|
|
|
5668
5715
|
const g = this.gestures.get(id);
|
|
5669
5716
|
if (!g) return;
|
|
5670
5717
|
this.gestures.delete(id);
|
|
5718
|
+
if (g.target) this.targetOwners.delete(g.target);
|
|
5671
5719
|
if (g.control) this.controlOwners.delete(g.control);
|
|
5672
5720
|
if (g.scroll) {
|
|
5673
5721
|
this.scrollOwners.delete(g.scroll);
|
|
@@ -5738,6 +5786,10 @@ var PointerRouting = class {
|
|
|
5738
5786
|
...[...this.keys.keys()].map((n) => () => this.releaseKeys(n))
|
|
5739
5787
|
], "Pointer release failed");
|
|
5740
5788
|
}
|
|
5789
|
+
/** @internal Host focus changes interrupt virtual devices without changing Canvas editing rules. */
|
|
5790
|
+
releaseVirtual() {
|
|
5791
|
+
cleanupAll([...this.gestures].filter(([, g]) => g.target).map(([id]) => () => this.cancel(id)), "Virtual pointer release failed");
|
|
5792
|
+
}
|
|
5741
5793
|
dispose() {
|
|
5742
5794
|
try {
|
|
5743
5795
|
this.reset();
|
|
@@ -5753,6 +5805,716 @@ var PointerRouting = class {
|
|
|
5753
5805
|
cleanupAll([...off, () => this.release()], "Pointer routing cleanup failed");
|
|
5754
5806
|
}
|
|
5755
5807
|
};
|
|
5808
|
+
// packages/core/src/interaction/virtual-regions.ts
|
|
5809
|
+
var VirtualRegions = Object.freeze({
|
|
5810
|
+
/** A shared edge is not an overlap. Use separate rectangles to leave a center gap. */
|
|
5811
|
+
Left: Object.freeze({
|
|
5812
|
+
kind: "rect",
|
|
5813
|
+
x: 0,
|
|
5814
|
+
y: 0,
|
|
5815
|
+
width: .5,
|
|
5816
|
+
height: 1
|
|
5817
|
+
}),
|
|
5818
|
+
Right: Object.freeze({
|
|
5819
|
+
kind: "rect",
|
|
5820
|
+
x: .5,
|
|
5821
|
+
y: 0,
|
|
5822
|
+
width: .5,
|
|
5823
|
+
height: 1
|
|
5824
|
+
})
|
|
5825
|
+
});
|
|
5826
|
+
/** @internal */
|
|
5827
|
+
function virtualFinite(value, field) {
|
|
5828
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new EngineError("INVALID_VIRTUAL_INPUT", `${field} must be finite`);
|
|
5829
|
+
return value;
|
|
5830
|
+
}
|
|
5831
|
+
/** @internal */
|
|
5832
|
+
function virtualBounds(value) {
|
|
5833
|
+
const result = {
|
|
5834
|
+
x: virtualFinite(value.x, "x"),
|
|
5835
|
+
y: virtualFinite(value.y, "y"),
|
|
5836
|
+
width: virtualFinite(value.width, "width"),
|
|
5837
|
+
height: virtualFinite(value.height, "height")
|
|
5838
|
+
};
|
|
5839
|
+
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");
|
|
5840
|
+
return Object.freeze(result);
|
|
5841
|
+
}
|
|
5842
|
+
/** @internal */
|
|
5843
|
+
function virtualPosition(value, viewport) {
|
|
5844
|
+
const space = value.space ?? "viewport";
|
|
5845
|
+
if (space !== "viewport" && space !== "screen") throw new EngineError("INVALID_VIRTUAL_INPUT", "Unknown virtual coordinate space");
|
|
5846
|
+
const x = virtualFinite(value.x, "position.x"), y = virtualFinite(value.y, "position.y");
|
|
5847
|
+
return Object.freeze(space === "screen" ? {
|
|
5848
|
+
x,
|
|
5849
|
+
y
|
|
5850
|
+
} : {
|
|
5851
|
+
x: virtualFinite(viewport.x + x * viewport.width, "resolved x"),
|
|
5852
|
+
y: virtualFinite(viewport.y + y * viewport.height, "resolved y")
|
|
5853
|
+
});
|
|
5854
|
+
}
|
|
5855
|
+
/** @internal Copy configuration before retaining it. Predicate functions are deliberately live. */
|
|
5856
|
+
function checkedVirtualRegion(region) {
|
|
5857
|
+
if (!region || ![
|
|
5858
|
+
"rect",
|
|
5859
|
+
"circle",
|
|
5860
|
+
"custom"
|
|
5861
|
+
].includes(region.kind)) throw new EngineError("INVALID_VIRTUAL_INPUT", "Unknown activation region");
|
|
5862
|
+
const copy = Object.freeze({ ...region });
|
|
5863
|
+
if (copy.kind === "custom") {
|
|
5864
|
+
if (typeof copy.bounds !== "function" || typeof copy.contains !== "function") throw new EngineError("INVALID_VIRTUAL_INPUT", "Custom regions require bounds and contains");
|
|
5865
|
+
} else resolveVirtualRegion(copy, {
|
|
5866
|
+
x: 0,
|
|
5867
|
+
y: 0,
|
|
5868
|
+
width: 1,
|
|
5869
|
+
height: 1
|
|
5870
|
+
});
|
|
5871
|
+
return copy;
|
|
5872
|
+
}
|
|
5873
|
+
/** @internal */
|
|
5874
|
+
function resolveVirtualRegion(region, viewport) {
|
|
5875
|
+
if (region.kind === "custom") {
|
|
5876
|
+
if (typeof region.bounds !== "function" || typeof region.contains !== "function") throw new EngineError("INVALID_VIRTUAL_INPUT", "Custom regions require bounds and contains functions");
|
|
5877
|
+
return {
|
|
5878
|
+
kind: "custom",
|
|
5879
|
+
bounds: virtualBounds(region.bounds(viewport)),
|
|
5880
|
+
test: region.contains
|
|
5881
|
+
};
|
|
5882
|
+
}
|
|
5883
|
+
const center = virtualPosition(region, viewport);
|
|
5884
|
+
if (region.kind === "rect") {
|
|
5885
|
+
const relative = (region.space ?? "viewport") === "viewport";
|
|
5886
|
+
const width = virtualFinite(region.width, "region.width"), height = virtualFinite(region.height, "region.height");
|
|
5887
|
+
if (width < 0 || height < 0) throw new EngineError("INVALID_VIRTUAL_INPUT", "Region sizes must be nonnegative");
|
|
5888
|
+
return {
|
|
5889
|
+
kind: "rect",
|
|
5890
|
+
bounds: virtualBounds({
|
|
5891
|
+
...center,
|
|
5892
|
+
width: width * (relative ? viewport.width : 1),
|
|
5893
|
+
height: height * (relative ? viewport.height : 1)
|
|
5894
|
+
})
|
|
5895
|
+
};
|
|
5896
|
+
}
|
|
5897
|
+
const radius = virtualFinite(region.radius, "region.radius") * ((region.space ?? "viewport") === "viewport" ? Math.min(viewport.width, viewport.height) : 1);
|
|
5898
|
+
if (radius < 0) throw new EngineError("INVALID_VIRTUAL_INPUT", "Region radius must be nonnegative");
|
|
5899
|
+
return {
|
|
5900
|
+
kind: "circle",
|
|
5901
|
+
center,
|
|
5902
|
+
radius,
|
|
5903
|
+
bounds: virtualBounds({
|
|
5904
|
+
x: center.x - radius,
|
|
5905
|
+
y: center.y - radius,
|
|
5906
|
+
width: radius * 2,
|
|
5907
|
+
height: radius * 2
|
|
5908
|
+
})
|
|
5909
|
+
};
|
|
5910
|
+
}
|
|
5911
|
+
/** @internal Rectangles are half-open; circles exclude their circumference. */
|
|
5912
|
+
function virtualContains(region, point, viewport) {
|
|
5913
|
+
const b = region.bounds;
|
|
5914
|
+
if (point.x < b.x || point.y < b.y || point.x >= b.x + b.width || point.y >= b.y + b.height) return false;
|
|
5915
|
+
if (region.kind === "circle") return Math.hypot(point.x - region.center.x, point.y - region.center.y) < region.radius;
|
|
5916
|
+
if (region.kind === "custom") {
|
|
5917
|
+
const value = region.test(point, viewport);
|
|
5918
|
+
if (typeof value !== "boolean") throw new EngineError("INVALID_VIRTUAL_INPUT", "Custom region predicate must return boolean");
|
|
5919
|
+
return value;
|
|
5920
|
+
}
|
|
5921
|
+
return true;
|
|
5922
|
+
}
|
|
5923
|
+
/** @internal Exact built-in intersection; custom bounds are conservatively ambiguous. */
|
|
5924
|
+
function virtualOverlap(a, b) {
|
|
5925
|
+
const x = a.bounds, y = b.bounds;
|
|
5926
|
+
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;
|
|
5927
|
+
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;
|
|
5928
|
+
const circle = a.kind === "circle" ? a : b.kind === "circle" ? b : void 0;
|
|
5929
|
+
const rect = a.kind === "circle" ? b : a;
|
|
5930
|
+
if (circle && rect.kind === "rect") {
|
|
5931
|
+
const c = circle.center, r = rect.bounds;
|
|
5932
|
+
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;
|
|
5933
|
+
}
|
|
5934
|
+
return true;
|
|
5935
|
+
}
|
|
5936
|
+
// packages/core/src/interaction/virtual-control.ts
|
|
5937
|
+
var VirtualStickMode = {
|
|
5938
|
+
Fixed: "fixed",
|
|
5939
|
+
Floating: "floating",
|
|
5940
|
+
Follow: "follow"
|
|
5941
|
+
};
|
|
5942
|
+
var screenPoint$1 = (x, y) => ({
|
|
5943
|
+
space: "screen",
|
|
5944
|
+
x: virtualFinite(x, "screen.x"),
|
|
5945
|
+
y: virtualFinite(y, "screen.y")
|
|
5946
|
+
});
|
|
5947
|
+
var zero = Object.freeze({
|
|
5948
|
+
x: 0,
|
|
5949
|
+
y: 0
|
|
5950
|
+
});
|
|
5951
|
+
var Start = defineEvent("VirtualStart");
|
|
5952
|
+
var Change = defineEvent("VirtualChange");
|
|
5953
|
+
var End = defineEvent("VirtualEnd");
|
|
5954
|
+
var Cancel = defineEvent("VirtualCancel");
|
|
5955
|
+
var Hold = defineEvent("VirtualHold");
|
|
5956
|
+
function flag(value) {
|
|
5957
|
+
if (typeof value !== "boolean") throw new EngineError("INVALID_VIRTUAL_INPUT", "Virtual control flags must be boolean");
|
|
5958
|
+
return value;
|
|
5959
|
+
}
|
|
5960
|
+
function zone(value) {
|
|
5961
|
+
virtualFinite(value, "deadZone");
|
|
5962
|
+
if (value < 0 || value >= 1) throw new EngineError("INVALID_VIRTUAL_INPUT", "Dead zone must satisfy 0 <= value < 1");
|
|
5963
|
+
return value;
|
|
5964
|
+
}
|
|
5965
|
+
function mode(value) {
|
|
5966
|
+
if (!Object.values(VirtualStickMode).includes(value)) throw new EngineError("INVALID_VIRTUAL_INPUT", "Unknown virtual stick mode");
|
|
5967
|
+
return value;
|
|
5968
|
+
}
|
|
5969
|
+
/** @internal Validate before allocating ActionSources or registering a target. */
|
|
5970
|
+
function checkVirtualOptions(options) {
|
|
5971
|
+
checkedVirtualRegion(options.region);
|
|
5972
|
+
positive(options.radius ?? 64, "radius");
|
|
5973
|
+
zone(options.deadZone ?? .15);
|
|
5974
|
+
flag(options.enabled ?? true);
|
|
5975
|
+
flag(options.visible ?? true);
|
|
5976
|
+
flag(options.direction ?? false);
|
|
5977
|
+
mode(options.mode ?? VirtualStickMode.Fixed);
|
|
5978
|
+
if (virtualFinite(options.holdDelay ?? .5, "holdDelay") < 0) throw new EngineError("INVALID_VIRTUAL_INPUT", "Hold delay must be nonnegative");
|
|
5979
|
+
if (options.position) {
|
|
5980
|
+
virtualFinite(options.position.x, "position.x");
|
|
5981
|
+
virtualFinite(options.position.y, "position.y");
|
|
5982
|
+
if (options.position.space !== void 0 && options.position.space !== "screen" && options.position.space !== "viewport") throw new EngineError("INVALID_VIRTUAL_INPUT", "Unknown position space");
|
|
5983
|
+
}
|
|
5984
|
+
const color = options.color ?? 6736821;
|
|
5985
|
+
if (!Number.isInteger(color) || color < 0 || color > 16777215) throw new EngineError("INVALID_VIRTUAL_INPUT", "Virtual color must be RGB");
|
|
5986
|
+
}
|
|
5987
|
+
/** Portable gesture state. Routing, Action composition and rendering retain their existing owners. */
|
|
5988
|
+
var VirtualControl = class {
|
|
5989
|
+
kind;
|
|
5990
|
+
host;
|
|
5991
|
+
binding;
|
|
5992
|
+
life = new Lifetime();
|
|
5993
|
+
events = new EventBus();
|
|
5994
|
+
observers = /* @__PURE__ */ new Set();
|
|
5995
|
+
activation;
|
|
5996
|
+
activeFlag;
|
|
5997
|
+
shown;
|
|
5998
|
+
size;
|
|
5999
|
+
deadZoneValue;
|
|
6000
|
+
version = 0;
|
|
6001
|
+
gesture;
|
|
6002
|
+
vector = zero;
|
|
6003
|
+
dragMode;
|
|
6004
|
+
position;
|
|
6005
|
+
holdDelay;
|
|
6006
|
+
color;
|
|
6007
|
+
directional;
|
|
6008
|
+
constructor(kind, options, host, binding) {
|
|
6009
|
+
this.kind = kind;
|
|
6010
|
+
this.host = host;
|
|
6011
|
+
this.binding = binding;
|
|
6012
|
+
checkVirtualOptions(options);
|
|
6013
|
+
this.activation = checkedVirtualRegion(options.region);
|
|
6014
|
+
this.position = options.position ? Object.freeze({ ...options.position }) : void 0;
|
|
6015
|
+
this.activeFlag = options.enabled ?? true;
|
|
6016
|
+
this.shown = options.visible ?? true;
|
|
6017
|
+
this.size = options.radius ?? (kind === "stick" ? 64 : 28);
|
|
6018
|
+
this.deadZoneValue = options.deadZone ?? .15;
|
|
6019
|
+
this.dragMode = mode(options.mode ?? VirtualStickMode.Fixed);
|
|
6020
|
+
this.holdDelay = options.holdDelay ?? .5;
|
|
6021
|
+
this.color = options.color ?? 6736821;
|
|
6022
|
+
this.directional = kind === "stick" || (options.direction ?? false);
|
|
6023
|
+
this.life.onDispose(binding.onDispose(() => this.dispose()));
|
|
6024
|
+
}
|
|
6025
|
+
get disposed() {
|
|
6026
|
+
return this.life.disposed;
|
|
6027
|
+
}
|
|
6028
|
+
get available() {
|
|
6029
|
+
return !this.disposed && this.activeFlag && this.host.available() && this.binding.active();
|
|
6030
|
+
}
|
|
6031
|
+
get revision() {
|
|
6032
|
+
return this.version;
|
|
6033
|
+
}
|
|
6034
|
+
get enabled() {
|
|
6035
|
+
return this.activeFlag;
|
|
6036
|
+
}
|
|
6037
|
+
set enabled(value) {
|
|
6038
|
+
this.alive();
|
|
6039
|
+
const next = flag(value);
|
|
6040
|
+
if (next === this.activeFlag) return;
|
|
6041
|
+
this.activeFlag = next;
|
|
6042
|
+
this.interrupt();
|
|
6043
|
+
}
|
|
6044
|
+
get visible() {
|
|
6045
|
+
return this.shown;
|
|
6046
|
+
}
|
|
6047
|
+
set visible(value) {
|
|
6048
|
+
this.alive();
|
|
6049
|
+
this.shown = flag(value);
|
|
6050
|
+
}
|
|
6051
|
+
get radius() {
|
|
6052
|
+
return this.size;
|
|
6053
|
+
}
|
|
6054
|
+
set radius(value) {
|
|
6055
|
+
this.alive();
|
|
6056
|
+
const next = positive(value, "radius");
|
|
6057
|
+
if (next === this.size) return;
|
|
6058
|
+
this.size = next;
|
|
6059
|
+
this.interrupt();
|
|
6060
|
+
}
|
|
6061
|
+
get deadZone() {
|
|
6062
|
+
return this.deadZoneValue;
|
|
6063
|
+
}
|
|
6064
|
+
set deadZone(value) {
|
|
6065
|
+
this.alive();
|
|
6066
|
+
const next = zone(value);
|
|
6067
|
+
if (next === this.deadZoneValue) return;
|
|
6068
|
+
this.deadZoneValue = next;
|
|
6069
|
+
this.interrupt();
|
|
6070
|
+
}
|
|
6071
|
+
get region() {
|
|
6072
|
+
return this.activation;
|
|
6073
|
+
}
|
|
6074
|
+
set region(value) {
|
|
6075
|
+
this.alive();
|
|
6076
|
+
const next = checkedVirtualRegion(value);
|
|
6077
|
+
this.host.region(this, next);
|
|
6078
|
+
this.activation = next;
|
|
6079
|
+
this.interrupt();
|
|
6080
|
+
}
|
|
6081
|
+
get stickMode() {
|
|
6082
|
+
return this.dragMode;
|
|
6083
|
+
}
|
|
6084
|
+
set stickMode(value) {
|
|
6085
|
+
this.alive();
|
|
6086
|
+
const next = mode(value);
|
|
6087
|
+
if (next === this.dragMode) return;
|
|
6088
|
+
this.dragMode = next;
|
|
6089
|
+
this.interrupt();
|
|
6090
|
+
}
|
|
6091
|
+
get pointerId() {
|
|
6092
|
+
return this.gesture?.last.pointerId;
|
|
6093
|
+
}
|
|
6094
|
+
get pressed() {
|
|
6095
|
+
return this.gesture !== void 0;
|
|
6096
|
+
}
|
|
6097
|
+
get elapsed() {
|
|
6098
|
+
return this.gesture?.elapsed ?? 0;
|
|
6099
|
+
}
|
|
6100
|
+
get direction() {
|
|
6101
|
+
return this.vector;
|
|
6102
|
+
}
|
|
6103
|
+
get center() {
|
|
6104
|
+
const p = this.gesture?.center ?? this.host.center(this);
|
|
6105
|
+
return Object.freeze(screenPoint$1(p.x, p.y));
|
|
6106
|
+
}
|
|
6107
|
+
get knob() {
|
|
6108
|
+
const center = this.center, last = this.gesture?.last.screen;
|
|
6109
|
+
if (!last || !this.directional) return center;
|
|
6110
|
+
const origin = this.kind === "button" ? this.gesture?.origin ?? center : center;
|
|
6111
|
+
const dx = last.x - origin.x, dy = last.y - origin.y, length = Math.hypot(dx, dy);
|
|
6112
|
+
const scale = length > this.size ? this.size / length : 1;
|
|
6113
|
+
return Object.freeze(screenPoint$1(center.x + dx * scale, center.y + dy * scale));
|
|
6114
|
+
}
|
|
6115
|
+
onDispose(callback) {
|
|
6116
|
+
return this.life.onDispose(callback);
|
|
6117
|
+
}
|
|
6118
|
+
/** @internal Geometry/availability invalidation, not a per-frame observer. */
|
|
6119
|
+
subscribe(callback) {
|
|
6120
|
+
this.alive();
|
|
6121
|
+
this.observers.add(callback);
|
|
6122
|
+
return () => {
|
|
6123
|
+
this.observers.delete(callback);
|
|
6124
|
+
};
|
|
6125
|
+
}
|
|
6126
|
+
onStart(callback, owner) {
|
|
6127
|
+
return this.events.on(Start, callback, owner);
|
|
6128
|
+
}
|
|
6129
|
+
onChange(callback, owner) {
|
|
6130
|
+
return this.events.on(Change, callback, owner);
|
|
6131
|
+
}
|
|
6132
|
+
onEnd(callback, owner) {
|
|
6133
|
+
return this.events.on(End, callback, owner);
|
|
6134
|
+
}
|
|
6135
|
+
onCancel(callback, owner) {
|
|
6136
|
+
return this.events.on(Cancel, callback, owner);
|
|
6137
|
+
}
|
|
6138
|
+
onHold(callback, owner) {
|
|
6139
|
+
return this.events.on(Hold, callback, owner);
|
|
6140
|
+
}
|
|
6141
|
+
/** Release/cancel only; the captured physical ID stays suppressed until its release. */
|
|
6142
|
+
cancel() {
|
|
6143
|
+
this.alive();
|
|
6144
|
+
this.interrupt();
|
|
6145
|
+
}
|
|
6146
|
+
/** @internal */
|
|
6147
|
+
invalidate() {
|
|
6148
|
+
this.interrupt();
|
|
6149
|
+
}
|
|
6150
|
+
alive() {
|
|
6151
|
+
if (this.disposed) throw new EngineError("OWNER_DISPOSED", "Virtual control has been disposed");
|
|
6152
|
+
}
|
|
6153
|
+
interrupt() {
|
|
6154
|
+
this.version++;
|
|
6155
|
+
cleanupAll([...[...this.observers], () => {
|
|
6156
|
+
if (this.gesture) this.finish(true);
|
|
6157
|
+
else this.binding.clear();
|
|
6158
|
+
}], "Virtual gesture interruption failed");
|
|
6159
|
+
}
|
|
6160
|
+
contains(data) {
|
|
6161
|
+
return this.host.contains(this, data);
|
|
6162
|
+
}
|
|
6163
|
+
snapshot(g) {
|
|
6164
|
+
return Object.freeze({
|
|
6165
|
+
control: this,
|
|
6166
|
+
pointerId: g.last.pointerId,
|
|
6167
|
+
pointerType: g.last.pointerType,
|
|
6168
|
+
origin: g.origin,
|
|
6169
|
+
position: Object.freeze(screenPoint$1(g.last.screen.x, g.last.screen.y)),
|
|
6170
|
+
center: g.center,
|
|
6171
|
+
direction: this.vector,
|
|
6172
|
+
inside: this.contains(g.last),
|
|
6173
|
+
elapsed: g.elapsed
|
|
6174
|
+
});
|
|
6175
|
+
}
|
|
6176
|
+
update(g) {
|
|
6177
|
+
if (!this.directional) {
|
|
6178
|
+
this.vector = zero;
|
|
6179
|
+
return;
|
|
6180
|
+
}
|
|
6181
|
+
const start = this.kind === "button" ? g.origin : g.center;
|
|
6182
|
+
const dx = g.last.screen.x - start.x, dy = g.last.screen.y - start.y;
|
|
6183
|
+
const length = Math.hypot(dx, dy);
|
|
6184
|
+
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));
|
|
6185
|
+
const magnitude = Math.min(1, length / this.size);
|
|
6186
|
+
const output = magnitude <= this.deadZoneValue ? 0 : (magnitude - this.deadZoneValue) / (1 - this.deadZoneValue);
|
|
6187
|
+
this.vector = output === 0 ? zero : Object.freeze({
|
|
6188
|
+
x: dx / length * output,
|
|
6189
|
+
y: dy / length * output
|
|
6190
|
+
});
|
|
6191
|
+
}
|
|
6192
|
+
deliver(type, data) {
|
|
6193
|
+
if (type === "cancel") {
|
|
6194
|
+
if (this.gesture?.last.pointerId === data.pointerId) this.finish(true);
|
|
6195
|
+
return;
|
|
6196
|
+
}
|
|
6197
|
+
if (type === "down") {
|
|
6198
|
+
if (!this.available || this.gesture) return;
|
|
6199
|
+
const center = this.kind === "stick" && this.dragMode !== VirtualStickMode.Fixed ? data.screen : this.host.center(this);
|
|
6200
|
+
const g = {
|
|
6201
|
+
last: data,
|
|
6202
|
+
origin: Object.freeze(screenPoint$1(data.screen.x, data.screen.y)),
|
|
6203
|
+
center: Object.freeze(screenPoint$1(center.x, center.y)),
|
|
6204
|
+
elapsed: 0,
|
|
6205
|
+
held: false
|
|
6206
|
+
};
|
|
6207
|
+
this.gesture = g;
|
|
6208
|
+
this.update(g);
|
|
6209
|
+
this.binding.write(this.vector, true);
|
|
6210
|
+
const event = this.snapshot(g);
|
|
6211
|
+
cleanupAll([() => this.events.emit(Start, event), () => {
|
|
6212
|
+
if (this.gesture === g) this.events.emit(Change, event);
|
|
6213
|
+
}], "Virtual start handlers failed");
|
|
6214
|
+
return;
|
|
6215
|
+
}
|
|
6216
|
+
const g = this.gesture;
|
|
6217
|
+
if (!g || g.last.pointerId !== data.pointerId) return;
|
|
6218
|
+
if (!this.available) {
|
|
6219
|
+
this.interrupt();
|
|
6220
|
+
return;
|
|
6221
|
+
}
|
|
6222
|
+
if (data.screen.x !== g.last.screen.x || data.screen.y !== g.last.screen.y) {
|
|
6223
|
+
g.last = data;
|
|
6224
|
+
this.update(g);
|
|
6225
|
+
this.binding.write(this.vector, true);
|
|
6226
|
+
this.events.emit(Change, this.snapshot(g));
|
|
6227
|
+
}
|
|
6228
|
+
if (type === "up" && this.gesture === g) {
|
|
6229
|
+
g.last = data;
|
|
6230
|
+
this.finish(false);
|
|
6231
|
+
}
|
|
6232
|
+
}
|
|
6233
|
+
/** @internal Explicit presentation clock; no browser/OS timer in Core. */
|
|
6234
|
+
advance(seconds) {
|
|
6235
|
+
const g = this.gesture;
|
|
6236
|
+
if (!g) return;
|
|
6237
|
+
if (!this.available) {
|
|
6238
|
+
this.interrupt();
|
|
6239
|
+
return;
|
|
6240
|
+
}
|
|
6241
|
+
g.elapsed += seconds;
|
|
6242
|
+
if (!g.held && g.elapsed >= this.holdDelay) {
|
|
6243
|
+
g.held = true;
|
|
6244
|
+
this.events.emit(Hold, this.snapshot(g));
|
|
6245
|
+
}
|
|
6246
|
+
}
|
|
6247
|
+
finish(cancelled) {
|
|
6248
|
+
const g = this.gesture;
|
|
6249
|
+
if (!g) return;
|
|
6250
|
+
let event;
|
|
6251
|
+
try {
|
|
6252
|
+
event = this.snapshot(g);
|
|
6253
|
+
} finally {
|
|
6254
|
+
this.gesture = void 0;
|
|
6255
|
+
this.vector = zero;
|
|
6256
|
+
this.binding.clear();
|
|
6257
|
+
}
|
|
6258
|
+
this.events.emit(cancelled ? Cancel : End, event);
|
|
6259
|
+
}
|
|
6260
|
+
dispose() {
|
|
6261
|
+
if (this.disposed) return;
|
|
6262
|
+
cleanupAll([
|
|
6263
|
+
() => this.life.dispose(),
|
|
6264
|
+
() => this.interrupt(),
|
|
6265
|
+
() => this.binding.dispose(),
|
|
6266
|
+
() => this.events.dispose(),
|
|
6267
|
+
() => this.observers.clear(),
|
|
6268
|
+
() => this.host.remove(this)
|
|
6269
|
+
], "Virtual control cleanup failed");
|
|
6270
|
+
}
|
|
6271
|
+
};
|
|
6272
|
+
var VirtualStick = class extends VirtualControl {
|
|
6273
|
+
/** @internal Use VirtualInput.stick. */
|
|
6274
|
+
constructor(options, host, binding) {
|
|
6275
|
+
super("stick", options, host, binding);
|
|
6276
|
+
}
|
|
6277
|
+
get value() {
|
|
6278
|
+
return this.direction;
|
|
6279
|
+
}
|
|
6280
|
+
get mode() {
|
|
6281
|
+
return this.stickMode;
|
|
6282
|
+
}
|
|
6283
|
+
set mode(value) {
|
|
6284
|
+
this.stickMode = value;
|
|
6285
|
+
}
|
|
6286
|
+
};
|
|
6287
|
+
var VirtualButton = class extends VirtualControl {
|
|
6288
|
+
/** @internal Use VirtualInput.button. */
|
|
6289
|
+
constructor(options, host, binding) {
|
|
6290
|
+
super("button", options, host, binding);
|
|
6291
|
+
}
|
|
6292
|
+
get value() {
|
|
6293
|
+
return this.pressed;
|
|
6294
|
+
}
|
|
6295
|
+
/** Confirmation on normal release, including a drag outside the activation region; never on cancel. */
|
|
6296
|
+
onCommit(callback, owner) {
|
|
6297
|
+
return this.onEnd(callback, owner);
|
|
6298
|
+
}
|
|
6299
|
+
};
|
|
6300
|
+
// packages/core/src/interaction/virtual-input.ts
|
|
6301
|
+
/** Scene-scoped virtual devices; all gameplay values flow through existing ActionSources. */
|
|
6302
|
+
var VirtualInput = class {
|
|
6303
|
+
pointer;
|
|
6304
|
+
input;
|
|
6305
|
+
life = new Lifetime();
|
|
6306
|
+
list = Object.freeze([]);
|
|
6307
|
+
regions = /* @__PURE__ */ new Map();
|
|
6308
|
+
anchors = /* @__PURE__ */ new Map();
|
|
6309
|
+
view = Object.freeze({
|
|
6310
|
+
x: 0,
|
|
6311
|
+
y: 0,
|
|
6312
|
+
width: 800,
|
|
6313
|
+
height: 450
|
|
6314
|
+
});
|
|
6315
|
+
active = true;
|
|
6316
|
+
shown = true;
|
|
6317
|
+
dirty = true;
|
|
6318
|
+
host;
|
|
6319
|
+
constructor(pointer, input) {
|
|
6320
|
+
this.pointer = pointer;
|
|
6321
|
+
this.input = input;
|
|
6322
|
+
if (pointer.disposed) throw new EngineError("OWNER_DISPOSED", "Scene pointer has been disposed");
|
|
6323
|
+
this.host = {
|
|
6324
|
+
available: () => !this.disposed && this.active && input.liveInput,
|
|
6325
|
+
center: (control) => this.anchors.get(control) ?? {
|
|
6326
|
+
x: 0,
|
|
6327
|
+
y: 0
|
|
6328
|
+
},
|
|
6329
|
+
contains: (control, data) => {
|
|
6330
|
+
const region = this.regions.get(control);
|
|
6331
|
+
return region ? virtualContains(region, data.screen, this.view) : false;
|
|
6332
|
+
},
|
|
6333
|
+
region: (control, region) => {
|
|
6334
|
+
const regions = this.resolve(this.view, control, region);
|
|
6335
|
+
this.regions = regions;
|
|
6336
|
+
this.anchors = this.centers(regions, this.view);
|
|
6337
|
+
this.dirty = true;
|
|
6338
|
+
},
|
|
6339
|
+
remove: (control) => {
|
|
6340
|
+
this.list = Object.freeze(this.list.filter((c) => c !== control));
|
|
6341
|
+
this.regions.delete(control);
|
|
6342
|
+
this.anchors.delete(control);
|
|
6343
|
+
}
|
|
6344
|
+
};
|
|
6345
|
+
try {
|
|
6346
|
+
this.life.onDispose(pointer.attachTargets(this));
|
|
6347
|
+
this.life.onDispose(input.addAdapter(this));
|
|
6348
|
+
this.life.onDispose(pointer.onDispose(() => this.dispose()));
|
|
6349
|
+
} catch (error) {
|
|
6350
|
+
this.life.dispose();
|
|
6351
|
+
throw error;
|
|
6352
|
+
}
|
|
6353
|
+
}
|
|
6354
|
+
get disposed() {
|
|
6355
|
+
return this.life.disposed;
|
|
6356
|
+
}
|
|
6357
|
+
get controls() {
|
|
6358
|
+
return this.list;
|
|
6359
|
+
}
|
|
6360
|
+
get viewport() {
|
|
6361
|
+
return this.view;
|
|
6362
|
+
}
|
|
6363
|
+
get enabled() {
|
|
6364
|
+
return this.active;
|
|
6365
|
+
}
|
|
6366
|
+
set enabled(value) {
|
|
6367
|
+
this.alive();
|
|
6368
|
+
this.flag(value);
|
|
6369
|
+
if (this.active === value) return;
|
|
6370
|
+
this.active = value;
|
|
6371
|
+
this.reset();
|
|
6372
|
+
}
|
|
6373
|
+
get visible() {
|
|
6374
|
+
return this.shown;
|
|
6375
|
+
}
|
|
6376
|
+
set visible(value) {
|
|
6377
|
+
this.alive();
|
|
6378
|
+
this.flag(value);
|
|
6379
|
+
this.shown = value;
|
|
6380
|
+
}
|
|
6381
|
+
onDispose(callback) {
|
|
6382
|
+
return this.life.onDispose(callback);
|
|
6383
|
+
}
|
|
6384
|
+
flag(value) {
|
|
6385
|
+
if (typeof value !== "boolean") throw new EngineError("INVALID_VIRTUAL_INPUT", "Virtual input flags must be boolean");
|
|
6386
|
+
}
|
|
6387
|
+
alive() {
|
|
6388
|
+
if (this.disposed) throw new EngineError("OWNER_DISPOSED", "Virtual input has been disposed");
|
|
6389
|
+
}
|
|
6390
|
+
validate(controller, action, kind, options) {
|
|
6391
|
+
this.alive();
|
|
6392
|
+
checkVirtualOptions(options);
|
|
6393
|
+
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");
|
|
6394
|
+
const region = resolveVirtualRegion(options.region, this.view);
|
|
6395
|
+
const peers = this.resolve(this.view);
|
|
6396
|
+
for (const control of this.list) {
|
|
6397
|
+
const other = peers.get(control);
|
|
6398
|
+
if (other && control.kind === (kind === "axis2d" ? "stick" : "button") && virtualOverlap(region, other)) throw new EngineError("VIRTUAL_REGION_OVERLAP", "Same-priority virtual activation regions overlap");
|
|
6399
|
+
}
|
|
6400
|
+
if (options.position) virtualPosition(options.position, this.view);
|
|
6401
|
+
}
|
|
6402
|
+
binding(controller, action) {
|
|
6403
|
+
const source = controller.createSource();
|
|
6404
|
+
return {
|
|
6405
|
+
get disposed() {
|
|
6406
|
+
return source.disposed;
|
|
6407
|
+
},
|
|
6408
|
+
onDispose: (callback) => controller.onDispose(callback),
|
|
6409
|
+
clear: () => source.clear(),
|
|
6410
|
+
dispose: () => source.dispose(),
|
|
6411
|
+
active: () => controller.active,
|
|
6412
|
+
write: (direction, pressed) => source.set(action, action.kind === "axis2d" ? direction : pressed)
|
|
6413
|
+
};
|
|
6414
|
+
}
|
|
6415
|
+
stick(controller, action, options) {
|
|
6416
|
+
this.validate(controller, action, "axis2d", options);
|
|
6417
|
+
const binding = this.binding(controller, action);
|
|
6418
|
+
try {
|
|
6419
|
+
return this.add(new VirtualStick(options, this.host, binding));
|
|
6420
|
+
} catch (error) {
|
|
6421
|
+
binding.dispose();
|
|
6422
|
+
throw error;
|
|
6423
|
+
}
|
|
6424
|
+
}
|
|
6425
|
+
button(controller, action, options) {
|
|
6426
|
+
this.validate(controller, action, "button", options);
|
|
6427
|
+
const binding = this.binding(controller, action);
|
|
6428
|
+
try {
|
|
6429
|
+
return this.add(new VirtualButton(options, this.host, binding));
|
|
6430
|
+
} catch (error) {
|
|
6431
|
+
binding.dispose();
|
|
6432
|
+
throw error;
|
|
6433
|
+
}
|
|
6434
|
+
}
|
|
6435
|
+
add(control) {
|
|
6436
|
+
this.list = Object.freeze([...this.list, control]);
|
|
6437
|
+
this.dirty = true;
|
|
6438
|
+
try {
|
|
6439
|
+
this.refresh();
|
|
6440
|
+
} catch (error) {
|
|
6441
|
+
control.dispose();
|
|
6442
|
+
throw error;
|
|
6443
|
+
}
|
|
6444
|
+
return control;
|
|
6445
|
+
}
|
|
6446
|
+
resolve(view, replaced, region) {
|
|
6447
|
+
const result = /* @__PURE__ */ new Map();
|
|
6448
|
+
for (const control of this.list) result.set(control, resolveVirtualRegion(control === replaced && region ? region : control.region, view));
|
|
6449
|
+
for (let i = 0; i < this.list.length; i++) {
|
|
6450
|
+
const a = this.list[i];
|
|
6451
|
+
if (!a) continue;
|
|
6452
|
+
for (let j = i + 1; j < this.list.length; j++) {
|
|
6453
|
+
const b = this.list[j], x = result.get(a), y = b ? result.get(b) : void 0;
|
|
6454
|
+
if (b && x && y && a.kind === b.kind && virtualOverlap(x, y)) throw new EngineError("VIRTUAL_REGION_OVERLAP", "Same-priority virtual activation regions overlap", {
|
|
6455
|
+
first: i,
|
|
6456
|
+
second: j
|
|
6457
|
+
});
|
|
6458
|
+
}
|
|
6459
|
+
}
|
|
6460
|
+
return result;
|
|
6461
|
+
}
|
|
6462
|
+
centers(regions, view) {
|
|
6463
|
+
return new Map([...regions].map(([control, region]) => {
|
|
6464
|
+
const b = region.bounds;
|
|
6465
|
+
return [control, control.position ? virtualPosition(control.position, view) : Object.freeze({
|
|
6466
|
+
x: b.x + b.width / 2,
|
|
6467
|
+
y: b.y + b.height / 2
|
|
6468
|
+
})];
|
|
6469
|
+
}));
|
|
6470
|
+
}
|
|
6471
|
+
/** Resolve after Canvas layout and before routing/rendering. Layout changes cancel active gestures. */
|
|
6472
|
+
refresh(viewport = this.view) {
|
|
6473
|
+
this.alive();
|
|
6474
|
+
const view = virtualBounds(viewport);
|
|
6475
|
+
const resized = Object.keys(view).some((key) => view[key] !== this.view[key]);
|
|
6476
|
+
if (!this.dirty && !resized && !this.list.some((c) => c.region.kind === "custom")) return;
|
|
6477
|
+
const regions = this.resolve(view), centers = this.centers(regions, view);
|
|
6478
|
+
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)));
|
|
6479
|
+
this.view = view;
|
|
6480
|
+
this.regions = regions;
|
|
6481
|
+
this.anchors = centers;
|
|
6482
|
+
this.dirty = false;
|
|
6483
|
+
cleanupAll(changed.map((c) => () => c.invalidate()), "Virtual layout cancellation failed");
|
|
6484
|
+
}
|
|
6485
|
+
pick(data) {
|
|
6486
|
+
if (this.disposed || !this.active || !this.input.liveInput || data.button !== 0) return;
|
|
6487
|
+
this.refresh();
|
|
6488
|
+
for (const kind of ["button", "stick"]) for (const control of this.list) if (control.kind === kind && control.available && control.contains(data)) return control;
|
|
6489
|
+
}
|
|
6490
|
+
sample() {
|
|
6491
|
+
if (this.disposed) return;
|
|
6492
|
+
this.refresh();
|
|
6493
|
+
cleanupAll(this.list.filter((c) => c.pressed && !c.available).map((c) => () => c.invalidate()), "Virtual availability cancellation failed");
|
|
6494
|
+
}
|
|
6495
|
+
advance(seconds) {
|
|
6496
|
+
this.alive();
|
|
6497
|
+
if (virtualFinite(seconds, "seconds") < 0) throw new EngineError("INVALID_VIRTUAL_INPUT", "Presentation seconds must be nonnegative");
|
|
6498
|
+
cleanupAll(this.list.map((c) => () => {
|
|
6499
|
+
if (!c.disposed) c.advance(seconds);
|
|
6500
|
+
}), "Virtual hold callbacks failed");
|
|
6501
|
+
}
|
|
6502
|
+
reset() {
|
|
6503
|
+
cleanupAll(this.list.map((c) => () => c.invalidate()), "Virtual input reset failed");
|
|
6504
|
+
}
|
|
6505
|
+
dispose() {
|
|
6506
|
+
if (this.disposed) return;
|
|
6507
|
+
cleanupAll([
|
|
6508
|
+
() => this.life.dispose(),
|
|
6509
|
+
...this.list.map((c) => () => c.dispose()),
|
|
6510
|
+
() => {
|
|
6511
|
+
this.regions.clear();
|
|
6512
|
+
this.anchors.clear();
|
|
6513
|
+
this.list = Object.freeze([]);
|
|
6514
|
+
}
|
|
6515
|
+
], "Virtual input cleanup failed");
|
|
6516
|
+
}
|
|
6517
|
+
};
|
|
5756
6518
|
// packages/core/src/runtime/engine.ts
|
|
5757
6519
|
var Engine = class {
|
|
5758
6520
|
input = new InputSystem();
|
|
@@ -6320,7 +7082,7 @@ var ReplayPlayer = class {
|
|
|
6320
7082
|
this.engine = engine;
|
|
6321
7083
|
this.data = parseReplayData(data);
|
|
6322
7084
|
this.bound = bindings(engine, options.controllers);
|
|
6323
|
-
if (this.data.engineVersion !== "0.1.
|
|
7085
|
+
if (this.data.engineVersion !== "0.1.5" || engine.random.seed !== this.data.random.seed) throw new EngineError("REPLAY_INCOMPATIBLE", "Engine version or initial seed does not match Replay");
|
|
6324
7086
|
if (!compatible(this.bound.map(([id, c]) => ({
|
|
6325
7087
|
id,
|
|
6326
7088
|
actions: c.schema()
|
|
@@ -8218,6 +8980,6 @@ var CanvasUI = class {
|
|
|
8218
8980
|
], "Canvas UI cleanup failed");
|
|
8219
8981
|
}
|
|
8220
8982
|
};
|
|
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 };
|
|
8983
|
+
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, VirtualButton, VirtualControl, VirtualInput, VirtualRegions, VirtualStick, VirtualStickMode, 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 };
|
|
8222
8984
|
|
|
8223
8985
|
//# sourceMappingURL=index.js.map
|