jspsych-tangram 0.0.7 → 0.0.9

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 (40) hide show
  1. package/dist/construct/index.browser.js +187 -214
  2. package/dist/construct/index.browser.js.map +1 -1
  3. package/dist/construct/index.browser.min.js +13 -10
  4. package/dist/construct/index.browser.min.js.map +1 -1
  5. package/dist/construct/index.cjs +187 -214
  6. package/dist/construct/index.cjs.map +1 -1
  7. package/dist/construct/index.d.ts +24 -0
  8. package/dist/construct/index.js +187 -214
  9. package/dist/construct/index.js.map +1 -1
  10. package/dist/index.cjs +198 -215
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.ts +36 -0
  13. package/dist/index.js +198 -215
  14. package/dist/index.js.map +1 -1
  15. package/dist/prep/index.browser.js +173 -213
  16. package/dist/prep/index.browser.js.map +1 -1
  17. package/dist/prep/index.browser.min.js +14 -11
  18. package/dist/prep/index.browser.min.js.map +1 -1
  19. package/dist/prep/index.cjs +173 -213
  20. package/dist/prep/index.cjs.map +1 -1
  21. package/dist/prep/index.d.ts +12 -0
  22. package/dist/prep/index.js +173 -213
  23. package/dist/prep/index.js.map +1 -1
  24. package/package.json +1 -1
  25. package/src/assets/README.md +6 -0
  26. package/src/assets/images.d.ts +19 -0
  27. package/src/assets/locked.png +0 -0
  28. package/src/assets/unlocked.png +0 -0
  29. package/src/core/components/board/BoardView.tsx +121 -39
  30. package/src/core/components/board/GameBoard.tsx +123 -7
  31. package/src/core/config/config.ts +8 -4
  32. package/src/core/domain/types.ts +1 -0
  33. package/src/core/io/InteractionTracker.ts +23 -24
  34. package/src/core/io/data-tracking.ts +6 -7
  35. package/src/plugins/tangram-construct/ConstructionApp.tsx +16 -1
  36. package/src/plugins/tangram-construct/index.ts +14 -0
  37. package/src/plugins/tangram-prep/PrepApp.tsx +6 -0
  38. package/src/plugins/tangram-prep/index.ts +7 -0
  39. package/tangram-construct.min.js +13 -10
  40. package/tangram-prep.min.js +14 -11
@@ -16669,7 +16669,8 @@ var TangramConstructPlugin = (function (jspsych) {
16669
16669
  anchors: { invalid: "#7dd3fc", valid: "#475569" },
16670
16670
  piece: { draggingFill: "#8e7cc3ff", validFill: "#8e7cc3ff", invalidFill: "#ef4444", invalidStroke: "#dc2626", selectedStroke: "#674ea7", allGreenStroke: "#86efac", borderStroke: "#674ea7" },
16671
16671
  ui: { light: "#60a5fa", dark: "#1d4ed8" },
16672
- blueprint: { fill: "#374151", selectedStroke: "#111827", badgeFill: "#000000", labelFill: "#ffffff" }
16672
+ blueprint: { fill: "#374151", selectedStroke: "#111827", badgeFill: "#000000", labelFill: "#ffffff" },
16673
+ tangramDecomposition: { stroke: "#fef2cc" }
16673
16674
  },
16674
16675
  opacity: {
16675
16676
  blueprint: 0.4,
@@ -16679,7 +16680,7 @@ var TangramConstructPlugin = (function (jspsych) {
16679
16680
  piece: { invalid: 0.35, dragging: 0.75, locked: 1, normal: 1 }
16680
16681
  },
16681
16682
  size: {
16682
- stroke: { bandPx: 5, pieceSelectedPx: 3, allGreenStrokePx: 10, pieceBorderPx: 2 },
16683
+ stroke: { bandPx: 5, pieceSelectedPx: 3, allGreenStrokePx: 10, pieceBorderPx: 2, tangramDecompositionPx: 1 },
16683
16684
  anchorRadiusPx: { valid: 1, invalid: 1 },
16684
16685
  badgeFontPx: 16,
16685
16686
  centerBadge: { fractionOfOuterR: 0.15, minPx: 20, marginPx: 4 }
@@ -16687,6 +16688,7 @@ var TangramConstructPlugin = (function (jspsych) {
16687
16688
  layout: {
16688
16689
  grid: { stepPx: 20, unitPx: 40 },
16689
16690
  paddingPx: 1,
16691
+ viewportScale: 0.8,
16690
16692
  constraints: {
16691
16693
  workspaceDiamAnchors: 10,
16692
16694
  // num anchors req'd to be on diagonal
@@ -16698,7 +16700,7 @@ var TangramConstructPlugin = (function (jspsych) {
16698
16700
  },
16699
16701
  game: {
16700
16702
  snapRadiusPx: 15,
16701
- showBorders: true,
16703
+ showBorders: false,
16702
16704
  hideTouchingBorders: true
16703
16705
  }
16704
16706
  };
@@ -17469,152 +17471,17 @@ var TangramConstructPlugin = (function (jspsych) {
17469
17471
  return { cx, cy, w, h };
17470
17472
  }
17471
17473
 
17472
- function generateEdgeStrokePaths(poly) {
17473
- const strokePaths = [];
17474
- for (let i = 0; i < poly.length; i++) {
17475
- const current = poly[i];
17476
- const next = poly[(i + 1) % poly.length];
17477
- if (current && next) {
17478
- strokePaths.push(`M ${current.x} ${current.y} L ${next.x} ${next.y}`);
17479
- }
17480
- }
17481
- return strokePaths;
17482
- }
17483
- function edgeToUnitSegments$1(start, end, gridSize) {
17484
- const segments = [];
17485
- const startGrid = {
17486
- x: Math.round(start.x / gridSize),
17487
- y: Math.round(start.y / gridSize)
17488
- };
17489
- const endGrid = {
17490
- x: Math.round(end.x / gridSize),
17491
- y: Math.round(end.y / gridSize)
17492
- };
17493
- const dx = endGrid.x - startGrid.x;
17494
- const dy = endGrid.y - startGrid.y;
17495
- if (dx === 0 && dy === 0) return [];
17496
- const steps = Math.max(Math.abs(dx), Math.abs(dy));
17497
- const stepX = dx / steps;
17498
- const stepY = dy / steps;
17499
- for (let i = 0; i < steps; i++) {
17500
- const aX = Math.round(startGrid.x + i * stepX);
17501
- const aY = Math.round(startGrid.y + i * stepY);
17502
- const bX = Math.round(startGrid.x + (i + 1) * stepX);
17503
- const bY = Math.round(startGrid.y + (i + 1) * stepY);
17504
- segments.push({
17505
- a: { x: aX, y: aY },
17506
- b: { x: bX, y: bY }
17507
- });
17508
- }
17509
- return segments;
17510
- }
17511
- function getHiddenEdgesForPolygon(piece, polyIndex, allPiecesInSector, getBlueprint, getPrimitive) {
17512
- let blueprint;
17513
- try {
17514
- blueprint = getBlueprint(piece.blueprintId);
17515
- } catch (error) {
17516
- console.warn("getBlueprint failed in getHiddenEdgesForPolygon:", error);
17517
- return [];
17518
- }
17519
- if (!blueprint?.shape) {
17520
- return [];
17521
- }
17522
- const poly = blueprint.shape[polyIndex];
17523
- if (!poly) return [];
17524
- const gridSize = CONFIG.layout.grid.stepPx;
17525
- const bb = boundsOfBlueprint(blueprint, getPrimitive);
17526
- const ox = piece.pos.x - bb.min.x;
17527
- const oy = piece.pos.y - bb.min.y;
17528
- const translatedPoly = poly.map((vertex) => ({
17529
- x: vertex.x + ox,
17530
- y: vertex.y + oy
17531
- }));
17532
- const hiddenEdges = [];
17533
- for (let i = 0; i < translatedPoly.length; i++) {
17534
- const current = translatedPoly[i];
17535
- const next = translatedPoly[(i + 1) % translatedPoly.length];
17536
- if (!current || !next) {
17537
- hiddenEdges.push(false);
17538
- continue;
17539
- }
17540
- const edgeSegments = edgeToUnitSegments$1(current, next, gridSize);
17541
- let isTouching = false;
17542
- if (piece.blueprintId.startsWith("comp:")) {
17543
- for (let otherPolyIndex = 0; otherPolyIndex < blueprint.shape.length; otherPolyIndex++) {
17544
- if (otherPolyIndex === polyIndex) continue;
17545
- const otherPoly = blueprint.shape[otherPolyIndex];
17546
- if (!otherPoly) continue;
17547
- const otherTranslatedPoly = otherPoly.map((vertex) => ({
17548
- x: vertex.x + ox,
17549
- y: vertex.y + oy
17550
- }));
17551
- for (let j = 0; j < otherTranslatedPoly.length; j++) {
17552
- const otherCurrent = otherTranslatedPoly[j];
17553
- const otherNext = otherTranslatedPoly[(j + 1) % otherTranslatedPoly.length];
17554
- if (!otherCurrent || !otherNext) continue;
17555
- const otherEdgeSegments = edgeToUnitSegments$1(otherCurrent, otherNext, gridSize);
17556
- for (const edgeSeg of edgeSegments) {
17557
- const isShared = otherEdgeSegments.some(
17558
- (otherSeg) => (
17559
- // Check if segments share the same endpoints (identical or reversed)
17560
- edgeSeg.a.x === otherSeg.a.x && edgeSeg.a.y === otherSeg.a.y && edgeSeg.b.x === otherSeg.b.x && edgeSeg.b.y === otherSeg.b.y || edgeSeg.a.x === otherSeg.b.x && edgeSeg.a.y === otherSeg.b.y && edgeSeg.b.x === otherSeg.a.x && edgeSeg.b.y === otherSeg.a.y
17561
- )
17562
- );
17563
- if (isShared) {
17564
- isTouching = true;
17565
- break;
17566
- }
17567
- }
17568
- if (isTouching) break;
17569
- }
17570
- if (isTouching) break;
17571
- }
17572
- }
17573
- if (!isTouching && CONFIG.game.hideTouchingBorders) {
17574
- for (const otherPiece of allPiecesInSector) {
17575
- if (otherPiece.id === piece.id) continue;
17576
- const otherBlueprint = getBlueprint(otherPiece.blueprintId);
17577
- if (!otherBlueprint?.shape) continue;
17578
- const otherBb = boundsOfBlueprint(otherBlueprint, getPrimitive);
17579
- const otherOx = otherPiece.pos.x - otherBb.min.x;
17580
- const otherOy = otherPiece.pos.y - otherBb.min.y;
17581
- for (const otherPoly of otherBlueprint.shape) {
17582
- const otherTranslatedPoly = otherPoly.map((vertex) => ({
17583
- x: vertex.x + otherOx,
17584
- y: vertex.y + otherOy
17585
- }));
17586
- for (let j = 0; j < otherTranslatedPoly.length; j++) {
17587
- const otherCurrent = otherTranslatedPoly[j];
17588
- const otherNext = otherTranslatedPoly[(j + 1) % otherTranslatedPoly.length];
17589
- if (!otherCurrent || !otherNext) continue;
17590
- const otherEdgeSegments = edgeToUnitSegments$1(otherCurrent, otherNext, gridSize);
17591
- for (const edgeSeg of edgeSegments) {
17592
- const isShared = otherEdgeSegments.some(
17593
- (otherSeg) => (
17594
- // Check if segments share the same endpoints (identical or reversed)
17595
- edgeSeg.a.x === otherSeg.a.x && edgeSeg.a.y === otherSeg.a.y && edgeSeg.b.x === otherSeg.b.x && edgeSeg.b.y === otherSeg.b.y || edgeSeg.a.x === otherSeg.b.x && edgeSeg.a.y === otherSeg.b.y && edgeSeg.b.x === otherSeg.a.x && edgeSeg.b.y === otherSeg.a.y
17596
- )
17597
- );
17598
- if (isShared) {
17599
- isTouching = true;
17600
- break;
17601
- }
17602
- }
17603
- if (isTouching) break;
17604
- }
17605
- if (isTouching) break;
17606
- }
17607
- if (isTouching) break;
17608
- }
17609
- }
17610
- hiddenEdges.push(isTouching);
17611
- }
17612
- return hiddenEdges;
17474
+ function shouldShowBorders() {
17475
+ return CONFIG.game.showBorders;
17613
17476
  }
17614
17477
  function shouldUseSelectiveBorders(blueprintId) {
17615
- return (CONFIG.game.hideTouchingBorders);
17478
+ return CONFIG.game.showBorders;
17616
17479
  }
17617
17480
 
17481
+ var lockedIcon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFoAAABaCAYAAAA4qEECAAAACXBIWXMAAAsSAAALEgHS3X78AAAFY0lEQVR4nO2d7XXrNgyGH/r0f7zB9QbXnaDqBt6gvhM0I2SE3A2UDXwnqLKBPEHdDewJ0B+kYieWZEoiQfrjPQcnjqOI0GuYAAFQMiJCLjDGLIEFsHQyB/7w/Pd397MCamAnInVgFUfDpCTaGLMACmDlfj4FHuKAJX4DVCKyC3x+b6gTbYyZA2sn31UHhy1QAqWI7FVHFhEVwVpsCUgmUgKF2vUrEVxlQGyXVBqE3zPBqoTHIHhOXlPEUCmBedZEY6OHfQZkTZU9sMqO6Buw4ujWHYLkJXaBkJqUWFIDy6REYx3eLUwVl2TPREc5heR1BgRoy1qV6DsleRLZD5KVyH6QrET2UMeX+uJyE28H6UvykvuILobKHs/Qz4fkObcdJ0+VGo9FzcV8tDGmBP7qPSg+DtgLqrFWBLYSs8C/AhMTbyKy7j3igjWvSPu1LPH4ajo9y4S6ChdyI5emjFTz8isjcgxYC98kNIxOnfuUTmEhk5e6Tvd1IiMpBxFNmlDOy6kMIDtVpNRqKK3O0BhToetk/sPOxZ0FU1cxX2CNAGxFZCc9lW3XvlARvrreh3cRKc7ezcSaOx0e1tH1hZc1PY6INA79zKrbFKuUlXrpccZDdKnomHqwzlXzmqpeotG35l0PyWMWSa3zPGkiqKKP6FJZmXUH0VVIa0pk1WUr0e5T11REaLe+dYwPEOtIk13fqSLPykp0Wd4uwLm7piTtnM1zM/aMI9boovr6hgvHvgU49zdjTNHyvnZ36bp5MYOPGFW74bDtoouA52871y7g+X3w3XH7YdGFsgJwzMKdYh55zCry+dtQwJHoVQIF7gUrSGvR94ICYOYckGYu4N7wZIxZzLDx5QNxsZxh04kPxMUS9JNINfBChB7kS4KNal4JsygaIhtNooNUTwKS/oxeoqky7kVsbB3JujuhLkCzMKBF9O+S0ebKU7il+j+xx5ldPmQyfuZKMoCIVMCv2ONoEP2qMMZUlLEHiD11HEQkdv4iCIwxUafQ2Bad7ZShjd9SK+CLkz3kTQJsQ4o93RMQNX4MFPN2NcN4t816jBE1ltZwhpPgLLmiPdZ9Aip3TNbInmjsdNG3oHhCvww3GDOOd27JFT5FidwLF+8z2ktKD4TFfkb+Idgm0DEpUV8L0Yeevx94ED0dYttyC9rJPmCzgjtFlcagnjkl+ywmOVxSasHn5M8vYJFzwsrhICK7JryrUmriA7cCPCW1vpJVYQXHODr3Oe6asYEj0VU6PW4eFTii3Ty9TajMrWLbOOrTJXiZRJXbRtm8eBAdF2Xz4oNo58HfUmhzo3g7jYq+Zu9KXV0Go+54nSPKT7+1JMArwiW86wiNL2sm3ERKKfF/VvA42zkbus9BREyoc8WC68r/N+Ap/3RtDB84S/y7A4LlqI0xueeKIWx/+PtXkoHzqeOkRpdV3TCmELbpsbWG2Td4yA2QwefUgCS/BLzO185xehSYB/6ksyObsLeX2zHmxihOkdB3Biixqc3UBC8If6eaom9Mn5tXvQJ/9x40HFtsGKmd5pxjHV/oPZU/ReS57wCvp1YYY2r0N3xeC7YicnF7im9fx4rMqzCJcMCz1cGLaDnW7R74jEI865XenUpia3M/xmp0g/ghQ+qViUOia5XBoWoO8ee1yaj1QC7B/rXIKJK94ug+JLqvXAo0jTqjc+CT2nbdwEtuu7C7xSaKphUaAi1pm62/qb/aoWXUzWiDztEdhD8e4aRB9I1YdzArjkr0CeGNo0xNnK9UBNp4pEr0CeFF5oRXXPODIzsILzMgtpFSg+BGHg/3VcLjcdVKSEr0Vzjil4x/AHvTrF5jm3d2wZUcif8BqSLxz8FiAOgAAAAASUVORK5CYII=";
17482
+
17483
+ var unlockedIcon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFoAAABaCAYAAAA4qEECAAAACXBIWXMAAAsSAAALEgHS3X78AAAFYklEQVR4nO1d0ZGjOBB9ct3/+iJYMlg2giODcwjeDCYEbwbeDNgMvBloIjgmAyYDHEHfh4SHsQUIaLVk7FfVNTblkZpHq1u0GqGICKlAKZUDyADkVrYA/vH891f7VwOoANREVDGrOBsqJtFKqQxAAWBn/35h7uIMQ/wJgCaimrl9b4gTrZTaAthb+SbaOfAGoARQElEj2jMRiQiMxZYAKBEpARRi5y9EsE6A2D7REoQ/MsGihIcgeIu0XMRUKQFskyYaZvbQJEDWUmkA7JIjegVWHNy6OUjOYW4QYpMSSioAeVSiYQLeGlzFmDRYGCiXkLxPgABp2YsS/aAkLyL7SbIQ2U+ShcieGvhin1xq4h0gvbJ3Nk+swZ/G9MEZZooFmKlkDB36cIYhezzv7WHJW8jOkxsAR5i7TOfNAszoOgKoBfXqk6pPz0muA3J3fDXmBBkTN2ITXi4i2lqVhKJHH6sYGXXHyGQP5kbGlJe465tsxSPWHYvoZshYYrsMNpITIbvXhfQpWwgo9eJB2AmfR1Vjjw1eoMhkF1OI1oGV0SNxofZoo8aAX7QXJAbRznOLZc1ZDzlzAtqxp60sEtEEh1XHsGanHwNwWNCm0w0h3mLEjVUnYc1M/d4k55GQVUtbQBUwJpx62q4jEV06iYaZN4fu/MafgtfqXKMlFtGEzrx6gw/sER6u5MuOsf1D94tS6gXAV8b2p2J/+dS58hKJo9tovCwIukTbNrXA+YxJ9cl1QC5ouIiONd+VkqzrOgrEQzI1zIFQALgQzeknh5AJ9ZMSdgDELTpzHDsJ9R0LRfshR4TgkNAUTMRPbyA7nL/ZxymuoQV1iIF8A2PRknDFg7W7j3wD+RnHTX9EdIJZUV4r8r8idPqvUmpLtw/rHBH+oscqV9gCskGhgblrY6+o9xWYmHSAfBWsWEenmAQ7CN9C9q5UpJMyNrEDhGsJDlTLdkC8wyTlZR+g9IR9wLRGYN+9Gf/JYhxSJRkArG7H0P1IWPTfKRMNXIo4/wvaB8IS/U5EWcD22aCUCmpwoV1HHbj9u4GEj2aBUmqnlKqUUmSlUkpJpXdZEHJao5mmYEOFNc4Cmhl9BJ9H65SJhl/NR5E40XoDcyuaMnzcwz60EgvRbJD+mp1PGjcLrcRCVPdAtM+IS31U3gXRPosCqS8cVG0gCJUy5Jp1DBX3ONchEwqGDdFHXYdmuGohUQD44zj+B3FrUnyggY8blqSHHhE1RLQD8LNz+CcR7VLPo8Byey8Wfc/QgCWazA6HbxGVWSveLLefch1lFFXWjbL98CQ6LMr2w4VoG1R+x9BmpfjdDdTXadJSVpdVo+x++UQ0EWl87MP8xHy8Wi4vcCX+DyKqzENbOnZG2nP/w/UB5w40SikN/53Ih8C+ZmgXUkHMu50zrhm+ElFx034P0Zyrwo+2Cv7dZQTONUP7w19MHd/Duh6Xjr96R9pANqut4FmavaqRUM1dz3lyZC8Hz7N3FdwO95cJV7MPXyFQCbQAJ/CUg+0HXaTHFefaq6hEQpYN3mrS0ZV4X6W4nqqtYRZS11Qf7bXw4LvBYGbJ5qy4fId8JRN3xf8ZplK2Hvuh93tYJAoB7xDOqZwL3iVhtsEfs1VaH374kgzAz0df+bg9eHzbPct+Mm8zA8ojkz2Z5NlEPzDZs0j2nnX0IfJ2x5Lw3764B4vqo23HOda9sPsGM4Vbli1kuglIYbfbELJoF2AWH91D+PMVThJEr8S62aw4KNEdwttAGZs4X9FgeFWTONEdwovECde45xdH9hBeJkBsK6UEwa08X+4rhOfrqoUQlehrWOJzzH8BewOTN69gEvI1u5Iz8T+TOcUtjzRRXwAAAABJRU5ErkJggg==";
17484
+
17618
17485
  function pathD(poly) {
17619
17486
  return `M ${poly.map((pt) => `${pt.x} ${pt.y}`).join(" L ")} Z`;
17620
17487
  }
@@ -17630,11 +17497,13 @@ var TangramConstructPlugin = (function (jspsych) {
17630
17497
  placedSilBySector,
17631
17498
  anchorDots,
17632
17499
  pieces,
17500
+ scaleS,
17633
17501
  clickMode,
17634
17502
  draggingId,
17635
17503
  selectedPieceId,
17636
17504
  dragInvalid,
17637
17505
  lockedPieceId,
17506
+ showTangramDecomposition,
17638
17507
  svgRef,
17639
17508
  setPieceRef,
17640
17509
  onPiecePointerDown,
@@ -17700,7 +17569,7 @@ var TangramConstructPlugin = (function (jspsych) {
17700
17569
  onPointerDown: (e) => {
17701
17570
  onRootPointerDown(e);
17702
17571
  },
17703
- style: { background: "#fff", touchAction: "none", userSelect: "none" }
17572
+ style: { background: "#f5f5f5", touchAction: "none", userSelect: "none" }
17704
17573
  },
17705
17574
  layout.sectors.map((s, i) => {
17706
17575
  const done = !!controller.state.sectors[s.id].completedAt;
@@ -17720,11 +17589,12 @@ var TangramConstructPlugin = (function (jspsych) {
17720
17589
  const isDragging = draggingId === p.id;
17721
17590
  const locked = p.sectorId && controller.isSectorCompleted(p.sectorId);
17722
17591
  const isConnectivityLocked = lockedPieceId === p.id;
17723
- const isSelected = selectedPieceId === p.id;
17592
+ selectedPieceId === p.id;
17724
17593
  const isCarriedInvalid = isDragging && dragInvalid;
17725
17594
  const translateX = p.x - bb.min.x;
17726
17595
  const translateY = p.y - bb.min.y;
17727
17596
  return /* @__PURE__ */ React.createElement("g", { key: p.id, ref: setPieceRef(p.id), transform: `translate(${translateX}, ${translateY})`, style: { cursor: locked ? "default" : clickMode ? "pointer" : "grab" }, pointerEvents: clickMode && isDragging ? "none" : "auto" }, ("shape" in bp ? bp.shape : []).map((poly, idx) => {
17597
+ const showBorders = shouldShowBorders();
17728
17598
  shouldUseSelectiveBorders(p.blueprintId);
17729
17599
  return /* @__PURE__ */ React.createElement(React.Fragment, { key: idx }, /* @__PURE__ */ React.createElement(
17730
17600
  "path",
@@ -17735,51 +17605,38 @@ var TangramConstructPlugin = (function (jspsych) {
17735
17605
  stroke: "none",
17736
17606
  onPointerDown: (e) => onPiecePointerDown(e, p)
17737
17607
  }
17738
- ), ((
17739
- // For pieces with selective borders: render individual edge strokes with edge detection
17740
- (() => {
17741
- const allPiecesInSector = pieces.filter((piece) => piece.sectorId === p.sectorId);
17742
- const pieceAsPiece = { ...p, pos: { x: p.x, y: p.y } };
17743
- const allPiecesAsPieces = allPiecesInSector.map((piece) => ({ ...piece, pos: { x: piece.x, y: piece.y } }));
17744
- const hiddenEdges = getHiddenEdgesForPolygon(pieceAsPiece, idx, allPiecesAsPieces, (id) => controller.getBlueprint(id), (kind) => controller.getPrimitive(kind));
17745
- const draggedPiece = draggingId ? allPiecesInSector.find((piece) => piece.id === draggingId) : null;
17746
- let wasTouchingDraggedPiece;
17747
- if (p.blueprintId.startsWith("comp:")) {
17748
- const internalHiddenEdges = getHiddenEdgesForPolygon(pieceAsPiece, idx, [], (id) => controller.getBlueprint(id), (kind) => controller.getPrimitive(kind));
17749
- const externalHiddenEdges = draggedPiece ? getHiddenEdgesForPolygon(pieceAsPiece, idx, [{ ...draggedPiece, pos: { x: draggedPiece.x, y: draggedPiece.y } }], (id) => controller.getBlueprint(id), (kind) => controller.getPrimitive(kind)) : new Array(hiddenEdges.length).fill(false);
17750
- wasTouchingDraggedPiece = externalHiddenEdges.map((external, i) => external && !internalHiddenEdges[i]);
17751
- } else {
17752
- wasTouchingDraggedPiece = draggedPiece ? getHiddenEdgesForPolygon(pieceAsPiece, idx, [{ ...draggedPiece, pos: { x: draggedPiece.x, y: draggedPiece.y } }], (id) => controller.getBlueprint(id), (kind) => controller.getPrimitive(kind)) : new Array(hiddenEdges.length).fill(false);
17753
- }
17754
- return generateEdgeStrokePaths(poly).map((strokePath, strokeIdx) => {
17755
- const wasHiddenDueToDraggedPiece = wasTouchingDraggedPiece[strokeIdx] || false;
17756
- let isHidden;
17757
- if (isDragging && p.blueprintId.startsWith("comp:")) {
17758
- const internalHiddenEdges = getHiddenEdgesForPolygon(pieceAsPiece, idx, [], (id) => controller.getBlueprint(id), (kind) => controller.getPrimitive(kind));
17759
- isHidden = internalHiddenEdges[strokeIdx] || false;
17760
- } else {
17761
- isHidden = isDragging ? false : (hiddenEdges[strokeIdx] || false) && !wasHiddenDueToDraggedPiece;
17762
- }
17763
- return /* @__PURE__ */ React.createElement(
17764
- "path",
17765
- {
17766
- key: `stroke-${idx}-${strokeIdx}`,
17767
- d: strokePath,
17768
- fill: "none",
17769
- stroke: isHidden ? "none" : isConnectivityLocked ? CONFIG.color.piece.invalidStroke : isCarriedInvalid ? CONFIG.color.piece.invalidStroke : isSelected || isDragging ? CONFIG.color.piece.selectedStroke : CONFIG.color.piece.borderStroke,
17770
- strokeWidth: isHidden ? 0 : isSelected || isDragging ? CONFIG.size.stroke.pieceSelectedPx : CONFIG.size.stroke.pieceBorderPx,
17771
- onPointerDown: (e) => onPiecePointerDown(e, p)
17772
- }
17773
- );
17774
- });
17775
- })()
17776
- ) ));
17608
+ ), showBorders);
17777
17609
  }));
17778
17610
  }),
17779
17611
  layout.sectors.map((s) => {
17780
- const placedPolys = placedSilBySector.get(s.id) ?? [];
17781
- if (!placedPolys.length) return null;
17782
- return /* @__PURE__ */ React.createElement("g", { key: `sil-${s.id}`, pointerEvents: "none" }, placedPolys.map((poly, i) => /* @__PURE__ */ React.createElement("path", { key: i, d: pathD(poly), fill: CONFIG.color.silhouetteMask, opacity: CONFIG.opacity.silhouetteMask })));
17612
+ const sectorCfg = controller.state.cfg.sectors.find((ss) => ss.id === s.id);
17613
+ if (showTangramDecomposition && sectorCfg?.silhouette.primitiveDecomposition) {
17614
+ const primitiveDecomposition = sectorCfg.silhouette.primitiveDecomposition;
17615
+ const rect = rectForBand(layout, s, "silhouette", 1);
17616
+ const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
17617
+ const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, { cx: rect.cx, cy: rect.cy });
17618
+ return /* @__PURE__ */ React.createElement("g", { key: `sil-decomposed-${s.id}`, pointerEvents: "none" }, placedPolys.map((scaledPoly, i) => /* @__PURE__ */ React.createElement(React.Fragment, { key: `prim-${i}` }, /* @__PURE__ */ React.createElement(
17619
+ "path",
17620
+ {
17621
+ d: pathD(scaledPoly),
17622
+ fill: CONFIG.color.silhouetteMask,
17623
+ opacity: CONFIG.opacity.silhouetteMask,
17624
+ stroke: "none"
17625
+ }
17626
+ ), /* @__PURE__ */ React.createElement(
17627
+ "path",
17628
+ {
17629
+ d: pathD(scaledPoly),
17630
+ fill: "none",
17631
+ stroke: CONFIG.color.tangramDecomposition.stroke,
17632
+ strokeWidth: CONFIG.size.stroke.tangramDecompositionPx
17633
+ }
17634
+ ))));
17635
+ } else {
17636
+ const placedPolys = placedSilBySector.get(s.id) ?? [];
17637
+ if (!placedPolys.length) return null;
17638
+ return /* @__PURE__ */ React.createElement("g", { key: `sil-${s.id}`, pointerEvents: "none" }, placedPolys.map((poly, i) => /* @__PURE__ */ React.createElement("path", { key: i, d: pathD(poly), fill: CONFIG.color.silhouetteMask, opacity: CONFIG.opacity.silhouetteMask })));
17639
+ }
17783
17640
  }),
17784
17641
  anchorDots.map(({ sectorId, valid, invalid }) => {
17785
17642
  const isInnerRing = sectorId === "inner-ring";
@@ -17795,10 +17652,20 @@ var TangramConstructPlugin = (function (jspsych) {
17795
17652
  }
17796
17653
  )));
17797
17654
  }),
17655
+ /* @__PURE__ */ React.createElement("defs", null, /* @__PURE__ */ React.createElement("filter", { id: "invert-to-white" }, /* @__PURE__ */ React.createElement(
17656
+ "feColorMatrix",
17657
+ {
17658
+ type: "matrix",
17659
+ values: "-1 0 0 0 1\n 0 -1 0 0 1\n 0 0 -1 0 1\n 0 0 0 1 0"
17660
+ }
17661
+ ))),
17798
17662
  (() => {
17799
17663
  const isPrep = controller.state.cfg.mode === "prep";
17800
17664
  const isSubmitEnabled = isPrep ? controller.isSubmitEnabled() : true;
17801
17665
  const isClickable = !draggingId && (!isPrep || isSubmitEnabled);
17666
+ const [imageError, setImageError] = React.useState(false);
17667
+ const iconSize = badgeR * 1.6;
17668
+ const iconOffset = iconSize / 2;
17802
17669
  return /* @__PURE__ */ React.createElement(
17803
17670
  "g",
17804
17671
  {
@@ -17814,7 +17681,7 @@ var TangramConstructPlugin = (function (jspsych) {
17814
17681
  opacity: isSubmitEnabled ? 1 : 0.5
17815
17682
  }
17816
17683
  ),
17817
- /* @__PURE__ */ React.createElement(
17684
+ isPrep ? /* @__PURE__ */ React.createElement(
17818
17685
  "text",
17819
17686
  {
17820
17687
  textAnchor: "middle",
@@ -17823,7 +17690,29 @@ var TangramConstructPlugin = (function (jspsych) {
17823
17690
  fill: isSubmitEnabled ? CONFIG.color.blueprint.labelFill : "#888",
17824
17691
  pointerEvents: "none"
17825
17692
  },
17826
- isPrep ? "Submit" : controller.state.blueprintView
17693
+ "Submit"
17694
+ ) : imageError ? /* @__PURE__ */ React.createElement(
17695
+ "text",
17696
+ {
17697
+ textAnchor: "middle",
17698
+ dominantBaseline: "middle",
17699
+ fontSize: CONFIG.size.badgeFontPx,
17700
+ fill: CONFIG.color.blueprint.labelFill,
17701
+ pointerEvents: "none"
17702
+ },
17703
+ "inventory"
17704
+ ) : /* @__PURE__ */ React.createElement(
17705
+ "image",
17706
+ {
17707
+ href: controller.state.blueprintView === "quickstash" ? lockedIcon : unlockedIcon,
17708
+ x: -iconOffset,
17709
+ y: -iconOffset,
17710
+ width: iconSize,
17711
+ height: iconSize,
17712
+ pointerEvents: "none",
17713
+ onError: () => setImageError(true),
17714
+ filter: "url(#invert-to-white)"
17715
+ }
17827
17716
  )
17828
17717
  );
17829
17718
  })(),
@@ -19297,7 +19186,7 @@ var TangramConstructPlugin = (function (jspsych) {
19297
19186
  }
19298
19187
 
19299
19188
  class InteractionTracker {
19300
- constructor(controller, callbacks, trialId, gameId) {
19189
+ constructor(controller, callbacks, trialParams) {
19301
19190
  this.gridStep = CONFIG.layout.grid.stepPx;
19302
19191
  // Interaction state
19303
19192
  this.interactionIndex = 0;
@@ -19314,8 +19203,7 @@ var TangramConstructPlugin = (function (jspsych) {
19314
19203
  this.createdMacros = [];
19315
19204
  this.controller = controller;
19316
19205
  this.callbacks = callbacks;
19317
- this.trialId = trialId || v4();
19318
- this.gameId = gameId || v4();
19206
+ this.trialParams = trialParams;
19319
19207
  this.trialStartTime = Date.now();
19320
19208
  this.controller.setTrackingCallbacks({
19321
19209
  onSectorCompleted: (sectorId) => this.recordSectorCompletion(sectorId)
@@ -19363,8 +19251,6 @@ var TangramConstructPlugin = (function (jspsych) {
19363
19251
  const event = {
19364
19252
  // Metadata
19365
19253
  interactionId: v4(),
19366
- trialId: this.trialId,
19367
- gameId: this.gameId,
19368
19254
  interactionIndex: this.interactionIndex++,
19369
19255
  // Interaction type
19370
19256
  interactionType,
@@ -19470,6 +19356,7 @@ var TangramConstructPlugin = (function (jspsych) {
19470
19356
  const trialEndTime = Date.now();
19471
19357
  const totalDuration = trialEndTime - this.trialStartTime;
19472
19358
  const finalSnapshot = this.buildStateSnapshot();
19359
+ const anchorToStimuliRatio = CONFIG.layout.grid.stepPx / CONFIG.layout.grid.unitPx;
19473
19360
  const mode = this.controller.state.cfg.mode;
19474
19361
  if (mode === "construction") {
19475
19362
  const finalBlueprintState = this.buildFinalBlueprintState(finalSnapshot);
@@ -19481,13 +19368,11 @@ var TangramConstructPlugin = (function (jspsych) {
19481
19368
  }));
19482
19369
  const data = {
19483
19370
  trialType: "construction",
19484
- trialId: this.trialId,
19485
- gameId: this.gameId,
19486
- trialNum: 0,
19487
- // TODO: Plugin should provide this
19488
19371
  trialStartTime: this.trialStartTime,
19489
19372
  trialEndTime,
19490
19373
  totalDuration,
19374
+ anchorToStimuliRatio,
19375
+ trialParams: this.trialParams,
19491
19376
  endReason,
19492
19377
  completionTimes: this.completionTimes,
19493
19378
  finalBlueprintState,
@@ -19506,13 +19391,11 @@ var TangramConstructPlugin = (function (jspsych) {
19506
19391
  }));
19507
19392
  const data = {
19508
19393
  trialType: "prep",
19509
- trialId: this.trialId,
19510
- gameId: this.gameId,
19511
- trialNum: 0,
19512
- // TODO: Plugin should provide this
19513
19394
  trialStartTime: this.trialStartTime,
19514
19395
  trialEndTime,
19515
19396
  totalDuration,
19397
+ anchorToStimuliRatio,
19398
+ trialParams: this.trialParams,
19516
19399
  endReason: "submit",
19517
19400
  createdMacros: finalMacros,
19518
19401
  quickstashMacros,
@@ -19770,6 +19653,9 @@ var TangramConstructPlugin = (function (jspsych) {
19770
19653
  maxQuickstashSlots,
19771
19654
  maxCompositeSize,
19772
19655
  mode,
19656
+ showTangramDecomposition,
19657
+ instructions,
19658
+ trialParams,
19773
19659
  width: _width,
19774
19660
  height: _height,
19775
19661
  onSectorComplete,
@@ -19779,6 +19665,7 @@ var TangramConstructPlugin = (function (jspsych) {
19779
19665
  onTrialEnd,
19780
19666
  onControllerReady
19781
19667
  } = props;
19668
+ const [timeRemaining, setTimeRemaining] = React.useState(timeLimitMs > 0 ? Math.floor(timeLimitMs / 1e3) : 0);
19782
19669
  const controller = React.useMemo(() => {
19783
19670
  const gameConfig = {
19784
19671
  n: sectors.length,
@@ -19805,8 +19692,8 @@ var TangramConstructPlugin = (function (jspsych) {
19805
19692
  const callbacks = {};
19806
19693
  if (onInteraction) callbacks.onInteraction = onInteraction;
19807
19694
  if (onTrialEnd) callbacks.onTrialEnd = onTrialEnd;
19808
- return new InteractionTracker(controller, callbacks);
19809
- }, [controller, onInteraction, onTrialEnd]);
19695
+ return new InteractionTracker(controller, callbacks, trialParams);
19696
+ }, [controller, onInteraction, onTrialEnd, trialParams]);
19810
19697
  React.useEffect(() => {
19811
19698
  if (onControllerReady) {
19812
19699
  onControllerReady(controller);
@@ -19886,22 +19773,22 @@ var TangramConstructPlugin = (function (jspsych) {
19886
19773
  }), [handleSectorComplete, onPiecePlace, onPieceRemove]);
19887
19774
  const getGameboardStyle = () => {
19888
19775
  const baseStyle = {
19889
- margin: "0 auto",
19776
+ margin: "10px",
19890
19777
  display: "flex",
19891
19778
  alignItems: "center",
19892
19779
  justifyContent: "center",
19893
19780
  position: "relative"
19894
19781
  };
19895
19782
  if (layoutMode === "circle") {
19896
- const size = Math.min(window.innerWidth * 0.96, window.innerHeight * 0.96);
19783
+ const size = Math.min(window.innerWidth * CONFIG.layout.viewportScale, window.innerHeight * CONFIG.layout.viewportScale);
19897
19784
  return {
19898
19785
  ...baseStyle,
19899
19786
  width: `${size}px`,
19900
19787
  height: `${size}px`
19901
19788
  };
19902
19789
  } else {
19903
- const maxWidth = Math.min(window.innerWidth * 0.96, window.innerHeight * 0.96 * 2);
19904
- const maxHeight = Math.min(window.innerWidth * 0.96 / 2, window.innerHeight * 0.96);
19790
+ const maxWidth = Math.min(window.innerWidth * CONFIG.layout.viewportScale, window.innerHeight * CONFIG.layout.viewportScale * 2);
19791
+ const maxHeight = Math.min(window.innerWidth * CONFIG.layout.viewportScale / 2, window.innerHeight * CONFIG.layout.viewportScale);
19905
19792
  return {
19906
19793
  ...baseStyle,
19907
19794
  width: `${maxWidth}px`,
@@ -20070,7 +19957,68 @@ var TangramConstructPlugin = (function (jspsych) {
20070
19957
  force();
20071
19958
  e.stopPropagation();
20072
19959
  };
20073
- return /* @__PURE__ */ React.createElement("div", { className: "tangram-gameboard", style: getGameboardStyle() }, /* @__PURE__ */ React.createElement(
19960
+ React.useEffect(() => {
19961
+ if (timeLimitMs === 0) return;
19962
+ const interval = setInterval(() => {
19963
+ setTimeRemaining((prev) => {
19964
+ if (prev <= 1) {
19965
+ clearInterval(interval);
19966
+ return 0;
19967
+ }
19968
+ return prev - 1;
19969
+ });
19970
+ }, 1e3);
19971
+ return () => clearInterval(interval);
19972
+ }, [timeLimitMs]);
19973
+ const formatTime = (seconds) => {
19974
+ const mins = Math.floor(seconds / 60);
19975
+ const secs = Math.floor(seconds % 60);
19976
+ return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
19977
+ };
19978
+ const containerStyle = {
19979
+ display: "flex",
19980
+ flexDirection: "column",
19981
+ width: "100%"
19982
+ // minHeight: '100vh'
19983
+ };
19984
+ const headerStyle = {
19985
+ display: "flex",
19986
+ flexDirection: "row",
19987
+ justifyContent: "space-between",
19988
+ alignItems: "center",
19989
+ padding: "20px",
19990
+ background: "#f5f5f5",
19991
+ flex: "0 0 auto"
19992
+ };
19993
+ const instructionsStyle = {
19994
+ flexGrow: 1,
19995
+ fontSize: "20px",
19996
+ lineHeight: 1.5,
19997
+ marginRight: "20px"
19998
+ };
19999
+ const timerStyle = {
20000
+ fontSize: "24px",
20001
+ fontWeight: "bold",
20002
+ fontFamily: "monospace",
20003
+ color: "#333",
20004
+ minWidth: "80px",
20005
+ textAlign: "right"
20006
+ };
20007
+ const gameboardWrapperStyle = {
20008
+ flex: "1 1 auto",
20009
+ display: "flex",
20010
+ alignItems: "center",
20011
+ justifyContent: "center",
20012
+ overflow: "hidden"
20013
+ };
20014
+ return /* @__PURE__ */ React.createElement("div", { className: "tangram-trial-container", style: containerStyle }, (instructions || timeLimitMs > 0) && /* @__PURE__ */ React.createElement("div", { className: "tangram-header", style: headerStyle }, instructions && /* @__PURE__ */ React.createElement(
20015
+ "div",
20016
+ {
20017
+ className: "tangram-instructions",
20018
+ style: instructionsStyle,
20019
+ dangerouslySetInnerHTML: { __html: instructions }
20020
+ }
20021
+ ), timeLimitMs > 0 && /* @__PURE__ */ React.createElement("div", { className: "tangram-timer", style: timerStyle }, formatTime(timeRemaining))), /* @__PURE__ */ React.createElement("div", { className: "tangram-gameboard-wrapper", style: gameboardWrapperStyle }, /* @__PURE__ */ React.createElement("div", { className: "tangram-gameboard", style: getGameboardStyle() }, /* @__PURE__ */ React.createElement(
20074
20022
  BoardView,
20075
20023
  {
20076
20024
  controller,
@@ -20096,9 +20044,11 @@ var TangramConstructPlugin = (function (jspsych) {
20096
20044
  onPointerMove,
20097
20045
  onPointerUp,
20098
20046
  onCenterBadgePointerDown,
20047
+ showTangramDecomposition: showTangramDecomposition ?? false,
20048
+ scaleS,
20099
20049
  ...eventCallbacks
20100
20050
  }
20101
- ));
20051
+ ))));
20102
20052
  }
20103
20053
 
20104
20054
  const U = 40;
@@ -20266,17 +20216,22 @@ var TangramConstructPlugin = (function (jspsych) {
20266
20216
  const isCanonical = CANON.has(tanName);
20267
20217
  return isCanonical;
20268
20218
  });
20269
- const mask = filteredTans.map((tan, tanIndex) => {
20219
+ const mask = filteredTans.map((tan) => {
20270
20220
  const polygon = tan.vertices.map(([x, y]) => ({ x: x ?? 0, y: -(y ?? 0) }));
20271
20221
  return polygon;
20272
20222
  });
20223
+ const primitiveDecomposition = filteredTans.map((tan) => ({
20224
+ kind: tan.name ?? tan.kind,
20225
+ polygon: tan.vertices.map(([x, y]) => ({ x: x ?? 0, y: -(y ?? 0) }))
20226
+ }));
20273
20227
  const sectorId = `sector${index}`;
20274
20228
  const sector = {
20275
20229
  id: sectorId,
20276
20230
  tangramId: tangramSpec.tangramID,
20277
20231
  silhouette: {
20278
20232
  id: sectorId,
20279
- mask
20233
+ mask,
20234
+ primitiveDecomposition
20280
20235
  }
20281
20236
  };
20282
20237
  return sector;
@@ -20295,6 +20250,7 @@ var TangramConstructPlugin = (function (jspsych) {
20295
20250
  quickstash = params.quickstash_macros;
20296
20251
  }
20297
20252
  }
20253
+ const { onInteraction, onTrialEnd, ...trialParams } = params;
20298
20254
  const gameBoardProps = {
20299
20255
  sectors,
20300
20256
  quickstash,
@@ -20305,6 +20261,9 @@ var TangramConstructPlugin = (function (jspsych) {
20305
20261
  timeLimitMs: params.time_limit_ms,
20306
20262
  maxQuickstashSlots: CONFIG.layout.defaults.maxQuickstashSlots,
20307
20263
  mode: "construction",
20264
+ showTangramDecomposition: params.show_tangram_decomposition ?? false,
20265
+ trialParams,
20266
+ ...params.instructions && { instructions: params.instructions },
20308
20267
  ...params.onInteraction && { onInteraction: params.onInteraction },
20309
20268
  ...params.onTrialEnd && { onTrialEnd: params.onTrialEnd }
20310
20269
  };
@@ -20368,6 +20327,18 @@ var TangramConstructPlugin = (function (jspsych) {
20368
20327
  default: 18,
20369
20328
  description: "Snap radius for anchor-based piece placement"
20370
20329
  },
20330
+ /** Whether to show tangram target shapes decomposed into individual primitives with borders */
20331
+ show_tangram_decomposition: {
20332
+ type: jspsych.ParameterType.BOOL,
20333
+ default: false,
20334
+ description: "Whether to show tangram target shapes decomposed into individual primitives with borders"
20335
+ },
20336
+ /** HTML content to display above the gameboard as instructions */
20337
+ instructions: {
20338
+ type: jspsych.ParameterType.STRING,
20339
+ default: "",
20340
+ description: "HTML content to display above the gameboard as instructions"
20341
+ },
20371
20342
  /** Callback fired after each interaction (piece pickup + placedown) */
20372
20343
  onInteraction: {
20373
20344
  type: jspsych.ParameterType.FUNCTION,
@@ -20441,6 +20412,8 @@ var TangramConstructPlugin = (function (jspsych) {
20441
20412
  input: trial.input,
20442
20413
  layout: trial.layout,
20443
20414
  time_limit_ms: trial.time_limit_ms,
20415
+ show_tangram_decomposition: trial.show_tangram_decomposition,
20416
+ instructions: trial.instructions,
20444
20417
  onInteraction: trial.onInteraction,
20445
20418
  onTrialEnd: wrappedOnTrialEnd
20446
20419
  };