jspsych-tangram 0.0.12 → 0.0.14
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 +283 -64
- package/dist/construct/index.browser.js.map +1 -1
- package/dist/construct/index.browser.min.js +11 -11
- package/dist/construct/index.browser.min.js.map +1 -1
- package/dist/construct/index.cjs +283 -64
- package/dist/construct/index.cjs.map +1 -1
- package/dist/construct/index.d.ts +36 -0
- package/dist/construct/index.js +283 -64
- package/dist/construct/index.js.map +1 -1
- package/dist/index.cjs +394 -94
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +84 -0
- package/dist/index.js +394 -94
- package/dist/index.js.map +1 -1
- package/dist/nback/index.browser.js +112 -37
- package/dist/nback/index.browser.js.map +1 -1
- package/dist/nback/index.browser.min.js +11 -11
- package/dist/nback/index.browser.min.js.map +1 -1
- package/dist/nback/index.cjs +112 -37
- package/dist/nback/index.cjs.map +1 -1
- package/dist/nback/index.d.ts +24 -0
- package/dist/nback/index.js +112 -37
- package/dist/nback/index.js.map +1 -1
- package/dist/prep/index.browser.js +278 -65
- package/dist/prep/index.browser.js.map +1 -1
- package/dist/prep/index.browser.min.js +11 -11
- package/dist/prep/index.browser.min.js.map +1 -1
- package/dist/prep/index.cjs +278 -65
- package/dist/prep/index.cjs.map +1 -1
- package/dist/prep/index.d.ts +24 -0
- package/dist/prep/index.js +278 -65
- package/dist/prep/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/components/board/BoardView.tsx +372 -124
- package/src/core/components/board/GameBoard.tsx +39 -44
- package/src/core/components/board/useGameBoard.ts +52 -0
- package/src/core/components/index.ts +4 -2
- package/src/core/components/pieces/BlueprintRing.tsx +100 -67
- package/src/core/components/pieces/useBlueprintRing.ts +39 -0
- package/src/core/config/config.ts +25 -10
- package/src/plugins/tangram-construct/ConstructionApp.tsx +7 -1
- package/src/plugins/tangram-construct/index.ts +22 -1
- package/src/plugins/tangram-nback/NBackApp.tsx +88 -29
- package/src/plugins/tangram-nback/index.ts +14 -0
- package/src/plugins/tangram-prep/PrepApp.tsx +7 -1
- package/src/plugins/tangram-prep/index.ts +14 -0
- package/tangram-construct.min.js +11 -11
- package/tangram-nback.min.js +11 -11
- package/tangram-prep.min.js +11 -11
|
@@ -218,6 +218,24 @@ export interface GameBoardProps extends GameBoardConfig {
|
|
|
218
218
|
* Useful for spawning initial pieces or setting up state
|
|
219
219
|
*/
|
|
220
220
|
onControllerReady?: (controller: BaseGameController, layout?: any, force?: () => void) => void;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Whether to use distinct colors for each primitive shape type in blueprints
|
|
224
|
+
* When true, each primitive type (square, triangle, etc.) gets its own color in dock area
|
|
225
|
+
*/
|
|
226
|
+
usePrimitiveColorsBlueprints?: boolean;
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Whether to use distinct colors for each primitive shape type in target tangrams
|
|
230
|
+
* When true, each primitive type gets its own color in target silhouettes
|
|
231
|
+
*/
|
|
232
|
+
usePrimitiveColorsTargets?: boolean;
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Array of 5 integers mapping primitives to colors from CONFIG.color.primitiveColors
|
|
236
|
+
* Maps [square, smalltriangle, parallelogram, medtriangle, largetriangle] to color indices
|
|
237
|
+
*/
|
|
238
|
+
primitiveColorIndices?: number[];
|
|
221
239
|
}
|
|
222
240
|
|
|
223
241
|
/**
|
|
@@ -266,7 +284,10 @@ export default function GameBoard(props: GameBoardProps) {
|
|
|
266
284
|
onPieceRemove,
|
|
267
285
|
onInteraction,
|
|
268
286
|
onTrialEnd,
|
|
269
|
-
onControllerReady
|
|
287
|
+
onControllerReady,
|
|
288
|
+
usePrimitiveColorsBlueprints,
|
|
289
|
+
usePrimitiveColorsTargets,
|
|
290
|
+
primitiveColorIndices,
|
|
270
291
|
} = props;
|
|
271
292
|
|
|
272
293
|
// Timer state for countdown
|
|
@@ -676,7 +697,7 @@ export default function GameBoard(props: GameBoardProps) {
|
|
|
676
697
|
justifyContent: 'center',
|
|
677
698
|
alignItems: 'center',
|
|
678
699
|
padding: '20px',
|
|
679
|
-
background:
|
|
700
|
+
background: CONFIG.color.background,
|
|
680
701
|
flex: '0 0 auto'
|
|
681
702
|
};
|
|
682
703
|
|
|
@@ -697,15 +718,25 @@ export default function GameBoard(props: GameBoardProps) {
|
|
|
697
718
|
textAlign: 'center'
|
|
698
719
|
};
|
|
699
720
|
|
|
700
|
-
// Timer style
|
|
721
|
+
// Timer style - positioned absolutely to align right edge with semicircle's right edge
|
|
722
|
+
// Independent of instructions content
|
|
723
|
+
const scaleX = svgDimensions.width / viewBox.w;
|
|
724
|
+
const rightEdgeOfSemicircleLogical = viewBox.w / 2 + layout.outerR;
|
|
725
|
+
const distanceFromRightEdgeLogical = viewBox.w - rightEdgeOfSemicircleLogical;
|
|
726
|
+
const distanceFromRightEdgePx = distanceFromRightEdgeLogical * scaleX;
|
|
727
|
+
|
|
728
|
+
// Shift left by 5 monospaced characters (monospace is typically 0.6em wide per char)
|
|
729
|
+
const charWidth = 24 * 0.6; // fontSize * typical monospace char width ratio
|
|
730
|
+
const offsetLeft = charWidth * 5;
|
|
731
|
+
|
|
701
732
|
const timerStyle: React.CSSProperties = {
|
|
702
733
|
position: 'absolute',
|
|
703
|
-
right:
|
|
734
|
+
right: `${distanceFromRightEdgePx + offsetLeft}px`,
|
|
704
735
|
fontSize: '24px',
|
|
705
736
|
fontWeight: 'bold',
|
|
706
737
|
fontFamily: 'monospace',
|
|
707
738
|
color: '#333',
|
|
708
|
-
|
|
739
|
+
whiteSpace: 'nowrap',
|
|
709
740
|
textAlign: 'right'
|
|
710
741
|
};
|
|
711
742
|
|
|
@@ -766,6 +797,9 @@ export default function GameBoard(props: GameBoardProps) {
|
|
|
766
797
|
onCenterBadgePointerDown={onCenterBadgePointerDown}
|
|
767
798
|
showTangramDecomposition={showTangramDecomposition ?? false}
|
|
768
799
|
scaleS={scaleS}
|
|
800
|
+
usePrimitiveColorsBlueprints={usePrimitiveColorsBlueprints ?? false}
|
|
801
|
+
usePrimitiveColorsTargets={usePrimitiveColorsTargets ?? false}
|
|
802
|
+
primitiveColorIndices={primitiveColorIndices ?? [0, 1, 2, 3, 4]}
|
|
769
803
|
{...eventCallbacks}
|
|
770
804
|
/>
|
|
771
805
|
</div>
|
|
@@ -773,42 +807,3 @@ export default function GameBoard(props: GameBoardProps) {
|
|
|
773
807
|
</div>
|
|
774
808
|
);
|
|
775
809
|
}
|
|
776
|
-
|
|
777
|
-
/**
|
|
778
|
-
* Hook for managing GameBoard state externally
|
|
779
|
-
* Useful for plugins that need direct access to game state
|
|
780
|
-
*/
|
|
781
|
-
export function useGameBoard(config: GameBoardConfig) {
|
|
782
|
-
const controller = React.useMemo(() => {
|
|
783
|
-
const gameConfig = {
|
|
784
|
-
n: config.sectors.length,
|
|
785
|
-
layout: config.layout,
|
|
786
|
-
target: config.target,
|
|
787
|
-
input: config.input,
|
|
788
|
-
timeLimitMs: config.timeLimitMs,
|
|
789
|
-
maxQuickstashSlots: config.maxQuickstashSlots,
|
|
790
|
-
...(config.maxCompositeSize !== undefined && { maxCompositeSize: config.maxCompositeSize }),
|
|
791
|
-
mode: config.mode || "construction",
|
|
792
|
-
...(config.minPiecesPerMacro !== undefined && { minPiecesPerMacro: config.minPiecesPerMacro }),
|
|
793
|
-
...(config.requireAllSlots !== undefined && { requireAllSlots: config.requireAllSlots })
|
|
794
|
-
};
|
|
795
|
-
|
|
796
|
-
return new BaseGameController(
|
|
797
|
-
config.sectors,
|
|
798
|
-
config.quickstash,
|
|
799
|
-
config.primitives,
|
|
800
|
-
gameConfig
|
|
801
|
-
);
|
|
802
|
-
}, [config]);
|
|
803
|
-
|
|
804
|
-
const snapshot = React.useMemo(() =>
|
|
805
|
-
controller.snapshot(),
|
|
806
|
-
[controller.updateCount]
|
|
807
|
-
);
|
|
808
|
-
|
|
809
|
-
return {
|
|
810
|
-
controller,
|
|
811
|
-
snapshot,
|
|
812
|
-
isComplete: !!controller.state.endedAt
|
|
813
|
-
};
|
|
814
|
-
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook for managing GameBoard state externally
|
|
3
|
+
*
|
|
4
|
+
* This hook provides external access to the GameBoard's internal state
|
|
5
|
+
* management, useful for plugins that need to track or control game state
|
|
6
|
+
* directly without rendering the full GameBoard component.
|
|
7
|
+
*
|
|
8
|
+
* REQUIRES: config contains valid GameBoardConfig properties
|
|
9
|
+
* EFFECTS: Creates and manages a BaseGameController instance
|
|
10
|
+
*
|
|
11
|
+
* @param config - GameBoard configuration object
|
|
12
|
+
* @returns Object containing controller, snapshot, and completion status
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import React from "react";
|
|
16
|
+
import { BaseGameController } from "@/core/engine/state/BaseGameController";
|
|
17
|
+
import type { GameBoardConfig } from "./GameBoard";
|
|
18
|
+
|
|
19
|
+
export function useGameBoard(config: GameBoardConfig) {
|
|
20
|
+
const controller = React.useMemo(() => {
|
|
21
|
+
const gameConfig = {
|
|
22
|
+
n: config.sectors.length,
|
|
23
|
+
layout: config.layout,
|
|
24
|
+
target: config.target,
|
|
25
|
+
input: config.input,
|
|
26
|
+
timeLimitMs: config.timeLimitMs,
|
|
27
|
+
maxQuickstashSlots: config.maxQuickstashSlots,
|
|
28
|
+
...(config.maxCompositeSize !== undefined && { maxCompositeSize: config.maxCompositeSize }),
|
|
29
|
+
mode: config.mode || "construction",
|
|
30
|
+
...(config.minPiecesPerMacro !== undefined && { minPiecesPerMacro: config.minPiecesPerMacro }),
|
|
31
|
+
...(config.requireAllSlots !== undefined && { requireAllSlots: config.requireAllSlots })
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return new BaseGameController(
|
|
35
|
+
config.sectors,
|
|
36
|
+
config.quickstash,
|
|
37
|
+
config.primitives,
|
|
38
|
+
gameConfig
|
|
39
|
+
);
|
|
40
|
+
}, [config]);
|
|
41
|
+
|
|
42
|
+
const snapshot = React.useMemo(() =>
|
|
43
|
+
controller.snapshot(),
|
|
44
|
+
[controller.updateCount]
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
controller,
|
|
49
|
+
snapshot,
|
|
50
|
+
isComplete: !!controller.state.endedAt
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
// Core game board component
|
|
9
|
-
export { default as GameBoard
|
|
9
|
+
export { default as GameBoard } from './board/GameBoard';
|
|
10
|
+
export { useGameBoard } from './board/useGameBoard';
|
|
10
11
|
export type { GameBoardConfig, GameBoardProps } from './board/GameBoard';
|
|
11
12
|
|
|
12
13
|
// Blueprint ring component for plugin UI
|
|
13
|
-
export { default as BlueprintRing
|
|
14
|
+
export { default as BlueprintRing } from './pieces/BlueprintRing';
|
|
15
|
+
export { useBlueprintRing } from './pieces/useBlueprintRing';
|
|
14
16
|
export type { BlueprintRingProps } from './pieces/BlueprintRing';
|
|
15
17
|
|
|
16
18
|
// Legacy Board components (deprecated - use GameBoard instead)
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* BlueprintRing - Reusable blueprint selection interface component
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* This component provides the central blueprint selection interface that displays
|
|
5
5
|
* available piece blueprints in a circular or semicircular arrangement around
|
|
6
6
|
* a center badge. It handles both primitive pieces and quickstash macros.
|
|
7
|
-
*
|
|
7
|
+
*
|
|
8
8
|
* ## Key Features
|
|
9
9
|
* - **Dual Mode Display**: Switches between primitives and quickstash collections
|
|
10
|
-
* - **Flexible Layout**: Adapts to circle and semicircle layout modes
|
|
10
|
+
* - **Flexible Layout**: Adapts to circle and semicircle layout modes
|
|
11
11
|
* - **Interactive Badge**: Center button to toggle between piece collections
|
|
12
12
|
* - **Responsive Geometry**: Automatically calculates ring radius based on content
|
|
13
13
|
* - **Event Integration**: Provides structured callbacks for piece selection
|
|
14
|
-
*
|
|
14
|
+
*
|
|
15
15
|
* ## Architecture
|
|
16
16
|
* - Pure presentation component (no internal state management)
|
|
17
17
|
* - Receives all data and callbacks via props (dependency injection)
|
|
18
18
|
* - Computes ring geometry based on layout constraints
|
|
19
19
|
* - Renders SVG blueprint glyphs with proper scaling and positioning
|
|
20
|
-
*
|
|
20
|
+
*
|
|
21
21
|
* ## Usage
|
|
22
22
|
* Typically used within GameBoard or plugin components:
|
|
23
23
|
* ```typescript
|
|
@@ -30,10 +30,10 @@
|
|
|
30
30
|
* onCenterBadgePointerDown={handleViewToggle}
|
|
31
31
|
* />
|
|
32
32
|
* ```
|
|
33
|
-
*
|
|
33
|
+
*
|
|
34
34
|
* @see {@link GameBoard} Primary container that uses this component
|
|
35
35
|
* @see {@link useBlueprintRing} Hook for managing blueprint ring state
|
|
36
|
-
* @since Phase 3.3 - Extracted from monolithic Board component
|
|
36
|
+
* @since Phase 3.3 - Extracted from monolithic Board component
|
|
37
37
|
* @author Claude Code Assistant
|
|
38
38
|
*/
|
|
39
39
|
|
|
@@ -47,45 +47,94 @@ function pathD(poly: any[]) {
|
|
|
47
47
|
return `M ${poly.map((pt: any) => `${pt.x} ${pt.y}`).join(" L ")} Z`;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Get the fill color for a blueprint based on its type
|
|
52
|
+
*
|
|
53
|
+
* REQUIRES: blueprint is valid, primitiveColorIndices is an array of 5 valid indices
|
|
54
|
+
* EFFECTS: Returns color based on primitive type if usePrimitiveColors is true,
|
|
55
|
+
* otherwise returns the default color. Composites always use default color.
|
|
56
|
+
*/
|
|
57
|
+
function getBlueprintColor(
|
|
58
|
+
blueprint: Blueprint,
|
|
59
|
+
usePrimitiveColors: boolean,
|
|
60
|
+
defaultColor: string,
|
|
61
|
+
primitiveColorIndices: number[]
|
|
62
|
+
): string {
|
|
63
|
+
if (!usePrimitiveColors) {
|
|
64
|
+
return defaultColor;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// For primitive blueprints, use the mapped color from CONFIG
|
|
68
|
+
if ('kind' in blueprint) {
|
|
69
|
+
const kind = blueprint.kind as TanKind;
|
|
70
|
+
// Map primitive kind to index: square=0, smalltriangle=1, parallelogram=2, medtriangle=3, largetriangle=4
|
|
71
|
+
const kindToIndex: Record<TanKind, number> = {
|
|
72
|
+
'square': 0,
|
|
73
|
+
'smalltriangle': 1,
|
|
74
|
+
'parallelogram': 2,
|
|
75
|
+
'medtriangle': 3,
|
|
76
|
+
'largetriangle': 4
|
|
77
|
+
};
|
|
78
|
+
const primitiveIndex = kindToIndex[kind];
|
|
79
|
+
if (primitiveIndex !== undefined && primitiveColorIndices[primitiveIndex] !== undefined) {
|
|
80
|
+
const colorIndex = primitiveColorIndices[primitiveIndex];
|
|
81
|
+
const color = CONFIG.color.primitiveColors[colorIndex];
|
|
82
|
+
if (color) {
|
|
83
|
+
return color;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return defaultColor;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// For composite blueprints, always use the default color
|
|
90
|
+
return defaultColor;
|
|
91
|
+
}
|
|
92
|
+
|
|
50
93
|
/**
|
|
51
94
|
* Props interface for BlueprintRing component
|
|
52
|
-
*
|
|
95
|
+
*
|
|
53
96
|
* This interface defines all the data and callbacks needed to render
|
|
54
97
|
* the blueprint selection interface. All props use dependency injection.
|
|
55
98
|
*/
|
|
56
99
|
export interface BlueprintRingProps {
|
|
57
100
|
/** Array of primitive tangram piece blueprints */
|
|
58
101
|
primitives: PrimitiveBlueprint[];
|
|
59
|
-
|
|
102
|
+
|
|
60
103
|
/** Array of quickstash/macro piece blueprints */
|
|
61
104
|
quickstash: Blueprint[];
|
|
62
|
-
|
|
63
|
-
/**
|
|
105
|
+
|
|
106
|
+
/**
|
|
64
107
|
* Current view mode for blueprint ring
|
|
65
108
|
* - "primitives": Show primitive tangram pieces
|
|
66
109
|
* - "quickstash": Show macro/composite pieces
|
|
67
110
|
*/
|
|
68
111
|
currentView: "primitives" | "quickstash";
|
|
69
|
-
|
|
112
|
+
|
|
70
113
|
/** Computed circle layout containing geometry information */
|
|
71
114
|
layout: CircleLayout;
|
|
72
|
-
|
|
115
|
+
|
|
73
116
|
/** Radius of the center toggle badge in pixels */
|
|
74
117
|
badgeR: number;
|
|
75
|
-
|
|
118
|
+
|
|
76
119
|
/** Center position of the toggle badge */
|
|
77
120
|
badgeCenter: { x: number; y: number };
|
|
78
|
-
|
|
121
|
+
|
|
79
122
|
/** Maximum number of quickstash slots to display */
|
|
80
123
|
maxQuickstashSlots: number;
|
|
81
|
-
|
|
124
|
+
|
|
82
125
|
/** ID of currently dragging piece (null if none) - used for interaction state */
|
|
83
126
|
draggingId: string | null;
|
|
84
|
-
|
|
85
|
-
/**
|
|
127
|
+
|
|
128
|
+
/** Whether to use distinct colors for each primitive shape type in blueprints */
|
|
129
|
+
usePrimitiveColorsBlueprints?: boolean;
|
|
130
|
+
|
|
131
|
+
/** Array of 5 integers mapping primitives to colors from CONFIG.color.primitiveColors */
|
|
132
|
+
primitiveColorIndices?: number[];
|
|
133
|
+
|
|
134
|
+
/**
|
|
86
135
|
* Callback fired when user starts interacting with a blueprint
|
|
87
136
|
* @param e - Pointer event from the interaction
|
|
88
|
-
* @param bp - Blueprint that was selected
|
|
137
|
+
* @param bp - Blueprint that was selected
|
|
89
138
|
* @param bpGeom - Geometry information for the selected blueprint
|
|
90
139
|
*/
|
|
91
140
|
onBlueprintPointerDown: (
|
|
@@ -93,14 +142,14 @@ export interface BlueprintRingProps {
|
|
|
93
142
|
bp: Blueprint,
|
|
94
143
|
bpGeom: { bx: number; by: number; cx: number; cy: number }
|
|
95
144
|
) => void;
|
|
96
|
-
|
|
97
|
-
/**
|
|
145
|
+
|
|
146
|
+
/**
|
|
98
147
|
* Callback fired when user clicks the center badge to toggle views
|
|
99
148
|
* @param e - Pointer event from the badge click
|
|
100
149
|
*/
|
|
101
150
|
onCenterBadgePointerDown: (e: React.PointerEvent) => void;
|
|
102
|
-
|
|
103
|
-
/**
|
|
151
|
+
|
|
152
|
+
/**
|
|
104
153
|
* Helper function to lookup primitive blueprints by kind
|
|
105
154
|
* Used for rendering composite pieces that reference primitives
|
|
106
155
|
*/
|
|
@@ -119,22 +168,24 @@ export default function BlueprintRing(props: BlueprintRingProps) {
|
|
|
119
168
|
draggingId,
|
|
120
169
|
onBlueprintPointerDown,
|
|
121
170
|
onCenterBadgePointerDown,
|
|
122
|
-
getPrimitive
|
|
171
|
+
getPrimitive,
|
|
172
|
+
usePrimitiveColorsBlueprints,
|
|
173
|
+
primitiveColorIndices
|
|
123
174
|
} = props;
|
|
124
|
-
|
|
175
|
+
|
|
125
176
|
// Determine which blueprints to show
|
|
126
177
|
const blueprints: Blueprint[] = currentView === "primitives" ? primitives : quickstash;
|
|
127
|
-
|
|
178
|
+
|
|
128
179
|
// Calculate ring geometry
|
|
129
180
|
const QS_SLOTS = maxQuickstashSlots;
|
|
130
181
|
const PRIM_SLOTS = primitives.length;
|
|
131
182
|
const slotsForView = currentView === "primitives" ? Math.max(1, PRIM_SLOTS) : Math.max(1, QS_SLOTS);
|
|
132
183
|
const sweep = layout.mode === "circle" ? Math.PI * 2 : Math.PI;
|
|
133
184
|
const delta = sweep / slotsForView;
|
|
134
|
-
|
|
185
|
+
|
|
135
186
|
const start = layout.mode === "circle" ? -Math.PI / 2 : Math.PI;
|
|
136
187
|
const blueprintTheta = (i: number) => start + (i + 0.5) * delta;
|
|
137
|
-
|
|
188
|
+
|
|
138
189
|
// Chord requirement calculation
|
|
139
190
|
const anchorsDiameterToPx = (anchorsDiag: number, gridPx: number = CONFIG.layout.grid.stepPx) =>
|
|
140
191
|
anchorsDiag * Math.SQRT2 * gridPx;
|
|
@@ -143,22 +194,29 @@ export default function BlueprintRing(props: BlueprintRingProps) {
|
|
|
143
194
|
: CONFIG.layout.constraints.quickstashDiamAnchors;
|
|
144
195
|
const D_slot = anchorsDiameterToPx(reqAnchors);
|
|
145
196
|
const R_needed = D_slot / (2 * Math.max(1e-9, Math.sin(delta / 2)));
|
|
146
|
-
|
|
197
|
+
|
|
147
198
|
// Minimum radius to avoid overlapping with badge
|
|
148
199
|
// Badge takes up: badgeR + margin, plus we need half the slot diameter for clearance
|
|
149
200
|
const R_min = badgeR + CONFIG.size.centerBadge.marginPx + D_slot / 2;
|
|
150
201
|
const ringMax = layout.innerR - (badgeR + CONFIG.size.centerBadge.marginPx);
|
|
151
|
-
|
|
202
|
+
|
|
152
203
|
// Clamp to [R_min, ringMax]
|
|
153
204
|
const blueprintRingR = Math.min(Math.max(R_needed, R_min), ringMax);
|
|
154
|
-
|
|
205
|
+
|
|
155
206
|
// Blueprint glyph renderer
|
|
156
207
|
const renderBlueprintGlyph = (bp: Blueprint, bx: number, by: number) => {
|
|
157
208
|
const bb = boundsOfBlueprint(bp, (k: string) => getPrimitive(k as TanKind)!);
|
|
158
209
|
const cx = bb.min.x + bb.width / 2;
|
|
159
210
|
const cy = bb.min.y + bb.height / 2;
|
|
160
211
|
const selected = false; // Future enhancement: add selection highlighting
|
|
161
|
-
|
|
212
|
+
|
|
213
|
+
const fillColor = getBlueprintColor(
|
|
214
|
+
bp,
|
|
215
|
+
usePrimitiveColorsBlueprints || false,
|
|
216
|
+
CONFIG.color.blueprint.fill,
|
|
217
|
+
primitiveColorIndices || [0, 1, 2, 3, 4]
|
|
218
|
+
);
|
|
219
|
+
|
|
162
220
|
return (
|
|
163
221
|
<g
|
|
164
222
|
key={bp.id}
|
|
@@ -168,7 +226,7 @@ export default function BlueprintRing(props: BlueprintRingProps) {
|
|
|
168
226
|
<path
|
|
169
227
|
key={idx}
|
|
170
228
|
d={pathD(poly)}
|
|
171
|
-
fill={
|
|
229
|
+
fill={fillColor}
|
|
172
230
|
opacity={CONFIG.opacity.blueprint}
|
|
173
231
|
stroke={selected ? CONFIG.color.blueprint.selectedStroke : "none"}
|
|
174
232
|
strokeWidth={selected ? 2 : 0}
|
|
@@ -180,21 +238,21 @@ export default function BlueprintRing(props: BlueprintRingProps) {
|
|
|
180
238
|
</g>
|
|
181
239
|
);
|
|
182
240
|
};
|
|
183
|
-
|
|
241
|
+
|
|
184
242
|
return (
|
|
185
243
|
<g className="blueprint-ring">
|
|
186
244
|
{/* Center badge */}
|
|
187
|
-
<g
|
|
188
|
-
transform={`translate(${badgeCenter.x}, ${badgeCenter.y})`}
|
|
189
|
-
style={{ cursor: draggingId ? "default" : "pointer" }}
|
|
245
|
+
<g
|
|
246
|
+
transform={`translate(${badgeCenter.x}, ${badgeCenter.y})`}
|
|
247
|
+
style={{ cursor: draggingId ? "default" : "pointer" }}
|
|
190
248
|
onPointerDown={onCenterBadgePointerDown}
|
|
191
249
|
>
|
|
192
250
|
<circle r={badgeR} fill={CONFIG.color.blueprint.badgeFill} />
|
|
193
|
-
<text
|
|
194
|
-
textAnchor="middle"
|
|
195
|
-
dominantBaseline="middle"
|
|
196
|
-
fontSize={CONFIG.size.badgeFontPx}
|
|
197
|
-
fill={CONFIG.color.blueprint.labelFill}
|
|
251
|
+
<text
|
|
252
|
+
textAnchor="middle"
|
|
253
|
+
dominantBaseline="middle"
|
|
254
|
+
fontSize={CONFIG.size.badgeFontPx}
|
|
255
|
+
fill={CONFIG.color.blueprint.labelFill}
|
|
198
256
|
pointerEvents="none"
|
|
199
257
|
>
|
|
200
258
|
{currentView}
|
|
@@ -210,29 +268,4 @@ export default function BlueprintRing(props: BlueprintRingProps) {
|
|
|
210
268
|
})}
|
|
211
269
|
</g>
|
|
212
270
|
);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Hook for managing blueprint ring state
|
|
217
|
-
* Useful for components that need more control over blueprint ring behavior
|
|
218
|
-
*/
|
|
219
|
-
export function useBlueprintRing(
|
|
220
|
-
primitives: PrimitiveBlueprint[],
|
|
221
|
-
quickstash: Blueprint[],
|
|
222
|
-
initialView: "primitives" | "quickstash" = "quickstash"
|
|
223
|
-
) {
|
|
224
|
-
const [currentView, setCurrentView] = React.useState<"primitives" | "quickstash">(initialView);
|
|
225
|
-
|
|
226
|
-
const switchView = React.useCallback(() => {
|
|
227
|
-
setCurrentView(prev => prev === "primitives" ? "quickstash" : "primitives");
|
|
228
|
-
}, []);
|
|
229
|
-
|
|
230
|
-
const blueprints = currentView === "primitives" ? primitives : quickstash;
|
|
231
|
-
|
|
232
|
-
return {
|
|
233
|
-
currentView,
|
|
234
|
-
blueprints,
|
|
235
|
-
switchView,
|
|
236
|
-
setCurrentView
|
|
237
|
-
};
|
|
238
271
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook for managing BlueprintRing state externally
|
|
3
|
+
*
|
|
4
|
+
* This hook manages the state for switching between primitives and quickstash
|
|
5
|
+
* views in the blueprint ring. Useful for components that need more control
|
|
6
|
+
* over blueprint ring behavior.
|
|
7
|
+
*
|
|
8
|
+
* REQUIRES: primitives and quickstash are valid arrays of blueprints
|
|
9
|
+
* EFFECTS: Manages view state and provides toggle functionality
|
|
10
|
+
*
|
|
11
|
+
* @param primitives - Array of primitive piece blueprints
|
|
12
|
+
* @param quickstash - Array of quickstash macro blueprints
|
|
13
|
+
* @param initialView - Initial view to display (defaults to "quickstash")
|
|
14
|
+
* @returns Object with current view, blueprints, and control functions
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import React from "react";
|
|
18
|
+
import type { Blueprint, PrimitiveBlueprint } from "@/core/domain/types";
|
|
19
|
+
|
|
20
|
+
export function useBlueprintRing(
|
|
21
|
+
primitives: PrimitiveBlueprint[],
|
|
22
|
+
quickstash: Blueprint[],
|
|
23
|
+
initialView: "primitives" | "quickstash" = "quickstash"
|
|
24
|
+
) {
|
|
25
|
+
const [currentView, setCurrentView] = React.useState<"primitives" | "quickstash">(initialView);
|
|
26
|
+
|
|
27
|
+
const switchView = React.useCallback(() => {
|
|
28
|
+
setCurrentView(prev => prev === "primitives" ? "quickstash" : "primitives");
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
const blueprints = currentView === "primitives" ? primitives : quickstash;
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
currentView,
|
|
35
|
+
blueprints,
|
|
36
|
+
switchView,
|
|
37
|
+
setCurrentView
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/core/config/config.ts
|
|
2
2
|
export type Config = {
|
|
3
3
|
color: {
|
|
4
|
+
background: string;
|
|
4
5
|
bands: {
|
|
5
6
|
silhouette: { fillEven: string; fillOdd: string; stroke: string };
|
|
6
7
|
workspace: { fillEven: string; fillOdd: string; stroke: string };
|
|
@@ -12,6 +13,7 @@ export type Config = {
|
|
|
12
13
|
ui: { light: string; dark: string };
|
|
13
14
|
blueprint: { fill: string; selectedStroke: string; badgeFill: string; labelFill: string };
|
|
14
15
|
tangramDecomposition: { stroke: string };
|
|
16
|
+
primitiveColors: string[];
|
|
15
17
|
};
|
|
16
18
|
opacity: {
|
|
17
19
|
blueprint: number;
|
|
@@ -24,6 +26,7 @@ export type Config = {
|
|
|
24
26
|
anchorRadiusPx: { valid: number; invalid: number };
|
|
25
27
|
badgeFontPx: number;
|
|
26
28
|
centerBadge: { fractionOfOuterR: number; minPx: number; marginPx: number };
|
|
29
|
+
invalidMarker: { sizePx: number; strokePx: number };
|
|
27
30
|
};
|
|
28
31
|
layout: {
|
|
29
32
|
grid: { stepPx: number; unitPx: number };
|
|
@@ -41,35 +44,46 @@ export type Config = {
|
|
|
41
44
|
snapRadiusPx: number;
|
|
42
45
|
showBorders: boolean;
|
|
43
46
|
hideTouchingBorders: boolean;
|
|
47
|
+
silhouettesBelowPieces: boolean;
|
|
44
48
|
};
|
|
45
49
|
};
|
|
46
50
|
|
|
47
51
|
export const CONFIG: Config = {
|
|
48
52
|
color: {
|
|
53
|
+
background: "#fff7e0ff",
|
|
49
54
|
bands: {
|
|
50
|
-
silhouette: { fillEven: "#
|
|
51
|
-
workspace: { fillEven: "#
|
|
55
|
+
silhouette: { fillEven: "#ffffff", fillOdd: "#ffffff", stroke: "#b1b1b1" },
|
|
56
|
+
workspace: { fillEven: "#ffffff", fillOdd: "#ffffff", stroke: "#b1b1b1" }
|
|
52
57
|
},
|
|
53
|
-
completion: { fill: "#
|
|
58
|
+
completion: { fill: "#ccffcc", stroke: "#13da57" },
|
|
54
59
|
silhouetteMask: "#374151",
|
|
55
60
|
anchors: { invalid: "#7dd3fc", valid: "#475569" },
|
|
56
|
-
|
|
61
|
+
// validFill used here for placed composites
|
|
62
|
+
piece: { draggingFill: "#8e7cc3", validFill: "#8e7cc3", invalidFill: "#d55c00", invalidStroke: "#dc2626", selectedStroke: "#674ea7", allGreenStroke: "#86efac", borderStroke: "#674ea7" },
|
|
57
63
|
ui: { light: "#60a5fa", dark: "#1d4ed8" },
|
|
58
64
|
blueprint: { fill: "#374151", selectedStroke: "#111827", badgeFill: "#000000", labelFill: "#ffffff" },
|
|
59
|
-
tangramDecomposition: { stroke: "#fef2cc" }
|
|
65
|
+
tangramDecomposition: { stroke: "#fef2cc" },
|
|
66
|
+
primitiveColors: [ // from seaborn "colorblind" palette, 6 colors, with red omitted
|
|
67
|
+
'#0173b2',
|
|
68
|
+
'#de8f05',
|
|
69
|
+
'#029e73',
|
|
70
|
+
'#cc78bc',
|
|
71
|
+
'#ca9161'
|
|
72
|
+
]
|
|
60
73
|
},
|
|
61
74
|
opacity: {
|
|
62
|
-
blueprint: 0.
|
|
75
|
+
blueprint: 0.6,
|
|
63
76
|
silhouetteMask: 0.25,
|
|
64
77
|
//anchors: { valid: 0.80, invalid: 0.50 },
|
|
65
78
|
anchors: { invalid: 0.0, valid: 0.0 },
|
|
66
|
-
piece: { invalid:
|
|
79
|
+
piece: { invalid: 1, dragging: 1, locked: 1, normal: 1 },
|
|
67
80
|
},
|
|
68
81
|
size: {
|
|
69
|
-
stroke: { bandPx: 5, pieceSelectedPx:
|
|
82
|
+
stroke: { bandPx: 5, pieceSelectedPx: 5, allGreenStrokePx: 10, pieceBorderPx: 2, tangramDecompositionPx: 1 },
|
|
70
83
|
anchorRadiusPx: { valid: 1.0, invalid: 1.0 },
|
|
71
84
|
badgeFontPx: 16,
|
|
72
|
-
centerBadge: { fractionOfOuterR: 0.15, minPx: 20, marginPx: 4 }
|
|
85
|
+
centerBadge: { fractionOfOuterR: 0.15, minPx: 20, marginPx: 4 },
|
|
86
|
+
invalidMarker: { sizePx: 10, strokePx: 4 }
|
|
73
87
|
},
|
|
74
88
|
layout: {
|
|
75
89
|
grid: { stepPx: 20, unitPx: 40 },
|
|
@@ -85,6 +99,7 @@ export const CONFIG: Config = {
|
|
|
85
99
|
game: {
|
|
86
100
|
snapRadiusPx: 15,
|
|
87
101
|
showBorders: false,
|
|
88
|
-
hideTouchingBorders: true
|
|
102
|
+
hideTouchingBorders: true,
|
|
103
|
+
silhouettesBelowPieces: true
|
|
89
104
|
}
|
|
90
105
|
};
|
|
@@ -34,6 +34,9 @@ export interface StartConstructionTrialParams {
|
|
|
34
34
|
instructions?: string;
|
|
35
35
|
onInteraction?: (event: any) => void;
|
|
36
36
|
onTrialEnd?: (data: any) => void;
|
|
37
|
+
usePrimitiveColorsBlueprints?: boolean;
|
|
38
|
+
usePrimitiveColorsTargets?: boolean;
|
|
39
|
+
primitiveColorIndices?: number[];
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
// Type for anchor-based composite definitions
|
|
@@ -143,7 +146,10 @@ export function startConstructionTrial(
|
|
|
143
146
|
trialParams,
|
|
144
147
|
...(params.instructions && { instructions: params.instructions }),
|
|
145
148
|
...(params.onInteraction && { onInteraction: params.onInteraction }),
|
|
146
|
-
...(params.onTrialEnd && { onTrialEnd: params.onTrialEnd })
|
|
149
|
+
...(params.onTrialEnd && { onTrialEnd: params.onTrialEnd }),
|
|
150
|
+
...(params.usePrimitiveColorsBlueprints !== undefined && { usePrimitiveColorsBlueprints: params.usePrimitiveColorsBlueprints }),
|
|
151
|
+
...(params.usePrimitiveColorsTargets !== undefined && { usePrimitiveColorsTargets: params.usePrimitiveColorsTargets }),
|
|
152
|
+
...(params.primitiveColorIndices !== undefined && { primitiveColorIndices: params.primitiveColorIndices })
|
|
147
153
|
};
|
|
148
154
|
|
|
149
155
|
const root = createRoot(display_element);
|
|
@@ -79,6 +79,24 @@ const info = {
|
|
|
79
79
|
type: ParameterType.FUNCTION,
|
|
80
80
|
default: undefined,
|
|
81
81
|
description: "Callback when trial completes with full data"
|
|
82
|
+
},
|
|
83
|
+
/** Whether to use distinct colors for each primitive shape type in blueprints */
|
|
84
|
+
use_primitive_colors_blueprints: {
|
|
85
|
+
type: ParameterType.BOOL,
|
|
86
|
+
default: false,
|
|
87
|
+
description: "Whether each primitive shape type should have its own distinct color in the blueprint dock area"
|
|
88
|
+
},
|
|
89
|
+
/** Whether to use distinct colors for each primitive shape type in target tangrams */
|
|
90
|
+
use_primitive_colors_targets: {
|
|
91
|
+
type: ParameterType.BOOL,
|
|
92
|
+
default: false,
|
|
93
|
+
description: "Whether each primitive shape type should have its own distinct color in target tangram silhouettes"
|
|
94
|
+
},
|
|
95
|
+
/** Indices mapping primitives to colors from the color palette */
|
|
96
|
+
primitive_color_indices: {
|
|
97
|
+
type: ParameterType.OBJECT,
|
|
98
|
+
default: [0, 1, 2, 3, 4],
|
|
99
|
+
description: "Array of 5 integers indexing into primitiveColors array, mapping [square, smalltriangle, parallelogram, medtriangle, largetriangle] to colors"
|
|
82
100
|
}
|
|
83
101
|
},
|
|
84
102
|
data: {
|
|
@@ -163,7 +181,10 @@ class TangramConstructPlugin implements JsPsychPlugin<Info> {
|
|
|
163
181
|
show_tangram_decomposition: trial.show_tangram_decomposition,
|
|
164
182
|
instructions: trial.instructions,
|
|
165
183
|
onInteraction: trial.onInteraction,
|
|
166
|
-
onTrialEnd: wrappedOnTrialEnd
|
|
184
|
+
onTrialEnd: wrappedOnTrialEnd,
|
|
185
|
+
usePrimitiveColorsBlueprints: trial.use_primitive_colors_blueprints,
|
|
186
|
+
usePrimitiveColorsTargets: trial.use_primitive_colors_targets,
|
|
187
|
+
primitiveColorIndices: trial.primitive_color_indices
|
|
167
188
|
};
|
|
168
189
|
|
|
169
190
|
// Use React wrapper to start the trial
|