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,283 @@
1
+ /**
2
+ * Grid Snapping Engine - PHASE 2 MILESTONE
3
+ *
4
+ * Consolidates duplicate anchor functions from anchors.ts and anchors-mode.ts
5
+ * into a single, parameterized grid snapping system.
6
+ */
7
+
8
+ import type { Blueprint, Poly, Vec, PrimitiveBlueprint } from "../../domain/types";
9
+ import type { CircleLayout, SectorGeom } from "../../domain/layout";
10
+ import {
11
+ boundsOfBlueprint,
12
+ pointInPolygon,
13
+ gridNodesInAABB,
14
+ filterNodesToBandAndSector,
15
+ filterNodesInPolys
16
+ } from "../geometry";
17
+ import { CONFIG } from "../../config/config";
18
+
19
+ export interface GridConfig {
20
+ stepPx: number;
21
+ snapRadiusPx: number;
22
+ densitySampleStepPx?: number; // For polyFullyInside sampling
23
+ }
24
+
25
+ export interface SnapResult {
26
+ tl: Vec;
27
+ node: Vec | null;
28
+ dist: number;
29
+ accepted: boolean;
30
+ }
31
+
32
+ /**
33
+ * GridSnapper handles all anchor-based snapping and node generation
34
+ */
35
+ export class GridSnapper {
36
+ private config: GridConfig;
37
+
38
+ constructor(config: GridConfig) {
39
+ this.config = config;
40
+ }
41
+
42
+ // ===== Reference Vertex Management =====
43
+
44
+ /**
45
+ * Choose a reference vertex (v0 of first poly) for snapping.
46
+ * Consolidated from both anchors.ts and anchors-mode.ts
47
+ */
48
+ referenceVertex(bp: Blueprint): Vec {
49
+ const poly = ("shape" in bp && bp.shape?.[0]) ? bp.shape[0] : [{x:0,y:0}];
50
+ return poly[0] ?? { x: 0, y: 0 };
51
+ }
52
+
53
+ // ===== Snapping Operations =====
54
+
55
+ /**
56
+ * Perform nearest-node snapping with configurable snap radius
57
+ * Consolidated from both files with enhanced configuration
58
+ */
59
+ nearestNodeSnap(
60
+ tl: Vec,
61
+ bp: Blueprint,
62
+ primitiveLookup: (k: string) => PrimitiveBlueprint | undefined,
63
+ nodes: Vec[]
64
+ ): SnapResult {
65
+ if (!nodes.length) {
66
+ return { tl, node: null, dist: Infinity, accepted: false };
67
+ }
68
+
69
+ const bb = boundsOfBlueprint(bp, primitiveLookup);
70
+ const v0 = this.referenceVertex(bp);
71
+ const v0World = { x: tl.x + (v0.x - bb.min.x), y: tl.y + (v0.y - bb.min.y) };
72
+
73
+ let best: Vec | null = null;
74
+ let bestD2 = Infinity;
75
+
76
+ for (const n of nodes) {
77
+ const dx = n.x - v0World.x;
78
+ const dy = n.y - v0World.y;
79
+ const d2 = dx * dx + dy * dy;
80
+ if (d2 < bestD2) {
81
+ bestD2 = d2;
82
+ best = n;
83
+ }
84
+ }
85
+
86
+ const distance = Math.sqrt(bestD2);
87
+ const accepted = distance <= this.config.snapRadiusPx;
88
+
89
+ const snapped = (best && accepted)
90
+ ? { x: tl.x + (best.x - v0World.x), y: tl.y + (best.y - v0World.y) }
91
+ : tl;
92
+
93
+ return {
94
+ tl: snapped,
95
+ node: best,
96
+ dist: distance,
97
+ accepted
98
+ };
99
+ }
100
+
101
+ /**
102
+ * Back-compatibility shim for existing Board.tsx usage
103
+ */
104
+ snapTopLeftToNodes(
105
+ tl: Vec,
106
+ bp: Blueprint,
107
+ primitiveLookup: (k: string) => PrimitiveBlueprint | undefined,
108
+ nodes: Vec[]
109
+ ): { x: number; y: number } {
110
+ const { tl: snapped } = this.nearestNodeSnap(tl, bp, primitiveLookup, nodes);
111
+ return snapped;
112
+ }
113
+
114
+ // ===== Node Generation =====
115
+
116
+ /**
117
+ * Generate workspace-band grid nodes for a sector
118
+ * Extracted from anchors-mode.ts
119
+ */
120
+ generateWorkspaceNodes(layout: CircleLayout, sector: SectorGeom): Vec[] {
121
+ const pad = 6;
122
+ const min = { x: layout.cx - layout.outerR, y: layout.cy - layout.outerR - pad };
123
+ const max = { x: layout.cx + layout.outerR, y: layout.cy + layout.outerR + pad };
124
+ const nodes = gridNodesInAABB(min, max);
125
+ return filterNodesToBandAndSector(nodes, layout, "workspace", sector);
126
+ }
127
+
128
+ /**
129
+ * Generate silhouette-mask nodes for a sector
130
+ * Extracted from anchors-mode.ts
131
+ */
132
+ generateSilhouetteNodes(layout: CircleLayout, sector: SectorGeom, fittedMask: Poly[]): Vec[] {
133
+ const pad = 6;
134
+ const min = { x: layout.cx - layout.outerR, y: layout.cy - layout.outerR - pad };
135
+ const max = { x: layout.cx + layout.outerR, y: layout.cy + layout.outerR + pad };
136
+ const nodes = gridNodesInAABB(min, max);
137
+ const banded = filterNodesToBandAndSector(nodes, layout, "silhouette", sector);
138
+ return filterNodesInPolys(banded, fittedMask, pointInPolygon);
139
+ }
140
+
141
+ /**
142
+ * Generate silhouette-band nodes for a sector (no mask filter)
143
+ * Extracted from anchors-mode.ts
144
+ */
145
+ generateSilhouetteBandNodes(layout: CircleLayout, sector: SectorGeom): Vec[] {
146
+ const pad = 6;
147
+ const min = { x: layout.cx - layout.outerR, y: layout.cy - layout.outerR - pad };
148
+ const max = { x: layout.cx + layout.outerR, y: layout.cy + layout.outerR + pad };
149
+ const nodes = gridNodesInAABB(min, max);
150
+ return filterNodesToBandAndSector(nodes, layout, "silhouette", sector);
151
+ }
152
+
153
+ // ===== Validation Helpers =====
154
+
155
+ /**
156
+ * Check if point lies inside any silhouette polygon (union semantics)
157
+ * Private helper used by polyFullyInside
158
+ */
159
+ private insideAnySilhouette(pt: Vec, silPolys: Poly[]): boolean {
160
+ for (const sp of silPolys) {
161
+ if (pointInPolygon(pt, sp)) return true;
162
+ }
163
+ return false;
164
+ }
165
+
166
+ /**
167
+ * Robust "fully inside" check with configurable sampling density
168
+ * Consolidated from both files with enhanced configuration
169
+ */
170
+ polyFullyInside(
171
+ piecePolys: Poly[],
172
+ silhouettePolys: Poly[],
173
+ customStep?: number
174
+ ): boolean {
175
+ // Quick exits
176
+ if (!piecePolys.length) return true;
177
+ if (!silhouettePolys.length) return false;
178
+
179
+ const step = customStep ??
180
+ this.config.densitySampleStepPx ??
181
+ Math.max(1, Math.round(CONFIG.layout.grid.stepPx / 3));
182
+
183
+ for (const poly of piecePolys) {
184
+ if (poly.length < 2) continue;
185
+
186
+ for (let i = 0; i < poly.length; i++) {
187
+ const a = poly[i];
188
+ const b = poly[(i + 1) % poly.length];
189
+
190
+ if (!a || !b) continue;
191
+
192
+ const dx = b.x - a.x;
193
+ const dy = b.y - a.y;
194
+ const len = Math.hypot(dx, dy);
195
+ const n = Math.max(1, Math.ceil(len / step));
196
+
197
+ // Sample all points along the edge, including endpoints
198
+ for (let k = 0; k <= n; k++) {
199
+ const t = k / n;
200
+ const p = { x: a.x + t * dx, y: a.y + t * dy };
201
+ if (!this.insideAnySilhouette(p, silhouettePolys)) {
202
+ return false; // One point outside → reject
203
+ }
204
+ }
205
+ }
206
+ }
207
+ return true;
208
+ }
209
+ }
210
+
211
+ // ===== Factory Functions =====
212
+
213
+ /**
214
+ * Create a GridSnapper with default configuration
215
+ */
216
+ export function createDefaultGridSnapper(): GridSnapper {
217
+ return new GridSnapper({
218
+ stepPx: CONFIG.layout.grid.stepPx,
219
+ snapRadiusPx: CONFIG.game.snapRadiusPx,
220
+ densitySampleStepPx: Math.max(1, Math.round(CONFIG.layout.grid.stepPx / 3))
221
+ });
222
+ }
223
+
224
+ /**
225
+ * Create a GridSnapper with custom snap radius
226
+ */
227
+ export function createGridSnapper(snapRadiusPx: number): GridSnapper {
228
+ return new GridSnapper({
229
+ stepPx: CONFIG.layout.grid.stepPx,
230
+ snapRadiusPx,
231
+ densitySampleStepPx: Math.max(1, Math.round(CONFIG.layout.grid.stepPx / 3))
232
+ });
233
+ }
234
+
235
+ // ===== Legacy Compatibility Exports =====
236
+ // These maintain compatibility with existing code while using the new system
237
+
238
+ export const defaultGridSnapper = createDefaultGridSnapper();
239
+
240
+ export function referenceVertex(bp: Blueprint): Vec {
241
+ return defaultGridSnapper.referenceVertex(bp);
242
+ }
243
+
244
+ export function nearestNodeSnap(
245
+ tl: Vec,
246
+ bp: Blueprint,
247
+ primitiveLookup: (k: string) => PrimitiveBlueprint | undefined,
248
+ nodes: Vec[]
249
+ ): { tl: Vec; node: Vec | null; dist: number } {
250
+ const result = defaultGridSnapper.nearestNodeSnap(tl, bp, primitiveLookup, nodes);
251
+ return { tl: result.tl, node: result.node, dist: result.dist };
252
+ }
253
+
254
+ export function snapTopLeftToNodes(
255
+ tl: Vec,
256
+ bp: Blueprint,
257
+ primitiveLookup: (k: string) => PrimitiveBlueprint | undefined,
258
+ nodes: Vec[]
259
+ ): { x: number; y: number } {
260
+ return defaultGridSnapper.snapTopLeftToNodes(tl, bp, primitiveLookup, nodes);
261
+ }
262
+
263
+ export function polyFullyInside(
264
+ piecePolys: Poly[],
265
+ silhouettePolys: Poly[],
266
+ step?: number
267
+ ): boolean {
268
+ return defaultGridSnapper.polyFullyInside(piecePolys, silhouettePolys, step);
269
+ }
270
+
271
+ export function workspaceNodes(layout: CircleLayout, sector: SectorGeom): Vec[] {
272
+ return defaultGridSnapper.generateWorkspaceNodes(layout, sector);
273
+ }
274
+
275
+ export function silhouetteNodes(layout: CircleLayout, sector: SectorGeom, fittedMask: Poly[]): Vec[] {
276
+ return defaultGridSnapper.generateSilhouetteNodes(layout, sector, fittedMask);
277
+ }
278
+
279
+ export function silhouetteBandNodes(layout: CircleLayout, sector: SectorGeom): Vec[] {
280
+ return defaultGridSnapper.generateSilhouetteBandNodes(layout, sector);
281
+ }
282
+
283
+ export { innerRingNodes } from "../geometry";
@@ -0,0 +1,4 @@
1
+ // Central exports for collision detection engine
2
+ export { convexIntersects } from './sat-collision';
3
+ export { GridSnapper, createGridSnapper } from './grid-snapping';
4
+ export { ValidationEngine } from './validation';
@@ -0,0 +1,46 @@
1
+ // Separating Axis Theorem (SAT) collision detection
2
+ import type { Poly } from '@/core/domain/types';
3
+ import { project, polygonAABB } from '../geometry/math';
4
+
5
+ const SEP_EPS = 1e-6; // allow touching within this tolerance
6
+
7
+ /** SAT for convex polygons. Edges/vertices touching are allowed (non-overlap). */
8
+ export function convexIntersects(a: Poly, b: Poly): boolean {
9
+ const polys = [a, b];
10
+ for (const poly of polys) {
11
+ for (let i = 0; i < poly.length; i++) {
12
+ const p0 = poly[i];
13
+ const p1 = poly[(i + 1) % poly.length];
14
+ if (!p0 || !p1) continue;
15
+ const ex = p1.x - p0.x, ey = p1.y - p0.y;
16
+ const nx = -ey, ny = ex; // outward normal
17
+ const pa = project(a, nx, ny);
18
+ const pb = project(b, nx, ny);
19
+ // Treat touching (zero overlap) as separated: allow edges/vertices to touch.
20
+ if (pa.max <= pb.min + SEP_EPS || pb.max <= pa.min + SEP_EPS) return false;
21
+ }
22
+ }
23
+ return true;
24
+ }
25
+
26
+ /** True if any poly from A overlaps any poly from B (fast AABB short-circuit). */
27
+ export function polysOverlap(aPolys: Poly[], bPolys: Poly[]): boolean {
28
+ // quick AABB test to skip obvious separations
29
+ const aabbsA = aPolys.map(polygonAABB);
30
+ const aabbsB = bPolys.map(polygonAABB);
31
+
32
+ for (let i = 0; i < aPolys.length; i++) {
33
+ for (let j = 0; j < bPolys.length; j++) {
34
+ const A = aPolys[i], B = bPolys[j];
35
+ const aa = aabbsA[i], bb = aabbsB[j];
36
+ if (!aa || !bb) continue;
37
+ if (aa.maxX <= bb.minX + SEP_EPS || bb.maxX <= aa.minX + SEP_EPS ||
38
+ aa.maxY <= bb.minY + SEP_EPS || bb.maxY <= aa.minY + SEP_EPS) continue;
39
+ if (!A || !B) continue;
40
+ if (convexIntersects(A, B)) return true;
41
+ }
42
+ }
43
+ return false;
44
+ }
45
+
46
+ export { SEP_EPS };
@@ -0,0 +1,166 @@
1
+ /**
2
+ * ValidationEngine - Anchor-based validation system
3
+ *
4
+ * Provides grid cell-based validation for tangram completion checking.
5
+ * Uses anchor grid alignment to determine if pieces correctly fill target shapes.
6
+ */
7
+
8
+ import type { Poly } from "../../domain/types";
9
+ import { GridSnapper, createGridSnapper } from "./grid-snapping";
10
+ import { anchorsSilhouetteComplete, anchorsWorkspaceComplete } from "../validation/complete";
11
+
12
+ export interface ValidationConfig {
13
+ snapRadiusPx: number;
14
+ densitySampleStepPx?: number;
15
+ }
16
+
17
+ export interface ValidationResult {
18
+ completed: boolean;
19
+ confidence: number; // 0-1 scale
20
+ details?: {
21
+ piecesInside: number;
22
+ totalPieces: number;
23
+ anchorMatches?: number;
24
+ };
25
+ }
26
+
27
+ /**
28
+ * ValidationEngine handles all collision detection and completion checking
29
+ * using grid cell-based anchor validation.
30
+ */
31
+ export class ValidationEngine {
32
+ private gridSnapper: GridSnapper;
33
+ private config: ValidationConfig;
34
+
35
+ constructor(config: ValidationConfig) {
36
+ this.config = config;
37
+ this.gridSnapper = createGridSnapper(config.snapRadiusPx);
38
+ }
39
+
40
+ // ===== Primary Validation Methods =====
41
+
42
+ /**
43
+ * Check if silhouette is complete using anchor-based cell validation.
44
+ * Uses subset check: silhouette cells ⊆ piece cells
45
+ */
46
+ checkSilhouetteComplete(silPolys: Poly[], piecePolys: Poly[]): ValidationResult {
47
+ const completed = anchorsSilhouetteComplete(silPolys, piecePolys);
48
+
49
+ return {
50
+ completed,
51
+ confidence: completed ? 1.0 : this.calculatePartialCompletionScore(silPolys, piecePolys),
52
+ details: {
53
+ piecesInside: this.countPiecesInside(piecePolys, silPolys),
54
+ totalPieces: piecePolys.length
55
+ }
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Check if workspace is complete using anchor-based cell validation.
61
+ * Uses exact match: silhouette cells === piece cells (translation-invariant)
62
+ */
63
+ checkWorkspaceComplete(silPolys: Poly[], piecePolys: Poly[]): ValidationResult {
64
+ const completed = anchorsWorkspaceComplete(silPolys, piecePolys);
65
+
66
+ return {
67
+ completed,
68
+ confidence: completed ? 1.0 : this.calculatePartialCompletionScore(silPolys, piecePolys),
69
+ details: {
70
+ piecesInside: this.countPiecesInside(piecePolys, silPolys),
71
+ totalPieces: piecePolys.length
72
+ }
73
+ };
74
+ }
75
+
76
+ // ===== Geometric Validation =====
77
+
78
+ /**
79
+ * Check if piece polygons are fully inside silhouette polygons
80
+ * Uses the consolidated GridSnapper polyFullyInside method
81
+ */
82
+ checkPiecesFullyInside(piecePolys: Poly[], silhouettePolys: Poly[]): boolean {
83
+ return this.gridSnapper.polyFullyInside(piecePolys, silhouettePolys);
84
+ }
85
+
86
+ /**
87
+ * Check if pieces overlap with each other (collision detection)
88
+ * TODO: Implement SAT collision detection when needed
89
+ */
90
+ checkPieceOverlap(_pieces1: Poly[], _pieces2: Poly[]): boolean {
91
+ // Placeholder - will use SAT collision detection from sat-collision.ts
92
+ return false;
93
+ }
94
+
95
+ // ===== Helper Methods =====
96
+
97
+ /**
98
+ * Calculate partial completion score for progress tracking
99
+ * Returns value between 0-1 based on how much is correctly placed
100
+ */
101
+ private calculatePartialCompletionScore(silPolys: Poly[], piecePolys: Poly[]): number {
102
+ if (!piecePolys.length) return 0;
103
+
104
+ const piecesInside = this.countPiecesInside(piecePolys, silPolys);
105
+ return piecesInside / piecePolys.length;
106
+ }
107
+
108
+ /**
109
+ * Count how many piece polygons are fully inside silhouette
110
+ */
111
+ private countPiecesInside(piecePolys: Poly[], silPolys: Poly[]): number {
112
+ let count = 0;
113
+ for (const piece of piecePolys) {
114
+ if (this.gridSnapper.polyFullyInside([piece], silPolys)) {
115
+ count++;
116
+ }
117
+ }
118
+ return count;
119
+ }
120
+
121
+ // ===== Configuration Access =====
122
+
123
+ getSnapRadius(): number {
124
+ return this.config.snapRadiusPx;
125
+ }
126
+
127
+ getGridSnapper(): GridSnapper {
128
+ return this.gridSnapper;
129
+ }
130
+
131
+ // ===== Static Factory Methods =====
132
+
133
+ static create(snapRadiusPx: number): ValidationEngine {
134
+ return new ValidationEngine({
135
+ snapRadiusPx,
136
+ });
137
+ }
138
+ }
139
+
140
+ // ===== Default Instance =====
141
+
142
+ /**
143
+ * Default validation engine instance for backward compatibility
144
+ */
145
+ export const defaultValidationEngine = ValidationEngine.create(18); // Default snap radius
146
+
147
+ // ===== Legacy Compatibility Exports =====
148
+ // These maintain compatibility with existing Board.tsx usage
149
+
150
+ export function createValidationEngine(snapRadiusPx: number = 18): ValidationEngine {
151
+ return ValidationEngine.create(snapRadiusPx);
152
+ }
153
+
154
+ /**
155
+ * Legacy function that Board.tsx expects - delegates to ValidationEngine
156
+ */
157
+ export function checkSilhouetteComplete(silPolys: Poly[], piecePolys: Poly[]): boolean {
158
+ return defaultValidationEngine.checkSilhouetteComplete(silPolys, piecePolys).completed;
159
+ }
160
+
161
+ /**
162
+ * Legacy function that Board.tsx expects - delegates to ValidationEngine
163
+ */
164
+ export function checkWorkspaceComplete(silPolys: Poly[], piecePolys: Poly[]): boolean {
165
+ return defaultValidationEngine.checkWorkspaceComplete(silPolys, piecePolys).completed;
166
+ }
@@ -0,0 +1,91 @@
1
+ import type { Poly, Vec, PrimitiveBlueprint, CompositeBlueprint, Blueprint } from "@/core/domain/types";
2
+
3
+ export type AABB = { min: Vec; max: Vec; width: number; height: number };
4
+
5
+ /** AABB of a single polygon (numeric). */
6
+ export function aabbOf(poly: Poly): { minX: number; minY: number; maxX: number; maxY: number } {
7
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
8
+ for (const p of poly) {
9
+ if (p.x < minX) minX = p.x;
10
+ if (p.y < minY) minY = p.y;
11
+ if (p.x > maxX) maxX = p.x;
12
+ if (p.y > maxY) maxY = p.y;
13
+ }
14
+ return { minX, minY, maxX, maxY };
15
+ }
16
+
17
+ /** AABB of one or more polygons with width/height and center. */
18
+ export function polysAABB(polys: Poly[]) {
19
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
20
+ for (const poly of polys) {
21
+ for (const p of poly) {
22
+ if (p.x < minX) minX = p.x;
23
+ if (p.y < minY) minY = p.y;
24
+ if (p.x > maxX) maxX = p.x;
25
+ if (p.y > maxY) maxY = p.y;
26
+ }
27
+ }
28
+ const width = Math.max(1, maxX - minX);
29
+ const height = Math.max(1, maxY - minY);
30
+ return { min: { x: minX, y: minY }, max: { x: maxX, y: maxY }, width, height, cx:(minX+maxX)/2, cy:(minY+maxY)/2 };
31
+ }
32
+
33
+ /** Bounds of multiple polygons at the origin. */
34
+ export function boundsOfShapes(shapes: Poly[]): AABB {
35
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
36
+ for (const poly of shapes) {
37
+ for (const p of poly) {
38
+ if (p.x < minX) minX = p.x;
39
+ if (p.y < minY) minY = p.y;
40
+ if (p.x > maxX) maxX = p.x;
41
+ if (p.y > maxY) maxY = p.y;
42
+ }
43
+ }
44
+ const width = Math.max(0, maxX - minX);
45
+ const height = Math.max(0, maxY - minY);
46
+ return { min: { x: minX, y: minY }, max: { x: maxX, y: maxY }, width, height };
47
+ }
48
+
49
+ /** Bounds of a primitive blueprint (uses its canonical shape). */
50
+ export function boundsOfPrimitive(bp: PrimitiveBlueprint): AABB {
51
+ return boundsOfShapes(bp.shape);
52
+ }
53
+
54
+ /**
55
+ * Bounds of a composite blueprint.
56
+ * If `shape` is provided as precomputed union polygons, we use that.
57
+ * Otherwise we bound the parts’ primitives at their offsets (loose but safe).
58
+ */
59
+ export function boundsOfComposite(
60
+ bp: CompositeBlueprint,
61
+ primitiveLookup: (kind: string) => PrimitiveBlueprint | undefined
62
+ ): AABB {
63
+ if (bp.shape && bp.shape.length) return boundsOfShapes(bp.shape);
64
+
65
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
66
+ for (const part of bp.parts) {
67
+ const prim = primitiveLookup(part.kind);
68
+ if (!prim) continue;
69
+ const b = boundsOfPrimitive(prim);
70
+ const pMinX = part.offset.x + b.min.x;
71
+ const pMinY = part.offset.y + b.min.y;
72
+ const pMaxX = part.offset.x + b.max.x;
73
+ const pMaxY = part.offset.y + b.max.y;
74
+ if (pMinX < minX) minX = pMinX;
75
+ if (pMinY < minY) minY = pMinY;
76
+ if (pMaxX > maxX) maxX = pMaxX;
77
+ if (pMaxY > maxY) maxY = pMaxY;
78
+ }
79
+ const width = Math.max(0, maxX - minX);
80
+ const height = Math.max(0, maxY - minY);
81
+ return { min: { x: minX, y: minY }, max: { x: maxX, y: maxY }, width, height };
82
+ }
83
+
84
+ /** Unified bounds helper. */
85
+ export function boundsOfBlueprint(
86
+ bp: Blueprint,
87
+ primitiveLookup: (kind: string) => PrimitiveBlueprint | undefined
88
+ ): AABB {
89
+ if ("parts" in bp) return boundsOfComposite(bp, primitiveLookup);
90
+ return boundsOfPrimitive(bp);
91
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * SAT (Separating Axis Theorem) collision detection for convex polygons
3
+ * Extracted from polygon.ts for better organization
4
+ */
5
+
6
+ import type { Poly } from '@/core/domain/types';
7
+ import { project } from './math';
8
+
9
+ const SEP_EPS = 1e-6; // allow touching within this tolerance
10
+
11
+ /** SAT for convex polygons. Edges/vertices touching are allowed (non-overlap). */
12
+ export function convexIntersects(a: Poly, b: Poly): boolean {
13
+ const polys = [a, b];
14
+ for (const poly of polys) {
15
+ for (let i = 0; i < poly.length; i++) {
16
+ const p0 = poly[i], p1 = poly[(i + 1) % poly.length];
17
+ if (!p0 || !p1) continue;
18
+ const ex = p1.x - p0.x, ey = p1.y - p0.y;
19
+ const nx = -ey, ny = ex; // outward normal
20
+ const pa = project(a, nx, ny);
21
+ const pb = project(b, nx, ny);
22
+ // Treat touching (zero overlap) as separated: allow edges/vertices to touch.
23
+ if (pa.max <= pb.min + SEP_EPS || pb.max <= pa.min + SEP_EPS) return false;
24
+ }
25
+ }
26
+ return true;
27
+ }
28
+
29
+ /** True if any poly from A overlaps any poly from B (fast AABB short-circuit). */
30
+ export function polysOverlap(aPolys: Poly[], bPolys: Poly[]): boolean {
31
+ // quick AABB test to skip obvious separations
32
+ const aabbsA = aPolys.map(p => {
33
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
34
+ for (const pt of p) {
35
+ if (pt.x < minX) minX = pt.x;
36
+ if (pt.y < minY) minY = pt.y;
37
+ if (pt.x > maxX) maxX = pt.x;
38
+ if (pt.y > maxY) maxY = pt.y;
39
+ }
40
+ return { minX, minY, maxX, maxY };
41
+ });
42
+ const aabbsB = bPolys.map(p => {
43
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
44
+ for (const pt of p) {
45
+ if (pt.x < minX) minX = pt.x;
46
+ if (pt.y < minY) minY = pt.y;
47
+ if (pt.x > maxX) maxX = pt.x;
48
+ if (pt.y > maxY) maxY = pt.y;
49
+ }
50
+ return { minX, minY, maxX, maxY };
51
+ });
52
+
53
+ for (let i = 0; i < aPolys.length; i++) {
54
+ for (let j = 0; j < bPolys.length; j++) {
55
+ const A = aPolys[i], B = bPolys[j];
56
+ const aa = aabbsA[i], bb = aabbsB[j];
57
+ if (!A || !B || !aa || !bb) continue;
58
+ if (aa.maxX <= bb.minX + SEP_EPS || bb.maxX <= aa.minX + SEP_EPS ||
59
+ aa.maxY <= bb.minY + SEP_EPS || bb.maxY <= aa.minY + SEP_EPS) continue;
60
+ if (convexIntersects(A, B)) return true;
61
+ }
62
+ }
63
+ return false;
64
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Geometry module barrel exports
3
+ * Consolidated from 10 files down to 5 for better organization
4
+ */
5
+
6
+ // Pure mathematical operations (vectors, polygons)
7
+ export * from "./math";
8
+
9
+ // SAT collision detection
10
+ export * from "./collision";
11
+
12
+ // Polygon operations (subdivision, etc.)
13
+ export * from "./polygons";
14
+
15
+ // Bounding box calculations
16
+ export * from "./bounds";
17
+
18
+ // Game-specific piece geometry (positioning, placement, grid, anchors)
19
+ export * from "./pieces";