jspsych-tangram 0.0.6 → 0.0.8

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 (36) hide show
  1. package/dist/construct/index.browser.js +157 -222
  2. package/dist/construct/index.browser.js.map +1 -1
  3. package/dist/construct/index.browser.min.js +11 -11
  4. package/dist/construct/index.browser.min.js.map +1 -1
  5. package/dist/construct/index.cjs +157 -222
  6. package/dist/construct/index.cjs.map +1 -1
  7. package/dist/construct/index.d.ts +24 -0
  8. package/dist/construct/index.js +157 -222
  9. package/dist/construct/index.js.map +1 -1
  10. package/dist/index.cjs +168 -223
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.ts +36 -0
  13. package/dist/index.js +168 -223
  14. package/dist/index.js.map +1 -1
  15. package/dist/prep/index.browser.js +143 -221
  16. package/dist/prep/index.browser.js.map +1 -1
  17. package/dist/prep/index.browser.min.js +13 -13
  18. package/dist/prep/index.browser.min.js.map +1 -1
  19. package/dist/prep/index.cjs +143 -221
  20. package/dist/prep/index.cjs.map +1 -1
  21. package/dist/prep/index.d.ts +12 -0
  22. package/dist/prep/index.js +143 -221
  23. package/dist/prep/index.js.map +1 -1
  24. package/package.json +1 -1
  25. package/src/core/components/board/BoardView.tsx +53 -14
  26. package/src/core/components/board/GameBoard.tsx +123 -7
  27. package/src/core/config/config.ts +19 -14
  28. package/src/core/domain/types.ts +1 -0
  29. package/src/core/io/InteractionTracker.ts +7 -16
  30. package/src/core/io/data-tracking.ts +3 -7
  31. package/src/plugins/tangram-construct/ConstructionApp.tsx +16 -1
  32. package/src/plugins/tangram-construct/index.ts +14 -0
  33. package/src/plugins/tangram-prep/PrepApp.tsx +6 -0
  34. package/src/plugins/tangram-prep/index.ts +7 -0
  35. package/tangram-construct.min.js +11 -11
  36. package/tangram-prep.min.js +13 -13
@@ -16661,31 +16661,34 @@ var TangramConstructPlugin = (function (jspsych) {
16661
16661
  const CONFIG = {
16662
16662
  color: {
16663
16663
  bands: {
16664
- silhouette: { fillEven: "#eef2ff", fillOdd: "#f6f7fb", stroke: "#c7d2fe" },
16665
- workspace: { fillEven: "#f3f4f6", fillOdd: "#f9fafb", stroke: "#e5e7eb" }
16664
+ silhouette: { fillEven: "#fff2ccff", fillOdd: "#fff2ccff", stroke: "#b1b1b1" },
16665
+ workspace: { fillEven: "#fff2ccff", fillOdd: "#fff2ccff", stroke: "#b1b1b1" }
16666
16666
  },
16667
- completion: { fill: "#dcfce7", stroke: "#86efac" },
16668
- silhouetteMask: "#94a3b8",
16667
+ completion: { fill: "#ccfff2", stroke: "#13da57" },
16668
+ silhouetteMask: "#374151",
16669
16669
  anchors: { invalid: "#7dd3fc", valid: "#475569" },
16670
- piece: { draggingFill: "#1d4ed8", validFill: "#60a5fa", invalidFill: "#ef4444", invalidStroke: "#dc2626", selectedStroke: "#111827", allGreenStroke: "#86efac", borderStroke: "#374151" },
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: "#eef2ff", labelFill: "#374151" }
16672
+ blueprint: { fill: "#374151", selectedStroke: "#111827", badgeFill: "#000000", labelFill: "#ffffff" },
16673
+ tangramDecomposition: { stroke: "#fef2cc" }
16673
16674
  },
16674
16675
  opacity: {
16675
- blueprint: 0.95,
16676
- silhouetteMask: 0.45,
16677
- anchors: { valid: 0.8, invalid: 0.5 },
16678
- piece: { invalid: 0.35, dragging: 0.6, locked: 0.7, normal: 0.95 }
16676
+ blueprint: 0.4,
16677
+ silhouetteMask: 0.25,
16678
+ //anchors: { valid: 0.80, invalid: 0.50 },
16679
+ anchors: { invalid: 0, valid: 0 },
16680
+ piece: { invalid: 0.35, dragging: 0.75, locked: 1, normal: 1 }
16679
16681
  },
16680
16682
  size: {
16681
- stroke: { bandPx: 1, pieceSelectedPx: 1.5, allGreenStrokePx: 6, pieceBorderPx: 1 },
16683
+ stroke: { bandPx: 5, pieceSelectedPx: 3, allGreenStrokePx: 10, pieceBorderPx: 2, tangramDecompositionPx: 1 },
16682
16684
  anchorRadiusPx: { valid: 1, invalid: 1 },
16683
- badgeFontPx: 12,
16685
+ badgeFontPx: 16,
16684
16686
  centerBadge: { fractionOfOuterR: 0.15, minPx: 20, marginPx: 4 }
16685
16687
  },
16686
16688
  layout: {
16687
16689
  grid: { stepPx: 20, unitPx: 40 },
16688
16690
  paddingPx: 1,
16691
+ viewportScale: 0.8,
16689
16692
  constraints: {
16690
16693
  workspaceDiamAnchors: 10,
16691
16694
  // num anchors req'd to be on diagonal
@@ -16697,7 +16700,7 @@ var TangramConstructPlugin = (function (jspsych) {
16697
16700
  },
16698
16701
  game: {
16699
16702
  snapRadiusPx: 15,
16700
- showBorders: true,
16703
+ showBorders: false,
16701
16704
  hideTouchingBorders: true
16702
16705
  }
16703
16706
  };
@@ -17468,150 +17471,11 @@ var TangramConstructPlugin = (function (jspsych) {
17468
17471
  return { cx, cy, w, h };
17469
17472
  }
17470
17473
 
17471
- function generateEdgeStrokePaths(poly) {
17472
- const strokePaths = [];
17473
- for (let i = 0; i < poly.length; i++) {
17474
- const current = poly[i];
17475
- const next = poly[(i + 1) % poly.length];
17476
- if (current && next) {
17477
- strokePaths.push(`M ${current.x} ${current.y} L ${next.x} ${next.y}`);
17478
- }
17479
- }
17480
- return strokePaths;
17481
- }
17482
- function edgeToUnitSegments$1(start, end, gridSize) {
17483
- const segments = [];
17484
- const startGrid = {
17485
- x: Math.round(start.x / gridSize),
17486
- y: Math.round(start.y / gridSize)
17487
- };
17488
- const endGrid = {
17489
- x: Math.round(end.x / gridSize),
17490
- y: Math.round(end.y / gridSize)
17491
- };
17492
- const dx = endGrid.x - startGrid.x;
17493
- const dy = endGrid.y - startGrid.y;
17494
- if (dx === 0 && dy === 0) return [];
17495
- const steps = Math.max(Math.abs(dx), Math.abs(dy));
17496
- const stepX = dx / steps;
17497
- const stepY = dy / steps;
17498
- for (let i = 0; i < steps; i++) {
17499
- const aX = Math.round(startGrid.x + i * stepX);
17500
- const aY = Math.round(startGrid.y + i * stepY);
17501
- const bX = Math.round(startGrid.x + (i + 1) * stepX);
17502
- const bY = Math.round(startGrid.y + (i + 1) * stepY);
17503
- segments.push({
17504
- a: { x: aX, y: aY },
17505
- b: { x: bX, y: bY }
17506
- });
17507
- }
17508
- return segments;
17509
- }
17510
- function getHiddenEdgesForPolygon(piece, polyIndex, allPiecesInSector, getBlueprint, getPrimitive) {
17511
- let blueprint;
17512
- try {
17513
- blueprint = getBlueprint(piece.blueprintId);
17514
- } catch (error) {
17515
- console.warn("getBlueprint failed in getHiddenEdgesForPolygon:", error);
17516
- return [];
17517
- }
17518
- if (!blueprint?.shape) {
17519
- return [];
17520
- }
17521
- const poly = blueprint.shape[polyIndex];
17522
- if (!poly) return [];
17523
- const gridSize = CONFIG.layout.grid.stepPx;
17524
- const bb = boundsOfBlueprint(blueprint, getPrimitive);
17525
- const ox = piece.pos.x - bb.min.x;
17526
- const oy = piece.pos.y - bb.min.y;
17527
- const translatedPoly = poly.map((vertex) => ({
17528
- x: vertex.x + ox,
17529
- y: vertex.y + oy
17530
- }));
17531
- const hiddenEdges = [];
17532
- for (let i = 0; i < translatedPoly.length; i++) {
17533
- const current = translatedPoly[i];
17534
- const next = translatedPoly[(i + 1) % translatedPoly.length];
17535
- if (!current || !next) {
17536
- hiddenEdges.push(false);
17537
- continue;
17538
- }
17539
- const edgeSegments = edgeToUnitSegments$1(current, next, gridSize);
17540
- let isTouching = false;
17541
- if (piece.blueprintId.startsWith("comp:")) {
17542
- for (let otherPolyIndex = 0; otherPolyIndex < blueprint.shape.length; otherPolyIndex++) {
17543
- if (otherPolyIndex === polyIndex) continue;
17544
- const otherPoly = blueprint.shape[otherPolyIndex];
17545
- if (!otherPoly) continue;
17546
- const otherTranslatedPoly = otherPoly.map((vertex) => ({
17547
- x: vertex.x + ox,
17548
- y: vertex.y + oy
17549
- }));
17550
- for (let j = 0; j < otherTranslatedPoly.length; j++) {
17551
- const otherCurrent = otherTranslatedPoly[j];
17552
- const otherNext = otherTranslatedPoly[(j + 1) % otherTranslatedPoly.length];
17553
- if (!otherCurrent || !otherNext) continue;
17554
- const otherEdgeSegments = edgeToUnitSegments$1(otherCurrent, otherNext, gridSize);
17555
- for (const edgeSeg of edgeSegments) {
17556
- const isShared = otherEdgeSegments.some(
17557
- (otherSeg) => (
17558
- // Check if segments share the same endpoints (identical or reversed)
17559
- 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
17560
- )
17561
- );
17562
- if (isShared) {
17563
- isTouching = true;
17564
- break;
17565
- }
17566
- }
17567
- if (isTouching) break;
17568
- }
17569
- if (isTouching) break;
17570
- }
17571
- }
17572
- if (!isTouching && CONFIG.game.hideTouchingBorders) {
17573
- for (const otherPiece of allPiecesInSector) {
17574
- if (otherPiece.id === piece.id) continue;
17575
- const otherBlueprint = getBlueprint(otherPiece.blueprintId);
17576
- if (!otherBlueprint?.shape) continue;
17577
- const otherBb = boundsOfBlueprint(otherBlueprint, getPrimitive);
17578
- const otherOx = otherPiece.pos.x - otherBb.min.x;
17579
- const otherOy = otherPiece.pos.y - otherBb.min.y;
17580
- for (const otherPoly of otherBlueprint.shape) {
17581
- const otherTranslatedPoly = otherPoly.map((vertex) => ({
17582
- x: vertex.x + otherOx,
17583
- y: vertex.y + otherOy
17584
- }));
17585
- for (let j = 0; j < otherTranslatedPoly.length; j++) {
17586
- const otherCurrent = otherTranslatedPoly[j];
17587
- const otherNext = otherTranslatedPoly[(j + 1) % otherTranslatedPoly.length];
17588
- if (!otherCurrent || !otherNext) continue;
17589
- const otherEdgeSegments = edgeToUnitSegments$1(otherCurrent, otherNext, gridSize);
17590
- for (const edgeSeg of edgeSegments) {
17591
- const isShared = otherEdgeSegments.some(
17592
- (otherSeg) => (
17593
- // Check if segments share the same endpoints (identical or reversed)
17594
- 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
17595
- )
17596
- );
17597
- if (isShared) {
17598
- isTouching = true;
17599
- break;
17600
- }
17601
- }
17602
- if (isTouching) break;
17603
- }
17604
- if (isTouching) break;
17605
- }
17606
- if (isTouching) break;
17607
- }
17608
- }
17609
- hiddenEdges.push(isTouching);
17610
- }
17611
- return hiddenEdges;
17474
+ function shouldShowBorders() {
17475
+ return CONFIG.game.showBorders;
17612
17476
  }
17613
17477
  function shouldUseSelectiveBorders(blueprintId) {
17614
- return (CONFIG.game.hideTouchingBorders);
17478
+ return CONFIG.game.showBorders;
17615
17479
  }
17616
17480
 
17617
17481
  function pathD(poly) {
@@ -17629,11 +17493,13 @@ var TangramConstructPlugin = (function (jspsych) {
17629
17493
  placedSilBySector,
17630
17494
  anchorDots,
17631
17495
  pieces,
17496
+ scaleS,
17632
17497
  clickMode,
17633
17498
  draggingId,
17634
17499
  selectedPieceId,
17635
17500
  dragInvalid,
17636
17501
  lockedPieceId,
17502
+ showTangramDecomposition,
17637
17503
  svgRef,
17638
17504
  setPieceRef,
17639
17505
  onPiecePointerDown,
@@ -17699,7 +17565,7 @@ var TangramConstructPlugin = (function (jspsych) {
17699
17565
  onPointerDown: (e) => {
17700
17566
  onRootPointerDown(e);
17701
17567
  },
17702
- style: { background: "#fff", touchAction: "none", userSelect: "none" }
17568
+ style: { background: "#f5f5f5", touchAction: "none", userSelect: "none" }
17703
17569
  },
17704
17570
  layout.sectors.map((s, i) => {
17705
17571
  const done = !!controller.state.sectors[s.id].completedAt;
@@ -17719,11 +17585,12 @@ var TangramConstructPlugin = (function (jspsych) {
17719
17585
  const isDragging = draggingId === p.id;
17720
17586
  const locked = p.sectorId && controller.isSectorCompleted(p.sectorId);
17721
17587
  const isConnectivityLocked = lockedPieceId === p.id;
17722
- const isSelected = selectedPieceId === p.id;
17588
+ selectedPieceId === p.id;
17723
17589
  const isCarriedInvalid = isDragging && dragInvalid;
17724
17590
  const translateX = p.x - bb.min.x;
17725
17591
  const translateY = p.y - bb.min.y;
17726
17592
  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) => {
17593
+ const showBorders = shouldShowBorders();
17727
17594
  shouldUseSelectiveBorders(p.blueprintId);
17728
17595
  return /* @__PURE__ */ React.createElement(React.Fragment, { key: idx }, /* @__PURE__ */ React.createElement(
17729
17596
  "path",
@@ -17734,51 +17601,38 @@ var TangramConstructPlugin = (function (jspsych) {
17734
17601
  stroke: "none",
17735
17602
  onPointerDown: (e) => onPiecePointerDown(e, p)
17736
17603
  }
17737
- ), ((
17738
- // For pieces with selective borders: render individual edge strokes with edge detection
17739
- (() => {
17740
- const allPiecesInSector = pieces.filter((piece) => piece.sectorId === p.sectorId);
17741
- const pieceAsPiece = { ...p, pos: { x: p.x, y: p.y } };
17742
- const allPiecesAsPieces = allPiecesInSector.map((piece) => ({ ...piece, pos: { x: piece.x, y: piece.y } }));
17743
- const hiddenEdges = getHiddenEdgesForPolygon(pieceAsPiece, idx, allPiecesAsPieces, (id) => controller.getBlueprint(id), (kind) => controller.getPrimitive(kind));
17744
- const draggedPiece = draggingId ? allPiecesInSector.find((piece) => piece.id === draggingId) : null;
17745
- let wasTouchingDraggedPiece;
17746
- if (p.blueprintId.startsWith("comp:")) {
17747
- const internalHiddenEdges = getHiddenEdgesForPolygon(pieceAsPiece, idx, [], (id) => controller.getBlueprint(id), (kind) => controller.getPrimitive(kind));
17748
- 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);
17749
- wasTouchingDraggedPiece = externalHiddenEdges.map((external, i) => external && !internalHiddenEdges[i]);
17750
- } else {
17751
- 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);
17752
- }
17753
- return generateEdgeStrokePaths(poly).map((strokePath, strokeIdx) => {
17754
- const wasHiddenDueToDraggedPiece = wasTouchingDraggedPiece[strokeIdx] || false;
17755
- let isHidden;
17756
- if (isDragging && p.blueprintId.startsWith("comp:")) {
17757
- const internalHiddenEdges = getHiddenEdgesForPolygon(pieceAsPiece, idx, [], (id) => controller.getBlueprint(id), (kind) => controller.getPrimitive(kind));
17758
- isHidden = internalHiddenEdges[strokeIdx] || false;
17759
- } else {
17760
- isHidden = isDragging ? false : (hiddenEdges[strokeIdx] || false) && !wasHiddenDueToDraggedPiece;
17761
- }
17762
- return /* @__PURE__ */ React.createElement(
17763
- "path",
17764
- {
17765
- key: `stroke-${idx}-${strokeIdx}`,
17766
- d: strokePath,
17767
- fill: "none",
17768
- stroke: isHidden ? "none" : isConnectivityLocked ? CONFIG.color.piece.invalidStroke : isCarriedInvalid ? CONFIG.color.piece.invalidStroke : isSelected || isDragging ? CONFIG.color.piece.selectedStroke : CONFIG.color.piece.borderStroke,
17769
- strokeWidth: isHidden ? 0 : isSelected || isDragging ? CONFIG.size.stroke.pieceSelectedPx : CONFIG.size.stroke.pieceBorderPx,
17770
- onPointerDown: (e) => onPiecePointerDown(e, p)
17771
- }
17772
- );
17773
- });
17774
- })()
17775
- ) ));
17604
+ ), showBorders);
17776
17605
  }));
17777
17606
  }),
17778
17607
  layout.sectors.map((s) => {
17779
- const placedPolys = placedSilBySector.get(s.id) ?? [];
17780
- if (!placedPolys.length) return null;
17781
- 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 })));
17608
+ const sectorCfg = controller.state.cfg.sectors.find((ss) => ss.id === s.id);
17609
+ if (showTangramDecomposition && sectorCfg?.silhouette.primitiveDecomposition) {
17610
+ const primitiveDecomposition = sectorCfg.silhouette.primitiveDecomposition;
17611
+ const rect = rectForBand(layout, s, "silhouette", 1);
17612
+ const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
17613
+ const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, { cx: rect.cx, cy: rect.cy });
17614
+ 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(
17615
+ "path",
17616
+ {
17617
+ d: pathD(scaledPoly),
17618
+ fill: CONFIG.color.silhouetteMask,
17619
+ opacity: CONFIG.opacity.silhouetteMask,
17620
+ stroke: "none"
17621
+ }
17622
+ ), /* @__PURE__ */ React.createElement(
17623
+ "path",
17624
+ {
17625
+ d: pathD(scaledPoly),
17626
+ fill: "none",
17627
+ stroke: CONFIG.color.tangramDecomposition.stroke,
17628
+ strokeWidth: CONFIG.size.stroke.tangramDecompositionPx
17629
+ }
17630
+ ))));
17631
+ } else {
17632
+ const placedPolys = placedSilBySector.get(s.id) ?? [];
17633
+ if (!placedPolys.length) return null;
17634
+ 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 })));
17635
+ }
17782
17636
  }),
17783
17637
  anchorDots.map(({ sectorId, valid, invalid }) => {
17784
17638
  const isInnerRing = sectorId === "inner-ring";
@@ -19296,7 +19150,7 @@ var TangramConstructPlugin = (function (jspsych) {
19296
19150
  }
19297
19151
 
19298
19152
  class InteractionTracker {
19299
- constructor(controller, callbacks, trialId, gameId) {
19153
+ constructor(controller, callbacks, trialParams) {
19300
19154
  this.gridStep = CONFIG.layout.grid.stepPx;
19301
19155
  // Interaction state
19302
19156
  this.interactionIndex = 0;
@@ -19313,8 +19167,7 @@ var TangramConstructPlugin = (function (jspsych) {
19313
19167
  this.createdMacros = [];
19314
19168
  this.controller = controller;
19315
19169
  this.callbacks = callbacks;
19316
- this.trialId = trialId || v4();
19317
- this.gameId = gameId || v4();
19170
+ this.trialParams = trialParams;
19318
19171
  this.trialStartTime = Date.now();
19319
19172
  this.controller.setTrackingCallbacks({
19320
19173
  onSectorCompleted: (sectorId) => this.recordSectorCompletion(sectorId)
@@ -19362,8 +19215,6 @@ var TangramConstructPlugin = (function (jspsych) {
19362
19215
  const event = {
19363
19216
  // Metadata
19364
19217
  interactionId: v4(),
19365
- trialId: this.trialId,
19366
- gameId: this.gameId,
19367
19218
  interactionIndex: this.interactionIndex++,
19368
19219
  // Interaction type
19369
19220
  interactionType,
@@ -19480,13 +19331,10 @@ var TangramConstructPlugin = (function (jspsych) {
19480
19331
  }));
19481
19332
  const data = {
19482
19333
  trialType: "construction",
19483
- trialId: this.trialId,
19484
- gameId: this.gameId,
19485
- trialNum: 0,
19486
- // TODO: Plugin should provide this
19487
19334
  trialStartTime: this.trialStartTime,
19488
19335
  trialEndTime,
19489
19336
  totalDuration,
19337
+ trialParams: this.trialParams,
19490
19338
  endReason,
19491
19339
  completionTimes: this.completionTimes,
19492
19340
  finalBlueprintState,
@@ -19505,13 +19353,10 @@ var TangramConstructPlugin = (function (jspsych) {
19505
19353
  }));
19506
19354
  const data = {
19507
19355
  trialType: "prep",
19508
- trialId: this.trialId,
19509
- gameId: this.gameId,
19510
- trialNum: 0,
19511
- // TODO: Plugin should provide this
19512
19356
  trialStartTime: this.trialStartTime,
19513
19357
  trialEndTime,
19514
19358
  totalDuration,
19359
+ trialParams: this.trialParams,
19515
19360
  endReason: "submit",
19516
19361
  createdMacros: finalMacros,
19517
19362
  quickstashMacros,
@@ -19769,6 +19614,9 @@ var TangramConstructPlugin = (function (jspsych) {
19769
19614
  maxQuickstashSlots,
19770
19615
  maxCompositeSize,
19771
19616
  mode,
19617
+ showTangramDecomposition,
19618
+ instructions,
19619
+ trialParams,
19772
19620
  width: _width,
19773
19621
  height: _height,
19774
19622
  onSectorComplete,
@@ -19778,6 +19626,7 @@ var TangramConstructPlugin = (function (jspsych) {
19778
19626
  onTrialEnd,
19779
19627
  onControllerReady
19780
19628
  } = props;
19629
+ const [timeRemaining, setTimeRemaining] = React.useState(timeLimitMs > 0 ? Math.floor(timeLimitMs / 1e3) : 0);
19781
19630
  const controller = React.useMemo(() => {
19782
19631
  const gameConfig = {
19783
19632
  n: sectors.length,
@@ -19804,8 +19653,8 @@ var TangramConstructPlugin = (function (jspsych) {
19804
19653
  const callbacks = {};
19805
19654
  if (onInteraction) callbacks.onInteraction = onInteraction;
19806
19655
  if (onTrialEnd) callbacks.onTrialEnd = onTrialEnd;
19807
- return new InteractionTracker(controller, callbacks);
19808
- }, [controller, onInteraction, onTrialEnd]);
19656
+ return new InteractionTracker(controller, callbacks, trialParams);
19657
+ }, [controller, onInteraction, onTrialEnd, trialParams]);
19809
19658
  React.useEffect(() => {
19810
19659
  if (onControllerReady) {
19811
19660
  onControllerReady(controller);
@@ -19885,22 +19734,22 @@ var TangramConstructPlugin = (function (jspsych) {
19885
19734
  }), [handleSectorComplete, onPiecePlace, onPieceRemove]);
19886
19735
  const getGameboardStyle = () => {
19887
19736
  const baseStyle = {
19888
- margin: "0 auto",
19737
+ margin: "10px",
19889
19738
  display: "flex",
19890
19739
  alignItems: "center",
19891
19740
  justifyContent: "center",
19892
19741
  position: "relative"
19893
19742
  };
19894
19743
  if (layoutMode === "circle") {
19895
- const size = Math.min(window.innerWidth * 0.96, window.innerHeight * 0.96);
19744
+ const size = Math.min(window.innerWidth * CONFIG.layout.viewportScale, window.innerHeight * CONFIG.layout.viewportScale);
19896
19745
  return {
19897
19746
  ...baseStyle,
19898
19747
  width: `${size}px`,
19899
19748
  height: `${size}px`
19900
19749
  };
19901
19750
  } else {
19902
- const maxWidth = Math.min(window.innerWidth * 0.96, window.innerHeight * 0.96 * 2);
19903
- const maxHeight = Math.min(window.innerWidth * 0.96 / 2, window.innerHeight * 0.96);
19751
+ const maxWidth = Math.min(window.innerWidth * CONFIG.layout.viewportScale, window.innerHeight * CONFIG.layout.viewportScale * 2);
19752
+ const maxHeight = Math.min(window.innerWidth * CONFIG.layout.viewportScale / 2, window.innerHeight * CONFIG.layout.viewportScale);
19904
19753
  return {
19905
19754
  ...baseStyle,
19906
19755
  width: `${maxWidth}px`,
@@ -20069,7 +19918,68 @@ var TangramConstructPlugin = (function (jspsych) {
20069
19918
  force();
20070
19919
  e.stopPropagation();
20071
19920
  };
20072
- return /* @__PURE__ */ React.createElement("div", { className: "tangram-gameboard", style: getGameboardStyle() }, /* @__PURE__ */ React.createElement(
19921
+ React.useEffect(() => {
19922
+ if (timeLimitMs === 0) return;
19923
+ const interval = setInterval(() => {
19924
+ setTimeRemaining((prev) => {
19925
+ if (prev <= 1) {
19926
+ clearInterval(interval);
19927
+ return 0;
19928
+ }
19929
+ return prev - 1;
19930
+ });
19931
+ }, 1e3);
19932
+ return () => clearInterval(interval);
19933
+ }, [timeLimitMs]);
19934
+ const formatTime = (seconds) => {
19935
+ const mins = Math.floor(seconds / 60);
19936
+ const secs = Math.floor(seconds % 60);
19937
+ return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
19938
+ };
19939
+ const containerStyle = {
19940
+ display: "flex",
19941
+ flexDirection: "column",
19942
+ width: "100%"
19943
+ // minHeight: '100vh'
19944
+ };
19945
+ const headerStyle = {
19946
+ display: "flex",
19947
+ flexDirection: "row",
19948
+ justifyContent: "space-between",
19949
+ alignItems: "center",
19950
+ padding: "20px",
19951
+ background: "#f5f5f5",
19952
+ flex: "0 0 auto"
19953
+ };
19954
+ const instructionsStyle = {
19955
+ flexGrow: 1,
19956
+ fontSize: "20px",
19957
+ lineHeight: 1.5,
19958
+ marginRight: "20px"
19959
+ };
19960
+ const timerStyle = {
19961
+ fontSize: "24px",
19962
+ fontWeight: "bold",
19963
+ fontFamily: "monospace",
19964
+ color: "#333",
19965
+ minWidth: "80px",
19966
+ textAlign: "right"
19967
+ };
19968
+ const gameboardWrapperStyle = {
19969
+ flex: "1 1 auto",
19970
+ display: "flex",
19971
+ alignItems: "center",
19972
+ justifyContent: "center",
19973
+ overflow: "hidden"
19974
+ };
19975
+ 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(
19976
+ "div",
19977
+ {
19978
+ className: "tangram-instructions",
19979
+ style: instructionsStyle,
19980
+ dangerouslySetInnerHTML: { __html: instructions }
19981
+ }
19982
+ ), 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(
20073
19983
  BoardView,
20074
19984
  {
20075
19985
  controller,
@@ -20095,9 +20005,11 @@ var TangramConstructPlugin = (function (jspsych) {
20095
20005
  onPointerMove,
20096
20006
  onPointerUp,
20097
20007
  onCenterBadgePointerDown,
20008
+ showTangramDecomposition: showTangramDecomposition ?? false,
20009
+ scaleS,
20098
20010
  ...eventCallbacks
20099
20011
  }
20100
- ));
20012
+ ))));
20101
20013
  }
20102
20014
 
20103
20015
  const U = 40;
@@ -20265,17 +20177,22 @@ var TangramConstructPlugin = (function (jspsych) {
20265
20177
  const isCanonical = CANON.has(tanName);
20266
20178
  return isCanonical;
20267
20179
  });
20268
- const mask = filteredTans.map((tan, tanIndex) => {
20180
+ const mask = filteredTans.map((tan) => {
20269
20181
  const polygon = tan.vertices.map(([x, y]) => ({ x: x ?? 0, y: -(y ?? 0) }));
20270
20182
  return polygon;
20271
20183
  });
20184
+ const primitiveDecomposition = filteredTans.map((tan) => ({
20185
+ kind: tan.name ?? tan.kind,
20186
+ polygon: tan.vertices.map(([x, y]) => ({ x: x ?? 0, y: -(y ?? 0) }))
20187
+ }));
20272
20188
  const sectorId = `sector${index}`;
20273
20189
  const sector = {
20274
20190
  id: sectorId,
20275
20191
  tangramId: tangramSpec.tangramID,
20276
20192
  silhouette: {
20277
20193
  id: sectorId,
20278
- mask
20194
+ mask,
20195
+ primitiveDecomposition
20279
20196
  }
20280
20197
  };
20281
20198
  return sector;
@@ -20294,6 +20211,7 @@ var TangramConstructPlugin = (function (jspsych) {
20294
20211
  quickstash = params.quickstash_macros;
20295
20212
  }
20296
20213
  }
20214
+ const { onInteraction, onTrialEnd, ...trialParams } = params;
20297
20215
  const gameBoardProps = {
20298
20216
  sectors,
20299
20217
  quickstash,
@@ -20304,6 +20222,9 @@ var TangramConstructPlugin = (function (jspsych) {
20304
20222
  timeLimitMs: params.time_limit_ms,
20305
20223
  maxQuickstashSlots: CONFIG.layout.defaults.maxQuickstashSlots,
20306
20224
  mode: "construction",
20225
+ showTangramDecomposition: params.show_tangram_decomposition ?? false,
20226
+ trialParams,
20227
+ ...params.instructions && { instructions: params.instructions },
20307
20228
  ...params.onInteraction && { onInteraction: params.onInteraction },
20308
20229
  ...params.onTrialEnd && { onTrialEnd: params.onTrialEnd }
20309
20230
  };
@@ -20367,6 +20288,18 @@ var TangramConstructPlugin = (function (jspsych) {
20367
20288
  default: 18,
20368
20289
  description: "Snap radius for anchor-based piece placement"
20369
20290
  },
20291
+ /** Whether to show tangram target shapes decomposed into individual primitives with borders */
20292
+ show_tangram_decomposition: {
20293
+ type: jspsych.ParameterType.BOOL,
20294
+ default: false,
20295
+ description: "Whether to show tangram target shapes decomposed into individual primitives with borders"
20296
+ },
20297
+ /** HTML content to display above the gameboard as instructions */
20298
+ instructions: {
20299
+ type: jspsych.ParameterType.STRING,
20300
+ default: "",
20301
+ description: "HTML content to display above the gameboard as instructions"
20302
+ },
20370
20303
  /** Callback fired after each interaction (piece pickup + placedown) */
20371
20304
  onInteraction: {
20372
20305
  type: jspsych.ParameterType.FUNCTION,
@@ -20440,6 +20373,8 @@ var TangramConstructPlugin = (function (jspsych) {
20440
20373
  input: trial.input,
20441
20374
  layout: trial.layout,
20442
20375
  time_limit_ms: trial.time_limit_ms,
20376
+ show_tangram_decomposition: trial.show_tangram_decomposition,
20377
+ instructions: trial.instructions,
20443
20378
  onInteraction: trial.onInteraction,
20444
20379
  onTrialEnd: wrappedOnTrialEnd
20445
20380
  };