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.
- package/dist/construct/index.browser.js +23 -63
- package/dist/construct/index.browser.js.map +1 -1
- package/dist/construct/index.browser.min.js +11 -15
- package/dist/construct/index.browser.min.js.map +1 -1
- package/dist/construct/index.cjs +23 -63
- package/dist/construct/index.cjs.map +1 -1
- package/dist/construct/index.js +23 -63
- package/dist/construct/index.js.map +1 -1
- package/dist/index.cjs +26 -64
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +26 -64
- package/dist/index.js.map +1 -1
- package/dist/prep/index.browser.js +16 -14
- package/dist/prep/index.browser.js.map +1 -1
- package/dist/prep/index.browser.min.js +1 -1
- package/dist/prep/index.browser.min.js.map +1 -1
- package/dist/prep/index.cjs +16 -14
- package/dist/prep/index.cjs.map +1 -1
- package/dist/prep/index.js +16 -14
- package/dist/prep/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/components/board/GameBoard.tsx +4 -1
- package/src/core/domain/primitives.ts +9 -10
- package/src/core/domain/types.ts +4 -3
- package/src/core/engine/state/BaseGameController.ts +0 -1
- package/src/core/io/InteractionTracker.ts +1 -1
- package/src/core/io/quickstash.ts +3 -29
- package/src/core/types/plugin-interfaces.ts +1 -1
- package/src/plugins/tangram-construct/ConstructionApp.tsx +13 -58
- package/src/plugins/tangram-construct/index.ts +1 -1
- package/src/plugins/tangram-prep/PrepApp.tsx +1 -0
- package/src/plugins/tangram-prep/index.ts +1 -1
- package/tangram-construct.min.js +11 -15
- package/tangram-prep.min.js +1 -1
- package/src/core/io/stims.ts +0 -107
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
"
|
|
80
|
+
"smalltriangle": [P(0, 0), P(0.5, 0.5)],
|
|
81
81
|
"parallelogram": [P(0, 0), P(0.5, 0)],
|
|
82
|
-
"
|
|
83
|
-
"
|
|
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:
|
|
108
|
-
kind: "
|
|
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:
|
|
122
|
-
kind: "
|
|
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:
|
|
129
|
-
kind: "
|
|
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
|
package/src/core/domain/types.ts
CHANGED
|
@@ -5,10 +5,10 @@ export type Poly = Vec[];
|
|
|
5
5
|
// Canonical tans
|
|
6
6
|
export type TanKind =
|
|
7
7
|
| "square"
|
|
8
|
-
| "
|
|
8
|
+
| "smalltriangle"
|
|
9
9
|
| "parallelogram"
|
|
10
|
-
| "
|
|
11
|
-
| "
|
|
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.
|
|
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:
|
|
61
|
+
id: "comp:smalltriangle+medtriangle",
|
|
88
62
|
parts: [
|
|
89
|
-
{ kind: "
|
|
90
|
-
{ kind: "
|
|
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., "
|
|
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
|
-
"
|
|
54
|
+
"smalltriangle",
|
|
55
55
|
"parallelogram",
|
|
56
|
-
"
|
|
57
|
-
"
|
|
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
|
-
|
|
65
|
-
|
|
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 =
|
|
96
|
-
|
|
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:
|
|
153
|
-
target:
|
|
154
|
-
input:
|
|
155
|
-
timeLimitMs: params.time_limit_ms
|
|
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,
|
|
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
|
|
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
|
|
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",
|