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.
Files changed (231) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/LICENSE +21 -0
  3. package/README.md +131 -0
  4. package/lib/module/assets/defineAssets.js +48 -0
  5. package/lib/module/assets/defineAssets.js.map +1 -0
  6. package/lib/module/assets/errors.js +38 -0
  7. package/lib/module/assets/errors.js.map +1 -0
  8. package/lib/module/assets/types.js +2 -0
  9. package/lib/module/assets/types.js.map +1 -0
  10. package/lib/module/assets/validation.js +99 -0
  11. package/lib/module/assets/validation.js.map +1 -0
  12. package/lib/module/core/frameDriver.js +20 -0
  13. package/lib/module/core/frameDriver.js.map +1 -0
  14. package/lib/module/core/input/createInputBuffer.js +296 -0
  15. package/lib/module/core/input/createInputBuffer.js.map +1 -0
  16. package/lib/module/core/input/types.js +4 -0
  17. package/lib/module/core/input/types.js.map +1 -0
  18. package/lib/module/core/session/createGameSession.js +554 -0
  19. package/lib/module/core/session/createGameSession.js.map +1 -0
  20. package/lib/module/core/session/deepFreeze.js +155 -0
  21. package/lib/module/core/session/deepFreeze.js.map +1 -0
  22. package/lib/module/core/session/diagnostics.js +2 -0
  23. package/lib/module/core/session/diagnostics.js.map +1 -0
  24. package/lib/module/core/session/types.js +43 -0
  25. package/lib/module/core/session/types.js.map +1 -0
  26. package/lib/module/definition/defineGame.js +57 -0
  27. package/lib/module/definition/defineGame.js.map +1 -0
  28. package/lib/module/definition/types.js +4 -0
  29. package/lib/module/definition/types.js.map +1 -0
  30. package/lib/module/geometry/types.js +2 -0
  31. package/lib/module/geometry/types.js.map +1 -0
  32. package/lib/module/index.js +12 -0
  33. package/lib/module/index.js.map +1 -0
  34. package/lib/module/package.json +1 -0
  35. package/lib/module/react/GamePointerInput.js +385 -0
  36. package/lib/module/react/GamePointerInput.js.map +1 -0
  37. package/lib/module/react/GameView.js +223 -0
  38. package/lib/module/react/GameView.js.map +1 -0
  39. package/lib/module/react/alphaClock.js +42 -0
  40. package/lib/module/react/alphaClock.js.map +1 -0
  41. package/lib/module/react/assets/createGameAssetStore.js +358 -0
  42. package/lib/module/react/assets/createGameAssetStore.js.map +1 -0
  43. package/lib/module/react/assets/decodeSkiaImage.js +54 -0
  44. package/lib/module/react/assets/decodeSkiaImage.js.map +1 -0
  45. package/lib/module/react/assets/errors.js +10 -0
  46. package/lib/module/react/assets/errors.js.map +1 -0
  47. package/lib/module/react/assets/useGameAssets.js +160 -0
  48. package/lib/module/react/assets/useGameAssets.js.map +1 -0
  49. package/lib/module/react/bindAppLifecycle.js +58 -0
  50. package/lib/module/react/bindAppLifecycle.js.map +1 -0
  51. package/lib/module/react/bindGameSession.js +37 -0
  52. package/lib/module/react/bindGameSession.js.map +1 -0
  53. package/lib/module/react/gestureLifecycle.js +68 -0
  54. package/lib/module/react/gestureLifecycle.js.map +1 -0
  55. package/lib/module/react/instrumentation.js +4 -0
  56. package/lib/module/react/instrumentation.js.map +1 -0
  57. package/lib/module/react/pointerBinding.js +196 -0
  58. package/lib/module/react/pointerBinding.js.map +1 -0
  59. package/lib/module/react/pointerCoalescer.js +224 -0
  60. package/lib/module/react/pointerCoalescer.js.map +1 -0
  61. package/lib/module/react/pointerContainment.js +27 -0
  62. package/lib/module/react/pointerContainment.js.map +1 -0
  63. package/lib/module/react/sprites/GameSprite.js +116 -0
  64. package/lib/module/react/sprites/GameSprite.js.map +1 -0
  65. package/lib/module/react/sprites/GameWorld2D.js +44 -0
  66. package/lib/module/react/sprites/GameWorld2D.js.map +1 -0
  67. package/lib/module/react/sprites/Sprite.js +147 -0
  68. package/lib/module/react/sprites/Sprite.js.map +1 -0
  69. package/lib/module/react/sprites/SpriteBatch.js +173 -0
  70. package/lib/module/react/sprites/SpriteBatch.js.map +1 -0
  71. package/lib/module/react/sprites/spriteBatchPolicy.js +25 -0
  72. package/lib/module/react/sprites/spriteBatchPolicy.js.map +1 -0
  73. package/lib/module/react/sprites/spriteTransform.js +135 -0
  74. package/lib/module/react/sprites/spriteTransform.js.map +1 -0
  75. package/lib/module/react/viewportBinding.js +101 -0
  76. package/lib/module/react/viewportBinding.js.map +1 -0
  77. package/lib/module/react.js +13 -0
  78. package/lib/module/react.js.map +1 -0
  79. package/lib/module/scene/defineScene.js +29 -0
  80. package/lib/module/scene/defineScene.js.map +1 -0
  81. package/lib/module/scene/types.js +4 -0
  82. package/lib/module/scene/types.js.map +1 -0
  83. package/lib/module/sprites/sampleSpriteClip.js +50 -0
  84. package/lib/module/sprites/sampleSpriteClip.js.map +1 -0
  85. package/lib/module/sprites/spriteAnimationState.js +165 -0
  86. package/lib/module/sprites/spriteAnimationState.js.map +1 -0
  87. package/lib/module/testing.js +57 -0
  88. package/lib/module/testing.js.map +1 -0
  89. package/lib/module/viewport2d/index.js +4 -0
  90. package/lib/module/viewport2d/index.js.map +1 -0
  91. package/lib/module/viewport2d/math.js +129 -0
  92. package/lib/module/viewport2d/math.js.map +1 -0
  93. package/lib/module/viewport2d/types.js +2 -0
  94. package/lib/module/viewport2d/types.js.map +1 -0
  95. package/lib/typescript/package.json +1 -0
  96. package/lib/typescript/src/assets/defineAssets.d.ts +20 -0
  97. package/lib/typescript/src/assets/defineAssets.d.ts.map +1 -0
  98. package/lib/typescript/src/assets/errors.d.ts +20 -0
  99. package/lib/typescript/src/assets/errors.d.ts.map +1 -0
  100. package/lib/typescript/src/assets/types.d.ts +138 -0
  101. package/lib/typescript/src/assets/types.d.ts.map +1 -0
  102. package/lib/typescript/src/assets/validation.d.ts +15 -0
  103. package/lib/typescript/src/assets/validation.d.ts.map +1 -0
  104. package/lib/typescript/src/core/frameDriver.d.ts +12 -0
  105. package/lib/typescript/src/core/frameDriver.d.ts.map +1 -0
  106. package/lib/typescript/src/core/input/createInputBuffer.d.ts +12 -0
  107. package/lib/typescript/src/core/input/createInputBuffer.d.ts.map +1 -0
  108. package/lib/typescript/src/core/input/types.d.ts +64 -0
  109. package/lib/typescript/src/core/input/types.d.ts.map +1 -0
  110. package/lib/typescript/src/core/session/createGameSession.d.ts +18 -0
  111. package/lib/typescript/src/core/session/createGameSession.d.ts.map +1 -0
  112. package/lib/typescript/src/core/session/deepFreeze.d.ts +27 -0
  113. package/lib/typescript/src/core/session/deepFreeze.d.ts.map +1 -0
  114. package/lib/typescript/src/core/session/diagnostics.d.ts +36 -0
  115. package/lib/typescript/src/core/session/diagnostics.d.ts.map +1 -0
  116. package/lib/typescript/src/core/session/types.d.ts +104 -0
  117. package/lib/typescript/src/core/session/types.d.ts.map +1 -0
  118. package/lib/typescript/src/definition/defineGame.d.ts +66 -0
  119. package/lib/typescript/src/definition/defineGame.d.ts.map +1 -0
  120. package/lib/typescript/src/definition/types.d.ts +56 -0
  121. package/lib/typescript/src/definition/types.d.ts.map +1 -0
  122. package/lib/typescript/src/geometry/types.d.ts +8 -0
  123. package/lib/typescript/src/geometry/types.d.ts.map +1 -0
  124. package/lib/typescript/src/index.d.ts +18 -0
  125. package/lib/typescript/src/index.d.ts.map +1 -0
  126. package/lib/typescript/src/react/GamePointerInput.d.ts +37 -0
  127. package/lib/typescript/src/react/GamePointerInput.d.ts.map +1 -0
  128. package/lib/typescript/src/react/GameView.d.ts +61 -0
  129. package/lib/typescript/src/react/GameView.d.ts.map +1 -0
  130. package/lib/typescript/src/react/alphaClock.d.ts +29 -0
  131. package/lib/typescript/src/react/alphaClock.d.ts.map +1 -0
  132. package/lib/typescript/src/react/assets/createGameAssetStore.d.ts +33 -0
  133. package/lib/typescript/src/react/assets/createGameAssetStore.d.ts.map +1 -0
  134. package/lib/typescript/src/react/assets/decodeSkiaImage.d.ts +12 -0
  135. package/lib/typescript/src/react/assets/decodeSkiaImage.d.ts.map +1 -0
  136. package/lib/typescript/src/react/assets/errors.d.ts +8 -0
  137. package/lib/typescript/src/react/assets/errors.d.ts.map +1 -0
  138. package/lib/typescript/src/react/assets/useGameAssets.d.ts +35 -0
  139. package/lib/typescript/src/react/assets/useGameAssets.d.ts.map +1 -0
  140. package/lib/typescript/src/react/bindAppLifecycle.d.ts +32 -0
  141. package/lib/typescript/src/react/bindAppLifecycle.d.ts.map +1 -0
  142. package/lib/typescript/src/react/bindGameSession.d.ts +14 -0
  143. package/lib/typescript/src/react/bindGameSession.d.ts.map +1 -0
  144. package/lib/typescript/src/react/gestureLifecycle.d.ts +37 -0
  145. package/lib/typescript/src/react/gestureLifecycle.d.ts.map +1 -0
  146. package/lib/typescript/src/react/instrumentation.d.ts +47 -0
  147. package/lib/typescript/src/react/instrumentation.d.ts.map +1 -0
  148. package/lib/typescript/src/react/pointerBinding.d.ts +104 -0
  149. package/lib/typescript/src/react/pointerBinding.d.ts.map +1 -0
  150. package/lib/typescript/src/react/pointerCoalescer.d.ts +106 -0
  151. package/lib/typescript/src/react/pointerCoalescer.d.ts.map +1 -0
  152. package/lib/typescript/src/react/pointerContainment.d.ts +12 -0
  153. package/lib/typescript/src/react/pointerContainment.d.ts.map +1 -0
  154. package/lib/typescript/src/react/sprites/GameSprite.d.ts +75 -0
  155. package/lib/typescript/src/react/sprites/GameSprite.d.ts.map +1 -0
  156. package/lib/typescript/src/react/sprites/GameWorld2D.d.ts +29 -0
  157. package/lib/typescript/src/react/sprites/GameWorld2D.d.ts.map +1 -0
  158. package/lib/typescript/src/react/sprites/Sprite.d.ts +43 -0
  159. package/lib/typescript/src/react/sprites/Sprite.d.ts.map +1 -0
  160. package/lib/typescript/src/react/sprites/SpriteBatch.d.ts +41 -0
  161. package/lib/typescript/src/react/sprites/SpriteBatch.d.ts.map +1 -0
  162. package/lib/typescript/src/react/sprites/spriteBatchPolicy.d.ts +10 -0
  163. package/lib/typescript/src/react/sprites/spriteBatchPolicy.d.ts.map +1 -0
  164. package/lib/typescript/src/react/sprites/spriteTransform.d.ts +86 -0
  165. package/lib/typescript/src/react/sprites/spriteTransform.d.ts.map +1 -0
  166. package/lib/typescript/src/react/viewportBinding.d.ts +42 -0
  167. package/lib/typescript/src/react/viewportBinding.d.ts.map +1 -0
  168. package/lib/typescript/src/react.d.ts +20 -0
  169. package/lib/typescript/src/react.d.ts.map +1 -0
  170. package/lib/typescript/src/scene/defineScene.d.ts +26 -0
  171. package/lib/typescript/src/scene/defineScene.d.ts.map +1 -0
  172. package/lib/typescript/src/scene/types.d.ts +71 -0
  173. package/lib/typescript/src/scene/types.d.ts.map +1 -0
  174. package/lib/typescript/src/sprites/sampleSpriteClip.d.ts +24 -0
  175. package/lib/typescript/src/sprites/sampleSpriteClip.d.ts.map +1 -0
  176. package/lib/typescript/src/sprites/spriteAnimationState.d.ts +45 -0
  177. package/lib/typescript/src/sprites/spriteAnimationState.d.ts.map +1 -0
  178. package/lib/typescript/src/testing.d.ts +28 -0
  179. package/lib/typescript/src/testing.d.ts.map +1 -0
  180. package/lib/typescript/src/viewport2d/index.d.ts +3 -0
  181. package/lib/typescript/src/viewport2d/index.d.ts.map +1 -0
  182. package/lib/typescript/src/viewport2d/math.d.ts +25 -0
  183. package/lib/typescript/src/viewport2d/math.d.ts.map +1 -0
  184. package/lib/typescript/src/viewport2d/types.d.ts +59 -0
  185. package/lib/typescript/src/viewport2d/types.d.ts.map +1 -0
  186. package/package.json +120 -0
  187. package/src/assets/defineAssets.ts +62 -0
  188. package/src/assets/errors.ts +64 -0
  189. package/src/assets/types.ts +164 -0
  190. package/src/assets/validation.ts +158 -0
  191. package/src/core/frameDriver.ts +31 -0
  192. package/src/core/input/createInputBuffer.ts +337 -0
  193. package/src/core/input/types.ts +67 -0
  194. package/src/core/session/createGameSession.ts +652 -0
  195. package/src/core/session/deepFreeze.ts +158 -0
  196. package/src/core/session/diagnostics.ts +35 -0
  197. package/src/core/session/types.ts +122 -0
  198. package/src/definition/defineGame.ts +94 -0
  199. package/src/definition/types.ts +67 -0
  200. package/src/geometry/types.ts +7 -0
  201. package/src/index.ts +84 -0
  202. package/src/react/GamePointerInput.tsx +478 -0
  203. package/src/react/GameView.tsx +317 -0
  204. package/src/react/alphaClock.ts +59 -0
  205. package/src/react/assets/createGameAssetStore.ts +459 -0
  206. package/src/react/assets/decodeSkiaImage.ts +64 -0
  207. package/src/react/assets/errors.ts +7 -0
  208. package/src/react/assets/useGameAssets.ts +171 -0
  209. package/src/react/bindAppLifecycle.ts +79 -0
  210. package/src/react/bindGameSession.ts +41 -0
  211. package/src/react/gestureLifecycle.ts +73 -0
  212. package/src/react/instrumentation.ts +54 -0
  213. package/src/react/pointerBinding.ts +226 -0
  214. package/src/react/pointerCoalescer.ts +234 -0
  215. package/src/react/pointerContainment.ts +23 -0
  216. package/src/react/sprites/GameSprite.tsx +192 -0
  217. package/src/react/sprites/GameWorld2D.tsx +46 -0
  218. package/src/react/sprites/Sprite.tsx +194 -0
  219. package/src/react/sprites/SpriteBatch.tsx +219 -0
  220. package/src/react/sprites/spriteBatchPolicy.ts +21 -0
  221. package/src/react/sprites/spriteTransform.ts +163 -0
  222. package/src/react/viewportBinding.ts +104 -0
  223. package/src/react.ts +37 -0
  224. package/src/scene/defineScene.ts +38 -0
  225. package/src/scene/types.ts +79 -0
  226. package/src/sprites/sampleSpriteClip.ts +51 -0
  227. package/src/sprites/spriteAnimationState.ts +176 -0
  228. package/src/testing.ts +61 -0
  229. package/src/viewport2d/index.ts +7 -0
  230. package/src/viewport2d/math.ts +134 -0
  231. 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
+ }