jspsych-tangram 0.0.12 → 0.0.14
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 +283 -64
- 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 +283 -64
- package/dist/construct/index.cjs.map +1 -1
- package/dist/construct/index.d.ts +36 -0
- package/dist/construct/index.js +283 -64
- package/dist/construct/index.js.map +1 -1
- package/dist/index.cjs +394 -94
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +84 -0
- package/dist/index.js +394 -94
- package/dist/index.js.map +1 -1
- package/dist/nback/index.browser.js +112 -37
- package/dist/nback/index.browser.js.map +1 -1
- package/dist/nback/index.browser.min.js +11 -11
- package/dist/nback/index.browser.min.js.map +1 -1
- package/dist/nback/index.cjs +112 -37
- package/dist/nback/index.cjs.map +1 -1
- package/dist/nback/index.d.ts +24 -0
- package/dist/nback/index.js +112 -37
- package/dist/nback/index.js.map +1 -1
- package/dist/prep/index.browser.js +278 -65
- package/dist/prep/index.browser.js.map +1 -1
- package/dist/prep/index.browser.min.js +11 -11
- package/dist/prep/index.browser.min.js.map +1 -1
- package/dist/prep/index.cjs +278 -65
- package/dist/prep/index.cjs.map +1 -1
- package/dist/prep/index.d.ts +24 -0
- package/dist/prep/index.js +278 -65
- package/dist/prep/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/components/board/BoardView.tsx +372 -124
- package/src/core/components/board/GameBoard.tsx +39 -44
- package/src/core/components/board/useGameBoard.ts +52 -0
- package/src/core/components/index.ts +4 -2
- package/src/core/components/pieces/BlueprintRing.tsx +100 -67
- package/src/core/components/pieces/useBlueprintRing.ts +39 -0
- package/src/core/config/config.ts +25 -10
- package/src/plugins/tangram-construct/ConstructionApp.tsx +7 -1
- package/src/plugins/tangram-construct/index.ts +22 -1
- package/src/plugins/tangram-nback/NBackApp.tsx +88 -29
- package/src/plugins/tangram-nback/index.ts +14 -0
- package/src/plugins/tangram-prep/PrepApp.tsx +7 -1
- package/src/plugins/tangram-prep/index.ts +14 -0
- package/tangram-construct.min.js +11 -11
- package/tangram-nback.min.js +11 -11
- package/tangram-prep.min.js +11 -11
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { Poly, TanKind, Blueprint, PrimitiveBlueprint } from "@/core/domain/types";
|
|
3
3
|
import { wedgePath, rectForBand, type CircleLayout } from "@/core/domain/layout";
|
|
4
|
-
import { boundsOfBlueprint, placeSilhouetteGridAlignedAsPolys } from "@/core/engine/geometry";
|
|
4
|
+
import { boundsOfBlueprint, placeSilhouetteGridAlignedAsPolys, polysAABB } from "@/core/engine/geometry";
|
|
5
5
|
import { CONFIG } from "@/core/config/config";
|
|
6
6
|
import { generateEdgeStrokePaths, shouldUseSelectiveBorders, shouldShowBorders, getHiddenEdgesForPolygon } from "@/core/engine/validation/border-rendering";
|
|
7
7
|
|
|
@@ -34,6 +34,9 @@ export type BoardViewProps = {
|
|
|
34
34
|
dragInvalid: boolean;
|
|
35
35
|
lockedPieceId: string | null;
|
|
36
36
|
showTangramDecomposition?: boolean; // whether to show tangram decomposed into primitives
|
|
37
|
+
usePrimitiveColorsBlueprints?: boolean; // whether to use distinct colors for blueprints in dock
|
|
38
|
+
usePrimitiveColorsTargets?: boolean; // whether to use distinct colors for target tangrams
|
|
39
|
+
primitiveColorIndices?: number[]; // array of 5 integers mapping primitives to color indices
|
|
37
40
|
|
|
38
41
|
// refs + handlers wired by the container
|
|
39
42
|
svgRef: React.RefObject<SVGSVGElement>;
|
|
@@ -57,124 +60,138 @@ function pathD(poly: Poly) {
|
|
|
57
60
|
return `M ${poly.map((pt) => `${pt.x} ${pt.y}`).join(" L ")} Z`;
|
|
58
61
|
}
|
|
59
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Get the fill color for a piece or blueprint based on its type
|
|
65
|
+
*
|
|
66
|
+
* REQUIRES: blueprint is valid, primitiveColorIndices is an array of 5 valid indices
|
|
67
|
+
* EFFECTS: Returns color based on primitive type if usePrimitiveColors is true,
|
|
68
|
+
* otherwise returns the default color. Composites always use default color.
|
|
69
|
+
*/
|
|
70
|
+
function getPieceColor(
|
|
71
|
+
blueprint: Blueprint,
|
|
72
|
+
usePrimitiveColors: boolean,
|
|
73
|
+
defaultColor: string,
|
|
74
|
+
primitiveColorIndices: number[]
|
|
75
|
+
): string {
|
|
76
|
+
if (!usePrimitiveColors) {
|
|
77
|
+
return defaultColor;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// For primitive blueprints, use the mapped color from CONFIG
|
|
81
|
+
if ('kind' in blueprint) {
|
|
82
|
+
const kind = blueprint.kind as TanKind;
|
|
83
|
+
// Map primitive kind to index: square=0, smalltriangle=1, parallelogram=2, medtriangle=3, largetriangle=4
|
|
84
|
+
const kindToIndex: Record<TanKind, number> = {
|
|
85
|
+
'square': 0,
|
|
86
|
+
'smalltriangle': 1,
|
|
87
|
+
'parallelogram': 2,
|
|
88
|
+
'medtriangle': 3,
|
|
89
|
+
'largetriangle': 4
|
|
90
|
+
};
|
|
91
|
+
const primitiveIndex = kindToIndex[kind];
|
|
92
|
+
if (primitiveIndex !== undefined && primitiveColorIndices[primitiveIndex] !== undefined) {
|
|
93
|
+
const colorIndex = primitiveColorIndices[primitiveIndex];
|
|
94
|
+
const color = CONFIG.color.primitiveColors[colorIndex];
|
|
95
|
+
if (color) {
|
|
96
|
+
return color;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return defaultColor;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// For composite blueprints, always use the default color
|
|
103
|
+
return defaultColor;
|
|
104
|
+
}
|
|
105
|
+
|
|
60
106
|
export default function BoardView(props: BoardViewProps) {
|
|
61
107
|
const {
|
|
62
108
|
controller, layout, viewBox, width, height,
|
|
63
109
|
badgeR, badgeCenter,
|
|
64
110
|
placedSilBySector, anchorDots, pieces, scaleS,
|
|
65
111
|
clickMode, draggingId, selectedPieceId, dragInvalid, lockedPieceId, showTangramDecomposition,
|
|
112
|
+
usePrimitiveColorsBlueprints, usePrimitiveColorsTargets, primitiveColorIndices,
|
|
66
113
|
svgRef, setPieceRef,
|
|
67
114
|
onPiecePointerDown, onBlueprintPointerDown, onRootPointerDown, onPointerMove, onPointerUp, onCenterBadgePointerDown,
|
|
68
115
|
} = props;
|
|
69
116
|
|
|
70
117
|
const VW = viewBox.w, VH = viewBox.h;
|
|
71
118
|
|
|
72
|
-
//
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
{layout.sectors.map((s, i) => {
|
|
149
|
-
const done = !!controller.state.sectors[s.id].completedAt;
|
|
150
|
-
const baseSil = i % 2 ? CONFIG.color.bands.silhouette.fillOdd : CONFIG.color.bands.silhouette.fillEven;
|
|
151
|
-
const baseWork = i % 2 ? CONFIG.color.bands.workspace.fillOdd : CONFIG.color.bands.workspace.fillEven;
|
|
152
|
-
const sil = layout.bands.silhouette;
|
|
153
|
-
const work = layout.bands.workspace;
|
|
154
|
-
return (
|
|
155
|
-
<g key={`bands-${s.id}`}>
|
|
156
|
-
{controller.state.cfg.target === "workspace" ? (
|
|
157
|
-
<>
|
|
158
|
-
<path d={wedgePath(layout.cx, layout.cy, sil[0], sil[1], s.start, s.end)} fill={baseSil} stroke={CONFIG.color.bands.silhouette.stroke} strokeWidth={CONFIG.size.stroke.bandPx} pointerEvents="none" />
|
|
159
|
-
<path d={wedgePath(layout.cx, layout.cy, work[0], work[1], s.start, s.end)} fill={done ? CONFIG.color.completion.fill : baseWork} stroke={done ? CONFIG.color.completion.stroke : CONFIG.color.bands.workspace.stroke} strokeWidth={CONFIG.size.stroke.bandPx} pointerEvents="none" />
|
|
160
|
-
</>
|
|
161
|
-
) : (
|
|
162
|
-
<path d={wedgePath(layout.cx, layout.cy, sil[0], sil[1], s.start, s.end)} fill={done ? CONFIG.color.completion.fill : baseSil} stroke={done ? CONFIG.color.completion.stroke : CONFIG.color.bands.silhouette.stroke} strokeWidth={CONFIG.size.stroke.bandPx} pointerEvents="none" />
|
|
163
|
-
)}
|
|
164
|
-
</g>
|
|
165
|
-
);
|
|
166
|
-
})}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
{/* live pieces - MOVED ABOVE SILHOUETTES FOR TESTING */}
|
|
170
|
-
{pieces
|
|
171
|
-
.sort((a, b) => {
|
|
172
|
-
// Sort so that dragged piece is rendered last (on top)
|
|
173
|
-
if (draggingId === a.id) return 1;
|
|
174
|
-
if (draggingId === b.id) return -1;
|
|
175
|
-
return 0;
|
|
176
|
-
})
|
|
177
|
-
.map((p) => {
|
|
119
|
+
// Helper function to render silhouettes
|
|
120
|
+
const renderSilhouettes = () => layout.sectors.map((s) => {
|
|
121
|
+
const sectorCfg = controller.state.cfg.sectors.find((ss: any) => ss.id === s.id);
|
|
122
|
+
|
|
123
|
+
// Check if we should render decomposed primitives
|
|
124
|
+
if (showTangramDecomposition && sectorCfg?.silhouette.primitiveDecomposition) {
|
|
125
|
+
const primitiveDecomposition = sectorCfg.silhouette.primitiveDecomposition;
|
|
126
|
+
|
|
127
|
+
// Use same positioning logic as unified silhouette
|
|
128
|
+
const rect = rectForBand(layout, s, "silhouette", 1.0);
|
|
129
|
+
const rawPolys = primitiveDecomposition.map((primInfo: any) => primInfo.polygon);
|
|
130
|
+
const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, { cx: rect.cx, cy: rect.cy });
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<g key={`sil-decomposed-${s.id}`} pointerEvents="none">
|
|
134
|
+
{placedPolys.map((scaledPoly, i) => {
|
|
135
|
+
// Get color for this primitive based on its kind
|
|
136
|
+
const primInfo = primitiveDecomposition[i];
|
|
137
|
+
let fillColor = CONFIG.color.silhouetteMask;
|
|
138
|
+
|
|
139
|
+
if (usePrimitiveColorsTargets && primInfo?.kind && primitiveColorIndices) {
|
|
140
|
+
// Map primitive kind to index
|
|
141
|
+
const kindToIndex: Record<TanKind, number> = {
|
|
142
|
+
'square': 0,
|
|
143
|
+
'smalltriangle': 1,
|
|
144
|
+
'parallelogram': 2,
|
|
145
|
+
'medtriangle': 3,
|
|
146
|
+
'largetriangle': 4
|
|
147
|
+
};
|
|
148
|
+
const primitiveIndex = kindToIndex[primInfo.kind as TanKind];
|
|
149
|
+
if (primitiveIndex !== undefined && primitiveColorIndices[primitiveIndex] !== undefined) {
|
|
150
|
+
const colorIndex = primitiveColorIndices[primitiveIndex];
|
|
151
|
+
const color = CONFIG.color.primitiveColors[colorIndex];
|
|
152
|
+
if (color) {
|
|
153
|
+
fillColor = color;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<path
|
|
160
|
+
key={`prim-fill-${i}`}
|
|
161
|
+
d={pathD(scaledPoly)}
|
|
162
|
+
fill={fillColor}
|
|
163
|
+
opacity={CONFIG.opacity.silhouetteMask}
|
|
164
|
+
stroke="none"
|
|
165
|
+
/>
|
|
166
|
+
);
|
|
167
|
+
})}
|
|
168
|
+
</g>
|
|
169
|
+
);
|
|
170
|
+
} else {
|
|
171
|
+
// Default: render unified silhouette
|
|
172
|
+
const placedPolys = placedSilBySector.get(s.id) ?? [];
|
|
173
|
+
if (!placedPolys.length) return null;
|
|
174
|
+
return (
|
|
175
|
+
<g key={`sil-${s.id}`} pointerEvents="none">
|
|
176
|
+
{placedPolys.map((poly, i) => (
|
|
177
|
+
<path key={i} d={pathD(poly)} fill={CONFIG.color.silhouetteMask} opacity={CONFIG.opacity.silhouetteMask} />
|
|
178
|
+
))}
|
|
179
|
+
</g>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Helper function to render pieces (with optional filter)
|
|
185
|
+
const renderPieces = (piecesFilter?: (p: PieceView) => boolean) => {
|
|
186
|
+
const piecesToRender = piecesFilter ? pieces.filter(piecesFilter) : pieces;
|
|
187
|
+
return piecesToRender
|
|
188
|
+
.sort((a, b) => {
|
|
189
|
+
// Sort so that dragged piece is rendered last (on top)
|
|
190
|
+
if (draggingId === a.id) return 1;
|
|
191
|
+
if (draggingId === b.id) return -1;
|
|
192
|
+
return 0;
|
|
193
|
+
})
|
|
194
|
+
.map((p) => {
|
|
178
195
|
const bp = controller.getBlueprint(p.blueprintId)!;
|
|
179
196
|
const bb = boundsOfBlueprint(bp, (k: string) => controller.getPrimitive(k as TanKind)!);
|
|
180
197
|
const isDragging = draggingId === p.id;
|
|
@@ -184,6 +201,20 @@ export default function BoardView(props: BoardViewProps) {
|
|
|
184
201
|
const isCarriedInvalid = isDragging && dragInvalid;
|
|
185
202
|
const translateX = p.x - bb.min.x;
|
|
186
203
|
const translateY = p.y - bb.min.y;
|
|
204
|
+
|
|
205
|
+
const validFillColor = getPieceColor(
|
|
206
|
+
bp,
|
|
207
|
+
usePrimitiveColorsBlueprints || false,
|
|
208
|
+
CONFIG.color.piece.validFill,
|
|
209
|
+
primitiveColorIndices || [0, 1, 2, 3, 4]
|
|
210
|
+
);
|
|
211
|
+
const draggingFillColor = getPieceColor(
|
|
212
|
+
bp,
|
|
213
|
+
usePrimitiveColorsBlueprints || false,
|
|
214
|
+
CONFIG.color.piece.draggingFill,
|
|
215
|
+
primitiveColorIndices || [0, 1, 2, 3, 4]
|
|
216
|
+
);
|
|
217
|
+
|
|
187
218
|
return (
|
|
188
219
|
<g key={p.id} ref={setPieceRef(p.id)} transform={`translate(${translateX}, ${translateY})`} style={{ cursor: locked ? "default" : clickMode ? "pointer" : "grab" }} pointerEvents={clickMode && isDragging ? "none" : "auto"}>
|
|
189
220
|
{("shape" in bp ? bp.shape : []).map((poly: any, idx: number) => {
|
|
@@ -194,7 +225,7 @@ export default function BoardView(props: BoardViewProps) {
|
|
|
194
225
|
{/* Fill path - always rendered with no borders */}
|
|
195
226
|
<path
|
|
196
227
|
d={pathD(poly)}
|
|
197
|
-
fill={isConnectivityLocked ? CONFIG.color.piece.invalidFill : (isCarriedInvalid ? CONFIG.color.piece.invalidFill : (isDragging ?
|
|
228
|
+
fill={isConnectivityLocked ? CONFIG.color.piece.invalidFill : (isCarriedInvalid ? CONFIG.color.piece.invalidFill : (isDragging ? draggingFillColor : validFillColor))}
|
|
198
229
|
opacity={isConnectivityLocked ? CONFIG.opacity.piece.invalid : (isCarriedInvalid ? CONFIG.opacity.piece.invalid : (isDragging ? CONFIG.opacity.piece.dragging : (locked ? CONFIG.opacity.piece.locked : CONFIG.opacity.piece.normal)))}
|
|
199
230
|
stroke="none"
|
|
200
231
|
onPointerDown={(e) => onPiecePointerDown(e, p)}
|
|
@@ -272,11 +303,188 @@ export default function BoardView(props: BoardViewProps) {
|
|
|
272
303
|
</React.Fragment>
|
|
273
304
|
);
|
|
274
305
|
})}
|
|
306
|
+
|
|
307
|
+
{/* Invalid marker 'X' - shown at piece center when dragging invalid piece */}
|
|
308
|
+
{isDragging && isCarriedInvalid && "shape" in bp && bp.shape.length > 0 && (() => {
|
|
309
|
+
const { cx, cy } = polysAABB(bp.shape);
|
|
310
|
+
const size = CONFIG.size.invalidMarker.sizePx;
|
|
311
|
+
const strokeWidth = CONFIG.size.invalidMarker.strokePx;
|
|
312
|
+
const borderWidth = strokeWidth + 2;
|
|
313
|
+
|
|
314
|
+
return (
|
|
315
|
+
<g key="invalid-marker">
|
|
316
|
+
{/* White border for first line */}
|
|
317
|
+
<line
|
|
318
|
+
x1={cx - size}
|
|
319
|
+
y1={cy - size}
|
|
320
|
+
x2={cx + size}
|
|
321
|
+
y2={cy + size}
|
|
322
|
+
stroke="white"
|
|
323
|
+
strokeWidth={borderWidth}
|
|
324
|
+
strokeLinecap="round"
|
|
325
|
+
/>
|
|
326
|
+
{/* Red interior for first line */}
|
|
327
|
+
<line
|
|
328
|
+
x1={cx - size}
|
|
329
|
+
y1={cy - size}
|
|
330
|
+
x2={cx + size}
|
|
331
|
+
y2={cy + size}
|
|
332
|
+
stroke={CONFIG.color.piece.invalidStroke}
|
|
333
|
+
strokeWidth={strokeWidth}
|
|
334
|
+
strokeLinecap="round"
|
|
335
|
+
/>
|
|
336
|
+
{/* White border for second line */}
|
|
337
|
+
<line
|
|
338
|
+
x1={cx + size}
|
|
339
|
+
y1={cy - size}
|
|
340
|
+
x2={cx - size}
|
|
341
|
+
y2={cy + size}
|
|
342
|
+
stroke="white"
|
|
343
|
+
strokeWidth={borderWidth}
|
|
344
|
+
strokeLinecap="round"
|
|
345
|
+
/>
|
|
346
|
+
{/* Red interior for second line */}
|
|
347
|
+
<line
|
|
348
|
+
x1={cx + size}
|
|
349
|
+
y1={cy - size}
|
|
350
|
+
x2={cx - size}
|
|
351
|
+
y2={cy + size}
|
|
352
|
+
stroke={CONFIG.color.piece.invalidStroke}
|
|
353
|
+
strokeWidth={strokeWidth}
|
|
354
|
+
strokeLinecap="round"
|
|
355
|
+
/>
|
|
356
|
+
</g>
|
|
357
|
+
);
|
|
358
|
+
})()}
|
|
359
|
+
</g>
|
|
360
|
+
);
|
|
361
|
+
});
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
// blueprint ring geometry
|
|
365
|
+
const centerView = controller.state.blueprintView;
|
|
366
|
+
const bps: Blueprint[] =
|
|
367
|
+
centerView === "primitives"
|
|
368
|
+
? (controller.state.primitives as PrimitiveBlueprint[])
|
|
369
|
+
: (controller.state.quickstash as Blueprint[]);
|
|
370
|
+
|
|
371
|
+
const QS_SLOTS = controller.state.cfg.maxQuickstashSlots;
|
|
372
|
+
const PRIM_SLOTS = controller.state.primitives.length;
|
|
373
|
+
const slotsForView = centerView === "primitives" ? Math.max(1, PRIM_SLOTS) : Math.max(1, QS_SLOTS);
|
|
374
|
+
const sweep = layout.mode === "circle" ? Math.PI * 2 : Math.PI;
|
|
375
|
+
const delta = sweep / slotsForView;
|
|
376
|
+
|
|
377
|
+
const start = layout.mode === "circle" ? -Math.PI / 2 : Math.PI;
|
|
378
|
+
const blueprintTheta = (i: number) => start + (i + 0.5) * delta;
|
|
379
|
+
|
|
380
|
+
// chord requirement (reuse same logic as container already used for radii)
|
|
381
|
+
const anchorsDiameterToPx = (anchorsDiag: number, gridPx: number = CONFIG.layout.grid.stepPx) =>
|
|
382
|
+
anchorsDiag * Math.SQRT2 * gridPx;
|
|
383
|
+
const reqAnchors = centerView === "primitives"
|
|
384
|
+
? CONFIG.layout.constraints.primitiveDiamAnchors
|
|
385
|
+
: CONFIG.layout.constraints.quickstashDiamAnchors;
|
|
386
|
+
const D_slot = anchorsDiameterToPx(reqAnchors);
|
|
387
|
+
const R_needed = D_slot / (2 * Math.max(1e-9, Math.sin(delta / 2)));
|
|
388
|
+
|
|
389
|
+
// Minimum radius to avoid overlapping with badge
|
|
390
|
+
// Badge takes up: badgeR + margin, plus we need half the slot diameter for clearance
|
|
391
|
+
const R_min = badgeR + CONFIG.size.centerBadge.marginPx + D_slot / 2;
|
|
392
|
+
const ringMax = layout.innerR - (badgeR + CONFIG.size.centerBadge.marginPx);
|
|
393
|
+
|
|
394
|
+
// Clamp to [R_min, ringMax]
|
|
395
|
+
const blueprintRingR = Math.min(Math.max(R_needed, R_min), ringMax);
|
|
396
|
+
|
|
397
|
+
const renderBlueprintGlyph = (bp: Blueprint, bx: number, by: number) => {
|
|
398
|
+
const bb = boundsOfBlueprint(bp, (k: string) => controller.getPrimitive(k as TanKind)!);
|
|
399
|
+
const cx = bb.min.x + bb.width / 2;
|
|
400
|
+
const cy = bb.min.y + bb.height / 2;
|
|
401
|
+
const selected = false; // container highlights via pending state; optional
|
|
402
|
+
|
|
403
|
+
const fillColor = getPieceColor(
|
|
404
|
+
bp,
|
|
405
|
+
usePrimitiveColorsBlueprints || false,
|
|
406
|
+
CONFIG.color.blueprint.fill,
|
|
407
|
+
primitiveColorIndices || [0, 1, 2, 3, 4]
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
return (
|
|
411
|
+
<g
|
|
412
|
+
key={bp.id}
|
|
413
|
+
transform={`translate(${bx}, ${by}) scale(1) translate(${-cx}, ${-cy})`}
|
|
414
|
+
>
|
|
415
|
+
{("shape" in bp ? bp.shape : []).map((poly, idx) => (
|
|
416
|
+
<path
|
|
417
|
+
key={idx}
|
|
418
|
+
d={pathD(poly)}
|
|
419
|
+
fill={fillColor}
|
|
420
|
+
opacity={CONFIG.opacity.blueprint}
|
|
421
|
+
stroke={selected ? CONFIG.color.blueprint.selectedStroke : "none"}
|
|
422
|
+
strokeWidth={selected ? 2 : 0}
|
|
423
|
+
pointerEvents="visiblePainted"
|
|
424
|
+
style={{ cursor: "pointer" }}
|
|
425
|
+
onPointerDown={(e) => onBlueprintPointerDown(e as any, bp, { bx, by, cx, cy })}
|
|
426
|
+
/>
|
|
427
|
+
))}
|
|
428
|
+
</g>
|
|
429
|
+
);
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
return (
|
|
433
|
+
<svg
|
|
434
|
+
ref={svgRef}
|
|
435
|
+
width={width}
|
|
436
|
+
height={height}
|
|
437
|
+
viewBox={`0 0 ${VW} ${VH}`}
|
|
438
|
+
preserveAspectRatio="xMidYMid meet"
|
|
439
|
+
onPointerMove={onPointerMove}
|
|
440
|
+
onPointerUp={onPointerUp}
|
|
441
|
+
onPointerDown={(e) => {
|
|
442
|
+
onRootPointerDown(e);
|
|
443
|
+
}}
|
|
444
|
+
style={{ background: CONFIG.color.background, touchAction: "none", userSelect: "none" }}
|
|
445
|
+
>
|
|
446
|
+
{/* bands as wedges */}
|
|
447
|
+
{layout.sectors.map((s, i) => {
|
|
448
|
+
const done = !!controller.state.sectors[s.id].completedAt;
|
|
449
|
+
const baseSil = i % 2 ? CONFIG.color.bands.silhouette.fillOdd : CONFIG.color.bands.silhouette.fillEven;
|
|
450
|
+
const baseWork = i % 2 ? CONFIG.color.bands.workspace.fillOdd : CONFIG.color.bands.workspace.fillEven;
|
|
451
|
+
const sil = layout.bands.silhouette;
|
|
452
|
+
const work = layout.bands.workspace;
|
|
453
|
+
return (
|
|
454
|
+
<g key={`bands-${s.id}`}>
|
|
455
|
+
{controller.state.cfg.target === "workspace" ? (
|
|
456
|
+
<>
|
|
457
|
+
<path d={wedgePath(layout.cx, layout.cy, sil[0], sil[1], s.start, s.end)} fill={baseSil} stroke={CONFIG.color.bands.silhouette.stroke} strokeWidth={CONFIG.size.stroke.bandPx} pointerEvents="none" />
|
|
458
|
+
<path d={wedgePath(layout.cx, layout.cy, work[0], work[1], s.start, s.end)} fill={done ? CONFIG.color.completion.fill : baseWork} stroke={done ? CONFIG.color.completion.stroke : CONFIG.color.bands.workspace.stroke} strokeWidth={CONFIG.size.stroke.bandPx} pointerEvents="none" />
|
|
459
|
+
</>
|
|
460
|
+
) : (
|
|
461
|
+
<path d={wedgePath(layout.cx, layout.cy, sil[0], sil[1], s.start, s.end)} fill={done ? CONFIG.color.completion.fill : baseSil} stroke={done ? CONFIG.color.completion.stroke : CONFIG.color.bands.silhouette.stroke} strokeWidth={CONFIG.size.stroke.bandPx} pointerEvents="none" />
|
|
462
|
+
)}
|
|
275
463
|
</g>
|
|
276
464
|
);
|
|
277
465
|
})}
|
|
278
466
|
|
|
279
|
-
|
|
467
|
+
|
|
468
|
+
{/* Conditional rendering based on silhouettesBelowPieces config */}
|
|
469
|
+
{CONFIG.game.silhouettesBelowPieces ? (
|
|
470
|
+
<>
|
|
471
|
+
{/* Render silhouettes first (below all pieces) */}
|
|
472
|
+
{renderSilhouettes()}
|
|
473
|
+
|
|
474
|
+
{/* Then all pieces */}
|
|
475
|
+
{renderPieces()}
|
|
476
|
+
</>
|
|
477
|
+
) : (
|
|
478
|
+
<>
|
|
479
|
+
{/* Render all pieces first */}
|
|
480
|
+
{renderPieces()}
|
|
481
|
+
|
|
482
|
+
{/* Then silhouettes (above all pieces) */}
|
|
483
|
+
{renderSilhouettes()}
|
|
484
|
+
</>
|
|
485
|
+
)}
|
|
486
|
+
|
|
487
|
+
{/* silhouettes (use pre-placed polys) - rendered first so pieces appear on top */}
|
|
280
488
|
{layout.sectors.map((s) => {
|
|
281
489
|
const sectorCfg = controller.state.cfg.sectors.find((ss: any) => ss.id === s.id);
|
|
282
490
|
|
|
@@ -291,25 +499,40 @@ export default function BoardView(props: BoardViewProps) {
|
|
|
291
499
|
|
|
292
500
|
return (
|
|
293
501
|
<g key={`sil-decomposed-${s.id}`} pointerEvents="none">
|
|
294
|
-
{placedPolys.map((scaledPoly, i) =>
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
502
|
+
{placedPolys.map((scaledPoly, i) => {
|
|
503
|
+
// Get color for this primitive based on its kind
|
|
504
|
+
const primInfo = primitiveDecomposition[i];
|
|
505
|
+
let fillColor = CONFIG.color.silhouetteMask;
|
|
506
|
+
|
|
507
|
+
if (usePrimitiveColorsTargets && primInfo?.kind && primitiveColorIndices) {
|
|
508
|
+
// Map primitive kind to index
|
|
509
|
+
const kindToIndex: Record<TanKind, number> = {
|
|
510
|
+
'square': 0,
|
|
511
|
+
'smalltriangle': 1,
|
|
512
|
+
'parallelogram': 2,
|
|
513
|
+
'medtriangle': 3,
|
|
514
|
+
'largetriangle': 4
|
|
515
|
+
};
|
|
516
|
+
const primitiveIndex = kindToIndex[primInfo.kind as TanKind];
|
|
517
|
+
if (primitiveIndex !== undefined && primitiveColorIndices[primitiveIndex] !== undefined) {
|
|
518
|
+
const colorIndex = primitiveColorIndices[primitiveIndex];
|
|
519
|
+
const color = CONFIG.color.primitiveColors[colorIndex];
|
|
520
|
+
if (color) {
|
|
521
|
+
fillColor = color;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return (
|
|
305
527
|
<path
|
|
528
|
+
key={`prim-fill-${i}`}
|
|
306
529
|
d={pathD(scaledPoly)}
|
|
307
530
|
fill="none"
|
|
308
|
-
|
|
309
|
-
|
|
531
|
+
opacity={0}
|
|
532
|
+
stroke="none"
|
|
310
533
|
/>
|
|
311
|
-
|
|
312
|
-
)
|
|
534
|
+
);
|
|
535
|
+
})}
|
|
313
536
|
</g>
|
|
314
537
|
);
|
|
315
538
|
} else {
|
|
@@ -429,6 +652,31 @@ export default function BoardView(props: BoardViewProps) {
|
|
|
429
652
|
</circle>
|
|
430
653
|
</g>
|
|
431
654
|
)}
|
|
655
|
+
|
|
656
|
+
{/* tangram decomposition borders - rendered last so they appear on top of pieces */}
|
|
657
|
+
{showTangramDecomposition && layout.sectors.map((s) => {
|
|
658
|
+
const sectorCfg = controller.state.cfg.sectors.find((ss: any) => ss.id === s.id);
|
|
659
|
+
if (!sectorCfg?.silhouette.primitiveDecomposition) return null;
|
|
660
|
+
|
|
661
|
+
const primitiveDecomposition = sectorCfg.silhouette.primitiveDecomposition;
|
|
662
|
+
const rect = rectForBand(layout, s, "silhouette", 1.0);
|
|
663
|
+
const rawPolys = primitiveDecomposition.map((primInfo: any) => primInfo.polygon);
|
|
664
|
+
const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, { cx: rect.cx, cy: rect.cy });
|
|
665
|
+
|
|
666
|
+
return (
|
|
667
|
+
<g key={`sil-borders-${s.id}`} pointerEvents="none">
|
|
668
|
+
{placedPolys.map((scaledPoly, i) => (
|
|
669
|
+
<path
|
|
670
|
+
key={`prim-border-${i}`}
|
|
671
|
+
d={pathD(scaledPoly)}
|
|
672
|
+
fill="none"
|
|
673
|
+
stroke={CONFIG.color.tangramDecomposition.stroke}
|
|
674
|
+
strokeWidth={CONFIG.size.stroke.tangramDecompositionPx}
|
|
675
|
+
/>
|
|
676
|
+
))}
|
|
677
|
+
</g>
|
|
678
|
+
);
|
|
679
|
+
})}
|
|
432
680
|
</svg>
|
|
433
681
|
);
|
|
434
682
|
}
|