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.
Files changed (49) hide show
  1. package/dist/construct/index.browser.js +283 -64
  2. package/dist/construct/index.browser.js.map +1 -1
  3. package/dist/construct/index.browser.min.js +11 -11
  4. package/dist/construct/index.browser.min.js.map +1 -1
  5. package/dist/construct/index.cjs +283 -64
  6. package/dist/construct/index.cjs.map +1 -1
  7. package/dist/construct/index.d.ts +36 -0
  8. package/dist/construct/index.js +283 -64
  9. package/dist/construct/index.js.map +1 -1
  10. package/dist/index.cjs +394 -94
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.ts +84 -0
  13. package/dist/index.js +394 -94
  14. package/dist/index.js.map +1 -1
  15. package/dist/nback/index.browser.js +112 -37
  16. package/dist/nback/index.browser.js.map +1 -1
  17. package/dist/nback/index.browser.min.js +11 -11
  18. package/dist/nback/index.browser.min.js.map +1 -1
  19. package/dist/nback/index.cjs +112 -37
  20. package/dist/nback/index.cjs.map +1 -1
  21. package/dist/nback/index.d.ts +24 -0
  22. package/dist/nback/index.js +112 -37
  23. package/dist/nback/index.js.map +1 -1
  24. package/dist/prep/index.browser.js +278 -65
  25. package/dist/prep/index.browser.js.map +1 -1
  26. package/dist/prep/index.browser.min.js +11 -11
  27. package/dist/prep/index.browser.min.js.map +1 -1
  28. package/dist/prep/index.cjs +278 -65
  29. package/dist/prep/index.cjs.map +1 -1
  30. package/dist/prep/index.d.ts +24 -0
  31. package/dist/prep/index.js +278 -65
  32. package/dist/prep/index.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/core/components/board/BoardView.tsx +372 -124
  35. package/src/core/components/board/GameBoard.tsx +39 -44
  36. package/src/core/components/board/useGameBoard.ts +52 -0
  37. package/src/core/components/index.ts +4 -2
  38. package/src/core/components/pieces/BlueprintRing.tsx +100 -67
  39. package/src/core/components/pieces/useBlueprintRing.ts +39 -0
  40. package/src/core/config/config.ts +25 -10
  41. package/src/plugins/tangram-construct/ConstructionApp.tsx +7 -1
  42. package/src/plugins/tangram-construct/index.ts +22 -1
  43. package/src/plugins/tangram-nback/NBackApp.tsx +88 -29
  44. package/src/plugins/tangram-nback/index.ts +14 -0
  45. package/src/plugins/tangram-prep/PrepApp.tsx +7 -1
  46. package/src/plugins/tangram-prep/index.ts +14 -0
  47. package/tangram-construct.min.js +11 -11
  48. package/tangram-nback.min.js +11 -11
  49. 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: '#f5f5f5',
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 (positioned absolutely to not affect centering)
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: 0,
734
+ right: `${distanceFromRightEdgePx + offsetLeft}px`,
704
735
  fontSize: '24px',
705
736
  fontWeight: 'bold',
706
737
  fontFamily: 'monospace',
707
738
  color: '#333',
708
- minWidth: '80px',
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, useGameBoard } from './board/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, useBlueprintRing } from './pieces/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={CONFIG.color.blueprint.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: "#fff2ccff", fillOdd: "#fff2ccff", stroke: "#b1b1b1" },
51
- workspace: { fillEven: "#fff2ccff", fillOdd: "#fff2ccff", stroke: "#b1b1b1" }
55
+ silhouette: { fillEven: "#ffffff", fillOdd: "#ffffff", stroke: "#b1b1b1" },
56
+ workspace: { fillEven: "#ffffff", fillOdd: "#ffffff", stroke: "#b1b1b1" }
52
57
  },
53
- completion: { fill: "#ccfff2", stroke: "#13da57" },
58
+ completion: { fill: "#ccffcc", stroke: "#13da57" },
54
59
  silhouetteMask: "#374151",
55
60
  anchors: { invalid: "#7dd3fc", valid: "#475569" },
56
- piece: { draggingFill: "#8e7cc3ff", validFill: "#8e7cc3ff", invalidFill: "#ef4444", invalidStroke: "#dc2626", selectedStroke: "#674ea7", allGreenStroke: "#86efac", borderStroke: "#674ea7" },
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.4,
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: 0.35, dragging: 0.75, locked: 1, normal: 1 },
79
+ piece: { invalid: 1, dragging: 1, locked: 1, normal: 1 },
67
80
  },
68
81
  size: {
69
- stroke: { bandPx: 5, pieceSelectedPx: 3, allGreenStrokePx: 10, pieceBorderPx: 2, tangramDecompositionPx: 1 },
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