jspsych-tangram 0.0.11 → 0.0.12
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/dist/construct/index.browser.js +14 -5
- package/dist/construct/index.browser.js.map +1 -1
- package/dist/construct/index.browser.min.js +8 -8
- package/dist/construct/index.browser.min.js.map +1 -1
- package/dist/construct/index.cjs +14 -5
- package/dist/construct/index.cjs.map +1 -1
- package/dist/construct/index.js +14 -5
- package/dist/construct/index.js.map +1 -1
- package/dist/index.cjs +14 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +14 -5
- package/dist/index.js.map +1 -1
- package/dist/prep/index.browser.js +14 -5
- package/dist/prep/index.browser.js.map +1 -1
- package/dist/prep/index.browser.min.js +8 -8
- package/dist/prep/index.browser.min.js.map +1 -1
- package/dist/prep/index.cjs +14 -5
- package/dist/prep/index.cjs.map +1 -1
- package/dist/prep/index.js +14 -5
- package/dist/prep/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/components/board/GameBoard.tsx +102 -89
- package/tangram-construct.min.js +8 -8
- package/tangram-prep.min.js +8 -8
package/package.json
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* GameBoard - Core reusable tangram game board component
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* This component provides the complete tangram game functionality that can be
|
|
5
5
|
* shared between jsPsych plugins. It handles both construction and preparation
|
|
6
6
|
* game modes with full interaction support.
|
|
7
|
-
*
|
|
7
|
+
*
|
|
8
8
|
* ## Key Features
|
|
9
9
|
* - **Dependency Injection**: All game content provided via props (no hardcoded data)
|
|
10
10
|
* - **Multiple Input Modes**: Supports both click and drag interactions
|
|
11
11
|
* - **Layout Flexibility**: Circle and semicircle layout modes
|
|
12
12
|
* - **Event Tracking**: Comprehensive data collection for research
|
|
13
13
|
* - **State Management**: Uses BaseGameController for robust state handling
|
|
14
|
-
*
|
|
14
|
+
*
|
|
15
15
|
* ## Architecture
|
|
16
16
|
* - Uses modern React patterns with hooks
|
|
17
17
|
* - Integrates with BaseGameController for state management
|
|
18
18
|
* - Renders via Board component with computed layout
|
|
19
19
|
* - Handles completion events and data collection
|
|
20
|
-
*
|
|
20
|
+
*
|
|
21
21
|
* ## Usage in Plugins
|
|
22
22
|
* ```typescript
|
|
23
23
|
* // In jsPsych plugin trial method:
|
|
24
24
|
* const root = createRoot(display_element);
|
|
25
25
|
* root.render(React.createElement(GameBoard, {
|
|
26
26
|
* sectors: convertedSectors,
|
|
27
|
-
* quickstash: convertedMacros,
|
|
27
|
+
* quickstash: convertedMacros,
|
|
28
28
|
* primitives: PRIMITIVE_BLUEPRINTS,
|
|
29
29
|
* layout: "semicircle",
|
|
30
30
|
* target: "silhouette",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
* onComplete: handleTrialComplete
|
|
33
33
|
* }));
|
|
34
34
|
* ```
|
|
35
|
-
*
|
|
35
|
+
*
|
|
36
36
|
* @see {@link BaseGameController} For state management details
|
|
37
37
|
* @see {@link Board} For rendering implementation
|
|
38
38
|
* @since Phase 3.3 - Extracted from monolithic Board component
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
|
|
42
42
|
import React from "react";
|
|
43
43
|
import { BaseGameController } from "@/core/engine/state/BaseGameController";
|
|
44
|
-
import type {
|
|
45
|
-
Sector,
|
|
46
|
-
Blueprint,
|
|
44
|
+
import type {
|
|
45
|
+
Sector,
|
|
46
|
+
Blueprint,
|
|
47
47
|
PrimitiveBlueprint,
|
|
48
48
|
LayoutMode,
|
|
49
49
|
PlacementTarget,
|
|
@@ -54,8 +54,8 @@ import { solveLogicalBox } from "@/core/domain/solve";
|
|
|
54
54
|
import type { Poly } from "@/core/domain/types";
|
|
55
55
|
import { CONFIG } from "@/core/config/config";
|
|
56
56
|
import BoardView, { type AnchorDots as DotsType } from "./BoardView";
|
|
57
|
-
import {
|
|
58
|
-
inferUnitFromPolys,
|
|
57
|
+
import {
|
|
58
|
+
inferUnitFromPolys,
|
|
59
59
|
placeSilhouetteGridAlignedAsPolys } from "@/core/engine/geometry";
|
|
60
60
|
import { anchorsSilhouetteComplete, anchorsWorkspaceComplete } from "@/core/engine/validation/complete";
|
|
61
61
|
import { scalePolys } from "@/core/components/board/utils";
|
|
@@ -67,109 +67,109 @@ import { InteractionTracker } from "@/core/io/InteractionTracker";
|
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
69
|
* Configuration interface for GameBoard component
|
|
70
|
-
*
|
|
70
|
+
*
|
|
71
71
|
* This interface defines all the required game content and behavior settings
|
|
72
|
-
* that must be provided to the GameBoard component. All properties use
|
|
72
|
+
* that must be provided to the GameBoard component. All properties use
|
|
73
73
|
* dependency injection - no defaults are provided.
|
|
74
74
|
*/
|
|
75
75
|
export interface GameBoardConfig {
|
|
76
|
-
/**
|
|
76
|
+
/**
|
|
77
77
|
* Array of sector definitions containing target silhouettes to construct
|
|
78
78
|
* Each sector represents one puzzle to solve
|
|
79
79
|
*/
|
|
80
80
|
sectors: Sector[];
|
|
81
|
-
|
|
82
|
-
/**
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
83
|
* Array of pre-made composite piece blueprints (macros)
|
|
84
84
|
* Usually created in prep trials and passed to construction trials
|
|
85
85
|
*/
|
|
86
86
|
quickstash: Blueprint[];
|
|
87
|
-
|
|
88
|
-
/**
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
89
|
* Array of primitive tangram piece definitions
|
|
90
90
|
* These are the basic 7 tangram shapes that never change
|
|
91
91
|
*/
|
|
92
92
|
primitives: PrimitiveBlueprint[];
|
|
93
|
-
|
|
94
|
-
/**
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
95
|
* Layout mode for the game board
|
|
96
96
|
* - "circle": Full circular layout with pieces arranged in complete circle
|
|
97
97
|
* - "semicircle": Half-circle layout with pieces in arc above workspace
|
|
98
98
|
*/
|
|
99
99
|
layout: LayoutMode;
|
|
100
|
-
|
|
101
|
-
/**
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
102
|
* Target placement mode for piece validation
|
|
103
103
|
* - "workspace": Pieces can be placed freely in workspace area
|
|
104
104
|
* - "silhouette": Pieces must be placed within target silhouette bounds
|
|
105
105
|
*/
|
|
106
106
|
target: PlacementTarget;
|
|
107
|
-
|
|
108
|
-
/**
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
109
|
* Input interaction mode
|
|
110
110
|
* - "click": Click to select pieces, click to place (better for touch)
|
|
111
111
|
* - "drag": Drag pieces directly with mouse/touch
|
|
112
112
|
*/
|
|
113
113
|
input: InputMode;
|
|
114
|
-
|
|
115
|
-
/**
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
116
|
* Time limit for the trial in milliseconds
|
|
117
117
|
* Set to 0 for no time limit
|
|
118
118
|
*/
|
|
119
119
|
timeLimitMs: number;
|
|
120
|
-
|
|
121
|
-
/**
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
122
|
* Maximum number of quickstash/macro slots to display
|
|
123
123
|
* Controls size of the macro selection ring
|
|
124
124
|
*/
|
|
125
125
|
maxQuickstashSlots: number;
|
|
126
|
-
|
|
127
|
-
/**
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
128
|
* Maximum number of primitive pieces allowed in a composite
|
|
129
129
|
* Used for validation when creating macros (prep mode only)
|
|
130
130
|
*/
|
|
131
131
|
maxCompositeSize?: number;
|
|
132
|
-
|
|
132
|
+
|
|
133
133
|
/**
|
|
134
134
|
* Game mode to determine behavior differences
|
|
135
135
|
* - "construction": Standard puzzle-solving mode with toggle
|
|
136
136
|
* - "prep": Macro creation mode with primitives-only center
|
|
137
137
|
*/
|
|
138
138
|
mode?: "construction" | "prep";
|
|
139
|
-
|
|
139
|
+
|
|
140
140
|
/**
|
|
141
141
|
* Minimum pieces required per macro (prep mode only)
|
|
142
142
|
* Used for submit button validation
|
|
143
143
|
*/
|
|
144
144
|
minPiecesPerMacro?: number;
|
|
145
|
-
|
|
145
|
+
|
|
146
146
|
/**
|
|
147
147
|
* Whether all slots must be filled to complete trial (prep mode only)
|
|
148
148
|
* Used for submit button validation
|
|
149
149
|
*/
|
|
150
150
|
requireAllSlots?: boolean;
|
|
151
|
-
|
|
151
|
+
|
|
152
152
|
/**
|
|
153
153
|
* Whether to show tangram target shapes decomposed into individual primitives with borders
|
|
154
154
|
* Independent from showBorders/hideTouchingBorders (which control spawned pieces)
|
|
155
155
|
*/
|
|
156
156
|
showTangramDecomposition?: boolean;
|
|
157
|
-
|
|
157
|
+
|
|
158
158
|
/**
|
|
159
159
|
* HTML content to display above the gameboard as instructions
|
|
160
160
|
* Allows rich formatting via HTML
|
|
161
161
|
*/
|
|
162
162
|
instructions?: string;
|
|
163
|
-
|
|
163
|
+
|
|
164
164
|
/**
|
|
165
165
|
* Cleaned jsPsych plugin parameters (excluding callbacks)
|
|
166
166
|
* Saved to trial data for analysis
|
|
167
167
|
*/
|
|
168
168
|
trialParams?: any;
|
|
169
|
-
|
|
169
|
+
|
|
170
170
|
/** Optional CSS width for the game board SVG */
|
|
171
171
|
width?: number;
|
|
172
|
-
|
|
172
|
+
|
|
173
173
|
/** Optional CSS height for the game board SVG */
|
|
174
174
|
height?: number;
|
|
175
175
|
}
|
|
@@ -222,25 +222,25 @@ export interface GameBoardProps extends GameBoardConfig {
|
|
|
222
222
|
|
|
223
223
|
/**
|
|
224
224
|
* Core GameBoard component that encapsulates all tangram game functionality
|
|
225
|
-
*
|
|
225
|
+
*
|
|
226
226
|
* This is the main reusable component that provides complete tangram game
|
|
227
227
|
* functionality for jsPsych plugins. It handles all aspects of the game
|
|
228
228
|
* including piece interaction, collision detection, completion validation,
|
|
229
229
|
* and data collection.
|
|
230
|
-
*
|
|
230
|
+
*
|
|
231
231
|
* ## Key Responsibilities
|
|
232
232
|
* - Creates and manages BaseGameController for state management
|
|
233
|
-
* - Computes layout geometry for board rendering
|
|
233
|
+
* - Computes layout geometry for board rendering
|
|
234
234
|
* - Handles event callbacks for plugin integration
|
|
235
235
|
* - Manages CSS sizing for responsive display
|
|
236
|
-
*
|
|
236
|
+
*
|
|
237
237
|
* ## State Management
|
|
238
238
|
* Uses BaseGameController internally which provides:
|
|
239
239
|
* - Piece placement and validation
|
|
240
240
|
* - Sector completion tracking
|
|
241
241
|
* - Event logging for data collection
|
|
242
242
|
* - Collision detection and snapping
|
|
243
|
-
*
|
|
243
|
+
*
|
|
244
244
|
* @param props - Configuration and event callbacks for the game
|
|
245
245
|
* @returns React component rendering the complete tangram game interface
|
|
246
246
|
*/
|
|
@@ -268,10 +268,10 @@ export default function GameBoard(props: GameBoardProps) {
|
|
|
268
268
|
onTrialEnd,
|
|
269
269
|
onControllerReady
|
|
270
270
|
} = props;
|
|
271
|
-
|
|
271
|
+
|
|
272
272
|
// Timer state for countdown
|
|
273
273
|
const [timeRemaining, setTimeRemaining] = React.useState(timeLimitMs > 0 ? Math.floor(timeLimitMs / 1000) : 0);
|
|
274
|
-
|
|
274
|
+
|
|
275
275
|
// Initialize game controller with injected data
|
|
276
276
|
const controller = React.useMemo(() => {
|
|
277
277
|
const gameConfig = {
|
|
@@ -286,11 +286,11 @@ export default function GameBoard(props: GameBoardProps) {
|
|
|
286
286
|
...(props.minPiecesPerMacro !== undefined && { minPiecesPerMacro: props.minPiecesPerMacro }),
|
|
287
287
|
...(props.requireAllSlots !== undefined && { requireAllSlots: props.requireAllSlots })
|
|
288
288
|
};
|
|
289
|
-
|
|
289
|
+
|
|
290
290
|
return new BaseGameController(
|
|
291
291
|
sectors,
|
|
292
292
|
quickstash,
|
|
293
|
-
primitives,
|
|
293
|
+
primitives,
|
|
294
294
|
gameConfig
|
|
295
295
|
);
|
|
296
296
|
}, [sectors, quickstash, primitives, layoutMode, target, input, timeLimitMs, maxQuickstashSlots, maxCompositeSize, mode, props.minPiecesPerMacro, props.requireAllSlots]);
|
|
@@ -322,12 +322,12 @@ export default function GameBoard(props: GameBoardProps) {
|
|
|
322
322
|
};
|
|
323
323
|
}, [tracker]);
|
|
324
324
|
|
|
325
|
-
// Compute layout geometry
|
|
325
|
+
// Compute layout geometry
|
|
326
326
|
const { layout, viewBox } = React.useMemo(() => {
|
|
327
327
|
const nSectors = sectors.length;
|
|
328
328
|
const sectorIds = sectors.map(s => s.id);
|
|
329
329
|
const masksPerSector: Poly[][] = sectors.map(s => s.silhouette.mask ?? []);
|
|
330
|
-
|
|
330
|
+
|
|
331
331
|
const logicalBox = solveLogicalBox({
|
|
332
332
|
n: nSectors,
|
|
333
333
|
layoutMode,
|
|
@@ -337,7 +337,7 @@ export default function GameBoard(props: GameBoardProps) {
|
|
|
337
337
|
layoutPadPx: CONFIG.layout.paddingPx,
|
|
338
338
|
masks: masksPerSector,
|
|
339
339
|
});
|
|
340
|
-
|
|
340
|
+
|
|
341
341
|
const layout = computeCircleLayout(
|
|
342
342
|
{ w: logicalBox.LOGICAL_W, h: logicalBox.LOGICAL_H },
|
|
343
343
|
nSectors,
|
|
@@ -350,7 +350,7 @@ export default function GameBoard(props: GameBoardProps) {
|
|
|
350
350
|
masks: masksPerSector,
|
|
351
351
|
}
|
|
352
352
|
);
|
|
353
|
-
|
|
353
|
+
|
|
354
354
|
return {
|
|
355
355
|
layout,
|
|
356
356
|
viewBox: { w: logicalBox.LOGICAL_W, h: logicalBox.LOGICAL_H }
|
|
@@ -366,10 +366,10 @@ export default function GameBoard(props: GameBoardProps) {
|
|
|
366
366
|
onControllerReady(controller, layout, force);
|
|
367
367
|
}
|
|
368
368
|
}, [controller, layout, onControllerReady, force]);
|
|
369
|
-
|
|
369
|
+
|
|
370
370
|
// Game completion tracking
|
|
371
371
|
const [gameCompleted, setGameCompleted] = React.useState(false);
|
|
372
|
-
|
|
372
|
+
|
|
373
373
|
// Watch for game completion
|
|
374
374
|
React.useEffect(() => {
|
|
375
375
|
const checkGameCompletion = () => {
|
|
@@ -393,21 +393,21 @@ export default function GameBoard(props: GameBoardProps) {
|
|
|
393
393
|
// Check completion whenever controller state updates
|
|
394
394
|
checkGameCompletion();
|
|
395
395
|
}, [controller.updateCount, sectors, gameCompleted, controller, tracker]);
|
|
396
|
-
|
|
396
|
+
|
|
397
397
|
// Sector completion callback
|
|
398
398
|
const handleSectorComplete = React.useCallback((sectorId: string) => {
|
|
399
399
|
if (onSectorComplete) {
|
|
400
400
|
onSectorComplete(sectorId, controller.snapshot());
|
|
401
401
|
}
|
|
402
402
|
}, [onSectorComplete, controller]);
|
|
403
|
-
|
|
403
|
+
|
|
404
404
|
// Event callbacks for plugin integration
|
|
405
405
|
const eventCallbacks = React.useMemo(() => ({
|
|
406
406
|
onSectorComplete: handleSectorComplete,
|
|
407
407
|
onPiecePlace: onPiecePlace || (() => {}),
|
|
408
408
|
onPieceRemove: onPieceRemove || (() => {})
|
|
409
409
|
}), [handleSectorComplete, onPiecePlace, onPieceRemove]);
|
|
410
|
-
|
|
410
|
+
|
|
411
411
|
// Calculate sizing based on layout mode
|
|
412
412
|
const getGameboardStyle = () => {
|
|
413
413
|
const baseStyle = {
|
|
@@ -560,14 +560,14 @@ export default function GameBoard(props: GameBoardProps) {
|
|
|
560
560
|
tracker // Pass tracker for data collection
|
|
561
561
|
);
|
|
562
562
|
|
|
563
|
-
const {
|
|
564
|
-
draggingId,
|
|
565
|
-
dragInvalid,
|
|
566
|
-
svgRef,
|
|
567
|
-
onPiecePointerDown,
|
|
568
|
-
onBlueprintPointerDown,
|
|
569
|
-
onPointerMove,
|
|
570
|
-
onPointerUp,
|
|
563
|
+
const {
|
|
564
|
+
draggingId,
|
|
565
|
+
dragInvalid,
|
|
566
|
+
svgRef,
|
|
567
|
+
onPiecePointerDown,
|
|
568
|
+
onBlueprintPointerDown,
|
|
569
|
+
onPointerMove,
|
|
570
|
+
onPointerUp,
|
|
571
571
|
setPieceRef,
|
|
572
572
|
setDraggingId,
|
|
573
573
|
lockedPieceId,
|
|
@@ -640,7 +640,7 @@ export default function GameBoard(props: GameBoardProps) {
|
|
|
640
640
|
// Timer effect
|
|
641
641
|
React.useEffect(() => {
|
|
642
642
|
if (timeLimitMs === 0) return;
|
|
643
|
-
|
|
643
|
+
|
|
644
644
|
const interval = setInterval(() => {
|
|
645
645
|
setTimeRemaining(prev => {
|
|
646
646
|
if (prev <= 1) {
|
|
@@ -650,7 +650,7 @@ export default function GameBoard(props: GameBoardProps) {
|
|
|
650
650
|
return prev - 1;
|
|
651
651
|
});
|
|
652
652
|
}, 1000);
|
|
653
|
-
|
|
653
|
+
|
|
654
654
|
return () => clearInterval(interval);
|
|
655
655
|
}, [timeLimitMs]);
|
|
656
656
|
|
|
@@ -669,27 +669,38 @@ export default function GameBoard(props: GameBoardProps) {
|
|
|
669
669
|
// minHeight: '100vh'
|
|
670
670
|
};
|
|
671
671
|
|
|
672
|
-
// Header style
|
|
672
|
+
// Header style (constrained to gameboard width)
|
|
673
673
|
const headerStyle: React.CSSProperties = {
|
|
674
674
|
display: 'flex',
|
|
675
675
|
flexDirection: 'row',
|
|
676
|
-
justifyContent: '
|
|
676
|
+
justifyContent: 'center',
|
|
677
677
|
alignItems: 'center',
|
|
678
678
|
padding: '20px',
|
|
679
679
|
background: '#f5f5f5',
|
|
680
680
|
flex: '0 0 auto'
|
|
681
681
|
};
|
|
682
682
|
|
|
683
|
-
//
|
|
683
|
+
// Header content wrapper (matches gameboard width)
|
|
684
|
+
const headerContentStyle: React.CSSProperties = {
|
|
685
|
+
position: 'relative',
|
|
686
|
+
width: `${svgDimensions.width}px`,
|
|
687
|
+
maxWidth: '100%',
|
|
688
|
+
display: 'flex',
|
|
689
|
+
justifyContent: 'center',
|
|
690
|
+
alignItems: 'center'
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
// Instructions style (always centered)
|
|
684
694
|
const instructionsStyle: React.CSSProperties = {
|
|
685
|
-
flexGrow: 1,
|
|
686
695
|
fontSize: '20px',
|
|
687
696
|
lineHeight: 1.5,
|
|
688
|
-
|
|
697
|
+
textAlign: 'center'
|
|
689
698
|
};
|
|
690
699
|
|
|
691
|
-
// Timer style
|
|
700
|
+
// Timer style (positioned absolutely to not affect centering)
|
|
692
701
|
const timerStyle: React.CSSProperties = {
|
|
702
|
+
position: 'absolute',
|
|
703
|
+
right: 0,
|
|
693
704
|
fontSize: '24px',
|
|
694
705
|
fontWeight: 'bold',
|
|
695
706
|
fontFamily: 'monospace',
|
|
@@ -711,18 +722,20 @@ export default function GameBoard(props: GameBoardProps) {
|
|
|
711
722
|
<div className="tangram-trial-container" style={containerStyle}>
|
|
712
723
|
{(instructions || timeLimitMs > 0) && (
|
|
713
724
|
<div className="tangram-header" style={headerStyle}>
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
{
|
|
724
|
-
|
|
725
|
-
|
|
725
|
+
<div className="tangram-header-content" style={headerContentStyle}>
|
|
726
|
+
{instructions && (
|
|
727
|
+
<div
|
|
728
|
+
className="tangram-instructions"
|
|
729
|
+
style={instructionsStyle}
|
|
730
|
+
dangerouslySetInnerHTML={{ __html: instructions }}
|
|
731
|
+
/>
|
|
732
|
+
)}
|
|
733
|
+
{timeLimitMs > 0 && (
|
|
734
|
+
<div className="tangram-timer" style={timerStyle}>
|
|
735
|
+
{formatTime(timeRemaining)}
|
|
736
|
+
</div>
|
|
737
|
+
)}
|
|
738
|
+
</div>
|
|
726
739
|
</div>
|
|
727
740
|
)}
|
|
728
741
|
<div className="tangram-gameboard-wrapper" style={gameboardWrapperStyle}>
|
|
@@ -779,7 +792,7 @@ export function useGameBoard(config: GameBoardConfig) {
|
|
|
779
792
|
...(config.minPiecesPerMacro !== undefined && { minPiecesPerMacro: config.minPiecesPerMacro }),
|
|
780
793
|
...(config.requireAllSlots !== undefined && { requireAllSlots: config.requireAllSlots })
|
|
781
794
|
};
|
|
782
|
-
|
|
795
|
+
|
|
783
796
|
return new BaseGameController(
|
|
784
797
|
config.sectors,
|
|
785
798
|
config.quickstash,
|
|
@@ -787,12 +800,12 @@ export function useGameBoard(config: GameBoardConfig) {
|
|
|
787
800
|
gameConfig
|
|
788
801
|
);
|
|
789
802
|
}, [config]);
|
|
790
|
-
|
|
791
|
-
const snapshot = React.useMemo(() =>
|
|
792
|
-
controller.snapshot(),
|
|
803
|
+
|
|
804
|
+
const snapshot = React.useMemo(() =>
|
|
805
|
+
controller.snapshot(),
|
|
793
806
|
[controller.updateCount]
|
|
794
807
|
);
|
|
795
|
-
|
|
808
|
+
|
|
796
809
|
return {
|
|
797
810
|
controller,
|
|
798
811
|
snapshot,
|