jspsych-tangram 0.0.7 → 0.0.9
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 +187 -214
- package/dist/construct/index.browser.js.map +1 -1
- package/dist/construct/index.browser.min.js +13 -10
- package/dist/construct/index.browser.min.js.map +1 -1
- package/dist/construct/index.cjs +187 -214
- package/dist/construct/index.cjs.map +1 -1
- package/dist/construct/index.d.ts +24 -0
- package/dist/construct/index.js +187 -214
- package/dist/construct/index.js.map +1 -1
- package/dist/index.cjs +198 -215
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +36 -0
- package/dist/index.js +198 -215
- package/dist/index.js.map +1 -1
- package/dist/prep/index.browser.js +173 -213
- package/dist/prep/index.browser.js.map +1 -1
- package/dist/prep/index.browser.min.js +14 -11
- package/dist/prep/index.browser.min.js.map +1 -1
- package/dist/prep/index.cjs +173 -213
- package/dist/prep/index.cjs.map +1 -1
- package/dist/prep/index.d.ts +12 -0
- package/dist/prep/index.js +173 -213
- package/dist/prep/index.js.map +1 -1
- package/package.json +1 -1
- package/src/assets/README.md +6 -0
- package/src/assets/images.d.ts +19 -0
- package/src/assets/locked.png +0 -0
- package/src/assets/unlocked.png +0 -0
- package/src/core/components/board/BoardView.tsx +121 -39
- 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 +23 -24
- package/src/core/io/data-tracking.ts +6 -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 +13 -10
- package/tangram-prep.min.js +14 -11
package/dist/construct/index.cjs
CHANGED
|
@@ -16,7 +16,8 @@ const CONFIG = {
|
|
|
16
16
|
anchors: { invalid: "#7dd3fc", valid: "#475569" },
|
|
17
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: "#000000", labelFill: "#ffffff" }
|
|
19
|
+
blueprint: { fill: "#374151", selectedStroke: "#111827", badgeFill: "#000000", labelFill: "#ffffff" },
|
|
20
|
+
tangramDecomposition: { stroke: "#fef2cc" }
|
|
20
21
|
},
|
|
21
22
|
opacity: {
|
|
22
23
|
blueprint: 0.4,
|
|
@@ -26,7 +27,7 @@ const CONFIG = {
|
|
|
26
27
|
piece: { invalid: 0.35, dragging: 0.75, locked: 1, normal: 1 }
|
|
27
28
|
},
|
|
28
29
|
size: {
|
|
29
|
-
stroke: { bandPx: 5, pieceSelectedPx: 3, allGreenStrokePx: 10, pieceBorderPx: 2 },
|
|
30
|
+
stroke: { bandPx: 5, pieceSelectedPx: 3, allGreenStrokePx: 10, pieceBorderPx: 2, tangramDecompositionPx: 1 },
|
|
30
31
|
anchorRadiusPx: { valid: 1, invalid: 1 },
|
|
31
32
|
badgeFontPx: 16,
|
|
32
33
|
centerBadge: { fractionOfOuterR: 0.15, minPx: 20, marginPx: 4 }
|
|
@@ -34,6 +35,7 @@ const CONFIG = {
|
|
|
34
35
|
layout: {
|
|
35
36
|
grid: { stepPx: 20, unitPx: 40 },
|
|
36
37
|
paddingPx: 1,
|
|
38
|
+
viewportScale: 0.8,
|
|
37
39
|
constraints: {
|
|
38
40
|
workspaceDiamAnchors: 10,
|
|
39
41
|
// num anchors req'd to be on diagonal
|
|
@@ -45,7 +47,7 @@ const CONFIG = {
|
|
|
45
47
|
},
|
|
46
48
|
game: {
|
|
47
49
|
snapRadiusPx: 15,
|
|
48
|
-
showBorders:
|
|
50
|
+
showBorders: false,
|
|
49
51
|
hideTouchingBorders: true
|
|
50
52
|
}
|
|
51
53
|
};
|
|
@@ -816,152 +818,17 @@ function rectForBand(layout, sector, band, pad = 0.85) {
|
|
|
816
818
|
return { cx, cy, w, h };
|
|
817
819
|
}
|
|
818
820
|
|
|
819
|
-
function
|
|
820
|
-
|
|
821
|
-
for (let i = 0; i < poly.length; i++) {
|
|
822
|
-
const current = poly[i];
|
|
823
|
-
const next = poly[(i + 1) % poly.length];
|
|
824
|
-
if (current && next) {
|
|
825
|
-
strokePaths.push(`M ${current.x} ${current.y} L ${next.x} ${next.y}`);
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
return strokePaths;
|
|
829
|
-
}
|
|
830
|
-
function edgeToUnitSegments$1(start, end, gridSize) {
|
|
831
|
-
const segments = [];
|
|
832
|
-
const startGrid = {
|
|
833
|
-
x: Math.round(start.x / gridSize),
|
|
834
|
-
y: Math.round(start.y / gridSize)
|
|
835
|
-
};
|
|
836
|
-
const endGrid = {
|
|
837
|
-
x: Math.round(end.x / gridSize),
|
|
838
|
-
y: Math.round(end.y / gridSize)
|
|
839
|
-
};
|
|
840
|
-
const dx = endGrid.x - startGrid.x;
|
|
841
|
-
const dy = endGrid.y - startGrid.y;
|
|
842
|
-
if (dx === 0 && dy === 0) return [];
|
|
843
|
-
const steps = Math.max(Math.abs(dx), Math.abs(dy));
|
|
844
|
-
const stepX = dx / steps;
|
|
845
|
-
const stepY = dy / steps;
|
|
846
|
-
for (let i = 0; i < steps; i++) {
|
|
847
|
-
const aX = Math.round(startGrid.x + i * stepX);
|
|
848
|
-
const aY = Math.round(startGrid.y + i * stepY);
|
|
849
|
-
const bX = Math.round(startGrid.x + (i + 1) * stepX);
|
|
850
|
-
const bY = Math.round(startGrid.y + (i + 1) * stepY);
|
|
851
|
-
segments.push({
|
|
852
|
-
a: { x: aX, y: aY },
|
|
853
|
-
b: { x: bX, y: bY }
|
|
854
|
-
});
|
|
855
|
-
}
|
|
856
|
-
return segments;
|
|
857
|
-
}
|
|
858
|
-
function getHiddenEdgesForPolygon(piece, polyIndex, allPiecesInSector, getBlueprint, getPrimitive) {
|
|
859
|
-
let blueprint;
|
|
860
|
-
try {
|
|
861
|
-
blueprint = getBlueprint(piece.blueprintId);
|
|
862
|
-
} catch (error) {
|
|
863
|
-
console.warn("getBlueprint failed in getHiddenEdgesForPolygon:", error);
|
|
864
|
-
return [];
|
|
865
|
-
}
|
|
866
|
-
if (!blueprint?.shape) {
|
|
867
|
-
return [];
|
|
868
|
-
}
|
|
869
|
-
const poly = blueprint.shape[polyIndex];
|
|
870
|
-
if (!poly) return [];
|
|
871
|
-
const gridSize = CONFIG.layout.grid.stepPx;
|
|
872
|
-
const bb = boundsOfBlueprint(blueprint, getPrimitive);
|
|
873
|
-
const ox = piece.pos.x - bb.min.x;
|
|
874
|
-
const oy = piece.pos.y - bb.min.y;
|
|
875
|
-
const translatedPoly = poly.map((vertex) => ({
|
|
876
|
-
x: vertex.x + ox,
|
|
877
|
-
y: vertex.y + oy
|
|
878
|
-
}));
|
|
879
|
-
const hiddenEdges = [];
|
|
880
|
-
for (let i = 0; i < translatedPoly.length; i++) {
|
|
881
|
-
const current = translatedPoly[i];
|
|
882
|
-
const next = translatedPoly[(i + 1) % translatedPoly.length];
|
|
883
|
-
if (!current || !next) {
|
|
884
|
-
hiddenEdges.push(false);
|
|
885
|
-
continue;
|
|
886
|
-
}
|
|
887
|
-
const edgeSegments = edgeToUnitSegments$1(current, next, gridSize);
|
|
888
|
-
let isTouching = false;
|
|
889
|
-
if (piece.blueprintId.startsWith("comp:")) {
|
|
890
|
-
for (let otherPolyIndex = 0; otherPolyIndex < blueprint.shape.length; otherPolyIndex++) {
|
|
891
|
-
if (otherPolyIndex === polyIndex) continue;
|
|
892
|
-
const otherPoly = blueprint.shape[otherPolyIndex];
|
|
893
|
-
if (!otherPoly) continue;
|
|
894
|
-
const otherTranslatedPoly = otherPoly.map((vertex) => ({
|
|
895
|
-
x: vertex.x + ox,
|
|
896
|
-
y: vertex.y + oy
|
|
897
|
-
}));
|
|
898
|
-
for (let j = 0; j < otherTranslatedPoly.length; j++) {
|
|
899
|
-
const otherCurrent = otherTranslatedPoly[j];
|
|
900
|
-
const otherNext = otherTranslatedPoly[(j + 1) % otherTranslatedPoly.length];
|
|
901
|
-
if (!otherCurrent || !otherNext) continue;
|
|
902
|
-
const otherEdgeSegments = edgeToUnitSegments$1(otherCurrent, otherNext, gridSize);
|
|
903
|
-
for (const edgeSeg of edgeSegments) {
|
|
904
|
-
const isShared = otherEdgeSegments.some(
|
|
905
|
-
(otherSeg) => (
|
|
906
|
-
// Check if segments share the same endpoints (identical or reversed)
|
|
907
|
-
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
|
|
908
|
-
)
|
|
909
|
-
);
|
|
910
|
-
if (isShared) {
|
|
911
|
-
isTouching = true;
|
|
912
|
-
break;
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
if (isTouching) break;
|
|
916
|
-
}
|
|
917
|
-
if (isTouching) break;
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
if (!isTouching && CONFIG.game.hideTouchingBorders) {
|
|
921
|
-
for (const otherPiece of allPiecesInSector) {
|
|
922
|
-
if (otherPiece.id === piece.id) continue;
|
|
923
|
-
const otherBlueprint = getBlueprint(otherPiece.blueprintId);
|
|
924
|
-
if (!otherBlueprint?.shape) continue;
|
|
925
|
-
const otherBb = boundsOfBlueprint(otherBlueprint, getPrimitive);
|
|
926
|
-
const otherOx = otherPiece.pos.x - otherBb.min.x;
|
|
927
|
-
const otherOy = otherPiece.pos.y - otherBb.min.y;
|
|
928
|
-
for (const otherPoly of otherBlueprint.shape) {
|
|
929
|
-
const otherTranslatedPoly = otherPoly.map((vertex) => ({
|
|
930
|
-
x: vertex.x + otherOx,
|
|
931
|
-
y: vertex.y + otherOy
|
|
932
|
-
}));
|
|
933
|
-
for (let j = 0; j < otherTranslatedPoly.length; j++) {
|
|
934
|
-
const otherCurrent = otherTranslatedPoly[j];
|
|
935
|
-
const otherNext = otherTranslatedPoly[(j + 1) % otherTranslatedPoly.length];
|
|
936
|
-
if (!otherCurrent || !otherNext) continue;
|
|
937
|
-
const otherEdgeSegments = edgeToUnitSegments$1(otherCurrent, otherNext, gridSize);
|
|
938
|
-
for (const edgeSeg of edgeSegments) {
|
|
939
|
-
const isShared = otherEdgeSegments.some(
|
|
940
|
-
(otherSeg) => (
|
|
941
|
-
// Check if segments share the same endpoints (identical or reversed)
|
|
942
|
-
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
|
|
943
|
-
)
|
|
944
|
-
);
|
|
945
|
-
if (isShared) {
|
|
946
|
-
isTouching = true;
|
|
947
|
-
break;
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
if (isTouching) break;
|
|
951
|
-
}
|
|
952
|
-
if (isTouching) break;
|
|
953
|
-
}
|
|
954
|
-
if (isTouching) break;
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
hiddenEdges.push(isTouching);
|
|
958
|
-
}
|
|
959
|
-
return hiddenEdges;
|
|
821
|
+
function shouldShowBorders() {
|
|
822
|
+
return CONFIG.game.showBorders;
|
|
960
823
|
}
|
|
961
824
|
function shouldUseSelectiveBorders(blueprintId) {
|
|
962
|
-
return
|
|
825
|
+
return CONFIG.game.showBorders;
|
|
963
826
|
}
|
|
964
827
|
|
|
828
|
+
var lockedIcon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFoAAABaCAYAAAA4qEECAAAACXBIWXMAAAsSAAALEgHS3X78AAAFY0lEQVR4nO2d7XXrNgyGH/r0f7zB9QbXnaDqBt6gvhM0I2SE3A2UDXwnqLKBPEHdDewJ0B+kYieWZEoiQfrjPQcnjqOI0GuYAAFQMiJCLjDGLIEFsHQyB/7w/Pd397MCamAnInVgFUfDpCTaGLMACmDlfj4FHuKAJX4DVCKyC3x+b6gTbYyZA2sn31UHhy1QAqWI7FVHFhEVwVpsCUgmUgKF2vUrEVxlQGyXVBqE3zPBqoTHIHhOXlPEUCmBedZEY6OHfQZkTZU9sMqO6Buw4ujWHYLkJXaBkJqUWFIDy6REYx3eLUwVl2TPREc5heR1BgRoy1qV6DsleRLZD5KVyH6QrET2UMeX+uJyE28H6UvykvuILobKHs/Qz4fkObcdJ0+VGo9FzcV8tDGmBP7qPSg+DtgLqrFWBLYSs8C/AhMTbyKy7j3igjWvSPu1LPH4ajo9y4S6ChdyI5emjFTz8isjcgxYC98kNIxOnfuUTmEhk5e6Tvd1IiMpBxFNmlDOy6kMIDtVpNRqKK3O0BhToetk/sPOxZ0FU1cxX2CNAGxFZCc9lW3XvlARvrreh3cRKc7ezcSaOx0e1tH1hZc1PY6INA79zKrbFKuUlXrpccZDdKnomHqwzlXzmqpeotG35l0PyWMWSa3zPGkiqKKP6FJZmXUH0VVIa0pk1WUr0e5T11REaLe+dYwPEOtIk13fqSLPykp0Wd4uwLm7piTtnM1zM/aMI9boovr6hgvHvgU49zdjTNHyvnZ36bp5MYOPGFW74bDtoouA52871y7g+X3w3XH7YdGFsgJwzMKdYh55zCry+dtQwJHoVQIF7gUrSGvR94ICYOYckGYu4N7wZIxZzLDx5QNxsZxh04kPxMUS9JNINfBChB7kS4KNal4JsygaIhtNooNUTwKS/oxeoqky7kVsbB3JujuhLkCzMKBF9O+S0ebKU7il+j+xx5ldPmQyfuZKMoCIVMCv2ONoEP2qMMZUlLEHiD11HEQkdv4iCIwxUafQ2Bad7ZShjd9SK+CLkz3kTQJsQ4o93RMQNX4MFPN2NcN4t816jBE1ltZwhpPgLLmiPdZ9Aip3TNbInmjsdNG3oHhCvww3GDOOd27JFT5FidwLF+8z2ktKD4TFfkb+Idgm0DEpUV8L0Yeevx94ED0dYttyC9rJPmCzgjtFlcagnjkl+ywmOVxSasHn5M8vYJFzwsrhICK7JryrUmriA7cCPCW1vpJVYQXHODr3Oe6asYEj0VU6PW4eFTii3Ty9TajMrWLbOOrTJXiZRJXbRtm8eBAdF2Xz4oNo58HfUmhzo3g7jYq+Zu9KXV0Go+54nSPKT7+1JMArwiW86wiNL2sm3ERKKfF/VvA42zkbus9BREyoc8WC68r/N+Ap/3RtDB84S/y7A4LlqI0xueeKIWx/+PtXkoHzqeOkRpdV3TCmELbpsbWG2Td4yA2QwefUgCS/BLzO185xehSYB/6ksyObsLeX2zHmxihOkdB3Biixqc3UBC8If6eaom9Mn5tXvQJ/9x40HFtsGKmd5pxjHV/oPZU/ReS57wCvp1YYY2r0N3xeC7YicnF7im9fx4rMqzCJcMCz1cGLaDnW7R74jEI865XenUpia3M/xmp0g/ghQ+qViUOia5XBoWoO8ee1yaj1QC7B/rXIKJK94ug+JLqvXAo0jTqjc+CT2nbdwEtuu7C7xSaKphUaAi1pm62/qb/aoWXUzWiDztEdhD8e4aRB9I1YdzArjkr0CeGNo0xNnK9UBNp4pEr0CeFF5oRXXPODIzsILzMgtpFSg+BGHg/3VcLjcdVKSEr0Vzjil4x/AHvTrF5jm3d2wZUcif8BqSLxz8FiAOgAAAAASUVORK5CYII=";
|
|
829
|
+
|
|
830
|
+
var unlockedIcon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFoAAABaCAYAAAA4qEECAAAACXBIWXMAAAsSAAALEgHS3X78AAAFYklEQVR4nO1d0ZGjOBB9ct3/+iJYMlg2giODcwjeDCYEbwbeDNgMvBloIjgmAyYDHEHfh4SHsQUIaLVk7FfVNTblkZpHq1u0GqGICKlAKZUDyADkVrYA/vH891f7VwOoANREVDGrOBsqJtFKqQxAAWBn/35h7uIMQ/wJgCaimrl9b4gTrZTaAthb+SbaOfAGoARQElEj2jMRiQiMxZYAKBEpARRi5y9EsE6A2D7REoQ/MsGihIcgeIu0XMRUKQFskyYaZvbQJEDWUmkA7JIjegVWHNy6OUjOYW4QYpMSSioAeVSiYQLeGlzFmDRYGCiXkLxPgABp2YsS/aAkLyL7SbIQ2U+ShcieGvhin1xq4h0gvbJ3Nk+swZ/G9MEZZooFmKlkDB36cIYhezzv7WHJW8jOkxsAR5i7TOfNAszoOgKoBfXqk6pPz0muA3J3fDXmBBkTN2ITXi4i2lqVhKJHH6sYGXXHyGQP5kbGlJe465tsxSPWHYvoZshYYrsMNpITIbvXhfQpWwgo9eJB2AmfR1Vjjw1eoMhkF1OI1oGV0SNxofZoo8aAX7QXJAbRznOLZc1ZDzlzAtqxp60sEtEEh1XHsGanHwNwWNCm0w0h3mLEjVUnYc1M/d4k55GQVUtbQBUwJpx62q4jEV06iYaZN4fu/MafgtfqXKMlFtGEzrx6gw/sER6u5MuOsf1D94tS6gXAV8b2p2J/+dS58hKJo9tovCwIukTbNrXA+YxJ9cl1QC5ouIiONd+VkqzrOgrEQzI1zIFQALgQzeknh5AJ9ZMSdgDELTpzHDsJ9R0LRfshR4TgkNAUTMRPbyA7nL/ZxymuoQV1iIF8A2PRknDFg7W7j3wD+RnHTX9EdIJZUV4r8r8idPqvUmpLtw/rHBH+oscqV9gCskGhgblrY6+o9xWYmHSAfBWsWEenmAQ7CN9C9q5UpJMyNrEDhGsJDlTLdkC8wyTlZR+g9IR9wLRGYN+9Gf/JYhxSJRkArG7H0P1IWPTfKRMNXIo4/wvaB8IS/U5EWcD22aCUCmpwoV1HHbj9u4GEj2aBUmqnlKqUUmSlUkpJpXdZEHJao5mmYEOFNc4Cmhl9BJ9H65SJhl/NR5E40XoDcyuaMnzcwz60EgvRbJD+mp1PGjcLrcRCVPdAtM+IS31U3gXRPosCqS8cVG0gCJUy5Jp1DBX3ONchEwqGDdFHXYdmuGohUQD44zj+B3FrUnyggY8blqSHHhE1RLQD8LNz+CcR7VLPo8Byey8Wfc/QgCWazA6HbxGVWSveLLefch1lFFXWjbL98CQ6LMr2w4VoG1R+x9BmpfjdDdTXadJSVpdVo+x++UQ0EWl87MP8xHy8Wi4vcCX+DyKqzENbOnZG2nP/w/UB5w40SikN/53Ih8C+ZmgXUkHMu50zrhm+ElFx034P0Zyrwo+2Cv7dZQTONUP7w19MHd/Duh6Xjr96R9pANqut4FmavaqRUM1dz3lyZC8Hz7N3FdwO95cJV7MPXyFQCbQAJ/CUg+0HXaTHFefaq6hEQpYN3mrS0ZV4X6W4nqqtYRZS11Qf7bXw4LvBYGbJ5qy4fId8JRN3xf8ZplK2Hvuh93tYJAoB7xDOqZwL3iVhtsEfs1VaH374kgzAz0df+bg9eHzbPct+Mm8zA8ojkz2Z5NlEPzDZs0j2nnX0IfJ2x5Lw3764B4vqo23HOda9sPsGM4Vbli1kuglIYbfbELJoF2AWH91D+PMVThJEr8S62aw4KNEdwttAGZs4X9FgeFWTONEdwovECde45xdH9hBeJkBsK6UEwa08X+4rhOfrqoUQlehrWOJzzH8BewOTN69gEvI1u5Iz8T+TOcUtjzRRXwAAAABJRU5ErkJggg==";
|
|
831
|
+
|
|
965
832
|
function pathD(poly) {
|
|
966
833
|
return `M ${poly.map((pt) => `${pt.x} ${pt.y}`).join(" L ")} Z`;
|
|
967
834
|
}
|
|
@@ -977,11 +844,13 @@ function BoardView(props) {
|
|
|
977
844
|
placedSilBySector,
|
|
978
845
|
anchorDots,
|
|
979
846
|
pieces,
|
|
847
|
+
scaleS,
|
|
980
848
|
clickMode,
|
|
981
849
|
draggingId,
|
|
982
850
|
selectedPieceId,
|
|
983
851
|
dragInvalid,
|
|
984
852
|
lockedPieceId,
|
|
853
|
+
showTangramDecomposition,
|
|
985
854
|
svgRef,
|
|
986
855
|
setPieceRef,
|
|
987
856
|
onPiecePointerDown,
|
|
@@ -1047,7 +916,7 @@ function BoardView(props) {
|
|
|
1047
916
|
onPointerDown: (e) => {
|
|
1048
917
|
onRootPointerDown(e);
|
|
1049
918
|
},
|
|
1050
|
-
style: { background: "#
|
|
919
|
+
style: { background: "#f5f5f5", touchAction: "none", userSelect: "none" }
|
|
1051
920
|
},
|
|
1052
921
|
layout.sectors.map((s, i) => {
|
|
1053
922
|
const done = !!controller.state.sectors[s.id].completedAt;
|
|
@@ -1067,11 +936,12 @@ function BoardView(props) {
|
|
|
1067
936
|
const isDragging = draggingId === p.id;
|
|
1068
937
|
const locked = p.sectorId && controller.isSectorCompleted(p.sectorId);
|
|
1069
938
|
const isConnectivityLocked = lockedPieceId === p.id;
|
|
1070
|
-
|
|
939
|
+
selectedPieceId === p.id;
|
|
1071
940
|
const isCarriedInvalid = isDragging && dragInvalid;
|
|
1072
941
|
const translateX = p.x - bb.min.x;
|
|
1073
942
|
const translateY = p.y - bb.min.y;
|
|
1074
943
|
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) => {
|
|
944
|
+
const showBorders = shouldShowBorders();
|
|
1075
945
|
shouldUseSelectiveBorders(p.blueprintId);
|
|
1076
946
|
return /* @__PURE__ */ React.createElement(React.Fragment, { key: idx }, /* @__PURE__ */ React.createElement(
|
|
1077
947
|
"path",
|
|
@@ -1082,51 +952,38 @@ function BoardView(props) {
|
|
|
1082
952
|
stroke: "none",
|
|
1083
953
|
onPointerDown: (e) => onPiecePointerDown(e, p)
|
|
1084
954
|
}
|
|
1085
|
-
),
|
|
1086
|
-
// For pieces with selective borders: render individual edge strokes with edge detection
|
|
1087
|
-
(() => {
|
|
1088
|
-
const allPiecesInSector = pieces.filter((piece) => piece.sectorId === p.sectorId);
|
|
1089
|
-
const pieceAsPiece = { ...p, pos: { x: p.x, y: p.y } };
|
|
1090
|
-
const allPiecesAsPieces = allPiecesInSector.map((piece) => ({ ...piece, pos: { x: piece.x, y: piece.y } }));
|
|
1091
|
-
const hiddenEdges = getHiddenEdgesForPolygon(pieceAsPiece, idx, allPiecesAsPieces, (id) => controller.getBlueprint(id), (kind) => controller.getPrimitive(kind));
|
|
1092
|
-
const draggedPiece = draggingId ? allPiecesInSector.find((piece) => piece.id === draggingId) : null;
|
|
1093
|
-
let wasTouchingDraggedPiece;
|
|
1094
|
-
if (p.blueprintId.startsWith("comp:")) {
|
|
1095
|
-
const internalHiddenEdges = getHiddenEdgesForPolygon(pieceAsPiece, idx, [], (id) => controller.getBlueprint(id), (kind) => controller.getPrimitive(kind));
|
|
1096
|
-
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);
|
|
1097
|
-
wasTouchingDraggedPiece = externalHiddenEdges.map((external, i) => external && !internalHiddenEdges[i]);
|
|
1098
|
-
} else {
|
|
1099
|
-
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);
|
|
1100
|
-
}
|
|
1101
|
-
return generateEdgeStrokePaths(poly).map((strokePath, strokeIdx) => {
|
|
1102
|
-
const wasHiddenDueToDraggedPiece = wasTouchingDraggedPiece[strokeIdx] || false;
|
|
1103
|
-
let isHidden;
|
|
1104
|
-
if (isDragging && p.blueprintId.startsWith("comp:")) {
|
|
1105
|
-
const internalHiddenEdges = getHiddenEdgesForPolygon(pieceAsPiece, idx, [], (id) => controller.getBlueprint(id), (kind) => controller.getPrimitive(kind));
|
|
1106
|
-
isHidden = internalHiddenEdges[strokeIdx] || false;
|
|
1107
|
-
} else {
|
|
1108
|
-
isHidden = isDragging ? false : (hiddenEdges[strokeIdx] || false) && !wasHiddenDueToDraggedPiece;
|
|
1109
|
-
}
|
|
1110
|
-
return /* @__PURE__ */ React.createElement(
|
|
1111
|
-
"path",
|
|
1112
|
-
{
|
|
1113
|
-
key: `stroke-${idx}-${strokeIdx}`,
|
|
1114
|
-
d: strokePath,
|
|
1115
|
-
fill: "none",
|
|
1116
|
-
stroke: isHidden ? "none" : isConnectivityLocked ? CONFIG.color.piece.invalidStroke : isCarriedInvalid ? CONFIG.color.piece.invalidStroke : isSelected || isDragging ? CONFIG.color.piece.selectedStroke : CONFIG.color.piece.borderStroke,
|
|
1117
|
-
strokeWidth: isHidden ? 0 : isSelected || isDragging ? CONFIG.size.stroke.pieceSelectedPx : CONFIG.size.stroke.pieceBorderPx,
|
|
1118
|
-
onPointerDown: (e) => onPiecePointerDown(e, p)
|
|
1119
|
-
}
|
|
1120
|
-
);
|
|
1121
|
-
});
|
|
1122
|
-
})()
|
|
1123
|
-
) ));
|
|
955
|
+
), showBorders);
|
|
1124
956
|
}));
|
|
1125
957
|
}),
|
|
1126
958
|
layout.sectors.map((s) => {
|
|
1127
|
-
const
|
|
1128
|
-
if (
|
|
1129
|
-
|
|
959
|
+
const sectorCfg = controller.state.cfg.sectors.find((ss) => ss.id === s.id);
|
|
960
|
+
if (showTangramDecomposition && sectorCfg?.silhouette.primitiveDecomposition) {
|
|
961
|
+
const primitiveDecomposition = sectorCfg.silhouette.primitiveDecomposition;
|
|
962
|
+
const rect = rectForBand(layout, s, "silhouette", 1);
|
|
963
|
+
const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
|
|
964
|
+
const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, { cx: rect.cx, cy: rect.cy });
|
|
965
|
+
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(
|
|
966
|
+
"path",
|
|
967
|
+
{
|
|
968
|
+
d: pathD(scaledPoly),
|
|
969
|
+
fill: CONFIG.color.silhouetteMask,
|
|
970
|
+
opacity: CONFIG.opacity.silhouetteMask,
|
|
971
|
+
stroke: "none"
|
|
972
|
+
}
|
|
973
|
+
), /* @__PURE__ */ React.createElement(
|
|
974
|
+
"path",
|
|
975
|
+
{
|
|
976
|
+
d: pathD(scaledPoly),
|
|
977
|
+
fill: "none",
|
|
978
|
+
stroke: CONFIG.color.tangramDecomposition.stroke,
|
|
979
|
+
strokeWidth: CONFIG.size.stroke.tangramDecompositionPx
|
|
980
|
+
}
|
|
981
|
+
))));
|
|
982
|
+
} else {
|
|
983
|
+
const placedPolys = placedSilBySector.get(s.id) ?? [];
|
|
984
|
+
if (!placedPolys.length) return null;
|
|
985
|
+
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 })));
|
|
986
|
+
}
|
|
1130
987
|
}),
|
|
1131
988
|
anchorDots.map(({ sectorId, valid, invalid }) => {
|
|
1132
989
|
const isInnerRing = sectorId === "inner-ring";
|
|
@@ -1142,10 +999,20 @@ function BoardView(props) {
|
|
|
1142
999
|
}
|
|
1143
1000
|
)));
|
|
1144
1001
|
}),
|
|
1002
|
+
/* @__PURE__ */ React.createElement("defs", null, /* @__PURE__ */ React.createElement("filter", { id: "invert-to-white" }, /* @__PURE__ */ React.createElement(
|
|
1003
|
+
"feColorMatrix",
|
|
1004
|
+
{
|
|
1005
|
+
type: "matrix",
|
|
1006
|
+
values: "-1 0 0 0 1\n 0 -1 0 0 1\n 0 0 -1 0 1\n 0 0 0 1 0"
|
|
1007
|
+
}
|
|
1008
|
+
))),
|
|
1145
1009
|
(() => {
|
|
1146
1010
|
const isPrep = controller.state.cfg.mode === "prep";
|
|
1147
1011
|
const isSubmitEnabled = isPrep ? controller.isSubmitEnabled() : true;
|
|
1148
1012
|
const isClickable = !draggingId && (!isPrep || isSubmitEnabled);
|
|
1013
|
+
const [imageError, setImageError] = React.useState(false);
|
|
1014
|
+
const iconSize = badgeR * 1.6;
|
|
1015
|
+
const iconOffset = iconSize / 2;
|
|
1149
1016
|
return /* @__PURE__ */ React.createElement(
|
|
1150
1017
|
"g",
|
|
1151
1018
|
{
|
|
@@ -1161,7 +1028,7 @@ function BoardView(props) {
|
|
|
1161
1028
|
opacity: isSubmitEnabled ? 1 : 0.5
|
|
1162
1029
|
}
|
|
1163
1030
|
),
|
|
1164
|
-
/* @__PURE__ */ React.createElement(
|
|
1031
|
+
isPrep ? /* @__PURE__ */ React.createElement(
|
|
1165
1032
|
"text",
|
|
1166
1033
|
{
|
|
1167
1034
|
textAnchor: "middle",
|
|
@@ -1170,7 +1037,29 @@ function BoardView(props) {
|
|
|
1170
1037
|
fill: isSubmitEnabled ? CONFIG.color.blueprint.labelFill : "#888",
|
|
1171
1038
|
pointerEvents: "none"
|
|
1172
1039
|
},
|
|
1173
|
-
|
|
1040
|
+
"Submit"
|
|
1041
|
+
) : imageError ? /* @__PURE__ */ React.createElement(
|
|
1042
|
+
"text",
|
|
1043
|
+
{
|
|
1044
|
+
textAnchor: "middle",
|
|
1045
|
+
dominantBaseline: "middle",
|
|
1046
|
+
fontSize: CONFIG.size.badgeFontPx,
|
|
1047
|
+
fill: CONFIG.color.blueprint.labelFill,
|
|
1048
|
+
pointerEvents: "none"
|
|
1049
|
+
},
|
|
1050
|
+
"inventory"
|
|
1051
|
+
) : /* @__PURE__ */ React.createElement(
|
|
1052
|
+
"image",
|
|
1053
|
+
{
|
|
1054
|
+
href: controller.state.blueprintView === "quickstash" ? lockedIcon : unlockedIcon,
|
|
1055
|
+
x: -iconOffset,
|
|
1056
|
+
y: -iconOffset,
|
|
1057
|
+
width: iconSize,
|
|
1058
|
+
height: iconSize,
|
|
1059
|
+
pointerEvents: "none",
|
|
1060
|
+
onError: () => setImageError(true),
|
|
1061
|
+
filter: "url(#invert-to-white)"
|
|
1062
|
+
}
|
|
1174
1063
|
)
|
|
1175
1064
|
);
|
|
1176
1065
|
})(),
|
|
@@ -2588,7 +2477,7 @@ function useClickController(controller, layout, pieces, clickMode, draggingId, s
|
|
|
2588
2477
|
}
|
|
2589
2478
|
|
|
2590
2479
|
class InteractionTracker {
|
|
2591
|
-
constructor(controller, callbacks,
|
|
2480
|
+
constructor(controller, callbacks, trialParams) {
|
|
2592
2481
|
this.gridStep = CONFIG.layout.grid.stepPx;
|
|
2593
2482
|
// Interaction state
|
|
2594
2483
|
this.interactionIndex = 0;
|
|
@@ -2605,8 +2494,7 @@ class InteractionTracker {
|
|
|
2605
2494
|
this.createdMacros = [];
|
|
2606
2495
|
this.controller = controller;
|
|
2607
2496
|
this.callbacks = callbacks;
|
|
2608
|
-
this.
|
|
2609
|
-
this.gameId = gameId || uuid.v4();
|
|
2497
|
+
this.trialParams = trialParams;
|
|
2610
2498
|
this.trialStartTime = Date.now();
|
|
2611
2499
|
this.controller.setTrackingCallbacks({
|
|
2612
2500
|
onSectorCompleted: (sectorId) => this.recordSectorCompletion(sectorId)
|
|
@@ -2654,8 +2542,6 @@ class InteractionTracker {
|
|
|
2654
2542
|
const event = {
|
|
2655
2543
|
// Metadata
|
|
2656
2544
|
interactionId: uuid.v4(),
|
|
2657
|
-
trialId: this.trialId,
|
|
2658
|
-
gameId: this.gameId,
|
|
2659
2545
|
interactionIndex: this.interactionIndex++,
|
|
2660
2546
|
// Interaction type
|
|
2661
2547
|
interactionType,
|
|
@@ -2761,6 +2647,7 @@ class InteractionTracker {
|
|
|
2761
2647
|
const trialEndTime = Date.now();
|
|
2762
2648
|
const totalDuration = trialEndTime - this.trialStartTime;
|
|
2763
2649
|
const finalSnapshot = this.buildStateSnapshot();
|
|
2650
|
+
const anchorToStimuliRatio = CONFIG.layout.grid.stepPx / CONFIG.layout.grid.unitPx;
|
|
2764
2651
|
const mode = this.controller.state.cfg.mode;
|
|
2765
2652
|
if (mode === "construction") {
|
|
2766
2653
|
const finalBlueprintState = this.buildFinalBlueprintState(finalSnapshot);
|
|
@@ -2772,13 +2659,11 @@ class InteractionTracker {
|
|
|
2772
2659
|
}));
|
|
2773
2660
|
const data = {
|
|
2774
2661
|
trialType: "construction",
|
|
2775
|
-
trialId: this.trialId,
|
|
2776
|
-
gameId: this.gameId,
|
|
2777
|
-
trialNum: 0,
|
|
2778
|
-
// TODO: Plugin should provide this
|
|
2779
2662
|
trialStartTime: this.trialStartTime,
|
|
2780
2663
|
trialEndTime,
|
|
2781
2664
|
totalDuration,
|
|
2665
|
+
anchorToStimuliRatio,
|
|
2666
|
+
trialParams: this.trialParams,
|
|
2782
2667
|
endReason,
|
|
2783
2668
|
completionTimes: this.completionTimes,
|
|
2784
2669
|
finalBlueprintState,
|
|
@@ -2797,13 +2682,11 @@ class InteractionTracker {
|
|
|
2797
2682
|
}));
|
|
2798
2683
|
const data = {
|
|
2799
2684
|
trialType: "prep",
|
|
2800
|
-
trialId: this.trialId,
|
|
2801
|
-
gameId: this.gameId,
|
|
2802
|
-
trialNum: 0,
|
|
2803
|
-
// TODO: Plugin should provide this
|
|
2804
2685
|
trialStartTime: this.trialStartTime,
|
|
2805
2686
|
trialEndTime,
|
|
2806
2687
|
totalDuration,
|
|
2688
|
+
anchorToStimuliRatio,
|
|
2689
|
+
trialParams: this.trialParams,
|
|
2807
2690
|
endReason: "submit",
|
|
2808
2691
|
createdMacros: finalMacros,
|
|
2809
2692
|
quickstashMacros,
|
|
@@ -3061,6 +2944,9 @@ function GameBoard(props) {
|
|
|
3061
2944
|
maxQuickstashSlots,
|
|
3062
2945
|
maxCompositeSize,
|
|
3063
2946
|
mode,
|
|
2947
|
+
showTangramDecomposition,
|
|
2948
|
+
instructions,
|
|
2949
|
+
trialParams,
|
|
3064
2950
|
width: _width,
|
|
3065
2951
|
height: _height,
|
|
3066
2952
|
onSectorComplete,
|
|
@@ -3070,6 +2956,7 @@ function GameBoard(props) {
|
|
|
3070
2956
|
onTrialEnd,
|
|
3071
2957
|
onControllerReady
|
|
3072
2958
|
} = props;
|
|
2959
|
+
const [timeRemaining, setTimeRemaining] = React.useState(timeLimitMs > 0 ? Math.floor(timeLimitMs / 1e3) : 0);
|
|
3073
2960
|
const controller = React.useMemo(() => {
|
|
3074
2961
|
const gameConfig = {
|
|
3075
2962
|
n: sectors.length,
|
|
@@ -3096,8 +2983,8 @@ function GameBoard(props) {
|
|
|
3096
2983
|
const callbacks = {};
|
|
3097
2984
|
if (onInteraction) callbacks.onInteraction = onInteraction;
|
|
3098
2985
|
if (onTrialEnd) callbacks.onTrialEnd = onTrialEnd;
|
|
3099
|
-
return new InteractionTracker(controller, callbacks);
|
|
3100
|
-
}, [controller, onInteraction, onTrialEnd]);
|
|
2986
|
+
return new InteractionTracker(controller, callbacks, trialParams);
|
|
2987
|
+
}, [controller, onInteraction, onTrialEnd, trialParams]);
|
|
3101
2988
|
React.useEffect(() => {
|
|
3102
2989
|
if (onControllerReady) {
|
|
3103
2990
|
onControllerReady(controller);
|
|
@@ -3177,22 +3064,22 @@ function GameBoard(props) {
|
|
|
3177
3064
|
}), [handleSectorComplete, onPiecePlace, onPieceRemove]);
|
|
3178
3065
|
const getGameboardStyle = () => {
|
|
3179
3066
|
const baseStyle = {
|
|
3180
|
-
margin: "
|
|
3067
|
+
margin: "10px",
|
|
3181
3068
|
display: "flex",
|
|
3182
3069
|
alignItems: "center",
|
|
3183
3070
|
justifyContent: "center",
|
|
3184
3071
|
position: "relative"
|
|
3185
3072
|
};
|
|
3186
3073
|
if (layoutMode === "circle") {
|
|
3187
|
-
const size = Math.min(window.innerWidth *
|
|
3074
|
+
const size = Math.min(window.innerWidth * CONFIG.layout.viewportScale, window.innerHeight * CONFIG.layout.viewportScale);
|
|
3188
3075
|
return {
|
|
3189
3076
|
...baseStyle,
|
|
3190
3077
|
width: `${size}px`,
|
|
3191
3078
|
height: `${size}px`
|
|
3192
3079
|
};
|
|
3193
3080
|
} else {
|
|
3194
|
-
const maxWidth = Math.min(window.innerWidth *
|
|
3195
|
-
const maxHeight = Math.min(window.innerWidth *
|
|
3081
|
+
const maxWidth = Math.min(window.innerWidth * CONFIG.layout.viewportScale, window.innerHeight * CONFIG.layout.viewportScale * 2);
|
|
3082
|
+
const maxHeight = Math.min(window.innerWidth * CONFIG.layout.viewportScale / 2, window.innerHeight * CONFIG.layout.viewportScale);
|
|
3196
3083
|
return {
|
|
3197
3084
|
...baseStyle,
|
|
3198
3085
|
width: `${maxWidth}px`,
|
|
@@ -3361,7 +3248,68 @@ function GameBoard(props) {
|
|
|
3361
3248
|
force();
|
|
3362
3249
|
e.stopPropagation();
|
|
3363
3250
|
};
|
|
3364
|
-
|
|
3251
|
+
React.useEffect(() => {
|
|
3252
|
+
if (timeLimitMs === 0) return;
|
|
3253
|
+
const interval = setInterval(() => {
|
|
3254
|
+
setTimeRemaining((prev) => {
|
|
3255
|
+
if (prev <= 1) {
|
|
3256
|
+
clearInterval(interval);
|
|
3257
|
+
return 0;
|
|
3258
|
+
}
|
|
3259
|
+
return prev - 1;
|
|
3260
|
+
});
|
|
3261
|
+
}, 1e3);
|
|
3262
|
+
return () => clearInterval(interval);
|
|
3263
|
+
}, [timeLimitMs]);
|
|
3264
|
+
const formatTime = (seconds) => {
|
|
3265
|
+
const mins = Math.floor(seconds / 60);
|
|
3266
|
+
const secs = Math.floor(seconds % 60);
|
|
3267
|
+
return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
|
|
3268
|
+
};
|
|
3269
|
+
const containerStyle = {
|
|
3270
|
+
display: "flex",
|
|
3271
|
+
flexDirection: "column",
|
|
3272
|
+
width: "100%"
|
|
3273
|
+
// minHeight: '100vh'
|
|
3274
|
+
};
|
|
3275
|
+
const headerStyle = {
|
|
3276
|
+
display: "flex",
|
|
3277
|
+
flexDirection: "row",
|
|
3278
|
+
justifyContent: "space-between",
|
|
3279
|
+
alignItems: "center",
|
|
3280
|
+
padding: "20px",
|
|
3281
|
+
background: "#f5f5f5",
|
|
3282
|
+
flex: "0 0 auto"
|
|
3283
|
+
};
|
|
3284
|
+
const instructionsStyle = {
|
|
3285
|
+
flexGrow: 1,
|
|
3286
|
+
fontSize: "20px",
|
|
3287
|
+
lineHeight: 1.5,
|
|
3288
|
+
marginRight: "20px"
|
|
3289
|
+
};
|
|
3290
|
+
const timerStyle = {
|
|
3291
|
+
fontSize: "24px",
|
|
3292
|
+
fontWeight: "bold",
|
|
3293
|
+
fontFamily: "monospace",
|
|
3294
|
+
color: "#333",
|
|
3295
|
+
minWidth: "80px",
|
|
3296
|
+
textAlign: "right"
|
|
3297
|
+
};
|
|
3298
|
+
const gameboardWrapperStyle = {
|
|
3299
|
+
flex: "1 1 auto",
|
|
3300
|
+
display: "flex",
|
|
3301
|
+
alignItems: "center",
|
|
3302
|
+
justifyContent: "center",
|
|
3303
|
+
overflow: "hidden"
|
|
3304
|
+
};
|
|
3305
|
+
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(
|
|
3306
|
+
"div",
|
|
3307
|
+
{
|
|
3308
|
+
className: "tangram-instructions",
|
|
3309
|
+
style: instructionsStyle,
|
|
3310
|
+
dangerouslySetInnerHTML: { __html: instructions }
|
|
3311
|
+
}
|
|
3312
|
+
), 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(
|
|
3365
3313
|
BoardView,
|
|
3366
3314
|
{
|
|
3367
3315
|
controller,
|
|
@@ -3387,9 +3335,11 @@ function GameBoard(props) {
|
|
|
3387
3335
|
onPointerMove,
|
|
3388
3336
|
onPointerUp,
|
|
3389
3337
|
onCenterBadgePointerDown,
|
|
3338
|
+
showTangramDecomposition: showTangramDecomposition ?? false,
|
|
3339
|
+
scaleS,
|
|
3390
3340
|
...eventCallbacks
|
|
3391
3341
|
}
|
|
3392
|
-
));
|
|
3342
|
+
))));
|
|
3393
3343
|
}
|
|
3394
3344
|
|
|
3395
3345
|
const U = 40;
|
|
@@ -3557,17 +3507,22 @@ function startConstructionTrial(display_element, params, _jsPsych) {
|
|
|
3557
3507
|
const isCanonical = CANON.has(tanName);
|
|
3558
3508
|
return isCanonical;
|
|
3559
3509
|
});
|
|
3560
|
-
const mask = filteredTans.map((tan
|
|
3510
|
+
const mask = filteredTans.map((tan) => {
|
|
3561
3511
|
const polygon = tan.vertices.map(([x, y]) => ({ x: x ?? 0, y: -(y ?? 0) }));
|
|
3562
3512
|
return polygon;
|
|
3563
3513
|
});
|
|
3514
|
+
const primitiveDecomposition = filteredTans.map((tan) => ({
|
|
3515
|
+
kind: tan.name ?? tan.kind,
|
|
3516
|
+
polygon: tan.vertices.map(([x, y]) => ({ x: x ?? 0, y: -(y ?? 0) }))
|
|
3517
|
+
}));
|
|
3564
3518
|
const sectorId = `sector${index}`;
|
|
3565
3519
|
const sector = {
|
|
3566
3520
|
id: sectorId,
|
|
3567
3521
|
tangramId: tangramSpec.tangramID,
|
|
3568
3522
|
silhouette: {
|
|
3569
3523
|
id: sectorId,
|
|
3570
|
-
mask
|
|
3524
|
+
mask,
|
|
3525
|
+
primitiveDecomposition
|
|
3571
3526
|
}
|
|
3572
3527
|
};
|
|
3573
3528
|
return sector;
|
|
@@ -3586,6 +3541,7 @@ function startConstructionTrial(display_element, params, _jsPsych) {
|
|
|
3586
3541
|
quickstash = params.quickstash_macros;
|
|
3587
3542
|
}
|
|
3588
3543
|
}
|
|
3544
|
+
const { onInteraction, onTrialEnd, ...trialParams } = params;
|
|
3589
3545
|
const gameBoardProps = {
|
|
3590
3546
|
sectors,
|
|
3591
3547
|
quickstash,
|
|
@@ -3596,6 +3552,9 @@ function startConstructionTrial(display_element, params, _jsPsych) {
|
|
|
3596
3552
|
timeLimitMs: params.time_limit_ms,
|
|
3597
3553
|
maxQuickstashSlots: CONFIG.layout.defaults.maxQuickstashSlots,
|
|
3598
3554
|
mode: "construction",
|
|
3555
|
+
showTangramDecomposition: params.show_tangram_decomposition ?? false,
|
|
3556
|
+
trialParams,
|
|
3557
|
+
...params.instructions && { instructions: params.instructions },
|
|
3599
3558
|
...params.onInteraction && { onInteraction: params.onInteraction },
|
|
3600
3559
|
...params.onTrialEnd && { onTrialEnd: params.onTrialEnd }
|
|
3601
3560
|
};
|
|
@@ -3659,6 +3618,18 @@ const info = {
|
|
|
3659
3618
|
default: 18,
|
|
3660
3619
|
description: "Snap radius for anchor-based piece placement"
|
|
3661
3620
|
},
|
|
3621
|
+
/** Whether to show tangram target shapes decomposed into individual primitives with borders */
|
|
3622
|
+
show_tangram_decomposition: {
|
|
3623
|
+
type: jspsych.ParameterType.BOOL,
|
|
3624
|
+
default: false,
|
|
3625
|
+
description: "Whether to show tangram target shapes decomposed into individual primitives with borders"
|
|
3626
|
+
},
|
|
3627
|
+
/** HTML content to display above the gameboard as instructions */
|
|
3628
|
+
instructions: {
|
|
3629
|
+
type: jspsych.ParameterType.STRING,
|
|
3630
|
+
default: "",
|
|
3631
|
+
description: "HTML content to display above the gameboard as instructions"
|
|
3632
|
+
},
|
|
3662
3633
|
/** Callback fired after each interaction (piece pickup + placedown) */
|
|
3663
3634
|
onInteraction: {
|
|
3664
3635
|
type: jspsych.ParameterType.FUNCTION,
|
|
@@ -3732,6 +3703,8 @@ class TangramConstructPlugin {
|
|
|
3732
3703
|
input: trial.input,
|
|
3733
3704
|
layout: trial.layout,
|
|
3734
3705
|
time_limit_ms: trial.time_limit_ms,
|
|
3706
|
+
show_tangram_decomposition: trial.show_tangram_decomposition,
|
|
3707
|
+
instructions: trial.instructions,
|
|
3735
3708
|
onInteraction: trial.onInteraction,
|
|
3736
3709
|
onTrialEnd: wrappedOnTrialEnd
|
|
3737
3710
|
};
|