react-native-chess-kit 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +168 -168
- package/lib/commonjs/board-annotations.js +8 -8
- package/lib/commonjs/board-arrows.js +2 -2
- package/lib/commonjs/board-arrows.js.map +1 -1
- package/lib/commonjs/board-background.js +5 -5
- package/lib/commonjs/board-coordinates.js +8 -8
- package/lib/commonjs/board-drag-ghost.js +10 -10
- package/lib/commonjs/board-highlights.js +15 -15
- package/lib/commonjs/board-legal-dots.js +5 -5
- package/lib/commonjs/board-piece.js +25 -25
- package/lib/commonjs/board-pieces.js +6 -6
- package/lib/commonjs/board.js +24 -24
- package/lib/commonjs/promotion-picker.js +8 -8
- package/lib/commonjs/static-board.js +7 -7
- package/lib/commonjs/use-board-gesture.js +52 -33
- package/lib/commonjs/use-board-gesture.js.map +1 -1
- package/lib/commonjs/use-board-pieces.js +15 -15
- package/lib/commonjs/use-board-state.js +8 -8
- package/lib/commonjs/use-premove.js +12 -12
- package/lib/module/board-annotations.js +8 -8
- package/lib/module/board-arrows.js +2 -2
- package/lib/module/board-arrows.js.map +1 -1
- package/lib/module/board-background.js +5 -5
- package/lib/module/board-coordinates.js +8 -8
- package/lib/module/board-drag-ghost.js +10 -10
- package/lib/module/board-highlights.js +15 -15
- package/lib/module/board-legal-dots.js +5 -5
- package/lib/module/board-piece.js +25 -25
- package/lib/module/board-pieces.js +6 -6
- package/lib/module/board.js +24 -24
- package/lib/module/promotion-picker.js +8 -8
- package/lib/module/static-board.js +7 -7
- package/lib/module/use-board-gesture.js +52 -33
- package/lib/module/use-board-gesture.js.map +1 -1
- package/lib/module/use-board-pieces.js +15 -15
- package/lib/module/use-board-state.js +8 -8
- package/lib/module/use-premove.js +12 -12
- package/lib/typescript/use-board-gesture.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/board-annotations.tsx +147 -147
- package/src/board-arrows.tsx +2 -2
- package/src/board-background.tsx +46 -46
- package/src/board-coordinates.tsx +192 -192
- package/src/board-drag-ghost.tsx +132 -132
- package/src/board-highlights.tsx +226 -226
- package/src/board-legal-dots.tsx +73 -73
- package/src/board-piece.tsx +160 -160
- package/src/board-pieces.tsx +63 -63
- package/src/board.tsx +685 -685
- package/src/constants.ts +103 -103
- package/src/index.ts +101 -101
- package/src/pieces/default-pieces.tsx +383 -383
- package/src/pieces/index.ts +1 -1
- package/src/promotion-picker.tsx +147 -147
- package/src/static-board.tsx +187 -187
- package/src/themes.ts +129 -129
- package/src/types.ts +373 -373
- package/src/use-board-gesture.ts +429 -412
- package/src/use-board-pieces.ts +158 -158
- package/src/use-board-state.ts +111 -111
- package/src/use-premove.ts +59 -59
package/src/use-board-gesture.ts
CHANGED
|
@@ -1,412 +1,429 @@
|
|
|
1
|
-
import { useMemo, useCallback, useRef } from 'react';
|
|
2
|
-
import { Gesture } from 'react-native-gesture-handler';
|
|
3
|
-
import {
|
|
4
|
-
useSharedValue,
|
|
5
|
-
runOnJS,
|
|
6
|
-
} from 'react-native-reanimated';
|
|
7
|
-
|
|
8
|
-
import type {
|
|
9
|
-
ChessColor,
|
|
10
|
-
MoveMethod,
|
|
11
|
-
BoardPiece,
|
|
12
|
-
LegalMoveTarget,
|
|
13
|
-
GestureState,
|
|
14
|
-
PieceCode,
|
|
15
|
-
HapticType,
|
|
16
|
-
PremoveData,
|
|
17
|
-
} from './types';
|
|
18
|
-
import { xyToSquare } from './use-board-pieces';
|
|
19
|
-
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
// Callback types
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
|
|
24
|
-
type GestureCallbacks = {
|
|
25
|
-
onPieceSelected: (square: string) => void;
|
|
26
|
-
onPieceMoved: (from: string, to: string) => void;
|
|
27
|
-
onSelectionCleared: () => void;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
/** Rich callbacks exposed to consumers (all optional) */
|
|
31
|
-
type RichCallbacks = {
|
|
32
|
-
onPieceClick?: (square: string, piece: PieceCode) => void;
|
|
33
|
-
onSquareClick?: (square: string) => void;
|
|
34
|
-
onPieceDragBegin?: (square: string, piece: PieceCode) => void;
|
|
35
|
-
onPieceDragEnd?: (square: string, piece: PieceCode) => void;
|
|
36
|
-
onSquareLongPress?: (square: string) => void;
|
|
37
|
-
onHaptic?: (type: HapticType) => void;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
/** Premove-related callbacks from board.tsx */
|
|
41
|
-
type PremoveCallbacks = {
|
|
42
|
-
onPremoveSet?: (premove: PremoveData) => void;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
// Params
|
|
47
|
-
// ---------------------------------------------------------------------------
|
|
48
|
-
|
|
49
|
-
type UseBoardGestureParams = {
|
|
50
|
-
squareSize: number;
|
|
51
|
-
orientation: ChessColor;
|
|
52
|
-
gestureEnabled: boolean;
|
|
53
|
-
player: ChessColor | 'both';
|
|
54
|
-
moveMethod: MoveMethod;
|
|
55
|
-
pieces: BoardPiece[];
|
|
56
|
-
callbacks: GestureCallbacks;
|
|
57
|
-
richCallbacks?: RichCallbacks;
|
|
58
|
-
premoveCallbacks?: PremoveCallbacks;
|
|
59
|
-
/** Whether premoves are enabled */
|
|
60
|
-
premovesEnabled?: boolean;
|
|
61
|
-
/** Currently selected square (for tap-to-move second tap) */
|
|
62
|
-
selectedSquare: string | null;
|
|
63
|
-
/** Legal move targets from the currently selected piece */
|
|
64
|
-
legalMoves: LegalMoveTarget[];
|
|
65
|
-
/** Whose turn it is ('w' or 'b') — used for premove detection */
|
|
66
|
-
currentTurn?: 'w' | 'b';
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
type UseBoardGestureReturn = {
|
|
70
|
-
gesture: ReturnType<typeof Gesture.Pan>;
|
|
71
|
-
gestureState: GestureState;
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
// ---------------------------------------------------------------------------
|
|
75
|
-
// Helpers
|
|
76
|
-
// ---------------------------------------------------------------------------
|
|
77
|
-
|
|
78
|
-
/** Check if a piece color matches the current player turn */
|
|
79
|
-
function isPieceTurn(pieceColor: 'w' | 'b', currentTurn: 'w' | 'b'): boolean {
|
|
80
|
-
return pieceColor === currentTurn;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** Map piece color char to player ChessColor */
|
|
84
|
-
function pieceColorToPlayer(color: 'w' | 'b'): ChessColor {
|
|
85
|
-
return color === 'w' ? 'white' : 'black';
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// ---------------------------------------------------------------------------
|
|
89
|
-
// Hook
|
|
90
|
-
// ---------------------------------------------------------------------------
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Single centralized gesture handler for the entire board.
|
|
94
|
-
*
|
|
95
|
-
* Instead of 32 separate Gesture.Pan() handlers (one per piece), we use ONE
|
|
96
|
-
* handler on the board container. Touch -> coordinate math -> which piece.
|
|
97
|
-
*
|
|
98
|
-
* Supports three modes:
|
|
99
|
-
* - 'drag': drag piece to target square
|
|
100
|
-
* - 'click': tap source piece, then tap target square
|
|
101
|
-
* - 'both': drag or click (default)
|
|
102
|
-
*
|
|
103
|
-
* All drag position tracking uses shared values — zero JS bridge calls,
|
|
104
|
-
* zero re-renders during drag. Only the final move triggers JS via runOnJS.
|
|
105
|
-
*
|
|
106
|
-
* The gesture object is STABLE (only recreated when squareSize, orientation,
|
|
107
|
-
* gestureEnabled, player, or moveMethod change). Frequently-changing data
|
|
108
|
-
* (pieces, selectedSquare, legalMoves) is read from refs via runOnJS bridge
|
|
109
|
-
* functions, avoiding costly gesture teardown/rebuild on every move.
|
|
110
|
-
*
|
|
111
|
-
* v0.2.0 additions:
|
|
112
|
-
* - Rich callbacks (onPieceClick, onSquareClick, onPieceDragBegin, onPieceDragEnd)
|
|
113
|
-
* - Drag target square tracking (shared value for DragTargetHighlight)
|
|
114
|
-
* - Premove support (queue move when not your turn)
|
|
115
|
-
* - Haptic feedback via callback
|
|
116
|
-
* - Long press detection for onSquareLongPress
|
|
117
|
-
*/
|
|
118
|
-
export function useBoardGesture({
|
|
119
|
-
squareSize,
|
|
120
|
-
orientation,
|
|
121
|
-
gestureEnabled,
|
|
122
|
-
player,
|
|
123
|
-
moveMethod,
|
|
124
|
-
pieces,
|
|
125
|
-
callbacks,
|
|
126
|
-
richCallbacks,
|
|
127
|
-
premoveCallbacks,
|
|
128
|
-
premovesEnabled = false,
|
|
129
|
-
selectedSquare,
|
|
130
|
-
legalMoves,
|
|
131
|
-
currentTurn,
|
|
132
|
-
}: UseBoardGestureParams): UseBoardGestureReturn {
|
|
133
|
-
// Shared values for drag tracking — updated on UI thread only
|
|
134
|
-
const activeSquare = useSharedValue<string | null>(null);
|
|
135
|
-
const dragX = useSharedValue(0);
|
|
136
|
-
const dragY = useSharedValue(0);
|
|
137
|
-
const isDragging = useSharedValue(false);
|
|
138
|
-
const dragPieceCode = useSharedValue<string | null>(null);
|
|
139
|
-
const dragTargetSquare = useSharedValue<string | null>(null);
|
|
140
|
-
|
|
141
|
-
const gestureState: GestureState = {
|
|
142
|
-
activeSquare,
|
|
143
|
-
dragX,
|
|
144
|
-
dragY,
|
|
145
|
-
isDragging,
|
|
146
|
-
dragPieceCode,
|
|
147
|
-
dragTargetSquare,
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
// --- Refs for frequently-changing data (read from JS thread via runOnJS) ---
|
|
151
|
-
// These update every move but do NOT cause gesture object recreation.
|
|
152
|
-
const piecesRef = useRef(pieces);
|
|
153
|
-
piecesRef.current = pieces;
|
|
154
|
-
|
|
155
|
-
const selectedSquareRef = useRef(selectedSquare);
|
|
156
|
-
selectedSquareRef.current = selectedSquare;
|
|
157
|
-
|
|
158
|
-
const legalMovesRef = useRef(legalMoves);
|
|
159
|
-
legalMovesRef.current = legalMoves;
|
|
160
|
-
|
|
161
|
-
const callbacksRef = useRef(callbacks);
|
|
162
|
-
callbacksRef.current = callbacks;
|
|
163
|
-
|
|
164
|
-
const richCallbacksRef = useRef(richCallbacks);
|
|
165
|
-
richCallbacksRef.current = richCallbacks;
|
|
166
|
-
|
|
167
|
-
const premoveCallbacksRef = useRef(premoveCallbacks);
|
|
168
|
-
premoveCallbacksRef.current = premoveCallbacks;
|
|
169
|
-
|
|
170
|
-
const currentTurnRef = useRef(currentTurn);
|
|
171
|
-
currentTurnRef.current = currentTurn;
|
|
172
|
-
|
|
173
|
-
// Track the piece being dragged for rich drag-end callback
|
|
174
|
-
const draggedPieceRef = useRef<{ square: string; code: PieceCode } | null>(null);
|
|
175
|
-
|
|
176
|
-
// --- JS-thread bridge functions called from worklets via runOnJS ---
|
|
177
|
-
// These read current values from refs, so they always have fresh data.
|
|
178
|
-
|
|
179
|
-
const handleBegin = useCallback((touchX: number, touchY: number) => {
|
|
180
|
-
const square = xyToSquare(touchX, touchY, squareSize, orientation);
|
|
181
|
-
const currentPieces = piecesRef.current;
|
|
182
|
-
const currentSelected = selectedSquareRef.current;
|
|
183
|
-
const currentLegalMoves = legalMovesRef.current;
|
|
184
|
-
const cbs = callbacksRef.current;
|
|
185
|
-
const rich = richCallbacksRef.current;
|
|
186
|
-
const canClick = moveMethod !== 'drag';
|
|
187
|
-
|
|
188
|
-
// Build lookup for the current touch
|
|
189
|
-
const piece = currentPieces.find((p) => p.square === square);
|
|
190
|
-
const isPlayerPiece = piece
|
|
191
|
-
? player === 'both' || pieceColorToPlayer(piece.color) === player
|
|
192
|
-
: false;
|
|
193
|
-
|
|
194
|
-
// Check if it's this piece's turn (for premove detection)
|
|
195
|
-
const turn = currentTurnRef.current;
|
|
196
|
-
const isOwnTurn = piece && turn
|
|
197
|
-
? isPieceTurn(piece.color, turn)
|
|
198
|
-
: true; // default to true if turn not tracked
|
|
199
|
-
|
|
200
|
-
// Click-to-move: second tap on a legal target square
|
|
201
|
-
const legalSquares = new Set(currentLegalMoves.map((m) => m.square));
|
|
202
|
-
if (canClick && currentSelected && legalSquares.has(square)) {
|
|
203
|
-
cbs.onPieceMoved(currentSelected, square);
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
//
|
|
291
|
-
const square = xyToSquare(touchX, touchY, squareSize, orientation);
|
|
292
|
-
dragTargetSquare.value = square;
|
|
293
|
-
}, [squareSize, orientation, dragTargetSquare]);
|
|
294
|
-
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
//
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
lastDragTargetRow.value
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
1
|
+
import { useMemo, useCallback, useRef } from 'react';
|
|
2
|
+
import { Gesture } from 'react-native-gesture-handler';
|
|
3
|
+
import {
|
|
4
|
+
useSharedValue,
|
|
5
|
+
runOnJS,
|
|
6
|
+
} from 'react-native-reanimated';
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
ChessColor,
|
|
10
|
+
MoveMethod,
|
|
11
|
+
BoardPiece,
|
|
12
|
+
LegalMoveTarget,
|
|
13
|
+
GestureState,
|
|
14
|
+
PieceCode,
|
|
15
|
+
HapticType,
|
|
16
|
+
PremoveData,
|
|
17
|
+
} from './types';
|
|
18
|
+
import { xyToSquare } from './use-board-pieces';
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Callback types
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
type GestureCallbacks = {
|
|
25
|
+
onPieceSelected: (square: string) => void;
|
|
26
|
+
onPieceMoved: (from: string, to: string) => void;
|
|
27
|
+
onSelectionCleared: () => void;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Rich callbacks exposed to consumers (all optional) */
|
|
31
|
+
type RichCallbacks = {
|
|
32
|
+
onPieceClick?: (square: string, piece: PieceCode) => void;
|
|
33
|
+
onSquareClick?: (square: string) => void;
|
|
34
|
+
onPieceDragBegin?: (square: string, piece: PieceCode) => void;
|
|
35
|
+
onPieceDragEnd?: (square: string, piece: PieceCode) => void;
|
|
36
|
+
onSquareLongPress?: (square: string) => void;
|
|
37
|
+
onHaptic?: (type: HapticType) => void;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** Premove-related callbacks from board.tsx */
|
|
41
|
+
type PremoveCallbacks = {
|
|
42
|
+
onPremoveSet?: (premove: PremoveData) => void;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Params
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
type UseBoardGestureParams = {
|
|
50
|
+
squareSize: number;
|
|
51
|
+
orientation: ChessColor;
|
|
52
|
+
gestureEnabled: boolean;
|
|
53
|
+
player: ChessColor | 'both';
|
|
54
|
+
moveMethod: MoveMethod;
|
|
55
|
+
pieces: BoardPiece[];
|
|
56
|
+
callbacks: GestureCallbacks;
|
|
57
|
+
richCallbacks?: RichCallbacks;
|
|
58
|
+
premoveCallbacks?: PremoveCallbacks;
|
|
59
|
+
/** Whether premoves are enabled */
|
|
60
|
+
premovesEnabled?: boolean;
|
|
61
|
+
/** Currently selected square (for tap-to-move second tap) */
|
|
62
|
+
selectedSquare: string | null;
|
|
63
|
+
/** Legal move targets from the currently selected piece */
|
|
64
|
+
legalMoves: LegalMoveTarget[];
|
|
65
|
+
/** Whose turn it is ('w' or 'b') — used for premove detection */
|
|
66
|
+
currentTurn?: 'w' | 'b';
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
type UseBoardGestureReturn = {
|
|
70
|
+
gesture: ReturnType<typeof Gesture.Pan>;
|
|
71
|
+
gestureState: GestureState;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Helpers
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
/** Check if a piece color matches the current player turn */
|
|
79
|
+
function isPieceTurn(pieceColor: 'w' | 'b', currentTurn: 'w' | 'b'): boolean {
|
|
80
|
+
return pieceColor === currentTurn;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Map piece color char to player ChessColor */
|
|
84
|
+
function pieceColorToPlayer(color: 'w' | 'b'): ChessColor {
|
|
85
|
+
return color === 'w' ? 'white' : 'black';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Hook
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Single centralized gesture handler for the entire board.
|
|
94
|
+
*
|
|
95
|
+
* Instead of 32 separate Gesture.Pan() handlers (one per piece), we use ONE
|
|
96
|
+
* handler on the board container. Touch -> coordinate math -> which piece.
|
|
97
|
+
*
|
|
98
|
+
* Supports three modes:
|
|
99
|
+
* - 'drag': drag piece to target square
|
|
100
|
+
* - 'click': tap source piece, then tap target square
|
|
101
|
+
* - 'both': drag or click (default)
|
|
102
|
+
*
|
|
103
|
+
* All drag position tracking uses shared values — zero JS bridge calls,
|
|
104
|
+
* zero re-renders during drag. Only the final move triggers JS via runOnJS.
|
|
105
|
+
*
|
|
106
|
+
* The gesture object is STABLE (only recreated when squareSize, orientation,
|
|
107
|
+
* gestureEnabled, player, or moveMethod change). Frequently-changing data
|
|
108
|
+
* (pieces, selectedSquare, legalMoves) is read from refs via runOnJS bridge
|
|
109
|
+
* functions, avoiding costly gesture teardown/rebuild on every move.
|
|
110
|
+
*
|
|
111
|
+
* v0.2.0 additions:
|
|
112
|
+
* - Rich callbacks (onPieceClick, onSquareClick, onPieceDragBegin, onPieceDragEnd)
|
|
113
|
+
* - Drag target square tracking (shared value for DragTargetHighlight)
|
|
114
|
+
* - Premove support (queue move when not your turn)
|
|
115
|
+
* - Haptic feedback via callback
|
|
116
|
+
* - Long press detection for onSquareLongPress
|
|
117
|
+
*/
|
|
118
|
+
export function useBoardGesture({
|
|
119
|
+
squareSize,
|
|
120
|
+
orientation,
|
|
121
|
+
gestureEnabled,
|
|
122
|
+
player,
|
|
123
|
+
moveMethod,
|
|
124
|
+
pieces,
|
|
125
|
+
callbacks,
|
|
126
|
+
richCallbacks,
|
|
127
|
+
premoveCallbacks,
|
|
128
|
+
premovesEnabled = false,
|
|
129
|
+
selectedSquare,
|
|
130
|
+
legalMoves,
|
|
131
|
+
currentTurn,
|
|
132
|
+
}: UseBoardGestureParams): UseBoardGestureReturn {
|
|
133
|
+
// Shared values for drag tracking — updated on UI thread only
|
|
134
|
+
const activeSquare = useSharedValue<string | null>(null);
|
|
135
|
+
const dragX = useSharedValue(0);
|
|
136
|
+
const dragY = useSharedValue(0);
|
|
137
|
+
const isDragging = useSharedValue(false);
|
|
138
|
+
const dragPieceCode = useSharedValue<string | null>(null);
|
|
139
|
+
const dragTargetSquare = useSharedValue<string | null>(null);
|
|
140
|
+
|
|
141
|
+
const gestureState: GestureState = {
|
|
142
|
+
activeSquare,
|
|
143
|
+
dragX,
|
|
144
|
+
dragY,
|
|
145
|
+
isDragging,
|
|
146
|
+
dragPieceCode,
|
|
147
|
+
dragTargetSquare,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// --- Refs for frequently-changing data (read from JS thread via runOnJS) ---
|
|
151
|
+
// These update every move but do NOT cause gesture object recreation.
|
|
152
|
+
const piecesRef = useRef(pieces);
|
|
153
|
+
piecesRef.current = pieces;
|
|
154
|
+
|
|
155
|
+
const selectedSquareRef = useRef(selectedSquare);
|
|
156
|
+
selectedSquareRef.current = selectedSquare;
|
|
157
|
+
|
|
158
|
+
const legalMovesRef = useRef(legalMoves);
|
|
159
|
+
legalMovesRef.current = legalMoves;
|
|
160
|
+
|
|
161
|
+
const callbacksRef = useRef(callbacks);
|
|
162
|
+
callbacksRef.current = callbacks;
|
|
163
|
+
|
|
164
|
+
const richCallbacksRef = useRef(richCallbacks);
|
|
165
|
+
richCallbacksRef.current = richCallbacks;
|
|
166
|
+
|
|
167
|
+
const premoveCallbacksRef = useRef(premoveCallbacks);
|
|
168
|
+
premoveCallbacksRef.current = premoveCallbacks;
|
|
169
|
+
|
|
170
|
+
const currentTurnRef = useRef(currentTurn);
|
|
171
|
+
currentTurnRef.current = currentTurn;
|
|
172
|
+
|
|
173
|
+
// Track the piece being dragged for rich drag-end callback
|
|
174
|
+
const draggedPieceRef = useRef<{ square: string; code: PieceCode } | null>(null);
|
|
175
|
+
|
|
176
|
+
// --- JS-thread bridge functions called from worklets via runOnJS ---
|
|
177
|
+
// These read current values from refs, so they always have fresh data.
|
|
178
|
+
|
|
179
|
+
const handleBegin = useCallback((touchX: number, touchY: number) => {
|
|
180
|
+
const square = xyToSquare(touchX, touchY, squareSize, orientation);
|
|
181
|
+
const currentPieces = piecesRef.current;
|
|
182
|
+
const currentSelected = selectedSquareRef.current;
|
|
183
|
+
const currentLegalMoves = legalMovesRef.current;
|
|
184
|
+
const cbs = callbacksRef.current;
|
|
185
|
+
const rich = richCallbacksRef.current;
|
|
186
|
+
const canClick = moveMethod !== 'drag';
|
|
187
|
+
|
|
188
|
+
// Build lookup for the current touch
|
|
189
|
+
const piece = currentPieces.find((p) => p.square === square);
|
|
190
|
+
const isPlayerPiece = piece
|
|
191
|
+
? player === 'both' || pieceColorToPlayer(piece.color) === player
|
|
192
|
+
: false;
|
|
193
|
+
|
|
194
|
+
// Check if it's this piece's turn (for premove detection)
|
|
195
|
+
const turn = currentTurnRef.current;
|
|
196
|
+
const isOwnTurn = piece && turn
|
|
197
|
+
? isPieceTurn(piece.color, turn)
|
|
198
|
+
: true; // default to true if turn not tracked
|
|
199
|
+
|
|
200
|
+
// Click-to-move: second tap on a legal target square
|
|
201
|
+
const legalSquares = new Set(currentLegalMoves.map((m) => m.square));
|
|
202
|
+
if (canClick && currentSelected && legalSquares.has(square)) {
|
|
203
|
+
cbs.onPieceMoved(currentSelected, square);
|
|
204
|
+
// Clear optimistic worklet value — move is complete
|
|
205
|
+
activeSquare.value = null;
|
|
206
|
+
isDragging.value = false;
|
|
207
|
+
dragPieceCode.value = null;
|
|
208
|
+
dragTargetSquare.value = null;
|
|
209
|
+
rich?.onHaptic?.('move');
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Chess.com-style deselect: tapping the already-selected piece toggles it off
|
|
214
|
+
if (canClick && currentSelected === square) {
|
|
215
|
+
cbs.onSelectionCleared();
|
|
216
|
+
activeSquare.value = null;
|
|
217
|
+
dragPieceCode.value = null;
|
|
218
|
+
dragTargetSquare.value = null;
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (isPlayerPiece && piece) {
|
|
223
|
+
// Premove: player piece but not their turn
|
|
224
|
+
if (premovesEnabled && !isOwnTurn) {
|
|
225
|
+
// If there's already a selected square, this tap completes a premove
|
|
226
|
+
if (currentSelected && currentSelected !== square) {
|
|
227
|
+
premoveCallbacksRef.current?.onPremoveSet?.({
|
|
228
|
+
from: currentSelected,
|
|
229
|
+
to: square,
|
|
230
|
+
});
|
|
231
|
+
cbs.onSelectionCleared();
|
|
232
|
+
activeSquare.value = null;
|
|
233
|
+
dragPieceCode.value = null;
|
|
234
|
+
dragTargetSquare.value = null;
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
// First tap: select the piece for premove
|
|
238
|
+
// activeSquare.value / dragX.value / dragY.value already set by onBegin worklet
|
|
239
|
+
dragPieceCode.value = piece.code;
|
|
240
|
+
cbs.onPieceSelected(square);
|
|
241
|
+
rich?.onPieceClick?.(square, piece.code as PieceCode);
|
|
242
|
+
rich?.onHaptic?.('select');
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Normal case: tapped/started dragging a player piece on their turn
|
|
247
|
+
// activeSquare.value / dragX.value / dragY.value already set by onBegin worklet
|
|
248
|
+
dragPieceCode.value = piece.code;
|
|
249
|
+
draggedPieceRef.current = { square, code: piece.code as PieceCode };
|
|
250
|
+
cbs.onPieceSelected(square);
|
|
251
|
+
|
|
252
|
+
// Rich callbacks
|
|
253
|
+
rich?.onPieceClick?.(square, piece.code as PieceCode);
|
|
254
|
+
rich?.onHaptic?.('select');
|
|
255
|
+
} else {
|
|
256
|
+
// Tapped empty square or opponent piece
|
|
257
|
+
|
|
258
|
+
// If premoves enabled and there's a selection, check if this is a premove target
|
|
259
|
+
if (premovesEnabled && currentSelected && !isOwnTurn) {
|
|
260
|
+
premoveCallbacksRef.current?.onPremoveSet?.({
|
|
261
|
+
from: currentSelected,
|
|
262
|
+
to: square,
|
|
263
|
+
});
|
|
264
|
+
cbs.onSelectionCleared();
|
|
265
|
+
activeSquare.value = null;
|
|
266
|
+
dragPieceCode.value = null;
|
|
267
|
+
dragTargetSquare.value = null;
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Clear the optimistic worklet value — no valid piece here
|
|
272
|
+
activeSquare.value = null;
|
|
273
|
+
dragPieceCode.value = null;
|
|
274
|
+
dragTargetSquare.value = null;
|
|
275
|
+
if (currentSelected) {
|
|
276
|
+
cbs.onSelectionCleared();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Rich callback: square click (empty square or opponent piece)
|
|
280
|
+
rich?.onSquareClick?.(square);
|
|
281
|
+
}
|
|
282
|
+
}, [squareSize, orientation, player, moveMethod, premovesEnabled, activeSquare, dragX, dragY, isDragging, dragPieceCode, dragTargetSquare]);
|
|
283
|
+
|
|
284
|
+
const handleDragStart = useCallback((touchX: number, touchY: number) => {
|
|
285
|
+
const rich = richCallbacksRef.current;
|
|
286
|
+
const dragged = draggedPieceRef.current;
|
|
287
|
+
if (dragged) {
|
|
288
|
+
rich?.onPieceDragBegin?.(dragged.square, dragged.code);
|
|
289
|
+
}
|
|
290
|
+
// Update drag target to current square
|
|
291
|
+
const square = xyToSquare(touchX, touchY, squareSize, orientation);
|
|
292
|
+
dragTargetSquare.value = square;
|
|
293
|
+
}, [squareSize, orientation, dragTargetSquare]);
|
|
294
|
+
|
|
295
|
+
const handleDragUpdate = useCallback((touchX: number, touchY: number) => {
|
|
296
|
+
// Update drag target square (for highlight). This runs on JS thread
|
|
297
|
+
// but is called from worklet via runOnJS only when the square changes.
|
|
298
|
+
const square = xyToSquare(touchX, touchY, squareSize, orientation);
|
|
299
|
+
dragTargetSquare.value = square;
|
|
300
|
+
}, [squareSize, orientation, dragTargetSquare]);
|
|
301
|
+
|
|
302
|
+
const handleEnd = useCallback((touchX: number, touchY: number) => {
|
|
303
|
+
const fromSquare = activeSquare.value;
|
|
304
|
+
if (!fromSquare) return;
|
|
305
|
+
|
|
306
|
+
const toSquare = xyToSquare(touchX, touchY, squareSize, orientation);
|
|
307
|
+
isDragging.value = false;
|
|
308
|
+
dragTargetSquare.value = null;
|
|
309
|
+
|
|
310
|
+
const rich = richCallbacksRef.current;
|
|
311
|
+
|
|
312
|
+
// Fire drag end callback
|
|
313
|
+
const dragged = draggedPieceRef.current;
|
|
314
|
+
if (dragged) {
|
|
315
|
+
rich?.onPieceDragEnd?.(toSquare, dragged.code);
|
|
316
|
+
draggedPieceRef.current = null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (fromSquare !== toSquare) {
|
|
320
|
+
// Check if this is a premove (not your turn)
|
|
321
|
+
const turn = currentTurnRef.current;
|
|
322
|
+
const piece = piecesRef.current.find((p) => p.square === fromSquare);
|
|
323
|
+
const isOwnTurn = piece && turn ? isPieceTurn(piece.color, turn) : true;
|
|
324
|
+
|
|
325
|
+
if (premovesEnabled && !isOwnTurn) {
|
|
326
|
+
premoveCallbacksRef.current?.onPremoveSet?.({
|
|
327
|
+
from: fromSquare,
|
|
328
|
+
to: toSquare,
|
|
329
|
+
});
|
|
330
|
+
} else {
|
|
331
|
+
callbacksRef.current.onPieceMoved(fromSquare, toSquare);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Move completed — clear selection
|
|
335
|
+
activeSquare.value = null;
|
|
336
|
+
dragPieceCode.value = null;
|
|
337
|
+
}
|
|
338
|
+
// When fromSquare === toSquare (tap with no movement), keep selection alive
|
|
339
|
+
// so the user can complete a click-to-move on the next tap.
|
|
340
|
+
}, [squareSize, orientation, activeSquare, isDragging, dragPieceCode, dragTargetSquare, premovesEnabled]);
|
|
341
|
+
|
|
342
|
+
// Long press handler (separate gesture, composed with pan)
|
|
343
|
+
const handleLongPress = useCallback((touchX: number, touchY: number) => {
|
|
344
|
+
const square = xyToSquare(touchX, touchY, squareSize, orientation);
|
|
345
|
+
richCallbacksRef.current?.onSquareLongPress?.(square);
|
|
346
|
+
}, [squareSize, orientation]);
|
|
347
|
+
|
|
348
|
+
// --- Build the gesture (STABLE — only changes on layout/config changes) ---
|
|
349
|
+
const canDrag = moveMethod !== 'click';
|
|
350
|
+
|
|
351
|
+
// Track the last drag target square to avoid redundant runOnJS calls
|
|
352
|
+
const lastDragTargetCol = useSharedValue(-1);
|
|
353
|
+
const lastDragTargetRow = useSharedValue(-1);
|
|
354
|
+
|
|
355
|
+
const gesture = useMemo(() => {
|
|
356
|
+
return Gesture.Pan()
|
|
357
|
+
.enabled(gestureEnabled)
|
|
358
|
+
.minDistance(0) // Also detect taps (zero-distance pans)
|
|
359
|
+
.onBegin((e) => {
|
|
360
|
+
'worklet';
|
|
361
|
+
// Set shared values IMMEDIATELY on the UI thread so onStart can read them
|
|
362
|
+
// synchronously. xyToSquare is tagged 'worklet' — safe to call here.
|
|
363
|
+
// handleBegin (JS thread) will CLEAR activeSquare if the square is invalid.
|
|
364
|
+
const sq = xyToSquare(e.x, e.y, squareSize, orientation);
|
|
365
|
+
activeSquare.value = sq;
|
|
366
|
+
dragX.value = e.x;
|
|
367
|
+
dragY.value = e.y;
|
|
368
|
+
// Bridge to JS for piece lookup + selection logic
|
|
369
|
+
runOnJS(handleBegin)(e.x, e.y);
|
|
370
|
+
})
|
|
371
|
+
.onStart((e) => {
|
|
372
|
+
'worklet';
|
|
373
|
+
if (!canDrag || !activeSquare.value) return;
|
|
374
|
+
isDragging.value = true;
|
|
375
|
+
runOnJS(handleDragStart)(e.x, e.y);
|
|
376
|
+
})
|
|
377
|
+
.onUpdate((e) => {
|
|
378
|
+
'worklet';
|
|
379
|
+
if (!canDrag || !isDragging.value) return;
|
|
380
|
+
// Only 2 shared value writes — no JS bridge, no re-renders
|
|
381
|
+
dragX.value = e.x;
|
|
382
|
+
dragY.value = e.y;
|
|
383
|
+
|
|
384
|
+
// Update drag target square (only when square changes to minimize JS bridge calls)
|
|
385
|
+
const col = Math.max(0, Math.min(7, Math.floor(e.x / squareSize)));
|
|
386
|
+
const row = Math.max(0, Math.min(7, Math.floor(e.y / squareSize)));
|
|
387
|
+
if (col !== lastDragTargetCol.value || row !== lastDragTargetRow.value) {
|
|
388
|
+
lastDragTargetCol.value = col;
|
|
389
|
+
lastDragTargetRow.value = row;
|
|
390
|
+
runOnJS(handleDragUpdate)(e.x, e.y);
|
|
391
|
+
}
|
|
392
|
+
})
|
|
393
|
+
.onEnd((e) => {
|
|
394
|
+
'worklet';
|
|
395
|
+
if (!isDragging.value || !activeSquare.value) return;
|
|
396
|
+
runOnJS(handleEnd)(e.x, e.y);
|
|
397
|
+
})
|
|
398
|
+
.onFinalize(() => {
|
|
399
|
+
'worklet';
|
|
400
|
+
// Safety reset if gesture was interrupted
|
|
401
|
+
isDragging.value = false;
|
|
402
|
+
dragTargetSquare.value = null;
|
|
403
|
+
lastDragTargetCol.value = -1;
|
|
404
|
+
lastDragTargetRow.value = -1;
|
|
405
|
+
});
|
|
406
|
+
}, [
|
|
407
|
+
gestureEnabled,
|
|
408
|
+
canDrag,
|
|
409
|
+
squareSize,
|
|
410
|
+
handleBegin,
|
|
411
|
+
handleDragStart,
|
|
412
|
+
handleDragUpdate,
|
|
413
|
+
handleEnd,
|
|
414
|
+
// Shared values are stable refs — listed for exhaustive-deps but don't cause recreations
|
|
415
|
+
activeSquare,
|
|
416
|
+
dragX,
|
|
417
|
+
dragY,
|
|
418
|
+
isDragging,
|
|
419
|
+
dragTargetSquare,
|
|
420
|
+
lastDragTargetCol,
|
|
421
|
+
lastDragTargetRow,
|
|
422
|
+
]);
|
|
423
|
+
|
|
424
|
+
// Compose with long press if the consumer wants it
|
|
425
|
+
// For now, long press is detected via a separate gesture that runs simultaneously.
|
|
426
|
+
// This is done at the board level if needed — keeping the pan gesture clean here.
|
|
427
|
+
|
|
428
|
+
return { gesture, gestureState };
|
|
429
|
+
}
|