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.
- package/dist/construct/index.browser.js +157 -222
- 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 +157 -222
- package/dist/construct/index.cjs.map +1 -1
- package/dist/construct/index.d.ts +24 -0
- package/dist/construct/index.js +157 -222
- package/dist/construct/index.js.map +1 -1
- package/dist/index.cjs +168 -223
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +36 -0
- package/dist/index.js +168 -223
- package/dist/index.js.map +1 -1
- package/dist/prep/index.browser.js +143 -221
- 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 +143 -221
- package/dist/prep/index.cjs.map +1 -1
- package/dist/prep/index.d.ts +12 -0
- package/dist/prep/index.js +143 -221
- 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 +19 -14
- 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
package/dist/construct/index.cjs
CHANGED
|
@@ -8,31 +8,34 @@ var uuid = require('uuid');
|
|
|
8
8
|
const CONFIG = {
|
|
9
9
|
color: {
|
|
10
10
|
bands: {
|
|
11
|
-
silhouette: { fillEven: "#
|
|
12
|
-
workspace: { fillEven: "#
|
|
11
|
+
silhouette: { fillEven: "#fff2ccff", fillOdd: "#fff2ccff", stroke: "#b1b1b1" },
|
|
12
|
+
workspace: { fillEven: "#fff2ccff", fillOdd: "#fff2ccff", stroke: "#b1b1b1" }
|
|
13
13
|
},
|
|
14
|
-
completion: { fill: "#
|
|
15
|
-
silhouetteMask: "#
|
|
14
|
+
completion: { fill: "#ccfff2", stroke: "#13da57" },
|
|
15
|
+
silhouetteMask: "#374151",
|
|
16
16
|
anchors: { invalid: "#7dd3fc", valid: "#475569" },
|
|
17
|
-
piece: { draggingFill: "#
|
|
17
|
+
piece: { draggingFill: "#8e7cc3ff", validFill: "#8e7cc3ff", invalidFill: "#ef4444", invalidStroke: "#dc2626", selectedStroke: "#674ea7", allGreenStroke: "#86efac", borderStroke: "#674ea7" },
|
|
18
18
|
ui: { light: "#60a5fa", dark: "#1d4ed8" },
|
|
19
|
-
blueprint: { fill: "#374151", selectedStroke: "#111827", badgeFill: "#
|
|
19
|
+
blueprint: { fill: "#374151", selectedStroke: "#111827", badgeFill: "#000000", labelFill: "#ffffff" },
|
|
20
|
+
tangramDecomposition: { stroke: "#fef2cc" }
|
|
20
21
|
},
|
|
21
22
|
opacity: {
|
|
22
|
-
blueprint: 0.
|
|
23
|
-
silhouetteMask: 0.
|
|
24
|
-
anchors: { valid: 0.
|
|
25
|
-
|
|
23
|
+
blueprint: 0.4,
|
|
24
|
+
silhouetteMask: 0.25,
|
|
25
|
+
//anchors: { valid: 0.80, invalid: 0.50 },
|
|
26
|
+
anchors: { invalid: 0, valid: 0 },
|
|
27
|
+
piece: { invalid: 0.35, dragging: 0.75, locked: 1, normal: 1 }
|
|
26
28
|
},
|
|
27
29
|
size: {
|
|
28
|
-
stroke: { bandPx:
|
|
30
|
+
stroke: { bandPx: 5, pieceSelectedPx: 3, allGreenStrokePx: 10, pieceBorderPx: 2, tangramDecompositionPx: 1 },
|
|
29
31
|
anchorRadiusPx: { valid: 1, invalid: 1 },
|
|
30
|
-
badgeFontPx:
|
|
32
|
+
badgeFontPx: 16,
|
|
31
33
|
centerBadge: { fractionOfOuterR: 0.15, minPx: 20, marginPx: 4 }
|
|
32
34
|
},
|
|
33
35
|
layout: {
|
|
34
36
|
grid: { stepPx: 20, unitPx: 40 },
|
|
35
37
|
paddingPx: 1,
|
|
38
|
+
viewportScale: 0.8,
|
|
36
39
|
constraints: {
|
|
37
40
|
workspaceDiamAnchors: 10,
|
|
38
41
|
// num anchors req'd to be on diagonal
|
|
@@ -44,7 +47,7 @@ const CONFIG = {
|
|
|
44
47
|
},
|
|
45
48
|
game: {
|
|
46
49
|
snapRadiusPx: 15,
|
|
47
|
-
showBorders:
|
|
50
|
+
showBorders: false,
|
|
48
51
|
hideTouchingBorders: true
|
|
49
52
|
}
|
|
50
53
|
};
|
|
@@ -815,150 +818,11 @@ function rectForBand(layout, sector, band, pad = 0.85) {
|
|
|
815
818
|
return { cx, cy, w, h };
|
|
816
819
|
}
|
|
817
820
|
|
|
818
|
-
function
|
|
819
|
-
|
|
820
|
-
for (let i = 0; i < poly.length; i++) {
|
|
821
|
-
const current = poly[i];
|
|
822
|
-
const next = poly[(i + 1) % poly.length];
|
|
823
|
-
if (current && next) {
|
|
824
|
-
strokePaths.push(`M ${current.x} ${current.y} L ${next.x} ${next.y}`);
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
return strokePaths;
|
|
828
|
-
}
|
|
829
|
-
function edgeToUnitSegments$1(start, end, gridSize) {
|
|
830
|
-
const segments = [];
|
|
831
|
-
const startGrid = {
|
|
832
|
-
x: Math.round(start.x / gridSize),
|
|
833
|
-
y: Math.round(start.y / gridSize)
|
|
834
|
-
};
|
|
835
|
-
const endGrid = {
|
|
836
|
-
x: Math.round(end.x / gridSize),
|
|
837
|
-
y: Math.round(end.y / gridSize)
|
|
838
|
-
};
|
|
839
|
-
const dx = endGrid.x - startGrid.x;
|
|
840
|
-
const dy = endGrid.y - startGrid.y;
|
|
841
|
-
if (dx === 0 && dy === 0) return [];
|
|
842
|
-
const steps = Math.max(Math.abs(dx), Math.abs(dy));
|
|
843
|
-
const stepX = dx / steps;
|
|
844
|
-
const stepY = dy / steps;
|
|
845
|
-
for (let i = 0; i < steps; i++) {
|
|
846
|
-
const aX = Math.round(startGrid.x + i * stepX);
|
|
847
|
-
const aY = Math.round(startGrid.y + i * stepY);
|
|
848
|
-
const bX = Math.round(startGrid.x + (i + 1) * stepX);
|
|
849
|
-
const bY = Math.round(startGrid.y + (i + 1) * stepY);
|
|
850
|
-
segments.push({
|
|
851
|
-
a: { x: aX, y: aY },
|
|
852
|
-
b: { x: bX, y: bY }
|
|
853
|
-
});
|
|
854
|
-
}
|
|
855
|
-
return segments;
|
|
856
|
-
}
|
|
857
|
-
function getHiddenEdgesForPolygon(piece, polyIndex, allPiecesInSector, getBlueprint, getPrimitive) {
|
|
858
|
-
let blueprint;
|
|
859
|
-
try {
|
|
860
|
-
blueprint = getBlueprint(piece.blueprintId);
|
|
861
|
-
} catch (error) {
|
|
862
|
-
console.warn("getBlueprint failed in getHiddenEdgesForPolygon:", error);
|
|
863
|
-
return [];
|
|
864
|
-
}
|
|
865
|
-
if (!blueprint?.shape) {
|
|
866
|
-
return [];
|
|
867
|
-
}
|
|
868
|
-
const poly = blueprint.shape[polyIndex];
|
|
869
|
-
if (!poly) return [];
|
|
870
|
-
const gridSize = CONFIG.layout.grid.stepPx;
|
|
871
|
-
const bb = boundsOfBlueprint(blueprint, getPrimitive);
|
|
872
|
-
const ox = piece.pos.x - bb.min.x;
|
|
873
|
-
const oy = piece.pos.y - bb.min.y;
|
|
874
|
-
const translatedPoly = poly.map((vertex) => ({
|
|
875
|
-
x: vertex.x + ox,
|
|
876
|
-
y: vertex.y + oy
|
|
877
|
-
}));
|
|
878
|
-
const hiddenEdges = [];
|
|
879
|
-
for (let i = 0; i < translatedPoly.length; i++) {
|
|
880
|
-
const current = translatedPoly[i];
|
|
881
|
-
const next = translatedPoly[(i + 1) % translatedPoly.length];
|
|
882
|
-
if (!current || !next) {
|
|
883
|
-
hiddenEdges.push(false);
|
|
884
|
-
continue;
|
|
885
|
-
}
|
|
886
|
-
const edgeSegments = edgeToUnitSegments$1(current, next, gridSize);
|
|
887
|
-
let isTouching = false;
|
|
888
|
-
if (piece.blueprintId.startsWith("comp:")) {
|
|
889
|
-
for (let otherPolyIndex = 0; otherPolyIndex < blueprint.shape.length; otherPolyIndex++) {
|
|
890
|
-
if (otherPolyIndex === polyIndex) continue;
|
|
891
|
-
const otherPoly = blueprint.shape[otherPolyIndex];
|
|
892
|
-
if (!otherPoly) continue;
|
|
893
|
-
const otherTranslatedPoly = otherPoly.map((vertex) => ({
|
|
894
|
-
x: vertex.x + ox,
|
|
895
|
-
y: vertex.y + oy
|
|
896
|
-
}));
|
|
897
|
-
for (let j = 0; j < otherTranslatedPoly.length; j++) {
|
|
898
|
-
const otherCurrent = otherTranslatedPoly[j];
|
|
899
|
-
const otherNext = otherTranslatedPoly[(j + 1) % otherTranslatedPoly.length];
|
|
900
|
-
if (!otherCurrent || !otherNext) continue;
|
|
901
|
-
const otherEdgeSegments = edgeToUnitSegments$1(otherCurrent, otherNext, gridSize);
|
|
902
|
-
for (const edgeSeg of edgeSegments) {
|
|
903
|
-
const isShared = otherEdgeSegments.some(
|
|
904
|
-
(otherSeg) => (
|
|
905
|
-
// Check if segments share the same endpoints (identical or reversed)
|
|
906
|
-
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
|
|
907
|
-
)
|
|
908
|
-
);
|
|
909
|
-
if (isShared) {
|
|
910
|
-
isTouching = true;
|
|
911
|
-
break;
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
if (isTouching) break;
|
|
915
|
-
}
|
|
916
|
-
if (isTouching) break;
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
if (!isTouching && CONFIG.game.hideTouchingBorders) {
|
|
920
|
-
for (const otherPiece of allPiecesInSector) {
|
|
921
|
-
if (otherPiece.id === piece.id) continue;
|
|
922
|
-
const otherBlueprint = getBlueprint(otherPiece.blueprintId);
|
|
923
|
-
if (!otherBlueprint?.shape) continue;
|
|
924
|
-
const otherBb = boundsOfBlueprint(otherBlueprint, getPrimitive);
|
|
925
|
-
const otherOx = otherPiece.pos.x - otherBb.min.x;
|
|
926
|
-
const otherOy = otherPiece.pos.y - otherBb.min.y;
|
|
927
|
-
for (const otherPoly of otherBlueprint.shape) {
|
|
928
|
-
const otherTranslatedPoly = otherPoly.map((vertex) => ({
|
|
929
|
-
x: vertex.x + otherOx,
|
|
930
|
-
y: vertex.y + otherOy
|
|
931
|
-
}));
|
|
932
|
-
for (let j = 0; j < otherTranslatedPoly.length; j++) {
|
|
933
|
-
const otherCurrent = otherTranslatedPoly[j];
|
|
934
|
-
const otherNext = otherTranslatedPoly[(j + 1) % otherTranslatedPoly.length];
|
|
935
|
-
if (!otherCurrent || !otherNext) continue;
|
|
936
|
-
const otherEdgeSegments = edgeToUnitSegments$1(otherCurrent, otherNext, gridSize);
|
|
937
|
-
for (const edgeSeg of edgeSegments) {
|
|
938
|
-
const isShared = otherEdgeSegments.some(
|
|
939
|
-
(otherSeg) => (
|
|
940
|
-
// Check if segments share the same endpoints (identical or reversed)
|
|
941
|
-
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
|
|
942
|
-
)
|
|
943
|
-
);
|
|
944
|
-
if (isShared) {
|
|
945
|
-
isTouching = true;
|
|
946
|
-
break;
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
if (isTouching) break;
|
|
950
|
-
}
|
|
951
|
-
if (isTouching) break;
|
|
952
|
-
}
|
|
953
|
-
if (isTouching) break;
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
hiddenEdges.push(isTouching);
|
|
957
|
-
}
|
|
958
|
-
return hiddenEdges;
|
|
821
|
+
function shouldShowBorders() {
|
|
822
|
+
return CONFIG.game.showBorders;
|
|
959
823
|
}
|
|
960
824
|
function shouldUseSelectiveBorders(blueprintId) {
|
|
961
|
-
return
|
|
825
|
+
return CONFIG.game.showBorders;
|
|
962
826
|
}
|
|
963
827
|
|
|
964
828
|
function pathD(poly) {
|
|
@@ -976,11 +840,13 @@ function BoardView(props) {
|
|
|
976
840
|
placedSilBySector,
|
|
977
841
|
anchorDots,
|
|
978
842
|
pieces,
|
|
843
|
+
scaleS,
|
|
979
844
|
clickMode,
|
|
980
845
|
draggingId,
|
|
981
846
|
selectedPieceId,
|
|
982
847
|
dragInvalid,
|
|
983
848
|
lockedPieceId,
|
|
849
|
+
showTangramDecomposition,
|
|
984
850
|
svgRef,
|
|
985
851
|
setPieceRef,
|
|
986
852
|
onPiecePointerDown,
|
|
@@ -1046,7 +912,7 @@ function BoardView(props) {
|
|
|
1046
912
|
onPointerDown: (e) => {
|
|
1047
913
|
onRootPointerDown(e);
|
|
1048
914
|
},
|
|
1049
|
-
style: { background: "#
|
|
915
|
+
style: { background: "#f5f5f5", touchAction: "none", userSelect: "none" }
|
|
1050
916
|
},
|
|
1051
917
|
layout.sectors.map((s, i) => {
|
|
1052
918
|
const done = !!controller.state.sectors[s.id].completedAt;
|
|
@@ -1066,11 +932,12 @@ function BoardView(props) {
|
|
|
1066
932
|
const isDragging = draggingId === p.id;
|
|
1067
933
|
const locked = p.sectorId && controller.isSectorCompleted(p.sectorId);
|
|
1068
934
|
const isConnectivityLocked = lockedPieceId === p.id;
|
|
1069
|
-
|
|
935
|
+
selectedPieceId === p.id;
|
|
1070
936
|
const isCarriedInvalid = isDragging && dragInvalid;
|
|
1071
937
|
const translateX = p.x - bb.min.x;
|
|
1072
938
|
const translateY = p.y - bb.min.y;
|
|
1073
939
|
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) => {
|
|
940
|
+
const showBorders = shouldShowBorders();
|
|
1074
941
|
shouldUseSelectiveBorders(p.blueprintId);
|
|
1075
942
|
return /* @__PURE__ */ React.createElement(React.Fragment, { key: idx }, /* @__PURE__ */ React.createElement(
|
|
1076
943
|
"path",
|
|
@@ -1081,51 +948,38 @@ function BoardView(props) {
|
|
|
1081
948
|
stroke: "none",
|
|
1082
949
|
onPointerDown: (e) => onPiecePointerDown(e, p)
|
|
1083
950
|
}
|
|
1084
|
-
),
|
|
1085
|
-
// For pieces with selective borders: render individual edge strokes with edge detection
|
|
1086
|
-
(() => {
|
|
1087
|
-
const allPiecesInSector = pieces.filter((piece) => piece.sectorId === p.sectorId);
|
|
1088
|
-
const pieceAsPiece = { ...p, pos: { x: p.x, y: p.y } };
|
|
1089
|
-
const allPiecesAsPieces = allPiecesInSector.map((piece) => ({ ...piece, pos: { x: piece.x, y: piece.y } }));
|
|
1090
|
-
const hiddenEdges = getHiddenEdgesForPolygon(pieceAsPiece, idx, allPiecesAsPieces, (id) => controller.getBlueprint(id), (kind) => controller.getPrimitive(kind));
|
|
1091
|
-
const draggedPiece = draggingId ? allPiecesInSector.find((piece) => piece.id === draggingId) : null;
|
|
1092
|
-
let wasTouchingDraggedPiece;
|
|
1093
|
-
if (p.blueprintId.startsWith("comp:")) {
|
|
1094
|
-
const internalHiddenEdges = getHiddenEdgesForPolygon(pieceAsPiece, idx, [], (id) => controller.getBlueprint(id), (kind) => controller.getPrimitive(kind));
|
|
1095
|
-
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);
|
|
1096
|
-
wasTouchingDraggedPiece = externalHiddenEdges.map((external, i) => external && !internalHiddenEdges[i]);
|
|
1097
|
-
} else {
|
|
1098
|
-
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);
|
|
1099
|
-
}
|
|
1100
|
-
return generateEdgeStrokePaths(poly).map((strokePath, strokeIdx) => {
|
|
1101
|
-
const wasHiddenDueToDraggedPiece = wasTouchingDraggedPiece[strokeIdx] || false;
|
|
1102
|
-
let isHidden;
|
|
1103
|
-
if (isDragging && p.blueprintId.startsWith("comp:")) {
|
|
1104
|
-
const internalHiddenEdges = getHiddenEdgesForPolygon(pieceAsPiece, idx, [], (id) => controller.getBlueprint(id), (kind) => controller.getPrimitive(kind));
|
|
1105
|
-
isHidden = internalHiddenEdges[strokeIdx] || false;
|
|
1106
|
-
} else {
|
|
1107
|
-
isHidden = isDragging ? false : (hiddenEdges[strokeIdx] || false) && !wasHiddenDueToDraggedPiece;
|
|
1108
|
-
}
|
|
1109
|
-
return /* @__PURE__ */ React.createElement(
|
|
1110
|
-
"path",
|
|
1111
|
-
{
|
|
1112
|
-
key: `stroke-${idx}-${strokeIdx}`,
|
|
1113
|
-
d: strokePath,
|
|
1114
|
-
fill: "none",
|
|
1115
|
-
stroke: isHidden ? "none" : isConnectivityLocked ? CONFIG.color.piece.invalidStroke : isCarriedInvalid ? CONFIG.color.piece.invalidStroke : isSelected || isDragging ? CONFIG.color.piece.selectedStroke : CONFIG.color.piece.borderStroke,
|
|
1116
|
-
strokeWidth: isHidden ? 0 : isSelected || isDragging ? CONFIG.size.stroke.pieceSelectedPx : CONFIG.size.stroke.pieceBorderPx,
|
|
1117
|
-
onPointerDown: (e) => onPiecePointerDown(e, p)
|
|
1118
|
-
}
|
|
1119
|
-
);
|
|
1120
|
-
});
|
|
1121
|
-
})()
|
|
1122
|
-
) ));
|
|
951
|
+
), showBorders);
|
|
1123
952
|
}));
|
|
1124
953
|
}),
|
|
1125
954
|
layout.sectors.map((s) => {
|
|
1126
|
-
const
|
|
1127
|
-
if (
|
|
1128
|
-
|
|
955
|
+
const sectorCfg = controller.state.cfg.sectors.find((ss) => ss.id === s.id);
|
|
956
|
+
if (showTangramDecomposition && sectorCfg?.silhouette.primitiveDecomposition) {
|
|
957
|
+
const primitiveDecomposition = sectorCfg.silhouette.primitiveDecomposition;
|
|
958
|
+
const rect = rectForBand(layout, s, "silhouette", 1);
|
|
959
|
+
const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
|
|
960
|
+
const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, { cx: rect.cx, cy: rect.cy });
|
|
961
|
+
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(
|
|
962
|
+
"path",
|
|
963
|
+
{
|
|
964
|
+
d: pathD(scaledPoly),
|
|
965
|
+
fill: CONFIG.color.silhouetteMask,
|
|
966
|
+
opacity: CONFIG.opacity.silhouetteMask,
|
|
967
|
+
stroke: "none"
|
|
968
|
+
}
|
|
969
|
+
), /* @__PURE__ */ React.createElement(
|
|
970
|
+
"path",
|
|
971
|
+
{
|
|
972
|
+
d: pathD(scaledPoly),
|
|
973
|
+
fill: "none",
|
|
974
|
+
stroke: CONFIG.color.tangramDecomposition.stroke,
|
|
975
|
+
strokeWidth: CONFIG.size.stroke.tangramDecompositionPx
|
|
976
|
+
}
|
|
977
|
+
))));
|
|
978
|
+
} else {
|
|
979
|
+
const placedPolys = placedSilBySector.get(s.id) ?? [];
|
|
980
|
+
if (!placedPolys.length) return null;
|
|
981
|
+
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 })));
|
|
982
|
+
}
|
|
1129
983
|
}),
|
|
1130
984
|
anchorDots.map(({ sectorId, valid, invalid }) => {
|
|
1131
985
|
const isInnerRing = sectorId === "inner-ring";
|
|
@@ -2587,7 +2441,7 @@ function useClickController(controller, layout, pieces, clickMode, draggingId, s
|
|
|
2587
2441
|
}
|
|
2588
2442
|
|
|
2589
2443
|
class InteractionTracker {
|
|
2590
|
-
constructor(controller, callbacks,
|
|
2444
|
+
constructor(controller, callbacks, trialParams) {
|
|
2591
2445
|
this.gridStep = CONFIG.layout.grid.stepPx;
|
|
2592
2446
|
// Interaction state
|
|
2593
2447
|
this.interactionIndex = 0;
|
|
@@ -2604,8 +2458,7 @@ class InteractionTracker {
|
|
|
2604
2458
|
this.createdMacros = [];
|
|
2605
2459
|
this.controller = controller;
|
|
2606
2460
|
this.callbacks = callbacks;
|
|
2607
|
-
this.
|
|
2608
|
-
this.gameId = gameId || uuid.v4();
|
|
2461
|
+
this.trialParams = trialParams;
|
|
2609
2462
|
this.trialStartTime = Date.now();
|
|
2610
2463
|
this.controller.setTrackingCallbacks({
|
|
2611
2464
|
onSectorCompleted: (sectorId) => this.recordSectorCompletion(sectorId)
|
|
@@ -2653,8 +2506,6 @@ class InteractionTracker {
|
|
|
2653
2506
|
const event = {
|
|
2654
2507
|
// Metadata
|
|
2655
2508
|
interactionId: uuid.v4(),
|
|
2656
|
-
trialId: this.trialId,
|
|
2657
|
-
gameId: this.gameId,
|
|
2658
2509
|
interactionIndex: this.interactionIndex++,
|
|
2659
2510
|
// Interaction type
|
|
2660
2511
|
interactionType,
|
|
@@ -2771,13 +2622,10 @@ class InteractionTracker {
|
|
|
2771
2622
|
}));
|
|
2772
2623
|
const data = {
|
|
2773
2624
|
trialType: "construction",
|
|
2774
|
-
trialId: this.trialId,
|
|
2775
|
-
gameId: this.gameId,
|
|
2776
|
-
trialNum: 0,
|
|
2777
|
-
// TODO: Plugin should provide this
|
|
2778
2625
|
trialStartTime: this.trialStartTime,
|
|
2779
2626
|
trialEndTime,
|
|
2780
2627
|
totalDuration,
|
|
2628
|
+
trialParams: this.trialParams,
|
|
2781
2629
|
endReason,
|
|
2782
2630
|
completionTimes: this.completionTimes,
|
|
2783
2631
|
finalBlueprintState,
|
|
@@ -2796,13 +2644,10 @@ class InteractionTracker {
|
|
|
2796
2644
|
}));
|
|
2797
2645
|
const data = {
|
|
2798
2646
|
trialType: "prep",
|
|
2799
|
-
trialId: this.trialId,
|
|
2800
|
-
gameId: this.gameId,
|
|
2801
|
-
trialNum: 0,
|
|
2802
|
-
// TODO: Plugin should provide this
|
|
2803
2647
|
trialStartTime: this.trialStartTime,
|
|
2804
2648
|
trialEndTime,
|
|
2805
2649
|
totalDuration,
|
|
2650
|
+
trialParams: this.trialParams,
|
|
2806
2651
|
endReason: "submit",
|
|
2807
2652
|
createdMacros: finalMacros,
|
|
2808
2653
|
quickstashMacros,
|
|
@@ -3060,6 +2905,9 @@ function GameBoard(props) {
|
|
|
3060
2905
|
maxQuickstashSlots,
|
|
3061
2906
|
maxCompositeSize,
|
|
3062
2907
|
mode,
|
|
2908
|
+
showTangramDecomposition,
|
|
2909
|
+
instructions,
|
|
2910
|
+
trialParams,
|
|
3063
2911
|
width: _width,
|
|
3064
2912
|
height: _height,
|
|
3065
2913
|
onSectorComplete,
|
|
@@ -3069,6 +2917,7 @@ function GameBoard(props) {
|
|
|
3069
2917
|
onTrialEnd,
|
|
3070
2918
|
onControllerReady
|
|
3071
2919
|
} = props;
|
|
2920
|
+
const [timeRemaining, setTimeRemaining] = React.useState(timeLimitMs > 0 ? Math.floor(timeLimitMs / 1e3) : 0);
|
|
3072
2921
|
const controller = React.useMemo(() => {
|
|
3073
2922
|
const gameConfig = {
|
|
3074
2923
|
n: sectors.length,
|
|
@@ -3095,8 +2944,8 @@ function GameBoard(props) {
|
|
|
3095
2944
|
const callbacks = {};
|
|
3096
2945
|
if (onInteraction) callbacks.onInteraction = onInteraction;
|
|
3097
2946
|
if (onTrialEnd) callbacks.onTrialEnd = onTrialEnd;
|
|
3098
|
-
return new InteractionTracker(controller, callbacks);
|
|
3099
|
-
}, [controller, onInteraction, onTrialEnd]);
|
|
2947
|
+
return new InteractionTracker(controller, callbacks, trialParams);
|
|
2948
|
+
}, [controller, onInteraction, onTrialEnd, trialParams]);
|
|
3100
2949
|
React.useEffect(() => {
|
|
3101
2950
|
if (onControllerReady) {
|
|
3102
2951
|
onControllerReady(controller);
|
|
@@ -3176,22 +3025,22 @@ function GameBoard(props) {
|
|
|
3176
3025
|
}), [handleSectorComplete, onPiecePlace, onPieceRemove]);
|
|
3177
3026
|
const getGameboardStyle = () => {
|
|
3178
3027
|
const baseStyle = {
|
|
3179
|
-
margin: "
|
|
3028
|
+
margin: "10px",
|
|
3180
3029
|
display: "flex",
|
|
3181
3030
|
alignItems: "center",
|
|
3182
3031
|
justifyContent: "center",
|
|
3183
3032
|
position: "relative"
|
|
3184
3033
|
};
|
|
3185
3034
|
if (layoutMode === "circle") {
|
|
3186
|
-
const size = Math.min(window.innerWidth *
|
|
3035
|
+
const size = Math.min(window.innerWidth * CONFIG.layout.viewportScale, window.innerHeight * CONFIG.layout.viewportScale);
|
|
3187
3036
|
return {
|
|
3188
3037
|
...baseStyle,
|
|
3189
3038
|
width: `${size}px`,
|
|
3190
3039
|
height: `${size}px`
|
|
3191
3040
|
};
|
|
3192
3041
|
} else {
|
|
3193
|
-
const maxWidth = Math.min(window.innerWidth *
|
|
3194
|
-
const maxHeight = Math.min(window.innerWidth *
|
|
3042
|
+
const maxWidth = Math.min(window.innerWidth * CONFIG.layout.viewportScale, window.innerHeight * CONFIG.layout.viewportScale * 2);
|
|
3043
|
+
const maxHeight = Math.min(window.innerWidth * CONFIG.layout.viewportScale / 2, window.innerHeight * CONFIG.layout.viewportScale);
|
|
3195
3044
|
return {
|
|
3196
3045
|
...baseStyle,
|
|
3197
3046
|
width: `${maxWidth}px`,
|
|
@@ -3360,7 +3209,68 @@ function GameBoard(props) {
|
|
|
3360
3209
|
force();
|
|
3361
3210
|
e.stopPropagation();
|
|
3362
3211
|
};
|
|
3363
|
-
|
|
3212
|
+
React.useEffect(() => {
|
|
3213
|
+
if (timeLimitMs === 0) return;
|
|
3214
|
+
const interval = setInterval(() => {
|
|
3215
|
+
setTimeRemaining((prev) => {
|
|
3216
|
+
if (prev <= 1) {
|
|
3217
|
+
clearInterval(interval);
|
|
3218
|
+
return 0;
|
|
3219
|
+
}
|
|
3220
|
+
return prev - 1;
|
|
3221
|
+
});
|
|
3222
|
+
}, 1e3);
|
|
3223
|
+
return () => clearInterval(interval);
|
|
3224
|
+
}, [timeLimitMs]);
|
|
3225
|
+
const formatTime = (seconds) => {
|
|
3226
|
+
const mins = Math.floor(seconds / 60);
|
|
3227
|
+
const secs = Math.floor(seconds % 60);
|
|
3228
|
+
return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
|
|
3229
|
+
};
|
|
3230
|
+
const containerStyle = {
|
|
3231
|
+
display: "flex",
|
|
3232
|
+
flexDirection: "column",
|
|
3233
|
+
width: "100%"
|
|
3234
|
+
// minHeight: '100vh'
|
|
3235
|
+
};
|
|
3236
|
+
const headerStyle = {
|
|
3237
|
+
display: "flex",
|
|
3238
|
+
flexDirection: "row",
|
|
3239
|
+
justifyContent: "space-between",
|
|
3240
|
+
alignItems: "center",
|
|
3241
|
+
padding: "20px",
|
|
3242
|
+
background: "#f5f5f5",
|
|
3243
|
+
flex: "0 0 auto"
|
|
3244
|
+
};
|
|
3245
|
+
const instructionsStyle = {
|
|
3246
|
+
flexGrow: 1,
|
|
3247
|
+
fontSize: "20px",
|
|
3248
|
+
lineHeight: 1.5,
|
|
3249
|
+
marginRight: "20px"
|
|
3250
|
+
};
|
|
3251
|
+
const timerStyle = {
|
|
3252
|
+
fontSize: "24px",
|
|
3253
|
+
fontWeight: "bold",
|
|
3254
|
+
fontFamily: "monospace",
|
|
3255
|
+
color: "#333",
|
|
3256
|
+
minWidth: "80px",
|
|
3257
|
+
textAlign: "right"
|
|
3258
|
+
};
|
|
3259
|
+
const gameboardWrapperStyle = {
|
|
3260
|
+
flex: "1 1 auto",
|
|
3261
|
+
display: "flex",
|
|
3262
|
+
alignItems: "center",
|
|
3263
|
+
justifyContent: "center",
|
|
3264
|
+
overflow: "hidden"
|
|
3265
|
+
};
|
|
3266
|
+
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(
|
|
3267
|
+
"div",
|
|
3268
|
+
{
|
|
3269
|
+
className: "tangram-instructions",
|
|
3270
|
+
style: instructionsStyle,
|
|
3271
|
+
dangerouslySetInnerHTML: { __html: instructions }
|
|
3272
|
+
}
|
|
3273
|
+
), 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(
|
|
3364
3274
|
BoardView,
|
|
3365
3275
|
{
|
|
3366
3276
|
controller,
|
|
@@ -3386,9 +3296,11 @@ function GameBoard(props) {
|
|
|
3386
3296
|
onPointerMove,
|
|
3387
3297
|
onPointerUp,
|
|
3388
3298
|
onCenterBadgePointerDown,
|
|
3299
|
+
showTangramDecomposition: showTangramDecomposition ?? false,
|
|
3300
|
+
scaleS,
|
|
3389
3301
|
...eventCallbacks
|
|
3390
3302
|
}
|
|
3391
|
-
));
|
|
3303
|
+
))));
|
|
3392
3304
|
}
|
|
3393
3305
|
|
|
3394
3306
|
const U = 40;
|
|
@@ -3556,17 +3468,22 @@ function startConstructionTrial(display_element, params, _jsPsych) {
|
|
|
3556
3468
|
const isCanonical = CANON.has(tanName);
|
|
3557
3469
|
return isCanonical;
|
|
3558
3470
|
});
|
|
3559
|
-
const mask = filteredTans.map((tan
|
|
3471
|
+
const mask = filteredTans.map((tan) => {
|
|
3560
3472
|
const polygon = tan.vertices.map(([x, y]) => ({ x: x ?? 0, y: -(y ?? 0) }));
|
|
3561
3473
|
return polygon;
|
|
3562
3474
|
});
|
|
3475
|
+
const primitiveDecomposition = filteredTans.map((tan) => ({
|
|
3476
|
+
kind: tan.name ?? tan.kind,
|
|
3477
|
+
polygon: tan.vertices.map(([x, y]) => ({ x: x ?? 0, y: -(y ?? 0) }))
|
|
3478
|
+
}));
|
|
3563
3479
|
const sectorId = `sector${index}`;
|
|
3564
3480
|
const sector = {
|
|
3565
3481
|
id: sectorId,
|
|
3566
3482
|
tangramId: tangramSpec.tangramID,
|
|
3567
3483
|
silhouette: {
|
|
3568
3484
|
id: sectorId,
|
|
3569
|
-
mask
|
|
3485
|
+
mask,
|
|
3486
|
+
primitiveDecomposition
|
|
3570
3487
|
}
|
|
3571
3488
|
};
|
|
3572
3489
|
return sector;
|
|
@@ -3585,6 +3502,7 @@ function startConstructionTrial(display_element, params, _jsPsych) {
|
|
|
3585
3502
|
quickstash = params.quickstash_macros;
|
|
3586
3503
|
}
|
|
3587
3504
|
}
|
|
3505
|
+
const { onInteraction, onTrialEnd, ...trialParams } = params;
|
|
3588
3506
|
const gameBoardProps = {
|
|
3589
3507
|
sectors,
|
|
3590
3508
|
quickstash,
|
|
@@ -3595,6 +3513,9 @@ function startConstructionTrial(display_element, params, _jsPsych) {
|
|
|
3595
3513
|
timeLimitMs: params.time_limit_ms,
|
|
3596
3514
|
maxQuickstashSlots: CONFIG.layout.defaults.maxQuickstashSlots,
|
|
3597
3515
|
mode: "construction",
|
|
3516
|
+
showTangramDecomposition: params.show_tangram_decomposition ?? false,
|
|
3517
|
+
trialParams,
|
|
3518
|
+
...params.instructions && { instructions: params.instructions },
|
|
3598
3519
|
...params.onInteraction && { onInteraction: params.onInteraction },
|
|
3599
3520
|
...params.onTrialEnd && { onTrialEnd: params.onTrialEnd }
|
|
3600
3521
|
};
|
|
@@ -3658,6 +3579,18 @@ const info = {
|
|
|
3658
3579
|
default: 18,
|
|
3659
3580
|
description: "Snap radius for anchor-based piece placement"
|
|
3660
3581
|
},
|
|
3582
|
+
/** Whether to show tangram target shapes decomposed into individual primitives with borders */
|
|
3583
|
+
show_tangram_decomposition: {
|
|
3584
|
+
type: jspsych.ParameterType.BOOL,
|
|
3585
|
+
default: false,
|
|
3586
|
+
description: "Whether to show tangram target shapes decomposed into individual primitives with borders"
|
|
3587
|
+
},
|
|
3588
|
+
/** HTML content to display above the gameboard as instructions */
|
|
3589
|
+
instructions: {
|
|
3590
|
+
type: jspsych.ParameterType.STRING,
|
|
3591
|
+
default: "",
|
|
3592
|
+
description: "HTML content to display above the gameboard as instructions"
|
|
3593
|
+
},
|
|
3661
3594
|
/** Callback fired after each interaction (piece pickup + placedown) */
|
|
3662
3595
|
onInteraction: {
|
|
3663
3596
|
type: jspsych.ParameterType.FUNCTION,
|
|
@@ -3731,6 +3664,8 @@ class TangramConstructPlugin {
|
|
|
3731
3664
|
input: trial.input,
|
|
3732
3665
|
layout: trial.layout,
|
|
3733
3666
|
time_limit_ms: trial.time_limit_ms,
|
|
3667
|
+
show_tangram_decomposition: trial.show_tangram_decomposition,
|
|
3668
|
+
instructions: trial.instructions,
|
|
3734
3669
|
onInteraction: trial.onInteraction,
|
|
3735
3670
|
onTrialEnd: wrappedOnTrialEnd
|
|
3736
3671
|
};
|