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/dist/construct/index.browser.js +94 -40
- package/dist/construct/index.browser.js.map +1 -1
- package/dist/construct/index.browser.min.js +15 -11
- package/dist/construct/index.browser.min.js.map +1 -1
- package/dist/construct/index.cjs +94 -40
- package/dist/construct/index.cjs.map +1 -1
- package/dist/construct/index.js +94 -40
- package/dist/construct/index.js.map +1 -1
- package/dist/index.cjs +96 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +96 -40
- package/dist/index.js.map +1 -1
- package/dist/prep/index.browser.js +14 -13
- 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 +14 -13
- package/dist/prep/index.cjs.map +1 -1
- package/dist/prep/index.js +14 -13
- package/dist/prep/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/domain/primitives.ts +11 -12
- package/src/core/domain/types.ts +4 -4
- package/src/core/io/InteractionTracker.ts +1 -1
- package/src/core/io/quickstash.ts +3 -29
- package/src/core/types/plugin-interfaces.ts +6 -5
- package/src/index.spec.ts +0 -19
- package/src/plugins/tangram-construct/ConstructionApp.tsx +103 -30
- package/src/plugins/tangram-prep/PrepApp.tsx +1 -0
- package/tangram-construct.min.js +15 -11
- package/tangram-prep.min.js +1 -1
- package/src/core/io/json-to-tangram-spec.ts +0 -110
- package/src/core/io/stims.ts +0 -110
package/package.json
CHANGED
|
@@ -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
|
-
|
|
81
|
-
parallelogram: [P(0, 0), P(0.5, 0)],
|
|
82
|
-
|
|
83
|
-
|
|
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:
|
|
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";
|
|
@@ -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.
|
|
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
|
},
|
|
@@ -37,11 +37,12 @@ export interface PrepTrialData {
|
|
|
37
37
|
|
|
38
38
|
// Supporting types for plugin interfaces
|
|
39
39
|
export interface TangramSpec {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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: []
|