rn-gamekit 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 +18 -0
- package/LICENSE +21 -0
- package/README.md +131 -0
- package/lib/module/assets/defineAssets.js +48 -0
- package/lib/module/assets/defineAssets.js.map +1 -0
- package/lib/module/assets/errors.js +38 -0
- package/lib/module/assets/errors.js.map +1 -0
- package/lib/module/assets/types.js +2 -0
- package/lib/module/assets/types.js.map +1 -0
- package/lib/module/assets/validation.js +99 -0
- package/lib/module/assets/validation.js.map +1 -0
- package/lib/module/core/frameDriver.js +20 -0
- package/lib/module/core/frameDriver.js.map +1 -0
- package/lib/module/core/input/createInputBuffer.js +296 -0
- package/lib/module/core/input/createInputBuffer.js.map +1 -0
- package/lib/module/core/input/types.js +4 -0
- package/lib/module/core/input/types.js.map +1 -0
- package/lib/module/core/session/createGameSession.js +554 -0
- package/lib/module/core/session/createGameSession.js.map +1 -0
- package/lib/module/core/session/deepFreeze.js +155 -0
- package/lib/module/core/session/deepFreeze.js.map +1 -0
- package/lib/module/core/session/diagnostics.js +2 -0
- package/lib/module/core/session/diagnostics.js.map +1 -0
- package/lib/module/core/session/types.js +43 -0
- package/lib/module/core/session/types.js.map +1 -0
- package/lib/module/definition/defineGame.js +57 -0
- package/lib/module/definition/defineGame.js.map +1 -0
- package/lib/module/definition/types.js +4 -0
- package/lib/module/definition/types.js.map +1 -0
- package/lib/module/geometry/types.js +2 -0
- package/lib/module/geometry/types.js.map +1 -0
- package/lib/module/index.js +12 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/react/GamePointerInput.js +385 -0
- package/lib/module/react/GamePointerInput.js.map +1 -0
- package/lib/module/react/GameView.js +223 -0
- package/lib/module/react/GameView.js.map +1 -0
- package/lib/module/react/alphaClock.js +42 -0
- package/lib/module/react/alphaClock.js.map +1 -0
- package/lib/module/react/assets/createGameAssetStore.js +358 -0
- package/lib/module/react/assets/createGameAssetStore.js.map +1 -0
- package/lib/module/react/assets/decodeSkiaImage.js +54 -0
- package/lib/module/react/assets/decodeSkiaImage.js.map +1 -0
- package/lib/module/react/assets/errors.js +10 -0
- package/lib/module/react/assets/errors.js.map +1 -0
- package/lib/module/react/assets/useGameAssets.js +160 -0
- package/lib/module/react/assets/useGameAssets.js.map +1 -0
- package/lib/module/react/bindAppLifecycle.js +58 -0
- package/lib/module/react/bindAppLifecycle.js.map +1 -0
- package/lib/module/react/bindGameSession.js +37 -0
- package/lib/module/react/bindGameSession.js.map +1 -0
- package/lib/module/react/gestureLifecycle.js +68 -0
- package/lib/module/react/gestureLifecycle.js.map +1 -0
- package/lib/module/react/instrumentation.js +4 -0
- package/lib/module/react/instrumentation.js.map +1 -0
- package/lib/module/react/pointerBinding.js +196 -0
- package/lib/module/react/pointerBinding.js.map +1 -0
- package/lib/module/react/pointerCoalescer.js +224 -0
- package/lib/module/react/pointerCoalescer.js.map +1 -0
- package/lib/module/react/pointerContainment.js +27 -0
- package/lib/module/react/pointerContainment.js.map +1 -0
- package/lib/module/react/sprites/GameSprite.js +116 -0
- package/lib/module/react/sprites/GameSprite.js.map +1 -0
- package/lib/module/react/sprites/GameWorld2D.js +44 -0
- package/lib/module/react/sprites/GameWorld2D.js.map +1 -0
- package/lib/module/react/sprites/Sprite.js +147 -0
- package/lib/module/react/sprites/Sprite.js.map +1 -0
- package/lib/module/react/sprites/SpriteBatch.js +173 -0
- package/lib/module/react/sprites/SpriteBatch.js.map +1 -0
- package/lib/module/react/sprites/spriteBatchPolicy.js +25 -0
- package/lib/module/react/sprites/spriteBatchPolicy.js.map +1 -0
- package/lib/module/react/sprites/spriteTransform.js +135 -0
- package/lib/module/react/sprites/spriteTransform.js.map +1 -0
- package/lib/module/react/viewportBinding.js +101 -0
- package/lib/module/react/viewportBinding.js.map +1 -0
- package/lib/module/react.js +13 -0
- package/lib/module/react.js.map +1 -0
- package/lib/module/scene/defineScene.js +29 -0
- package/lib/module/scene/defineScene.js.map +1 -0
- package/lib/module/scene/types.js +4 -0
- package/lib/module/scene/types.js.map +1 -0
- package/lib/module/sprites/sampleSpriteClip.js +50 -0
- package/lib/module/sprites/sampleSpriteClip.js.map +1 -0
- package/lib/module/sprites/spriteAnimationState.js +165 -0
- package/lib/module/sprites/spriteAnimationState.js.map +1 -0
- package/lib/module/testing.js +57 -0
- package/lib/module/testing.js.map +1 -0
- package/lib/module/viewport2d/index.js +4 -0
- package/lib/module/viewport2d/index.js.map +1 -0
- package/lib/module/viewport2d/math.js +129 -0
- package/lib/module/viewport2d/math.js.map +1 -0
- package/lib/module/viewport2d/types.js +2 -0
- package/lib/module/viewport2d/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/assets/defineAssets.d.ts +20 -0
- package/lib/typescript/src/assets/defineAssets.d.ts.map +1 -0
- package/lib/typescript/src/assets/errors.d.ts +20 -0
- package/lib/typescript/src/assets/errors.d.ts.map +1 -0
- package/lib/typescript/src/assets/types.d.ts +138 -0
- package/lib/typescript/src/assets/types.d.ts.map +1 -0
- package/lib/typescript/src/assets/validation.d.ts +15 -0
- package/lib/typescript/src/assets/validation.d.ts.map +1 -0
- package/lib/typescript/src/core/frameDriver.d.ts +12 -0
- package/lib/typescript/src/core/frameDriver.d.ts.map +1 -0
- package/lib/typescript/src/core/input/createInputBuffer.d.ts +12 -0
- package/lib/typescript/src/core/input/createInputBuffer.d.ts.map +1 -0
- package/lib/typescript/src/core/input/types.d.ts +64 -0
- package/lib/typescript/src/core/input/types.d.ts.map +1 -0
- package/lib/typescript/src/core/session/createGameSession.d.ts +18 -0
- package/lib/typescript/src/core/session/createGameSession.d.ts.map +1 -0
- package/lib/typescript/src/core/session/deepFreeze.d.ts +27 -0
- package/lib/typescript/src/core/session/deepFreeze.d.ts.map +1 -0
- package/lib/typescript/src/core/session/diagnostics.d.ts +36 -0
- package/lib/typescript/src/core/session/diagnostics.d.ts.map +1 -0
- package/lib/typescript/src/core/session/types.d.ts +104 -0
- package/lib/typescript/src/core/session/types.d.ts.map +1 -0
- package/lib/typescript/src/definition/defineGame.d.ts +66 -0
- package/lib/typescript/src/definition/defineGame.d.ts.map +1 -0
- package/lib/typescript/src/definition/types.d.ts +56 -0
- package/lib/typescript/src/definition/types.d.ts.map +1 -0
- package/lib/typescript/src/geometry/types.d.ts +8 -0
- package/lib/typescript/src/geometry/types.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +18 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/react/GamePointerInput.d.ts +37 -0
- package/lib/typescript/src/react/GamePointerInput.d.ts.map +1 -0
- package/lib/typescript/src/react/GameView.d.ts +61 -0
- package/lib/typescript/src/react/GameView.d.ts.map +1 -0
- package/lib/typescript/src/react/alphaClock.d.ts +29 -0
- package/lib/typescript/src/react/alphaClock.d.ts.map +1 -0
- package/lib/typescript/src/react/assets/createGameAssetStore.d.ts +33 -0
- package/lib/typescript/src/react/assets/createGameAssetStore.d.ts.map +1 -0
- package/lib/typescript/src/react/assets/decodeSkiaImage.d.ts +12 -0
- package/lib/typescript/src/react/assets/decodeSkiaImage.d.ts.map +1 -0
- package/lib/typescript/src/react/assets/errors.d.ts +8 -0
- package/lib/typescript/src/react/assets/errors.d.ts.map +1 -0
- package/lib/typescript/src/react/assets/useGameAssets.d.ts +35 -0
- package/lib/typescript/src/react/assets/useGameAssets.d.ts.map +1 -0
- package/lib/typescript/src/react/bindAppLifecycle.d.ts +32 -0
- package/lib/typescript/src/react/bindAppLifecycle.d.ts.map +1 -0
- package/lib/typescript/src/react/bindGameSession.d.ts +14 -0
- package/lib/typescript/src/react/bindGameSession.d.ts.map +1 -0
- package/lib/typescript/src/react/gestureLifecycle.d.ts +37 -0
- package/lib/typescript/src/react/gestureLifecycle.d.ts.map +1 -0
- package/lib/typescript/src/react/instrumentation.d.ts +47 -0
- package/lib/typescript/src/react/instrumentation.d.ts.map +1 -0
- package/lib/typescript/src/react/pointerBinding.d.ts +104 -0
- package/lib/typescript/src/react/pointerBinding.d.ts.map +1 -0
- package/lib/typescript/src/react/pointerCoalescer.d.ts +106 -0
- package/lib/typescript/src/react/pointerCoalescer.d.ts.map +1 -0
- package/lib/typescript/src/react/pointerContainment.d.ts +12 -0
- package/lib/typescript/src/react/pointerContainment.d.ts.map +1 -0
- package/lib/typescript/src/react/sprites/GameSprite.d.ts +75 -0
- package/lib/typescript/src/react/sprites/GameSprite.d.ts.map +1 -0
- package/lib/typescript/src/react/sprites/GameWorld2D.d.ts +29 -0
- package/lib/typescript/src/react/sprites/GameWorld2D.d.ts.map +1 -0
- package/lib/typescript/src/react/sprites/Sprite.d.ts +43 -0
- package/lib/typescript/src/react/sprites/Sprite.d.ts.map +1 -0
- package/lib/typescript/src/react/sprites/SpriteBatch.d.ts +41 -0
- package/lib/typescript/src/react/sprites/SpriteBatch.d.ts.map +1 -0
- package/lib/typescript/src/react/sprites/spriteBatchPolicy.d.ts +10 -0
- package/lib/typescript/src/react/sprites/spriteBatchPolicy.d.ts.map +1 -0
- package/lib/typescript/src/react/sprites/spriteTransform.d.ts +86 -0
- package/lib/typescript/src/react/sprites/spriteTransform.d.ts.map +1 -0
- package/lib/typescript/src/react/viewportBinding.d.ts +42 -0
- package/lib/typescript/src/react/viewportBinding.d.ts.map +1 -0
- package/lib/typescript/src/react.d.ts +20 -0
- package/lib/typescript/src/react.d.ts.map +1 -0
- package/lib/typescript/src/scene/defineScene.d.ts +26 -0
- package/lib/typescript/src/scene/defineScene.d.ts.map +1 -0
- package/lib/typescript/src/scene/types.d.ts +71 -0
- package/lib/typescript/src/scene/types.d.ts.map +1 -0
- package/lib/typescript/src/sprites/sampleSpriteClip.d.ts +24 -0
- package/lib/typescript/src/sprites/sampleSpriteClip.d.ts.map +1 -0
- package/lib/typescript/src/sprites/spriteAnimationState.d.ts +45 -0
- package/lib/typescript/src/sprites/spriteAnimationState.d.ts.map +1 -0
- package/lib/typescript/src/testing.d.ts +28 -0
- package/lib/typescript/src/testing.d.ts.map +1 -0
- package/lib/typescript/src/viewport2d/index.d.ts +3 -0
- package/lib/typescript/src/viewport2d/index.d.ts.map +1 -0
- package/lib/typescript/src/viewport2d/math.d.ts +25 -0
- package/lib/typescript/src/viewport2d/math.d.ts.map +1 -0
- package/lib/typescript/src/viewport2d/types.d.ts +59 -0
- package/lib/typescript/src/viewport2d/types.d.ts.map +1 -0
- package/package.json +120 -0
- package/src/assets/defineAssets.ts +62 -0
- package/src/assets/errors.ts +64 -0
- package/src/assets/types.ts +164 -0
- package/src/assets/validation.ts +158 -0
- package/src/core/frameDriver.ts +31 -0
- package/src/core/input/createInputBuffer.ts +337 -0
- package/src/core/input/types.ts +67 -0
- package/src/core/session/createGameSession.ts +652 -0
- package/src/core/session/deepFreeze.ts +158 -0
- package/src/core/session/diagnostics.ts +35 -0
- package/src/core/session/types.ts +122 -0
- package/src/definition/defineGame.ts +94 -0
- package/src/definition/types.ts +67 -0
- package/src/geometry/types.ts +7 -0
- package/src/index.ts +84 -0
- package/src/react/GamePointerInput.tsx +478 -0
- package/src/react/GameView.tsx +317 -0
- package/src/react/alphaClock.ts +59 -0
- package/src/react/assets/createGameAssetStore.ts +459 -0
- package/src/react/assets/decodeSkiaImage.ts +64 -0
- package/src/react/assets/errors.ts +7 -0
- package/src/react/assets/useGameAssets.ts +171 -0
- package/src/react/bindAppLifecycle.ts +79 -0
- package/src/react/bindGameSession.ts +41 -0
- package/src/react/gestureLifecycle.ts +73 -0
- package/src/react/instrumentation.ts +54 -0
- package/src/react/pointerBinding.ts +226 -0
- package/src/react/pointerCoalescer.ts +234 -0
- package/src/react/pointerContainment.ts +23 -0
- package/src/react/sprites/GameSprite.tsx +192 -0
- package/src/react/sprites/GameWorld2D.tsx +46 -0
- package/src/react/sprites/Sprite.tsx +194 -0
- package/src/react/sprites/SpriteBatch.tsx +219 -0
- package/src/react/sprites/spriteBatchPolicy.ts +21 -0
- package/src/react/sprites/spriteTransform.ts +163 -0
- package/src/react/viewportBinding.ts +104 -0
- package/src/react.ts +37 -0
- package/src/scene/defineScene.ts +38 -0
- package/src/scene/types.ts +79 -0
- package/src/sprites/sampleSpriteClip.ts +51 -0
- package/src/sprites/spriteAnimationState.ts +176 -0
- package/src/testing.ts +61 -0
- package/src/viewport2d/index.ts +7 -0
- package/src/viewport2d/math.ts +134 -0
- package/src/viewport2d/types.ts +63 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { GameSessionStatus } from '../core/session/types';
|
|
2
|
+
|
|
3
|
+
/** Minimal AppState-like source used to keep this seam platform-neutral. */
|
|
4
|
+
export interface AppLifecycleSource {
|
|
5
|
+
/** The current app state, read synchronously at bind time. */
|
|
6
|
+
readonly currentState: string | null | undefined;
|
|
7
|
+
addEventListener(
|
|
8
|
+
state: 'change',
|
|
9
|
+
listener: (next: string) => void,
|
|
10
|
+
): { remove(): void };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Session operations the lifecycle binding may drive. */
|
|
14
|
+
export interface AppLifecycleSession {
|
|
15
|
+
/** Read the current session status. */
|
|
16
|
+
getStatus(): GameSessionStatus;
|
|
17
|
+
/** Pause simulation and discard suspended wall time. */
|
|
18
|
+
pause(): void;
|
|
19
|
+
/** Resume a paused session. */
|
|
20
|
+
resume(): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Pause a running session when the app becomes inactive or backgrounded and
|
|
25
|
+
* resume it on foreground only when this binding performed that pause.
|
|
26
|
+
*
|
|
27
|
+
* Platform-neutral: the React adapter supplies React Native's `AppState`.
|
|
28
|
+
* The initial `currentState` is synchronized at bind time, so an app that
|
|
29
|
+
* mounts while already backgrounded never leaves a session running
|
|
30
|
+
* invisibly. Manual pauses that precede backgrounding are never claimed, and
|
|
31
|
+
* a session that another actor resumed (or disposed) meanwhile is not
|
|
32
|
+
* resumed again. Cleanup clears the binding flag so a late foreground event
|
|
33
|
+
* cannot restart an unmounted game.
|
|
34
|
+
*/
|
|
35
|
+
export function bindAppLifecycle(
|
|
36
|
+
source: AppLifecycleSource,
|
|
37
|
+
session: AppLifecycleSession,
|
|
38
|
+
): () => void {
|
|
39
|
+
let pausedByLifecycle = false;
|
|
40
|
+
|
|
41
|
+
const pauseForBackground = () => {
|
|
42
|
+
if (session.getStatus() === 'running') {
|
|
43
|
+
session.pause();
|
|
44
|
+
pausedByLifecycle = true;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Synchronize the initial app state before any change event arrives.
|
|
49
|
+
if (source.currentState === 'inactive' || source.currentState === 'background') {
|
|
50
|
+
pauseForBackground();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const subscription = source.addEventListener('change', (next) => {
|
|
54
|
+
if (next === 'active') {
|
|
55
|
+
if (pausedByLifecycle) {
|
|
56
|
+
pausedByLifecycle = false;
|
|
57
|
+
// Resume only if the session is still paused; a different actor may
|
|
58
|
+
// have resumed or disposed it while the app was backgrounded.
|
|
59
|
+
if (session.getStatus() === 'paused') {
|
|
60
|
+
session.resume();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (next === 'inactive' || next === 'background') {
|
|
66
|
+
pauseForBackground();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
let cleanedUp = false;
|
|
71
|
+
return () => {
|
|
72
|
+
if (cleanedUp) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
cleanedUp = true;
|
|
76
|
+
subscription.remove();
|
|
77
|
+
pausedByLifecycle = false;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { InputMap, SceneMap } from '../definition/types';
|
|
2
|
+
import type { CommitFrame, GameSession } from '../core/session/types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Bind a live session to an imperative presentation sink.
|
|
6
|
+
*
|
|
7
|
+
* This platform-neutral lifecycle seam keeps the React adapter testable
|
|
8
|
+
* without loading React Native or Skia in the headless test runner. The sink
|
|
9
|
+
* receives commit frames only (simulation frequency), never per-display
|
|
10
|
+
* frames, and a stale write can never replace a newer commit: revisions are
|
|
11
|
+
* strictly increasing per session, and the initial envelope is presented
|
|
12
|
+
* through the same guard.
|
|
13
|
+
*/
|
|
14
|
+
export function bindGameSession<TScenes extends SceneMap, TInput extends InputMap>(
|
|
15
|
+
game: GameSession<TScenes, TInput>,
|
|
16
|
+
present: (frame: CommitFrame<TScenes>) => void,
|
|
17
|
+
): () => void {
|
|
18
|
+
let lastRevision = -1;
|
|
19
|
+
const presentIfNewer = (frame: CommitFrame<TScenes>) => {
|
|
20
|
+
if (frame.revision <= lastRevision) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
lastRevision = frame.revision;
|
|
24
|
+
present(frame);
|
|
25
|
+
};
|
|
26
|
+
presentIfNewer(game.getRenderFrame());
|
|
27
|
+
const subscription = game.addCommitListener(presentIfNewer);
|
|
28
|
+
game.start();
|
|
29
|
+
let cleanedUp = false;
|
|
30
|
+
|
|
31
|
+
return () => {
|
|
32
|
+
if (cleanedUp) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
cleanedUp = true;
|
|
36
|
+
subscription.remove();
|
|
37
|
+
if (game.status !== 'disposed') {
|
|
38
|
+
game.pause();
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { CoalescedPointerEvent } from './pointerCoalescer';
|
|
2
|
+
|
|
3
|
+
/** RN-side mirror of whether the trailing-flush sampler should be mounted. */
|
|
4
|
+
export interface SamplerMirrorState {
|
|
5
|
+
readonly generation: number;
|
|
6
|
+
readonly active: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Manual-gesture terminal lifecycle (F3).
|
|
11
|
+
*
|
|
12
|
+
* The RNGH 3 manual recognizer is activated explicitly on touch-down and
|
|
13
|
+
* must be returned to its terminal state explicitly: it stays active across
|
|
14
|
+
* separate touches and screen lifecycles otherwise. This pure decision
|
|
15
|
+
* drives the deactivation call in `GamePointerInput`; the recognizer-state
|
|
16
|
+
* operations themselves stay on the UI runtime.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** A single-pointer binding begins only with the first native touch. */
|
|
20
|
+
export function canBeginPrimaryPointer(nativeTouchCount: number): boolean {
|
|
21
|
+
'worklet';
|
|
22
|
+
return nativeTouchCount === 1;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** After an up edge, deactivate once RNGH reports that no touches remain. */
|
|
26
|
+
export function deactivateAfterUp(remainingTouches: number): boolean {
|
|
27
|
+
'worklet';
|
|
28
|
+
return remainingTouches === 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Unexpected finalization while a pointer is still active must neutralize ownership. */
|
|
32
|
+
export function cancelOnActiveFinalize(activePointerId: number | undefined): boolean {
|
|
33
|
+
'worklet';
|
|
34
|
+
return activePointerId !== undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Sampler mirror for the F2 trailing flush (F2 lifecycle).
|
|
39
|
+
*
|
|
40
|
+
* The trailing-flush frame callback must stay active exactly while the
|
|
41
|
+
* coalescer owns a pointer. Returns `true` when a begin crossed (pointer
|
|
42
|
+
* owned), `false` when a terminal edge crossed (pointer released), and
|
|
43
|
+
* `undefined` for empty batches (secondary touches — never toggle the
|
|
44
|
+
* sampler) and flushed moves (the pointer stays owned).
|
|
45
|
+
*/
|
|
46
|
+
export function samplerMirrorFromBatch(
|
|
47
|
+
batch: readonly CoalescedPointerEvent[],
|
|
48
|
+
): boolean | undefined {
|
|
49
|
+
'worklet';
|
|
50
|
+
if (batch.length === 0) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const kind = batch[batch.length - 1]?.kind;
|
|
54
|
+
if (kind === 'begin') {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
if (kind === 'end' || kind === 'cancel') {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Apply a UI-originated sampler update only when it belongs to the current
|
|
65
|
+
* binding generation.
|
|
66
|
+
*/
|
|
67
|
+
export function reduceSamplerMirrorState(
|
|
68
|
+
currentGeneration: number,
|
|
69
|
+
previous: SamplerMirrorState,
|
|
70
|
+
next: SamplerMirrorState,
|
|
71
|
+
): SamplerMirrorState {
|
|
72
|
+
return next.generation === currentGeneration ? next : previous;
|
|
73
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional measurement seams for the mounted game pipeline (F1).
|
|
3
|
+
*
|
|
4
|
+
* These callbacks let the Performance Lab attribute results to the real
|
|
5
|
+
* mounted components — GameView presentation, RNGH raw touches, and
|
|
6
|
+
* UI→RN forwarding — instead of headless substitutes. They are optional,
|
|
7
|
+
* cost nothing when absent (a single optional chain at event sites, never
|
|
8
|
+
* per-frame), and are not part of the gameplay API contract: the playground
|
|
9
|
+
* lab is their only consumer until the device matrix approves them.
|
|
10
|
+
*
|
|
11
|
+
* Runtime ownership:
|
|
12
|
+
* - Pointer callbacks run on the **UI runtime** (inside gesture worklets);
|
|
13
|
+
* pass stable workletized closures and only touch worklet-safe state
|
|
14
|
+
* (shared values, counters).
|
|
15
|
+
* - `onPresentCommit` runs on the **RN runtime** (inside the commit
|
|
16
|
+
* binding); pass a plain JS callback.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { CoalescedPointerEvent } from './pointerCoalescer';
|
|
20
|
+
|
|
21
|
+
/** Raw touch kinds as observed by the manual gesture callbacks. */
|
|
22
|
+
export type PointerStageKind = 'down' | 'move' | 'up' | 'cancel';
|
|
23
|
+
|
|
24
|
+
/** Pointer pipeline instrumentation (UI runtime callbacks). */
|
|
25
|
+
export interface GamePointerInstrumentation {
|
|
26
|
+
/** A raw touch reached the manual gesture handlers. */
|
|
27
|
+
readonly onRawTouch?: (kind: PointerStageKind, pointerId: number, atMs: number) => void;
|
|
28
|
+
/** A coalesced event crossed from the UI runtime into the RN runtime. */
|
|
29
|
+
readonly onForwarded?: (
|
|
30
|
+
kind: CoalescedPointerEvent['kind'],
|
|
31
|
+
pointerId: number,
|
|
32
|
+
atMs: number,
|
|
33
|
+
) => void;
|
|
34
|
+
/**
|
|
35
|
+
* RN runtime: the binding's verdict for a dispatched packet (F1). Only
|
|
36
|
+
* accepted packets may become latency samples; stale/rejected packets are
|
|
37
|
+
* counted but never sampled.
|
|
38
|
+
*/
|
|
39
|
+
readonly onDispatchResult?: (seq: number, atMs: number, accepted: boolean) => void;
|
|
40
|
+
/** The trailing-flush sampler mounted (true) or unmounted (false). */
|
|
41
|
+
readonly onSamplerChanged?: (mounted: boolean) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** GameView presentation instrumentation (RN runtime callback). */
|
|
45
|
+
export interface GameViewInstrumentation {
|
|
46
|
+
/** A commit was presented to the canvas frame shared value. */
|
|
47
|
+
readonly onPresentCommit?: (revision: number, atMs: number) => void;
|
|
48
|
+
/**
|
|
49
|
+
* UI runtime: the first UI frame that observed a new commit revision (the
|
|
50
|
+
* alpha clock's reset detects it). Skia GPU presentation is not proven by
|
|
51
|
+
* this hook, so latency consumers must name the stage honestly.
|
|
52
|
+
*/
|
|
53
|
+
readonly onUiRevisionObserved?: (revision: number, atMs: number) => void;
|
|
54
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import type { InputController } from '../core/input/types';
|
|
2
|
+
import type { Point2D } from '../geometry/types';
|
|
3
|
+
import { containsSurfacePoint, surfaceToWorld, type ResolvedViewport2D } from '../viewport2d';
|
|
4
|
+
import type { CoalescedPointerEvent } from './pointerCoalescer';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A coalesced pointer event stamped with the binding epoch it was scheduled
|
|
8
|
+
* under (F6). Packets scheduled before a layout revision or binding
|
|
9
|
+
* replacement carry the old epoch and are rejected on the RN runtime, so a
|
|
10
|
+
* stale begin cannot reacquire input with old coordinates and a stale
|
|
11
|
+
* terminal edge cannot release a newer capture that reused the pointer id.
|
|
12
|
+
*/
|
|
13
|
+
export type PointerPacket = CoalescedPointerEvent & {
|
|
14
|
+
/** Monotonic adapter-owned binding generation (F3 follow-up). */
|
|
15
|
+
readonly generation: number;
|
|
16
|
+
/** Adapter-owned layout epoch; bumped on layout revisions and unmount. */
|
|
17
|
+
readonly layoutEpoch: number;
|
|
18
|
+
/** Monotonic UI-runtime forward sequence (F1 latency causality). */
|
|
19
|
+
readonly seq: number;
|
|
20
|
+
/** UI-runtime timestamp of the forward (F1 latency causality). */
|
|
21
|
+
readonly atMs: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Monotonic generation source for factory-created bindings (F3). */
|
|
25
|
+
let nextFactoryGeneration = 1;
|
|
26
|
+
|
|
27
|
+
/** The identity a pointer binding is scoped to. */
|
|
28
|
+
export interface PointerBindingIdentity<TName extends string> {
|
|
29
|
+
/** The session input controller (identity changes when the session changes). */
|
|
30
|
+
readonly input: InputController<TName>;
|
|
31
|
+
/** The declared pointer action name. */
|
|
32
|
+
readonly action: TName;
|
|
33
|
+
/** Identity token for the viewport provider (changes on layout owner swaps). */
|
|
34
|
+
readonly viewport: unknown;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A binding plus the identity it was created for. */
|
|
38
|
+
export interface PointerBindingEntry<TName extends string> {
|
|
39
|
+
readonly binding: PointerBinding<TName>;
|
|
40
|
+
readonly identity: PointerBindingIdentity<TName>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Reuse the previous binding when the identity is unchanged, otherwise dispose
|
|
45
|
+
* it and create a fresh binding. Returns whether a new binding was created.
|
|
46
|
+
*/
|
|
47
|
+
export function createPointerBinding<TName extends string>(
|
|
48
|
+
identity: PointerBindingIdentity<TName>,
|
|
49
|
+
getViewport: () => ResolvedViewport2D | undefined,
|
|
50
|
+
previous: PointerBindingEntry<TName> | undefined,
|
|
51
|
+
): { readonly entry: PointerBindingEntry<TName>; readonly created: boolean } {
|
|
52
|
+
if (
|
|
53
|
+
previous !== undefined &&
|
|
54
|
+
previous.identity.input === identity.input &&
|
|
55
|
+
previous.identity.action === identity.action &&
|
|
56
|
+
previous.identity.viewport === identity.viewport
|
|
57
|
+
) {
|
|
58
|
+
return { entry: previous, created: false };
|
|
59
|
+
}
|
|
60
|
+
if (previous !== undefined) {
|
|
61
|
+
previous.binding.dispose();
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
entry: {
|
|
65
|
+
binding: new PointerBinding(
|
|
66
|
+
identity.action,
|
|
67
|
+
identity.input,
|
|
68
|
+
getViewport,
|
|
69
|
+
nextFactoryGeneration++,
|
|
70
|
+
),
|
|
71
|
+
identity,
|
|
72
|
+
},
|
|
73
|
+
created: true,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Platform-neutral pointer binding seam.
|
|
79
|
+
*
|
|
80
|
+
* It converts gesture positions from surface coordinates into logical world
|
|
81
|
+
* coordinates through the current resolved viewport, rejects begins that
|
|
82
|
+
* start in `fit` letterbox space, and forwards semantic events into the
|
|
83
|
+
* session input buffer. The high-level `handleTouches*` methods mirror the
|
|
84
|
+
* adapter's touch dispatch (including forwarding the final up position) so
|
|
85
|
+
* the full adapter behavior is testable without mounting native gesture
|
|
86
|
+
* views. Ownership and neutralization live in the input buffer.
|
|
87
|
+
*/
|
|
88
|
+
export class PointerBinding<TActionName extends string> {
|
|
89
|
+
readonly #action: TActionName;
|
|
90
|
+
readonly #input: InputController<TActionName>;
|
|
91
|
+
readonly #getViewport: () => ResolvedViewport2D | undefined;
|
|
92
|
+
readonly #generation: number;
|
|
93
|
+
#disposed = false;
|
|
94
|
+
|
|
95
|
+
constructor(
|
|
96
|
+
action: TActionName,
|
|
97
|
+
input: InputController<TActionName>,
|
|
98
|
+
getViewport: () => ResolvedViewport2D | undefined,
|
|
99
|
+
generation: number,
|
|
100
|
+
) {
|
|
101
|
+
this.#action = action;
|
|
102
|
+
this.#input = input;
|
|
103
|
+
this.#getViewport = getViewport;
|
|
104
|
+
this.#generation = generation;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The monotonic adapter-owned generation this binding belongs to (F3).
|
|
109
|
+
* Generations never reset to zero, so a replacement binding and the
|
|
110
|
+
* worklet closures that stamp its packets agree by construction — no
|
|
111
|
+
* post-commit synchronization is needed.
|
|
112
|
+
*/
|
|
113
|
+
get generation(): number {
|
|
114
|
+
return this.#generation;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Dispatch a coalesced packet stamped with its scheduling generation.
|
|
119
|
+
*
|
|
120
|
+
* Returns `false` (and forwards nothing) when the binding is disposed or
|
|
121
|
+
* the packet belongs to a different generation; the active gesture's
|
|
122
|
+
* packets keep flowing under the current generation with the latest
|
|
123
|
+
* viewport. Layout-epoch rejection happens on the adapter side (the epoch
|
|
124
|
+
* is adapter-owned and never resets, so it cannot desynchronize).
|
|
125
|
+
*/
|
|
126
|
+
dispatch(packet: PointerPacket): boolean {
|
|
127
|
+
if (this.#disposed || packet.generation !== this.#generation) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
switch (packet.kind) {
|
|
131
|
+
case 'begin':
|
|
132
|
+
this.handleTouchesDown(packet.pointerId, packet.x, packet.y);
|
|
133
|
+
break;
|
|
134
|
+
case 'move':
|
|
135
|
+
this.handleTouchesMove(packet.pointerId, packet.x, packet.y);
|
|
136
|
+
break;
|
|
137
|
+
case 'end':
|
|
138
|
+
this.handleTouchesUp(packet.pointerId, packet.x, packet.y);
|
|
139
|
+
break;
|
|
140
|
+
case 'cancel':
|
|
141
|
+
this.handleTouchesCancelled();
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Begin a pointer at a surface point.
|
|
149
|
+
*
|
|
150
|
+
* Returns `false` when the layout is invalid or the point starts in
|
|
151
|
+
* letterbox space; the gesture must be rejected and never becomes the owner.
|
|
152
|
+
*/
|
|
153
|
+
begin(pointerId: number, surfacePoint: Point2D): boolean {
|
|
154
|
+
if (this.#disposed) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
const viewport = this.#getViewport();
|
|
158
|
+
if (viewport === undefined) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
if (!containsSurfacePoint(viewport, surfacePoint)) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
this.#input.begin(this.#action, pointerId, surfaceToWorld(viewport, surfacePoint));
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Move a pointer. Conversion remains unbounded outside content bounds. */
|
|
169
|
+
move(pointerId: number, surfacePoint: Point2D): void {
|
|
170
|
+
if (this.#disposed) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const viewport = this.#getViewport();
|
|
174
|
+
if (viewport === undefined) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
this.#input.move(this.#action, pointerId, surfaceToWorld(viewport, surfacePoint));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** End a pointer. The owning pointer is released by the input buffer. */
|
|
181
|
+
end(pointerId: number): void {
|
|
182
|
+
if (this.#disposed) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
this.#input.end(this.#action, pointerId);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Cancel the active pointer (gesture cancellation, layout change, unmount). */
|
|
189
|
+
cancel(): void {
|
|
190
|
+
if (this.#disposed) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
this.#input.cancel(this.#action);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Handle a touch-down gesture event with a surface position. */
|
|
197
|
+
handleTouchesDown(pointerId: number, x: number, y: number): boolean {
|
|
198
|
+
return this.begin(pointerId, { x, y });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Handle a touch-move gesture event with a surface position. */
|
|
202
|
+
handleTouchesMove(pointerId: number, x: number, y: number): void {
|
|
203
|
+
this.move(pointerId, { x, y });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Handle a touch-up gesture event with its final surface position.
|
|
208
|
+
*
|
|
209
|
+
* The final position is forwarded before ownership ends so the documented
|
|
210
|
+
* release frame contains the actual point where the pointer lifted.
|
|
211
|
+
*/
|
|
212
|
+
handleTouchesUp(pointerId: number, x: number, y: number): void {
|
|
213
|
+
this.move(pointerId, { x, y });
|
|
214
|
+
this.end(pointerId);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Handle a platform touch cancellation. */
|
|
218
|
+
handleTouchesCancelled(): void {
|
|
219
|
+
this.cancel();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Stop forwarding events. Safe to call repeatedly. */
|
|
223
|
+
dispose(): void {
|
|
224
|
+
this.#disposed = true;
|
|
225
|
+
}
|
|
226
|
+
}
|