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,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
+ }