jspsych-tangram 0.0.1

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.
Files changed (72) hide show
  1. package/README.md +25 -0
  2. package/dist/construct/index.browser.js +20431 -0
  3. package/dist/construct/index.browser.js.map +1 -0
  4. package/dist/construct/index.browser.min.js +42 -0
  5. package/dist/construct/index.browser.min.js.map +1 -0
  6. package/dist/construct/index.cjs +3720 -0
  7. package/dist/construct/index.cjs.map +1 -0
  8. package/dist/construct/index.d.ts +204 -0
  9. package/dist/construct/index.js +3718 -0
  10. package/dist/construct/index.js.map +1 -0
  11. package/dist/index.cjs +3920 -0
  12. package/dist/index.cjs.map +1 -0
  13. package/dist/index.d.ts +340 -0
  14. package/dist/index.js +3917 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/prep/index.browser.js +20455 -0
  17. package/dist/prep/index.browser.js.map +1 -0
  18. package/dist/prep/index.browser.min.js +42 -0
  19. package/dist/prep/index.browser.min.js.map +1 -0
  20. package/dist/prep/index.cjs +3744 -0
  21. package/dist/prep/index.cjs.map +1 -0
  22. package/dist/prep/index.d.ts +139 -0
  23. package/dist/prep/index.js +3742 -0
  24. package/dist/prep/index.js.map +1 -0
  25. package/package.json +77 -0
  26. package/src/core/components/README.md +249 -0
  27. package/src/core/components/board/BoardView.tsx +352 -0
  28. package/src/core/components/board/GameBoard.tsx +682 -0
  29. package/src/core/components/board/index.ts +70 -0
  30. package/src/core/components/board/useAnchorGrid.ts +110 -0
  31. package/src/core/components/board/useClickController.ts +436 -0
  32. package/src/core/components/board/useDragController.ts +1051 -0
  33. package/src/core/components/board/usePieceState.ts +178 -0
  34. package/src/core/components/board/utils.ts +76 -0
  35. package/src/core/components/index.ts +33 -0
  36. package/src/core/components/pieces/BlueprintRing.tsx +238 -0
  37. package/src/core/config/config.ts +85 -0
  38. package/src/core/domain/blueprints.ts +25 -0
  39. package/src/core/domain/layout.ts +159 -0
  40. package/src/core/domain/primitives.ts +159 -0
  41. package/src/core/domain/solve.ts +184 -0
  42. package/src/core/domain/types.ts +111 -0
  43. package/src/core/engine/collision/grid-snapping.ts +283 -0
  44. package/src/core/engine/collision/index.ts +4 -0
  45. package/src/core/engine/collision/sat-collision.ts +46 -0
  46. package/src/core/engine/collision/validation.ts +166 -0
  47. package/src/core/engine/geometry/bounds.ts +91 -0
  48. package/src/core/engine/geometry/collision.ts +64 -0
  49. package/src/core/engine/geometry/index.ts +19 -0
  50. package/src/core/engine/geometry/math.ts +101 -0
  51. package/src/core/engine/geometry/pieces.ts +290 -0
  52. package/src/core/engine/geometry/polygons.ts +43 -0
  53. package/src/core/engine/state/BaseGameController.ts +368 -0
  54. package/src/core/engine/validation/border-rendering.ts +318 -0
  55. package/src/core/engine/validation/complete.ts +102 -0
  56. package/src/core/engine/validation/face-to-face.ts +217 -0
  57. package/src/core/index.ts +3 -0
  58. package/src/core/io/InteractionTracker.ts +742 -0
  59. package/src/core/io/data-tracking.ts +271 -0
  60. package/src/core/io/json-to-tangram-spec.ts +110 -0
  61. package/src/core/io/quickstash.ts +141 -0
  62. package/src/core/io/stims.ts +110 -0
  63. package/src/core/types/index.ts +5 -0
  64. package/src/core/types/plugin-interfaces.ts +101 -0
  65. package/src/index.spec.ts +19 -0
  66. package/src/index.ts +2 -0
  67. package/src/plugins/tangram-construct/ConstructionApp.tsx +105 -0
  68. package/src/plugins/tangram-construct/index.ts +156 -0
  69. package/src/plugins/tangram-prep/PrepApp.tsx +182 -0
  70. package/src/plugins/tangram-prep/index.ts +122 -0
  71. package/tangram-construct.min.js +42 -0
  72. package/tangram-prep.min.js +42 -0
@@ -0,0 +1,102 @@
1
+ // core/validate/complete.ts
2
+ import type { Poly, Vec } from "@/core/domain/types";
3
+ import { pointInPolygon } from "@/core/engine/geometry";
4
+ import { CONFIG } from "@/core/config/config";
5
+
6
+ /** AABB of one or more polys */
7
+ function aabb(polys: Poly[]) {
8
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
9
+ for (const poly of polys) for (const p of poly) {
10
+ if (p.x < minX) minX = p.x;
11
+ if (p.y < minY) minY = p.y;
12
+ if (p.x > maxX) maxX = p.x;
13
+ if (p.y > maxY) maxY = p.y;
14
+ }
15
+ return { minX, minY, maxX, maxY };
16
+ }
17
+
18
+ /** Cell centers over an AABB, aligned to GRID_PX centers (k + GRID_PX/2). */
19
+ function cellCentersInAABB(minX: number, minY: number, maxX: number, maxY: number): Vec[] {
20
+ const GRID_PX = CONFIG.layout.grid.stepPx;
21
+ const x0 = Math.floor((minX - GRID_PX/2) / GRID_PX) * GRID_PX + GRID_PX/2;
22
+ const y0 = Math.floor((minY - GRID_PX/2) / GRID_PX) * GRID_PX + GRID_PX/2;
23
+ const out: Vec[] = [];
24
+ for (let y = y0; y <= maxY; y += GRID_PX) {
25
+ for (let x = x0; x <= maxX; x += GRID_PX) out.push({ x, y });
26
+ }
27
+ return out;
28
+ }
29
+
30
+ /** Build a set of grid centers that lie inside ANY of the polygons. */
31
+ export function cellSetForPolys(polys: Poly[]): Set<string> {
32
+ if (!polys.length) return new Set();
33
+ const { minX, minY, maxX, maxY } = aabb(polys);
34
+ const centers = cellCentersInAABB(minX, minY, maxX, maxY);
35
+ const key = (p: Vec) => `${p.x},${p.y}`;
36
+ const S = new Set<string>();
37
+ center: for (const c of centers) {
38
+ for (const poly of polys) {
39
+ if (pointInPolygon(c, poly)) { S.add(key(c)); continue center; }
40
+ }
41
+ }
42
+ return S;
43
+ }
44
+
45
+ /** Anchors | Silhouette: complete iff silhouette cell set ⊆ piece cell set. */
46
+ export function anchorsSilhouetteComplete(silPolys: Poly[], piecePolys: Poly[]): boolean {
47
+ if (!silPolys.length) return false;
48
+ const S = cellSetForPolys(silPolys);
49
+ if (S.size === 0) return false;
50
+ const P = cellSetForPolys(piecePolys);
51
+ if (P.size === 0) return false;
52
+ for (const k of S) if (!P.has(k)) return false;
53
+ return true;
54
+ }
55
+
56
+ // --- helpers for translation-invariant equality on the grid -----------------
57
+ function stringSetToVecs(S: Set<string>): Vec[] {
58
+ const out: Vec[] = [];
59
+ for (const k of S) {
60
+ const [xs, ys] = k.split(",");
61
+ out.push({ x: Number(xs), y: Number(ys) });
62
+ }
63
+ return out;
64
+ }
65
+
66
+ function normalizeCells(S: Set<string>): Set<string> {
67
+ if (S.size === 0) return S;
68
+ const pts = stringSetToVecs(S);
69
+ let minX = Infinity,
70
+ minY = Infinity;
71
+ for (const p of pts) {
72
+ if (p.x < minX) minX = p.x;
73
+ if (p.y < minY) minY = p.y;
74
+ }
75
+ const N = new Set<string>();
76
+ for (const p of pts) N.add(`${p.x - minX},${p.y - minY}`);
77
+ return N;
78
+ }
79
+
80
+ function setsEqual(a: Set<string>, b: Set<string>): boolean {
81
+ if (a.size !== b.size) return false;
82
+ for (const k of a) if (!b.has(k)) return false;
83
+ return true;
84
+ }
85
+
86
+ /**
87
+ * Anchors | Workspace:
88
+ * complete iff the pieces form the sector's silhouette pattern up to a GRID_PX translation.
89
+ * (No rotation/mirror; exact coverage — no extra cells.)
90
+ */
91
+ export function anchorsWorkspaceComplete(
92
+ silPolys: Poly[],
93
+ piecePolys: Poly[]
94
+ ): boolean {
95
+ if (!silPolys.length) return false;
96
+ const Sraw = cellSetForPolys(silPolys);
97
+ const Praw = cellSetForPolys(piecePolys);
98
+ if (Sraw.size === 0 || Praw.size === 0) return false;
99
+ const S = normalizeCells(Sraw);
100
+ const P = normalizeCells(Praw);
101
+ return setsEqual(S, P);
102
+ }
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Face-to-Face Attachment Validation for Prep Mode
3
+ *
4
+ * Validates that pieces in prep mode attach to existing pieces by sharing
5
+ * at least 1 anchor unit of edge contact. Uses discrete unit segments
6
+ * for precise edge detection.
7
+ */
8
+
9
+ import type { Piece, Vec, Blueprint } from "../../domain/types";
10
+ import { CONFIG } from "../../config/config";
11
+ import { boundsOfBlueprint } from "../geometry";
12
+
13
+ export interface EdgeAttachmentResult {
14
+ isAttached: boolean;
15
+ attachedPieceIds: string[];
16
+ sharedEdgeLength: number; // In anchor units
17
+ }
18
+
19
+ export interface UnitSegment {
20
+ a: Vec;
21
+ b: Vec;
22
+ }
23
+
24
+ /**
25
+ * Convert a polygon edge into unit-length segments
26
+ * Each segment represents a discrete coordinate unit along the edge
27
+ */
28
+ function edgeToUnitSegments(start: Vec, end: Vec, gridSize: number): UnitSegment[] {
29
+ const segments: UnitSegment[] = [];
30
+
31
+ // Convert to grid coordinates
32
+ const startGrid = {
33
+ x: Math.round(start.x / gridSize),
34
+ y: Math.round(start.y / gridSize)
35
+ };
36
+ const endGrid = {
37
+ x: Math.round(end.x / gridSize),
38
+ y: Math.round(end.y / gridSize)
39
+ };
40
+
41
+ const dx = endGrid.x - startGrid.x;
42
+ const dy = endGrid.y - startGrid.y;
43
+
44
+ if (dx === 0 && dy === 0) return []; // Zero-length edge
45
+
46
+ const steps = Math.max(Math.abs(dx), Math.abs(dy));
47
+ const stepX = dx / steps;
48
+ const stepY = dy / steps;
49
+
50
+ for (let i = 0; i < steps; i++) {
51
+ const aX = Math.round(startGrid.x + i * stepX);
52
+ const aY = Math.round(startGrid.y + i * stepY);
53
+ const bX = Math.round(startGrid.x + (i + 1) * stepX);
54
+ const bY = Math.round(startGrid.y + (i + 1) * stepY);
55
+ segments.push({
56
+ a: { x: aX, y: aY },
57
+ b: { x: bX, y: bY }
58
+ });
59
+ }
60
+
61
+ return segments;
62
+ }
63
+
64
+
65
+
66
+ /**
67
+ * Get all unit segments for a piece (all edges broken down into unit-length segments)
68
+ * Uses the same coordinate system as piecePolysAt for consistency with visual rendering
69
+ */
70
+ function getPieceUnitSegments(
71
+ piece: Piece,
72
+ getBlueprint: (id: string) => Blueprint | undefined,
73
+ getPrimitive: (kind: string) => any,
74
+ gridSize: number
75
+ ): UnitSegment[] {
76
+ const blueprint = getBlueprint(piece.blueprintId);
77
+ if (!blueprint?.shape) return [];
78
+
79
+ // Calculate offset using same logic as piecePolysAt
80
+ const bb = boundsOfBlueprint(blueprint, getPrimitive);
81
+ const ox = piece.pos.x - bb.min.x;
82
+ const oy = piece.pos.y - bb.min.y;
83
+
84
+ const allSegments: UnitSegment[] = [];
85
+
86
+ // Translate polygons and convert edges to unit segments
87
+ for (const poly of blueprint.shape) {
88
+ const translatedPoly = poly.map((vertex: Vec) => ({
89
+ x: vertex.x + ox,
90
+ y: vertex.y + oy
91
+ }));
92
+
93
+ for (let i = 0; i < translatedPoly.length; i++) {
94
+ const current = translatedPoly[i];
95
+ const next = translatedPoly[(i + 1) % translatedPoly.length];
96
+
97
+ if (!current || !next) continue;
98
+
99
+ allSegments.push(...edgeToUnitSegments(current, next, gridSize));
100
+ }
101
+ }
102
+
103
+ return allSegments;
104
+ }
105
+
106
+ /**
107
+ * Main face-to-face attachment validation function
108
+ */
109
+ export function checkFaceToFaceAttachment(
110
+ piece: Piece,
111
+ existingPieces: Piece[],
112
+ getBlueprint: (id: string) => Blueprint | undefined,
113
+ getPrimitive: (kind: string) => any
114
+ ): EdgeAttachmentResult {
115
+ if (existingPieces.length === 0) {
116
+ return { isAttached: true, attachedPieceIds: [], sharedEdgeLength: 0 };
117
+ }
118
+
119
+ const gridSize = CONFIG.layout.grid.stepPx;
120
+ const newPieceSegments = getPieceUnitSegments(piece, getBlueprint, getPrimitive, gridSize);
121
+
122
+ if (newPieceSegments.length === 0) {
123
+ return { isAttached: false, attachedPieceIds: [], sharedEdgeLength: 0 };
124
+ }
125
+
126
+ let totalSharedLength = 0;
127
+ const attachedPieceIds: string[] = [];
128
+
129
+ // Check against each existing piece
130
+ for (const existingPiece of existingPieces) {
131
+ const existingSegments = getPieceUnitSegments(existingPiece, getBlueprint, getPrimitive, gridSize);
132
+
133
+ // Find shared segments (undirected edge matching)
134
+ const sharedSegments = newPieceSegments.filter(newSeg =>
135
+ existingSegments.some(existingSeg =>
136
+ // Check if segments share the same endpoints (identical or reversed)
137
+ (newSeg.a.x === existingSeg.a.x && newSeg.a.y === existingSeg.a.y &&
138
+ newSeg.b.x === existingSeg.b.x && newSeg.b.y === existingSeg.b.y) ||
139
+ (newSeg.a.x === existingSeg.b.x && newSeg.a.y === existingSeg.b.y &&
140
+ newSeg.b.x === existingSeg.a.x && newSeg.b.y === existingSeg.a.y)
141
+ )
142
+ );
143
+
144
+ if (sharedSegments.length > 0) {
145
+ totalSharedLength += sharedSegments.length;
146
+ attachedPieceIds.push(existingPiece.id);
147
+ }
148
+ }
149
+
150
+ return {
151
+ isAttached: totalSharedLength >= 1,
152
+ attachedPieceIds,
153
+ sharedEdgeLength: totalSharedLength
154
+ };
155
+ }
156
+
157
+ /**
158
+ * Check if removing a piece would disconnect the remaining pieces in a sector
159
+ * Uses graph connectivity to ensure all remaining pieces stay connected
160
+ */
161
+ export function wouldRemovalDisconnectSector(
162
+ pieceToRemove: Piece,
163
+ allPiecesInSector: Piece[],
164
+ getBlueprint: (id: string) => Blueprint | undefined,
165
+ getPrimitive: (kind: string) => any
166
+ ): boolean {
167
+ // If there are 0 or 1 pieces remaining, no disconnection possible
168
+ if (allPiecesInSector.length <= 2) return false;
169
+
170
+ const remainingPieces = allPiecesInSector.filter(p => p.id !== pieceToRemove.id);
171
+ if (remainingPieces.length <= 1) return false;
172
+
173
+ // Build adjacency graph of remaining pieces
174
+ const adjacencyMap = new Map<string, string[]>();
175
+
176
+ // Initialize adjacency map
177
+ for (const piece of remainingPieces) {
178
+ adjacencyMap.set(piece.id, []);
179
+ }
180
+
181
+ // Check all pairs of remaining pieces for connectivity
182
+ for (let i = 0; i < remainingPieces.length; i++) {
183
+ for (let j = i + 1; j < remainingPieces.length; j++) {
184
+ const piece1 = remainingPieces[i];
185
+ const piece2 = remainingPieces[j];
186
+
187
+ if (!piece1 || !piece2) continue;
188
+
189
+ const result = checkFaceToFaceAttachment(piece1, [piece2], getBlueprint, getPrimitive);
190
+ if (result.isAttached) {
191
+ adjacencyMap.get(piece1.id)!.push(piece2.id);
192
+ adjacencyMap.get(piece2.id)!.push(piece1.id);
193
+ }
194
+ }
195
+ }
196
+
197
+ // Check if graph is connected using DFS
198
+ const visited = new Set<string>();
199
+ const startPiece = remainingPieces[0];
200
+
201
+ if (!startPiece) return false;
202
+
203
+ function dfs(pieceId: string) {
204
+ if (visited.has(pieceId)) return;
205
+ visited.add(pieceId);
206
+
207
+ const neighbors = adjacencyMap.get(pieceId) || [];
208
+ for (const neighborId of neighbors) {
209
+ dfs(neighborId);
210
+ }
211
+ }
212
+
213
+ dfs(startPiece.id);
214
+
215
+ // If we visited all pieces, the graph is connected
216
+ return visited.size !== remainingPieces.length;
217
+ }
@@ -0,0 +1,3 @@
1
+ // re-export the bits you want public from "Core"
2
+ export * from "./domain/types";
3
+ export { BaseGameController } from "./engine/state/BaseGameController";