jspsych-tangram 0.0.2 → 0.0.4

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.2",
3
+ "version": "0.0.4",
4
4
  "description": "Tangram tasks for jsPsych: prep and construct.",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -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)],
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)],
84
- square: [P(0, 0), P(0.5, 0)],
80
+ "smalltriangle": [P(0, 0), P(0.5, 0.5)],
81
+ "parallelogram": [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
+ "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";
@@ -51,11 +51,11 @@ export type SilhouetteSpec = {
51
51
  id: string;
52
52
  anchors?: Anchor[];
53
53
  mask?: Poly[]; // polygon masks for silhouette matching
54
- requiredCount?: number; // minimal pieces to consider the sector complete (M4)
55
54
  };
56
55
 
57
56
  export type Sector = {
58
57
  id: string;
58
+ tangramId: string;
59
59
  silhouette: SilhouetteSpec;
60
60
  };
61
61
 
@@ -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
  },
@@ -37,11 +37,12 @@ export interface PrepTrialData {
37
37
 
38
38
  // Supporting types for plugin interfaces
39
39
  export interface TangramSpec {
40
- id: string;
41
- silhouette: {
42
- mask: number[][][]; // Polygon arrays as numbers
43
- requiredCount: number;
44
- };
40
+ tangramID: string;
41
+ setLabel: string;
42
+ solutionTans: Array<{
43
+ name: string; // TanKind as string (e.g., "smalltriangle")
44
+ vertices: number[][]; // Array of [x, y] coordinate pairs
45
+ }>;
45
46
  }
46
47
 
47
48
  export interface MacroSpec {
package/src/index.spec.ts CHANGED
@@ -1,19 +0,0 @@
1
- import { startTimeline } from "@jspsych/test-utils";
2
-
3
- import { TangramConstructPlugin } from ".";
4
-
5
- jest.useFakeTimers();
6
-
7
- describe("my plugin", () => {
8
- it("should load", async () => {
9
- const { expectFinished } = await startTimeline([
10
- {
11
- type: TangramConstructPlugin,
12
- parameter_name: 1,
13
- parameter_name2: "img.png",
14
- },
15
- ]);
16
-
17
- await expectFinished();
18
- });
19
- });
@@ -44,62 +44,135 @@ type AnchorComposite = {
44
44
  * Start a construction trial by rendering the GameBoard component
45
45
  */
46
46
  export function startConstructionTrial(
47
- display_element: HTMLElement,
47
+ display_element: HTMLElement,
48
48
  params: StartConstructionTrialParams,
49
49
  _jsPsych: JsPsych
50
50
  ) {
51
+ // Canonical piece names we accept
52
+ const CANON = new Set([
53
+ "square",
54
+ "smalltriangle",
55
+ "parallelogram",
56
+ "medtriangle",
57
+ "largetriangle",
58
+ ]);
59
+
51
60
  // Convert JSON plugin parameters to internal Sector[] format
52
- const sectors: Sector[] = params.tangrams.map(tangramSpec => ({
53
- id: tangramSpec.id,
54
- silhouette: {
55
- id: tangramSpec.id,
56
- mask: tangramSpec.silhouette.mask.map((polygonArray: number[][]) =>
57
- polygonArray.map(([x, y]: number[]) => ({ x: x ?? 0, y: -(y ?? 0) })) // Convert to SVG coords (flip Y)
58
- ),
59
- requiredCount: tangramSpec.silhouette.requiredCount,
60
- },
61
- }));
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);
71
+
72
+ // Filter to canonical pieces only and convert vertices to polygon format
73
+ const filteredTans = tangramSpec.solutionTans.filter((tan: any) => {
74
+ // Support both "name" and "kind" fields (different JSON formats)
75
+ const tanName = tan.name ?? tan.kind;
76
+ const isCanonical = CANON.has(tanName);
77
+ console.log(`[ConstructionApp] Tan "${tanName}": canonical=${isCanonical}, vertices count=${tan.vertices?.length}`);
78
+ return isCanonical;
79
+ });
80
+
81
+ console.log(`[ConstructionApp] Filtered to ${filteredTans.length} canonical pieces`);
82
+
83
+ const mask = filteredTans.map((tan: any, tanIndex: number) => {
84
+ const tanName = tan.name ?? tan.kind;
85
+ 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
+ return polygon;
89
+ });
90
+
91
+ // Assign sector ID from alphabetical sequence
92
+ const sectorId = `sector${index}`;
93
+
94
+ console.log(`[ConstructionApp] Assigned sector ID: ${sectorId}`);
95
+ console.log(`[ConstructionApp] Final mask has ${mask.length} polygons`);
96
+
97
+ const sector = {
98
+ id: sectorId,
99
+ tangramId: tangramSpec.tangramID,
100
+ silhouette: {
101
+ id: sectorId,
102
+ mask,
103
+ },
104
+ };
105
+
106
+ console.log(`[ConstructionApp] Created sector:`, sector);
107
+ return sector;
108
+ });
109
+
110
+ console.log('\n[ConstructionApp] Final sectors array:', sectors);
111
+ console.log(`[ConstructionApp] Total sectors created: ${sectors.length}`);
62
112
 
63
113
  // Convert quickstash_macros to Blueprint[] format
64
114
  // 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
+
65
119
  let quickstash: Blueprint[] = [];
66
-
120
+
67
121
  if (params.quickstash_macros && params.quickstash_macros.length > 0) {
68
122
  // Check if the first item has anchorOffset (anchor-based) or offset (pixel-based)
69
123
  const firstMacro = params.quickstash_macros[0];
124
+ console.log('[ConstructionApp] First macro:', firstMacro);
70
125
  if (firstMacro && 'parts' in firstMacro && firstMacro.parts && firstMacro.parts[0] && 'anchorOffset' in firstMacro.parts[0]) {
71
-
126
+ console.log('[ConstructionApp] Detected anchor-based composites, converting to pixels...');
127
+
72
128
  // Create primitive map for conversion
73
129
  const primsByKind = new Map<TanKind, PrimitiveBlueprint>();
74
130
  PRIMITIVE_BLUEPRINTS.forEach(p => primsByKind.set(p.kind, p));
75
-
131
+
76
132
  // Convert each anchor composite to pixel-based blueprint
77
- quickstash = (params.quickstash_macros as AnchorComposite[]).map(anchorComposite =>
133
+ quickstash = (params.quickstash_macros as AnchorComposite[]).map(anchorComposite =>
78
134
  convertAnchorCompositeToPixels(anchorComposite, primsByKind, CONFIG.layout.grid.stepPx) // Use current CONFIG grid step
79
135
  );
136
+ console.log('[ConstructionApp] Converted to pixel-based blueprints:', quickstash);
80
137
  } else {
138
+ console.log('[ConstructionApp] Already pixel-based blueprints');
81
139
  // Already pixel-based blueprints
82
140
  quickstash = params.quickstash_macros as Blueprint[];
83
141
  }
142
+ } else {
143
+ console.log('[ConstructionApp] No quickstash macros provided');
84
144
  }
85
145
 
86
146
  // Create React root and render GameBoard
147
+ const gameBoardProps = {
148
+ sectors,
149
+ 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,
155
+ maxQuickstashSlots: CONFIG.layout.defaults.maxQuickstashSlots,
156
+ mode: 'construction' as const, // Explicit construction mode
157
+ ...(params.onInteraction && { onInteraction: params.onInteraction }),
158
+ ...(params.onTrialEnd && { onTrialEnd: params.onTrialEnd })
159
+ };
160
+
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
+
87
172
  const root = createRoot(display_element);
88
- root.render(
89
- React.createElement(GameBoard, {
90
- sectors,
91
- quickstash,
92
- primitives: PRIMITIVE_BLUEPRINTS,
93
- layout: (params.layout || "semicircle") as LayoutMode,
94
- target: (params.target || "silhouette") as PlacementTarget,
95
- input: (params.input || "drag") as InputMode,
96
- timeLimitMs: params.time_limit_ms || 0,
97
- maxQuickstashSlots: CONFIG.layout.defaults.maxQuickstashSlots,
98
- mode: 'construction', // Explicit construction mode
99
- ...(params.onInteraction && { onInteraction: params.onInteraction }),
100
- ...(params.onTrialEnd && { onTrialEnd: params.onTrialEnd })
101
- })
102
- );
173
+ root.render(React.createElement(GameBoard, gameBoardProps));
174
+
175
+ console.log('[ConstructionApp] GameBoard rendered successfully');
103
176
 
104
177
  return { root, display_element, jsPsych: _jsPsych };
105
178
  }
@@ -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: []