jspsych-tangram 0.0.4 → 0.0.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jspsych-tangram",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Tangram tasks for jsPsych: prep and construct.",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -357,8 +357,11 @@ export default function GameBoard(props: GameBoardProps) {
357
357
  setGameCompleted(true);
358
358
 
359
359
  // Finalize trial data tracking (which calls onTrialEnd)
360
+ // Defer to avoid unmounting React while still in commit phase
360
361
  if (tracker) {
361
- tracker.finalizeTrial('auto_complete');
362
+ setTimeout(() => {
363
+ tracker.finalizeTrial('auto_complete');
364
+ }, 0);
362
365
  }
363
366
  }
364
367
  };
@@ -196,7 +196,6 @@ export class BaseGameController {
196
196
  const allDone = Object.values(this.state.sectors).every((s: SectorState) => !!s.completedAt);
197
197
  if (allDone && !this.state.endedAt) {
198
198
  this.state.endedAt = NOW();
199
- console.log("[BaseGameController] all sectors complete");
200
199
  }
201
200
  }
202
201
 
@@ -25,6 +25,7 @@ import { CONFIG } from "../../core/config/config";
25
25
  export interface StartConstructionTrialParams {
26
26
  tangrams: any[];
27
27
  quickstash_macros?: Blueprint[] | AnchorComposite[];
28
+ primitiveOrder: string[];
28
29
  target?: PlacementTarget;
29
30
  input?: InputMode;
30
31
  layout?: LayoutMode;
@@ -57,43 +58,28 @@ export function startConstructionTrial(
57
58
  "largetriangle",
58
59
  ]);
59
60
 
60
- // Convert JSON plugin parameters to internal Sector[] format
61
- console.log('[ConstructionApp] Starting tangram conversion...');
62
- console.log('[ConstructionApp] Received tangrams:', params.tangrams);
63
- console.log('[ConstructionApp] Number of tangrams:', params.tangrams.length);
64
-
65
- const sectors: Sector[] = params.tangrams.map((tangramSpec, index) => {
66
- console.log(`\n[ConstructionApp] Processing tangram ${index}:`, tangramSpec);
67
- console.log(`[ConstructionApp] tangramID: ${tangramSpec.tangramID}`);
68
- console.log(`[ConstructionApp] setLabel: ${tangramSpec.setLabel}`);
69
- console.log(`[ConstructionApp] solutionTans count: ${tangramSpec.solutionTans?.length}`);
70
- console.log(`[ConstructionApp] solutionTans:`, tangramSpec.solutionTans);
61
+ // make copy of PRIMITIVE_BLUEPRINTS sorted by primitiveOrder
62
+ const PRIMITIVE_BLUEPRINTS_ORDERED = [...PRIMITIVE_BLUEPRINTS].sort((a, b) => params.primitiveOrder.indexOf(a.kind) - params.primitiveOrder.indexOf(b.kind));
71
63
 
64
+ // Convert JSON plugin parameters to internal Sector[] format
65
+ const sectors: Sector[] = params.tangrams.map((tangramSpec, index) => {
66
+
72
67
  // Filter to canonical pieces only and convert vertices to polygon format
73
68
  const filteredTans = tangramSpec.solutionTans.filter((tan: any) => {
74
69
  // Support both "name" and "kind" fields (different JSON formats)
75
70
  const tanName = tan.name ?? tan.kind;
76
71
  const isCanonical = CANON.has(tanName);
77
- console.log(`[ConstructionApp] Tan "${tanName}": canonical=${isCanonical}, vertices count=${tan.vertices?.length}`);
78
72
  return isCanonical;
79
73
  });
80
74
 
81
- console.log(`[ConstructionApp] Filtered to ${filteredTans.length} canonical pieces`);
82
-
83
75
  const mask = filteredTans.map((tan: any, tanIndex: number) => {
84
- const tanName = tan.name ?? tan.kind;
85
76
  const polygon = tan.vertices.map(([x, y]: number[]) => ({ x: x ?? 0, y: -(y ?? 0) }));
86
- console.log(`[ConstructionApp] Polygon ${tanIndex} (${tanName}): ${tan.vertices.length} vertices -> ${polygon.length} points`);
87
- console.log(`[ConstructionApp] First vertex: [${tan.vertices[0]?.[0]}, ${tan.vertices[0]?.[1]}] -> {x: ${polygon[0]?.x}, y: ${polygon[0]?.y}}`);
88
77
  return polygon;
89
78
  });
90
79
 
91
80
  // Assign sector ID from alphabetical sequence
92
81
  const sectorId = `sector${index}`;
93
82
 
94
- console.log(`[ConstructionApp] Assigned sector ID: ${sectorId}`);
95
- console.log(`[ConstructionApp] Final mask has ${mask.length} polygons`);
96
-
97
83
  const sector = {
98
84
  id: sectorId,
99
85
  tangramId: tangramSpec.tangramID,
@@ -103,76 +89,50 @@ export function startConstructionTrial(
103
89
  },
104
90
  };
105
91
 
106
- console.log(`[ConstructionApp] Created sector:`, sector);
107
92
  return sector;
108
93
  });
109
94
 
110
- console.log('\n[ConstructionApp] Final sectors array:', sectors);
111
- console.log(`[ConstructionApp] Total sectors created: ${sectors.length}`);
112
-
113
95
  // Convert quickstash_macros to Blueprint[] format
114
96
  // Handle both anchor-based composites and pre-converted blueprints
115
- console.log('\n[ConstructionApp] Processing quickstash macros...');
116
- console.log('[ConstructionApp] quickstash_macros:', params.quickstash_macros);
117
- console.log('[ConstructionApp] quickstash_macros count:', params.quickstash_macros?.length ?? 0);
118
-
119
97
  let quickstash: Blueprint[] = [];
120
98
 
121
99
  if (params.quickstash_macros && params.quickstash_macros.length > 0) {
122
100
  // Check if the first item has anchorOffset (anchor-based) or offset (pixel-based)
123
101
  const firstMacro = params.quickstash_macros[0];
124
- console.log('[ConstructionApp] First macro:', firstMacro);
125
102
  if (firstMacro && 'parts' in firstMacro && firstMacro.parts && firstMacro.parts[0] && 'anchorOffset' in firstMacro.parts[0]) {
126
- console.log('[ConstructionApp] Detected anchor-based composites, converting to pixels...');
127
103
 
128
104
  // Create primitive map for conversion
129
105
  const primsByKind = new Map<TanKind, PrimitiveBlueprint>();
130
- PRIMITIVE_BLUEPRINTS.forEach(p => primsByKind.set(p.kind, p));
106
+ PRIMITIVE_BLUEPRINTS_ORDERED.forEach(p => primsByKind.set(p.kind, p));
131
107
 
132
108
  // Convert each anchor composite to pixel-based blueprint
133
109
  quickstash = (params.quickstash_macros as AnchorComposite[]).map(anchorComposite =>
134
110
  convertAnchorCompositeToPixels(anchorComposite, primsByKind, CONFIG.layout.grid.stepPx) // Use current CONFIG grid step
135
111
  );
136
- console.log('[ConstructionApp] Converted to pixel-based blueprints:', quickstash);
137
112
  } else {
138
- console.log('[ConstructionApp] Already pixel-based blueprints');
139
113
  // Already pixel-based blueprints
140
114
  quickstash = params.quickstash_macros as Blueprint[];
141
115
  }
142
116
  } else {
143
- console.log('[ConstructionApp] No quickstash macros provided');
144
117
  }
145
118
 
146
119
  // Create React root and render GameBoard
147
120
  const gameBoardProps = {
148
121
  sectors,
149
122
  quickstash,
150
- primitives: PRIMITIVE_BLUEPRINTS,
151
- layout: (params.layout || "semicircle") as LayoutMode,
152
- target: (params.target || "silhouette") as PlacementTarget,
153
- input: (params.input || "drag") as InputMode,
154
- timeLimitMs: params.time_limit_ms || 0,
123
+ primitives: PRIMITIVE_BLUEPRINTS_ORDERED,
124
+ layout: params.layout as LayoutMode,
125
+ target: params.target as PlacementTarget,
126
+ input: params.input as InputMode,
127
+ timeLimitMs: params.time_limit_ms,
155
128
  maxQuickstashSlots: CONFIG.layout.defaults.maxQuickstashSlots,
156
- mode: 'construction' as const, // Explicit construction mode
129
+ mode: 'construction' as const,
157
130
  ...(params.onInteraction && { onInteraction: params.onInteraction }),
158
131
  ...(params.onTrialEnd && { onTrialEnd: params.onTrialEnd })
159
132
  };
160
133
 
161
- console.log('\n[ConstructionApp] Final GameBoard props:');
162
- console.log('[ConstructionApp] sectors count:', gameBoardProps.sectors.length);
163
- console.log('[ConstructionApp] quickstash count:', gameBoardProps.quickstash.length);
164
- console.log('[ConstructionApp] primitives count:', gameBoardProps.primitives.length);
165
- console.log('[ConstructionApp] layout:', gameBoardProps.layout);
166
- console.log('[ConstructionApp] target:', gameBoardProps.target);
167
- console.log('[ConstructionApp] input:', gameBoardProps.input);
168
- console.log('[ConstructionApp] timeLimitMs:', gameBoardProps.timeLimitMs);
169
- console.log('[ConstructionApp] mode:', gameBoardProps.mode);
170
- console.log('[ConstructionApp] Full props:', gameBoardProps);
171
-
172
134
  const root = createRoot(display_element);
173
135
  root.render(React.createElement(GameBoard, gameBoardProps));
174
136
 
175
- console.log('[ConstructionApp] GameBoard rendered successfully');
176
-
177
137
  return { root, display_element, jsPsych: _jsPsych };
178
138
  }
@@ -17,6 +17,12 @@ const info = {
17
17
  default: [],
18
18
  description: "Array of MacroSpec objects created in prep trial"
19
19
  },
20
+ /** Array of primitive names in the order they should be displayed */
21
+ primitive_order: {
22
+ type: ParameterType.OBJECT,
23
+ default: ["square", "smalltriangle", "parallelogram", "medtriangle", "largetriangle"],
24
+ description: "Array of primitive names in the order they should be displayed"
25
+ },
20
26
  /** Whether to place pieces in workspace or directly on silhouette */
21
27
  target: {
22
28
  type: ParameterType.SELECT,
@@ -137,10 +143,11 @@ class TangramConstructPlugin implements JsPsychPlugin<Info> {
137
143
  const params: StartConstructionTrialParams = {
138
144
  tangrams: trial.tangrams,
139
145
  quickstash_macros: trial.quickstash_macros,
146
+ primitiveOrder: trial.primitive_order,
140
147
  target: trial.target,
141
148
  input: trial.input,
142
149
  layout: trial.layout,
143
- time_limit_ms: trial.time_limit_ms || 0,
150
+ time_limit_ms: trial.time_limit_ms,
144
151
  onInteraction: trial.onInteraction,
145
152
  onTrialEnd: wrappedOnTrialEnd
146
153
  };
@@ -32,6 +32,7 @@ export interface StartPrepTrialParams {
32
32
  layoutMode: "circle" | "semicircle";
33
33
  requireAllSlots: boolean;
34
34
  quickstashMacros?: AnchorComposite[];
35
+ primitiveOrder: string[];
35
36
  onInteraction?: (event: any) => void;
36
37
  onTrialEnd?: (data: any) => void;
37
38
  }
@@ -53,10 +54,14 @@ export function startPrepTrial(
53
54
  layoutMode,
54
55
  requireAllSlots,
55
56
  quickstashMacros,
57
+ primitiveOrder,
56
58
  onInteraction,
57
59
  onTrialEnd,
58
60
  } = params;
59
61
 
62
+ // make copy of PRIMITIVE_BLUEPRINTS sorted by primitiveOrder
63
+ const PRIMITIVE_BLUEPRINTS_ORDERED = [...PRIMITIVE_BLUEPRINTS].sort((a, b) => primitiveOrder.indexOf(a.kind) - primitiveOrder.indexOf(b.kind));
64
+
60
65
  // Create blank prep sectors (no silhouettes)
61
66
  const prepSectors: Sector[] = Array.from({ length: numQuickstashSlots }, (_, i) => ({
62
67
  id: `prep-sector-${i}`,
@@ -75,7 +80,8 @@ export function startPrepTrial(
75
80
  if (quickstashMacros && quickstashMacros.length > 0 && layout) {
76
81
  // Spawn pieces immediately when controller and layout are ready
77
82
  const primsByKind = new Map<TanKind, PrimitiveBlueprint>();
78
- PRIMITIVE_BLUEPRINTS.forEach(p => primsByKind.set(p.kind, p));
83
+
84
+ PRIMITIVE_BLUEPRINTS_ORDERED.forEach(p => primsByKind.set(p.kind, p));
79
85
 
80
86
  quickstashMacros.forEach((anchorComposite, macroIndex) => {
81
87
  const sectorId = `prep-sector-${macroIndex}`;
@@ -164,7 +170,7 @@ export function startPrepTrial(
164
170
  root.render(React.createElement(GameBoard, {
165
171
  sectors: prepSectors,
166
172
  quickstash: [], // No pre-made macros
167
- primitives: PRIMITIVE_BLUEPRINTS,
173
+ primitives: PRIMITIVE_BLUEPRINTS_ORDERED,
168
174
  layout: layoutMode,
169
175
  target: 'workspace', // Pieces go in sectors
170
176
  input: inputMode,
@@ -41,6 +41,12 @@ const info = {
41
41
  default: [],
42
42
  description: "Array of AnchorComposite objects to edit as primitive pieces"
43
43
  },
44
+ /** Array of primitive names in the order they should be displayed */
45
+ primitive_order: {
46
+ type: ParameterType.OBJECT,
47
+ default: ["square", "smalltriangle", "parallelogram", "medtriangle", "largetriangle"],
48
+ description: "Array of primitive names in the order they should be displayed"
49
+ },
44
50
  /** Callback fired after each interaction (optional analytics hook) */
45
51
  onInteraction: {
46
52
  type: ParameterType.FUNCTION,
@@ -101,13 +107,14 @@ class TangramPrepPlugin implements JsPsychPlugin<Info> {
101
107
  };
102
108
 
103
109
  const params: StartPrepTrialParams = {
104
- numQuickstashSlots: trial.num_quickstash_slots || 4,
110
+ numQuickstashSlots: trial.num_quickstash_slots,
105
111
  maxPiecesPerMacro: trial.max_pieces_per_macro,
106
112
  minPiecesPerMacro: trial.min_pieces_per_macro,
107
113
  inputMode: trial.input as "click" | "drag",
108
114
  layoutMode: trial.layout as "circle" | "semicircle",
109
115
  requireAllSlots: trial.require_all_slots,
110
116
  quickstashMacros: trial.quickstash_macros,
117
+ primitiveOrder: trial.primitive_order,
111
118
  onInteraction: trial.onInteraction,
112
119
  onTrialEnd: wrappedOnTrialEnd,
113
120
  };