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/dist/construct/index.browser.js +91 -36
- 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 +90 -35
- package/dist/construct/index.cjs.map +1 -1
- package/dist/construct/index.js +90 -35
- package/dist/construct/index.js.map +1 -1
- package/dist/index.cjs +90 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +90 -35
- package/dist/index.js.map +1 -1
- package/dist/prep/index.browser.js +9 -9
- package/dist/prep/index.browser.js.map +1 -1
- package/dist/prep/index.browser.min.js +2 -2
- package/dist/prep/index.browser.min.js.map +1 -1
- package/dist/prep/index.cjs +8 -8
- package/dist/prep/index.cjs.map +1 -1
- package/dist/prep/index.js +8 -8
- package/dist/prep/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/domain/primitives.ts +8 -8
- package/src/core/domain/types.ts +3 -4
- package/src/core/io/quickstash.ts +3 -3
- package/src/core/io/stims.ts +6 -9
- package/src/core/types/plugin-interfaces.ts +6 -5
- package/src/index.spec.ts +0 -19
- package/src/plugins/tangram-construct/ConstructionApp.tsx +104 -30
- package/tangram-construct.min.js +15 -11
- package/tangram-prep.min.js +2 -2
- package/src/core/io/json-to-tangram-spec.ts +0 -110
package/package.json
CHANGED
|
@@ -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
|
-
|
|
81
|
-
parallelogram: [P(0, 0), P(0.5, 0)],
|
|
82
|
-
|
|
83
|
-
|
|
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: "
|
|
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: "
|
|
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: "
|
|
129
|
+
kind: "large-triangle",
|
|
130
130
|
sideLens: [
|
|
131
131
|
HALFDIAGONAL, HALFDIAGONAL, HALFDIAGONAL, HALFDIAGONAL,
|
|
132
132
|
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
|
+
| "small-triangle"
|
|
9
9
|
| "parallelogram"
|
|
10
|
-
| "
|
|
11
|
-
| "
|
|
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:
|
|
87
|
+
id: "comp:small-triangle+med-triangle",
|
|
88
88
|
parts: [
|
|
89
|
-
{ kind: "
|
|
90
|
-
{ kind: "
|
|
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
|
},
|
package/src/core/io/stims.ts
CHANGED
|
@@ -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
|
-
"
|
|
21
|
+
"small-triangle",
|
|
22
22
|
"parallelogram",
|
|
23
|
-
"
|
|
24
|
-
"
|
|
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
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
);
|
|
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
|
}
|