jspsych-tangram 0.0.1 → 0.0.3

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.1",
3
+ "version": "0.0.3",
4
4
  "description": "Tangram tasks for jsPsych: prep and construct.",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -77,11 +77,11 @@ 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
+ "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)],
85
85
  };
86
86
 
87
87
  // ===== canonical half-edge primitives ===================
@@ -105,7 +105,7 @@ const PRIMITIVE_BLUEPRINTS_CACHE = (() => {
105
105
  },
106
106
  {
107
107
  id: "prim:small",
108
- kind: "small_triangle",
108
+ kind: "small-triangle",
109
109
  sideLens: [HALFDIAGONAL, HALFDIAGONAL, HALFUNIT, HALFUNIT, HALFUNIT, HALFUNIT],
110
110
  angles: [180, 45, 180, 90, 180, 45],
111
111
  color: "#f59e0b",
@@ -119,14 +119,14 @@ const PRIMITIVE_BLUEPRINTS_CACHE = (() => {
119
119
  },
120
120
  {
121
121
  id: "prim:med",
122
- kind: "med_triangle",
122
+ kind: "med-triangle",
123
123
  sideLens: [HALFUNIT, HALFUNIT, HALFUNIT, HALFUNIT, HALFDIAGONAL, HALFDIAGONAL, HALFDIAGONAL, HALFDIAGONAL],
124
124
  angles: [180, 180, 180, 45, 180, 90, 180, 45],
125
125
  color: "#3b82f6",
126
126
  },
127
127
  {
128
128
  id: "prim:large",
129
- kind: "large_triangle",
129
+ kind: "large-triangle",
130
130
  sideLens: [
131
131
  HALFDIAGONAL, HALFDIAGONAL, HALFDIAGONAL, HALFDIAGONAL,
132
132
  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
+ | "small-triangle"
9
9
  | "parallelogram"
10
- | "med_triangle"
11
- | "large_triangle";
10
+ | "med-triangle"
11
+ | "large-triangle";
12
12
 
13
13
  // 2×2 interaction axes
14
14
  export type PlacementTarget = "workspace" | "silhouette";
@@ -51,7 +51,6 @@ 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 = {
@@ -84,10 +84,10 @@ const ANCHOR_COMPOSITES = [
84
84
  label: "Parallelogram+Parallelogram"
85
85
  },
86
86
  {
87
- id: "comp:small_triangle+med_triangle",
87
+ id: "comp:small-triangle+med-triangle",
88
88
  parts: [
89
- { kind: "small_triangle" as TanKind, anchorOffset: { x: -2, y: -2 } },
90
- { kind: "med_triangle" as TanKind, anchorOffset: { x: 0, y: 0 } },
89
+ { kind: "small-triangle" as TanKind, anchorOffset: { x: -2, y: -2 } },
90
+ { kind: "med-triangle" as TanKind, anchorOffset: { x: 0, y: 0 } },
91
91
  ],
92
92
  label: "SmallTriangle+MedTriangle"
93
93
  },
@@ -18,10 +18,10 @@ type RawStim = {
18
18
  /** Canonical tangram names we accept as actual piece polys. */
19
19
  const CANON = new Set([
20
20
  "square",
21
- "small_triangle",
21
+ "small-triangle",
22
22
  "parallelogram",
23
- "med_triangle",
24
- "large_triangle",
23
+ "med-triangle",
24
+ "large-triangle",
25
25
  ]);
26
26
 
27
27
  // ----------------------- guards & converters -----------------------
@@ -59,8 +59,7 @@ function polyFromVertices(vertices: Array<[number, number] | { x: number; y: num
59
59
  */
60
60
  export function normalizeStims(
61
61
  src: unknown,
62
- fallbackSectorIds: string[],
63
- defaultRequired = 2
62
+ fallbackSectorIds: string[]
64
63
  ): Sector[] {
65
64
  const rawList: RawStim[] = Array.isArray(src)
66
65
  ? (src as RawStim[])
@@ -89,7 +88,6 @@ export function normalizeStims(
89
88
  silhouette: {
90
89
  id,
91
90
  mask: polys,
92
- requiredCount: defaultRequired,
93
91
  },
94
92
  });
95
93
  }
@@ -100,11 +98,10 @@ export function normalizeStims(
100
98
  /** Fetch + normalize helper for dev path (e.g., "/dev/assets/stims_dev.json"). */
101
99
  export async function loadStimSectorsFromUrl(
102
100
  url: string,
103
- sectorIds: string[],
104
- defaultRequired = 2
101
+ sectorIds: string[]
105
102
  ): Promise<Sector[]> {
106
103
  const res = await fetch(url);
107
104
  if (!res.ok) throw new Error(`Failed to load stims: ${res.status} ${res.statusText}`);
108
105
  const json = await res.json();
109
- return normalizeStims(json, sectorIds, defaultRequired);
106
+ return normalizeStims(json, sectorIds);
110
107
  }
@@ -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., "small-triangle")
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,136 @@ 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
+ "small-triangle",
55
+ "parallelogram",
56
+ "med-triangle",
57
+ "large-triangle",
58
+ ]);
59
+
60
+ // Sector IDs in alphabetical order
61
+ const SECTOR_IDS = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L'];
62
+
51
63
  // 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
- }));
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
+
75
+ // Filter to canonical pieces only and convert vertices to polygon format
76
+ const filteredTans = tangramSpec.solutionTans.filter((tan: any) => {
77
+ // Support both "name" and "kind" fields (different JSON formats)
78
+ const tanName = tan.name ?? tan.kind;
79
+ const isCanonical = CANON.has(tanName);
80
+ console.log(`[ConstructionApp] Tan "${tanName}": canonical=${isCanonical}, vertices count=${tan.vertices?.length}`);
81
+ return isCanonical;
82
+ });
83
+
84
+ console.log(`[ConstructionApp] Filtered to ${filteredTans.length} canonical pieces`);
85
+
86
+ const mask = filteredTans.map((tan: any, tanIndex: number) => {
87
+ const tanName = tan.name ?? tan.kind;
88
+ 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
+ return polygon;
92
+ });
93
+
94
+ // 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
+
99
+ const sector = {
100
+ id: sectorId,
101
+ silhouette: {
102
+ id: sectorId,
103
+ mask,
104
+ },
105
+ };
106
+
107
+ console.log(`[ConstructionApp] Created sector:`, sector);
108
+ return sector;
109
+ });
110
+
111
+ console.log('\n[ConstructionApp] Final sectors array:', sectors);
112
+ console.log(`[ConstructionApp] Total sectors created: ${sectors.length}`);
62
113
 
63
114
  // Convert quickstash_macros to Blueprint[] format
64
115
  // 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
+
65
120
  let quickstash: Blueprint[] = [];
66
-
121
+
67
122
  if (params.quickstash_macros && params.quickstash_macros.length > 0) {
68
123
  // Check if the first item has anchorOffset (anchor-based) or offset (pixel-based)
69
124
  const firstMacro = params.quickstash_macros[0];
125
+ console.log('[ConstructionApp] First macro:', firstMacro);
70
126
  if (firstMacro && 'parts' in firstMacro && firstMacro.parts && firstMacro.parts[0] && 'anchorOffset' in firstMacro.parts[0]) {
71
-
127
+ console.log('[ConstructionApp] Detected anchor-based composites, converting to pixels...');
128
+
72
129
  // Create primitive map for conversion
73
130
  const primsByKind = new Map<TanKind, PrimitiveBlueprint>();
74
131
  PRIMITIVE_BLUEPRINTS.forEach(p => primsByKind.set(p.kind, p));
75
-
132
+
76
133
  // Convert each anchor composite to pixel-based blueprint
77
- quickstash = (params.quickstash_macros as AnchorComposite[]).map(anchorComposite =>
134
+ quickstash = (params.quickstash_macros as AnchorComposite[]).map(anchorComposite =>
78
135
  convertAnchorCompositeToPixels(anchorComposite, primsByKind, CONFIG.layout.grid.stepPx) // Use current CONFIG grid step
79
136
  );
137
+ console.log('[ConstructionApp] Converted to pixel-based blueprints:', quickstash);
80
138
  } else {
139
+ console.log('[ConstructionApp] Already pixel-based blueprints');
81
140
  // Already pixel-based blueprints
82
141
  quickstash = params.quickstash_macros as Blueprint[];
83
142
  }
143
+ } else {
144
+ console.log('[ConstructionApp] No quickstash macros provided');
84
145
  }
85
146
 
86
147
  // Create React root and render GameBoard
148
+ const gameBoardProps = {
149
+ sectors,
150
+ quickstash,
151
+ 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,
156
+ maxQuickstashSlots: CONFIG.layout.defaults.maxQuickstashSlots,
157
+ mode: 'construction' as const, // Explicit construction mode
158
+ ...(params.onInteraction && { onInteraction: params.onInteraction }),
159
+ ...(params.onTrialEnd && { onTrialEnd: params.onTrialEnd })
160
+ };
161
+
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
+
87
173
  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
- );
174
+ root.render(React.createElement(GameBoard, gameBoardProps));
175
+
176
+ console.log('[ConstructionApp] GameBoard rendered successfully');
103
177
 
104
178
  return { root, display_element, jsPsych: _jsPsych };
105
179
  }