@zvk/game-bridge 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/LICENSE.md +21 -0
- package/README.md +38 -0
- package/dist/core/channel.d.ts +67 -0
- package/dist/core/channel.js +153 -0
- package/dist/core/coalesced-publisher.d.ts +35 -0
- package/dist/core/coalesced-publisher.js +127 -0
- package/dist/core/game-bridge.d.ts +60 -0
- package/dist/core/game-bridge.js +118 -0
- package/dist/core/intents.d.ts +33 -0
- package/dist/core/intents.js +43 -0
- package/dist/core/serializable.d.ts +3 -0
- package/dist/core/serializable.js +71 -0
- package/dist/core/types.d.ts +17 -0
- package/dist/core/types.js +1 -0
- package/dist/dom/index.d.ts +68 -0
- package/dist/dom/index.js +244 -0
- package/dist/geometry/anchors.d.ts +48 -0
- package/dist/geometry/anchors.js +96 -0
- package/dist/geometry/index.d.ts +6 -0
- package/dist/geometry/index.js +6 -0
- package/dist/geometry/matrix.d.ts +45 -0
- package/dist/geometry/matrix.js +77 -0
- package/dist/geometry/placement.d.ts +24 -0
- package/dist/geometry/placement.js +49 -0
- package/dist/geometry/projection.d.ts +118 -0
- package/dist/geometry/projection.js +194 -0
- package/dist/geometry/types.d.ts +62 -0
- package/dist/geometry/types.js +79 -0
- package/dist/geometry/visibility.d.ts +30 -0
- package/dist/geometry/visibility.js +128 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +5 -0
- package/dist/phaser/index.d.ts +187 -0
- package/dist/phaser/index.js +347 -0
- package/dist/react/index.d.ts +20 -0
- package/dist/react/index.js +47 -0
- package/dist/test-utils/assertions.d.ts +2 -0
- package/dist/test-utils/assertions.js +9 -0
- package/dist/test-utils/fixtures.d.ts +7 -0
- package/dist/test-utils/fixtures.js +9 -0
- package/dist/test-utils/index.d.ts +3 -0
- package/dist/test-utils/index.js +3 -0
- package/dist/test-utils/manual-scheduler.d.ts +10 -0
- package/dist/test-utils/manual-scheduler.js +43 -0
- package/docs/content/core-contracts.md +5 -0
- package/docs/content/dom.md +5 -0
- package/docs/content/errors-and-performance.md +5 -0
- package/docs/content/geometry.md +5 -0
- package/docs/content/index.md +5 -0
- package/docs/content/migration.md +5 -0
- package/docs/content/phaser.md +5 -0
- package/docs/content/react.md +5 -0
- package/docs/examples/react-phaser-overlay.tsx +14 -0
- package/package.json +109 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { point2, rect2 } from "./types.js";
|
|
2
|
+
function assertInsets(insets) {
|
|
3
|
+
for (const [name, value] of Object.entries(insets)) {
|
|
4
|
+
if (!Number.isFinite(value) || value < 0)
|
|
5
|
+
throw new RangeError(`${name} inset must be finite and non-negative`);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
function finiteMargins(margins) {
|
|
9
|
+
if (![margins.top, margins.right, margins.bottom, margins.left].every(Number.isFinite)) {
|
|
10
|
+
throw new RangeError("Visibility margins must be finite");
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function insetRect(rect, insets) {
|
|
14
|
+
assertInsets(insets);
|
|
15
|
+
const width = Math.max(0, rect.width - insets.left - insets.right);
|
|
16
|
+
const height = Math.max(0, rect.height - insets.top - insets.bottom);
|
|
17
|
+
const x = width === 0 ? rect.x + rect.width / 2 : rect.x + insets.left;
|
|
18
|
+
const y = height === 0 ? rect.y + rect.height / 2 : rect.y + insets.top;
|
|
19
|
+
return rect2("overlay", x, y, width, height);
|
|
20
|
+
}
|
|
21
|
+
export function expandRect(rect, margins) {
|
|
22
|
+
finiteMargins(margins);
|
|
23
|
+
const rawWidth = rect.width + margins.left + margins.right;
|
|
24
|
+
const rawHeight = rect.height + margins.top + margins.bottom;
|
|
25
|
+
const width = Math.max(0, rawWidth);
|
|
26
|
+
const height = Math.max(0, rawHeight);
|
|
27
|
+
return rect2("overlay", width === 0 ? rect.x + rect.width / 2 : rect.x - margins.left, height === 0 ? rect.y + rect.height / 2 : rect.y - margins.top, width, height);
|
|
28
|
+
}
|
|
29
|
+
export function containsPoint(rect, point) {
|
|
30
|
+
return point.x >= rect.x && point.y >= rect.y && point.x < rect.x + rect.width && point.y < rect.y + rect.height;
|
|
31
|
+
}
|
|
32
|
+
export function containsPointInConvexQuad(quad, point) {
|
|
33
|
+
const points = [quad.topLeft, quad.topRight, quad.bottomRight, quad.bottomLeft];
|
|
34
|
+
let sign = 0;
|
|
35
|
+
for (let index = 0; index < points.length; index += 1) {
|
|
36
|
+
const first = points[index];
|
|
37
|
+
const second = points[(index + 1) % points.length];
|
|
38
|
+
const cross = (second.x - first.x) * (point.y - first.y) - (second.y - first.y) * (point.x - first.x);
|
|
39
|
+
if (cross === 0)
|
|
40
|
+
continue;
|
|
41
|
+
const current = Math.sign(cross);
|
|
42
|
+
if (sign !== 0 && current !== sign)
|
|
43
|
+
return false;
|
|
44
|
+
sign = current;
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
export function intersectRects(left, right) {
|
|
49
|
+
const x = Math.max(left.x, right.x);
|
|
50
|
+
const y = Math.max(left.y, right.y);
|
|
51
|
+
const farX = Math.min(left.x + left.width, right.x + right.width);
|
|
52
|
+
const farY = Math.min(left.y + left.height, right.y + right.height);
|
|
53
|
+
return farX <= x || farY <= y ? null : rect2("overlay", x, y, farX - x, farY - y);
|
|
54
|
+
}
|
|
55
|
+
export function unionRects(left, right) {
|
|
56
|
+
const x = Math.min(left.x, right.x);
|
|
57
|
+
const y = Math.min(left.y, right.y);
|
|
58
|
+
return rect2("overlay", x, y, Math.max(left.x + left.width, right.x + right.width) - x, Math.max(left.y + left.height, right.y + right.height) - y);
|
|
59
|
+
}
|
|
60
|
+
export function clampPointToRect(point, rect) {
|
|
61
|
+
return point2("overlay", Math.min(Math.max(point.x, rect.x), rect.x + rect.width), Math.min(Math.max(point.y, rect.y), rect.y + rect.height));
|
|
62
|
+
}
|
|
63
|
+
function previousRepresentable(value) {
|
|
64
|
+
if (!Number.isFinite(value))
|
|
65
|
+
return value;
|
|
66
|
+
if (value === 0)
|
|
67
|
+
return -Number.MIN_VALUE;
|
|
68
|
+
const buffer = new ArrayBuffer(8);
|
|
69
|
+
const float = new Float64Array(buffer);
|
|
70
|
+
const bits = new BigUint64Array(buffer);
|
|
71
|
+
float[0] = value;
|
|
72
|
+
bits[0] = bits[0] + (value > 0 ? -1n : 1n);
|
|
73
|
+
return float[0];
|
|
74
|
+
}
|
|
75
|
+
export function clampPointToHalfOpenRect(point, rect) {
|
|
76
|
+
if (rect.width <= 0 || rect.height <= 0)
|
|
77
|
+
return point2("overlay", rect.x, rect.y);
|
|
78
|
+
return point2("overlay", Math.min(Math.max(point.x, rect.x), previousRepresentable(rect.x + rect.width)), Math.min(Math.max(point.y, rect.y), previousRepresentable(rect.y + rect.height)));
|
|
79
|
+
}
|
|
80
|
+
export function closestPointOnRectEdge(point, rect) {
|
|
81
|
+
const clamped = clampPointToRect(point, rect);
|
|
82
|
+
if (!containsPoint(rect, point))
|
|
83
|
+
return clamped;
|
|
84
|
+
const distances = [
|
|
85
|
+
{ distance: point.y - rect.y, x: point.x, y: rect.y },
|
|
86
|
+
{ distance: rect.x + rect.width - point.x, x: rect.x + rect.width, y: point.y },
|
|
87
|
+
{ distance: rect.y + rect.height - point.y, x: point.x, y: rect.y + rect.height },
|
|
88
|
+
{ distance: point.x - rect.x, x: rect.x, y: point.y },
|
|
89
|
+
];
|
|
90
|
+
const closest = distances.reduce((best, candidate) => candidate.distance < best.distance ? candidate : best);
|
|
91
|
+
return point2("overlay", closest.x, closest.y);
|
|
92
|
+
}
|
|
93
|
+
export function intersectRayWithRect(origin, direction, rect) {
|
|
94
|
+
if (![origin.x, origin.y, direction.x, direction.y].every(Number.isFinite) || (direction.x === 0 && direction.y === 0))
|
|
95
|
+
return null;
|
|
96
|
+
const candidates = [];
|
|
97
|
+
const add = (distance, edge) => {
|
|
98
|
+
if (distance < 0 || !Number.isFinite(distance))
|
|
99
|
+
return;
|
|
100
|
+
const x = origin.x + direction.x * distance;
|
|
101
|
+
const y = origin.y + direction.y * distance;
|
|
102
|
+
if (x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height) {
|
|
103
|
+
candidates.push({ point: point2("overlay", x, y), edge, distance });
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
if (direction.y !== 0) {
|
|
107
|
+
add((rect.y - origin.y) / direction.y, "top");
|
|
108
|
+
add((rect.y + rect.height - origin.y) / direction.y, "bottom");
|
|
109
|
+
}
|
|
110
|
+
if (direction.x !== 0) {
|
|
111
|
+
add((rect.x - origin.x) / direction.x, "left");
|
|
112
|
+
add((rect.x + rect.width - origin.x) / direction.x, "right");
|
|
113
|
+
}
|
|
114
|
+
candidates.sort((left, right) => left.distance - right.distance || left.edge.localeCompare(right.edge));
|
|
115
|
+
return candidates[0] ?? null;
|
|
116
|
+
}
|
|
117
|
+
export function overflowInsets(inner, outer) {
|
|
118
|
+
return {
|
|
119
|
+
top: Math.max(0, outer.y - inner.y),
|
|
120
|
+
right: Math.max(0, inner.x + inner.width - (outer.x + outer.width)),
|
|
121
|
+
bottom: Math.max(0, inner.y + inner.height - (outer.y + outer.height)),
|
|
122
|
+
left: Math.max(0, outer.x - inner.x),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
export function overlapArea(left, right) {
|
|
126
|
+
const intersection = intersectRects(left, right);
|
|
127
|
+
return intersection === null ? 0 : intersection.width * intersection.height;
|
|
128
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * from "./core/channel.js";
|
|
2
|
+
export * from "./core/coalesced-publisher.js";
|
|
3
|
+
export * from "./core/game-bridge.js";
|
|
4
|
+
export type { IntentDispatchResult, IntentEnvelope, IntentSink, } from "./core/intents.js";
|
|
5
|
+
export { assertJsonValue } from "./core/serializable.js";
|
|
6
|
+
export * from "./core/types.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type { BridgePublishResult, BridgePublisher, GameBridgeDiagnostic, GameBridgeFrame, JsonValue } from "../index.js";
|
|
2
|
+
import { type AffineTransform2D, type InverseProjectionResult, type OutsideProjectionPolicy, type Point2, type Rect2, type Size2, type WorldAnchorSnapshot } from "../geometry/index.js";
|
|
3
|
+
import { type CanvasDomMetrics, type DomReferenceBox } from "../dom/index.js";
|
|
4
|
+
export interface PhaserPointLike {
|
|
5
|
+
readonly x: number;
|
|
6
|
+
readonly y: number;
|
|
7
|
+
}
|
|
8
|
+
export interface PhaserTransformMatrixLike {
|
|
9
|
+
readonly a: number;
|
|
10
|
+
readonly b: number;
|
|
11
|
+
readonly c: number;
|
|
12
|
+
readonly d: number;
|
|
13
|
+
readonly e: number;
|
|
14
|
+
readonly f: number;
|
|
15
|
+
}
|
|
16
|
+
export interface PhaserCameraLike {
|
|
17
|
+
readonly name?: string;
|
|
18
|
+
readonly id?: number;
|
|
19
|
+
readonly x: number;
|
|
20
|
+
readonly y: number;
|
|
21
|
+
readonly width: number;
|
|
22
|
+
readonly height: number;
|
|
23
|
+
readonly scrollX: number;
|
|
24
|
+
readonly scrollY: number;
|
|
25
|
+
readonly zoom?: number;
|
|
26
|
+
readonly zoomX?: number;
|
|
27
|
+
readonly zoomY?: number;
|
|
28
|
+
readonly rotation?: number;
|
|
29
|
+
readonly originX?: number;
|
|
30
|
+
readonly originY?: number;
|
|
31
|
+
readonly roundPixels?: boolean;
|
|
32
|
+
readonly visible?: boolean;
|
|
33
|
+
readonly inputEnabled?: boolean;
|
|
34
|
+
readonly matrix?: PhaserTransformMatrixLike;
|
|
35
|
+
readonly matrixCombined?: PhaserTransformMatrixLike;
|
|
36
|
+
readonly getWorldPoint: (x: number, y: number, output?: {
|
|
37
|
+
x: number;
|
|
38
|
+
y: number;
|
|
39
|
+
}) => PhaserPointLike;
|
|
40
|
+
}
|
|
41
|
+
export interface PhaserEventEmitterLike {
|
|
42
|
+
readonly on: (event: string, listener: (...args: unknown[]) => void) => unknown;
|
|
43
|
+
readonly once: (event: string, listener: (...args: unknown[]) => void) => unknown;
|
|
44
|
+
readonly off: (event: string, listener: (...args: unknown[]) => void) => unknown;
|
|
45
|
+
}
|
|
46
|
+
export interface PhaserSizeLike {
|
|
47
|
+
readonly width: number;
|
|
48
|
+
readonly height: number;
|
|
49
|
+
}
|
|
50
|
+
export interface PhaserScaleManagerLike extends PhaserEventEmitterLike {
|
|
51
|
+
readonly gameSize: PhaserSizeLike;
|
|
52
|
+
readonly baseSize: PhaserSizeLike;
|
|
53
|
+
readonly displaySize: PhaserSizeLike;
|
|
54
|
+
readonly displayScale?: PhaserPointLike;
|
|
55
|
+
readonly scaleMode?: string | number;
|
|
56
|
+
}
|
|
57
|
+
export interface PhaserSceneLike {
|
|
58
|
+
readonly events: PhaserEventEmitterLike;
|
|
59
|
+
readonly game: {
|
|
60
|
+
readonly canvas: HTMLCanvasElement;
|
|
61
|
+
};
|
|
62
|
+
readonly scale: PhaserScaleManagerLike;
|
|
63
|
+
}
|
|
64
|
+
export interface PhaserBridgeError {
|
|
65
|
+
readonly code: "dom-unavailable" | "resize-observer-unavailable" | "scheduler-unavailable" | "camera-unavailable" | "camera-id-unavailable" | "canvas-disconnected" | "overlay-disconnected" | "zero-sized-surface" | "invalid-backing-size" | "non-finite-measurement" | "invalid-clock-id" | "non-finite-transform" | "non-invertible-transform" | "inconsistent-transform-snapshot" | "unsupported-camera-transform" | "unsupported-css-transform" | "duplicate-anchor-id" | "source-frame-regression" | "clock-mismatch" | "capture-callback-threw" | "destroyed";
|
|
66
|
+
readonly message: string;
|
|
67
|
+
}
|
|
68
|
+
export interface PhaserCameraProjectionSnapshot {
|
|
69
|
+
readonly cameraId: string;
|
|
70
|
+
readonly worldToCamera: AffineTransform2D<"world", "camera">;
|
|
71
|
+
readonly cameraViewport: Rect2<"game">;
|
|
72
|
+
readonly visible: boolean;
|
|
73
|
+
readonly roundPixels: boolean;
|
|
74
|
+
readonly matrixSource: "render-matrix" | "world-point-sampling";
|
|
75
|
+
readonly cameraEffectsIncluded: boolean;
|
|
76
|
+
}
|
|
77
|
+
export interface ReadPhaserCameraOptions {
|
|
78
|
+
readonly cameraId?: string;
|
|
79
|
+
readonly includeCameraEffects?: boolean;
|
|
80
|
+
readonly matrixSource?: "render-matrix" | "world-point-sampling";
|
|
81
|
+
}
|
|
82
|
+
export type ReadPhaserCameraResult = {
|
|
83
|
+
readonly ok: true;
|
|
84
|
+
readonly snapshot: PhaserCameraProjectionSnapshot;
|
|
85
|
+
} | {
|
|
86
|
+
readonly ok: false;
|
|
87
|
+
readonly error: PhaserBridgeError;
|
|
88
|
+
};
|
|
89
|
+
export declare function readPhaserCameraProjection(camera: PhaserCameraLike, options?: ReadPhaserCameraOptions): ReadPhaserCameraResult;
|
|
90
|
+
export interface PhaserSurfaceProjection {
|
|
91
|
+
readonly capturedAtMs: number;
|
|
92
|
+
readonly clockId: string;
|
|
93
|
+
readonly gameLogicalSize: Size2<"game">;
|
|
94
|
+
readonly gameViewportInBacking: Rect2<"backing">;
|
|
95
|
+
readonly canvasBackingSize: Size2<"backing">;
|
|
96
|
+
readonly gameToBacking: AffineTransform2D<"game", "backing">;
|
|
97
|
+
readonly dom: CanvasDomMetrics;
|
|
98
|
+
readonly scaleMode?: string | number;
|
|
99
|
+
}
|
|
100
|
+
export type PhaserSurfaceProjectionResult = {
|
|
101
|
+
readonly ok: true;
|
|
102
|
+
readonly surface: PhaserSurfaceProjection;
|
|
103
|
+
} | {
|
|
104
|
+
readonly ok: false;
|
|
105
|
+
readonly error: PhaserBridgeError;
|
|
106
|
+
};
|
|
107
|
+
export interface ReadPhaserSurfaceOptions {
|
|
108
|
+
readonly scene: PhaserSceneLike;
|
|
109
|
+
readonly canvas?: HTMLCanvasElement;
|
|
110
|
+
readonly overlay: HTMLElement;
|
|
111
|
+
readonly overlayBox?: DomReferenceBox;
|
|
112
|
+
readonly capturedAtMs?: number;
|
|
113
|
+
readonly clockId?: string;
|
|
114
|
+
}
|
|
115
|
+
export declare function readPhaserSurfaceProjection(options: ReadPhaserSurfaceOptions): PhaserSurfaceProjectionResult;
|
|
116
|
+
export interface PhaserWorldAnchor<TMetadata extends JsonValue = JsonValue> extends WorldAnchorSnapshot<TMetadata> {
|
|
117
|
+
readonly scrollFactor?: {
|
|
118
|
+
readonly x: number;
|
|
119
|
+
readonly y: number;
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
export declare function clientPointToPhaserWorld(camera: PhaserCameraLike, surface: PhaserSurfaceProjection, point: Point2<"client">, options?: {
|
|
123
|
+
readonly outside?: OutsideProjectionPolicy;
|
|
124
|
+
}): InverseProjectionResult;
|
|
125
|
+
export interface CreatePhaserSceneBridgeOptions<TMetadata extends JsonValue = JsonValue> {
|
|
126
|
+
readonly scene: PhaserSceneLike;
|
|
127
|
+
readonly camera: PhaserCameraLike | (() => PhaserCameraLike | null);
|
|
128
|
+
readonly overlay: HTMLElement | (() => HTMLElement | null);
|
|
129
|
+
readonly publisher: BridgePublisher<GameBridgeFrame<TMetadata>>;
|
|
130
|
+
readonly readAnchors: () => readonly PhaserWorldAnchor<TMetadata>[];
|
|
131
|
+
readonly nextSourceFrameId?: () => number;
|
|
132
|
+
readonly clock?: {
|
|
133
|
+
readonly id: string;
|
|
134
|
+
readonly now: () => number;
|
|
135
|
+
};
|
|
136
|
+
readonly phase?: "render" | "postupdate" | "manual";
|
|
137
|
+
readonly inactivePolicy?: "retain" | "not-ready";
|
|
138
|
+
readonly onDiagnostic?: (diagnostic: GameBridgeDiagnostic) => void;
|
|
139
|
+
}
|
|
140
|
+
export type PhaserFrameCaptureResult = {
|
|
141
|
+
readonly status: "staged-ready" | "staged-not-ready" | "replaced-pending";
|
|
142
|
+
readonly sourceFrameId: number;
|
|
143
|
+
readonly diagnostics: readonly GameBridgeDiagnostic[];
|
|
144
|
+
} | {
|
|
145
|
+
readonly status: "disconnected" | "destroyed";
|
|
146
|
+
};
|
|
147
|
+
export type PhaserFrameFlushResult = {
|
|
148
|
+
readonly status: "empty";
|
|
149
|
+
} | BridgePublishResult;
|
|
150
|
+
export type PhaserBridgeDisconnectResult = {
|
|
151
|
+
readonly status: "disconnected";
|
|
152
|
+
readonly publication: BridgePublishResult;
|
|
153
|
+
} | {
|
|
154
|
+
readonly status: "already-disconnected";
|
|
155
|
+
} | {
|
|
156
|
+
readonly status: "destroyed";
|
|
157
|
+
};
|
|
158
|
+
export type PhaserSceneBridgeDestroyResult = {
|
|
159
|
+
readonly status: "destroyed";
|
|
160
|
+
readonly publication?: BridgePublishResult;
|
|
161
|
+
} | {
|
|
162
|
+
readonly status: "already-destroyed";
|
|
163
|
+
};
|
|
164
|
+
export interface PhaserSceneBridgeStatus {
|
|
165
|
+
readonly lifecycle: "connected" | "inactive" | "disconnected" | "destroyed";
|
|
166
|
+
readonly phase: "render" | "postupdate" | "manual";
|
|
167
|
+
readonly generation: number;
|
|
168
|
+
readonly lastSourceFrameId: number;
|
|
169
|
+
readonly pending: boolean;
|
|
170
|
+
readonly listenerCount: number;
|
|
171
|
+
readonly observerCount: number;
|
|
172
|
+
}
|
|
173
|
+
export interface PhaserSceneBridgeConnection {
|
|
174
|
+
readonly capture: () => PhaserFrameCaptureResult;
|
|
175
|
+
readonly flush: () => PhaserFrameFlushResult;
|
|
176
|
+
readonly disconnect: (reason?: string) => PhaserBridgeDisconnectResult;
|
|
177
|
+
readonly destroy: () => PhaserSceneBridgeDestroyResult;
|
|
178
|
+
readonly getStatus: () => PhaserSceneBridgeStatus;
|
|
179
|
+
}
|
|
180
|
+
export type CreatePhaserSceneBridgeResult = {
|
|
181
|
+
readonly ok: true;
|
|
182
|
+
readonly connection: PhaserSceneBridgeConnection;
|
|
183
|
+
} | {
|
|
184
|
+
readonly ok: false;
|
|
185
|
+
readonly error: PhaserBridgeError;
|
|
186
|
+
};
|
|
187
|
+
export declare function createPhaserSceneBridge<TMetadata extends JsonValue>(options: CreatePhaserSceneBridgeOptions<TMetadata>): CreatePhaserSceneBridgeResult;
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { affineTransform, clientPoint, compileProjectionContext, composeTransforms, gamePoint, invertTransform, projectWorldAnchors, rect2, size2, transformPoint, translationTransform, } from "../geometry/index.js";
|
|
2
|
+
import { measureGameSurface, observeGameSurface, } from "../dom/index.js";
|
|
3
|
+
const phaserError = (code, message) => ({ ok: false, error: { code, message } });
|
|
4
|
+
const finite = (...values) => values.every(Number.isFinite);
|
|
5
|
+
export function readPhaserCameraProjection(camera, options = {}) {
|
|
6
|
+
const cameraId = options.cameraId?.trim() || camera.name?.trim() || (Number.isFinite(camera.id) ? `camera-${camera.id}` : "");
|
|
7
|
+
if (!cameraId)
|
|
8
|
+
return phaserError("camera-id-unavailable", "An explicit stable camera identity is required");
|
|
9
|
+
if (!finite(camera.x, camera.y, camera.width, camera.height) || camera.width < 0 || camera.height < 0)
|
|
10
|
+
return phaserError("non-finite-transform", "Camera viewport is invalid");
|
|
11
|
+
const source = options.matrixSource ?? ((camera.matrixCombined ?? camera.matrix) ? "render-matrix" : "world-point-sampling");
|
|
12
|
+
let worldToGame;
|
|
13
|
+
if (source === "render-matrix") {
|
|
14
|
+
const matrix = camera.matrixCombined ?? camera.matrix;
|
|
15
|
+
if (!matrix || !finite(matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f))
|
|
16
|
+
return phaserError("unsupported-camera-transform", "A finite public render matrix is unavailable");
|
|
17
|
+
const scroll = translationTransform("world", "world", -camera.scrollX, -camera.scrollY);
|
|
18
|
+
worldToGame = composeTransforms(scroll, affineTransform("world", "game", matrix));
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
try {
|
|
22
|
+
const origin = camera.getWorldPoint(camera.x, camera.y);
|
|
23
|
+
const x = camera.getWorldPoint(camera.x + 1, camera.y);
|
|
24
|
+
const y = camera.getWorldPoint(camera.x, camera.y + 1);
|
|
25
|
+
if (!finite(origin.x, origin.y, x.x, x.y, y.x, y.y))
|
|
26
|
+
return phaserError("non-finite-transform", "Camera world-point samples were non-finite");
|
|
27
|
+
const gameToWorld = affineTransform("game", "world", {
|
|
28
|
+
a: x.x - origin.x, b: x.y - origin.y, c: y.x - origin.x, d: y.y - origin.y,
|
|
29
|
+
e: origin.x - (x.x - origin.x) * camera.x - (y.x - origin.x) * camera.y,
|
|
30
|
+
f: origin.y - (x.y - origin.y) * camera.x - (y.y - origin.y) * camera.y,
|
|
31
|
+
});
|
|
32
|
+
const inverse = invertTransform(gameToWorld);
|
|
33
|
+
if (!inverse.ok)
|
|
34
|
+
return phaserError(inverse.error.code, "Camera world-point transform is not invertible");
|
|
35
|
+
worldToGame = inverse.value;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return phaserError("unsupported-camera-transform", "Camera world-point sampling failed");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const gameToCamera = translationTransform("game", "camera", -camera.x, -camera.y);
|
|
42
|
+
return {
|
|
43
|
+
ok: true,
|
|
44
|
+
snapshot: Object.freeze({
|
|
45
|
+
cameraId,
|
|
46
|
+
worldToCamera: Object.freeze(composeTransforms(worldToGame, gameToCamera)),
|
|
47
|
+
cameraViewport: Object.freeze(rect2("game", camera.x, camera.y, camera.width, camera.height)),
|
|
48
|
+
visible: camera.visible !== false,
|
|
49
|
+
roundPixels: camera.roundPixels === true,
|
|
50
|
+
matrixSource: source,
|
|
51
|
+
cameraEffectsIncluded: options.includeCameraEffects !== false,
|
|
52
|
+
}),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function readPhaserSurfaceProjection(options) {
|
|
56
|
+
const canvas = options.canvas ?? options.scene.game.canvas;
|
|
57
|
+
const dom = measureGameSurface({
|
|
58
|
+
canvas,
|
|
59
|
+
overlay: options.overlay,
|
|
60
|
+
...(options.overlayBox === undefined ? {} : { overlayBox: options.overlayBox }),
|
|
61
|
+
...(options.capturedAtMs === undefined ? {} : { capturedAtMs: options.capturedAtMs }),
|
|
62
|
+
...(options.clockId === undefined ? {} : { clockId: options.clockId }),
|
|
63
|
+
});
|
|
64
|
+
if (!dom.ok)
|
|
65
|
+
return phaserError(dom.error.code, dom.error.message);
|
|
66
|
+
const game = options.scene.scale.gameSize;
|
|
67
|
+
const base = options.scene.scale.baseSize;
|
|
68
|
+
if (!finite(game.width, game.height, base.width, base.height) || game.width <= 0 || game.height <= 0 || base.width <= 0 || base.height <= 0)
|
|
69
|
+
return phaserError("zero-sized-surface", "Phaser game or base size is zero or invalid");
|
|
70
|
+
const mode = options.scene.scale.scaleMode;
|
|
71
|
+
const uniformMode = mode === 3 || mode === 6 || (typeof mode === "string" && /^(FIT|EXPAND)$/i.test(mode));
|
|
72
|
+
const naturalScaleX = base.width / game.width;
|
|
73
|
+
const naturalScaleY = base.height / game.height;
|
|
74
|
+
const uniformScale = Math.min(naturalScaleX, naturalScaleY);
|
|
75
|
+
const scaleX = uniformMode ? uniformScale : naturalScaleX;
|
|
76
|
+
const scaleY = uniformMode ? uniformScale : naturalScaleY;
|
|
77
|
+
const viewportWidth = game.width * scaleX;
|
|
78
|
+
const viewportHeight = game.height * scaleY;
|
|
79
|
+
const viewportX = (canvas.width - viewportWidth) / 2;
|
|
80
|
+
const viewportY = (canvas.height - viewportHeight) / 2;
|
|
81
|
+
const gameToBacking = affineTransform("game", "backing", { a: scaleX, b: 0, c: 0, d: scaleY, e: viewportX, f: viewportY });
|
|
82
|
+
const surface = {
|
|
83
|
+
capturedAtMs: dom.metrics.capturedAtMs,
|
|
84
|
+
clockId: dom.metrics.clockId,
|
|
85
|
+
gameLogicalSize: size2("game", game.width, game.height),
|
|
86
|
+
gameViewportInBacking: rect2("backing", viewportX, viewportY, viewportWidth, viewportHeight),
|
|
87
|
+
canvasBackingSize: size2("backing", canvas.width, canvas.height),
|
|
88
|
+
gameToBacking,
|
|
89
|
+
dom: dom.metrics,
|
|
90
|
+
...(options.scene.scale.scaleMode !== undefined ? { scaleMode: options.scene.scale.scaleMode } : {}),
|
|
91
|
+
};
|
|
92
|
+
return { ok: true, surface: Object.freeze(surface) };
|
|
93
|
+
}
|
|
94
|
+
export function clientPointToPhaserWorld(camera, surface, point, options = {}) {
|
|
95
|
+
const cssToClient = invertTransform(surface.dom.canvasCssToClient);
|
|
96
|
+
const backingToCss = invertTransform(surface.dom.backingToCanvasCss);
|
|
97
|
+
const gameToBacking = invertTransform(surface.gameToBacking);
|
|
98
|
+
if (!cssToClient.ok || !backingToCss.ok || !gameToBacking.ok)
|
|
99
|
+
return { ok: false, error: { code: "non-invertible-transform" } };
|
|
100
|
+
let game = transformPoint(gameToBacking.value, transformPoint(backingToCss.value, transformPoint(cssToClient.value, clientPoint(point.x, point.y))));
|
|
101
|
+
const viewport = { x: camera.x, y: camera.y, width: camera.width, height: camera.height };
|
|
102
|
+
const outside = game.x < viewport.x || game.y < viewport.y || game.x >= viewport.x + viewport.width || game.y >= viewport.y + viewport.height;
|
|
103
|
+
if (outside && (options.outside ?? "reject") === "reject")
|
|
104
|
+
return { ok: false, error: { code: "outside-camera" } };
|
|
105
|
+
let wasClamped = false;
|
|
106
|
+
if (outside && options.outside === "clamp-to-camera") {
|
|
107
|
+
game = gamePoint(Math.min(Math.max(game.x, viewport.x), Math.max(viewport.x, viewport.x + viewport.width - Number.EPSILON)), Math.min(Math.max(game.y, viewport.y), Math.max(viewport.y, viewport.y + viewport.height - Number.EPSILON)));
|
|
108
|
+
wasClamped = true;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const world = camera.getWorldPoint(game.x, game.y);
|
|
112
|
+
if (!finite(world.x, world.y))
|
|
113
|
+
return { ok: false, error: { code: "invalid-point" } };
|
|
114
|
+
return { ok: true, worldPoint: { x: world.x, y: world.y }, gamePoint: game, wasClamped };
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return { ok: false, error: { code: "invalid-point" } };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
let connectorGeneration = 0;
|
|
121
|
+
function diagnostic(code, message, capturedAtMs) {
|
|
122
|
+
return { code, severity: "error", message, ...(capturedAtMs === undefined ? {} : { capturedAtMs }) };
|
|
123
|
+
}
|
|
124
|
+
function geometryDiagnosticCode(code) {
|
|
125
|
+
switch (code) {
|
|
126
|
+
case "unsupported-protocol-version":
|
|
127
|
+
case "invalid-camera-id":
|
|
128
|
+
case "negative-dimension":
|
|
129
|
+
case "transform-space-mismatch":
|
|
130
|
+
return "inconsistent-transform-snapshot";
|
|
131
|
+
case "invalid-anchor-batch":
|
|
132
|
+
return "invalid-anchor";
|
|
133
|
+
default:
|
|
134
|
+
return code;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
export function createPhaserSceneBridge(options) {
|
|
138
|
+
if (!options || typeof options !== "object")
|
|
139
|
+
throw new TypeError("Scene bridge options must be an object");
|
|
140
|
+
if (typeof options.readAnchors !== "function")
|
|
141
|
+
throw new TypeError("readAnchors must be a function");
|
|
142
|
+
if (typeof options.publisher?.publish !== "function")
|
|
143
|
+
throw new TypeError("publisher must be a bridge publisher");
|
|
144
|
+
if (options.nextSourceFrameId !== undefined && typeof options.nextSourceFrameId !== "function")
|
|
145
|
+
throw new TypeError("nextSourceFrameId must be a function");
|
|
146
|
+
if (options.onDiagnostic !== undefined && typeof options.onDiagnostic !== "function")
|
|
147
|
+
throw new TypeError("onDiagnostic must be a function");
|
|
148
|
+
const phase = options.phase ?? "render";
|
|
149
|
+
const clock = options.clock ?? (typeof performance !== "undefined" && typeof performance.now === "function"
|
|
150
|
+
? { id: "performance", now: () => performance.now() }
|
|
151
|
+
: undefined);
|
|
152
|
+
if (!clock)
|
|
153
|
+
return phaserError("scheduler-unavailable", "A monotonic clock is unavailable");
|
|
154
|
+
if (!clock.id.trim())
|
|
155
|
+
return phaserError("invalid-clock-id", "Clock ID must be non-empty");
|
|
156
|
+
if (typeof clock.now !== "function")
|
|
157
|
+
throw new TypeError("clock.now must be a function");
|
|
158
|
+
let lifecycle = "connected";
|
|
159
|
+
const generation = ++connectorGeneration;
|
|
160
|
+
let lastSourceFrameId = 0;
|
|
161
|
+
let ownedSourceFrameId = 0;
|
|
162
|
+
let lastClock = -Infinity;
|
|
163
|
+
let pending;
|
|
164
|
+
let observer;
|
|
165
|
+
const listeners = [];
|
|
166
|
+
const report = (item) => {
|
|
167
|
+
try {
|
|
168
|
+
options.onDiagnostic?.(item);
|
|
169
|
+
}
|
|
170
|
+
catch (callbackError) {
|
|
171
|
+
queueMicrotask(() => { throw callbackError; });
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
const stageNotReady = (sourceFrameId, capturedAtMs, item) => {
|
|
175
|
+
const replaced = pending !== undefined;
|
|
176
|
+
report(item);
|
|
177
|
+
pending = Object.freeze({ protocolVersion: 1, sourceFrameId, capturedAtMs, clockId: clock.id, status: "not-ready", transform: null, anchors: [], diagnostics: [item] });
|
|
178
|
+
return { status: replaced ? "replaced-pending" : "staged-not-ready", sourceFrameId, diagnostics: [item] };
|
|
179
|
+
};
|
|
180
|
+
const nextId = () => {
|
|
181
|
+
if (options.nextSourceFrameId)
|
|
182
|
+
return options.nextSourceFrameId();
|
|
183
|
+
ownedSourceFrameId += 1;
|
|
184
|
+
return ownedSourceFrameId;
|
|
185
|
+
};
|
|
186
|
+
const resolveCamera = () => typeof options.camera === "function" ? options.camera() : options.camera;
|
|
187
|
+
const resolveOverlay = () => typeof options.overlay === "function" ? options.overlay() : options.overlay;
|
|
188
|
+
const capture = () => {
|
|
189
|
+
if (lifecycle === "destroyed")
|
|
190
|
+
return { status: "destroyed" };
|
|
191
|
+
if (lifecycle === "disconnected")
|
|
192
|
+
return { status: "disconnected" };
|
|
193
|
+
let sourceFrameId;
|
|
194
|
+
let capturedAtMs;
|
|
195
|
+
try {
|
|
196
|
+
sourceFrameId = nextId();
|
|
197
|
+
if (!Number.isSafeInteger(sourceFrameId) || sourceFrameId <= lastSourceFrameId) {
|
|
198
|
+
return stageNotReady(lastSourceFrameId, Number.isFinite(lastClock) ? lastClock : 0, diagnostic("source-frame-regression", "Source frame IDs must be increasing safe integers"));
|
|
199
|
+
}
|
|
200
|
+
capturedAtMs = clock.now();
|
|
201
|
+
if (!Number.isFinite(capturedAtMs) || capturedAtMs < lastClock) {
|
|
202
|
+
return stageNotReady(lastSourceFrameId, Number.isFinite(lastClock) ? lastClock : 0, diagnostic("clock-mismatch", "Clock values must be finite and nondecreasing"));
|
|
203
|
+
}
|
|
204
|
+
lastSourceFrameId = sourceFrameId;
|
|
205
|
+
lastClock = capturedAtMs;
|
|
206
|
+
const camera = resolveCamera();
|
|
207
|
+
if (!camera)
|
|
208
|
+
return stageNotReady(sourceFrameId, capturedAtMs, diagnostic("camera-unavailable", "The selected camera is unavailable", capturedAtMs));
|
|
209
|
+
const overlay = resolveOverlay();
|
|
210
|
+
if (!overlay)
|
|
211
|
+
return stageNotReady(sourceFrameId, capturedAtMs, diagnostic("overlay-disconnected", "The overlay is unavailable", capturedAtMs));
|
|
212
|
+
const cameraResult = readPhaserCameraProjection(camera);
|
|
213
|
+
if (!cameraResult.ok)
|
|
214
|
+
return stageNotReady(sourceFrameId, capturedAtMs, diagnostic(cameraResult.error.code, cameraResult.error.message, capturedAtMs));
|
|
215
|
+
const surfaceResult = readPhaserSurfaceProjection({ scene: options.scene, overlay, capturedAtMs, clockId: clock.id });
|
|
216
|
+
if (!surfaceResult.ok)
|
|
217
|
+
return stageNotReady(sourceFrameId, capturedAtMs, diagnostic(surfaceResult.error.code, surfaceResult.error.message, capturedAtMs));
|
|
218
|
+
const cameraToGame = translationTransform("camera", "game", cameraResult.snapshot.cameraViewport.x, cameraResult.snapshot.cameraViewport.y);
|
|
219
|
+
const dom = surfaceResult.surface.dom;
|
|
220
|
+
const transform = {
|
|
221
|
+
protocolVersion: 1,
|
|
222
|
+
cameraId: cameraResult.snapshot.cameraId,
|
|
223
|
+
capturedAtMs,
|
|
224
|
+
clockId: clock.id,
|
|
225
|
+
worldToCamera: cameraResult.snapshot.worldToCamera,
|
|
226
|
+
cameraToGame,
|
|
227
|
+
gameToBacking: surfaceResult.surface.gameToBacking,
|
|
228
|
+
backingToCanvasCss: dom.backingToCanvasCss,
|
|
229
|
+
canvasCssToClient: dom.canvasCssToClient,
|
|
230
|
+
clientToOverlay: dom.clientToOverlay,
|
|
231
|
+
cameraViewport: cameraResult.snapshot.cameraViewport,
|
|
232
|
+
gameLogicalSize: surfaceResult.surface.gameLogicalSize,
|
|
233
|
+
gameViewportInBacking: surfaceResult.surface.gameViewportInBacking,
|
|
234
|
+
canvasBackingSize: surfaceResult.surface.canvasBackingSize,
|
|
235
|
+
canvasCssSize: dom.canvasCssSize,
|
|
236
|
+
canvasContentClientRect: dom.canvasContentClientRect,
|
|
237
|
+
overlayClientRect: dom.overlayClientRect,
|
|
238
|
+
cameraVisible: cameraResult.snapshot.visible,
|
|
239
|
+
roundPixels: cameraResult.snapshot.roundPixels,
|
|
240
|
+
};
|
|
241
|
+
const compiled = compileProjectionContext(transform);
|
|
242
|
+
if (!compiled.ok)
|
|
243
|
+
return stageNotReady(sourceFrameId, capturedAtMs, diagnostic(geometryDiagnosticCode(compiled.error.code), compiled.error.message, capturedAtMs));
|
|
244
|
+
const inputs = options.readAnchors().map((anchor) => {
|
|
245
|
+
const factor = anchor.scrollFactor ?? { x: 1, y: 1 };
|
|
246
|
+
if (!finite(factor.x, factor.y))
|
|
247
|
+
return anchor;
|
|
248
|
+
return {
|
|
249
|
+
...anchor,
|
|
250
|
+
worldPoint: {
|
|
251
|
+
x: anchor.worldPoint.x + camera.scrollX * (1 - factor.x),
|
|
252
|
+
y: anchor.worldPoint.y + camera.scrollY * (1 - factor.y),
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
});
|
|
256
|
+
const projected = projectWorldAnchors(compiled.context, inputs);
|
|
257
|
+
if (!projected.ok)
|
|
258
|
+
return stageNotReady(sourceFrameId, capturedAtMs, diagnostic(geometryDiagnosticCode(projected.error.code), projected.error.message, capturedAtMs));
|
|
259
|
+
const replaced = pending !== undefined;
|
|
260
|
+
pending = Object.freeze({ protocolVersion: 1, sourceFrameId, capturedAtMs, clockId: clock.id, status: "ready", transform: Object.freeze(transform), anchors: projected.anchors, diagnostics: projected.diagnostics });
|
|
261
|
+
return { status: replaced ? "replaced-pending" : "staged-ready", sourceFrameId, diagnostics: projected.diagnostics };
|
|
262
|
+
}
|
|
263
|
+
catch (caught) {
|
|
264
|
+
return stageNotReady(lastSourceFrameId, Number.isFinite(lastClock) ? lastClock : 0, diagnostic("capture-callback-threw", caught instanceof Error ? caught.message : "A capture callback threw"));
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
const flush = () => {
|
|
268
|
+
if (!pending)
|
|
269
|
+
return { status: "empty" };
|
|
270
|
+
const frame = pending;
|
|
271
|
+
pending = undefined;
|
|
272
|
+
return options.publisher.publish(frame);
|
|
273
|
+
};
|
|
274
|
+
const addListener = (emitter, event, listener) => {
|
|
275
|
+
emitter.on(event, listener);
|
|
276
|
+
listeners.push({ emitter, event, listener });
|
|
277
|
+
};
|
|
278
|
+
const removeRuntime = () => {
|
|
279
|
+
for (const item of listeners.splice(0))
|
|
280
|
+
item.emitter.off(item.event, item.listener);
|
|
281
|
+
observer?.disconnect();
|
|
282
|
+
observer = undefined;
|
|
283
|
+
pending = undefined;
|
|
284
|
+
};
|
|
285
|
+
const phaseHandler = () => { capture(); flush(); };
|
|
286
|
+
if (phase !== "manual")
|
|
287
|
+
addListener(options.scene.events, phase, phaseHandler);
|
|
288
|
+
addListener(options.scene.scale, "resize", () => { });
|
|
289
|
+
addListener(options.scene.events, "pause", () => { lifecycle = "inactive"; });
|
|
290
|
+
addListener(options.scene.events, "resume", () => { lifecycle = "connected"; capture(); flush(); });
|
|
291
|
+
addListener(options.scene.events, "sleep", () => {
|
|
292
|
+
lifecycle = "inactive";
|
|
293
|
+
if (options.inactivePolicy !== "retain") {
|
|
294
|
+
const item = diagnostic("camera-unavailable", "Scene is sleeping", Number.isFinite(lastClock) ? lastClock : 0);
|
|
295
|
+
stageNotReady(lastSourceFrameId, Number.isFinite(lastClock) ? lastClock : 0, item);
|
|
296
|
+
flush();
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
addListener(options.scene.events, "wake", () => { lifecycle = "connected"; capture(); flush(); });
|
|
300
|
+
const publishDisconnected = (reason) => options.publisher.publish({
|
|
301
|
+
protocolVersion: 1,
|
|
302
|
+
sourceFrameId: lastSourceFrameId,
|
|
303
|
+
capturedAtMs: Number.isFinite(lastClock) ? lastClock : 0,
|
|
304
|
+
clockId: Number.isFinite(lastClock) ? clock.id : "bridge-idle",
|
|
305
|
+
status: "disconnected",
|
|
306
|
+
transform: null,
|
|
307
|
+
anchors: [],
|
|
308
|
+
diagnostics: reason ? [diagnostic("destroyed", reason)] : [],
|
|
309
|
+
});
|
|
310
|
+
const disconnect = (reason) => {
|
|
311
|
+
if (lifecycle === "destroyed")
|
|
312
|
+
return { status: "destroyed" };
|
|
313
|
+
if (lifecycle === "disconnected")
|
|
314
|
+
return { status: "already-disconnected" };
|
|
315
|
+
lifecycle = "disconnected";
|
|
316
|
+
removeRuntime();
|
|
317
|
+
return { status: "disconnected", publication: publishDisconnected(reason) };
|
|
318
|
+
};
|
|
319
|
+
const destroy = () => {
|
|
320
|
+
if (lifecycle === "destroyed")
|
|
321
|
+
return { status: "already-destroyed" };
|
|
322
|
+
const disconnected = lifecycle === "disconnected" ? undefined : disconnect("scene bridge destroyed");
|
|
323
|
+
const publication = disconnected?.status === "disconnected" ? disconnected.publication : undefined;
|
|
324
|
+
lifecycle = "destroyed";
|
|
325
|
+
removeRuntime();
|
|
326
|
+
return { status: "destroyed", ...(publication ? { publication } : {}) };
|
|
327
|
+
};
|
|
328
|
+
addListener(options.scene.events, "shutdown", () => { disconnect("scene shutdown"); });
|
|
329
|
+
addListener(options.scene.events, "destroy", () => { destroy(); });
|
|
330
|
+
const initialOverlay = resolveOverlay();
|
|
331
|
+
if (initialOverlay) {
|
|
332
|
+
const observed = observeGameSurface({ canvas: options.scene.game.canvas, overlay: initialOverlay, onChange: () => { } });
|
|
333
|
+
if (!observed.ok) {
|
|
334
|
+
removeRuntime();
|
|
335
|
+
return phaserError(observed.error.code, observed.error.message);
|
|
336
|
+
}
|
|
337
|
+
observer = observed.observer;
|
|
338
|
+
}
|
|
339
|
+
const connection = {
|
|
340
|
+
capture,
|
|
341
|
+
flush,
|
|
342
|
+
disconnect,
|
|
343
|
+
destroy,
|
|
344
|
+
getStatus: () => ({ lifecycle, phase, generation, lastSourceFrameId, pending: pending !== undefined, listenerCount: listeners.length, observerCount: observer ? 1 : 0 }),
|
|
345
|
+
};
|
|
346
|
+
return { ok: true, connection };
|
|
347
|
+
}
|