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 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:
|
|
16703
|
+
showBorders: false,
|
|
16702
16704
|
hideTouchingBorders: true
|
|
16703
16705
|
}
|
|
16704
16706
|
};
|
|
@@ -17469,150 +17471,11 @@ var TangramConstructPlugin = (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 TangramConstructPlugin = (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 TangramConstructPlugin = (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 TangramConstructPlugin = (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 TangramConstructPlugin = (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 TangramConstructPlugin = (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 TangramConstructPlugin = (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 TangramConstructPlugin = (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 TangramConstructPlugin = (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 TangramConstructPlugin = (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 TangramConstructPlugin = (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 TangramConstructPlugin = (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 TangramConstructPlugin = (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 TangramConstructPlugin = (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 TangramConstructPlugin = (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 TangramConstructPlugin = (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;
|
|
@@ -20266,17 +20177,22 @@ var TangramConstructPlugin = (function (jspsych) {
|
|
|
20266
20177
|
const isCanonical = CANON.has(tanName);
|
|
20267
20178
|
return isCanonical;
|
|
20268
20179
|
});
|
|
20269
|
-
const mask = filteredTans.map((tan
|
|
20180
|
+
const mask = filteredTans.map((tan) => {
|
|
20270
20181
|
const polygon = tan.vertices.map(([x, y]) => ({ x: x ?? 0, y: -(y ?? 0) }));
|
|
20271
20182
|
return polygon;
|
|
20272
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
|
+
}));
|
|
20273
20188
|
const sectorId = `sector${index}`;
|
|
20274
20189
|
const sector = {
|
|
20275
20190
|
id: sectorId,
|
|
20276
20191
|
tangramId: tangramSpec.tangramID,
|
|
20277
20192
|
silhouette: {
|
|
20278
20193
|
id: sectorId,
|
|
20279
|
-
mask
|
|
20194
|
+
mask,
|
|
20195
|
+
primitiveDecomposition
|
|
20280
20196
|
}
|
|
20281
20197
|
};
|
|
20282
20198
|
return sector;
|
|
@@ -20295,6 +20211,7 @@ var TangramConstructPlugin = (function (jspsych) {
|
|
|
20295
20211
|
quickstash = params.quickstash_macros;
|
|
20296
20212
|
}
|
|
20297
20213
|
}
|
|
20214
|
+
const { onInteraction, onTrialEnd, ...trialParams } = params;
|
|
20298
20215
|
const gameBoardProps = {
|
|
20299
20216
|
sectors,
|
|
20300
20217
|
quickstash,
|
|
@@ -20305,6 +20222,9 @@ var TangramConstructPlugin = (function (jspsych) {
|
|
|
20305
20222
|
timeLimitMs: params.time_limit_ms,
|
|
20306
20223
|
maxQuickstashSlots: CONFIG.layout.defaults.maxQuickstashSlots,
|
|
20307
20224
|
mode: "construction",
|
|
20225
|
+
showTangramDecomposition: params.show_tangram_decomposition ?? false,
|
|
20226
|
+
trialParams,
|
|
20227
|
+
...params.instructions && { instructions: params.instructions },
|
|
20308
20228
|
...params.onInteraction && { onInteraction: params.onInteraction },
|
|
20309
20229
|
...params.onTrialEnd && { onTrialEnd: params.onTrialEnd }
|
|
20310
20230
|
};
|
|
@@ -20368,6 +20288,18 @@ var TangramConstructPlugin = (function (jspsych) {
|
|
|
20368
20288
|
default: 18,
|
|
20369
20289
|
description: "Snap radius for anchor-based piece placement"
|
|
20370
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
|
+
},
|
|
20371
20303
|
/** Callback fired after each interaction (piece pickup + placedown) */
|
|
20372
20304
|
onInteraction: {
|
|
20373
20305
|
type: jspsych.ParameterType.FUNCTION,
|
|
@@ -20441,6 +20373,8 @@ var TangramConstructPlugin = (function (jspsych) {
|
|
|
20441
20373
|
input: trial.input,
|
|
20442
20374
|
layout: trial.layout,
|
|
20443
20375
|
time_limit_ms: trial.time_limit_ms,
|
|
20376
|
+
show_tangram_decomposition: trial.show_tangram_decomposition,
|
|
20377
|
+
instructions: trial.instructions,
|
|
20444
20378
|
onInteraction: trial.onInteraction,
|
|
20445
20379
|
onTrialEnd: wrappedOnTrialEnd
|
|
20446
20380
|
};
|