jspsych-tangram 0.0.7 → 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 +146 -212
  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 +146 -212
  6. package/dist/construct/index.cjs.map +1 -1
  7. package/dist/construct/index.d.ts +24 -0
  8. package/dist/construct/index.js +146 -212
  9. package/dist/construct/index.js.map +1 -1
  10. package/dist/index.cjs +157 -213
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.ts +36 -0
  13. package/dist/index.js +157 -213
  14. package/dist/index.js.map +1 -1
  15. package/dist/prep/index.browser.js +132 -211
  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 +132 -211
  20. package/dist/prep/index.cjs.map +1 -1
  21. package/dist/prep/index.d.ts +12 -0
  22. package/dist/prep/index.js +132 -211
  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 +8 -4
  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
@@ -16669,7 +16669,8 @@ var TangramPrepPlugin = (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 TangramPrepPlugin = (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 TangramPrepPlugin = (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 TangramPrepPlugin = (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,150 +17471,11 @@ var TangramPrepPlugin = (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
 
17618
17481
  function pathD(poly) {
@@ -17630,11 +17493,13 @@ var TangramPrepPlugin = (function (jspsych) {
17630
17493
  placedSilBySector,
17631
17494
  anchorDots,
17632
17495
  pieces,
17496
+ scaleS,
17633
17497
  clickMode,
17634
17498
  draggingId,
17635
17499
  selectedPieceId,
17636
17500
  dragInvalid,
17637
17501
  lockedPieceId,
17502
+ showTangramDecomposition,
17638
17503
  svgRef,
17639
17504
  setPieceRef,
17640
17505
  onPiecePointerDown,
@@ -17700,7 +17565,7 @@ var TangramPrepPlugin = (function (jspsych) {
17700
17565
  onPointerDown: (e) => {
17701
17566
  onRootPointerDown(e);
17702
17567
  },
17703
- style: { background: "#fff", touchAction: "none", userSelect: "none" }
17568
+ style: { background: "#f5f5f5", touchAction: "none", userSelect: "none" }
17704
17569
  },
17705
17570
  layout.sectors.map((s, i) => {
17706
17571
  const done = !!controller.state.sectors[s.id].completedAt;
@@ -17720,11 +17585,12 @@ var TangramPrepPlugin = (function (jspsych) {
17720
17585
  const isDragging = draggingId === p.id;
17721
17586
  const locked = p.sectorId && controller.isSectorCompleted(p.sectorId);
17722
17587
  const isConnectivityLocked = lockedPieceId === p.id;
17723
- const isSelected = selectedPieceId === p.id;
17588
+ selectedPieceId === p.id;
17724
17589
  const isCarriedInvalid = isDragging && dragInvalid;
17725
17590
  const translateX = p.x - bb.min.x;
17726
17591
  const translateY = p.y - bb.min.y;
17727
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();
17728
17594
  shouldUseSelectiveBorders(p.blueprintId);
17729
17595
  return /* @__PURE__ */ React.createElement(React.Fragment, { key: idx }, /* @__PURE__ */ React.createElement(
17730
17596
  "path",
@@ -17735,51 +17601,38 @@ var TangramPrepPlugin = (function (jspsych) {
17735
17601
  stroke: "none",
17736
17602
  onPointerDown: (e) => onPiecePointerDown(e, p)
17737
17603
  }
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
- ) ));
17604
+ ), showBorders);
17777
17605
  }));
17778
17606
  }),
17779
17607
  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 })));
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
+ }
17783
17636
  }),
17784
17637
  anchorDots.map(({ sectorId, valid, invalid }) => {
17785
17638
  const isInnerRing = sectorId === "inner-ring";
@@ -19297,7 +19150,7 @@ var TangramPrepPlugin = (function (jspsych) {
19297
19150
  }
19298
19151
 
19299
19152
  class InteractionTracker {
19300
- constructor(controller, callbacks, trialId, gameId) {
19153
+ constructor(controller, callbacks, trialParams) {
19301
19154
  this.gridStep = CONFIG.layout.grid.stepPx;
19302
19155
  // Interaction state
19303
19156
  this.interactionIndex = 0;
@@ -19314,8 +19167,7 @@ var TangramPrepPlugin = (function (jspsych) {
19314
19167
  this.createdMacros = [];
19315
19168
  this.controller = controller;
19316
19169
  this.callbacks = callbacks;
19317
- this.trialId = trialId || v4();
19318
- this.gameId = gameId || v4();
19170
+ this.trialParams = trialParams;
19319
19171
  this.trialStartTime = Date.now();
19320
19172
  this.controller.setTrackingCallbacks({
19321
19173
  onSectorCompleted: (sectorId) => this.recordSectorCompletion(sectorId)
@@ -19363,8 +19215,6 @@ var TangramPrepPlugin = (function (jspsych) {
19363
19215
  const event = {
19364
19216
  // Metadata
19365
19217
  interactionId: v4(),
19366
- trialId: this.trialId,
19367
- gameId: this.gameId,
19368
19218
  interactionIndex: this.interactionIndex++,
19369
19219
  // Interaction type
19370
19220
  interactionType,
@@ -19481,13 +19331,10 @@ var TangramPrepPlugin = (function (jspsych) {
19481
19331
  }));
19482
19332
  const data = {
19483
19333
  trialType: "construction",
19484
- trialId: this.trialId,
19485
- gameId: this.gameId,
19486
- trialNum: 0,
19487
- // TODO: Plugin should provide this
19488
19334
  trialStartTime: this.trialStartTime,
19489
19335
  trialEndTime,
19490
19336
  totalDuration,
19337
+ trialParams: this.trialParams,
19491
19338
  endReason,
19492
19339
  completionTimes: this.completionTimes,
19493
19340
  finalBlueprintState,
@@ -19506,13 +19353,10 @@ var TangramPrepPlugin = (function (jspsych) {
19506
19353
  }));
19507
19354
  const data = {
19508
19355
  trialType: "prep",
19509
- trialId: this.trialId,
19510
- gameId: this.gameId,
19511
- trialNum: 0,
19512
- // TODO: Plugin should provide this
19513
19356
  trialStartTime: this.trialStartTime,
19514
19357
  trialEndTime,
19515
19358
  totalDuration,
19359
+ trialParams: this.trialParams,
19516
19360
  endReason: "submit",
19517
19361
  createdMacros: finalMacros,
19518
19362
  quickstashMacros,
@@ -19770,6 +19614,9 @@ var TangramPrepPlugin = (function (jspsych) {
19770
19614
  maxQuickstashSlots,
19771
19615
  maxCompositeSize,
19772
19616
  mode,
19617
+ showTangramDecomposition,
19618
+ instructions,
19619
+ trialParams,
19773
19620
  width: _width,
19774
19621
  height: _height,
19775
19622
  onSectorComplete,
@@ -19779,6 +19626,7 @@ var TangramPrepPlugin = (function (jspsych) {
19779
19626
  onTrialEnd,
19780
19627
  onControllerReady
19781
19628
  } = props;
19629
+ const [timeRemaining, setTimeRemaining] = React.useState(timeLimitMs > 0 ? Math.floor(timeLimitMs / 1e3) : 0);
19782
19630
  const controller = React.useMemo(() => {
19783
19631
  const gameConfig = {
19784
19632
  n: sectors.length,
@@ -19805,8 +19653,8 @@ var TangramPrepPlugin = (function (jspsych) {
19805
19653
  const callbacks = {};
19806
19654
  if (onInteraction) callbacks.onInteraction = onInteraction;
19807
19655
  if (onTrialEnd) callbacks.onTrialEnd = onTrialEnd;
19808
- return new InteractionTracker(controller, callbacks);
19809
- }, [controller, onInteraction, onTrialEnd]);
19656
+ return new InteractionTracker(controller, callbacks, trialParams);
19657
+ }, [controller, onInteraction, onTrialEnd, trialParams]);
19810
19658
  React.useEffect(() => {
19811
19659
  if (onControllerReady) {
19812
19660
  onControllerReady(controller);
@@ -19886,22 +19734,22 @@ var TangramPrepPlugin = (function (jspsych) {
19886
19734
  }), [handleSectorComplete, onPiecePlace, onPieceRemove]);
19887
19735
  const getGameboardStyle = () => {
19888
19736
  const baseStyle = {
19889
- margin: "0 auto",
19737
+ margin: "10px",
19890
19738
  display: "flex",
19891
19739
  alignItems: "center",
19892
19740
  justifyContent: "center",
19893
19741
  position: "relative"
19894
19742
  };
19895
19743
  if (layoutMode === "circle") {
19896
- 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);
19897
19745
  return {
19898
19746
  ...baseStyle,
19899
19747
  width: `${size}px`,
19900
19748
  height: `${size}px`
19901
19749
  };
19902
19750
  } 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);
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);
19905
19753
  return {
19906
19754
  ...baseStyle,
19907
19755
  width: `${maxWidth}px`,
@@ -20070,7 +19918,68 @@ var TangramPrepPlugin = (function (jspsych) {
20070
19918
  force();
20071
19919
  e.stopPropagation();
20072
19920
  };
20073
- 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(
20074
19983
  BoardView,
20075
19984
  {
20076
19985
  controller,
@@ -20096,9 +20005,11 @@ var TangramPrepPlugin = (function (jspsych) {
20096
20005
  onPointerMove,
20097
20006
  onPointerUp,
20098
20007
  onCenterBadgePointerDown,
20008
+ showTangramDecomposition: showTangramDecomposition ?? false,
20009
+ scaleS,
20099
20010
  ...eventCallbacks
20100
20011
  }
20101
- ));
20012
+ ))));
20102
20013
  }
20103
20014
 
20104
20015
  const U = 40;
@@ -20264,8 +20175,9 @@ var TangramPrepPlugin = (function (jspsych) {
20264
20175
  onInteraction,
20265
20176
  onTrialEnd
20266
20177
  } = params;
20178
+ const { onInteraction: _, onTrialEnd: __, ...trialParams } = params;
20267
20179
  const PRIMITIVE_BLUEPRINTS_ORDERED = [...PRIMITIVE_BLUEPRINTS].sort((a, b) => primitiveOrder.indexOf(a.kind) - primitiveOrder.indexOf(b.kind));
20268
- const prepSectors = Array.from({ length: numQuickstashSlots }, (_, i) => ({
20180
+ const prepSectors = Array.from({ length: numQuickstashSlots }, (_2, i) => ({
20269
20181
  id: `prep-sector-${i}`,
20270
20182
  tangramId: `prep-sector-${i}`,
20271
20183
  // dummy value since prep mode doesn't have tangrams
@@ -20349,6 +20261,8 @@ var TangramPrepPlugin = (function (jspsych) {
20349
20261
  // Enable prep-specific behavior
20350
20262
  minPiecesPerMacro,
20351
20263
  requireAllSlots,
20264
+ trialParams,
20265
+ ...params.instructions && { instructions: params.instructions },
20352
20266
  onControllerReady: handleControllerReady,
20353
20267
  ...onInteraction && { onInteraction },
20354
20268
  ...onTrialEnd && { onTrialEnd }
@@ -20402,6 +20316,12 @@ var TangramPrepPlugin = (function (jspsych) {
20402
20316
  default: ["square", "smalltriangle", "parallelogram", "medtriangle", "largetriangle"],
20403
20317
  description: "Array of primitive names in the order they should be displayed"
20404
20318
  },
20319
+ /** HTML content to display above the gameboard as instructions */
20320
+ instructions: {
20321
+ type: jspsych.ParameterType.STRING,
20322
+ default: void 0,
20323
+ description: "HTML content to display above the gameboard as instructions"
20324
+ },
20405
20325
  /** Callback fired after each interaction (optional analytics hook) */
20406
20326
  onInteraction: {
20407
20327
  type: jspsych.ParameterType.FUNCTION,
@@ -20453,6 +20373,7 @@ var TangramPrepPlugin = (function (jspsych) {
20453
20373
  requireAllSlots: trial.require_all_slots,
20454
20374
  quickstashMacros: trial.quickstash_macros,
20455
20375
  primitiveOrder: trial.primitive_order,
20376
+ instructions: trial.instructions,
20456
20377
  onInteraction: trial.onInteraction,
20457
20378
  onTrialEnd: wrappedOnTrialEnd
20458
20379
  };