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,478 @@
|
|
|
1
|
+
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { StyleSheet, View } from 'react-native';
|
|
3
|
+
import {
|
|
4
|
+
GestureDetector,
|
|
5
|
+
GestureStateManager,
|
|
6
|
+
useManualGesture,
|
|
7
|
+
type ManualGestureConfig,
|
|
8
|
+
} from 'react-native-gesture-handler';
|
|
9
|
+
import { useFrameCallback, useSharedValue, type SharedValue } from 'react-native-reanimated';
|
|
10
|
+
import { scheduleOnRN } from 'react-native-worklets';
|
|
11
|
+
|
|
12
|
+
import type { InputMap, PointerInputAction, SceneMap } from '../definition/types';
|
|
13
|
+
import type { GameSession } from '../core/session/types';
|
|
14
|
+
import { GameViewportContext } from './GameView';
|
|
15
|
+
import {
|
|
16
|
+
canBeginPrimaryPointer,
|
|
17
|
+
cancelOnActiveFinalize,
|
|
18
|
+
deactivateAfterUp,
|
|
19
|
+
reduceSamplerMirrorState,
|
|
20
|
+
samplerMirrorFromBatch,
|
|
21
|
+
type SamplerMirrorState,
|
|
22
|
+
} from './gestureLifecycle';
|
|
23
|
+
import type { GamePointerInstrumentation } from './instrumentation';
|
|
24
|
+
import { isBeginAllowed } from './pointerContainment';
|
|
25
|
+
import {
|
|
26
|
+
createPointerCoalescerState,
|
|
27
|
+
reducePointerCoalescer,
|
|
28
|
+
type CoalescedPointerEvent,
|
|
29
|
+
type PointerCoalescerInput,
|
|
30
|
+
type PointerCoalescerState,
|
|
31
|
+
} from './pointerCoalescer';
|
|
32
|
+
import { PointerBinding, type PointerPacket } from './pointerBinding';
|
|
33
|
+
|
|
34
|
+
/** Declared pointer action names of an input map. */
|
|
35
|
+
type PointerActionName<TInput extends InputMap> = {
|
|
36
|
+
[TName in Extract<keyof TInput, string>]: TInput[TName] extends PointerInputAction ? TName : never;
|
|
37
|
+
}[Extract<keyof TInput, string>];
|
|
38
|
+
|
|
39
|
+
/** Monotonic adapter-owned binding generation; never resets to zero (F3). */
|
|
40
|
+
let nextBindingGeneration = 1;
|
|
41
|
+
|
|
42
|
+
type ManualTouchHandler = NonNullable<ManualGestureConfig['onTouchesDown']>;
|
|
43
|
+
type ManualFinalizeHandler = NonNullable<ManualGestureConfig['onFinalize']>;
|
|
44
|
+
|
|
45
|
+
/** Props for the pointer input adapter that covers the game surface. */
|
|
46
|
+
export interface GamePointerInputProps<TScenes extends SceneMap, TInput extends InputMap> {
|
|
47
|
+
/** The session whose input buffer receives pointer events. */
|
|
48
|
+
readonly game: GameSession<TScenes, TInput>;
|
|
49
|
+
/** A declared pointer action on the game. */
|
|
50
|
+
readonly action: PointerActionName<TInput>;
|
|
51
|
+
/** Optional measurement callbacks for the Performance Lab (F1). */
|
|
52
|
+
readonly instrumentation?: GamePointerInstrumentation;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function advanceSharedCoalescer(
|
|
56
|
+
sharedState: SharedValue<PointerCoalescerState>,
|
|
57
|
+
input: PointerCoalescerInput,
|
|
58
|
+
): readonly CoalescedPointerEvent[] {
|
|
59
|
+
'worklet';
|
|
60
|
+
const transition = reducePointerCoalescer(sharedState.value, input);
|
|
61
|
+
sharedState.value = transition.state;
|
|
62
|
+
return transition.events;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* F2 trailing-flush sampler (review fix).
|
|
67
|
+
*
|
|
68
|
+
* Mounted only while the coalescer owns a pointer: the frame callback starts
|
|
69
|
+
* active at creation (autostart), forwards a deferred trailing move whenever
|
|
70
|
+
* the flush interval elapses, and the component unmounts — unregistering the
|
|
71
|
+
* callback — when the final pointer exits. No permanent frame callback and
|
|
72
|
+
* no runtime activation calls.
|
|
73
|
+
*/
|
|
74
|
+
function TrailingFlushSampler({
|
|
75
|
+
coalescerState,
|
|
76
|
+
forwardEventOnJS,
|
|
77
|
+
instrumentation,
|
|
78
|
+
layoutEpoch,
|
|
79
|
+
forwardSeq,
|
|
80
|
+
generation,
|
|
81
|
+
}: {
|
|
82
|
+
readonly coalescerState: SharedValue<PointerCoalescerState>;
|
|
83
|
+
readonly forwardEventOnJS: (packet: PointerPacket) => void;
|
|
84
|
+
readonly instrumentation: GamePointerInstrumentation | undefined;
|
|
85
|
+
readonly layoutEpoch: SharedValue<number>;
|
|
86
|
+
readonly forwardSeq: SharedValue<number>;
|
|
87
|
+
readonly generation: number;
|
|
88
|
+
}) {
|
|
89
|
+
// F2 acceptance: report the sampler's mounted lifecycle so the lab can
|
|
90
|
+
// prove no idle frame callback survives pointer exit, replacement, or
|
|
91
|
+
// screen close/reopen.
|
|
92
|
+
const instrumentationRef = useRef(instrumentation);
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
instrumentationRef.current?.onSamplerChanged?.(true);
|
|
95
|
+
return () => {
|
|
96
|
+
instrumentationRef.current?.onSamplerChanged?.(false);
|
|
97
|
+
};
|
|
98
|
+
}, []);
|
|
99
|
+
|
|
100
|
+
useFrameCallback(() => {
|
|
101
|
+
'worklet';
|
|
102
|
+
const batch = advanceSharedCoalescer(coalescerState, {
|
|
103
|
+
kind: 'flush',
|
|
104
|
+
nowMs: Date.now(),
|
|
105
|
+
});
|
|
106
|
+
for (const forwarded of batch) {
|
|
107
|
+
instrumentation?.onForwarded?.(
|
|
108
|
+
forwarded.kind,
|
|
109
|
+
'pointerId' in forwarded ? forwarded.pointerId : -1,
|
|
110
|
+
Date.now(),
|
|
111
|
+
);
|
|
112
|
+
forwardSeq.value += 1;
|
|
113
|
+
scheduleOnRN(forwardEventOnJS, {
|
|
114
|
+
...forwarded,
|
|
115
|
+
generation,
|
|
116
|
+
layoutEpoch: layoutEpoch.value,
|
|
117
|
+
seq: forwardSeq.value,
|
|
118
|
+
atMs: Date.now(),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Bind one primary pointer to a declared pointer action.
|
|
127
|
+
*
|
|
128
|
+
* Gesture Handler is an implementation detail: the manual gesture activates
|
|
129
|
+
* explicitly on touch down, mirrors the letterbox containment check on the UI
|
|
130
|
+
* runtime (failing the gesture before activation), forwards plain touch data
|
|
131
|
+
* through a coalescer that crosses runtimes at most once per fixed step
|
|
132
|
+
* (latest dirty position only; edges never throttled or dropped), and ends
|
|
133
|
+
* explicitly when the last touch lifts. Ownership and edge sampling stay
|
|
134
|
+
* authoritative in the session input buffer, which re-validates every
|
|
135
|
+
* forwarded event on the JS side.
|
|
136
|
+
*
|
|
137
|
+
* All separately registered touch-handler worklets share one explicit
|
|
138
|
+
* coalescer SharedValue; closure-local mutation is not used because each
|
|
139
|
+
* handler can receive its own serialized closure copy. The binding is stable
|
|
140
|
+
* across ordinary React renders and is recreated only when its semantic
|
|
141
|
+
* session, action, or viewport owner changes.
|
|
142
|
+
*/
|
|
143
|
+
export function GamePointerInput<TScenes extends SceneMap, TInput extends InputMap>({
|
|
144
|
+
game,
|
|
145
|
+
action,
|
|
146
|
+
instrumentation,
|
|
147
|
+
}: GamePointerInputProps<TScenes, TInput>) {
|
|
148
|
+
const viewportContext = useContext(GameViewportContext);
|
|
149
|
+
|
|
150
|
+
if (viewportContext === null) {
|
|
151
|
+
throw new Error('GamePointerInput must be rendered inside a GameView');
|
|
152
|
+
}
|
|
153
|
+
const viewportShared = viewportContext.viewport;
|
|
154
|
+
const maxMoveIntervalMs = game.getRenderFrame().stepMs;
|
|
155
|
+
const coalescerState = useSharedValue<PointerCoalescerState>(() =>
|
|
156
|
+
createPointerCoalescerState(maxMoveIntervalMs),
|
|
157
|
+
);
|
|
158
|
+
const viewportBinding = viewportContext.binding;
|
|
159
|
+
// F3 follow-up: the binding identity includes the declared action, the
|
|
160
|
+
// session input controller, and the viewport owner; changing any of them
|
|
161
|
+
// creates a fresh binding stamped with a NEW monotonic generation that
|
|
162
|
+
// never resets to zero. The worklet closures created in the same render
|
|
163
|
+
// stamp packets with that same generation, so the packet producer and
|
|
164
|
+
// consumer agree by construction — no post-commit synchronization.
|
|
165
|
+
const binding = useMemo(
|
|
166
|
+
() => new PointerBinding(action, game.input, () => viewportBinding.resolved, nextBindingGeneration++),
|
|
167
|
+
[action, game, viewportBinding],
|
|
168
|
+
);
|
|
169
|
+
const generation = binding.generation;
|
|
170
|
+
// F6/F3: the layout epoch is adapter-owned (ref + UI mirror), bumped only
|
|
171
|
+
// on layout revisions and unmount; it never resets, so replacement cannot
|
|
172
|
+
// desynchronize it. The RN-side dispatch check rejects old-layout packets.
|
|
173
|
+
const layoutEpochRef = useRef(0);
|
|
174
|
+
const layoutEpoch = useSharedValue(0);
|
|
175
|
+
const bumpLayoutEpoch = useCallback(() => {
|
|
176
|
+
layoutEpochRef.current += 1;
|
|
177
|
+
layoutEpoch.value = layoutEpochRef.current;
|
|
178
|
+
}, [layoutEpoch]);
|
|
179
|
+
// F1: monotonic forward sequence carried with every packet so the RN side
|
|
180
|
+
// can attribute latency causally (never by reading a separate "latest").
|
|
181
|
+
const forwardSeq = useSharedValue(0);
|
|
182
|
+
|
|
183
|
+
// F2 follow-up: the sampler mirror is keyed by binding generation, so a
|
|
184
|
+
// replacement binding is inactive by construction and a stale terminal
|
|
185
|
+
// edge from an older generation can never alter the current generation's
|
|
186
|
+
// sampler state.
|
|
187
|
+
const [samplerState, setSamplerState] = useState<SamplerMirrorState>({
|
|
188
|
+
generation: -1,
|
|
189
|
+
active: false,
|
|
190
|
+
});
|
|
191
|
+
const reportSamplerState = useCallback(
|
|
192
|
+
(next: SamplerMirrorState) => {
|
|
193
|
+
setSamplerState((previous) =>
|
|
194
|
+
reduceSamplerMirrorState(generation, previous, next),
|
|
195
|
+
);
|
|
196
|
+
},
|
|
197
|
+
[generation],
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
// F2 review: `autostart` is only consulted at creation (changing it later
|
|
201
|
+
// re-registers but never activates) and `setActive` from an effect proved
|
|
202
|
+
// unreliable in this stack, so the sampler is a conditionally mounted
|
|
203
|
+
// component (see TrailingFlushSampler): it mounts with autostart active
|
|
204
|
+
// exactly while this mirror is true and unmounts when the pointer exits.
|
|
205
|
+
|
|
206
|
+
// JS-thread handler (never captured by gesture worklets): the binding
|
|
207
|
+
// rejects packets stamped with a stale epoch (layout revision, binding
|
|
208
|
+
// replacement, unmount) and re-validates the viewport before forwarding
|
|
209
|
+
// into the session input buffer.
|
|
210
|
+
const forwardEventOnJS = useCallback(
|
|
211
|
+
(packet: PointerPacket) => {
|
|
212
|
+
// F6/F3: adapter-owned layout epoch — packets scheduled under an older
|
|
213
|
+
// layout die here, before the binding sees them.
|
|
214
|
+
if (packet.layoutEpoch !== layoutEpochRef.current) {
|
|
215
|
+
instrumentation?.onDispatchResult?.(packet.seq, packet.atMs, false);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const accepted = binding.dispatch(packet);
|
|
219
|
+
instrumentation?.onDispatchResult?.(packet.seq, packet.atMs, accepted);
|
|
220
|
+
},
|
|
221
|
+
[binding, instrumentation],
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
const handleTouchesDown = useCallback<ManualTouchHandler>(
|
|
225
|
+
(event) => {
|
|
226
|
+
'worklet';
|
|
227
|
+
const touch = event.changedTouches[0];
|
|
228
|
+
if (touch === undefined) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
instrumentation?.onRawTouch?.('down', touch.id, Date.now());
|
|
232
|
+
// This adapter owns one primary pointer for the whole native gesture.
|
|
233
|
+
// If another touch is already present, it cannot replace a pointer that
|
|
234
|
+
// ended earlier while the manual recognizer is still active.
|
|
235
|
+
if (!canBeginPrimaryPointer(event.numberOfTouches)) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
// UI-side containment mirror: an invalid layout or `fit` letterbox
|
|
239
|
+
// begin fails the gesture before it activates. JS re-validates.
|
|
240
|
+
if (!isBeginAllowed(viewportShared.value, touch.x, touch.y)) {
|
|
241
|
+
GestureStateManager.fail(event.handlerTag);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const batch = advanceSharedCoalescer(coalescerState, {
|
|
245
|
+
kind: 'down',
|
|
246
|
+
pointerId: touch.id,
|
|
247
|
+
x: touch.x,
|
|
248
|
+
y: touch.y,
|
|
249
|
+
nowMs: Date.now(),
|
|
250
|
+
});
|
|
251
|
+
for (const forwarded of batch) {
|
|
252
|
+
instrumentation?.onForwarded?.(
|
|
253
|
+
forwarded.kind,
|
|
254
|
+
'pointerId' in forwarded ? forwarded.pointerId : -1,
|
|
255
|
+
Date.now(),
|
|
256
|
+
);
|
|
257
|
+
forwardSeq.value += 1;
|
|
258
|
+
scheduleOnRN(forwardEventOnJS, {
|
|
259
|
+
...forwarded,
|
|
260
|
+
generation,
|
|
261
|
+
layoutEpoch: layoutEpoch.value,
|
|
262
|
+
seq: forwardSeq.value,
|
|
263
|
+
atMs: Date.now(),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
const nextActive = samplerMirrorFromBatch(batch);
|
|
267
|
+
if (nextActive !== undefined) {
|
|
268
|
+
scheduleOnRN(reportSamplerState, { generation, active: nextActive });
|
|
269
|
+
}
|
|
270
|
+
GestureStateManager.activate(event.handlerTag);
|
|
271
|
+
},
|
|
272
|
+
[coalescerState, forwardEventOnJS, generation, instrumentation, layoutEpoch, reportSamplerState, viewportShared],
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
const handleTouchesMove = useCallback<ManualTouchHandler>(
|
|
276
|
+
(event) => {
|
|
277
|
+
'worklet';
|
|
278
|
+
for (const touch of event.changedTouches) {
|
|
279
|
+
instrumentation?.onRawTouch?.('move', touch.id, Date.now());
|
|
280
|
+
const batch = advanceSharedCoalescer(coalescerState, {
|
|
281
|
+
kind: 'move',
|
|
282
|
+
pointerId: touch.id,
|
|
283
|
+
x: touch.x,
|
|
284
|
+
y: touch.y,
|
|
285
|
+
nowMs: Date.now(),
|
|
286
|
+
});
|
|
287
|
+
for (const forwarded of batch) {
|
|
288
|
+
instrumentation?.onForwarded?.(
|
|
289
|
+
forwarded.kind,
|
|
290
|
+
'pointerId' in forwarded ? forwarded.pointerId : -1,
|
|
291
|
+
Date.now(),
|
|
292
|
+
);
|
|
293
|
+
forwardSeq.value += 1;
|
|
294
|
+
scheduleOnRN(forwardEventOnJS, {
|
|
295
|
+
...forwarded,
|
|
296
|
+
generation,
|
|
297
|
+
layoutEpoch: layoutEpoch.value,
|
|
298
|
+
seq: forwardSeq.value,
|
|
299
|
+
atMs: Date.now(),
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
[coalescerState, forwardEventOnJS, generation, instrumentation, layoutEpoch],
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
const handleTouchesUp = useCallback<ManualTouchHandler>(
|
|
308
|
+
(event) => {
|
|
309
|
+
'worklet';
|
|
310
|
+
let nextActive: boolean | undefined;
|
|
311
|
+
for (const touch of event.changedTouches) {
|
|
312
|
+
instrumentation?.onRawTouch?.('up', touch.id, Date.now());
|
|
313
|
+
const batch = advanceSharedCoalescer(coalescerState, {
|
|
314
|
+
kind: 'up',
|
|
315
|
+
pointerId: touch.id,
|
|
316
|
+
x: touch.x,
|
|
317
|
+
y: touch.y,
|
|
318
|
+
nowMs: Date.now(),
|
|
319
|
+
});
|
|
320
|
+
for (const forwarded of batch) {
|
|
321
|
+
instrumentation?.onForwarded?.(
|
|
322
|
+
forwarded.kind,
|
|
323
|
+
'pointerId' in forwarded ? forwarded.pointerId : -1,
|
|
324
|
+
Date.now(),
|
|
325
|
+
);
|
|
326
|
+
forwardSeq.value += 1;
|
|
327
|
+
scheduleOnRN(forwardEventOnJS, {
|
|
328
|
+
...forwarded,
|
|
329
|
+
generation,
|
|
330
|
+
layoutEpoch: layoutEpoch.value,
|
|
331
|
+
seq: forwardSeq.value,
|
|
332
|
+
atMs: Date.now(),
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
const mirror = samplerMirrorFromBatch(batch);
|
|
336
|
+
if (mirror !== undefined) {
|
|
337
|
+
nextActive = mirror;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (nextActive !== undefined) {
|
|
341
|
+
scheduleOnRN(reportSamplerState, { generation, active: nextActive });
|
|
342
|
+
}
|
|
343
|
+
if (deactivateAfterUp(event.numberOfTouches)) {
|
|
344
|
+
GestureStateManager.deactivate(event.handlerTag);
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
[coalescerState, forwardEventOnJS, generation, instrumentation, layoutEpoch, reportSamplerState],
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
const handleTouchesCancel = useCallback<ManualTouchHandler>(
|
|
351
|
+
() => {
|
|
352
|
+
'worklet';
|
|
353
|
+
instrumentation?.onRawTouch?.('cancel', -1, Date.now());
|
|
354
|
+
const batch = advanceSharedCoalescer(coalescerState, {
|
|
355
|
+
kind: 'cancel',
|
|
356
|
+
nowMs: Date.now(),
|
|
357
|
+
});
|
|
358
|
+
for (const forwarded of batch) {
|
|
359
|
+
instrumentation?.onForwarded?.(forwarded.kind, -1, Date.now());
|
|
360
|
+
forwardSeq.value += 1;
|
|
361
|
+
scheduleOnRN(forwardEventOnJS, {
|
|
362
|
+
...forwarded,
|
|
363
|
+
generation,
|
|
364
|
+
layoutEpoch: layoutEpoch.value,
|
|
365
|
+
seq: forwardSeq.value,
|
|
366
|
+
atMs: Date.now(),
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
const nextActive = samplerMirrorFromBatch(batch);
|
|
370
|
+
if (nextActive !== undefined) {
|
|
371
|
+
scheduleOnRN(reportSamplerState, { generation, active: nextActive });
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
[coalescerState, forwardEventOnJS, generation, instrumentation, layoutEpoch, reportSamplerState],
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
const handleFinalize = useCallback<ManualFinalizeHandler>(
|
|
378
|
+
() => {
|
|
379
|
+
'worklet';
|
|
380
|
+
if (!cancelOnActiveFinalize(coalescerState.value.active?.pointerId)) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
instrumentation?.onRawTouch?.('cancel', -1, Date.now());
|
|
384
|
+
const batch = advanceSharedCoalescer(coalescerState, {
|
|
385
|
+
kind: 'cancel',
|
|
386
|
+
nowMs: Date.now(),
|
|
387
|
+
});
|
|
388
|
+
for (const forwarded of batch) {
|
|
389
|
+
instrumentation?.onForwarded?.(forwarded.kind, -1, Date.now());
|
|
390
|
+
forwardSeq.value += 1;
|
|
391
|
+
scheduleOnRN(forwardEventOnJS, {
|
|
392
|
+
...forwarded,
|
|
393
|
+
generation,
|
|
394
|
+
layoutEpoch: layoutEpoch.value,
|
|
395
|
+
seq: forwardSeq.value,
|
|
396
|
+
atMs: Date.now(),
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
const nextActive = samplerMirrorFromBatch(batch);
|
|
400
|
+
if (nextActive !== undefined) {
|
|
401
|
+
scheduleOnRN(reportSamplerState, { generation, active: nextActive });
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
[coalescerState, forwardEventOnJS, generation, instrumentation, layoutEpoch, reportSamplerState],
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
// RNGH 3 re-registers its gesture callbacks whenever this config identity
|
|
408
|
+
// changes. Keep both the config and its worklets stable across unrelated
|
|
409
|
+
// React renders (for example, a score HUD update during a live drag).
|
|
410
|
+
const gestureConfig = useMemo<ManualGestureConfig>(
|
|
411
|
+
() => ({
|
|
412
|
+
shouldCancelWhenOutside: false,
|
|
413
|
+
onTouchesDown: handleTouchesDown,
|
|
414
|
+
onTouchesMove: handleTouchesMove,
|
|
415
|
+
onTouchesUp: handleTouchesUp,
|
|
416
|
+
onTouchesCancel: handleTouchesCancel,
|
|
417
|
+
onFinalize: handleFinalize,
|
|
418
|
+
}),
|
|
419
|
+
[
|
|
420
|
+
handleFinalize,
|
|
421
|
+
handleTouchesCancel,
|
|
422
|
+
handleTouchesDown,
|
|
423
|
+
handleTouchesMove,
|
|
424
|
+
handleTouchesUp,
|
|
425
|
+
],
|
|
426
|
+
);
|
|
427
|
+
const gesture = useManualGesture(gestureConfig);
|
|
428
|
+
|
|
429
|
+
useEffect(() => {
|
|
430
|
+
// F6/F3: a layout revision advances the adapter-owned layout epoch so
|
|
431
|
+
// queued old-layout packets die on arrival, while the active gesture
|
|
432
|
+
// keeps flowing (the binding reads the viewport lazily and the
|
|
433
|
+
// coalescer state is preserved). Binding replacement needs no epoch
|
|
434
|
+
// synchronization: the generation agreement is by construction.
|
|
435
|
+
const unsubscribeLayout = viewportBinding.subscribe(() => {
|
|
436
|
+
bumpLayoutEpoch();
|
|
437
|
+
});
|
|
438
|
+
return () => {
|
|
439
|
+
unsubscribeLayout();
|
|
440
|
+
// Unmount or replacement: bump the layout epoch so packets already in
|
|
441
|
+
// flight become harmless no-ops, neutralize old input ownership exactly
|
|
442
|
+
// once, reset the coalescer, remove the old sampler (generation
|
|
443
|
+
// mismatch makes it inactive by construction), and dispose the binding.
|
|
444
|
+
bumpLayoutEpoch();
|
|
445
|
+
setSamplerState({ generation: -1, active: false });
|
|
446
|
+
if (game.status !== 'disposed') {
|
|
447
|
+
binding.cancel();
|
|
448
|
+
}
|
|
449
|
+
coalescerState.value = createPointerCoalescerState(maxMoveIntervalMs);
|
|
450
|
+
binding.dispose();
|
|
451
|
+
};
|
|
452
|
+
}, [
|
|
453
|
+
binding,
|
|
454
|
+
bumpLayoutEpoch,
|
|
455
|
+
coalescerState,
|
|
456
|
+
game,
|
|
457
|
+
layoutEpoch,
|
|
458
|
+
maxMoveIntervalMs,
|
|
459
|
+
viewportBinding,
|
|
460
|
+
]);
|
|
461
|
+
|
|
462
|
+
return (
|
|
463
|
+
<GestureDetector gesture={gesture}>
|
|
464
|
+
<View style={StyleSheet.absoluteFill}>
|
|
465
|
+
{samplerState.active && samplerState.generation === generation ? (
|
|
466
|
+
<TrailingFlushSampler
|
|
467
|
+
coalescerState={coalescerState}
|
|
468
|
+
forwardEventOnJS={forwardEventOnJS}
|
|
469
|
+
instrumentation={instrumentation}
|
|
470
|
+
layoutEpoch={layoutEpoch}
|
|
471
|
+
forwardSeq={forwardSeq}
|
|
472
|
+
generation={generation}
|
|
473
|
+
/>
|
|
474
|
+
) : null}
|
|
475
|
+
</View>
|
|
476
|
+
</GestureDetector>
|
|
477
|
+
);
|
|
478
|
+
}
|