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.
- package/dist/construct/index.browser.js +146 -212
- package/dist/construct/index.browser.js.map +1 -1
- package/dist/construct/index.browser.min.js +11 -11
- package/dist/construct/index.browser.min.js.map +1 -1
- package/dist/construct/index.cjs +146 -212
- package/dist/construct/index.cjs.map +1 -1
- package/dist/construct/index.d.ts +24 -0
- package/dist/construct/index.js +146 -212
- package/dist/construct/index.js.map +1 -1
- package/dist/index.cjs +157 -213
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +36 -0
- package/dist/index.js +157 -213
- package/dist/index.js.map +1 -1
- package/dist/prep/index.browser.js +132 -211
- package/dist/prep/index.browser.js.map +1 -1
- package/dist/prep/index.browser.min.js +13 -13
- package/dist/prep/index.browser.min.js.map +1 -1
- package/dist/prep/index.cjs +132 -211
- package/dist/prep/index.cjs.map +1 -1
- package/dist/prep/index.d.ts +12 -0
- package/dist/prep/index.js +132 -211
- package/dist/prep/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/components/board/BoardView.tsx +53 -14
- package/src/core/components/board/GameBoard.tsx +123 -7
- package/src/core/config/config.ts +8 -4
- package/src/core/domain/types.ts +1 -0
- package/src/core/io/InteractionTracker.ts +7 -16
- package/src/core/io/data-tracking.ts +3 -7
- package/src/plugins/tangram-construct/ConstructionApp.tsx +16 -1
- package/src/plugins/tangram-construct/index.ts +14 -0
- package/src/plugins/tangram-prep/PrepApp.tsx +6 -0
- package/src/plugins/tangram-prep/index.ts +7 -0
- package/tangram-construct.min.js +11 -11
- 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:
|
|
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
|
|
17473
|
-
|
|
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
|
|
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: "#
|
|
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
|
-
|
|
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
|
|
17781
|
-
if (
|
|
17782
|
-
|
|
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,
|
|
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.
|
|
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: "
|
|
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 *
|
|
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 *
|
|
19904
|
-
const maxHeight = Math.min(window.innerWidth *
|
|
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
|
-
|
|
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 }, (
|
|
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
|
};
|