jspsych-tangram 0.0.3 → 0.0.5

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 (35) hide show
  1. package/dist/construct/index.browser.js +23 -63
  2. package/dist/construct/index.browser.js.map +1 -1
  3. package/dist/construct/index.browser.min.js +11 -15
  4. package/dist/construct/index.browser.min.js.map +1 -1
  5. package/dist/construct/index.cjs +23 -63
  6. package/dist/construct/index.cjs.map +1 -1
  7. package/dist/construct/index.js +23 -63
  8. package/dist/construct/index.js.map +1 -1
  9. package/dist/index.cjs +26 -64
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.js +26 -64
  12. package/dist/index.js.map +1 -1
  13. package/dist/prep/index.browser.js +16 -14
  14. package/dist/prep/index.browser.js.map +1 -1
  15. package/dist/prep/index.browser.min.js +1 -1
  16. package/dist/prep/index.browser.min.js.map +1 -1
  17. package/dist/prep/index.cjs +16 -14
  18. package/dist/prep/index.cjs.map +1 -1
  19. package/dist/prep/index.js +16 -14
  20. package/dist/prep/index.js.map +1 -1
  21. package/package.json +1 -1
  22. package/src/core/components/board/GameBoard.tsx +4 -1
  23. package/src/core/domain/primitives.ts +9 -10
  24. package/src/core/domain/types.ts +4 -3
  25. package/src/core/engine/state/BaseGameController.ts +0 -1
  26. package/src/core/io/InteractionTracker.ts +1 -1
  27. package/src/core/io/quickstash.ts +3 -29
  28. package/src/core/types/plugin-interfaces.ts +1 -1
  29. package/src/plugins/tangram-construct/ConstructionApp.tsx +13 -58
  30. package/src/plugins/tangram-construct/index.ts +1 -1
  31. package/src/plugins/tangram-prep/PrepApp.tsx +1 -0
  32. package/src/plugins/tangram-prep/index.ts +1 -1
  33. package/tangram-construct.min.js +11 -15
  34. package/tangram-prep.min.js +1 -1
  35. package/src/core/io/stims.ts +0 -107
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jspsych-tangram",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
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
  };
@@ -77,15 +77,14 @@ function constructFromSpec(
77
77
  // ===== default first edges in UNIT coords ( +y is UP here ) =================
78
78
  // Copied from your `default_pilot_rotations`. We invert y above for canvas.
79
79
  const FIRST_EDGES_UNITS: Record<TanKind, [Vec, Vec]> = {
80
- "small-triangle": [P(0, 0), P(0.5, 0.5)],
80
+ "smalltriangle": [P(0, 0), P(0.5, 0.5)],
81
81
  "parallelogram": [P(0, 0), P(0.5, 0)],
82
- "large-triangle": [P(0, 0), P(0.5, -0.5)],
83
- "med-triangle": [P(0, 0), P(0.5, 0)],
82
+ "largetriangle": [P(0, 0), P(0.5, -0.5)],
83
+ "medtriangle": [P(0, 0), P(0.5, 0)],
84
84
  "square": [P(0, 0), P(0.5, 0)],
85
85
  };
86
86
 
87
87
  // ===== canonical half-edge primitives ===================
88
- // TODO: Sean we need to talk about aligning this with your Python implementation
89
88
  // Unchanging primitive tangram pieces - these are constants, not configurable
90
89
  const PRIMITIVE_BLUEPRINTS_CACHE = (() => {
91
90
  // Sequences copied verbatim from your Python tanprimitives_halfedges()
@@ -104,8 +103,8 @@ const PRIMITIVE_BLUEPRINTS_CACHE = (() => {
104
103
  color: "#f43f5e",
105
104
  },
106
105
  {
107
- id: "prim:small",
108
- kind: "small-triangle",
106
+ id: "prim:smalltriangle",
107
+ kind: "smalltriangle",
109
108
  sideLens: [HALFDIAGONAL, HALFDIAGONAL, HALFUNIT, HALFUNIT, HALFUNIT, HALFUNIT],
110
109
  angles: [180, 45, 180, 90, 180, 45],
111
110
  color: "#f59e0b",
@@ -118,15 +117,15 @@ const PRIMITIVE_BLUEPRINTS_CACHE = (() => {
118
117
  color: "#10b981",
119
118
  },
120
119
  {
121
- id: "prim:med",
122
- kind: "med-triangle",
120
+ id: "prim:medtriangle",
121
+ kind: "medtriangle",
123
122
  sideLens: [HALFUNIT, HALFUNIT, HALFUNIT, HALFUNIT, HALFDIAGONAL, HALFDIAGONAL, HALFDIAGONAL, HALFDIAGONAL],
124
123
  angles: [180, 180, 180, 45, 180, 90, 180, 45],
125
124
  color: "#3b82f6",
126
125
  },
127
126
  {
128
- id: "prim:large",
129
- kind: "large-triangle",
127
+ id: "prim:largetriangle",
128
+ kind: "largetriangle",
130
129
  sideLens: [
131
130
  HALFDIAGONAL, HALFDIAGONAL, HALFDIAGONAL, HALFDIAGONAL,
132
131
  HALFUNIT, HALFUNIT, HALFUNIT, HALFUNIT, HALFUNIT, HALFUNIT, HALFUNIT, HALFUNIT
@@ -5,10 +5,10 @@ export type Poly = Vec[];
5
5
  // Canonical tans
6
6
  export type TanKind =
7
7
  | "square"
8
- | "small-triangle"
8
+ | "smalltriangle"
9
9
  | "parallelogram"
10
- | "med-triangle"
11
- | "large-triangle";
10
+ | "medtriangle"
11
+ | "largetriangle";
12
12
 
13
13
  // 2×2 interaction axes
14
14
  export type PlacementTarget = "workspace" | "silhouette";
@@ -55,6 +55,7 @@ export type SilhouetteSpec = {
55
55
 
56
56
  export type Sector = {
57
57
  id: string;
58
+ tangramId: string;
58
59
  silhouette: SilhouetteSpec;
59
60
  };
60
61
 
@@ -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
 
@@ -571,7 +571,7 @@ export class InteractionTracker {
571
571
  // Build sector-tangram map
572
572
  const sectorTangramMap = this.controller.state.cfg.sectors.map(s => ({
573
573
  sectorId: s.id,
574
- tangramId: s.id // In our system, sector ID == tangram ID
574
+ tangramId: s.tangramId
575
575
  }));
576
576
 
577
577
  // Build blueprint order
@@ -3,32 +3,6 @@ import { CONFIG } from "@/core/config/config";
3
3
  import type { Blueprint, CompositeBlueprint, PrimitiveBlueprint, TanKind, Vec } from "@/core/domain/types";
4
4
  import { primitiveBlueprintsHalfEdge } from "@/core/domain/primitives";
5
5
 
6
- /**
7
- * Convert anchor coordinates to pixel coordinates
8
- * @param anchorX - X coordinate in anchor units (integer)
9
- * @param anchorY - Y coordinate in anchor units (integer)
10
- * @returns Pixel coordinates
11
- */
12
- export function anchorToPixels(anchorX: number, anchorY: number): Vec {
13
- return {
14
- x: anchorX * CONFIG.layout.grid.stepPx,
15
- y: anchorY * CONFIG.layout.grid.stepPx
16
- };
17
- }
18
-
19
- /**
20
- * Convert pixel coordinates to anchor coordinates
21
- * @param pixelX - X coordinate in pixels
22
- * @param pixelY - Y coordinate in pixels
23
- * @returns Anchor coordinates (rounded to nearest grid point)
24
- */
25
- export function pixelsToAnchor(pixelX: number, pixelY: number): Vec {
26
- return {
27
- x: Math.round(pixelX / CONFIG.layout.grid.stepPx),
28
- y: Math.round(pixelY / CONFIG.layout.grid.stepPx)
29
- };
30
- }
31
-
32
6
  /**
33
7
  * Convert anchor-based composite definition to pixel-based composite with custom grid step
34
8
  * @param anchorComposite - Composite defined with anchor coordinates
@@ -84,10 +58,10 @@ const ANCHOR_COMPOSITES = [
84
58
  label: "Parallelogram+Parallelogram"
85
59
  },
86
60
  {
87
- id: "comp:small-triangle+med-triangle",
61
+ id: "comp:smalltriangle+medtriangle",
88
62
  parts: [
89
- { kind: "small-triangle" as TanKind, anchorOffset: { x: -2, y: -2 } },
90
- { kind: "med-triangle" as TanKind, anchorOffset: { x: 0, y: 0 } },
63
+ { kind: "smalltriangle" as TanKind, anchorOffset: { x: -2, y: -2 } },
64
+ { kind: "medtriangle" as TanKind, anchorOffset: { x: 0, y: 0 } },
91
65
  ],
92
66
  label: "SmallTriangle+MedTriangle"
93
67
  },
@@ -40,7 +40,7 @@ export interface TangramSpec {
40
40
  tangramID: string;
41
41
  setLabel: string;
42
42
  solutionTans: Array<{
43
- name: string; // TanKind as string (e.g., "small-triangle")
43
+ name: string; // TanKind as string (e.g., "smalltriangle")
44
44
  vertices: number[][]; // Array of [x, y] coordinate pairs
45
45
  }>;
46
46
  }
@@ -51,80 +51,51 @@ export function startConstructionTrial(
51
51
  // Canonical piece names we accept
52
52
  const CANON = new Set([
53
53
  "square",
54
- "small-triangle",
54
+ "smalltriangle",
55
55
  "parallelogram",
56
- "med-triangle",
57
- "large-triangle",
56
+ "medtriangle",
57
+ "largetriangle",
58
58
  ]);
59
59
 
60
- // Sector IDs in alphabetical order
61
- const SECTOR_IDS = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L'];
62
-
63
60
  // Convert JSON plugin parameters to internal Sector[] format
64
- console.log('[ConstructionApp] Starting tangram conversion...');
65
- console.log('[ConstructionApp] Received tangrams:', params.tangrams);
66
- console.log('[ConstructionApp] Number of tangrams:', params.tangrams.length);
67
-
68
- const sectors: Sector[] = params.tangrams.map((tangramSpec, index) => {
69
- console.log(`\n[ConstructionApp] Processing tangram ${index}:`, tangramSpec);
70
- console.log(`[ConstructionApp] tangramID: ${tangramSpec.tangramID}`);
71
- console.log(`[ConstructionApp] setLabel: ${tangramSpec.setLabel}`);
72
- console.log(`[ConstructionApp] solutionTans count: ${tangramSpec.solutionTans?.length}`);
73
- console.log(`[ConstructionApp] solutionTans:`, tangramSpec.solutionTans);
74
-
61
+ const sectors: Sector[] = params.tangrams.map((tangramSpec, index) => {
62
+
75
63
  // Filter to canonical pieces only and convert vertices to polygon format
76
64
  const filteredTans = tangramSpec.solutionTans.filter((tan: any) => {
77
65
  // Support both "name" and "kind" fields (different JSON formats)
78
66
  const tanName = tan.name ?? tan.kind;
79
67
  const isCanonical = CANON.has(tanName);
80
- console.log(`[ConstructionApp] Tan "${tanName}": canonical=${isCanonical}, vertices count=${tan.vertices?.length}`);
81
68
  return isCanonical;
82
69
  });
83
70
 
84
- console.log(`[ConstructionApp] Filtered to ${filteredTans.length} canonical pieces`);
85
-
86
71
  const mask = filteredTans.map((tan: any, tanIndex: number) => {
87
- const tanName = tan.name ?? tan.kind;
88
72
  const polygon = tan.vertices.map(([x, y]: number[]) => ({ x: x ?? 0, y: -(y ?? 0) }));
89
- console.log(`[ConstructionApp] Polygon ${tanIndex} (${tanName}): ${tan.vertices.length} vertices -> ${polygon.length} points`);
90
- console.log(`[ConstructionApp] First vertex: [${tan.vertices[0]?.[0]}, ${tan.vertices[0]?.[1]}] -> {x: ${polygon[0]?.x}, y: ${polygon[0]?.y}}`);
91
73
  return polygon;
92
74
  });
93
75
 
94
76
  // Assign sector ID from alphabetical sequence
95
- const sectorId = SECTOR_IDS[index] ?? `S${index}`;
96
- console.log(`[ConstructionApp] Assigned sector ID: ${sectorId}`);
97
- console.log(`[ConstructionApp] Final mask has ${mask.length} polygons`);
98
-
77
+ const sectorId = `sector${index}`;
78
+
99
79
  const sector = {
100
80
  id: sectorId,
81
+ tangramId: tangramSpec.tangramID,
101
82
  silhouette: {
102
83
  id: sectorId,
103
84
  mask,
104
85
  },
105
86
  };
106
87
 
107
- console.log(`[ConstructionApp] Created sector:`, sector);
108
88
  return sector;
109
89
  });
110
90
 
111
- console.log('\n[ConstructionApp] Final sectors array:', sectors);
112
- console.log(`[ConstructionApp] Total sectors created: ${sectors.length}`);
113
-
114
91
  // Convert quickstash_macros to Blueprint[] format
115
92
  // Handle both anchor-based composites and pre-converted blueprints
116
- console.log('\n[ConstructionApp] Processing quickstash macros...');
117
- console.log('[ConstructionApp] quickstash_macros:', params.quickstash_macros);
118
- console.log('[ConstructionApp] quickstash_macros count:', params.quickstash_macros?.length ?? 0);
119
-
120
93
  let quickstash: Blueprint[] = [];
121
94
 
122
95
  if (params.quickstash_macros && params.quickstash_macros.length > 0) {
123
96
  // Check if the first item has anchorOffset (anchor-based) or offset (pixel-based)
124
97
  const firstMacro = params.quickstash_macros[0];
125
- console.log('[ConstructionApp] First macro:', firstMacro);
126
98
  if (firstMacro && 'parts' in firstMacro && firstMacro.parts && firstMacro.parts[0] && 'anchorOffset' in firstMacro.parts[0]) {
127
- console.log('[ConstructionApp] Detected anchor-based composites, converting to pixels...');
128
99
 
129
100
  // Create primitive map for conversion
130
101
  const primsByKind = new Map<TanKind, PrimitiveBlueprint>();
@@ -134,14 +105,11 @@ export function startConstructionTrial(
134
105
  quickstash = (params.quickstash_macros as AnchorComposite[]).map(anchorComposite =>
135
106
  convertAnchorCompositeToPixels(anchorComposite, primsByKind, CONFIG.layout.grid.stepPx) // Use current CONFIG grid step
136
107
  );
137
- console.log('[ConstructionApp] Converted to pixel-based blueprints:', quickstash);
138
108
  } else {
139
- console.log('[ConstructionApp] Already pixel-based blueprints');
140
109
  // Already pixel-based blueprints
141
110
  quickstash = params.quickstash_macros as Blueprint[];
142
111
  }
143
112
  } else {
144
- console.log('[ConstructionApp] No quickstash macros provided');
145
113
  }
146
114
 
147
115
  // Create React root and render GameBoard
@@ -149,31 +117,18 @@ export function startConstructionTrial(
149
117
  sectors,
150
118
  quickstash,
151
119
  primitives: PRIMITIVE_BLUEPRINTS,
152
- layout: (params.layout || "semicircle") as LayoutMode,
153
- target: (params.target || "silhouette") as PlacementTarget,
154
- input: (params.input || "drag") as InputMode,
155
- timeLimitMs: params.time_limit_ms || 0,
120
+ layout: params.layout as LayoutMode,
121
+ target: params.target as PlacementTarget,
122
+ input: params.input as InputMode,
123
+ timeLimitMs: params.time_limit_ms,
156
124
  maxQuickstashSlots: CONFIG.layout.defaults.maxQuickstashSlots,
157
- mode: 'construction' as const, // Explicit construction mode
125
+ mode: 'construction' as const,
158
126
  ...(params.onInteraction && { onInteraction: params.onInteraction }),
159
127
  ...(params.onTrialEnd && { onTrialEnd: params.onTrialEnd })
160
128
  };
161
129
 
162
- console.log('\n[ConstructionApp] Final GameBoard props:');
163
- console.log('[ConstructionApp] sectors count:', gameBoardProps.sectors.length);
164
- console.log('[ConstructionApp] quickstash count:', gameBoardProps.quickstash.length);
165
- console.log('[ConstructionApp] primitives count:', gameBoardProps.primitives.length);
166
- console.log('[ConstructionApp] layout:', gameBoardProps.layout);
167
- console.log('[ConstructionApp] target:', gameBoardProps.target);
168
- console.log('[ConstructionApp] input:', gameBoardProps.input);
169
- console.log('[ConstructionApp] timeLimitMs:', gameBoardProps.timeLimitMs);
170
- console.log('[ConstructionApp] mode:', gameBoardProps.mode);
171
- console.log('[ConstructionApp] Full props:', gameBoardProps);
172
-
173
130
  const root = createRoot(display_element);
174
131
  root.render(React.createElement(GameBoard, gameBoardProps));
175
132
 
176
- console.log('[ConstructionApp] GameBoard rendered successfully');
177
-
178
133
  return { root, display_element, jsPsych: _jsPsych };
179
134
  }
@@ -140,7 +140,7 @@ class TangramConstructPlugin implements JsPsychPlugin<Info> {
140
140
  target: trial.target,
141
141
  input: trial.input,
142
142
  layout: trial.layout,
143
- time_limit_ms: trial.time_limit_ms || 0,
143
+ time_limit_ms: trial.time_limit_ms,
144
144
  onInteraction: trial.onInteraction,
145
145
  onTrialEnd: wrappedOnTrialEnd
146
146
  };
@@ -60,6 +60,7 @@ export function startPrepTrial(
60
60
  // Create blank prep sectors (no silhouettes)
61
61
  const prepSectors: Sector[] = Array.from({ length: numQuickstashSlots }, (_, i) => ({
62
62
  id: `prep-sector-${i}`,
63
+ tangramId: `prep-sector-${i}`, // dummy value since prep mode doesn't have tangrams
63
64
  silhouette: {
64
65
  id: `prep-silhouette-${i}`,
65
66
  mask: []
@@ -101,7 +101,7 @@ class TangramPrepPlugin implements JsPsychPlugin<Info> {
101
101
  };
102
102
 
103
103
  const params: StartPrepTrialParams = {
104
- numQuickstashSlots: trial.num_quickstash_slots || 4,
104
+ numQuickstashSlots: trial.num_quickstash_slots,
105
105
  maxPiecesPerMacro: trial.max_pieces_per_macro,
106
106
  minPiecesPerMacro: trial.min_pieces_per_macro,
107
107
  inputMode: trial.input as "click" | "drag",