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,337 @@
1
+ import type { InputMap } from '../../definition/types';
2
+ import type { Point2D } from '../../geometry/types';
3
+ import type { ButtonState, InputController, InputFrame, PointerState } from './types';
4
+
5
+ type InputActionKind = 'button' | 'pointer';
6
+
7
+ interface MutableButtonState {
8
+ kind: 'button';
9
+ held: boolean;
10
+ pressed: boolean;
11
+ released: boolean;
12
+ cancelled: boolean;
13
+ }
14
+
15
+ interface MutablePointerState {
16
+ kind: 'pointer';
17
+ /** Whether the pointer owns the action slot (held until the terminal edge is sampled). */
18
+ ownsSlot: boolean;
19
+ /** Whether the owning pointer is currently down (published as `active`). */
20
+ active: boolean;
21
+ pressed: boolean;
22
+ released: boolean;
23
+ cancelled: boolean;
24
+ pointerId: number | undefined;
25
+ position: Point2D | undefined;
26
+ delta: { x: number; y: number };
27
+ /** A new pointer that arrived before the terminal edge was sampled. */
28
+ pendingBegin: { readonly pointerId: number; readonly position: Point2D } | undefined;
29
+ }
30
+
31
+ type MutableInputState = MutableButtonState | MutablePointerState;
32
+
33
+ interface InputBuffer<TActionName extends string> {
34
+ readonly controller: InputController<TActionName>;
35
+ sample(): InputFrame<TActionName>;
36
+ /** Clear scene-local edges while retaining pointers that remain physically down. */
37
+ resetForTransition(targetActions: readonly string[]): void;
38
+ reset(): void;
39
+ }
40
+
41
+ function freezePoint(x: number, y: number): Point2D {
42
+ return Object.freeze({ x, y });
43
+ }
44
+
45
+ function assertFinitePointerId(pointerId: number): void {
46
+ if (!Number.isFinite(pointerId)) {
47
+ throw new TypeError(`Pointer id must be finite (got ${pointerId})`);
48
+ }
49
+ }
50
+
51
+ function assertFinitePosition(position: Point2D): void {
52
+ if (!Number.isFinite(position.x) || !Number.isFinite(position.y)) {
53
+ throw new TypeError(`Pointer position must be finite (got ${position.x}, ${position.y})`);
54
+ }
55
+ }
56
+
57
+ export function createInputBuffer<TInput extends InputMap>(
58
+ input: TInput,
59
+ assertLive: () => void,
60
+ ): InputBuffer<Extract<keyof TInput, string>> {
61
+ type ActionName = Extract<keyof TInput, string>;
62
+ const states = new Map<ActionName, MutableInputState>();
63
+ // F1: monotonic count of accepted input events. The lab associates a commit
64
+ // with the inputs its sampled step actually consumed via this counter.
65
+ let acceptedCount = 0;
66
+
67
+ for (const [action, actionConfig] of Object.entries(input) as Array<[ActionName, { readonly type: InputActionKind }]>) {
68
+ if (actionConfig.type === 'pointer') {
69
+ states.set(action, {
70
+ kind: 'pointer',
71
+ ownsSlot: false,
72
+ active: false,
73
+ pressed: false,
74
+ released: false,
75
+ cancelled: false,
76
+ pointerId: undefined,
77
+ position: undefined,
78
+ delta: { x: 0, y: 0 },
79
+ pendingBegin: undefined,
80
+ });
81
+ } else {
82
+ states.set(action, {
83
+ kind: 'button',
84
+ held: false,
85
+ pressed: false,
86
+ released: false,
87
+ cancelled: false,
88
+ });
89
+ }
90
+ }
91
+
92
+ const getState = (action: ActionName): MutableInputState => {
93
+ const state = states.get(action);
94
+ if (!state) {
95
+ throw new Error(`Unknown input action: ${action}`);
96
+ }
97
+ return state;
98
+ };
99
+
100
+ const getButton = (action: ActionName): MutableButtonState => {
101
+ const state = getState(action);
102
+ if (state.kind !== 'button') {
103
+ throw new Error(`Input action "${action}" is a pointer action, not a button`);
104
+ }
105
+ return state;
106
+ };
107
+
108
+ const getPointer = (action: ActionName): MutablePointerState => {
109
+ const state = getState(action);
110
+ if (state.kind !== 'pointer') {
111
+ throw new Error(`Input action "${action}" is a button action, not a pointer`);
112
+ }
113
+ return state;
114
+ };
115
+
116
+ const controller: InputController<ActionName> = Object.freeze({
117
+ get acceptedCount() {
118
+ return acceptedCount;
119
+ },
120
+ press(action: ActionName) {
121
+ assertLive();
122
+ acceptedCount += 1;
123
+ const state = getButton(action);
124
+ state.pressed ||= !state.held;
125
+ state.held = true;
126
+ },
127
+ release(action: ActionName) {
128
+ assertLive();
129
+ acceptedCount += 1;
130
+ const state = getButton(action);
131
+ state.released ||= state.held;
132
+ state.held = false;
133
+ },
134
+ begin(action: ActionName, pointerId: number, position: Point2D) {
135
+ assertLive();
136
+ assertFinitePointerId(pointerId);
137
+ assertFinitePosition(position);
138
+ const state = getPointer(action);
139
+ if (state.ownsSlot && state.active) {
140
+ return; // Secondary pointer: the first pointer owns the action slot.
141
+ }
142
+ if (state.ownsSlot && !state.active) {
143
+ // The current owner released but the terminal edge has not been
144
+ // sampled yet. Queue the first new pointer and transfer after the
145
+ // release/cancel frame.
146
+ if (state.pendingBegin === undefined) {
147
+ state.pendingBegin = { pointerId, position: freezePoint(position.x, position.y) };
148
+ }
149
+ acceptedCount += 1;
150
+ return;
151
+ }
152
+ acceptedCount += 1;
153
+ state.ownsSlot = true;
154
+ state.active = true;
155
+ state.pressed = true;
156
+ state.pointerId = pointerId;
157
+ state.position = freezePoint(position.x, position.y);
158
+ state.delta.x = 0;
159
+ state.delta.y = 0;
160
+ },
161
+ move(action: ActionName, pointerId: number, position: Point2D) {
162
+ assertLive();
163
+ assertFinitePointerId(pointerId);
164
+ assertFinitePosition(position);
165
+ const state = getPointer(action);
166
+ if (!state.ownsSlot || !state.active || state.pointerId !== pointerId) {
167
+ return;
168
+ }
169
+ acceptedCount += 1;
170
+ if (state.position !== undefined) {
171
+ state.delta.x += position.x - state.position.x;
172
+ state.delta.y += position.y - state.position.y;
173
+ }
174
+ state.position = freezePoint(position.x, position.y);
175
+ },
176
+ end(action: ActionName, pointerId: number) {
177
+ assertLive();
178
+ assertFinitePointerId(pointerId);
179
+ const state = getPointer(action);
180
+ if (!state.ownsSlot || state.pointerId !== pointerId) {
181
+ return;
182
+ }
183
+ acceptedCount += 1;
184
+ // The slot stays owned until the release edge is sampled so a fast
185
+ // end -> begin between ticks cannot corrupt the terminal frame.
186
+ state.active = false;
187
+ state.released = true;
188
+ },
189
+ cancel(action: ActionName) {
190
+ assertLive();
191
+ const state = getState(action);
192
+ if (state.kind === 'button') {
193
+ acceptedCount += 1;
194
+ state.cancelled = true;
195
+ state.released ||= state.held;
196
+ state.held = false;
197
+ return;
198
+ }
199
+ if (!state.ownsSlot) {
200
+ return;
201
+ }
202
+ acceptedCount += 1;
203
+ // Same slot semantics as `end`.
204
+ state.active = false;
205
+ state.cancelled = true;
206
+ },
207
+ });
208
+
209
+ const samplePointer = (state: MutablePointerState): PointerState => {
210
+ const sampled = Object.freeze({
211
+ active: state.active,
212
+ pressed: state.pressed,
213
+ released: state.released,
214
+ cancelled: state.cancelled,
215
+ ...(state.pointerId !== undefined ? { pointerId: state.pointerId } : {}),
216
+ ...(state.position !== undefined ? { position: state.position } : {}),
217
+ delta: freezePoint(state.delta.x, state.delta.y),
218
+ });
219
+ state.pressed = false;
220
+ state.released = false;
221
+ state.cancelled = false;
222
+ state.delta.x = 0;
223
+ state.delta.y = 0;
224
+ if (sampled.released || sampled.cancelled) {
225
+ if (state.pendingBegin !== undefined) {
226
+ // Transfer to the queued pointer after the terminal frame.
227
+ state.active = true;
228
+ state.pressed = true;
229
+ state.pointerId = state.pendingBegin.pointerId;
230
+ state.position = state.pendingBegin.position;
231
+ state.delta.x = 0;
232
+ state.delta.y = 0;
233
+ state.pendingBegin = undefined;
234
+ } else {
235
+ state.ownsSlot = false;
236
+ state.pointerId = undefined;
237
+ state.position = undefined;
238
+ }
239
+ }
240
+ return sampled;
241
+ };
242
+
243
+ return {
244
+ controller,
245
+ sample() {
246
+ const sampled = new Map<ActionName, { readonly kind: InputActionKind; readonly state: ButtonState | PointerState }>();
247
+ for (const [action, state] of states) {
248
+ if (state.kind === 'pointer') {
249
+ sampled.set(action, { kind: 'pointer', state: samplePointer(state) });
250
+ } else {
251
+ sampled.set(action, {
252
+ kind: 'button',
253
+ state: Object.freeze({
254
+ held: state.held,
255
+ pressed: state.pressed,
256
+ released: state.released,
257
+ cancelled: state.cancelled,
258
+ }),
259
+ });
260
+ state.pressed = false;
261
+ state.released = false;
262
+ state.cancelled = false;
263
+ }
264
+ }
265
+
266
+ return Object.freeze({
267
+ button(action: ActionName) {
268
+ const entry = sampled.get(action);
269
+ if (!entry) {
270
+ throw new Error(`Unknown input action: ${action}`);
271
+ }
272
+ if (entry.kind !== 'button') {
273
+ throw new Error(`Input action "${action}" is a pointer action, not a button`);
274
+ }
275
+ return entry.state as ButtonState;
276
+ },
277
+ pointer(action: ActionName) {
278
+ const entry = sampled.get(action);
279
+ if (!entry) {
280
+ throw new Error(`Unknown input action: ${action}`);
281
+ }
282
+ if (entry.kind !== 'pointer') {
283
+ throw new Error(`Input action "${action}" is a button action, not a pointer`);
284
+ }
285
+ return entry.state as PointerState;
286
+ },
287
+ });
288
+ },
289
+ resetForTransition(targetActions: readonly string[]) {
290
+ const targetActionSet = new Set(targetActions);
291
+ for (const [action, state] of states) {
292
+ const preservePointer =
293
+ state.kind === 'pointer' &&
294
+ state.ownsSlot &&
295
+ state.active &&
296
+ targetActionSet.has(action);
297
+
298
+ if (state.kind === 'pointer') {
299
+ if (!preservePointer) {
300
+ state.ownsSlot = false;
301
+ state.active = false;
302
+ state.pointerId = undefined;
303
+ state.position = undefined;
304
+ }
305
+ state.pendingBegin = undefined;
306
+ state.delta.x = 0;
307
+ state.delta.y = 0;
308
+ } else {
309
+ // Buttons are semantic scene actions rather than a continuous spatial
310
+ // gesture. Do not leak a held button into a newly created scene.
311
+ state.held = false;
312
+ }
313
+ state.pressed = false;
314
+ state.released = false;
315
+ state.cancelled = false;
316
+ }
317
+ },
318
+ reset() {
319
+ for (const state of states.values()) {
320
+ if (state.kind === 'pointer') {
321
+ state.ownsSlot = false;
322
+ state.active = false;
323
+ state.pointerId = undefined;
324
+ state.position = undefined;
325
+ state.pendingBegin = undefined;
326
+ state.delta.x = 0;
327
+ state.delta.y = 0;
328
+ } else {
329
+ state.held = false;
330
+ }
331
+ state.pressed = false;
332
+ state.released = false;
333
+ state.cancelled = false;
334
+ }
335
+ },
336
+ };
337
+ }
@@ -0,0 +1,67 @@
1
+ import type { Point2D } from '../../geometry/types';
2
+
3
+ /** The sampled state of a digital action for one simulation tick. */
4
+ export interface ButtonState {
5
+ /** Whether the button is down at the tick boundary. */
6
+ readonly held: boolean;
7
+ /** Whether at least one press arrived since the previous tick. */
8
+ readonly pressed: boolean;
9
+ /** Whether at least one release arrived since the previous tick. */
10
+ readonly released: boolean;
11
+ /** Whether the platform cancelled ownership since the previous tick. */
12
+ readonly cancelled: boolean;
13
+ }
14
+
15
+ /**
16
+ * The sampled state of a pointer action for one simulation tick.
17
+ *
18
+ * `pressed`, `released`, and `cancelled` are one-tick edges. `active`
19
+ * persists while the owning pointer is down. `delta` accumulates movement
20
+ * between ticks and resets after each sample.
21
+ */
22
+ export interface PointerState {
23
+ /** Whether the owning pointer is currently down. */
24
+ readonly active: boolean;
25
+ /** Whether a pointer began since the previous tick. */
26
+ readonly pressed: boolean;
27
+ /** Whether the owning pointer was released since the previous tick. */
28
+ readonly released: boolean;
29
+ /** Whether the owning pointer was cancelled since the previous tick. */
30
+ readonly cancelled: boolean;
31
+ /** Stable identifier of the pointer that owns the action. */
32
+ readonly pointerId?: number;
33
+ /** Current logical position of the owning pointer. */
34
+ readonly position?: Point2D;
35
+ /** Logical movement accumulated since the previous tick. */
36
+ readonly delta: Point2D;
37
+ }
38
+
39
+ /** An immutable semantic input snapshot sampled once per simulation tick. */
40
+ export interface InputFrame<TActionName extends string> {
41
+ /** Read a declared digital button action. */
42
+ button(action: TActionName): ButtonState;
43
+ /** Read a declared pointer action. */
44
+ pointer(action: TActionName): PointerState;
45
+ }
46
+
47
+ /** Platform-facing controls for enqueueing semantic input changes. */
48
+ export interface InputController<TActionName extends string> {
49
+ /** Enqueue a press for a declared button action. */
50
+ press(action: TActionName): void;
51
+ /** Enqueue a release for a declared button action. */
52
+ release(action: TActionName): void;
53
+ /** Begin a pointer for a declared pointer action. */
54
+ begin(action: TActionName, pointerId: number, position: Point2D): void;
55
+ /** Move the owning pointer of a declared pointer action. */
56
+ move(action: TActionName, pointerId: number, position: Point2D): void;
57
+ /** End the owning pointer of a declared pointer action. */
58
+ end(action: TActionName, pointerId: number): void;
59
+ /** Cancel the active button or owning pointer of a declared action. */
60
+ cancel(action: TActionName): void;
61
+ /**
62
+ * Monotonic count of input events accepted by this controller (F1). The
63
+ * session's commit instrumentation uses it to associate a commit with the
64
+ * inputs its fixed step actually sampled.
65
+ */
66
+ readonly acceptedCount: number;
67
+ }