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/index.js
CHANGED
|
@@ -14,7 +14,8 @@ const CONFIG = {
|
|
|
14
14
|
anchors: { invalid: "#7dd3fc", valid: "#475569" },
|
|
15
15
|
piece: { draggingFill: "#8e7cc3ff", validFill: "#8e7cc3ff", invalidFill: "#ef4444", invalidStroke: "#dc2626", selectedStroke: "#674ea7", allGreenStroke: "#86efac", borderStroke: "#674ea7" },
|
|
16
16
|
ui: { light: "#60a5fa", dark: "#1d4ed8" },
|
|
17
|
-
blueprint: { fill: "#374151", selectedStroke: "#111827", badgeFill: "#000000", labelFill: "#ffffff" }
|
|
17
|
+
blueprint: { fill: "#374151", selectedStroke: "#111827", badgeFill: "#000000", labelFill: "#ffffff" },
|
|
18
|
+
tangramDecomposition: { stroke: "#fef2cc" }
|
|
18
19
|
},
|
|
19
20
|
opacity: {
|
|
20
21
|
blueprint: 0.4,
|
|
@@ -24,7 +25,7 @@ const CONFIG = {
|
|
|
24
25
|
piece: { invalid: 0.35, dragging: 0.75, locked: 1, normal: 1 }
|
|
25
26
|
},
|
|
26
27
|
size: {
|
|
27
|
-
stroke: { bandPx: 5, pieceSelectedPx: 3, allGreenStrokePx: 10, pieceBorderPx: 2 },
|
|
28
|
+
stroke: { bandPx: 5, pieceSelectedPx: 3, allGreenStrokePx: 10, pieceBorderPx: 2, tangramDecompositionPx: 1 },
|
|
28
29
|
anchorRadiusPx: { valid: 1, invalid: 1 },
|
|
29
30
|
badgeFontPx: 16,
|
|
30
31
|
centerBadge: { fractionOfOuterR: 0.15, minPx: 20, marginPx: 4 }
|
|
@@ -32,6 +33,7 @@ const CONFIG = {
|
|
|
32
33
|
layout: {
|
|
33
34
|
grid: { stepPx: 20, unitPx: 40 },
|
|
34
35
|
paddingPx: 1,
|
|
36
|
+
viewportScale: 0.8,
|
|
35
37
|
constraints: {
|
|
36
38
|
workspaceDiamAnchors: 10,
|
|
37
39
|
// num anchors req'd to be on diagonal
|
|
@@ -43,7 +45,7 @@ const CONFIG = {
|
|
|
43
45
|
},
|
|
44
46
|
game: {
|
|
45
47
|
snapRadiusPx: 15,
|
|
46
|
-
showBorders:
|
|
48
|
+
showBorders: false,
|
|
47
49
|
hideTouchingBorders: true
|
|
48
50
|
}
|
|
49
51
|
};
|
|
@@ -814,152 +816,17 @@ function rectForBand(layout, sector, band, pad = 0.85) {
|
|
|
814
816
|
return { cx, cy, w, h };
|
|
815
817
|
}
|
|
816
818
|
|
|
817
|
-
function
|
|
818
|
-
|
|
819
|
-
for (let i = 0; i < poly.length; i++) {
|
|
820
|
-
const current = poly[i];
|
|
821
|
-
const next = poly[(i + 1) % poly.length];
|
|
822
|
-
if (current && next) {
|
|
823
|
-
strokePaths.push(`M ${current.x} ${current.y} L ${next.x} ${next.y}`);
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
return strokePaths;
|
|
827
|
-
}
|
|
828
|
-
function edgeToUnitSegments$1(start, end, gridSize) {
|
|
829
|
-
const segments = [];
|
|
830
|
-
const startGrid = {
|
|
831
|
-
x: Math.round(start.x / gridSize),
|
|
832
|
-
y: Math.round(start.y / gridSize)
|
|
833
|
-
};
|
|
834
|
-
const endGrid = {
|
|
835
|
-
x: Math.round(end.x / gridSize),
|
|
836
|
-
y: Math.round(end.y / gridSize)
|
|
837
|
-
};
|
|
838
|
-
const dx = endGrid.x - startGrid.x;
|
|
839
|
-
const dy = endGrid.y - startGrid.y;
|
|
840
|
-
if (dx === 0 && dy === 0) return [];
|
|
841
|
-
const steps = Math.max(Math.abs(dx), Math.abs(dy));
|
|
842
|
-
const stepX = dx / steps;
|
|
843
|
-
const stepY = dy / steps;
|
|
844
|
-
for (let i = 0; i < steps; i++) {
|
|
845
|
-
const aX = Math.round(startGrid.x + i * stepX);
|
|
846
|
-
const aY = Math.round(startGrid.y + i * stepY);
|
|
847
|
-
const bX = Math.round(startGrid.x + (i + 1) * stepX);
|
|
848
|
-
const bY = Math.round(startGrid.y + (i + 1) * stepY);
|
|
849
|
-
segments.push({
|
|
850
|
-
a: { x: aX, y: aY },
|
|
851
|
-
b: { x: bX, y: bY }
|
|
852
|
-
});
|
|
853
|
-
}
|
|
854
|
-
return segments;
|
|
855
|
-
}
|
|
856
|
-
function getHiddenEdgesForPolygon(piece, polyIndex, allPiecesInSector, getBlueprint, getPrimitive) {
|
|
857
|
-
let blueprint;
|
|
858
|
-
try {
|
|
859
|
-
blueprint = getBlueprint(piece.blueprintId);
|
|
860
|
-
} catch (error) {
|
|
861
|
-
console.warn("getBlueprint failed in getHiddenEdgesForPolygon:", error);
|
|
862
|
-
return [];
|
|
863
|
-
}
|
|
864
|
-
if (!blueprint?.shape) {
|
|
865
|
-
return [];
|
|
866
|
-
}
|
|
867
|
-
const poly = blueprint.shape[polyIndex];
|
|
868
|
-
if (!poly) return [];
|
|
869
|
-
const gridSize = CONFIG.layout.grid.stepPx;
|
|
870
|
-
const bb = boundsOfBlueprint(blueprint, getPrimitive);
|
|
871
|
-
const ox = piece.pos.x - bb.min.x;
|
|
872
|
-
const oy = piece.pos.y - bb.min.y;
|
|
873
|
-
const translatedPoly = poly.map((vertex) => ({
|
|
874
|
-
x: vertex.x + ox,
|
|
875
|
-
y: vertex.y + oy
|
|
876
|
-
}));
|
|
877
|
-
const hiddenEdges = [];
|
|
878
|
-
for (let i = 0; i < translatedPoly.length; i++) {
|
|
879
|
-
const current = translatedPoly[i];
|
|
880
|
-
const next = translatedPoly[(i + 1) % translatedPoly.length];
|
|
881
|
-
if (!current || !next) {
|
|
882
|
-
hiddenEdges.push(false);
|
|
883
|
-
continue;
|
|
884
|
-
}
|
|
885
|
-
const edgeSegments = edgeToUnitSegments$1(current, next, gridSize);
|
|
886
|
-
let isTouching = false;
|
|
887
|
-
if (piece.blueprintId.startsWith("comp:")) {
|
|
888
|
-
for (let otherPolyIndex = 0; otherPolyIndex < blueprint.shape.length; otherPolyIndex++) {
|
|
889
|
-
if (otherPolyIndex === polyIndex) continue;
|
|
890
|
-
const otherPoly = blueprint.shape[otherPolyIndex];
|
|
891
|
-
if (!otherPoly) continue;
|
|
892
|
-
const otherTranslatedPoly = otherPoly.map((vertex) => ({
|
|
893
|
-
x: vertex.x + ox,
|
|
894
|
-
y: vertex.y + oy
|
|
895
|
-
}));
|
|
896
|
-
for (let j = 0; j < otherTranslatedPoly.length; j++) {
|
|
897
|
-
const otherCurrent = otherTranslatedPoly[j];
|
|
898
|
-
const otherNext = otherTranslatedPoly[(j + 1) % otherTranslatedPoly.length];
|
|
899
|
-
if (!otherCurrent || !otherNext) continue;
|
|
900
|
-
const otherEdgeSegments = edgeToUnitSegments$1(otherCurrent, otherNext, gridSize);
|
|
901
|
-
for (const edgeSeg of edgeSegments) {
|
|
902
|
-
const isShared = otherEdgeSegments.some(
|
|
903
|
-
(otherSeg) => (
|
|
904
|
-
// Check if segments share the same endpoints (identical or reversed)
|
|
905
|
-
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
|
|
906
|
-
)
|
|
907
|
-
);
|
|
908
|
-
if (isShared) {
|
|
909
|
-
isTouching = true;
|
|
910
|
-
break;
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
if (isTouching) break;
|
|
914
|
-
}
|
|
915
|
-
if (isTouching) break;
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
if (!isTouching && CONFIG.game.hideTouchingBorders) {
|
|
919
|
-
for (const otherPiece of allPiecesInSector) {
|
|
920
|
-
if (otherPiece.id === piece.id) continue;
|
|
921
|
-
const otherBlueprint = getBlueprint(otherPiece.blueprintId);
|
|
922
|
-
if (!otherBlueprint?.shape) continue;
|
|
923
|
-
const otherBb = boundsOfBlueprint(otherBlueprint, getPrimitive);
|
|
924
|
-
const otherOx = otherPiece.pos.x - otherBb.min.x;
|
|
925
|
-
const otherOy = otherPiece.pos.y - otherBb.min.y;
|
|
926
|
-
for (const otherPoly of otherBlueprint.shape) {
|
|
927
|
-
const otherTranslatedPoly = otherPoly.map((vertex) => ({
|
|
928
|
-
x: vertex.x + otherOx,
|
|
929
|
-
y: vertex.y + otherOy
|
|
930
|
-
}));
|
|
931
|
-
for (let j = 0; j < otherTranslatedPoly.length; j++) {
|
|
932
|
-
const otherCurrent = otherTranslatedPoly[j];
|
|
933
|
-
const otherNext = otherTranslatedPoly[(j + 1) % otherTranslatedPoly.length];
|
|
934
|
-
if (!otherCurrent || !otherNext) continue;
|
|
935
|
-
const otherEdgeSegments = edgeToUnitSegments$1(otherCurrent, otherNext, gridSize);
|
|
936
|
-
for (const edgeSeg of edgeSegments) {
|
|
937
|
-
const isShared = otherEdgeSegments.some(
|
|
938
|
-
(otherSeg) => (
|
|
939
|
-
// Check if segments share the same endpoints (identical or reversed)
|
|
940
|
-
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
|
|
941
|
-
)
|
|
942
|
-
);
|
|
943
|
-
if (isShared) {
|
|
944
|
-
isTouching = true;
|
|
945
|
-
break;
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
if (isTouching) break;
|
|
949
|
-
}
|
|
950
|
-
if (isTouching) break;
|
|
951
|
-
}
|
|
952
|
-
if (isTouching) break;
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
hiddenEdges.push(isTouching);
|
|
956
|
-
}
|
|
957
|
-
return hiddenEdges;
|
|
819
|
+
function shouldShowBorders() {
|
|
820
|
+
return CONFIG.game.showBorders;
|
|
958
821
|
}
|
|
959
822
|
function shouldUseSelectiveBorders(blueprintId) {
|
|
960
|
-
return
|
|
823
|
+
return CONFIG.game.showBorders;
|
|
961
824
|
}
|
|
962
825
|
|
|
826
|
+
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=";
|
|
827
|
+
|
|
828
|
+
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==";
|
|
829
|
+
|
|
963
830
|
function pathD(poly) {
|
|
964
831
|
return `M ${poly.map((pt) => `${pt.x} ${pt.y}`).join(" L ")} Z`;
|
|
965
832
|
}
|
|
@@ -975,11 +842,13 @@ function BoardView(props) {
|
|
|
975
842
|
placedSilBySector,
|
|
976
843
|
anchorDots,
|
|
977
844
|
pieces,
|
|
845
|
+
scaleS,
|
|
978
846
|
clickMode,
|
|
979
847
|
draggingId,
|
|
980
848
|
selectedPieceId,
|
|
981
849
|
dragInvalid,
|
|
982
850
|
lockedPieceId,
|
|
851
|
+
showTangramDecomposition,
|
|
983
852
|
svgRef,
|
|
984
853
|
setPieceRef,
|
|
985
854
|
onPiecePointerDown,
|
|
@@ -1045,7 +914,7 @@ function BoardView(props) {
|
|
|
1045
914
|
onPointerDown: (e) => {
|
|
1046
915
|
onRootPointerDown(e);
|
|
1047
916
|
},
|
|
1048
|
-
style: { background: "#
|
|
917
|
+
style: { background: "#f5f5f5", touchAction: "none", userSelect: "none" }
|
|
1049
918
|
},
|
|
1050
919
|
layout.sectors.map((s, i) => {
|
|
1051
920
|
const done = !!controller.state.sectors[s.id].completedAt;
|
|
@@ -1065,11 +934,12 @@ function BoardView(props) {
|
|
|
1065
934
|
const isDragging = draggingId === p.id;
|
|
1066
935
|
const locked = p.sectorId && controller.isSectorCompleted(p.sectorId);
|
|
1067
936
|
const isConnectivityLocked = lockedPieceId === p.id;
|
|
1068
|
-
|
|
937
|
+
selectedPieceId === p.id;
|
|
1069
938
|
const isCarriedInvalid = isDragging && dragInvalid;
|
|
1070
939
|
const translateX = p.x - bb.min.x;
|
|
1071
940
|
const translateY = p.y - bb.min.y;
|
|
1072
941
|
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) => {
|
|
942
|
+
const showBorders = shouldShowBorders();
|
|
1073
943
|
shouldUseSelectiveBorders(p.blueprintId);
|
|
1074
944
|
return /* @__PURE__ */ React.createElement(React.Fragment, { key: idx }, /* @__PURE__ */ React.createElement(
|
|
1075
945
|
"path",
|
|
@@ -1080,51 +950,38 @@ function BoardView(props) {
|
|
|
1080
950
|
stroke: "none",
|
|
1081
951
|
onPointerDown: (e) => onPiecePointerDown(e, p)
|
|
1082
952
|
}
|
|
1083
|
-
),
|
|
1084
|
-
// For pieces with selective borders: render individual edge strokes with edge detection
|
|
1085
|
-
(() => {
|
|
1086
|
-
const allPiecesInSector = pieces.filter((piece) => piece.sectorId === p.sectorId);
|
|
1087
|
-
const pieceAsPiece = { ...p, pos: { x: p.x, y: p.y } };
|
|
1088
|
-
const allPiecesAsPieces = allPiecesInSector.map((piece) => ({ ...piece, pos: { x: piece.x, y: piece.y } }));
|
|
1089
|
-
const hiddenEdges = getHiddenEdgesForPolygon(pieceAsPiece, idx, allPiecesAsPieces, (id) => controller.getBlueprint(id), (kind) => controller.getPrimitive(kind));
|
|
1090
|
-
const draggedPiece = draggingId ? allPiecesInSector.find((piece) => piece.id === draggingId) : null;
|
|
1091
|
-
let wasTouchingDraggedPiece;
|
|
1092
|
-
if (p.blueprintId.startsWith("comp:")) {
|
|
1093
|
-
const internalHiddenEdges = getHiddenEdgesForPolygon(pieceAsPiece, idx, [], (id) => controller.getBlueprint(id), (kind) => controller.getPrimitive(kind));
|
|
1094
|
-
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);
|
|
1095
|
-
wasTouchingDraggedPiece = externalHiddenEdges.map((external, i) => external && !internalHiddenEdges[i]);
|
|
1096
|
-
} else {
|
|
1097
|
-
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);
|
|
1098
|
-
}
|
|
1099
|
-
return generateEdgeStrokePaths(poly).map((strokePath, strokeIdx) => {
|
|
1100
|
-
const wasHiddenDueToDraggedPiece = wasTouchingDraggedPiece[strokeIdx] || false;
|
|
1101
|
-
let isHidden;
|
|
1102
|
-
if (isDragging && p.blueprintId.startsWith("comp:")) {
|
|
1103
|
-
const internalHiddenEdges = getHiddenEdgesForPolygon(pieceAsPiece, idx, [], (id) => controller.getBlueprint(id), (kind) => controller.getPrimitive(kind));
|
|
1104
|
-
isHidden = internalHiddenEdges[strokeIdx] || false;
|
|
1105
|
-
} else {
|
|
1106
|
-
isHidden = isDragging ? false : (hiddenEdges[strokeIdx] || false) && !wasHiddenDueToDraggedPiece;
|
|
1107
|
-
}
|
|
1108
|
-
return /* @__PURE__ */ React.createElement(
|
|
1109
|
-
"path",
|
|
1110
|
-
{
|
|
1111
|
-
key: `stroke-${idx}-${strokeIdx}`,
|
|
1112
|
-
d: strokePath,
|
|
1113
|
-
fill: "none",
|
|
1114
|
-
stroke: isHidden ? "none" : isConnectivityLocked ? CONFIG.color.piece.invalidStroke : isCarriedInvalid ? CONFIG.color.piece.invalidStroke : isSelected || isDragging ? CONFIG.color.piece.selectedStroke : CONFIG.color.piece.borderStroke,
|
|
1115
|
-
strokeWidth: isHidden ? 0 : isSelected || isDragging ? CONFIG.size.stroke.pieceSelectedPx : CONFIG.size.stroke.pieceBorderPx,
|
|
1116
|
-
onPointerDown: (e) => onPiecePointerDown(e, p)
|
|
1117
|
-
}
|
|
1118
|
-
);
|
|
1119
|
-
});
|
|
1120
|
-
})()
|
|
1121
|
-
) ));
|
|
953
|
+
), showBorders);
|
|
1122
954
|
}));
|
|
1123
955
|
}),
|
|
1124
956
|
layout.sectors.map((s) => {
|
|
1125
|
-
const
|
|
1126
|
-
if (
|
|
1127
|
-
|
|
957
|
+
const sectorCfg = controller.state.cfg.sectors.find((ss) => ss.id === s.id);
|
|
958
|
+
if (showTangramDecomposition && sectorCfg?.silhouette.primitiveDecomposition) {
|
|
959
|
+
const primitiveDecomposition = sectorCfg.silhouette.primitiveDecomposition;
|
|
960
|
+
const rect = rectForBand(layout, s, "silhouette", 1);
|
|
961
|
+
const rawPolys = primitiveDecomposition.map((primInfo) => primInfo.polygon);
|
|
962
|
+
const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, { cx: rect.cx, cy: rect.cy });
|
|
963
|
+
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(
|
|
964
|
+
"path",
|
|
965
|
+
{
|
|
966
|
+
d: pathD(scaledPoly),
|
|
967
|
+
fill: CONFIG.color.silhouetteMask,
|
|
968
|
+
opacity: CONFIG.opacity.silhouetteMask,
|
|
969
|
+
stroke: "none"
|
|
970
|
+
}
|
|
971
|
+
), /* @__PURE__ */ React.createElement(
|
|
972
|
+
"path",
|
|
973
|
+
{
|
|
974
|
+
d: pathD(scaledPoly),
|
|
975
|
+
fill: "none",
|
|
976
|
+
stroke: CONFIG.color.tangramDecomposition.stroke,
|
|
977
|
+
strokeWidth: CONFIG.size.stroke.tangramDecompositionPx
|
|
978
|
+
}
|
|
979
|
+
))));
|
|
980
|
+
} else {
|
|
981
|
+
const placedPolys = placedSilBySector.get(s.id) ?? [];
|
|
982
|
+
if (!placedPolys.length) return null;
|
|
983
|
+
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 })));
|
|
984
|
+
}
|
|
1128
985
|
}),
|
|
1129
986
|
anchorDots.map(({ sectorId, valid, invalid }) => {
|
|
1130
987
|
const isInnerRing = sectorId === "inner-ring";
|
|
@@ -1140,10 +997,20 @@ function BoardView(props) {
|
|
|
1140
997
|
}
|
|
1141
998
|
)));
|
|
1142
999
|
}),
|
|
1000
|
+
/* @__PURE__ */ React.createElement("defs", null, /* @__PURE__ */ React.createElement("filter", { id: "invert-to-white" }, /* @__PURE__ */ React.createElement(
|
|
1001
|
+
"feColorMatrix",
|
|
1002
|
+
{
|
|
1003
|
+
type: "matrix",
|
|
1004
|
+
values: "-1 0 0 0 1\n 0 -1 0 0 1\n 0 0 -1 0 1\n 0 0 0 1 0"
|
|
1005
|
+
}
|
|
1006
|
+
))),
|
|
1143
1007
|
(() => {
|
|
1144
1008
|
const isPrep = controller.state.cfg.mode === "prep";
|
|
1145
1009
|
const isSubmitEnabled = isPrep ? controller.isSubmitEnabled() : true;
|
|
1146
1010
|
const isClickable = !draggingId && (!isPrep || isSubmitEnabled);
|
|
1011
|
+
const [imageError, setImageError] = React.useState(false);
|
|
1012
|
+
const iconSize = badgeR * 1.6;
|
|
1013
|
+
const iconOffset = iconSize / 2;
|
|
1147
1014
|
return /* @__PURE__ */ React.createElement(
|
|
1148
1015
|
"g",
|
|
1149
1016
|
{
|
|
@@ -1159,7 +1026,7 @@ function BoardView(props) {
|
|
|
1159
1026
|
opacity: isSubmitEnabled ? 1 : 0.5
|
|
1160
1027
|
}
|
|
1161
1028
|
),
|
|
1162
|
-
/* @__PURE__ */ React.createElement(
|
|
1029
|
+
isPrep ? /* @__PURE__ */ React.createElement(
|
|
1163
1030
|
"text",
|
|
1164
1031
|
{
|
|
1165
1032
|
textAnchor: "middle",
|
|
@@ -1168,7 +1035,29 @@ function BoardView(props) {
|
|
|
1168
1035
|
fill: isSubmitEnabled ? CONFIG.color.blueprint.labelFill : "#888",
|
|
1169
1036
|
pointerEvents: "none"
|
|
1170
1037
|
},
|
|
1171
|
-
|
|
1038
|
+
"Submit"
|
|
1039
|
+
) : imageError ? /* @__PURE__ */ React.createElement(
|
|
1040
|
+
"text",
|
|
1041
|
+
{
|
|
1042
|
+
textAnchor: "middle",
|
|
1043
|
+
dominantBaseline: "middle",
|
|
1044
|
+
fontSize: CONFIG.size.badgeFontPx,
|
|
1045
|
+
fill: CONFIG.color.blueprint.labelFill,
|
|
1046
|
+
pointerEvents: "none"
|
|
1047
|
+
},
|
|
1048
|
+
"inventory"
|
|
1049
|
+
) : /* @__PURE__ */ React.createElement(
|
|
1050
|
+
"image",
|
|
1051
|
+
{
|
|
1052
|
+
href: controller.state.blueprintView === "quickstash" ? lockedIcon : unlockedIcon,
|
|
1053
|
+
x: -iconOffset,
|
|
1054
|
+
y: -iconOffset,
|
|
1055
|
+
width: iconSize,
|
|
1056
|
+
height: iconSize,
|
|
1057
|
+
pointerEvents: "none",
|
|
1058
|
+
onError: () => setImageError(true),
|
|
1059
|
+
filter: "url(#invert-to-white)"
|
|
1060
|
+
}
|
|
1172
1061
|
)
|
|
1173
1062
|
);
|
|
1174
1063
|
})(),
|
|
@@ -2586,7 +2475,7 @@ function useClickController(controller, layout, pieces, clickMode, draggingId, s
|
|
|
2586
2475
|
}
|
|
2587
2476
|
|
|
2588
2477
|
class InteractionTracker {
|
|
2589
|
-
constructor(controller, callbacks,
|
|
2478
|
+
constructor(controller, callbacks, trialParams) {
|
|
2590
2479
|
this.gridStep = CONFIG.layout.grid.stepPx;
|
|
2591
2480
|
// Interaction state
|
|
2592
2481
|
this.interactionIndex = 0;
|
|
@@ -2603,8 +2492,7 @@ class InteractionTracker {
|
|
|
2603
2492
|
this.createdMacros = [];
|
|
2604
2493
|
this.controller = controller;
|
|
2605
2494
|
this.callbacks = callbacks;
|
|
2606
|
-
this.
|
|
2607
|
-
this.gameId = gameId || v4();
|
|
2495
|
+
this.trialParams = trialParams;
|
|
2608
2496
|
this.trialStartTime = Date.now();
|
|
2609
2497
|
this.controller.setTrackingCallbacks({
|
|
2610
2498
|
onSectorCompleted: (sectorId) => this.recordSectorCompletion(sectorId)
|
|
@@ -2652,8 +2540,6 @@ class InteractionTracker {
|
|
|
2652
2540
|
const event = {
|
|
2653
2541
|
// Metadata
|
|
2654
2542
|
interactionId: v4(),
|
|
2655
|
-
trialId: this.trialId,
|
|
2656
|
-
gameId: this.gameId,
|
|
2657
2543
|
interactionIndex: this.interactionIndex++,
|
|
2658
2544
|
// Interaction type
|
|
2659
2545
|
interactionType,
|
|
@@ -2759,6 +2645,7 @@ class InteractionTracker {
|
|
|
2759
2645
|
const trialEndTime = Date.now();
|
|
2760
2646
|
const totalDuration = trialEndTime - this.trialStartTime;
|
|
2761
2647
|
const finalSnapshot = this.buildStateSnapshot();
|
|
2648
|
+
const anchorToStimuliRatio = CONFIG.layout.grid.stepPx / CONFIG.layout.grid.unitPx;
|
|
2762
2649
|
const mode = this.controller.state.cfg.mode;
|
|
2763
2650
|
if (mode === "construction") {
|
|
2764
2651
|
const finalBlueprintState = this.buildFinalBlueprintState(finalSnapshot);
|
|
@@ -2770,13 +2657,11 @@ class InteractionTracker {
|
|
|
2770
2657
|
}));
|
|
2771
2658
|
const data = {
|
|
2772
2659
|
trialType: "construction",
|
|
2773
|
-
trialId: this.trialId,
|
|
2774
|
-
gameId: this.gameId,
|
|
2775
|
-
trialNum: 0,
|
|
2776
|
-
// TODO: Plugin should provide this
|
|
2777
2660
|
trialStartTime: this.trialStartTime,
|
|
2778
2661
|
trialEndTime,
|
|
2779
2662
|
totalDuration,
|
|
2663
|
+
anchorToStimuliRatio,
|
|
2664
|
+
trialParams: this.trialParams,
|
|
2780
2665
|
endReason,
|
|
2781
2666
|
completionTimes: this.completionTimes,
|
|
2782
2667
|
finalBlueprintState,
|
|
@@ -2795,13 +2680,11 @@ class InteractionTracker {
|
|
|
2795
2680
|
}));
|
|
2796
2681
|
const data = {
|
|
2797
2682
|
trialType: "prep",
|
|
2798
|
-
trialId: this.trialId,
|
|
2799
|
-
gameId: this.gameId,
|
|
2800
|
-
trialNum: 0,
|
|
2801
|
-
// TODO: Plugin should provide this
|
|
2802
2683
|
trialStartTime: this.trialStartTime,
|
|
2803
2684
|
trialEndTime,
|
|
2804
2685
|
totalDuration,
|
|
2686
|
+
anchorToStimuliRatio,
|
|
2687
|
+
trialParams: this.trialParams,
|
|
2805
2688
|
endReason: "submit",
|
|
2806
2689
|
createdMacros: finalMacros,
|
|
2807
2690
|
quickstashMacros,
|
|
@@ -3059,6 +2942,9 @@ function GameBoard(props) {
|
|
|
3059
2942
|
maxQuickstashSlots,
|
|
3060
2943
|
maxCompositeSize,
|
|
3061
2944
|
mode,
|
|
2945
|
+
showTangramDecomposition,
|
|
2946
|
+
instructions,
|
|
2947
|
+
trialParams,
|
|
3062
2948
|
width: _width,
|
|
3063
2949
|
height: _height,
|
|
3064
2950
|
onSectorComplete,
|
|
@@ -3068,6 +2954,7 @@ function GameBoard(props) {
|
|
|
3068
2954
|
onTrialEnd,
|
|
3069
2955
|
onControllerReady
|
|
3070
2956
|
} = props;
|
|
2957
|
+
const [timeRemaining, setTimeRemaining] = React.useState(timeLimitMs > 0 ? Math.floor(timeLimitMs / 1e3) : 0);
|
|
3071
2958
|
const controller = React.useMemo(() => {
|
|
3072
2959
|
const gameConfig = {
|
|
3073
2960
|
n: sectors.length,
|
|
@@ -3094,8 +2981,8 @@ function GameBoard(props) {
|
|
|
3094
2981
|
const callbacks = {};
|
|
3095
2982
|
if (onInteraction) callbacks.onInteraction = onInteraction;
|
|
3096
2983
|
if (onTrialEnd) callbacks.onTrialEnd = onTrialEnd;
|
|
3097
|
-
return new InteractionTracker(controller, callbacks);
|
|
3098
|
-
}, [controller, onInteraction, onTrialEnd]);
|
|
2984
|
+
return new InteractionTracker(controller, callbacks, trialParams);
|
|
2985
|
+
}, [controller, onInteraction, onTrialEnd, trialParams]);
|
|
3099
2986
|
React.useEffect(() => {
|
|
3100
2987
|
if (onControllerReady) {
|
|
3101
2988
|
onControllerReady(controller);
|
|
@@ -3175,22 +3062,22 @@ function GameBoard(props) {
|
|
|
3175
3062
|
}), [handleSectorComplete, onPiecePlace, onPieceRemove]);
|
|
3176
3063
|
const getGameboardStyle = () => {
|
|
3177
3064
|
const baseStyle = {
|
|
3178
|
-
margin: "
|
|
3065
|
+
margin: "10px",
|
|
3179
3066
|
display: "flex",
|
|
3180
3067
|
alignItems: "center",
|
|
3181
3068
|
justifyContent: "center",
|
|
3182
3069
|
position: "relative"
|
|
3183
3070
|
};
|
|
3184
3071
|
if (layoutMode === "circle") {
|
|
3185
|
-
const size = Math.min(window.innerWidth *
|
|
3072
|
+
const size = Math.min(window.innerWidth * CONFIG.layout.viewportScale, window.innerHeight * CONFIG.layout.viewportScale);
|
|
3186
3073
|
return {
|
|
3187
3074
|
...baseStyle,
|
|
3188
3075
|
width: `${size}px`,
|
|
3189
3076
|
height: `${size}px`
|
|
3190
3077
|
};
|
|
3191
3078
|
} else {
|
|
3192
|
-
const maxWidth = Math.min(window.innerWidth *
|
|
3193
|
-
const maxHeight = Math.min(window.innerWidth *
|
|
3079
|
+
const maxWidth = Math.min(window.innerWidth * CONFIG.layout.viewportScale, window.innerHeight * CONFIG.layout.viewportScale * 2);
|
|
3080
|
+
const maxHeight = Math.min(window.innerWidth * CONFIG.layout.viewportScale / 2, window.innerHeight * CONFIG.layout.viewportScale);
|
|
3194
3081
|
return {
|
|
3195
3082
|
...baseStyle,
|
|
3196
3083
|
width: `${maxWidth}px`,
|
|
@@ -3359,7 +3246,68 @@ function GameBoard(props) {
|
|
|
3359
3246
|
force();
|
|
3360
3247
|
e.stopPropagation();
|
|
3361
3248
|
};
|
|
3362
|
-
|
|
3249
|
+
React.useEffect(() => {
|
|
3250
|
+
if (timeLimitMs === 0) return;
|
|
3251
|
+
const interval = setInterval(() => {
|
|
3252
|
+
setTimeRemaining((prev) => {
|
|
3253
|
+
if (prev <= 1) {
|
|
3254
|
+
clearInterval(interval);
|
|
3255
|
+
return 0;
|
|
3256
|
+
}
|
|
3257
|
+
return prev - 1;
|
|
3258
|
+
});
|
|
3259
|
+
}, 1e3);
|
|
3260
|
+
return () => clearInterval(interval);
|
|
3261
|
+
}, [timeLimitMs]);
|
|
3262
|
+
const formatTime = (seconds) => {
|
|
3263
|
+
const mins = Math.floor(seconds / 60);
|
|
3264
|
+
const secs = Math.floor(seconds % 60);
|
|
3265
|
+
return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
|
|
3266
|
+
};
|
|
3267
|
+
const containerStyle = {
|
|
3268
|
+
display: "flex",
|
|
3269
|
+
flexDirection: "column",
|
|
3270
|
+
width: "100%"
|
|
3271
|
+
// minHeight: '100vh'
|
|
3272
|
+
};
|
|
3273
|
+
const headerStyle = {
|
|
3274
|
+
display: "flex",
|
|
3275
|
+
flexDirection: "row",
|
|
3276
|
+
justifyContent: "space-between",
|
|
3277
|
+
alignItems: "center",
|
|
3278
|
+
padding: "20px",
|
|
3279
|
+
background: "#f5f5f5",
|
|
3280
|
+
flex: "0 0 auto"
|
|
3281
|
+
};
|
|
3282
|
+
const instructionsStyle = {
|
|
3283
|
+
flexGrow: 1,
|
|
3284
|
+
fontSize: "20px",
|
|
3285
|
+
lineHeight: 1.5,
|
|
3286
|
+
marginRight: "20px"
|
|
3287
|
+
};
|
|
3288
|
+
const timerStyle = {
|
|
3289
|
+
fontSize: "24px",
|
|
3290
|
+
fontWeight: "bold",
|
|
3291
|
+
fontFamily: "monospace",
|
|
3292
|
+
color: "#333",
|
|
3293
|
+
minWidth: "80px",
|
|
3294
|
+
textAlign: "right"
|
|
3295
|
+
};
|
|
3296
|
+
const gameboardWrapperStyle = {
|
|
3297
|
+
flex: "1 1 auto",
|
|
3298
|
+
display: "flex",
|
|
3299
|
+
alignItems: "center",
|
|
3300
|
+
justifyContent: "center",
|
|
3301
|
+
overflow: "hidden"
|
|
3302
|
+
};
|
|
3303
|
+
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(
|
|
3304
|
+
"div",
|
|
3305
|
+
{
|
|
3306
|
+
className: "tangram-instructions",
|
|
3307
|
+
style: instructionsStyle,
|
|
3308
|
+
dangerouslySetInnerHTML: { __html: instructions }
|
|
3309
|
+
}
|
|
3310
|
+
), 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(
|
|
3363
3311
|
BoardView,
|
|
3364
3312
|
{
|
|
3365
3313
|
controller,
|
|
@@ -3385,9 +3333,11 @@ function GameBoard(props) {
|
|
|
3385
3333
|
onPointerMove,
|
|
3386
3334
|
onPointerUp,
|
|
3387
3335
|
onCenterBadgePointerDown,
|
|
3336
|
+
showTangramDecomposition: showTangramDecomposition ?? false,
|
|
3337
|
+
scaleS,
|
|
3388
3338
|
...eventCallbacks
|
|
3389
3339
|
}
|
|
3390
|
-
));
|
|
3340
|
+
))));
|
|
3391
3341
|
}
|
|
3392
3342
|
|
|
3393
3343
|
const U = 40;
|
|
@@ -3555,17 +3505,22 @@ function startConstructionTrial(display_element, params, _jsPsych) {
|
|
|
3555
3505
|
const isCanonical = CANON.has(tanName);
|
|
3556
3506
|
return isCanonical;
|
|
3557
3507
|
});
|
|
3558
|
-
const mask = filteredTans.map((tan
|
|
3508
|
+
const mask = filteredTans.map((tan) => {
|
|
3559
3509
|
const polygon = tan.vertices.map(([x, y]) => ({ x: x ?? 0, y: -(y ?? 0) }));
|
|
3560
3510
|
return polygon;
|
|
3561
3511
|
});
|
|
3512
|
+
const primitiveDecomposition = filteredTans.map((tan) => ({
|
|
3513
|
+
kind: tan.name ?? tan.kind,
|
|
3514
|
+
polygon: tan.vertices.map(([x, y]) => ({ x: x ?? 0, y: -(y ?? 0) }))
|
|
3515
|
+
}));
|
|
3562
3516
|
const sectorId = `sector${index}`;
|
|
3563
3517
|
const sector = {
|
|
3564
3518
|
id: sectorId,
|
|
3565
3519
|
tangramId: tangramSpec.tangramID,
|
|
3566
3520
|
silhouette: {
|
|
3567
3521
|
id: sectorId,
|
|
3568
|
-
mask
|
|
3522
|
+
mask,
|
|
3523
|
+
primitiveDecomposition
|
|
3569
3524
|
}
|
|
3570
3525
|
};
|
|
3571
3526
|
return sector;
|
|
@@ -3584,6 +3539,7 @@ function startConstructionTrial(display_element, params, _jsPsych) {
|
|
|
3584
3539
|
quickstash = params.quickstash_macros;
|
|
3585
3540
|
}
|
|
3586
3541
|
}
|
|
3542
|
+
const { onInteraction, onTrialEnd, ...trialParams } = params;
|
|
3587
3543
|
const gameBoardProps = {
|
|
3588
3544
|
sectors,
|
|
3589
3545
|
quickstash,
|
|
@@ -3594,6 +3550,9 @@ function startConstructionTrial(display_element, params, _jsPsych) {
|
|
|
3594
3550
|
timeLimitMs: params.time_limit_ms,
|
|
3595
3551
|
maxQuickstashSlots: CONFIG.layout.defaults.maxQuickstashSlots,
|
|
3596
3552
|
mode: "construction",
|
|
3553
|
+
showTangramDecomposition: params.show_tangram_decomposition ?? false,
|
|
3554
|
+
trialParams,
|
|
3555
|
+
...params.instructions && { instructions: params.instructions },
|
|
3597
3556
|
...params.onInteraction && { onInteraction: params.onInteraction },
|
|
3598
3557
|
...params.onTrialEnd && { onTrialEnd: params.onTrialEnd }
|
|
3599
3558
|
};
|
|
@@ -3657,6 +3616,18 @@ const info$1 = {
|
|
|
3657
3616
|
default: 18,
|
|
3658
3617
|
description: "Snap radius for anchor-based piece placement"
|
|
3659
3618
|
},
|
|
3619
|
+
/** Whether to show tangram target shapes decomposed into individual primitives with borders */
|
|
3620
|
+
show_tangram_decomposition: {
|
|
3621
|
+
type: ParameterType.BOOL,
|
|
3622
|
+
default: false,
|
|
3623
|
+
description: "Whether to show tangram target shapes decomposed into individual primitives with borders"
|
|
3624
|
+
},
|
|
3625
|
+
/** HTML content to display above the gameboard as instructions */
|
|
3626
|
+
instructions: {
|
|
3627
|
+
type: ParameterType.STRING,
|
|
3628
|
+
default: "",
|
|
3629
|
+
description: "HTML content to display above the gameboard as instructions"
|
|
3630
|
+
},
|
|
3660
3631
|
/** Callback fired after each interaction (piece pickup + placedown) */
|
|
3661
3632
|
onInteraction: {
|
|
3662
3633
|
type: ParameterType.FUNCTION,
|
|
@@ -3730,6 +3701,8 @@ class TangramConstructPlugin {
|
|
|
3730
3701
|
input: trial.input,
|
|
3731
3702
|
layout: trial.layout,
|
|
3732
3703
|
time_limit_ms: trial.time_limit_ms,
|
|
3704
|
+
show_tangram_decomposition: trial.show_tangram_decomposition,
|
|
3705
|
+
instructions: trial.instructions,
|
|
3733
3706
|
onInteraction: trial.onInteraction,
|
|
3734
3707
|
onTrialEnd: wrappedOnTrialEnd
|
|
3735
3708
|
};
|
|
@@ -3751,8 +3724,9 @@ function startPrepTrial(display_element, params, jsPsych) {
|
|
|
3751
3724
|
onInteraction,
|
|
3752
3725
|
onTrialEnd
|
|
3753
3726
|
} = params;
|
|
3727
|
+
const { onInteraction: _, onTrialEnd: __, ...trialParams } = params;
|
|
3754
3728
|
const PRIMITIVE_BLUEPRINTS_ORDERED = [...PRIMITIVE_BLUEPRINTS].sort((a, b) => primitiveOrder.indexOf(a.kind) - primitiveOrder.indexOf(b.kind));
|
|
3755
|
-
const prepSectors = Array.from({ length: numQuickstashSlots }, (
|
|
3729
|
+
const prepSectors = Array.from({ length: numQuickstashSlots }, (_2, i) => ({
|
|
3756
3730
|
id: `prep-sector-${i}`,
|
|
3757
3731
|
tangramId: `prep-sector-${i}`,
|
|
3758
3732
|
// dummy value since prep mode doesn't have tangrams
|
|
@@ -3836,6 +3810,8 @@ function startPrepTrial(display_element, params, jsPsych) {
|
|
|
3836
3810
|
// Enable prep-specific behavior
|
|
3837
3811
|
minPiecesPerMacro,
|
|
3838
3812
|
requireAllSlots,
|
|
3813
|
+
trialParams,
|
|
3814
|
+
...params.instructions && { instructions: params.instructions },
|
|
3839
3815
|
onControllerReady: handleControllerReady,
|
|
3840
3816
|
...onInteraction && { onInteraction },
|
|
3841
3817
|
...onTrialEnd && { onTrialEnd }
|
|
@@ -3889,6 +3865,12 @@ const info = {
|
|
|
3889
3865
|
default: ["square", "smalltriangle", "parallelogram", "medtriangle", "largetriangle"],
|
|
3890
3866
|
description: "Array of primitive names in the order they should be displayed"
|
|
3891
3867
|
},
|
|
3868
|
+
/** HTML content to display above the gameboard as instructions */
|
|
3869
|
+
instructions: {
|
|
3870
|
+
type: ParameterType.STRING,
|
|
3871
|
+
default: void 0,
|
|
3872
|
+
description: "HTML content to display above the gameboard as instructions"
|
|
3873
|
+
},
|
|
3892
3874
|
/** Callback fired after each interaction (optional analytics hook) */
|
|
3893
3875
|
onInteraction: {
|
|
3894
3876
|
type: ParameterType.FUNCTION,
|
|
@@ -3940,6 +3922,7 @@ class TangramPrepPlugin {
|
|
|
3940
3922
|
requireAllSlots: trial.require_all_slots,
|
|
3941
3923
|
quickstashMacros: trial.quickstash_macros,
|
|
3942
3924
|
primitiveOrder: trial.primitive_order,
|
|
3925
|
+
instructions: trial.instructions,
|
|
3943
3926
|
onInteraction: trial.onInteraction,
|
|
3944
3927
|
onTrialEnd: wrappedOnTrialEnd
|
|
3945
3928
|
};
|