jspsych-tangram 0.0.13 → 0.0.15
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/afc/index.browser.js +17751 -0
- package/dist/afc/index.browser.js.map +1 -0
- package/dist/afc/index.browser.min.js +42 -0
- package/dist/afc/index.browser.min.js.map +1 -0
- package/dist/afc/index.cjs +443 -0
- package/dist/afc/index.cjs.map +1 -0
- package/dist/afc/index.d.ts +169 -0
- package/dist/afc/index.js +441 -0
- package/dist/afc/index.js.map +1 -0
- package/dist/construct/index.browser.js +8 -2
- package/dist/construct/index.browser.js.map +1 -1
- package/dist/construct/index.browser.min.js +10 -10
- package/dist/construct/index.browser.min.js.map +1 -1
- package/dist/construct/index.cjs +8 -2
- package/dist/construct/index.cjs.map +1 -1
- package/dist/construct/index.js +8 -2
- package/dist/construct/index.js.map +1 -1
- package/dist/index.cjs +379 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +178 -12
- package/dist/index.js +379 -12
- package/dist/index.js.map +1 -1
- package/dist/nback/index.browser.js +6 -4
- package/dist/nback/index.browser.js.map +1 -1
- package/dist/nback/index.browser.min.js +8 -8
- package/dist/nback/index.browser.min.js.map +1 -1
- package/dist/nback/index.cjs +6 -4
- package/dist/nback/index.cjs.map +1 -1
- package/dist/nback/index.js +6 -4
- package/dist/nback/index.js.map +1 -1
- package/dist/prep/index.browser.js +8 -2
- package/dist/prep/index.browser.js.map +1 -1
- package/dist/prep/index.browser.min.js +10 -10
- package/dist/prep/index.browser.min.js.map +1 -1
- package/dist/prep/index.cjs +8 -2
- package/dist/prep/index.cjs.map +1 -1
- package/dist/prep/index.js +8 -2
- package/dist/prep/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/components/board/GameBoard.tsx +13 -42
- package/src/core/components/board/useGameBoard.ts +52 -0
- package/src/core/components/index.ts +4 -2
- package/src/core/components/pieces/BlueprintRing.tsx +0 -25
- package/src/core/components/pieces/useBlueprintRing.ts +39 -0
- package/src/index.ts +2 -1
- package/src/plugins/tangram-afc/AFCApp.tsx +341 -0
- package/src/plugins/tangram-afc/index.ts +140 -0
- package/src/plugins/tangram-nback/NBackApp.tsx +3 -3
- package/tangram-construct.min.js +10 -10
- package/tangram-nback.min.js +8 -8
- package/tangram-prep.min.js +10 -10
package/dist/nback/index.cjs
CHANGED
|
@@ -35,7 +35,9 @@ const CONFIG = {
|
|
|
35
35
|
]
|
|
36
36
|
},
|
|
37
37
|
opacity: {
|
|
38
|
-
silhouetteMask: 0.25
|
|
38
|
+
silhouetteMask: 0.25,
|
|
39
|
+
piece: { normal: 1 }
|
|
40
|
+
},
|
|
39
41
|
size: {
|
|
40
42
|
stroke: { bandPx: 5, tangramDecompositionPx: 1 }},
|
|
41
43
|
layout: {
|
|
@@ -230,7 +232,7 @@ function NBackView({ params }) {
|
|
|
230
232
|
{
|
|
231
233
|
d: pathD(scaledPoly),
|
|
232
234
|
fill: fillColor,
|
|
233
|
-
opacity: CONFIG.opacity.silhouetteMask,
|
|
235
|
+
opacity: usePrimitiveColors ? CONFIG.opacity.piece.normal : CONFIG.opacity.silhouetteMask,
|
|
234
236
|
stroke: "none"
|
|
235
237
|
}
|
|
236
238
|
), /* @__PURE__ */ React.createElement(
|
|
@@ -271,7 +273,7 @@ function NBackView({ params }) {
|
|
|
271
273
|
key: `sil-${i}`,
|
|
272
274
|
d: pathD(scaledPoly),
|
|
273
275
|
fill: fillColor,
|
|
274
|
-
opacity: CONFIG.opacity.silhouetteMask,
|
|
276
|
+
opacity: usePrimitiveColors ? CONFIG.opacity.piece.normal : CONFIG.opacity.silhouetteMask,
|
|
275
277
|
stroke: "none"
|
|
276
278
|
}
|
|
277
279
|
);
|
|
@@ -284,7 +286,7 @@ function NBackView({ params }) {
|
|
|
284
286
|
alignItems: "center",
|
|
285
287
|
justifyContent: "center",
|
|
286
288
|
padding: "20px",
|
|
287
|
-
background: "#
|
|
289
|
+
background: "#fff7e0ff"
|
|
288
290
|
} }, instructions && /* @__PURE__ */ React.createElement(
|
|
289
291
|
"div",
|
|
290
292
|
{
|
package/dist/nback/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":["../../src/core/engine/geometry/bounds.ts","../../src/core/config/config.ts","../../src/core/engine/geometry/pieces.ts","../../src/plugins/tangram-nback/NBackApp.tsx","../../src/plugins/tangram-nback/index.ts"],"sourcesContent":["import type { Poly, Vec, PrimitiveBlueprint, CompositeBlueprint, Blueprint } from \"@/core/domain/types\";\n\nexport type AABB = { min: Vec; max: Vec; width: number; height: number };\n\n/** AABB of a single polygon (numeric). */\nexport function aabbOf(poly: Poly): { minX: number; minY: number; maxX: number; maxY: number } {\n let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\n for (const p of poly) {\n if (p.x < minX) minX = p.x;\n if (p.y < minY) minY = p.y;\n if (p.x > maxX) maxX = p.x;\n if (p.y > maxY) maxY = p.y;\n }\n return { minX, minY, maxX, maxY };\n}\n\n/** AABB of one or more polygons with width/height and center. */\nexport function polysAABB(polys: Poly[]) {\n let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\n for (const poly of polys) {\n for (const p of poly) {\n if (p.x < minX) minX = p.x;\n if (p.y < minY) minY = p.y;\n if (p.x > maxX) maxX = p.x;\n if (p.y > maxY) maxY = p.y;\n }\n }\n const width = Math.max(1, maxX - minX);\n const height = Math.max(1, maxY - minY);\n return { min: { x: minX, y: minY }, max: { x: maxX, y: maxY }, width, height, cx:(minX+maxX)/2, cy:(minY+maxY)/2 };\n}\n\n/** Bounds of multiple polygons at the origin. */\nexport function boundsOfShapes(shapes: Poly[]): AABB {\n let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\n for (const poly of shapes) {\n for (const p of poly) {\n if (p.x < minX) minX = p.x;\n if (p.y < minY) minY = p.y;\n if (p.x > maxX) maxX = p.x;\n if (p.y > maxY) maxY = p.y;\n }\n }\n const width = Math.max(0, maxX - minX);\n const height = Math.max(0, maxY - minY);\n return { min: { x: minX, y: minY }, max: { x: maxX, y: maxY }, width, height };\n}\n\n/** Bounds of a primitive blueprint (uses its canonical shape). */\nexport function boundsOfPrimitive(bp: PrimitiveBlueprint): AABB {\n return boundsOfShapes(bp.shape);\n}\n\n/**\n * Bounds of a composite blueprint.\n * If `shape` is provided as precomputed union polygons, we use that.\n * Otherwise we bound the parts’ primitives at their offsets (loose but safe).\n */\nexport function boundsOfComposite(\n bp: CompositeBlueprint,\n primitiveLookup: (kind: string) => PrimitiveBlueprint | undefined\n): AABB {\n if (bp.shape && bp.shape.length) return boundsOfShapes(bp.shape);\n\n let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\n for (const part of bp.parts) {\n const prim = primitiveLookup(part.kind);\n if (!prim) continue;\n const b = boundsOfPrimitive(prim);\n const pMinX = part.offset.x + b.min.x;\n const pMinY = part.offset.y + b.min.y;\n const pMaxX = part.offset.x + b.max.x;\n const pMaxY = part.offset.y + b.max.y;\n if (pMinX < minX) minX = pMinX;\n if (pMinY < minY) minY = pMinY;\n if (pMaxX > maxX) maxX = pMaxX;\n if (pMaxY > maxY) maxY = pMaxY;\n }\n const width = Math.max(0, maxX - minX);\n const height = Math.max(0, maxY - minY);\n return { min: { x: minX, y: minY }, max: { x: maxX, y: maxY }, width, height };\n}\n\n/** Unified bounds helper. */\nexport function boundsOfBlueprint(\n bp: Blueprint,\n primitiveLookup: (kind: string) => PrimitiveBlueprint | undefined\n): AABB {\n if (\"parts\" in bp) return boundsOfComposite(bp, primitiveLookup);\n return boundsOfPrimitive(bp);\n}\n","// src/core/config/config.ts\nexport type Config = {\n color: {\n background: string;\n bands: {\n silhouette: { fillEven: string; fillOdd: string; stroke: string };\n workspace: { fillEven: string; fillOdd: string; stroke: string };\n };\n completion: { fill: string; stroke: string };\n silhouetteMask: string;\n anchors: { invalid: string; valid: string };\n piece: { draggingFill: string; validFill: string; invalidFill: string; invalidStroke: string; selectedStroke: string; allGreenStroke: string; borderStroke: string };\n ui: { light: string; dark: string };\n blueprint: { fill: string; selectedStroke: string; badgeFill: string; labelFill: string };\n tangramDecomposition: { stroke: string };\n primitiveColors: string[];\n };\n opacity: {\n blueprint: number;\n silhouetteMask: number;\n anchors: { invalid: number; valid: number };\n piece: { invalid: number; dragging: number; locked: number; normal: number };\n };\n size: {\n stroke: { bandPx: number; pieceSelectedPx: number; allGreenStrokePx: number; pieceBorderPx: number; tangramDecompositionPx: number };\n anchorRadiusPx: { valid: number; invalid: number };\n badgeFontPx: number;\n centerBadge: { fractionOfOuterR: number; minPx: number; marginPx: number };\n invalidMarker: { sizePx: number; strokePx: number };\n };\n layout: {\n grid: { stepPx: number; unitPx: number };\n paddingPx: number;\n viewportScale: number;\n /** renamed from capacity → constraints */\n constraints: {\n workspaceDiamAnchors: number;\n quickstashDiamAnchors: number;\n primitiveDiamAnchors: number;\n };\n defaults: { maxQuickstashSlots: number };\n };\n game: {\n snapRadiusPx: number;\n showBorders: boolean;\n hideTouchingBorders: boolean;\n silhouettesBelowPieces: boolean;\n };\n};\n\nexport const CONFIG: Config = {\n color: {\n background: \"#fff7e0ff\",\n bands: {\n silhouette: { fillEven: \"#ffffff\", fillOdd: \"#ffffff\", stroke: \"#b1b1b1\" },\n workspace: { fillEven: \"#ffffff\", fillOdd: \"#ffffff\", stroke: \"#b1b1b1\" }\n },\n completion: { fill: \"#ccffcc\", stroke: \"#13da57\" },\n silhouetteMask: \"#374151\",\n anchors: { invalid: \"#7dd3fc\", valid: \"#475569\" },\n // validFill used here for placed composites\n piece: { draggingFill: \"#8e7cc3\", validFill: \"#8e7cc3\", invalidFill: \"#d55c00\", invalidStroke: \"#dc2626\", selectedStroke: \"#674ea7\", allGreenStroke: \"#86efac\", borderStroke: \"#674ea7\" },\n ui: { light: \"#60a5fa\", dark: \"#1d4ed8\" },\n blueprint: { fill: \"#374151\", selectedStroke: \"#111827\", badgeFill: \"#000000\", labelFill: \"#ffffff\" },\n tangramDecomposition: { stroke: \"#fef2cc\" },\n primitiveColors: [ // from seaborn \"colorblind\" palette, 6 colors, with red omitted\n '#0173b2',\n '#de8f05',\n '#029e73',\n '#cc78bc',\n '#ca9161'\n ]\n },\n opacity: {\n blueprint: 0.6,\n silhouetteMask: 0.25,\n //anchors: { valid: 0.80, invalid: 0.50 },\n anchors: { invalid: 0.0, valid: 0.0 },\n piece: { invalid: 1, dragging: 1, locked: 1, normal: 1 },\n },\n size: {\n stroke: { bandPx: 5, pieceSelectedPx: 5, allGreenStrokePx: 10, pieceBorderPx: 2, tangramDecompositionPx: 1 },\n anchorRadiusPx: { valid: 1.0, invalid: 1.0 },\n badgeFontPx: 16,\n centerBadge: { fractionOfOuterR: 0.15, minPx: 20, marginPx: 4 },\n invalidMarker: { sizePx: 10, strokePx: 4 }\n },\n layout: {\n grid: { stepPx: 20, unitPx: 40 },\n paddingPx: 1,\n viewportScale: 0.8,\n constraints: {\n workspaceDiamAnchors: 10, // num anchors req'd to be on diagonal\n quickstashDiamAnchors: 7, // num anchors req'd to be in single quickstash slot\n primitiveDiamAnchors: 5,\n },\n defaults: { maxQuickstashSlots: 1 }\n },\n game: {\n snapRadiusPx: 15,\n showBorders: false,\n hideTouchingBorders: true,\n silhouettesBelowPieces: true\n }\n};","/**\n * Game-specific piece geometry operations\n * Includes: piece positioning, placement, grid snapping, and anchor generation\n *\n * Consolidated from: piece.ts, placement.ts, grid.ts, anchors.ts\n */\n\nimport type { Blueprint, Vec, Poly, Anchor } from \"@/core/domain/types\";\nimport type { CircleLayout, SectorGeom } from \"@/core/domain/layout\";\nimport { polysAABB } from \"./bounds\";\nimport { pointInPolygon, dist2 } from \"./math\";\nimport { CONFIG } from \"@/core/config/config\";\n\nconst GRID_PX = CONFIG.layout.grid.stepPx;\n\n// ===== Piece Positioning (from piece.ts) =====\n\n/** Translate blueprint polys to world coords for a given top-left (TL). */\nexport function piecePolysAt(\n bp: Blueprint,\n bb: { min: { x: number; y: number } },\n tl: { x: number; y: number }\n) {\n const ox = tl.x - bb.min.x;\n const oy = tl.y - bb.min.y;\n const polys = (\"shape\" in bp && bp.shape) ? bp.shape : [];\n return polys.map(poly => poly.map(p => ({ x: p.x + ox, y: p.y + oy })));\n}\n\n/** Build support offsets: each vertex relative to the AABB center. */\nexport function computeSupportOffsets(\n bp: Blueprint,\n bb: { min: { x: number; y: number }; width: number; height: number }\n) {\n const cx = bb.min.x + bb.width / 2;\n const cy = bb.min.y + bb.height / 2;\n\n const polys = (\"shape\" in bp && bp.shape && bp.shape.length)\n ? bp.shape\n : [[\n { x: bb.min.x, y: bb.min.y },\n { x: bb.min.x + bb.width, y: bb.min.y },\n { x: bb.min.x + bb.width, y: bb.min.y + bb.height },\n { x: bb.min.x, y: bb.min.y + bb.height },\n ]];\n\n const offs: Vec[] = [];\n for (const poly of polys) for (const v of poly) offs.push({ x: v.x - cx, y: v.y - cy });\n return offs;\n}\n\n/** Clamp using directional vertex support → exact contact with the ring. */\nexport function clampTopLeftBySupport(\n tlx: number,\n tly: number,\n d: {\n aabb: { width: number; height: number };\n support: { x: number; y: number }[];\n },\n layout: CircleLayout,\n target: \"workspace\" | \"silhouette\",\n pointerInsideCenter: boolean\n) {\n const cx0 = tlx + d.aabb.width / 2;\n const cy0 = tly + d.aabb.height / 2;\n\n const vx = cx0 - layout.cx;\n const vy = cy0 - layout.cy;\n const r = Math.hypot(vx, vy);\n if (r === 0) return { x: tlx, y: tly };\n\n const ux = vx / r, uy = vy / r;\n\n let h_out = -Infinity, h_in = -Infinity;\n for (const o of d.support) {\n const outProj = o.x * ux + o.y * uy;\n if (outProj > h_out) h_out = outProj;\n const inProj = -(o.x * ux + o.y * uy);\n if (inProj > h_in) h_in = inProj;\n }\n\n if (target === \"workspace\") {\n const [rIn, rOut] = layout.bands.workspace;\n const minR = pointerInsideCenter ? 0 : (rIn + h_in);\n const maxR = rOut - h_out;\n const rClamped = Math.min(Math.max(r, minR), maxR);\n if (Math.abs(rClamped - r) < 1e-6) return { x: tlx, y: tly };\n const newCx = layout.cx + rClamped * ux;\n const newCy = layout.cy + rClamped * uy;\n return { x: newCx - d.aabb.width / 2, y: newCy - d.aabb.height / 2 };\n } else {\n const rOut = layout.bands.silhouette[1];\n const maxR = rOut - h_out;\n if (r <= maxR) return { x: tlx, y: tly };\n const newCx = layout.cx + maxR * ux;\n const newCy = layout.cy + maxR * uy;\n return { x: newCx - d.aabb.width / 2, y: newCy - d.aabb.height / 2 };\n }\n}\n\n// ===== Placement (from placement.ts) =====\n\n/** gcd for positive integers */\nfunction igcd(a: number, b: number) {\n a = Math.round(Math.abs(a)); b = Math.round(Math.abs(b));\n while (b) [a, b] = [b, a % b];\n return a || 1;\n}\n\n/** Infer the base lattice unit in raw silhouette coordinates. */\nexport function inferUnitFromPolys(polys: Poly[]): number {\n let g = 0;\n for (const poly of polys) {\n for (let i = 0; i < poly.length; i++) {\n const a = poly[i], b = poly[(i + 1) % poly.length];\n if (!a || !b) continue;\n const dx = Math.round(Math.abs(b.x - a.x));\n const dy = Math.round(Math.abs(b.y - a.y));\n if (dx) g = g ? igcd(g, dx) : dx;\n if (dy) g = g ? igcd(g, dy) : dy;\n }\n }\n return g || 1;\n}\n\n/**\n * Place polys using scale S, re-centered, and translation snapped to GRID_PX; returns polys.\n * The center is snapped to the grid so the lattice points under the silhouette stay invariant.\n */\nexport function placeSilhouetteGridAlignedAsPolys(\n polys: Poly[],\n S: number,\n rectCenter: { cx: number; cy: number }\n): Poly[] {\n if (!polys || polys.length === 0) return [];\n const a = polysAABB(polys);\n const cx0 = (a.min.x + a.max.x) / 2;\n const cy0 = (a.min.y + a.max.y) / 2;\n const cx = Math.round(rectCenter.cx / GRID_PX) * GRID_PX;\n const cy = Math.round(rectCenter.cy / GRID_PX) * GRID_PX;\n const tx = cx - S * cx0;\n const ty = cy - S * cy0;\n const stx = Math.round(tx / GRID_PX) * GRID_PX;\n const sty = Math.round(ty / GRID_PX) * GRID_PX;\n return polys.map(poly => poly.map(p => ({ x: S * p.x + stx, y: S * p.y + sty })));\n}\n\n// ===== Grid Operations (from grid.ts) =====\n\nexport function nearestGridNode(p: Vec): Vec {\n return { x: Math.round(p.x / GRID_PX) * GRID_PX, y: Math.round(p.y / GRID_PX) * GRID_PX };\n}\n\n/** Generate grid nodes inside an AABB. */\nexport function gridNodesInAABB(min: Vec, max: Vec): Vec[] {\n const out: Vec[] = [];\n const x0 = Math.ceil(min.x / GRID_PX) * GRID_PX;\n const y0 = Math.ceil(min.y / GRID_PX) * GRID_PX;\n for (let y = y0; y <= max.y; y += GRID_PX) {\n for (let x = x0; x <= max.x; x += GRID_PX) out.push({ x, y });\n }\n return out;\n}\n\n/** Keep nodes in a band (and optional sector wedge). */\nexport function filterNodesToBandAndSector(\n nodes: Vec[],\n layout: CircleLayout,\n band: \"workspace\" | \"silhouette\",\n sector?: SectorGeom\n): Vec[] {\n const [rIn, rOut] = layout.bands[band];\n const out: Vec[] = [];\n for (const n of nodes) {\n const dx = n.x - layout.cx, dy = n.y - layout.cy;\n const r = Math.hypot(dx, dy);\n if (r < rIn || r > rOut) continue;\n\n if (sector) {\n let theta = Math.atan2(dy, dx);\n if (layout.mode === \"circle\") {\n if (theta < -Math.PI / 2) theta += 2 * Math.PI;\n } else {\n if (theta < Math.PI) theta += 2 * Math.PI;\n }\n if (theta < sector.start || theta >= sector.end) continue;\n }\n out.push(n);\n }\n return out;\n}\n\n/** Keep nodes that lie inside any of the polygons. */\nexport function filterNodesInPolys(\n nodes: Vec[],\n polys: Poly[],\n pointInPolyFn: (pt: Vec, poly: Poly) => boolean = pointInPolygon\n): Vec[] {\n const out: Vec[] = [];\n node: for (const n of nodes) {\n for (const poly of polys) {\n if (pointInPolyFn(n, poly)) { out.push(n); continue node; }\n }\n }\n return out;\n}\n\n// ===== Anchor Operations (from anchors.ts) =====\n\n/**\n * Find the nearest anchor that \"accepts\" a piece signature (kind/compositeSignature).\n * Returns null if none within the optional radius.\n */\nexport function nearestAcceptingAnchor(\n anchors: Anchor[] | undefined,\n point: Vec,\n accepts: { kind?: string; compositeSignature?: string },\n withinPx: number = Infinity\n): { anchor: Anchor; d2: number } | null {\n if (!anchors || !anchors.length) return null;\n\n const { kind, compositeSignature } = accepts;\n const r2 = withinPx === Infinity ? Infinity : withinPx * withinPx;\n\n let best: { anchor: Anchor; d2: number } | null = null;\n for (const a of anchors) {\n const ok = a.accepts.some(acc =>\n (acc.kind === undefined || acc.kind === kind) &&\n (acc.compositeSignature === undefined || acc.compositeSignature === compositeSignature)\n );\n if (!ok) continue;\n\n const d2val = dist2(point, a.position);\n if (d2val <= r2 && (!best || d2val < best.d2)) {\n best = { anchor: a, d2: d2val };\n }\n }\n return best;\n}\n\n/** True if `point` is within `withinPx` of the given anchor. */\nexport function withinAnchor(anchor: Anchor, point: Vec, withinPx: number): boolean {\n const r2 = withinPx * withinPx;\n return dist2(point, anchor.position) <= r2;\n}\n\n// Node-grid helpers for anchor generation\nexport function workspaceNodes(layout: CircleLayout, sector: SectorGeom): Vec[] {\n const pad = 6;\n const min = { x: layout.cx - layout.outerR, y: layout.cy - layout.outerR - pad };\n const max = { x: layout.cx + layout.outerR, y: layout.cy + layout.outerR + pad };\n const nodes = gridNodesInAABB(min, max);\n return filterNodesToBandAndSector(nodes, layout, \"workspace\", sector);\n}\n\nexport function silhouetteNodes(layout: CircleLayout, sector: SectorGeom, fittedMask: Poly[]): Vec[] {\n const pad = 6;\n const min = { x: layout.cx - layout.outerR, y: layout.cy - layout.outerR - pad };\n const max = { x: layout.cx + layout.outerR, y: layout.cy + layout.outerR + pad };\n const nodes = gridNodesInAABB(min, max);\n const banded = filterNodesToBandAndSector(nodes, layout, \"silhouette\", sector);\n return filterNodesInPolys(banded, fittedMask);\n}\n\nexport function silhouetteBandNodes(layout: CircleLayout, sector: SectorGeom): Vec[] {\n const pad = 6;\n const min = { x: layout.cx - layout.outerR, y: layout.cy - layout.outerR - pad };\n const max = { x: layout.cx + layout.outerR, y: layout.cy + layout.outerR + pad };\n const nodes = gridNodesInAABB(min, max);\n return filterNodesToBandAndSector(nodes, layout, \"silhouette\", sector);\n}\n\n/** Generate anchor grid nodes for the inner ring (radius < innerR) */\nexport function innerRingNodes(layout: CircleLayout): Vec[] {\n const pad = 6;\n const min = { x: layout.cx - layout.innerR, y: layout.cy - layout.innerR - pad };\n const max = { x: layout.cx + layout.innerR, y: layout.cy + layout.innerR + pad };\n const nodes = gridNodesInAABB(min, max);\n // Filter to nodes within the inner circle\n const out: Vec[] = [];\n for (const n of nodes) {\n const dx = n.x - layout.cx, dy = n.y - layout.cy;\n const r = Math.hypot(dx, dy);\n if (r < layout.innerR) out.push(n);\n }\n return out;\n}\n\nexport const anchorsDiameterToPx = (anchorsDiag: number, gridPx: number = GRID_PX) =>\n anchorsDiag * Math.SQRT2 * gridPx;\n","/**\n * NBackApp.tsx - React wrapper for tangram n-back matching trials\n *\n * This component handles the React rendering logic for n-back trials,\n * displaying a single tangram silhouette with a response button.\n */\n\nimport React, { useRef, useEffect, useState } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport { JsPsych } from \"jspsych\";\nimport type { Poly, TanKind } from \"../../core/domain/types\";\nimport { placeSilhouetteGridAlignedAsPolys, inferUnitFromPolys } from \"../../core/engine/geometry\";\nimport { CONFIG } from \"../../core/config/config\";\n\nexport interface StartNBackTrialParams {\n tangram: any;\n isMatch?: boolean;\n show_tangram_decomposition?: boolean;\n instructions?: string;\n button_text?: string;\n duration: number;\n usePrimitiveColors?: boolean;\n primitiveColorIndices?: number[];\n onTrialEnd?: (data: any) => void;\n}\n\n/**\n * Start an n-back trial by rendering the NBackView component\n */\nexport function startNBackTrial(\n display_element: HTMLElement,\n params: StartNBackTrialParams,\n _jsPsych: JsPsych\n) {\n // Create React root and render NBackView\n const root = createRoot(display_element);\n root.render(React.createElement(NBackView, { params }));\n\n return { root, display_element, jsPsych: _jsPsych };\n}\n\ninterface NBackViewProps {\n params: StartNBackTrialParams;\n}\n\nfunction NBackView({ params }: NBackViewProps) {\n const {\n tangram,\n isMatch,\n show_tangram_decomposition,\n instructions,\n button_text,\n duration,\n usePrimitiveColors,\n primitiveColorIndices,\n onTrialEnd\n } = params;\n\n // Timing and response tracking\n const trialStartTime = useRef<number>(Date.now());\n const buttonEnabledRef = useRef<boolean>(true);\n const timeoutIdRef = useRef<NodeJS.Timeout | null>(null);\n const hasRespondedRef = useRef<boolean>(false);\n const responseDataRef = useRef<any>(null);\n const [buttonDisabled, setButtonDisabled] = useState(false);\n\n // Canonical piece names\n const CANON = new Set([\n \"square\",\n \"smalltriangle\",\n \"parallelogram\",\n \"medtriangle\",\n \"largetriangle\",\n ]);\n\n // Convert TangramSpec to internal format\n const filteredTans = tangram.solutionTans.filter((tan: any) => {\n const tanName = tan.name ?? tan.kind;\n return CANON.has(tanName);\n });\n\n const mask = filteredTans.map((tan: any) => {\n const polygon = tan.vertices.map(([x, y]: number[]) => ({ x: x ?? 0, y: -(y ?? 0) }));\n return polygon;\n });\n\n const primitiveDecomposition = filteredTans.map((tan: any) => ({\n kind: (tan.name ?? tan.kind) as TanKind,\n polygon: tan.vertices.map(([x, y]: number[]) => ({ x: x ?? 0, y: -(y ?? 0) }))\n }));\n\n // Use FIXED viewport size for n-back display (constant across all tangrams)\n const DISPLAY_SIZE = 400;\n const viewport = {\n w: DISPLAY_SIZE,\n h: DISPLAY_SIZE\n };\n\n // Compute scale factor to keep tangram pieces at constant physical size\n // This matches the approach in TangramConstructPlugin/GameBoard\n const scaleS = React.useMemo(() => {\n const u = inferUnitFromPolys(mask);\n return u ? (CONFIG.layout.grid.unitPx / u) : 1;\n }, [mask]);\n\n // For n-back display, we want tangrams centered in viewport\n // Don't use computeCircleLayout's positioning - just center manually\n const centerPos = {\n cx: viewport.w / 2,\n cy: viewport.h / 2\n };\n\n // Helper to convert polygon to SVG path\n const pathD = (poly: Poly): string => {\n if (!poly || poly.length === 0) return \"\";\n const moves = poly.map((p, i) => `${i === 0 ? \"M\" : \"L\"} ${p.x} ${p.y}`);\n return moves.join(\" \") + \" Z\";\n };\n\n // End trial with data\n const endTrial = (data: {\n responded_match: boolean;\n rt: number;\n responded_after_duration: boolean;\n rt_after_duration: number;\n }) => {\n // Clear timeout if it exists\n if (timeoutIdRef.current) {\n clearTimeout(timeoutIdRef.current);\n timeoutIdRef.current = null;\n }\n\n // Compute accuracy\n const accuracy = isMatch !== undefined\n ? (isMatch === data.responded_match ? 1 : 0)\n : NaN;\n\n const trialData = {\n ...data,\n accuracy,\n tangram_id: tangram.tangramID,\n is_match: isMatch,\n show_tangram_decomposition: show_tangram_decomposition,\n use_primitive_colors: usePrimitiveColors,\n primitive_color_indices: primitiveColorIndices,\n duration: duration,\n button_text: button_text\n };\n\n if (onTrialEnd) {\n onTrialEnd(trialData);\n }\n };\n\n // Handle button click\n const handleButtonClick = () => {\n if (!buttonEnabledRef.current) {\n // Late response after duration expired\n const rt_late = Date.now() - trialStartTime.current;\n hasRespondedRef.current = true;\n responseDataRef.current = {\n responded_match: true,\n rt: NaN,\n responded_after_duration: true,\n rt_after_duration: rt_late\n };\n endTrial(responseDataRef.current);\n } else {\n // Response before duration expired - disable button but continue showing trial\n const rt = Date.now() - trialStartTime.current;\n buttonEnabledRef.current = false;\n setButtonDisabled(true);\n hasRespondedRef.current = true;\n responseDataRef.current = {\n responded_match: true,\n rt,\n responded_after_duration: false,\n rt_after_duration: NaN\n };\n // Don't end trial yet - wait for duration timeout\n }\n };\n\n // Set up duration timeout\n useEffect(() => {\n timeoutIdRef.current = setTimeout(() => {\n buttonEnabledRef.current = false;\n\n // End trial with appropriate data\n if (hasRespondedRef.current && responseDataRef.current) {\n // User already responded - use saved response data\n endTrial(responseDataRef.current);\n } else {\n // No response - end with no-response data\n endTrial({\n responded_match: false,\n rt: NaN,\n responded_after_duration: false,\n rt_after_duration: NaN\n });\n }\n }, duration);\n\n return () => {\n if (timeoutIdRef.current) {\n clearTimeout(timeoutIdRef.current);\n }\n };\n }, []);\n\n // Render silhouette\n const renderSilhouette = () => {\n if (show_tangram_decomposition) {\n // Render decomposed primitives with borders\n const rawPolys = primitiveDecomposition.map((primInfo: any) => primInfo.polygon);\n const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, centerPos);\n\n return (\n <g key=\"sil-decomposed\" pointerEvents=\"none\">\n {placedPolys.map((scaledPoly, i) => {\n // Get color for this primitive based on its kind\n const primInfo = primitiveDecomposition[i];\n let fillColor = CONFIG.color.silhouetteMask;\n\n if (usePrimitiveColors && primInfo?.kind && primitiveColorIndices) {\n // Map primitive kind to index\n const kindToIndex: Record<TanKind, number> = {\n 'square': 0,\n 'smalltriangle': 1,\n 'parallelogram': 2,\n 'medtriangle': 3,\n 'largetriangle': 4\n };\n const primitiveIndex = kindToIndex[primInfo.kind as TanKind];\n if (primitiveIndex !== undefined && primitiveColorIndices[primitiveIndex] !== undefined) {\n const colorIndex = primitiveColorIndices[primitiveIndex];\n const color = CONFIG.color.primitiveColors[colorIndex];\n if (color) {\n fillColor = color;\n }\n }\n }\n\n return (\n <React.Fragment key={`prim-${i}`}>\n {/* Fill path */}\n <path\n d={pathD(scaledPoly)}\n fill={fillColor}\n opacity={CONFIG.opacity.silhouetteMask}\n stroke=\"none\"\n />\n\n {/* Full perimeter border */}\n <path\n d={pathD(scaledPoly)}\n fill=\"none\"\n stroke={CONFIG.color.tangramDecomposition.stroke}\n strokeWidth={CONFIG.size.stroke.tangramDecompositionPx}\n />\n </React.Fragment>\n );\n })}\n </g>\n );\n } else {\n // Render unified silhouette\n const placedPolys = placeSilhouetteGridAlignedAsPolys(mask, scaleS, centerPos);\n\n return (\n <g key=\"sil-unified\" pointerEvents=\"none\">\n {placedPolys.map((scaledPoly, i) => {\n // Get color for this primitive based on its kind\n const primInfo = primitiveDecomposition[i];\n let fillColor = CONFIG.color.silhouetteMask;\n\n if (usePrimitiveColors && primInfo?.kind && primitiveColorIndices) {\n // Map primitive kind to index\n const kindToIndex: Record<TanKind, number> = {\n 'square': 0,\n 'smalltriangle': 1,\n 'parallelogram': 2,\n 'medtriangle': 3,\n 'largetriangle': 4\n };\n const primitiveIndex = kindToIndex[primInfo.kind as TanKind];\n if (primitiveIndex !== undefined && primitiveColorIndices[primitiveIndex] !== undefined) {\n const colorIndex = primitiveColorIndices[primitiveIndex];\n const color = CONFIG.color.primitiveColors[colorIndex];\n if (color) {\n fillColor = color;\n }\n }\n }\n\n return (\n <path\n key={`sil-${i}`}\n d={pathD(scaledPoly)}\n fill={fillColor}\n opacity={CONFIG.opacity.silhouetteMask}\n stroke=\"none\"\n />\n );\n })}\n </g>\n );\n }\n };\n\n return (\n <div style={{\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n padding: \"20px\",\n background: \"#f5f5f5\"\n }}>\n {/* Instructions */}\n {instructions && (\n <div\n style={{\n maxWidth: \"800px\",\n width: \"100%\",\n marginBottom: \"30px\",\n textAlign: \"center\",\n fontSize: \"18px\",\n lineHeight: \"1.5\"\n }}\n dangerouslySetInnerHTML={{ __html: instructions }}\n />\n )}\n\n {/* Container for SVG and button */}\n <div style={{\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n gap: \"30px\"\n }}>\n {/* SVG with tangram silhouette */}\n <svg\n width={viewport.w}\n height={viewport.h}\n viewBox={`0 0 ${viewport.w} ${viewport.h}`}\n style={{\n display: \"block\",\n background: CONFIG.color.bands.silhouette.fillEven,\n border: `${CONFIG.size.stroke.bandPx}px solid ${CONFIG.color.bands.silhouette.stroke}`,\n borderRadius: \"8px\"\n }}\n >\n {renderSilhouette()}\n </svg>\n\n {/* Response button */}\n <button\n className=\"jspsych-btn\"\n onClick={handleButtonClick}\n disabled={buttonDisabled}\n style={{\n padding: \"12px 30px\",\n fontSize: \"16px\",\n cursor: buttonDisabled ? \"not-allowed\" : \"pointer\",\n opacity: buttonDisabled ? 0.5 : 1\n }}\n >\n {button_text}\n </button>\n </div>\n </div>\n );\n}\n","import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from \"jspsych\";\nimport { startNBackTrial, StartNBackTrialParams } from \"./NBackApp\";\n\nconst info = {\n name: \"tangram-nback\",\n version: \"1.0.0\",\n parameters: {\n /** Single tangram specification to display */\n tangram: {\n type: ParameterType.COMPLEX,\n default: undefined,\n description: \"TangramSpec object defining target shape to display\"\n },\n /** Whether this trial is a match (for computing accuracy) */\n isMatch: {\n type: ParameterType.BOOL,\n default: undefined,\n description: \"Whether this tangram matches the previous one (optional)\"\n },\n /** Whether to show tangram decomposed into individual primitives with borders */\n show_tangram_decomposition: {\n type: ParameterType.BOOL,\n default: false,\n description: \"Whether to show tangram decomposed into individual primitives with borders\"\n },\n /** HTML content to display above the tangram as instructions */\n instructions: {\n type: ParameterType.STRING,\n default: \"\",\n description: \"HTML content to display above the tangram as instructions\"\n },\n /** Text to display on response button */\n button_text: {\n type: ParameterType.STRING,\n default: \"Same as previous!\",\n description: \"Text to display on response button\"\n },\n /** Duration to display tangram and accept responses (milliseconds) */\n duration: {\n type: ParameterType.INT,\n default: 3000,\n description: \"Duration in milliseconds to display tangram and accept responses\"\n },\n /** Whether to use distinct colors for each primitive shape type */\n use_primitive_colors: {\n type: ParameterType.BOOL,\n default: false,\n description: \"Whether each primitive shape type should have its own distinct color in the displayed tangram\"\n },\n /** Indices mapping primitives to colors from the color palette */\n primitive_color_indices: {\n type: ParameterType.OBJECT,\n default: [0, 1, 2, 3, 4],\n description: \"Array of 5 integers indexing into primitiveColors array, mapping [square, smalltriangle, parallelogram, medtriangle, largetriangle] to colors\"\n },\n /** Callback fired when trial ends */\n onTrialEnd: {\n type: ParameterType.FUNCTION,\n default: undefined,\n description: \"Callback when trial completes with full data\"\n }\n },\n data: {\n /** Whether participant clicked the response button before duration expired */\n responded_match: {\n type: ParameterType.BOOL,\n description: \"True if participant clicked response button, false otherwise\"\n },\n /** Reaction time in milliseconds (NaN if no response or response after duration) */\n rt: {\n type: ParameterType.INT,\n description: \"Milliseconds between trial start and button click (NaN if no response or late response)\"\n },\n /** Accuracy: 1 if correct, 0 if incorrect, NaN if isMatch not provided */\n accuracy: {\n type: ParameterType.FLOAT,\n description: \"1 if response matches isMatch parameter, 0 otherwise (NaN if isMatch not provided)\"\n },\n /** Whether response occurred after duration expired */\n responded_after_duration: {\n type: ParameterType.BOOL,\n description: \"True if button clicked after duration expired, false otherwise\"\n },\n /** Time of late response (NaN if no late response) */\n rt_after_duration: {\n type: ParameterType.INT,\n description: \"Milliseconds between trial start and late button click (NaN if no late response)\"\n }\n },\n citations: \"\"\n};\n\ntype Info = typeof info;\n\n/**\n * **tangram-nback**\n *\n * A jsPsych plugin for n-back matching trials displaying a single tangram\n * with a response button.\n *\n * @author Justin Yang & Sean Paul Anderson\n * @see {@link https://github.com/cogtoolslab/tangram_construction.git/tree/main/experiments/jspsych-tangram-prep}\n */\nclass TangramNBackPlugin implements JsPsychPlugin<Info> {\n static info = info;\n\n constructor(private jsPsych: JsPsych) {}\n\n /**\n * Launches the trial by invoking startNBackTrial\n * with the display element, parameters, and jsPsych instance.\n */\n trial(display_element: HTMLElement, trial: TrialType<Info>) {\n // Wrap onTrialEnd to handle React cleanup and jsPsych trial completion\n const wrappedOnTrialEnd = (data: any) => {\n // Call user-provided callback if exists\n if (trial.onTrialEnd) {\n trial.onTrialEnd(data);\n }\n\n // Clean up React first (before clearing DOM)\n const reactContext = (display_element as any).__reactContext;\n if (reactContext?.root) {\n reactContext.root.unmount();\n }\n\n // Clear display after React cleanup\n display_element.innerHTML = '';\n\n // Finish jsPsych trial with data\n this.jsPsych.finishTrial(data);\n };\n\n // Create parameter object for wrapper\n const params: StartNBackTrialParams = {\n tangram: trial.tangram,\n isMatch: trial.isMatch,\n show_tangram_decomposition: trial.show_tangram_decomposition,\n instructions: trial.instructions,\n button_text: trial.button_text,\n duration: trial.duration,\n usePrimitiveColors: trial.use_primitive_colors,\n primitiveColorIndices: trial.primitive_color_indices,\n onTrialEnd: wrappedOnTrialEnd\n };\n\n // Use React wrapper to start the trial\n const { root, display_element: element, jsPsych } = startNBackTrial(display_element, params, this.jsPsych);\n\n // Store React context for cleanup\n (element as any).__reactContext = { root, jsPsych };\n }\n}\n\nexport default TangramNBackPlugin;\n"],"names":["createRoot","useRef","useState","useEffect","ParameterType"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../../src/core/engine/geometry/bounds.ts","../../src/core/config/config.ts","../../src/core/engine/geometry/pieces.ts","../../src/plugins/tangram-nback/NBackApp.tsx","../../src/plugins/tangram-nback/index.ts"],"sourcesContent":["import type { Poly, Vec, PrimitiveBlueprint, CompositeBlueprint, Blueprint } from \"@/core/domain/types\";\n\nexport type AABB = { min: Vec; max: Vec; width: number; height: number };\n\n/** AABB of a single polygon (numeric). */\nexport function aabbOf(poly: Poly): { minX: number; minY: number; maxX: number; maxY: number } {\n let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\n for (const p of poly) {\n if (p.x < minX) minX = p.x;\n if (p.y < minY) minY = p.y;\n if (p.x > maxX) maxX = p.x;\n if (p.y > maxY) maxY = p.y;\n }\n return { minX, minY, maxX, maxY };\n}\n\n/** AABB of one or more polygons with width/height and center. */\nexport function polysAABB(polys: Poly[]) {\n let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\n for (const poly of polys) {\n for (const p of poly) {\n if (p.x < minX) minX = p.x;\n if (p.y < minY) minY = p.y;\n if (p.x > maxX) maxX = p.x;\n if (p.y > maxY) maxY = p.y;\n }\n }\n const width = Math.max(1, maxX - minX);\n const height = Math.max(1, maxY - minY);\n return { min: { x: minX, y: minY }, max: { x: maxX, y: maxY }, width, height, cx:(minX+maxX)/2, cy:(minY+maxY)/2 };\n}\n\n/** Bounds of multiple polygons at the origin. */\nexport function boundsOfShapes(shapes: Poly[]): AABB {\n let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\n for (const poly of shapes) {\n for (const p of poly) {\n if (p.x < minX) minX = p.x;\n if (p.y < minY) minY = p.y;\n if (p.x > maxX) maxX = p.x;\n if (p.y > maxY) maxY = p.y;\n }\n }\n const width = Math.max(0, maxX - minX);\n const height = Math.max(0, maxY - minY);\n return { min: { x: minX, y: minY }, max: { x: maxX, y: maxY }, width, height };\n}\n\n/** Bounds of a primitive blueprint (uses its canonical shape). */\nexport function boundsOfPrimitive(bp: PrimitiveBlueprint): AABB {\n return boundsOfShapes(bp.shape);\n}\n\n/**\n * Bounds of a composite blueprint.\n * If `shape` is provided as precomputed union polygons, we use that.\n * Otherwise we bound the parts’ primitives at their offsets (loose but safe).\n */\nexport function boundsOfComposite(\n bp: CompositeBlueprint,\n primitiveLookup: (kind: string) => PrimitiveBlueprint | undefined\n): AABB {\n if (bp.shape && bp.shape.length) return boundsOfShapes(bp.shape);\n\n let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\n for (const part of bp.parts) {\n const prim = primitiveLookup(part.kind);\n if (!prim) continue;\n const b = boundsOfPrimitive(prim);\n const pMinX = part.offset.x + b.min.x;\n const pMinY = part.offset.y + b.min.y;\n const pMaxX = part.offset.x + b.max.x;\n const pMaxY = part.offset.y + b.max.y;\n if (pMinX < minX) minX = pMinX;\n if (pMinY < minY) minY = pMinY;\n if (pMaxX > maxX) maxX = pMaxX;\n if (pMaxY > maxY) maxY = pMaxY;\n }\n const width = Math.max(0, maxX - minX);\n const height = Math.max(0, maxY - minY);\n return { min: { x: minX, y: minY }, max: { x: maxX, y: maxY }, width, height };\n}\n\n/** Unified bounds helper. */\nexport function boundsOfBlueprint(\n bp: Blueprint,\n primitiveLookup: (kind: string) => PrimitiveBlueprint | undefined\n): AABB {\n if (\"parts\" in bp) return boundsOfComposite(bp, primitiveLookup);\n return boundsOfPrimitive(bp);\n}\n","// src/core/config/config.ts\nexport type Config = {\n color: {\n background: string;\n bands: {\n silhouette: { fillEven: string; fillOdd: string; stroke: string };\n workspace: { fillEven: string; fillOdd: string; stroke: string };\n };\n completion: { fill: string; stroke: string };\n silhouetteMask: string;\n anchors: { invalid: string; valid: string };\n piece: { draggingFill: string; validFill: string; invalidFill: string; invalidStroke: string; selectedStroke: string; allGreenStroke: string; borderStroke: string };\n ui: { light: string; dark: string };\n blueprint: { fill: string; selectedStroke: string; badgeFill: string; labelFill: string };\n tangramDecomposition: { stroke: string };\n primitiveColors: string[];\n };\n opacity: {\n blueprint: number;\n silhouetteMask: number;\n anchors: { invalid: number; valid: number };\n piece: { invalid: number; dragging: number; locked: number; normal: number };\n };\n size: {\n stroke: { bandPx: number; pieceSelectedPx: number; allGreenStrokePx: number; pieceBorderPx: number; tangramDecompositionPx: number };\n anchorRadiusPx: { valid: number; invalid: number };\n badgeFontPx: number;\n centerBadge: { fractionOfOuterR: number; minPx: number; marginPx: number };\n invalidMarker: { sizePx: number; strokePx: number };\n };\n layout: {\n grid: { stepPx: number; unitPx: number };\n paddingPx: number;\n viewportScale: number;\n /** renamed from capacity → constraints */\n constraints: {\n workspaceDiamAnchors: number;\n quickstashDiamAnchors: number;\n primitiveDiamAnchors: number;\n };\n defaults: { maxQuickstashSlots: number };\n };\n game: {\n snapRadiusPx: number;\n showBorders: boolean;\n hideTouchingBorders: boolean;\n silhouettesBelowPieces: boolean;\n };\n};\n\nexport const CONFIG: Config = {\n color: {\n background: \"#fff7e0ff\",\n bands: {\n silhouette: { fillEven: \"#ffffff\", fillOdd: \"#ffffff\", stroke: \"#b1b1b1\" },\n workspace: { fillEven: \"#ffffff\", fillOdd: \"#ffffff\", stroke: \"#b1b1b1\" }\n },\n completion: { fill: \"#ccffcc\", stroke: \"#13da57\" },\n silhouetteMask: \"#374151\",\n anchors: { invalid: \"#7dd3fc\", valid: \"#475569\" },\n // validFill used here for placed composites\n piece: { draggingFill: \"#8e7cc3\", validFill: \"#8e7cc3\", invalidFill: \"#d55c00\", invalidStroke: \"#dc2626\", selectedStroke: \"#674ea7\", allGreenStroke: \"#86efac\", borderStroke: \"#674ea7\" },\n ui: { light: \"#60a5fa\", dark: \"#1d4ed8\" },\n blueprint: { fill: \"#374151\", selectedStroke: \"#111827\", badgeFill: \"#000000\", labelFill: \"#ffffff\" },\n tangramDecomposition: { stroke: \"#fef2cc\" },\n primitiveColors: [ // from seaborn \"colorblind\" palette, 6 colors, with red omitted\n '#0173b2',\n '#de8f05',\n '#029e73',\n '#cc78bc',\n '#ca9161'\n ]\n },\n opacity: {\n blueprint: 0.6,\n silhouetteMask: 0.25,\n //anchors: { valid: 0.80, invalid: 0.50 },\n anchors: { invalid: 0.0, valid: 0.0 },\n piece: { invalid: 1, dragging: 1, locked: 1, normal: 1 },\n },\n size: {\n stroke: { bandPx: 5, pieceSelectedPx: 5, allGreenStrokePx: 10, pieceBorderPx: 2, tangramDecompositionPx: 1 },\n anchorRadiusPx: { valid: 1.0, invalid: 1.0 },\n badgeFontPx: 16,\n centerBadge: { fractionOfOuterR: 0.15, minPx: 20, marginPx: 4 },\n invalidMarker: { sizePx: 10, strokePx: 4 }\n },\n layout: {\n grid: { stepPx: 20, unitPx: 40 },\n paddingPx: 1,\n viewportScale: 0.8,\n constraints: {\n workspaceDiamAnchors: 10, // num anchors req'd to be on diagonal\n quickstashDiamAnchors: 7, // num anchors req'd to be in single quickstash slot\n primitiveDiamAnchors: 5,\n },\n defaults: { maxQuickstashSlots: 1 }\n },\n game: {\n snapRadiusPx: 15,\n showBorders: false,\n hideTouchingBorders: true,\n silhouettesBelowPieces: true\n }\n};","/**\n * Game-specific piece geometry operations\n * Includes: piece positioning, placement, grid snapping, and anchor generation\n *\n * Consolidated from: piece.ts, placement.ts, grid.ts, anchors.ts\n */\n\nimport type { Blueprint, Vec, Poly, Anchor } from \"@/core/domain/types\";\nimport type { CircleLayout, SectorGeom } from \"@/core/domain/layout\";\nimport { polysAABB } from \"./bounds\";\nimport { pointInPolygon, dist2 } from \"./math\";\nimport { CONFIG } from \"@/core/config/config\";\n\nconst GRID_PX = CONFIG.layout.grid.stepPx;\n\n// ===== Piece Positioning (from piece.ts) =====\n\n/** Translate blueprint polys to world coords for a given top-left (TL). */\nexport function piecePolysAt(\n bp: Blueprint,\n bb: { min: { x: number; y: number } },\n tl: { x: number; y: number }\n) {\n const ox = tl.x - bb.min.x;\n const oy = tl.y - bb.min.y;\n const polys = (\"shape\" in bp && bp.shape) ? bp.shape : [];\n return polys.map(poly => poly.map(p => ({ x: p.x + ox, y: p.y + oy })));\n}\n\n/** Build support offsets: each vertex relative to the AABB center. */\nexport function computeSupportOffsets(\n bp: Blueprint,\n bb: { min: { x: number; y: number }; width: number; height: number }\n) {\n const cx = bb.min.x + bb.width / 2;\n const cy = bb.min.y + bb.height / 2;\n\n const polys = (\"shape\" in bp && bp.shape && bp.shape.length)\n ? bp.shape\n : [[\n { x: bb.min.x, y: bb.min.y },\n { x: bb.min.x + bb.width, y: bb.min.y },\n { x: bb.min.x + bb.width, y: bb.min.y + bb.height },\n { x: bb.min.x, y: bb.min.y + bb.height },\n ]];\n\n const offs: Vec[] = [];\n for (const poly of polys) for (const v of poly) offs.push({ x: v.x - cx, y: v.y - cy });\n return offs;\n}\n\n/** Clamp using directional vertex support → exact contact with the ring. */\nexport function clampTopLeftBySupport(\n tlx: number,\n tly: number,\n d: {\n aabb: { width: number; height: number };\n support: { x: number; y: number }[];\n },\n layout: CircleLayout,\n target: \"workspace\" | \"silhouette\",\n pointerInsideCenter: boolean\n) {\n const cx0 = tlx + d.aabb.width / 2;\n const cy0 = tly + d.aabb.height / 2;\n\n const vx = cx0 - layout.cx;\n const vy = cy0 - layout.cy;\n const r = Math.hypot(vx, vy);\n if (r === 0) return { x: tlx, y: tly };\n\n const ux = vx / r, uy = vy / r;\n\n let h_out = -Infinity, h_in = -Infinity;\n for (const o of d.support) {\n const outProj = o.x * ux + o.y * uy;\n if (outProj > h_out) h_out = outProj;\n const inProj = -(o.x * ux + o.y * uy);\n if (inProj > h_in) h_in = inProj;\n }\n\n if (target === \"workspace\") {\n const [rIn, rOut] = layout.bands.workspace;\n const minR = pointerInsideCenter ? 0 : (rIn + h_in);\n const maxR = rOut - h_out;\n const rClamped = Math.min(Math.max(r, minR), maxR);\n if (Math.abs(rClamped - r) < 1e-6) return { x: tlx, y: tly };\n const newCx = layout.cx + rClamped * ux;\n const newCy = layout.cy + rClamped * uy;\n return { x: newCx - d.aabb.width / 2, y: newCy - d.aabb.height / 2 };\n } else {\n const rOut = layout.bands.silhouette[1];\n const maxR = rOut - h_out;\n if (r <= maxR) return { x: tlx, y: tly };\n const newCx = layout.cx + maxR * ux;\n const newCy = layout.cy + maxR * uy;\n return { x: newCx - d.aabb.width / 2, y: newCy - d.aabb.height / 2 };\n }\n}\n\n// ===== Placement (from placement.ts) =====\n\n/** gcd for positive integers */\nfunction igcd(a: number, b: number) {\n a = Math.round(Math.abs(a)); b = Math.round(Math.abs(b));\n while (b) [a, b] = [b, a % b];\n return a || 1;\n}\n\n/** Infer the base lattice unit in raw silhouette coordinates. */\nexport function inferUnitFromPolys(polys: Poly[]): number {\n let g = 0;\n for (const poly of polys) {\n for (let i = 0; i < poly.length; i++) {\n const a = poly[i], b = poly[(i + 1) % poly.length];\n if (!a || !b) continue;\n const dx = Math.round(Math.abs(b.x - a.x));\n const dy = Math.round(Math.abs(b.y - a.y));\n if (dx) g = g ? igcd(g, dx) : dx;\n if (dy) g = g ? igcd(g, dy) : dy;\n }\n }\n return g || 1;\n}\n\n/**\n * Place polys using scale S, re-centered, and translation snapped to GRID_PX; returns polys.\n * The center is snapped to the grid so the lattice points under the silhouette stay invariant.\n */\nexport function placeSilhouetteGridAlignedAsPolys(\n polys: Poly[],\n S: number,\n rectCenter: { cx: number; cy: number }\n): Poly[] {\n if (!polys || polys.length === 0) return [];\n const a = polysAABB(polys);\n const cx0 = (a.min.x + a.max.x) / 2;\n const cy0 = (a.min.y + a.max.y) / 2;\n const cx = Math.round(rectCenter.cx / GRID_PX) * GRID_PX;\n const cy = Math.round(rectCenter.cy / GRID_PX) * GRID_PX;\n const tx = cx - S * cx0;\n const ty = cy - S * cy0;\n const stx = Math.round(tx / GRID_PX) * GRID_PX;\n const sty = Math.round(ty / GRID_PX) * GRID_PX;\n return polys.map(poly => poly.map(p => ({ x: S * p.x + stx, y: S * p.y + sty })));\n}\n\n// ===== Grid Operations (from grid.ts) =====\n\nexport function nearestGridNode(p: Vec): Vec {\n return { x: Math.round(p.x / GRID_PX) * GRID_PX, y: Math.round(p.y / GRID_PX) * GRID_PX };\n}\n\n/** Generate grid nodes inside an AABB. */\nexport function gridNodesInAABB(min: Vec, max: Vec): Vec[] {\n const out: Vec[] = [];\n const x0 = Math.ceil(min.x / GRID_PX) * GRID_PX;\n const y0 = Math.ceil(min.y / GRID_PX) * GRID_PX;\n for (let y = y0; y <= max.y; y += GRID_PX) {\n for (let x = x0; x <= max.x; x += GRID_PX) out.push({ x, y });\n }\n return out;\n}\n\n/** Keep nodes in a band (and optional sector wedge). */\nexport function filterNodesToBandAndSector(\n nodes: Vec[],\n layout: CircleLayout,\n band: \"workspace\" | \"silhouette\",\n sector?: SectorGeom\n): Vec[] {\n const [rIn, rOut] = layout.bands[band];\n const out: Vec[] = [];\n for (const n of nodes) {\n const dx = n.x - layout.cx, dy = n.y - layout.cy;\n const r = Math.hypot(dx, dy);\n if (r < rIn || r > rOut) continue;\n\n if (sector) {\n let theta = Math.atan2(dy, dx);\n if (layout.mode === \"circle\") {\n if (theta < -Math.PI / 2) theta += 2 * Math.PI;\n } else {\n if (theta < Math.PI) theta += 2 * Math.PI;\n }\n if (theta < sector.start || theta >= sector.end) continue;\n }\n out.push(n);\n }\n return out;\n}\n\n/** Keep nodes that lie inside any of the polygons. */\nexport function filterNodesInPolys(\n nodes: Vec[],\n polys: Poly[],\n pointInPolyFn: (pt: Vec, poly: Poly) => boolean = pointInPolygon\n): Vec[] {\n const out: Vec[] = [];\n node: for (const n of nodes) {\n for (const poly of polys) {\n if (pointInPolyFn(n, poly)) { out.push(n); continue node; }\n }\n }\n return out;\n}\n\n// ===== Anchor Operations (from anchors.ts) =====\n\n/**\n * Find the nearest anchor that \"accepts\" a piece signature (kind/compositeSignature).\n * Returns null if none within the optional radius.\n */\nexport function nearestAcceptingAnchor(\n anchors: Anchor[] | undefined,\n point: Vec,\n accepts: { kind?: string; compositeSignature?: string },\n withinPx: number = Infinity\n): { anchor: Anchor; d2: number } | null {\n if (!anchors || !anchors.length) return null;\n\n const { kind, compositeSignature } = accepts;\n const r2 = withinPx === Infinity ? Infinity : withinPx * withinPx;\n\n let best: { anchor: Anchor; d2: number } | null = null;\n for (const a of anchors) {\n const ok = a.accepts.some(acc =>\n (acc.kind === undefined || acc.kind === kind) &&\n (acc.compositeSignature === undefined || acc.compositeSignature === compositeSignature)\n );\n if (!ok) continue;\n\n const d2val = dist2(point, a.position);\n if (d2val <= r2 && (!best || d2val < best.d2)) {\n best = { anchor: a, d2: d2val };\n }\n }\n return best;\n}\n\n/** True if `point` is within `withinPx` of the given anchor. */\nexport function withinAnchor(anchor: Anchor, point: Vec, withinPx: number): boolean {\n const r2 = withinPx * withinPx;\n return dist2(point, anchor.position) <= r2;\n}\n\n// Node-grid helpers for anchor generation\nexport function workspaceNodes(layout: CircleLayout, sector: SectorGeom): Vec[] {\n const pad = 6;\n const min = { x: layout.cx - layout.outerR, y: layout.cy - layout.outerR - pad };\n const max = { x: layout.cx + layout.outerR, y: layout.cy + layout.outerR + pad };\n const nodes = gridNodesInAABB(min, max);\n return filterNodesToBandAndSector(nodes, layout, \"workspace\", sector);\n}\n\nexport function silhouetteNodes(layout: CircleLayout, sector: SectorGeom, fittedMask: Poly[]): Vec[] {\n const pad = 6;\n const min = { x: layout.cx - layout.outerR, y: layout.cy - layout.outerR - pad };\n const max = { x: layout.cx + layout.outerR, y: layout.cy + layout.outerR + pad };\n const nodes = gridNodesInAABB(min, max);\n const banded = filterNodesToBandAndSector(nodes, layout, \"silhouette\", sector);\n return filterNodesInPolys(banded, fittedMask);\n}\n\nexport function silhouetteBandNodes(layout: CircleLayout, sector: SectorGeom): Vec[] {\n const pad = 6;\n const min = { x: layout.cx - layout.outerR, y: layout.cy - layout.outerR - pad };\n const max = { x: layout.cx + layout.outerR, y: layout.cy + layout.outerR + pad };\n const nodes = gridNodesInAABB(min, max);\n return filterNodesToBandAndSector(nodes, layout, \"silhouette\", sector);\n}\n\n/** Generate anchor grid nodes for the inner ring (radius < innerR) */\nexport function innerRingNodes(layout: CircleLayout): Vec[] {\n const pad = 6;\n const min = { x: layout.cx - layout.innerR, y: layout.cy - layout.innerR - pad };\n const max = { x: layout.cx + layout.innerR, y: layout.cy + layout.innerR + pad };\n const nodes = gridNodesInAABB(min, max);\n // Filter to nodes within the inner circle\n const out: Vec[] = [];\n for (const n of nodes) {\n const dx = n.x - layout.cx, dy = n.y - layout.cy;\n const r = Math.hypot(dx, dy);\n if (r < layout.innerR) out.push(n);\n }\n return out;\n}\n\nexport const anchorsDiameterToPx = (anchorsDiag: number, gridPx: number = GRID_PX) =>\n anchorsDiag * Math.SQRT2 * gridPx;\n","/**\n * NBackApp.tsx - React wrapper for tangram n-back matching trials\n *\n * This component handles the React rendering logic for n-back trials,\n * displaying a single tangram silhouette with a response button.\n */\n\nimport React, { useRef, useEffect, useState } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport { JsPsych } from \"jspsych\";\nimport type { Poly, TanKind } from \"../../core/domain/types\";\nimport { placeSilhouetteGridAlignedAsPolys, inferUnitFromPolys } from \"../../core/engine/geometry\";\nimport { CONFIG } from \"../../core/config/config\";\n\nexport interface StartNBackTrialParams {\n tangram: any;\n isMatch?: boolean;\n show_tangram_decomposition?: boolean;\n instructions?: string;\n button_text?: string;\n duration: number;\n usePrimitiveColors?: boolean;\n primitiveColorIndices?: number[];\n onTrialEnd?: (data: any) => void;\n}\n\n/**\n * Start an n-back trial by rendering the NBackView component\n */\nexport function startNBackTrial(\n display_element: HTMLElement,\n params: StartNBackTrialParams,\n _jsPsych: JsPsych\n) {\n // Create React root and render NBackView\n const root = createRoot(display_element);\n root.render(React.createElement(NBackView, { params }));\n\n return { root, display_element, jsPsych: _jsPsych };\n}\n\ninterface NBackViewProps {\n params: StartNBackTrialParams;\n}\n\nfunction NBackView({ params }: NBackViewProps) {\n const {\n tangram,\n isMatch,\n show_tangram_decomposition,\n instructions,\n button_text,\n duration,\n usePrimitiveColors,\n primitiveColorIndices,\n onTrialEnd\n } = params;\n\n // Timing and response tracking\n const trialStartTime = useRef<number>(Date.now());\n const buttonEnabledRef = useRef<boolean>(true);\n const timeoutIdRef = useRef<NodeJS.Timeout | null>(null);\n const hasRespondedRef = useRef<boolean>(false);\n const responseDataRef = useRef<any>(null);\n const [buttonDisabled, setButtonDisabled] = useState(false);\n\n // Canonical piece names\n const CANON = new Set([\n \"square\",\n \"smalltriangle\",\n \"parallelogram\",\n \"medtriangle\",\n \"largetriangle\",\n ]);\n\n // Convert TangramSpec to internal format\n const filteredTans = tangram.solutionTans.filter((tan: any) => {\n const tanName = tan.name ?? tan.kind;\n return CANON.has(tanName);\n });\n\n const mask = filteredTans.map((tan: any) => {\n const polygon = tan.vertices.map(([x, y]: number[]) => ({ x: x ?? 0, y: -(y ?? 0) }));\n return polygon;\n });\n\n const primitiveDecomposition = filteredTans.map((tan: any) => ({\n kind: (tan.name ?? tan.kind) as TanKind,\n polygon: tan.vertices.map(([x, y]: number[]) => ({ x: x ?? 0, y: -(y ?? 0) }))\n }));\n\n // Use FIXED viewport size for n-back display (constant across all tangrams)\n const DISPLAY_SIZE = 400;\n const viewport = {\n w: DISPLAY_SIZE,\n h: DISPLAY_SIZE\n };\n\n // Compute scale factor to keep tangram pieces at constant physical size\n // This matches the approach in TangramConstructPlugin/GameBoard\n const scaleS = React.useMemo(() => {\n const u = inferUnitFromPolys(mask);\n return u ? (CONFIG.layout.grid.unitPx / u) : 1;\n }, [mask]);\n\n // For n-back display, we want tangrams centered in viewport\n // Don't use computeCircleLayout's positioning - just center manually\n const centerPos = {\n cx: viewport.w / 2,\n cy: viewport.h / 2\n };\n\n // Helper to convert polygon to SVG path\n const pathD = (poly: Poly): string => {\n if (!poly || poly.length === 0) return \"\";\n const moves = poly.map((p, i) => `${i === 0 ? \"M\" : \"L\"} ${p.x} ${p.y}`);\n return moves.join(\" \") + \" Z\";\n };\n\n // End trial with data\n const endTrial = (data: {\n responded_match: boolean;\n rt: number;\n responded_after_duration: boolean;\n rt_after_duration: number;\n }) => {\n // Clear timeout if it exists\n if (timeoutIdRef.current) {\n clearTimeout(timeoutIdRef.current);\n timeoutIdRef.current = null;\n }\n\n // Compute accuracy\n const accuracy = isMatch !== undefined\n ? (isMatch === data.responded_match ? 1 : 0)\n : NaN;\n\n const trialData = {\n ...data,\n accuracy,\n tangram_id: tangram.tangramID,\n is_match: isMatch,\n show_tangram_decomposition: show_tangram_decomposition,\n use_primitive_colors: usePrimitiveColors,\n primitive_color_indices: primitiveColorIndices,\n duration: duration,\n button_text: button_text\n };\n\n if (onTrialEnd) {\n onTrialEnd(trialData);\n }\n };\n\n // Handle button click\n const handleButtonClick = () => {\n if (!buttonEnabledRef.current) {\n // Late response after duration expired\n const rt_late = Date.now() - trialStartTime.current;\n hasRespondedRef.current = true;\n responseDataRef.current = {\n responded_match: true,\n rt: NaN,\n responded_after_duration: true,\n rt_after_duration: rt_late\n };\n endTrial(responseDataRef.current);\n } else {\n // Response before duration expired - disable button but continue showing trial\n const rt = Date.now() - trialStartTime.current;\n buttonEnabledRef.current = false;\n setButtonDisabled(true);\n hasRespondedRef.current = true;\n responseDataRef.current = {\n responded_match: true,\n rt,\n responded_after_duration: false,\n rt_after_duration: NaN\n };\n // Don't end trial yet - wait for duration timeout\n }\n };\n\n // Set up duration timeout\n useEffect(() => {\n timeoutIdRef.current = setTimeout(() => {\n buttonEnabledRef.current = false;\n\n // End trial with appropriate data\n if (hasRespondedRef.current && responseDataRef.current) {\n // User already responded - use saved response data\n endTrial(responseDataRef.current);\n } else {\n // No response - end with no-response data\n endTrial({\n responded_match: false,\n rt: NaN,\n responded_after_duration: false,\n rt_after_duration: NaN\n });\n }\n }, duration);\n\n return () => {\n if (timeoutIdRef.current) {\n clearTimeout(timeoutIdRef.current);\n }\n };\n }, []);\n\n // Render silhouette\n const renderSilhouette = () => {\n if (show_tangram_decomposition) {\n // Render decomposed primitives with borders\n const rawPolys = primitiveDecomposition.map((primInfo: any) => primInfo.polygon);\n const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, centerPos);\n\n return (\n <g key=\"sil-decomposed\" pointerEvents=\"none\">\n {placedPolys.map((scaledPoly, i) => {\n // Get color for this primitive based on its kind\n const primInfo = primitiveDecomposition[i];\n let fillColor = CONFIG.color.silhouetteMask;\n\n if (usePrimitiveColors && primInfo?.kind && primitiveColorIndices) {\n // Map primitive kind to index\n const kindToIndex: Record<TanKind, number> = {\n 'square': 0,\n 'smalltriangle': 1,\n 'parallelogram': 2,\n 'medtriangle': 3,\n 'largetriangle': 4\n };\n const primitiveIndex = kindToIndex[primInfo.kind as TanKind];\n if (primitiveIndex !== undefined && primitiveColorIndices[primitiveIndex] !== undefined) {\n const colorIndex = primitiveColorIndices[primitiveIndex];\n const color = CONFIG.color.primitiveColors[colorIndex];\n if (color) {\n fillColor = color;\n }\n }\n }\n\n return (\n <React.Fragment key={`prim-${i}`}>\n {/* Fill path */}\n <path\n d={pathD(scaledPoly)}\n fill={fillColor}\n opacity={usePrimitiveColors ? CONFIG.opacity.piece.normal : CONFIG.opacity.silhouetteMask}\n stroke=\"none\"\n />\n\n {/* Full perimeter border */}\n <path\n d={pathD(scaledPoly)}\n fill=\"none\"\n stroke={CONFIG.color.tangramDecomposition.stroke}\n strokeWidth={CONFIG.size.stroke.tangramDecompositionPx}\n />\n </React.Fragment>\n );\n })}\n </g>\n );\n } else {\n // Render unified silhouette\n const placedPolys = placeSilhouetteGridAlignedAsPolys(mask, scaleS, centerPos);\n\n return (\n <g key=\"sil-unified\" pointerEvents=\"none\">\n {placedPolys.map((scaledPoly, i) => {\n // Get color for this primitive based on its kind\n const primInfo = primitiveDecomposition[i];\n let fillColor = CONFIG.color.silhouetteMask;\n\n if (usePrimitiveColors && primInfo?.kind && primitiveColorIndices) {\n // Map primitive kind to index\n const kindToIndex: Record<TanKind, number> = {\n 'square': 0,\n 'smalltriangle': 1,\n 'parallelogram': 2,\n 'medtriangle': 3,\n 'largetriangle': 4\n };\n const primitiveIndex = kindToIndex[primInfo.kind as TanKind];\n if (primitiveIndex !== undefined && primitiveColorIndices[primitiveIndex] !== undefined) {\n const colorIndex = primitiveColorIndices[primitiveIndex];\n const color = CONFIG.color.primitiveColors[colorIndex];\n if (color) {\n fillColor = color;\n }\n }\n }\n\n return (\n <path\n key={`sil-${i}`}\n d={pathD(scaledPoly)}\n fill={fillColor}\n opacity={usePrimitiveColors ? CONFIG.opacity.piece.normal : CONFIG.opacity.silhouetteMask}\n stroke=\"none\"\n />\n );\n })}\n </g>\n );\n }\n };\n\n return (\n <div style={{\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n padding: \"20px\",\n background: \"#fff7e0ff\"\n }}>\n {/* Instructions */}\n {instructions && (\n <div\n style={{\n maxWidth: \"800px\",\n width: \"100%\",\n marginBottom: \"30px\",\n textAlign: \"center\",\n fontSize: \"18px\",\n lineHeight: \"1.5\"\n }}\n dangerouslySetInnerHTML={{ __html: instructions }}\n />\n )}\n\n {/* Container for SVG and button */}\n <div style={{\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n gap: \"30px\"\n }}>\n {/* SVG with tangram silhouette */}\n <svg\n width={viewport.w}\n height={viewport.h}\n viewBox={`0 0 ${viewport.w} ${viewport.h}`}\n style={{\n display: \"block\",\n background: CONFIG.color.bands.silhouette.fillEven,\n border: `${CONFIG.size.stroke.bandPx}px solid ${CONFIG.color.bands.silhouette.stroke}`,\n borderRadius: \"8px\"\n }}\n >\n {renderSilhouette()}\n </svg>\n\n {/* Response button */}\n <button\n className=\"jspsych-btn\"\n onClick={handleButtonClick}\n disabled={buttonDisabled}\n style={{\n padding: \"12px 30px\",\n fontSize: \"16px\",\n cursor: buttonDisabled ? \"not-allowed\" : \"pointer\",\n opacity: buttonDisabled ? 0.5 : 1\n }}\n >\n {button_text}\n </button>\n </div>\n </div>\n );\n}\n","import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from \"jspsych\";\nimport { startNBackTrial, StartNBackTrialParams } from \"./NBackApp\";\n\nconst info = {\n name: \"tangram-nback\",\n version: \"1.0.0\",\n parameters: {\n /** Single tangram specification to display */\n tangram: {\n type: ParameterType.COMPLEX,\n default: undefined,\n description: \"TangramSpec object defining target shape to display\"\n },\n /** Whether this trial is a match (for computing accuracy) */\n isMatch: {\n type: ParameterType.BOOL,\n default: undefined,\n description: \"Whether this tangram matches the previous one (optional)\"\n },\n /** Whether to show tangram decomposed into individual primitives with borders */\n show_tangram_decomposition: {\n type: ParameterType.BOOL,\n default: false,\n description: \"Whether to show tangram decomposed into individual primitives with borders\"\n },\n /** HTML content to display above the tangram as instructions */\n instructions: {\n type: ParameterType.STRING,\n default: \"\",\n description: \"HTML content to display above the tangram as instructions\"\n },\n /** Text to display on response button */\n button_text: {\n type: ParameterType.STRING,\n default: \"Same as previous!\",\n description: \"Text to display on response button\"\n },\n /** Duration to display tangram and accept responses (milliseconds) */\n duration: {\n type: ParameterType.INT,\n default: 3000,\n description: \"Duration in milliseconds to display tangram and accept responses\"\n },\n /** Whether to use distinct colors for each primitive shape type */\n use_primitive_colors: {\n type: ParameterType.BOOL,\n default: false,\n description: \"Whether each primitive shape type should have its own distinct color in the displayed tangram\"\n },\n /** Indices mapping primitives to colors from the color palette */\n primitive_color_indices: {\n type: ParameterType.OBJECT,\n default: [0, 1, 2, 3, 4],\n description: \"Array of 5 integers indexing into primitiveColors array, mapping [square, smalltriangle, parallelogram, medtriangle, largetriangle] to colors\"\n },\n /** Callback fired when trial ends */\n onTrialEnd: {\n type: ParameterType.FUNCTION,\n default: undefined,\n description: \"Callback when trial completes with full data\"\n }\n },\n data: {\n /** Whether participant clicked the response button before duration expired */\n responded_match: {\n type: ParameterType.BOOL,\n description: \"True if participant clicked response button, false otherwise\"\n },\n /** Reaction time in milliseconds (NaN if no response or response after duration) */\n rt: {\n type: ParameterType.INT,\n description: \"Milliseconds between trial start and button click (NaN if no response or late response)\"\n },\n /** Accuracy: 1 if correct, 0 if incorrect, NaN if isMatch not provided */\n accuracy: {\n type: ParameterType.FLOAT,\n description: \"1 if response matches isMatch parameter, 0 otherwise (NaN if isMatch not provided)\"\n },\n /** Whether response occurred after duration expired */\n responded_after_duration: {\n type: ParameterType.BOOL,\n description: \"True if button clicked after duration expired, false otherwise\"\n },\n /** Time of late response (NaN if no late response) */\n rt_after_duration: {\n type: ParameterType.INT,\n description: \"Milliseconds between trial start and late button click (NaN if no late response)\"\n }\n },\n citations: \"\"\n};\n\ntype Info = typeof info;\n\n/**\n * **tangram-nback**\n *\n * A jsPsych plugin for n-back matching trials displaying a single tangram\n * with a response button.\n *\n * @author Justin Yang & Sean Paul Anderson\n * @see {@link https://github.com/cogtoolslab/tangram_construction.git/tree/main/experiments/jspsych-tangram-prep}\n */\nclass TangramNBackPlugin implements JsPsychPlugin<Info> {\n static info = info;\n\n constructor(private jsPsych: JsPsych) {}\n\n /**\n * Launches the trial by invoking startNBackTrial\n * with the display element, parameters, and jsPsych instance.\n */\n trial(display_element: HTMLElement, trial: TrialType<Info>) {\n // Wrap onTrialEnd to handle React cleanup and jsPsych trial completion\n const wrappedOnTrialEnd = (data: any) => {\n // Call user-provided callback if exists\n if (trial.onTrialEnd) {\n trial.onTrialEnd(data);\n }\n\n // Clean up React first (before clearing DOM)\n const reactContext = (display_element as any).__reactContext;\n if (reactContext?.root) {\n reactContext.root.unmount();\n }\n\n // Clear display after React cleanup\n display_element.innerHTML = '';\n\n // Finish jsPsych trial with data\n this.jsPsych.finishTrial(data);\n };\n\n // Create parameter object for wrapper\n const params: StartNBackTrialParams = {\n tangram: trial.tangram,\n isMatch: trial.isMatch,\n show_tangram_decomposition: trial.show_tangram_decomposition,\n instructions: trial.instructions,\n button_text: trial.button_text,\n duration: trial.duration,\n usePrimitiveColors: trial.use_primitive_colors,\n primitiveColorIndices: trial.primitive_color_indices,\n onTrialEnd: wrappedOnTrialEnd\n };\n\n // Use React wrapper to start the trial\n const { root, display_element: element, jsPsych } = startNBackTrial(display_element, params, this.jsPsych);\n\n // Store React context for cleanup\n (element as any).__reactContext = { root, jsPsych };\n }\n}\n\nexport default TangramNBackPlugin;\n"],"names":["createRoot","useRef","useState","useEffect","ParameterType"],"mappings":"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"}
|
package/dist/nback/index.js
CHANGED
|
@@ -33,7 +33,9 @@ const CONFIG = {
|
|
|
33
33
|
]
|
|
34
34
|
},
|
|
35
35
|
opacity: {
|
|
36
|
-
silhouetteMask: 0.25
|
|
36
|
+
silhouetteMask: 0.25,
|
|
37
|
+
piece: { normal: 1 }
|
|
38
|
+
},
|
|
37
39
|
size: {
|
|
38
40
|
stroke: { bandPx: 5, tangramDecompositionPx: 1 }},
|
|
39
41
|
layout: {
|
|
@@ -228,7 +230,7 @@ function NBackView({ params }) {
|
|
|
228
230
|
{
|
|
229
231
|
d: pathD(scaledPoly),
|
|
230
232
|
fill: fillColor,
|
|
231
|
-
opacity: CONFIG.opacity.silhouetteMask,
|
|
233
|
+
opacity: usePrimitiveColors ? CONFIG.opacity.piece.normal : CONFIG.opacity.silhouetteMask,
|
|
232
234
|
stroke: "none"
|
|
233
235
|
}
|
|
234
236
|
), /* @__PURE__ */ React.createElement(
|
|
@@ -269,7 +271,7 @@ function NBackView({ params }) {
|
|
|
269
271
|
key: `sil-${i}`,
|
|
270
272
|
d: pathD(scaledPoly),
|
|
271
273
|
fill: fillColor,
|
|
272
|
-
opacity: CONFIG.opacity.silhouetteMask,
|
|
274
|
+
opacity: usePrimitiveColors ? CONFIG.opacity.piece.normal : CONFIG.opacity.silhouetteMask,
|
|
273
275
|
stroke: "none"
|
|
274
276
|
}
|
|
275
277
|
);
|
|
@@ -282,7 +284,7 @@ function NBackView({ params }) {
|
|
|
282
284
|
alignItems: "center",
|
|
283
285
|
justifyContent: "center",
|
|
284
286
|
padding: "20px",
|
|
285
|
-
background: "#
|
|
287
|
+
background: "#fff7e0ff"
|
|
286
288
|
} }, instructions && /* @__PURE__ */ React.createElement(
|
|
287
289
|
"div",
|
|
288
290
|
{
|
package/dist/nback/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../../src/core/engine/geometry/bounds.ts","../../src/core/config/config.ts","../../src/core/engine/geometry/pieces.ts","../../src/plugins/tangram-nback/NBackApp.tsx","../../src/plugins/tangram-nback/index.ts"],"sourcesContent":["import type { Poly, Vec, PrimitiveBlueprint, CompositeBlueprint, Blueprint } from \"@/core/domain/types\";\n\nexport type AABB = { min: Vec; max: Vec; width: number; height: number };\n\n/** AABB of a single polygon (numeric). */\nexport function aabbOf(poly: Poly): { minX: number; minY: number; maxX: number; maxY: number } {\n let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\n for (const p of poly) {\n if (p.x < minX) minX = p.x;\n if (p.y < minY) minY = p.y;\n if (p.x > maxX) maxX = p.x;\n if (p.y > maxY) maxY = p.y;\n }\n return { minX, minY, maxX, maxY };\n}\n\n/** AABB of one or more polygons with width/height and center. */\nexport function polysAABB(polys: Poly[]) {\n let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\n for (const poly of polys) {\n for (const p of poly) {\n if (p.x < minX) minX = p.x;\n if (p.y < minY) minY = p.y;\n if (p.x > maxX) maxX = p.x;\n if (p.y > maxY) maxY = p.y;\n }\n }\n const width = Math.max(1, maxX - minX);\n const height = Math.max(1, maxY - minY);\n return { min: { x: minX, y: minY }, max: { x: maxX, y: maxY }, width, height, cx:(minX+maxX)/2, cy:(minY+maxY)/2 };\n}\n\n/** Bounds of multiple polygons at the origin. */\nexport function boundsOfShapes(shapes: Poly[]): AABB {\n let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\n for (const poly of shapes) {\n for (const p of poly) {\n if (p.x < minX) minX = p.x;\n if (p.y < minY) minY = p.y;\n if (p.x > maxX) maxX = p.x;\n if (p.y > maxY) maxY = p.y;\n }\n }\n const width = Math.max(0, maxX - minX);\n const height = Math.max(0, maxY - minY);\n return { min: { x: minX, y: minY }, max: { x: maxX, y: maxY }, width, height };\n}\n\n/** Bounds of a primitive blueprint (uses its canonical shape). */\nexport function boundsOfPrimitive(bp: PrimitiveBlueprint): AABB {\n return boundsOfShapes(bp.shape);\n}\n\n/**\n * Bounds of a composite blueprint.\n * If `shape` is provided as precomputed union polygons, we use that.\n * Otherwise we bound the parts’ primitives at their offsets (loose but safe).\n */\nexport function boundsOfComposite(\n bp: CompositeBlueprint,\n primitiveLookup: (kind: string) => PrimitiveBlueprint | undefined\n): AABB {\n if (bp.shape && bp.shape.length) return boundsOfShapes(bp.shape);\n\n let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\n for (const part of bp.parts) {\n const prim = primitiveLookup(part.kind);\n if (!prim) continue;\n const b = boundsOfPrimitive(prim);\n const pMinX = part.offset.x + b.min.x;\n const pMinY = part.offset.y + b.min.y;\n const pMaxX = part.offset.x + b.max.x;\n const pMaxY = part.offset.y + b.max.y;\n if (pMinX < minX) minX = pMinX;\n if (pMinY < minY) minY = pMinY;\n if (pMaxX > maxX) maxX = pMaxX;\n if (pMaxY > maxY) maxY = pMaxY;\n }\n const width = Math.max(0, maxX - minX);\n const height = Math.max(0, maxY - minY);\n return { min: { x: minX, y: minY }, max: { x: maxX, y: maxY }, width, height };\n}\n\n/** Unified bounds helper. */\nexport function boundsOfBlueprint(\n bp: Blueprint,\n primitiveLookup: (kind: string) => PrimitiveBlueprint | undefined\n): AABB {\n if (\"parts\" in bp) return boundsOfComposite(bp, primitiveLookup);\n return boundsOfPrimitive(bp);\n}\n","// src/core/config/config.ts\nexport type Config = {\n color: {\n background: string;\n bands: {\n silhouette: { fillEven: string; fillOdd: string; stroke: string };\n workspace: { fillEven: string; fillOdd: string; stroke: string };\n };\n completion: { fill: string; stroke: string };\n silhouetteMask: string;\n anchors: { invalid: string; valid: string };\n piece: { draggingFill: string; validFill: string; invalidFill: string; invalidStroke: string; selectedStroke: string; allGreenStroke: string; borderStroke: string };\n ui: { light: string; dark: string };\n blueprint: { fill: string; selectedStroke: string; badgeFill: string; labelFill: string };\n tangramDecomposition: { stroke: string };\n primitiveColors: string[];\n };\n opacity: {\n blueprint: number;\n silhouetteMask: number;\n anchors: { invalid: number; valid: number };\n piece: { invalid: number; dragging: number; locked: number; normal: number };\n };\n size: {\n stroke: { bandPx: number; pieceSelectedPx: number; allGreenStrokePx: number; pieceBorderPx: number; tangramDecompositionPx: number };\n anchorRadiusPx: { valid: number; invalid: number };\n badgeFontPx: number;\n centerBadge: { fractionOfOuterR: number; minPx: number; marginPx: number };\n invalidMarker: { sizePx: number; strokePx: number };\n };\n layout: {\n grid: { stepPx: number; unitPx: number };\n paddingPx: number;\n viewportScale: number;\n /** renamed from capacity → constraints */\n constraints: {\n workspaceDiamAnchors: number;\n quickstashDiamAnchors: number;\n primitiveDiamAnchors: number;\n };\n defaults: { maxQuickstashSlots: number };\n };\n game: {\n snapRadiusPx: number;\n showBorders: boolean;\n hideTouchingBorders: boolean;\n silhouettesBelowPieces: boolean;\n };\n};\n\nexport const CONFIG: Config = {\n color: {\n background: \"#fff7e0ff\",\n bands: {\n silhouette: { fillEven: \"#ffffff\", fillOdd: \"#ffffff\", stroke: \"#b1b1b1\" },\n workspace: { fillEven: \"#ffffff\", fillOdd: \"#ffffff\", stroke: \"#b1b1b1\" }\n },\n completion: { fill: \"#ccffcc\", stroke: \"#13da57\" },\n silhouetteMask: \"#374151\",\n anchors: { invalid: \"#7dd3fc\", valid: \"#475569\" },\n // validFill used here for placed composites\n piece: { draggingFill: \"#8e7cc3\", validFill: \"#8e7cc3\", invalidFill: \"#d55c00\", invalidStroke: \"#dc2626\", selectedStroke: \"#674ea7\", allGreenStroke: \"#86efac\", borderStroke: \"#674ea7\" },\n ui: { light: \"#60a5fa\", dark: \"#1d4ed8\" },\n blueprint: { fill: \"#374151\", selectedStroke: \"#111827\", badgeFill: \"#000000\", labelFill: \"#ffffff\" },\n tangramDecomposition: { stroke: \"#fef2cc\" },\n primitiveColors: [ // from seaborn \"colorblind\" palette, 6 colors, with red omitted\n '#0173b2',\n '#de8f05',\n '#029e73',\n '#cc78bc',\n '#ca9161'\n ]\n },\n opacity: {\n blueprint: 0.6,\n silhouetteMask: 0.25,\n //anchors: { valid: 0.80, invalid: 0.50 },\n anchors: { invalid: 0.0, valid: 0.0 },\n piece: { invalid: 1, dragging: 1, locked: 1, normal: 1 },\n },\n size: {\n stroke: { bandPx: 5, pieceSelectedPx: 5, allGreenStrokePx: 10, pieceBorderPx: 2, tangramDecompositionPx: 1 },\n anchorRadiusPx: { valid: 1.0, invalid: 1.0 },\n badgeFontPx: 16,\n centerBadge: { fractionOfOuterR: 0.15, minPx: 20, marginPx: 4 },\n invalidMarker: { sizePx: 10, strokePx: 4 }\n },\n layout: {\n grid: { stepPx: 20, unitPx: 40 },\n paddingPx: 1,\n viewportScale: 0.8,\n constraints: {\n workspaceDiamAnchors: 10, // num anchors req'd to be on diagonal\n quickstashDiamAnchors: 7, // num anchors req'd to be in single quickstash slot\n primitiveDiamAnchors: 5,\n },\n defaults: { maxQuickstashSlots: 1 }\n },\n game: {\n snapRadiusPx: 15,\n showBorders: false,\n hideTouchingBorders: true,\n silhouettesBelowPieces: true\n }\n};","/**\n * Game-specific piece geometry operations\n * Includes: piece positioning, placement, grid snapping, and anchor generation\n *\n * Consolidated from: piece.ts, placement.ts, grid.ts, anchors.ts\n */\n\nimport type { Blueprint, Vec, Poly, Anchor } from \"@/core/domain/types\";\nimport type { CircleLayout, SectorGeom } from \"@/core/domain/layout\";\nimport { polysAABB } from \"./bounds\";\nimport { pointInPolygon, dist2 } from \"./math\";\nimport { CONFIG } from \"@/core/config/config\";\n\nconst GRID_PX = CONFIG.layout.grid.stepPx;\n\n// ===== Piece Positioning (from piece.ts) =====\n\n/** Translate blueprint polys to world coords for a given top-left (TL). */\nexport function piecePolysAt(\n bp: Blueprint,\n bb: { min: { x: number; y: number } },\n tl: { x: number; y: number }\n) {\n const ox = tl.x - bb.min.x;\n const oy = tl.y - bb.min.y;\n const polys = (\"shape\" in bp && bp.shape) ? bp.shape : [];\n return polys.map(poly => poly.map(p => ({ x: p.x + ox, y: p.y + oy })));\n}\n\n/** Build support offsets: each vertex relative to the AABB center. */\nexport function computeSupportOffsets(\n bp: Blueprint,\n bb: { min: { x: number; y: number }; width: number; height: number }\n) {\n const cx = bb.min.x + bb.width / 2;\n const cy = bb.min.y + bb.height / 2;\n\n const polys = (\"shape\" in bp && bp.shape && bp.shape.length)\n ? bp.shape\n : [[\n { x: bb.min.x, y: bb.min.y },\n { x: bb.min.x + bb.width, y: bb.min.y },\n { x: bb.min.x + bb.width, y: bb.min.y + bb.height },\n { x: bb.min.x, y: bb.min.y + bb.height },\n ]];\n\n const offs: Vec[] = [];\n for (const poly of polys) for (const v of poly) offs.push({ x: v.x - cx, y: v.y - cy });\n return offs;\n}\n\n/** Clamp using directional vertex support → exact contact with the ring. */\nexport function clampTopLeftBySupport(\n tlx: number,\n tly: number,\n d: {\n aabb: { width: number; height: number };\n support: { x: number; y: number }[];\n },\n layout: CircleLayout,\n target: \"workspace\" | \"silhouette\",\n pointerInsideCenter: boolean\n) {\n const cx0 = tlx + d.aabb.width / 2;\n const cy0 = tly + d.aabb.height / 2;\n\n const vx = cx0 - layout.cx;\n const vy = cy0 - layout.cy;\n const r = Math.hypot(vx, vy);\n if (r === 0) return { x: tlx, y: tly };\n\n const ux = vx / r, uy = vy / r;\n\n let h_out = -Infinity, h_in = -Infinity;\n for (const o of d.support) {\n const outProj = o.x * ux + o.y * uy;\n if (outProj > h_out) h_out = outProj;\n const inProj = -(o.x * ux + o.y * uy);\n if (inProj > h_in) h_in = inProj;\n }\n\n if (target === \"workspace\") {\n const [rIn, rOut] = layout.bands.workspace;\n const minR = pointerInsideCenter ? 0 : (rIn + h_in);\n const maxR = rOut - h_out;\n const rClamped = Math.min(Math.max(r, minR), maxR);\n if (Math.abs(rClamped - r) < 1e-6) return { x: tlx, y: tly };\n const newCx = layout.cx + rClamped * ux;\n const newCy = layout.cy + rClamped * uy;\n return { x: newCx - d.aabb.width / 2, y: newCy - d.aabb.height / 2 };\n } else {\n const rOut = layout.bands.silhouette[1];\n const maxR = rOut - h_out;\n if (r <= maxR) return { x: tlx, y: tly };\n const newCx = layout.cx + maxR * ux;\n const newCy = layout.cy + maxR * uy;\n return { x: newCx - d.aabb.width / 2, y: newCy - d.aabb.height / 2 };\n }\n}\n\n// ===== Placement (from placement.ts) =====\n\n/** gcd for positive integers */\nfunction igcd(a: number, b: number) {\n a = Math.round(Math.abs(a)); b = Math.round(Math.abs(b));\n while (b) [a, b] = [b, a % b];\n return a || 1;\n}\n\n/** Infer the base lattice unit in raw silhouette coordinates. */\nexport function inferUnitFromPolys(polys: Poly[]): number {\n let g = 0;\n for (const poly of polys) {\n for (let i = 0; i < poly.length; i++) {\n const a = poly[i], b = poly[(i + 1) % poly.length];\n if (!a || !b) continue;\n const dx = Math.round(Math.abs(b.x - a.x));\n const dy = Math.round(Math.abs(b.y - a.y));\n if (dx) g = g ? igcd(g, dx) : dx;\n if (dy) g = g ? igcd(g, dy) : dy;\n }\n }\n return g || 1;\n}\n\n/**\n * Place polys using scale S, re-centered, and translation snapped to GRID_PX; returns polys.\n * The center is snapped to the grid so the lattice points under the silhouette stay invariant.\n */\nexport function placeSilhouetteGridAlignedAsPolys(\n polys: Poly[],\n S: number,\n rectCenter: { cx: number; cy: number }\n): Poly[] {\n if (!polys || polys.length === 0) return [];\n const a = polysAABB(polys);\n const cx0 = (a.min.x + a.max.x) / 2;\n const cy0 = (a.min.y + a.max.y) / 2;\n const cx = Math.round(rectCenter.cx / GRID_PX) * GRID_PX;\n const cy = Math.round(rectCenter.cy / GRID_PX) * GRID_PX;\n const tx = cx - S * cx0;\n const ty = cy - S * cy0;\n const stx = Math.round(tx / GRID_PX) * GRID_PX;\n const sty = Math.round(ty / GRID_PX) * GRID_PX;\n return polys.map(poly => poly.map(p => ({ x: S * p.x + stx, y: S * p.y + sty })));\n}\n\n// ===== Grid Operations (from grid.ts) =====\n\nexport function nearestGridNode(p: Vec): Vec {\n return { x: Math.round(p.x / GRID_PX) * GRID_PX, y: Math.round(p.y / GRID_PX) * GRID_PX };\n}\n\n/** Generate grid nodes inside an AABB. */\nexport function gridNodesInAABB(min: Vec, max: Vec): Vec[] {\n const out: Vec[] = [];\n const x0 = Math.ceil(min.x / GRID_PX) * GRID_PX;\n const y0 = Math.ceil(min.y / GRID_PX) * GRID_PX;\n for (let y = y0; y <= max.y; y += GRID_PX) {\n for (let x = x0; x <= max.x; x += GRID_PX) out.push({ x, y });\n }\n return out;\n}\n\n/** Keep nodes in a band (and optional sector wedge). */\nexport function filterNodesToBandAndSector(\n nodes: Vec[],\n layout: CircleLayout,\n band: \"workspace\" | \"silhouette\",\n sector?: SectorGeom\n): Vec[] {\n const [rIn, rOut] = layout.bands[band];\n const out: Vec[] = [];\n for (const n of nodes) {\n const dx = n.x - layout.cx, dy = n.y - layout.cy;\n const r = Math.hypot(dx, dy);\n if (r < rIn || r > rOut) continue;\n\n if (sector) {\n let theta = Math.atan2(dy, dx);\n if (layout.mode === \"circle\") {\n if (theta < -Math.PI / 2) theta += 2 * Math.PI;\n } else {\n if (theta < Math.PI) theta += 2 * Math.PI;\n }\n if (theta < sector.start || theta >= sector.end) continue;\n }\n out.push(n);\n }\n return out;\n}\n\n/** Keep nodes that lie inside any of the polygons. */\nexport function filterNodesInPolys(\n nodes: Vec[],\n polys: Poly[],\n pointInPolyFn: (pt: Vec, poly: Poly) => boolean = pointInPolygon\n): Vec[] {\n const out: Vec[] = [];\n node: for (const n of nodes) {\n for (const poly of polys) {\n if (pointInPolyFn(n, poly)) { out.push(n); continue node; }\n }\n }\n return out;\n}\n\n// ===== Anchor Operations (from anchors.ts) =====\n\n/**\n * Find the nearest anchor that \"accepts\" a piece signature (kind/compositeSignature).\n * Returns null if none within the optional radius.\n */\nexport function nearestAcceptingAnchor(\n anchors: Anchor[] | undefined,\n point: Vec,\n accepts: { kind?: string; compositeSignature?: string },\n withinPx: number = Infinity\n): { anchor: Anchor; d2: number } | null {\n if (!anchors || !anchors.length) return null;\n\n const { kind, compositeSignature } = accepts;\n const r2 = withinPx === Infinity ? Infinity : withinPx * withinPx;\n\n let best: { anchor: Anchor; d2: number } | null = null;\n for (const a of anchors) {\n const ok = a.accepts.some(acc =>\n (acc.kind === undefined || acc.kind === kind) &&\n (acc.compositeSignature === undefined || acc.compositeSignature === compositeSignature)\n );\n if (!ok) continue;\n\n const d2val = dist2(point, a.position);\n if (d2val <= r2 && (!best || d2val < best.d2)) {\n best = { anchor: a, d2: d2val };\n }\n }\n return best;\n}\n\n/** True if `point` is within `withinPx` of the given anchor. */\nexport function withinAnchor(anchor: Anchor, point: Vec, withinPx: number): boolean {\n const r2 = withinPx * withinPx;\n return dist2(point, anchor.position) <= r2;\n}\n\n// Node-grid helpers for anchor generation\nexport function workspaceNodes(layout: CircleLayout, sector: SectorGeom): Vec[] {\n const pad = 6;\n const min = { x: layout.cx - layout.outerR, y: layout.cy - layout.outerR - pad };\n const max = { x: layout.cx + layout.outerR, y: layout.cy + layout.outerR + pad };\n const nodes = gridNodesInAABB(min, max);\n return filterNodesToBandAndSector(nodes, layout, \"workspace\", sector);\n}\n\nexport function silhouetteNodes(layout: CircleLayout, sector: SectorGeom, fittedMask: Poly[]): Vec[] {\n const pad = 6;\n const min = { x: layout.cx - layout.outerR, y: layout.cy - layout.outerR - pad };\n const max = { x: layout.cx + layout.outerR, y: layout.cy + layout.outerR + pad };\n const nodes = gridNodesInAABB(min, max);\n const banded = filterNodesToBandAndSector(nodes, layout, \"silhouette\", sector);\n return filterNodesInPolys(banded, fittedMask);\n}\n\nexport function silhouetteBandNodes(layout: CircleLayout, sector: SectorGeom): Vec[] {\n const pad = 6;\n const min = { x: layout.cx - layout.outerR, y: layout.cy - layout.outerR - pad };\n const max = { x: layout.cx + layout.outerR, y: layout.cy + layout.outerR + pad };\n const nodes = gridNodesInAABB(min, max);\n return filterNodesToBandAndSector(nodes, layout, \"silhouette\", sector);\n}\n\n/** Generate anchor grid nodes for the inner ring (radius < innerR) */\nexport function innerRingNodes(layout: CircleLayout): Vec[] {\n const pad = 6;\n const min = { x: layout.cx - layout.innerR, y: layout.cy - layout.innerR - pad };\n const max = { x: layout.cx + layout.innerR, y: layout.cy + layout.innerR + pad };\n const nodes = gridNodesInAABB(min, max);\n // Filter to nodes within the inner circle\n const out: Vec[] = [];\n for (const n of nodes) {\n const dx = n.x - layout.cx, dy = n.y - layout.cy;\n const r = Math.hypot(dx, dy);\n if (r < layout.innerR) out.push(n);\n }\n return out;\n}\n\nexport const anchorsDiameterToPx = (anchorsDiag: number, gridPx: number = GRID_PX) =>\n anchorsDiag * Math.SQRT2 * gridPx;\n","/**\n * NBackApp.tsx - React wrapper for tangram n-back matching trials\n *\n * This component handles the React rendering logic for n-back trials,\n * displaying a single tangram silhouette with a response button.\n */\n\nimport React, { useRef, useEffect, useState } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport { JsPsych } from \"jspsych\";\nimport type { Poly, TanKind } from \"../../core/domain/types\";\nimport { placeSilhouetteGridAlignedAsPolys, inferUnitFromPolys } from \"../../core/engine/geometry\";\nimport { CONFIG } from \"../../core/config/config\";\n\nexport interface StartNBackTrialParams {\n tangram: any;\n isMatch?: boolean;\n show_tangram_decomposition?: boolean;\n instructions?: string;\n button_text?: string;\n duration: number;\n usePrimitiveColors?: boolean;\n primitiveColorIndices?: number[];\n onTrialEnd?: (data: any) => void;\n}\n\n/**\n * Start an n-back trial by rendering the NBackView component\n */\nexport function startNBackTrial(\n display_element: HTMLElement,\n params: StartNBackTrialParams,\n _jsPsych: JsPsych\n) {\n // Create React root and render NBackView\n const root = createRoot(display_element);\n root.render(React.createElement(NBackView, { params }));\n\n return { root, display_element, jsPsych: _jsPsych };\n}\n\ninterface NBackViewProps {\n params: StartNBackTrialParams;\n}\n\nfunction NBackView({ params }: NBackViewProps) {\n const {\n tangram,\n isMatch,\n show_tangram_decomposition,\n instructions,\n button_text,\n duration,\n usePrimitiveColors,\n primitiveColorIndices,\n onTrialEnd\n } = params;\n\n // Timing and response tracking\n const trialStartTime = useRef<number>(Date.now());\n const buttonEnabledRef = useRef<boolean>(true);\n const timeoutIdRef = useRef<NodeJS.Timeout | null>(null);\n const hasRespondedRef = useRef<boolean>(false);\n const responseDataRef = useRef<any>(null);\n const [buttonDisabled, setButtonDisabled] = useState(false);\n\n // Canonical piece names\n const CANON = new Set([\n \"square\",\n \"smalltriangle\",\n \"parallelogram\",\n \"medtriangle\",\n \"largetriangle\",\n ]);\n\n // Convert TangramSpec to internal format\n const filteredTans = tangram.solutionTans.filter((tan: any) => {\n const tanName = tan.name ?? tan.kind;\n return CANON.has(tanName);\n });\n\n const mask = filteredTans.map((tan: any) => {\n const polygon = tan.vertices.map(([x, y]: number[]) => ({ x: x ?? 0, y: -(y ?? 0) }));\n return polygon;\n });\n\n const primitiveDecomposition = filteredTans.map((tan: any) => ({\n kind: (tan.name ?? tan.kind) as TanKind,\n polygon: tan.vertices.map(([x, y]: number[]) => ({ x: x ?? 0, y: -(y ?? 0) }))\n }));\n\n // Use FIXED viewport size for n-back display (constant across all tangrams)\n const DISPLAY_SIZE = 400;\n const viewport = {\n w: DISPLAY_SIZE,\n h: DISPLAY_SIZE\n };\n\n // Compute scale factor to keep tangram pieces at constant physical size\n // This matches the approach in TangramConstructPlugin/GameBoard\n const scaleS = React.useMemo(() => {\n const u = inferUnitFromPolys(mask);\n return u ? (CONFIG.layout.grid.unitPx / u) : 1;\n }, [mask]);\n\n // For n-back display, we want tangrams centered in viewport\n // Don't use computeCircleLayout's positioning - just center manually\n const centerPos = {\n cx: viewport.w / 2,\n cy: viewport.h / 2\n };\n\n // Helper to convert polygon to SVG path\n const pathD = (poly: Poly): string => {\n if (!poly || poly.length === 0) return \"\";\n const moves = poly.map((p, i) => `${i === 0 ? \"M\" : \"L\"} ${p.x} ${p.y}`);\n return moves.join(\" \") + \" Z\";\n };\n\n // End trial with data\n const endTrial = (data: {\n responded_match: boolean;\n rt: number;\n responded_after_duration: boolean;\n rt_after_duration: number;\n }) => {\n // Clear timeout if it exists\n if (timeoutIdRef.current) {\n clearTimeout(timeoutIdRef.current);\n timeoutIdRef.current = null;\n }\n\n // Compute accuracy\n const accuracy = isMatch !== undefined\n ? (isMatch === data.responded_match ? 1 : 0)\n : NaN;\n\n const trialData = {\n ...data,\n accuracy,\n tangram_id: tangram.tangramID,\n is_match: isMatch,\n show_tangram_decomposition: show_tangram_decomposition,\n use_primitive_colors: usePrimitiveColors,\n primitive_color_indices: primitiveColorIndices,\n duration: duration,\n button_text: button_text\n };\n\n if (onTrialEnd) {\n onTrialEnd(trialData);\n }\n };\n\n // Handle button click\n const handleButtonClick = () => {\n if (!buttonEnabledRef.current) {\n // Late response after duration expired\n const rt_late = Date.now() - trialStartTime.current;\n hasRespondedRef.current = true;\n responseDataRef.current = {\n responded_match: true,\n rt: NaN,\n responded_after_duration: true,\n rt_after_duration: rt_late\n };\n endTrial(responseDataRef.current);\n } else {\n // Response before duration expired - disable button but continue showing trial\n const rt = Date.now() - trialStartTime.current;\n buttonEnabledRef.current = false;\n setButtonDisabled(true);\n hasRespondedRef.current = true;\n responseDataRef.current = {\n responded_match: true,\n rt,\n responded_after_duration: false,\n rt_after_duration: NaN\n };\n // Don't end trial yet - wait for duration timeout\n }\n };\n\n // Set up duration timeout\n useEffect(() => {\n timeoutIdRef.current = setTimeout(() => {\n buttonEnabledRef.current = false;\n\n // End trial with appropriate data\n if (hasRespondedRef.current && responseDataRef.current) {\n // User already responded - use saved response data\n endTrial(responseDataRef.current);\n } else {\n // No response - end with no-response data\n endTrial({\n responded_match: false,\n rt: NaN,\n responded_after_duration: false,\n rt_after_duration: NaN\n });\n }\n }, duration);\n\n return () => {\n if (timeoutIdRef.current) {\n clearTimeout(timeoutIdRef.current);\n }\n };\n }, []);\n\n // Render silhouette\n const renderSilhouette = () => {\n if (show_tangram_decomposition) {\n // Render decomposed primitives with borders\n const rawPolys = primitiveDecomposition.map((primInfo: any) => primInfo.polygon);\n const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, centerPos);\n\n return (\n <g key=\"sil-decomposed\" pointerEvents=\"none\">\n {placedPolys.map((scaledPoly, i) => {\n // Get color for this primitive based on its kind\n const primInfo = primitiveDecomposition[i];\n let fillColor = CONFIG.color.silhouetteMask;\n\n if (usePrimitiveColors && primInfo?.kind && primitiveColorIndices) {\n // Map primitive kind to index\n const kindToIndex: Record<TanKind, number> = {\n 'square': 0,\n 'smalltriangle': 1,\n 'parallelogram': 2,\n 'medtriangle': 3,\n 'largetriangle': 4\n };\n const primitiveIndex = kindToIndex[primInfo.kind as TanKind];\n if (primitiveIndex !== undefined && primitiveColorIndices[primitiveIndex] !== undefined) {\n const colorIndex = primitiveColorIndices[primitiveIndex];\n const color = CONFIG.color.primitiveColors[colorIndex];\n if (color) {\n fillColor = color;\n }\n }\n }\n\n return (\n <React.Fragment key={`prim-${i}`}>\n {/* Fill path */}\n <path\n d={pathD(scaledPoly)}\n fill={fillColor}\n opacity={CONFIG.opacity.silhouetteMask}\n stroke=\"none\"\n />\n\n {/* Full perimeter border */}\n <path\n d={pathD(scaledPoly)}\n fill=\"none\"\n stroke={CONFIG.color.tangramDecomposition.stroke}\n strokeWidth={CONFIG.size.stroke.tangramDecompositionPx}\n />\n </React.Fragment>\n );\n })}\n </g>\n );\n } else {\n // Render unified silhouette\n const placedPolys = placeSilhouetteGridAlignedAsPolys(mask, scaleS, centerPos);\n\n return (\n <g key=\"sil-unified\" pointerEvents=\"none\">\n {placedPolys.map((scaledPoly, i) => {\n // Get color for this primitive based on its kind\n const primInfo = primitiveDecomposition[i];\n let fillColor = CONFIG.color.silhouetteMask;\n\n if (usePrimitiveColors && primInfo?.kind && primitiveColorIndices) {\n // Map primitive kind to index\n const kindToIndex: Record<TanKind, number> = {\n 'square': 0,\n 'smalltriangle': 1,\n 'parallelogram': 2,\n 'medtriangle': 3,\n 'largetriangle': 4\n };\n const primitiveIndex = kindToIndex[primInfo.kind as TanKind];\n if (primitiveIndex !== undefined && primitiveColorIndices[primitiveIndex] !== undefined) {\n const colorIndex = primitiveColorIndices[primitiveIndex];\n const color = CONFIG.color.primitiveColors[colorIndex];\n if (color) {\n fillColor = color;\n }\n }\n }\n\n return (\n <path\n key={`sil-${i}`}\n d={pathD(scaledPoly)}\n fill={fillColor}\n opacity={CONFIG.opacity.silhouetteMask}\n stroke=\"none\"\n />\n );\n })}\n </g>\n );\n }\n };\n\n return (\n <div style={{\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n padding: \"20px\",\n background: \"#f5f5f5\"\n }}>\n {/* Instructions */}\n {instructions && (\n <div\n style={{\n maxWidth: \"800px\",\n width: \"100%\",\n marginBottom: \"30px\",\n textAlign: \"center\",\n fontSize: \"18px\",\n lineHeight: \"1.5\"\n }}\n dangerouslySetInnerHTML={{ __html: instructions }}\n />\n )}\n\n {/* Container for SVG and button */}\n <div style={{\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n gap: \"30px\"\n }}>\n {/* SVG with tangram silhouette */}\n <svg\n width={viewport.w}\n height={viewport.h}\n viewBox={`0 0 ${viewport.w} ${viewport.h}`}\n style={{\n display: \"block\",\n background: CONFIG.color.bands.silhouette.fillEven,\n border: `${CONFIG.size.stroke.bandPx}px solid ${CONFIG.color.bands.silhouette.stroke}`,\n borderRadius: \"8px\"\n }}\n >\n {renderSilhouette()}\n </svg>\n\n {/* Response button */}\n <button\n className=\"jspsych-btn\"\n onClick={handleButtonClick}\n disabled={buttonDisabled}\n style={{\n padding: \"12px 30px\",\n fontSize: \"16px\",\n cursor: buttonDisabled ? \"not-allowed\" : \"pointer\",\n opacity: buttonDisabled ? 0.5 : 1\n }}\n >\n {button_text}\n </button>\n </div>\n </div>\n );\n}\n","import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from \"jspsych\";\nimport { startNBackTrial, StartNBackTrialParams } from \"./NBackApp\";\n\nconst info = {\n name: \"tangram-nback\",\n version: \"1.0.0\",\n parameters: {\n /** Single tangram specification to display */\n tangram: {\n type: ParameterType.COMPLEX,\n default: undefined,\n description: \"TangramSpec object defining target shape to display\"\n },\n /** Whether this trial is a match (for computing accuracy) */\n isMatch: {\n type: ParameterType.BOOL,\n default: undefined,\n description: \"Whether this tangram matches the previous one (optional)\"\n },\n /** Whether to show tangram decomposed into individual primitives with borders */\n show_tangram_decomposition: {\n type: ParameterType.BOOL,\n default: false,\n description: \"Whether to show tangram decomposed into individual primitives with borders\"\n },\n /** HTML content to display above the tangram as instructions */\n instructions: {\n type: ParameterType.STRING,\n default: \"\",\n description: \"HTML content to display above the tangram as instructions\"\n },\n /** Text to display on response button */\n button_text: {\n type: ParameterType.STRING,\n default: \"Same as previous!\",\n description: \"Text to display on response button\"\n },\n /** Duration to display tangram and accept responses (milliseconds) */\n duration: {\n type: ParameterType.INT,\n default: 3000,\n description: \"Duration in milliseconds to display tangram and accept responses\"\n },\n /** Whether to use distinct colors for each primitive shape type */\n use_primitive_colors: {\n type: ParameterType.BOOL,\n default: false,\n description: \"Whether each primitive shape type should have its own distinct color in the displayed tangram\"\n },\n /** Indices mapping primitives to colors from the color palette */\n primitive_color_indices: {\n type: ParameterType.OBJECT,\n default: [0, 1, 2, 3, 4],\n description: \"Array of 5 integers indexing into primitiveColors array, mapping [square, smalltriangle, parallelogram, medtriangle, largetriangle] to colors\"\n },\n /** Callback fired when trial ends */\n onTrialEnd: {\n type: ParameterType.FUNCTION,\n default: undefined,\n description: \"Callback when trial completes with full data\"\n }\n },\n data: {\n /** Whether participant clicked the response button before duration expired */\n responded_match: {\n type: ParameterType.BOOL,\n description: \"True if participant clicked response button, false otherwise\"\n },\n /** Reaction time in milliseconds (NaN if no response or response after duration) */\n rt: {\n type: ParameterType.INT,\n description: \"Milliseconds between trial start and button click (NaN if no response or late response)\"\n },\n /** Accuracy: 1 if correct, 0 if incorrect, NaN if isMatch not provided */\n accuracy: {\n type: ParameterType.FLOAT,\n description: \"1 if response matches isMatch parameter, 0 otherwise (NaN if isMatch not provided)\"\n },\n /** Whether response occurred after duration expired */\n responded_after_duration: {\n type: ParameterType.BOOL,\n description: \"True if button clicked after duration expired, false otherwise\"\n },\n /** Time of late response (NaN if no late response) */\n rt_after_duration: {\n type: ParameterType.INT,\n description: \"Milliseconds between trial start and late button click (NaN if no late response)\"\n }\n },\n citations: \"\"\n};\n\ntype Info = typeof info;\n\n/**\n * **tangram-nback**\n *\n * A jsPsych plugin for n-back matching trials displaying a single tangram\n * with a response button.\n *\n * @author Justin Yang & Sean Paul Anderson\n * @see {@link https://github.com/cogtoolslab/tangram_construction.git/tree/main/experiments/jspsych-tangram-prep}\n */\nclass TangramNBackPlugin implements JsPsychPlugin<Info> {\n static info = info;\n\n constructor(private jsPsych: JsPsych) {}\n\n /**\n * Launches the trial by invoking startNBackTrial\n * with the display element, parameters, and jsPsych instance.\n */\n trial(display_element: HTMLElement, trial: TrialType<Info>) {\n // Wrap onTrialEnd to handle React cleanup and jsPsych trial completion\n const wrappedOnTrialEnd = (data: any) => {\n // Call user-provided callback if exists\n if (trial.onTrialEnd) {\n trial.onTrialEnd(data);\n }\n\n // Clean up React first (before clearing DOM)\n const reactContext = (display_element as any).__reactContext;\n if (reactContext?.root) {\n reactContext.root.unmount();\n }\n\n // Clear display after React cleanup\n display_element.innerHTML = '';\n\n // Finish jsPsych trial with data\n this.jsPsych.finishTrial(data);\n };\n\n // Create parameter object for wrapper\n const params: StartNBackTrialParams = {\n tangram: trial.tangram,\n isMatch: trial.isMatch,\n show_tangram_decomposition: trial.show_tangram_decomposition,\n instructions: trial.instructions,\n button_text: trial.button_text,\n duration: trial.duration,\n usePrimitiveColors: trial.use_primitive_colors,\n primitiveColorIndices: trial.primitive_color_indices,\n onTrialEnd: wrappedOnTrialEnd\n };\n\n // Use React wrapper to start the trial\n const { root, display_element: element, jsPsych } = startNBackTrial(display_element, params, this.jsPsych);\n\n // Store React context for cleanup\n (element as any).__reactContext = { root, jsPsych };\n }\n}\n\nexport default TangramNBackPlugin;\n"],"names":[],"mappings":";;;;AAiBO,SAAS,UAAU,KAAA,EAAe;AACvC,EAAA,IAAI,OAAO,QAAA,EAAU,IAAA,GAAO,QAAA,EAAU,IAAA,GAAO,WAAW,IAAA,GAAO,CAAA,QAAA;AAC/D,EAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,IAAA,KAAA,MAAW,KAAK,IAAA,EAAM;AACpB,MAAA,IAAI,CAAA,CAAE,CAAA,GAAI,IAAA,EAAM,IAAA,GAAO,CAAA,CAAE,CAAA;AACzB,MAAA,IAAI,CAAA,CAAE,CAAA,GAAI,IAAA,EAAM,IAAA,GAAO,CAAA,CAAE,CAAA;AACzB,MAAA,IAAI,CAAA,CAAE,CAAA,GAAI,IAAA,EAAM,IAAA,GAAO,CAAA,CAAE,CAAA;AACzB,MAAA,IAAI,CAAA,CAAE,CAAA,GAAI,IAAA,EAAM,IAAA,GAAO,CAAA,CAAE,CAAA;AAAA,IAC3B;AAAA,EACF;AACA,EAAA,MAAM,KAAA,GAAS,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,OAAO,IAAI,CAAA;AACtC,EAAA,MAAM,MAAA,GAAS,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,OAAO,IAAI,CAAA;AACtC,EAAA,OAAO,EAAE,GAAA,EAAK,EAAE,CAAA,EAAG,IAAA,EAAM,GAAG,IAAA,EAAK,EAAG,GAAA,EAAK,EAAE,CAAA,EAAG,IAAA,EAAM,GAAG,IAAA,EAAK,EAAG,KAAA,EAAO,MAAA,EAAQ,EAAA,EAAA,CAAI,IAAA,GAAK,QAAM,CAAA,EAAG,EAAA,EAAA,CAAI,IAAA,GAAK,IAAA,IAAM,CAAA,EAAE;AACnH;;ACoBO,MAAM,MAAA,GAAiB;AAAA,EAC5B,KAAA,EAAO;AAAA,IAEL,KAAA,EAAO;AAAA,MACL,YAAY,EAAE,QAAA,EAAU,WAA+B,QAAQ,SAAA,EAEjE,CAAA;AAAA,IAEA,cAAA,EAAgB,SAAA;AAAA,IAMhB,oBAAA,EAAsB,EAAE,MAAA,EAAQ,SAAA,EAAU;AAAA,IAC1C,eAAA,EAAiB;AAAA;AAAA,MACf,SAAA;AAAA,MACA,SAAA;AAAA,MACA,SAAA;AAAA,MACA,SAAA;AAAA,MACA;AAAA;AACJ,GACA;AAAA,EACA,OAAA,EAAS;AAAA,IAEP,cAAA,EAAgB,IAIlB,CAAA;AAAA,EACA,IAAA,EAAM;AAAA,IACJ,MAAA,EAAQ,EAAE,MAAA,EAAQ,CAAA,EAA+D,sBAAA,EAAwB,CAAA,EAK3G,CAAA;AAAA,EACA,MAAA,EAAQ;AAAA,IACN,IAAA,EAAM,EAAE,MAAA,EAAQ,EAAA,EAAI,QAAQ,EAAA,EAS9B,CAOF,CAAA;;AC3FA,MAAM,OAAA,GAAU,MAAA,CAAO,MAAA,CAAO,IAAA,CAAK,MAAA;AA0FnC,SAAS,IAAA,CAAK,GAAW,CAAA,EAAW;AAClC,EAAA,CAAA,GAAI,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,GAAA,CAAI,CAAC,CAAC,CAAA;AAAG,EAAA,CAAA,GAAI,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,GAAA,CAAI,CAAC,CAAC,CAAA;AACvD,EAAA,OAAO,CAAA,GAAI,CAAA,EAAG,CAAC,IAAI,CAAC,CAAA,EAAG,IAAI,CAAC,CAAA;AAC5B,EAAA,OAAO,CAAA,IAAK,CAAA;AACd;AAGO,SAAS,mBAAmB,KAAA,EAAuB;AACxD,EAAA,IAAI,CAAA,GAAI,CAAA;AACR,EAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,IAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,IAAA,CAAK,QAAQ,CAAA,EAAA,EAAK;AACpC,MAAA,MAAM,CAAA,GAAI,KAAK,CAAC,CAAA,EAAG,IAAI,IAAA,CAAA,CAAM,CAAA,GAAI,CAAA,IAAK,IAAA,CAAK,MAAM,CAAA;AACjD,MAAA,IAAI,CAAC,CAAA,IAAK,CAAC,CAAA,EAAG;AACd,MAAA,MAAM,EAAA,GAAK,KAAK,KAAA,CAAM,IAAA,CAAK,IAAI,CAAA,CAAE,CAAA,GAAI,CAAA,CAAE,CAAC,CAAC,CAAA;AACzC,MAAA,MAAM,EAAA,GAAK,KAAK,KAAA,CAAM,IAAA,CAAK,IAAI,CAAA,CAAE,CAAA,GAAI,CAAA,CAAE,CAAC,CAAC,CAAA;AACzC,MAAA,IAAI,IAAI,CAAA,GAAI,CAAA,GAAI,IAAA,CAAK,CAAA,EAAG,EAAE,CAAA,GAAI,EAAA;AAC9B,MAAA,IAAI,IAAI,CAAA,GAAI,CAAA,GAAI,IAAA,CAAK,CAAA,EAAG,EAAE,CAAA,GAAI,EAAA;AAAA,IAChC;AAAA,EACF;AACA,EAAA,OAAO,CAAA,IAAK,CAAA;AACd;AAMO,SAAS,iCAAA,CACd,KAAA,EACA,CAAA,EACA,UAAA,EACQ;AACR,EAAA,IAAI,CAAC,KAAA,IAAS,KAAA,CAAM,MAAA,KAAW,CAAA,SAAU,EAAC;AAC1C,EAAA,MAAM,CAAA,GAAI,UAAU,KAAK,CAAA;AACzB,EAAA,MAAM,OAAO,CAAA,CAAE,GAAA,CAAI,CAAA,GAAI,CAAA,CAAE,IAAI,CAAA,IAAK,CAAA;AAClC,EAAA,MAAM,OAAO,CAAA,CAAE,GAAA,CAAI,CAAA,GAAI,CAAA,CAAE,IAAI,CAAA,IAAK,CAAA;AAClC,EAAA,MAAM,KAAK,IAAA,CAAK,KAAA,CAAM,UAAA,CAAW,EAAA,GAAK,OAAO,CAAA,GAAI,OAAA;AACjD,EAAA,MAAM,KAAK,IAAA,CAAK,KAAA,CAAM,UAAA,CAAW,EAAA,GAAK,OAAO,CAAA,GAAI,OAAA;AACjD,EAAA,MAAM,EAAA,GAAK,KAAK,CAAA,GAAI,GAAA;AACpB,EAAA,MAAM,EAAA,GAAK,KAAK,CAAA,GAAI,GAAA;AACpB,EAAA,MAAM,GAAA,GAAM,IAAA,CAAK,KAAA,CAAM,EAAA,GAAK,OAAO,CAAA,GAAI,OAAA;AACvC,EAAA,MAAM,GAAA,GAAM,IAAA,CAAK,KAAA,CAAM,EAAA,GAAK,OAAO,CAAA,GAAI,OAAA;AACvC,EAAA,OAAO,MAAM,GAAA,CAAI,CAAA,IAAA,KAAQ,KAAK,GAAA,CAAI,CAAA,CAAA,MAAM,EAAE,CAAA,EAAG,CAAA,GAAI,CAAA,CAAE,CAAA,GAAI,KAAK,CAAA,EAAG,CAAA,GAAI,EAAE,CAAA,GAAI,GAAA,GAAM,CAAC,CAAA;AAClF;;ACpHO,SAAS,eAAA,CACd,eAAA,EACA,MAAA,EACA,QAAA,EACA;AAEA,EAAA,MAAM,IAAA,GAAO,WAAW,eAAe,CAAA;AACvC,EAAA,IAAA,CAAK,OAAO,KAAA,CAAM,aAAA,CAAc,WAAW,EAAE,MAAA,EAAQ,CAAC,CAAA;AAEtD,EAAA,OAAO,EAAE,IAAA,EAAM,eAAA,EAAiB,OAAA,EAAS,QAAA,EAAS;AACpD;AAMA,SAAS,SAAA,CAAU,EAAE,MAAA,EAAO,EAAmB;AAC7C,EAAA,MAAM;AAAA,IACJ,OAAA;AAAA,IACA,OAAA;AAAA,IACA,0BAAA;AAAA,IACA,YAAA;AAAA,IACA,WAAA;AAAA,IACA,QAAA;AAAA,IACA,kBAAA;AAAA,IACA,qBAAA;AAAA,IACA;AAAA,GACF,GAAI,MAAA;AAGJ,EAAA,MAAM,cAAA,GAAiB,MAAA,CAAe,IAAA,CAAK,GAAA,EAAK,CAAA;AAChD,EAAA,MAAM,gBAAA,GAAmB,OAAgB,IAAI,CAAA;AAC7C,EAAA,MAAM,YAAA,GAAe,OAA8B,IAAI,CAAA;AACvD,EAAA,MAAM,eAAA,GAAkB,OAAgB,KAAK,CAAA;AAC7C,EAAA,MAAM,eAAA,GAAkB,OAAY,IAAI,CAAA;AACxC,EAAA,MAAM,CAAC,cAAA,EAAgB,iBAAiB,CAAA,GAAI,SAAS,KAAK,CAAA;AAG1D,EAAA,MAAM,KAAA,uBAAY,GAAA,CAAI;AAAA,IACpB,QAAA;AAAA,IACA,eAAA;AAAA,IACA,eAAA;AAAA,IACA,aAAA;AAAA,IACA;AAAA,GACD,CAAA;AAGD,EAAA,MAAM,YAAA,GAAe,OAAA,CAAQ,YAAA,CAAa,MAAA,CAAO,CAAC,GAAA,KAAa;AAC7D,IAAA,MAAM,OAAA,GAAU,GAAA,CAAI,IAAA,IAAQ,GAAA,CAAI,IAAA;AAChC,IAAA,OAAO,KAAA,CAAM,IAAI,OAAO,CAAA;AAAA,EAC1B,CAAC,CAAA;AAED,EAAA,MAAM,IAAA,GAAO,YAAA,CAAa,GAAA,CAAI,CAAC,GAAA,KAAa;AAC1C,IAAA,MAAM,UAAU,GAAA,CAAI,QAAA,CAAS,GAAA,CAAI,CAAC,CAAC,CAAA,EAAG,CAAC,CAAA,MAAiB,EAAE,GAAG,CAAA,IAAK,CAAA,EAAG,GAAG,EAAE,CAAA,IAAK,IAAG,CAAE,CAAA;AACpF,IAAA,OAAO,OAAA;AAAA,EACT,CAAC,CAAA;AAED,EAAA,MAAM,sBAAA,GAAyB,YAAA,CAAa,GAAA,CAAI,CAAC,GAAA,MAAc;AAAA,IAC7D,IAAA,EAAO,GAAA,CAAI,IAAA,IAAQ,GAAA,CAAI,IAAA;AAAA,IACvB,SAAS,GAAA,CAAI,QAAA,CAAS,GAAA,CAAI,CAAC,CAAC,CAAA,EAAG,CAAC,CAAA,MAAiB,EAAE,GAAG,CAAA,IAAK,CAAA,EAAG,GAAG,EAAE,CAAA,IAAK,IAAG,CAAE;AAAA,GAC/E,CAAE,CAAA;AAGF,EAAA,MAAM,YAAA,GAAe,GAAA;AACrB,EAAA,MAAM,QAAA,GAAW;AAAA,IACf,CAAA,EAAG,YAAA;AAAA,IACH,CAAA,EAAG;AAAA,GACL;AAIA,EAAA,MAAM,MAAA,GAAS,KAAA,CAAM,OAAA,CAAQ,MAAM;AACjC,IAAA,MAAM,CAAA,GAAI,mBAAmB,IAAI,CAAA;AACjC,IAAA,OAAO,CAAA,GAAK,MAAA,CAAO,MAAA,CAAO,IAAA,CAAK,SAAS,CAAA,GAAK,CAAA;AAAA,EAC/C,CAAA,EAAG,CAAC,IAAI,CAAC,CAAA;AAIT,EAAA,MAAM,SAAA,GAAY;AAAA,IAChB,EAAA,EAAI,SAAS,CAAA,GAAI,CAAA;AAAA,IACjB,EAAA,EAAI,SAAS,CAAA,GAAI;AAAA,GACnB;AAGA,EAAA,MAAM,KAAA,GAAQ,CAAC,IAAA,KAAuB;AACpC,IAAA,IAAI,CAAC,IAAA,IAAQ,IAAA,CAAK,MAAA,KAAW,GAAG,OAAO,EAAA;AACvC,IAAA,MAAM,QAAQ,IAAA,CAAK,GAAA,CAAI,CAAC,CAAA,EAAG,MAAM,CAAA,EAAG,CAAA,KAAM,CAAA,GAAI,GAAA,GAAM,GAAG,CAAA,CAAA,EAAI,CAAA,CAAE,CAAC,CAAA,CAAA,EAAI,CAAA,CAAE,CAAC,CAAA,CAAE,CAAA;AACvE,IAAA,OAAO,KAAA,CAAM,IAAA,CAAK,GAAG,CAAA,GAAI,IAAA;AAAA,EAC3B,CAAA;AAGA,EAAA,MAAM,QAAA,GAAW,CAAC,IAAA,KAKZ;AAEJ,IAAA,IAAI,aAAa,OAAA,EAAS;AACxB,MAAA,YAAA,CAAa,aAAa,OAAO,CAAA;AACjC,MAAA,YAAA,CAAa,OAAA,GAAU,IAAA;AAAA,IACzB;AAGA,IAAA,MAAM,WAAW,OAAA,KAAY,MAAA,GACxB,YAAY,IAAA,CAAK,eAAA,GAAkB,IAAI,CAAA,GACxC,GAAA;AAEJ,IAAA,MAAM,SAAA,GAAY;AAAA,MAChB,GAAG,IAAA;AAAA,MACH,QAAA;AAAA,MACA,YAAY,OAAA,CAAQ,SAAA;AAAA,MACpB,QAAA,EAAU,OAAA;AAAA,MACV,0BAAA;AAAA,MACA,oBAAA,EAAsB,kBAAA;AAAA,MACtB,uBAAA,EAAyB,qBAAA;AAAA,MACzB,QAAA;AAAA,MACA;AAAA,KACF;AAEA,IAAA,IAAI,UAAA,EAAY;AACd,MAAA,UAAA,CAAW,SAAS,CAAA;AAAA,IACtB;AAAA,EACF,CAAA;AAGA,EAAA,MAAM,oBAAoB,MAAM;AAC9B,IAAA,IAAI,CAAC,iBAAiB,OAAA,EAAS;AAE7B,MAAA,MAAM,OAAA,GAAU,IAAA,CAAK,GAAA,EAAI,GAAI,cAAA,CAAe,OAAA;AAC5C,MAAA,eAAA,CAAgB,OAAA,GAAU,IAAA;AAC1B,MAAA,eAAA,CAAgB,OAAA,GAAU;AAAA,QACxB,eAAA,EAAiB,IAAA;AAAA,QACjB,EAAA,EAAI,GAAA;AAAA,QACJ,wBAAA,EAA0B,IAAA;AAAA,QAC1B,iBAAA,EAAmB;AAAA,OACrB;AACA,MAAA,QAAA,CAAS,gBAAgB,OAAO,CAAA;AAAA,IAClC,CAAA,MAAO;AAEL,MAAA,MAAM,EAAA,GAAK,IAAA,CAAK,GAAA,EAAI,GAAI,cAAA,CAAe,OAAA;AACvC,MAAA,gBAAA,CAAiB,OAAA,GAAU,KAAA;AAC3B,MAAA,iBAAA,CAAkB,IAAI,CAAA;AACtB,MAAA,eAAA,CAAgB,OAAA,GAAU,IAAA;AAC1B,MAAA,eAAA,CAAgB,OAAA,GAAU;AAAA,QACxB,eAAA,EAAiB,IAAA;AAAA,QACjB,EAAA;AAAA,QACA,wBAAA,EAA0B,KAAA;AAAA,QAC1B,iBAAA,EAAmB;AAAA,OACrB;AAAA,IAEF;AAAA,EACF,CAAA;AAGA,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,YAAA,CAAa,OAAA,GAAU,WAAW,MAAM;AACtC,MAAA,gBAAA,CAAiB,OAAA,GAAU,KAAA;AAG3B,MAAA,IAAI,eAAA,CAAgB,OAAA,IAAW,eAAA,CAAgB,OAAA,EAAS;AAEtD,QAAA,QAAA,CAAS,gBAAgB,OAAO,CAAA;AAAA,MAClC,CAAA,MAAO;AAEL,QAAA,QAAA,CAAS;AAAA,UACP,eAAA,EAAiB,KAAA;AAAA,UACjB,EAAA,EAAI,GAAA;AAAA,UACJ,wBAAA,EAA0B,KAAA;AAAA,UAC1B,iBAAA,EAAmB;AAAA,SACpB,CAAA;AAAA,MACH;AAAA,IACF,GAAG,QAAQ,CAAA;AAEX,IAAA,OAAO,MAAM;AACX,MAAA,IAAI,aAAa,OAAA,EAAS;AACxB,QAAA,YAAA,CAAa,aAAa,OAAO,CAAA;AAAA,MACnC;AAAA,IACF,CAAA;AAAA,EACF,CAAA,EAAG,EAAE,CAAA;AAGL,EAAA,MAAM,mBAAmB,MAAM;AAC7B,IAAA,IAAI,0BAAA,EAA4B;AAE9B,MAAA,MAAM,WAAW,sBAAA,CAAuB,GAAA,CAAI,CAAC,QAAA,KAAkB,SAAS,OAAO,CAAA;AAC/E,MAAA,MAAM,WAAA,GAAc,iCAAA,CAAkC,QAAA,EAAU,MAAA,EAAQ,SAAS,CAAA;AAEjF,MAAA,uBACE,KAAA,CAAA,aAAA,CAAC,GAAA,EAAA,EAAE,GAAA,EAAI,gBAAA,EAAiB,aAAA,EAAc,UACnC,WAAA,CAAY,GAAA,CAAI,CAAC,UAAA,EAAY,CAAA,KAAM;AAElC,QAAA,MAAM,QAAA,GAAW,uBAAuB,CAAC,CAAA;AACzC,QAAA,IAAI,SAAA,GAAY,OAAO,KAAA,CAAM,cAAA;AAE7B,QAAA,IAAI,kBAAA,IAAsB,QAAA,EAAU,IAAA,IAAQ,qBAAA,EAAuB;AAEjE,UAAA,MAAM,WAAA,GAAuC;AAAA,YAC3C,QAAA,EAAU,CAAA;AAAA,YACV,eAAA,EAAiB,CAAA;AAAA,YACjB,eAAA,EAAiB,CAAA;AAAA,YACjB,aAAA,EAAe,CAAA;AAAA,YACf,eAAA,EAAiB;AAAA,WACnB;AACA,UAAA,MAAM,cAAA,GAAiB,WAAA,CAAY,QAAA,CAAS,IAAe,CAAA;AAC3D,UAAA,IAAI,cAAA,KAAmB,MAAA,IAAa,qBAAA,CAAsB,cAAc,MAAM,MAAA,EAAW;AACvF,YAAA,MAAM,UAAA,GAAa,sBAAsB,cAAc,CAAA;AACvD,YAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,KAAA,CAAM,eAAA,CAAgB,UAAU,CAAA;AACrD,YAAA,IAAI,KAAA,EAAO;AACT,cAAA,SAAA,GAAY,KAAA;AAAA,YACd;AAAA,UACF;AAAA,QACF;AAEA,QAAA,2CACG,KAAA,CAAM,QAAA,EAAN,EAAe,GAAA,EAAK,CAAA,KAAA,EAAQ,CAAC,CAAA,CAAA,EAAA,kBAE5B,KAAA,CAAA,aAAA;AAAA,UAAC,MAAA;AAAA,UAAA;AAAA,YACC,CAAA,EAAG,MAAM,UAAU,CAAA;AAAA,YACnB,IAAA,EAAM,SAAA;AAAA,YACN,OAAA,EAAS,OAAO,OAAA,CAAQ,cAAA;AAAA,YACxB,MAAA,EAAO;AAAA;AAAA,SACT,kBAGA,KAAA,CAAA,aAAA;AAAA,UAAC,MAAA;AAAA,UAAA;AAAA,YACC,CAAA,EAAG,MAAM,UAAU,CAAA;AAAA,YACnB,IAAA,EAAK,MAAA;AAAA,YACL,MAAA,EAAQ,MAAA,CAAO,KAAA,CAAM,oBAAA,CAAqB,MAAA;AAAA,YAC1C,WAAA,EAAa,MAAA,CAAO,IAAA,CAAK,MAAA,CAAO;AAAA;AAAA,SAEpC,CAAA;AAAA,MAEJ,CAAC,CACH,CAAA;AAAA,IAEJ,CAAA,MAAO;AAEL,MAAA,MAAM,WAAA,GAAc,iCAAA,CAAkC,IAAA,EAAM,MAAA,EAAQ,SAAS,CAAA;AAE7E,MAAA,uBACE,KAAA,CAAA,aAAA,CAAC,GAAA,EAAA,EAAE,GAAA,EAAI,aAAA,EAAc,aAAA,EAAc,UAChC,WAAA,CAAY,GAAA,CAAI,CAAC,UAAA,EAAY,CAAA,KAAM;AAElC,QAAA,MAAM,QAAA,GAAW,uBAAuB,CAAC,CAAA;AACzC,QAAA,IAAI,SAAA,GAAY,OAAO,KAAA,CAAM,cAAA;AAE7B,QAAA,IAAI,kBAAA,IAAsB,QAAA,EAAU,IAAA,IAAQ,qBAAA,EAAuB;AAEjE,UAAA,MAAM,WAAA,GAAuC;AAAA,YAC3C,QAAA,EAAU,CAAA;AAAA,YACV,eAAA,EAAiB,CAAA;AAAA,YACjB,eAAA,EAAiB,CAAA;AAAA,YACjB,aAAA,EAAe,CAAA;AAAA,YACf,eAAA,EAAiB;AAAA,WACnB;AACA,UAAA,MAAM,cAAA,GAAiB,WAAA,CAAY,QAAA,CAAS,IAAe,CAAA;AAC3D,UAAA,IAAI,cAAA,KAAmB,MAAA,IAAa,qBAAA,CAAsB,cAAc,MAAM,MAAA,EAAW;AACvF,YAAA,MAAM,UAAA,GAAa,sBAAsB,cAAc,CAAA;AACvD,YAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,KAAA,CAAM,eAAA,CAAgB,UAAU,CAAA;AACrD,YAAA,IAAI,KAAA,EAAO;AACT,cAAA,SAAA,GAAY,KAAA;AAAA,YACd;AAAA,UACF;AAAA,QACF;AAEA,QAAA,uBACE,KAAA,CAAA,aAAA;AAAA,UAAC,MAAA;AAAA,UAAA;AAAA,YACC,GAAA,EAAK,OAAO,CAAC,CAAA,CAAA;AAAA,YACb,CAAA,EAAG,MAAM,UAAU,CAAA;AAAA,YACnB,IAAA,EAAM,SAAA;AAAA,YACN,OAAA,EAAS,OAAO,OAAA,CAAQ,cAAA;AAAA,YACxB,MAAA,EAAO;AAAA;AAAA,SACT;AAAA,MAEJ,CAAC,CACH,CAAA;AAAA,IAEJ;AAAA,EACF,CAAA;AAEA,EAAA,uBACE,KAAA,CAAA,aAAA,CAAC,SAAI,KAAA,EAAO;AAAA,IACV,OAAA,EAAS,MAAA;AAAA,IACT,aAAA,EAAe,QAAA;AAAA,IACf,UAAA,EAAY,QAAA;AAAA,IACZ,cAAA,EAAgB,QAAA;AAAA,IAChB,OAAA,EAAS,MAAA;AAAA,IACT,UAAA,EAAY;AAAA,OAGX,YAAA,oBACC,KAAA,CAAA,aAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,KAAA,EAAO;AAAA,QACL,QAAA,EAAU,OAAA;AAAA,QACV,KAAA,EAAO,MAAA;AAAA,QACP,YAAA,EAAc,MAAA;AAAA,QACd,SAAA,EAAW,QAAA;AAAA,QACX,QAAA,EAAU,MAAA;AAAA,QACV,UAAA,EAAY;AAAA,OACd;AAAA,MACA,uBAAA,EAAyB,EAAE,MAAA,EAAQ,YAAA;AAAa;AAAA,GAClD,kBAIF,KAAA,CAAA,aAAA,CAAC,KAAA,EAAA,EAAI,KAAA,EAAO;AAAA,IACV,OAAA,EAAS,MAAA;AAAA,IACT,aAAA,EAAe,QAAA;AAAA,IACf,UAAA,EAAY,QAAA;AAAA,IACZ,GAAA,EAAK;AAAA,GACP,EAAA,kBAEE,KAAA,CAAA,aAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,OAAO,QAAA,CAAS,CAAA;AAAA,MAChB,QAAQ,QAAA,CAAS,CAAA;AAAA,MACjB,SAAS,CAAA,IAAA,EAAO,QAAA,CAAS,CAAC,CAAA,CAAA,EAAI,SAAS,CAAC,CAAA,CAAA;AAAA,MACxC,KAAA,EAAO;AAAA,QACL,OAAA,EAAS,OAAA;AAAA,QACT,UAAA,EAAY,MAAA,CAAO,KAAA,CAAM,KAAA,CAAM,UAAA,CAAW,QAAA;AAAA,QAC1C,MAAA,EAAQ,CAAA,EAAG,MAAA,CAAO,IAAA,CAAK,MAAA,CAAO,MAAM,CAAA,SAAA,EAAY,MAAA,CAAO,KAAA,CAAM,KAAA,CAAM,UAAA,CAAW,MAAM,CAAA,CAAA;AAAA,QACpF,YAAA,EAAc;AAAA;AAChB,KAAA;AAAA,IAEC,gBAAA;AAAiB,GACpB,kBAGA,KAAA,CAAA,aAAA;AAAA,IAAC,QAAA;AAAA,IAAA;AAAA,MACC,SAAA,EAAU,aAAA;AAAA,MACV,OAAA,EAAS,iBAAA;AAAA,MACT,QAAA,EAAU,cAAA;AAAA,MACV,KAAA,EAAO;AAAA,QACL,OAAA,EAAS,WAAA;AAAA,QACT,QAAA,EAAU,MAAA;AAAA,QACV,MAAA,EAAQ,iBAAiB,aAAA,GAAgB,SAAA;AAAA,QACzC,OAAA,EAAS,iBAAiB,GAAA,GAAM;AAAA;AAClC,KAAA;AAAA,IAEC;AAAA,GAEL,CACF,CAAA;AAEJ;;AClXA,MAAM,IAAA,GAAO;AAAA,EACX,IAAA,EAAM,eAAA;AAAA,EACN,OAAA,EAAS,OAAA;AAAA,EACT,UAAA,EAAY;AAAA;AAAA,IAEV,OAAA,EAAS;AAAA,MACP,MAAM,aAAA,CAAc,OAAA;AAAA,MACpB,OAAA,EAAS,MAAA;AAAA,MACT,WAAA,EAAa;AAAA,KACf;AAAA;AAAA,IAEA,OAAA,EAAS;AAAA,MACP,MAAM,aAAA,CAAc,IAAA;AAAA,MACpB,OAAA,EAAS,MAAA;AAAA,MACT,WAAA,EAAa;AAAA,KACf;AAAA;AAAA,IAEA,0BAAA,EAA4B;AAAA,MAC1B,MAAM,aAAA,CAAc,IAAA;AAAA,MACpB,OAAA,EAAS,KAAA;AAAA,MACT,WAAA,EAAa;AAAA,KACf;AAAA;AAAA,IAEA,YAAA,EAAc;AAAA,MACZ,MAAM,aAAA,CAAc,MAAA;AAAA,MACpB,OAAA,EAAS,EAAA;AAAA,MACT,WAAA,EAAa;AAAA,KACf;AAAA;AAAA,IAEA,WAAA,EAAa;AAAA,MACX,MAAM,aAAA,CAAc,MAAA;AAAA,MACpB,OAAA,EAAS,mBAAA;AAAA,MACT,WAAA,EAAa;AAAA,KACf;AAAA;AAAA,IAEA,QAAA,EAAU;AAAA,MACR,MAAM,aAAA,CAAc,GAAA;AAAA,MACpB,OAAA,EAAS,GAAA;AAAA,MACT,WAAA,EAAa;AAAA,KACf;AAAA;AAAA,IAEA,oBAAA,EAAsB;AAAA,MACpB,MAAM,aAAA,CAAc,IAAA;AAAA,MACpB,OAAA,EAAS,KAAA;AAAA,MACT,WAAA,EAAa;AAAA,KACf;AAAA;AAAA,IAEA,uBAAA,EAAyB;AAAA,MACvB,MAAM,aAAA,CAAc,MAAA;AAAA,MACpB,SAAS,CAAC,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,GAAG,CAAC,CAAA;AAAA,MACvB,WAAA,EAAa;AAAA,KACf;AAAA;AAAA,IAEA,UAAA,EAAY;AAAA,MACV,MAAM,aAAA,CAAc,QAAA;AAAA,MACpB,OAAA,EAAS,MAAA;AAAA,MACT,WAAA,EAAa;AAAA;AACf,GACF;AAAA,EACA,IAAA,EAAM;AAAA;AAAA,IAEJ,eAAA,EAAiB;AAAA,MACf,MAAM,aAAA,CAAc,IAAA;AAAA,MACpB,WAAA,EAAa;AAAA,KACf;AAAA;AAAA,IAEA,EAAA,EAAI;AAAA,MACF,MAAM,aAAA,CAAc,GAAA;AAAA,MACpB,WAAA,EAAa;AAAA,KACf;AAAA;AAAA,IAEA,QAAA,EAAU;AAAA,MACR,MAAM,aAAA,CAAc,KAAA;AAAA,MACpB,WAAA,EAAa;AAAA,KACf;AAAA;AAAA,IAEA,wBAAA,EAA0B;AAAA,MACxB,MAAM,aAAA,CAAc,IAAA;AAAA,MACpB,WAAA,EAAa;AAAA,KACf;AAAA;AAAA,IAEA,iBAAA,EAAmB;AAAA,MACjB,MAAM,aAAA,CAAc,GAAA;AAAA,MACpB,WAAA,EAAa;AAAA;AACf,GACF;AAAA,EACA,SAAA,EAAW;AACb,CAAA;AAaA,MAAM,kBAAA,CAAkD;AAAA,EAGtD,YAAoB,OAAA,EAAkB;AAAlB,IAAA,IAAA,CAAA,OAAA,GAAA,OAAA;AAAA,EAAmB;AAAA,EAFvC;AAAA,IAAA,IAAA,CAAO,IAAA,GAAO,IAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQd,KAAA,CAAM,iBAA8B,KAAA,EAAwB;AAE1D,IAAA,MAAM,iBAAA,GAAoB,CAAC,IAAA,KAAc;AAEvC,MAAA,IAAI,MAAM,UAAA,EAAY;AACpB,QAAA,KAAA,CAAM,WAAW,IAAI,CAAA;AAAA,MACvB;AAGA,MAAA,MAAM,eAAgB,eAAA,CAAwB,cAAA;AAC9C,MAAA,IAAI,cAAc,IAAA,EAAM;AACtB,QAAA,YAAA,CAAa,KAAK,OAAA,EAAQ;AAAA,MAC5B;AAGA,MAAA,eAAA,CAAgB,SAAA,GAAY,EAAA;AAG5B,MAAA,IAAA,CAAK,OAAA,CAAQ,YAAY,IAAI,CAAA;AAAA,IAC/B,CAAA;AAGA,IAAA,MAAM,MAAA,GAAgC;AAAA,MACpC,SAAS,KAAA,CAAM,OAAA;AAAA,MACf,SAAS,KAAA,CAAM,OAAA;AAAA,MACf,4BAA4B,KAAA,CAAM,0BAAA;AAAA,MAClC,cAAc,KAAA,CAAM,YAAA;AAAA,MACpB,aAAa,KAAA,CAAM,WAAA;AAAA,MACnB,UAAU,KAAA,CAAM,QAAA;AAAA,MAChB,oBAAoB,KAAA,CAAM,oBAAA;AAAA,MAC1B,uBAAuB,KAAA,CAAM,uBAAA;AAAA,MAC7B,UAAA,EAAY;AAAA,KACd;AAGA,IAAA,MAAM,EAAE,IAAA,EAAM,eAAA,EAAiB,OAAA,EAAS,OAAA,KAAY,eAAA,CAAgB,eAAA,EAAiB,MAAA,EAAQ,IAAA,CAAK,OAAO,CAAA;AAGzG,IAAC,OAAA,CAAgB,cAAA,GAAiB,EAAE,IAAA,EAAM,OAAA,EAAQ;AAAA,EACpD;AACF;;;;"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../src/core/engine/geometry/bounds.ts","../../src/core/config/config.ts","../../src/core/engine/geometry/pieces.ts","../../src/plugins/tangram-nback/NBackApp.tsx","../../src/plugins/tangram-nback/index.ts"],"sourcesContent":["import type { Poly, Vec, PrimitiveBlueprint, CompositeBlueprint, Blueprint } from \"@/core/domain/types\";\n\nexport type AABB = { min: Vec; max: Vec; width: number; height: number };\n\n/** AABB of a single polygon (numeric). */\nexport function aabbOf(poly: Poly): { minX: number; minY: number; maxX: number; maxY: number } {\n let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\n for (const p of poly) {\n if (p.x < minX) minX = p.x;\n if (p.y < minY) minY = p.y;\n if (p.x > maxX) maxX = p.x;\n if (p.y > maxY) maxY = p.y;\n }\n return { minX, minY, maxX, maxY };\n}\n\n/** AABB of one or more polygons with width/height and center. */\nexport function polysAABB(polys: Poly[]) {\n let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\n for (const poly of polys) {\n for (const p of poly) {\n if (p.x < minX) minX = p.x;\n if (p.y < minY) minY = p.y;\n if (p.x > maxX) maxX = p.x;\n if (p.y > maxY) maxY = p.y;\n }\n }\n const width = Math.max(1, maxX - minX);\n const height = Math.max(1, maxY - minY);\n return { min: { x: minX, y: minY }, max: { x: maxX, y: maxY }, width, height, cx:(minX+maxX)/2, cy:(minY+maxY)/2 };\n}\n\n/** Bounds of multiple polygons at the origin. */\nexport function boundsOfShapes(shapes: Poly[]): AABB {\n let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\n for (const poly of shapes) {\n for (const p of poly) {\n if (p.x < minX) minX = p.x;\n if (p.y < minY) minY = p.y;\n if (p.x > maxX) maxX = p.x;\n if (p.y > maxY) maxY = p.y;\n }\n }\n const width = Math.max(0, maxX - minX);\n const height = Math.max(0, maxY - minY);\n return { min: { x: minX, y: minY }, max: { x: maxX, y: maxY }, width, height };\n}\n\n/** Bounds of a primitive blueprint (uses its canonical shape). */\nexport function boundsOfPrimitive(bp: PrimitiveBlueprint): AABB {\n return boundsOfShapes(bp.shape);\n}\n\n/**\n * Bounds of a composite blueprint.\n * If `shape` is provided as precomputed union polygons, we use that.\n * Otherwise we bound the parts’ primitives at their offsets (loose but safe).\n */\nexport function boundsOfComposite(\n bp: CompositeBlueprint,\n primitiveLookup: (kind: string) => PrimitiveBlueprint | undefined\n): AABB {\n if (bp.shape && bp.shape.length) return boundsOfShapes(bp.shape);\n\n let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\n for (const part of bp.parts) {\n const prim = primitiveLookup(part.kind);\n if (!prim) continue;\n const b = boundsOfPrimitive(prim);\n const pMinX = part.offset.x + b.min.x;\n const pMinY = part.offset.y + b.min.y;\n const pMaxX = part.offset.x + b.max.x;\n const pMaxY = part.offset.y + b.max.y;\n if (pMinX < minX) minX = pMinX;\n if (pMinY < minY) minY = pMinY;\n if (pMaxX > maxX) maxX = pMaxX;\n if (pMaxY > maxY) maxY = pMaxY;\n }\n const width = Math.max(0, maxX - minX);\n const height = Math.max(0, maxY - minY);\n return { min: { x: minX, y: minY }, max: { x: maxX, y: maxY }, width, height };\n}\n\n/** Unified bounds helper. */\nexport function boundsOfBlueprint(\n bp: Blueprint,\n primitiveLookup: (kind: string) => PrimitiveBlueprint | undefined\n): AABB {\n if (\"parts\" in bp) return boundsOfComposite(bp, primitiveLookup);\n return boundsOfPrimitive(bp);\n}\n","// src/core/config/config.ts\nexport type Config = {\n color: {\n background: string;\n bands: {\n silhouette: { fillEven: string; fillOdd: string; stroke: string };\n workspace: { fillEven: string; fillOdd: string; stroke: string };\n };\n completion: { fill: string; stroke: string };\n silhouetteMask: string;\n anchors: { invalid: string; valid: string };\n piece: { draggingFill: string; validFill: string; invalidFill: string; invalidStroke: string; selectedStroke: string; allGreenStroke: string; borderStroke: string };\n ui: { light: string; dark: string };\n blueprint: { fill: string; selectedStroke: string; badgeFill: string; labelFill: string };\n tangramDecomposition: { stroke: string };\n primitiveColors: string[];\n };\n opacity: {\n blueprint: number;\n silhouetteMask: number;\n anchors: { invalid: number; valid: number };\n piece: { invalid: number; dragging: number; locked: number; normal: number };\n };\n size: {\n stroke: { bandPx: number; pieceSelectedPx: number; allGreenStrokePx: number; pieceBorderPx: number; tangramDecompositionPx: number };\n anchorRadiusPx: { valid: number; invalid: number };\n badgeFontPx: number;\n centerBadge: { fractionOfOuterR: number; minPx: number; marginPx: number };\n invalidMarker: { sizePx: number; strokePx: number };\n };\n layout: {\n grid: { stepPx: number; unitPx: number };\n paddingPx: number;\n viewportScale: number;\n /** renamed from capacity → constraints */\n constraints: {\n workspaceDiamAnchors: number;\n quickstashDiamAnchors: number;\n primitiveDiamAnchors: number;\n };\n defaults: { maxQuickstashSlots: number };\n };\n game: {\n snapRadiusPx: number;\n showBorders: boolean;\n hideTouchingBorders: boolean;\n silhouettesBelowPieces: boolean;\n };\n};\n\nexport const CONFIG: Config = {\n color: {\n background: \"#fff7e0ff\",\n bands: {\n silhouette: { fillEven: \"#ffffff\", fillOdd: \"#ffffff\", stroke: \"#b1b1b1\" },\n workspace: { fillEven: \"#ffffff\", fillOdd: \"#ffffff\", stroke: \"#b1b1b1\" }\n },\n completion: { fill: \"#ccffcc\", stroke: \"#13da57\" },\n silhouetteMask: \"#374151\",\n anchors: { invalid: \"#7dd3fc\", valid: \"#475569\" },\n // validFill used here for placed composites\n piece: { draggingFill: \"#8e7cc3\", validFill: \"#8e7cc3\", invalidFill: \"#d55c00\", invalidStroke: \"#dc2626\", selectedStroke: \"#674ea7\", allGreenStroke: \"#86efac\", borderStroke: \"#674ea7\" },\n ui: { light: \"#60a5fa\", dark: \"#1d4ed8\" },\n blueprint: { fill: \"#374151\", selectedStroke: \"#111827\", badgeFill: \"#000000\", labelFill: \"#ffffff\" },\n tangramDecomposition: { stroke: \"#fef2cc\" },\n primitiveColors: [ // from seaborn \"colorblind\" palette, 6 colors, with red omitted\n '#0173b2',\n '#de8f05',\n '#029e73',\n '#cc78bc',\n '#ca9161'\n ]\n },\n opacity: {\n blueprint: 0.6,\n silhouetteMask: 0.25,\n //anchors: { valid: 0.80, invalid: 0.50 },\n anchors: { invalid: 0.0, valid: 0.0 },\n piece: { invalid: 1, dragging: 1, locked: 1, normal: 1 },\n },\n size: {\n stroke: { bandPx: 5, pieceSelectedPx: 5, allGreenStrokePx: 10, pieceBorderPx: 2, tangramDecompositionPx: 1 },\n anchorRadiusPx: { valid: 1.0, invalid: 1.0 },\n badgeFontPx: 16,\n centerBadge: { fractionOfOuterR: 0.15, minPx: 20, marginPx: 4 },\n invalidMarker: { sizePx: 10, strokePx: 4 }\n },\n layout: {\n grid: { stepPx: 20, unitPx: 40 },\n paddingPx: 1,\n viewportScale: 0.8,\n constraints: {\n workspaceDiamAnchors: 10, // num anchors req'd to be on diagonal\n quickstashDiamAnchors: 7, // num anchors req'd to be in single quickstash slot\n primitiveDiamAnchors: 5,\n },\n defaults: { maxQuickstashSlots: 1 }\n },\n game: {\n snapRadiusPx: 15,\n showBorders: false,\n hideTouchingBorders: true,\n silhouettesBelowPieces: true\n }\n};","/**\n * Game-specific piece geometry operations\n * Includes: piece positioning, placement, grid snapping, and anchor generation\n *\n * Consolidated from: piece.ts, placement.ts, grid.ts, anchors.ts\n */\n\nimport type { Blueprint, Vec, Poly, Anchor } from \"@/core/domain/types\";\nimport type { CircleLayout, SectorGeom } from \"@/core/domain/layout\";\nimport { polysAABB } from \"./bounds\";\nimport { pointInPolygon, dist2 } from \"./math\";\nimport { CONFIG } from \"@/core/config/config\";\n\nconst GRID_PX = CONFIG.layout.grid.stepPx;\n\n// ===== Piece Positioning (from piece.ts) =====\n\n/** Translate blueprint polys to world coords for a given top-left (TL). */\nexport function piecePolysAt(\n bp: Blueprint,\n bb: { min: { x: number; y: number } },\n tl: { x: number; y: number }\n) {\n const ox = tl.x - bb.min.x;\n const oy = tl.y - bb.min.y;\n const polys = (\"shape\" in bp && bp.shape) ? bp.shape : [];\n return polys.map(poly => poly.map(p => ({ x: p.x + ox, y: p.y + oy })));\n}\n\n/** Build support offsets: each vertex relative to the AABB center. */\nexport function computeSupportOffsets(\n bp: Blueprint,\n bb: { min: { x: number; y: number }; width: number; height: number }\n) {\n const cx = bb.min.x + bb.width / 2;\n const cy = bb.min.y + bb.height / 2;\n\n const polys = (\"shape\" in bp && bp.shape && bp.shape.length)\n ? bp.shape\n : [[\n { x: bb.min.x, y: bb.min.y },\n { x: bb.min.x + bb.width, y: bb.min.y },\n { x: bb.min.x + bb.width, y: bb.min.y + bb.height },\n { x: bb.min.x, y: bb.min.y + bb.height },\n ]];\n\n const offs: Vec[] = [];\n for (const poly of polys) for (const v of poly) offs.push({ x: v.x - cx, y: v.y - cy });\n return offs;\n}\n\n/** Clamp using directional vertex support → exact contact with the ring. */\nexport function clampTopLeftBySupport(\n tlx: number,\n tly: number,\n d: {\n aabb: { width: number; height: number };\n support: { x: number; y: number }[];\n },\n layout: CircleLayout,\n target: \"workspace\" | \"silhouette\",\n pointerInsideCenter: boolean\n) {\n const cx0 = tlx + d.aabb.width / 2;\n const cy0 = tly + d.aabb.height / 2;\n\n const vx = cx0 - layout.cx;\n const vy = cy0 - layout.cy;\n const r = Math.hypot(vx, vy);\n if (r === 0) return { x: tlx, y: tly };\n\n const ux = vx / r, uy = vy / r;\n\n let h_out = -Infinity, h_in = -Infinity;\n for (const o of d.support) {\n const outProj = o.x * ux + o.y * uy;\n if (outProj > h_out) h_out = outProj;\n const inProj = -(o.x * ux + o.y * uy);\n if (inProj > h_in) h_in = inProj;\n }\n\n if (target === \"workspace\") {\n const [rIn, rOut] = layout.bands.workspace;\n const minR = pointerInsideCenter ? 0 : (rIn + h_in);\n const maxR = rOut - h_out;\n const rClamped = Math.min(Math.max(r, minR), maxR);\n if (Math.abs(rClamped - r) < 1e-6) return { x: tlx, y: tly };\n const newCx = layout.cx + rClamped * ux;\n const newCy = layout.cy + rClamped * uy;\n return { x: newCx - d.aabb.width / 2, y: newCy - d.aabb.height / 2 };\n } else {\n const rOut = layout.bands.silhouette[1];\n const maxR = rOut - h_out;\n if (r <= maxR) return { x: tlx, y: tly };\n const newCx = layout.cx + maxR * ux;\n const newCy = layout.cy + maxR * uy;\n return { x: newCx - d.aabb.width / 2, y: newCy - d.aabb.height / 2 };\n }\n}\n\n// ===== Placement (from placement.ts) =====\n\n/** gcd for positive integers */\nfunction igcd(a: number, b: number) {\n a = Math.round(Math.abs(a)); b = Math.round(Math.abs(b));\n while (b) [a, b] = [b, a % b];\n return a || 1;\n}\n\n/** Infer the base lattice unit in raw silhouette coordinates. */\nexport function inferUnitFromPolys(polys: Poly[]): number {\n let g = 0;\n for (const poly of polys) {\n for (let i = 0; i < poly.length; i++) {\n const a = poly[i], b = poly[(i + 1) % poly.length];\n if (!a || !b) continue;\n const dx = Math.round(Math.abs(b.x - a.x));\n const dy = Math.round(Math.abs(b.y - a.y));\n if (dx) g = g ? igcd(g, dx) : dx;\n if (dy) g = g ? igcd(g, dy) : dy;\n }\n }\n return g || 1;\n}\n\n/**\n * Place polys using scale S, re-centered, and translation snapped to GRID_PX; returns polys.\n * The center is snapped to the grid so the lattice points under the silhouette stay invariant.\n */\nexport function placeSilhouetteGridAlignedAsPolys(\n polys: Poly[],\n S: number,\n rectCenter: { cx: number; cy: number }\n): Poly[] {\n if (!polys || polys.length === 0) return [];\n const a = polysAABB(polys);\n const cx0 = (a.min.x + a.max.x) / 2;\n const cy0 = (a.min.y + a.max.y) / 2;\n const cx = Math.round(rectCenter.cx / GRID_PX) * GRID_PX;\n const cy = Math.round(rectCenter.cy / GRID_PX) * GRID_PX;\n const tx = cx - S * cx0;\n const ty = cy - S * cy0;\n const stx = Math.round(tx / GRID_PX) * GRID_PX;\n const sty = Math.round(ty / GRID_PX) * GRID_PX;\n return polys.map(poly => poly.map(p => ({ x: S * p.x + stx, y: S * p.y + sty })));\n}\n\n// ===== Grid Operations (from grid.ts) =====\n\nexport function nearestGridNode(p: Vec): Vec {\n return { x: Math.round(p.x / GRID_PX) * GRID_PX, y: Math.round(p.y / GRID_PX) * GRID_PX };\n}\n\n/** Generate grid nodes inside an AABB. */\nexport function gridNodesInAABB(min: Vec, max: Vec): Vec[] {\n const out: Vec[] = [];\n const x0 = Math.ceil(min.x / GRID_PX) * GRID_PX;\n const y0 = Math.ceil(min.y / GRID_PX) * GRID_PX;\n for (let y = y0; y <= max.y; y += GRID_PX) {\n for (let x = x0; x <= max.x; x += GRID_PX) out.push({ x, y });\n }\n return out;\n}\n\n/** Keep nodes in a band (and optional sector wedge). */\nexport function filterNodesToBandAndSector(\n nodes: Vec[],\n layout: CircleLayout,\n band: \"workspace\" | \"silhouette\",\n sector?: SectorGeom\n): Vec[] {\n const [rIn, rOut] = layout.bands[band];\n const out: Vec[] = [];\n for (const n of nodes) {\n const dx = n.x - layout.cx, dy = n.y - layout.cy;\n const r = Math.hypot(dx, dy);\n if (r < rIn || r > rOut) continue;\n\n if (sector) {\n let theta = Math.atan2(dy, dx);\n if (layout.mode === \"circle\") {\n if (theta < -Math.PI / 2) theta += 2 * Math.PI;\n } else {\n if (theta < Math.PI) theta += 2 * Math.PI;\n }\n if (theta < sector.start || theta >= sector.end) continue;\n }\n out.push(n);\n }\n return out;\n}\n\n/** Keep nodes that lie inside any of the polygons. */\nexport function filterNodesInPolys(\n nodes: Vec[],\n polys: Poly[],\n pointInPolyFn: (pt: Vec, poly: Poly) => boolean = pointInPolygon\n): Vec[] {\n const out: Vec[] = [];\n node: for (const n of nodes) {\n for (const poly of polys) {\n if (pointInPolyFn(n, poly)) { out.push(n); continue node; }\n }\n }\n return out;\n}\n\n// ===== Anchor Operations (from anchors.ts) =====\n\n/**\n * Find the nearest anchor that \"accepts\" a piece signature (kind/compositeSignature).\n * Returns null if none within the optional radius.\n */\nexport function nearestAcceptingAnchor(\n anchors: Anchor[] | undefined,\n point: Vec,\n accepts: { kind?: string; compositeSignature?: string },\n withinPx: number = Infinity\n): { anchor: Anchor; d2: number } | null {\n if (!anchors || !anchors.length) return null;\n\n const { kind, compositeSignature } = accepts;\n const r2 = withinPx === Infinity ? Infinity : withinPx * withinPx;\n\n let best: { anchor: Anchor; d2: number } | null = null;\n for (const a of anchors) {\n const ok = a.accepts.some(acc =>\n (acc.kind === undefined || acc.kind === kind) &&\n (acc.compositeSignature === undefined || acc.compositeSignature === compositeSignature)\n );\n if (!ok) continue;\n\n const d2val = dist2(point, a.position);\n if (d2val <= r2 && (!best || d2val < best.d2)) {\n best = { anchor: a, d2: d2val };\n }\n }\n return best;\n}\n\n/** True if `point` is within `withinPx` of the given anchor. */\nexport function withinAnchor(anchor: Anchor, point: Vec, withinPx: number): boolean {\n const r2 = withinPx * withinPx;\n return dist2(point, anchor.position) <= r2;\n}\n\n// Node-grid helpers for anchor generation\nexport function workspaceNodes(layout: CircleLayout, sector: SectorGeom): Vec[] {\n const pad = 6;\n const min = { x: layout.cx - layout.outerR, y: layout.cy - layout.outerR - pad };\n const max = { x: layout.cx + layout.outerR, y: layout.cy + layout.outerR + pad };\n const nodes = gridNodesInAABB(min, max);\n return filterNodesToBandAndSector(nodes, layout, \"workspace\", sector);\n}\n\nexport function silhouetteNodes(layout: CircleLayout, sector: SectorGeom, fittedMask: Poly[]): Vec[] {\n const pad = 6;\n const min = { x: layout.cx - layout.outerR, y: layout.cy - layout.outerR - pad };\n const max = { x: layout.cx + layout.outerR, y: layout.cy + layout.outerR + pad };\n const nodes = gridNodesInAABB(min, max);\n const banded = filterNodesToBandAndSector(nodes, layout, \"silhouette\", sector);\n return filterNodesInPolys(banded, fittedMask);\n}\n\nexport function silhouetteBandNodes(layout: CircleLayout, sector: SectorGeom): Vec[] {\n const pad = 6;\n const min = { x: layout.cx - layout.outerR, y: layout.cy - layout.outerR - pad };\n const max = { x: layout.cx + layout.outerR, y: layout.cy + layout.outerR + pad };\n const nodes = gridNodesInAABB(min, max);\n return filterNodesToBandAndSector(nodes, layout, \"silhouette\", sector);\n}\n\n/** Generate anchor grid nodes for the inner ring (radius < innerR) */\nexport function innerRingNodes(layout: CircleLayout): Vec[] {\n const pad = 6;\n const min = { x: layout.cx - layout.innerR, y: layout.cy - layout.innerR - pad };\n const max = { x: layout.cx + layout.innerR, y: layout.cy + layout.innerR + pad };\n const nodes = gridNodesInAABB(min, max);\n // Filter to nodes within the inner circle\n const out: Vec[] = [];\n for (const n of nodes) {\n const dx = n.x - layout.cx, dy = n.y - layout.cy;\n const r = Math.hypot(dx, dy);\n if (r < layout.innerR) out.push(n);\n }\n return out;\n}\n\nexport const anchorsDiameterToPx = (anchorsDiag: number, gridPx: number = GRID_PX) =>\n anchorsDiag * Math.SQRT2 * gridPx;\n","/**\n * NBackApp.tsx - React wrapper for tangram n-back matching trials\n *\n * This component handles the React rendering logic for n-back trials,\n * displaying a single tangram silhouette with a response button.\n */\n\nimport React, { useRef, useEffect, useState } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport { JsPsych } from \"jspsych\";\nimport type { Poly, TanKind } from \"../../core/domain/types\";\nimport { placeSilhouetteGridAlignedAsPolys, inferUnitFromPolys } from \"../../core/engine/geometry\";\nimport { CONFIG } from \"../../core/config/config\";\n\nexport interface StartNBackTrialParams {\n tangram: any;\n isMatch?: boolean;\n show_tangram_decomposition?: boolean;\n instructions?: string;\n button_text?: string;\n duration: number;\n usePrimitiveColors?: boolean;\n primitiveColorIndices?: number[];\n onTrialEnd?: (data: any) => void;\n}\n\n/**\n * Start an n-back trial by rendering the NBackView component\n */\nexport function startNBackTrial(\n display_element: HTMLElement,\n params: StartNBackTrialParams,\n _jsPsych: JsPsych\n) {\n // Create React root and render NBackView\n const root = createRoot(display_element);\n root.render(React.createElement(NBackView, { params }));\n\n return { root, display_element, jsPsych: _jsPsych };\n}\n\ninterface NBackViewProps {\n params: StartNBackTrialParams;\n}\n\nfunction NBackView({ params }: NBackViewProps) {\n const {\n tangram,\n isMatch,\n show_tangram_decomposition,\n instructions,\n button_text,\n duration,\n usePrimitiveColors,\n primitiveColorIndices,\n onTrialEnd\n } = params;\n\n // Timing and response tracking\n const trialStartTime = useRef<number>(Date.now());\n const buttonEnabledRef = useRef<boolean>(true);\n const timeoutIdRef = useRef<NodeJS.Timeout | null>(null);\n const hasRespondedRef = useRef<boolean>(false);\n const responseDataRef = useRef<any>(null);\n const [buttonDisabled, setButtonDisabled] = useState(false);\n\n // Canonical piece names\n const CANON = new Set([\n \"square\",\n \"smalltriangle\",\n \"parallelogram\",\n \"medtriangle\",\n \"largetriangle\",\n ]);\n\n // Convert TangramSpec to internal format\n const filteredTans = tangram.solutionTans.filter((tan: any) => {\n const tanName = tan.name ?? tan.kind;\n return CANON.has(tanName);\n });\n\n const mask = filteredTans.map((tan: any) => {\n const polygon = tan.vertices.map(([x, y]: number[]) => ({ x: x ?? 0, y: -(y ?? 0) }));\n return polygon;\n });\n\n const primitiveDecomposition = filteredTans.map((tan: any) => ({\n kind: (tan.name ?? tan.kind) as TanKind,\n polygon: tan.vertices.map(([x, y]: number[]) => ({ x: x ?? 0, y: -(y ?? 0) }))\n }));\n\n // Use FIXED viewport size for n-back display (constant across all tangrams)\n const DISPLAY_SIZE = 400;\n const viewport = {\n w: DISPLAY_SIZE,\n h: DISPLAY_SIZE\n };\n\n // Compute scale factor to keep tangram pieces at constant physical size\n // This matches the approach in TangramConstructPlugin/GameBoard\n const scaleS = React.useMemo(() => {\n const u = inferUnitFromPolys(mask);\n return u ? (CONFIG.layout.grid.unitPx / u) : 1;\n }, [mask]);\n\n // For n-back display, we want tangrams centered in viewport\n // Don't use computeCircleLayout's positioning - just center manually\n const centerPos = {\n cx: viewport.w / 2,\n cy: viewport.h / 2\n };\n\n // Helper to convert polygon to SVG path\n const pathD = (poly: Poly): string => {\n if (!poly || poly.length === 0) return \"\";\n const moves = poly.map((p, i) => `${i === 0 ? \"M\" : \"L\"} ${p.x} ${p.y}`);\n return moves.join(\" \") + \" Z\";\n };\n\n // End trial with data\n const endTrial = (data: {\n responded_match: boolean;\n rt: number;\n responded_after_duration: boolean;\n rt_after_duration: number;\n }) => {\n // Clear timeout if it exists\n if (timeoutIdRef.current) {\n clearTimeout(timeoutIdRef.current);\n timeoutIdRef.current = null;\n }\n\n // Compute accuracy\n const accuracy = isMatch !== undefined\n ? (isMatch === data.responded_match ? 1 : 0)\n : NaN;\n\n const trialData = {\n ...data,\n accuracy,\n tangram_id: tangram.tangramID,\n is_match: isMatch,\n show_tangram_decomposition: show_tangram_decomposition,\n use_primitive_colors: usePrimitiveColors,\n primitive_color_indices: primitiveColorIndices,\n duration: duration,\n button_text: button_text\n };\n\n if (onTrialEnd) {\n onTrialEnd(trialData);\n }\n };\n\n // Handle button click\n const handleButtonClick = () => {\n if (!buttonEnabledRef.current) {\n // Late response after duration expired\n const rt_late = Date.now() - trialStartTime.current;\n hasRespondedRef.current = true;\n responseDataRef.current = {\n responded_match: true,\n rt: NaN,\n responded_after_duration: true,\n rt_after_duration: rt_late\n };\n endTrial(responseDataRef.current);\n } else {\n // Response before duration expired - disable button but continue showing trial\n const rt = Date.now() - trialStartTime.current;\n buttonEnabledRef.current = false;\n setButtonDisabled(true);\n hasRespondedRef.current = true;\n responseDataRef.current = {\n responded_match: true,\n rt,\n responded_after_duration: false,\n rt_after_duration: NaN\n };\n // Don't end trial yet - wait for duration timeout\n }\n };\n\n // Set up duration timeout\n useEffect(() => {\n timeoutIdRef.current = setTimeout(() => {\n buttonEnabledRef.current = false;\n\n // End trial with appropriate data\n if (hasRespondedRef.current && responseDataRef.current) {\n // User already responded - use saved response data\n endTrial(responseDataRef.current);\n } else {\n // No response - end with no-response data\n endTrial({\n responded_match: false,\n rt: NaN,\n responded_after_duration: false,\n rt_after_duration: NaN\n });\n }\n }, duration);\n\n return () => {\n if (timeoutIdRef.current) {\n clearTimeout(timeoutIdRef.current);\n }\n };\n }, []);\n\n // Render silhouette\n const renderSilhouette = () => {\n if (show_tangram_decomposition) {\n // Render decomposed primitives with borders\n const rawPolys = primitiveDecomposition.map((primInfo: any) => primInfo.polygon);\n const placedPolys = placeSilhouetteGridAlignedAsPolys(rawPolys, scaleS, centerPos);\n\n return (\n <g key=\"sil-decomposed\" pointerEvents=\"none\">\n {placedPolys.map((scaledPoly, i) => {\n // Get color for this primitive based on its kind\n const primInfo = primitiveDecomposition[i];\n let fillColor = CONFIG.color.silhouetteMask;\n\n if (usePrimitiveColors && primInfo?.kind && primitiveColorIndices) {\n // Map primitive kind to index\n const kindToIndex: Record<TanKind, number> = {\n 'square': 0,\n 'smalltriangle': 1,\n 'parallelogram': 2,\n 'medtriangle': 3,\n 'largetriangle': 4\n };\n const primitiveIndex = kindToIndex[primInfo.kind as TanKind];\n if (primitiveIndex !== undefined && primitiveColorIndices[primitiveIndex] !== undefined) {\n const colorIndex = primitiveColorIndices[primitiveIndex];\n const color = CONFIG.color.primitiveColors[colorIndex];\n if (color) {\n fillColor = color;\n }\n }\n }\n\n return (\n <React.Fragment key={`prim-${i}`}>\n {/* Fill path */}\n <path\n d={pathD(scaledPoly)}\n fill={fillColor}\n opacity={usePrimitiveColors ? CONFIG.opacity.piece.normal : CONFIG.opacity.silhouetteMask}\n stroke=\"none\"\n />\n\n {/* Full perimeter border */}\n <path\n d={pathD(scaledPoly)}\n fill=\"none\"\n stroke={CONFIG.color.tangramDecomposition.stroke}\n strokeWidth={CONFIG.size.stroke.tangramDecompositionPx}\n />\n </React.Fragment>\n );\n })}\n </g>\n );\n } else {\n // Render unified silhouette\n const placedPolys = placeSilhouetteGridAlignedAsPolys(mask, scaleS, centerPos);\n\n return (\n <g key=\"sil-unified\" pointerEvents=\"none\">\n {placedPolys.map((scaledPoly, i) => {\n // Get color for this primitive based on its kind\n const primInfo = primitiveDecomposition[i];\n let fillColor = CONFIG.color.silhouetteMask;\n\n if (usePrimitiveColors && primInfo?.kind && primitiveColorIndices) {\n // Map primitive kind to index\n const kindToIndex: Record<TanKind, number> = {\n 'square': 0,\n 'smalltriangle': 1,\n 'parallelogram': 2,\n 'medtriangle': 3,\n 'largetriangle': 4\n };\n const primitiveIndex = kindToIndex[primInfo.kind as TanKind];\n if (primitiveIndex !== undefined && primitiveColorIndices[primitiveIndex] !== undefined) {\n const colorIndex = primitiveColorIndices[primitiveIndex];\n const color = CONFIG.color.primitiveColors[colorIndex];\n if (color) {\n fillColor = color;\n }\n }\n }\n\n return (\n <path\n key={`sil-${i}`}\n d={pathD(scaledPoly)}\n fill={fillColor}\n opacity={usePrimitiveColors ? CONFIG.opacity.piece.normal : CONFIG.opacity.silhouetteMask}\n stroke=\"none\"\n />\n );\n })}\n </g>\n );\n }\n };\n\n return (\n <div style={{\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n padding: \"20px\",\n background: \"#fff7e0ff\"\n }}>\n {/* Instructions */}\n {instructions && (\n <div\n style={{\n maxWidth: \"800px\",\n width: \"100%\",\n marginBottom: \"30px\",\n textAlign: \"center\",\n fontSize: \"18px\",\n lineHeight: \"1.5\"\n }}\n dangerouslySetInnerHTML={{ __html: instructions }}\n />\n )}\n\n {/* Container for SVG and button */}\n <div style={{\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n gap: \"30px\"\n }}>\n {/* SVG with tangram silhouette */}\n <svg\n width={viewport.w}\n height={viewport.h}\n viewBox={`0 0 ${viewport.w} ${viewport.h}`}\n style={{\n display: \"block\",\n background: CONFIG.color.bands.silhouette.fillEven,\n border: `${CONFIG.size.stroke.bandPx}px solid ${CONFIG.color.bands.silhouette.stroke}`,\n borderRadius: \"8px\"\n }}\n >\n {renderSilhouette()}\n </svg>\n\n {/* Response button */}\n <button\n className=\"jspsych-btn\"\n onClick={handleButtonClick}\n disabled={buttonDisabled}\n style={{\n padding: \"12px 30px\",\n fontSize: \"16px\",\n cursor: buttonDisabled ? \"not-allowed\" : \"pointer\",\n opacity: buttonDisabled ? 0.5 : 1\n }}\n >\n {button_text}\n </button>\n </div>\n </div>\n );\n}\n","import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from \"jspsych\";\nimport { startNBackTrial, StartNBackTrialParams } from \"./NBackApp\";\n\nconst info = {\n name: \"tangram-nback\",\n version: \"1.0.0\",\n parameters: {\n /** Single tangram specification to display */\n tangram: {\n type: ParameterType.COMPLEX,\n default: undefined,\n description: \"TangramSpec object defining target shape to display\"\n },\n /** Whether this trial is a match (for computing accuracy) */\n isMatch: {\n type: ParameterType.BOOL,\n default: undefined,\n description: \"Whether this tangram matches the previous one (optional)\"\n },\n /** Whether to show tangram decomposed into individual primitives with borders */\n show_tangram_decomposition: {\n type: ParameterType.BOOL,\n default: false,\n description: \"Whether to show tangram decomposed into individual primitives with borders\"\n },\n /** HTML content to display above the tangram as instructions */\n instructions: {\n type: ParameterType.STRING,\n default: \"\",\n description: \"HTML content to display above the tangram as instructions\"\n },\n /** Text to display on response button */\n button_text: {\n type: ParameterType.STRING,\n default: \"Same as previous!\",\n description: \"Text to display on response button\"\n },\n /** Duration to display tangram and accept responses (milliseconds) */\n duration: {\n type: ParameterType.INT,\n default: 3000,\n description: \"Duration in milliseconds to display tangram and accept responses\"\n },\n /** Whether to use distinct colors for each primitive shape type */\n use_primitive_colors: {\n type: ParameterType.BOOL,\n default: false,\n description: \"Whether each primitive shape type should have its own distinct color in the displayed tangram\"\n },\n /** Indices mapping primitives to colors from the color palette */\n primitive_color_indices: {\n type: ParameterType.OBJECT,\n default: [0, 1, 2, 3, 4],\n description: \"Array of 5 integers indexing into primitiveColors array, mapping [square, smalltriangle, parallelogram, medtriangle, largetriangle] to colors\"\n },\n /** Callback fired when trial ends */\n onTrialEnd: {\n type: ParameterType.FUNCTION,\n default: undefined,\n description: \"Callback when trial completes with full data\"\n }\n },\n data: {\n /** Whether participant clicked the response button before duration expired */\n responded_match: {\n type: ParameterType.BOOL,\n description: \"True if participant clicked response button, false otherwise\"\n },\n /** Reaction time in milliseconds (NaN if no response or response after duration) */\n rt: {\n type: ParameterType.INT,\n description: \"Milliseconds between trial start and button click (NaN if no response or late response)\"\n },\n /** Accuracy: 1 if correct, 0 if incorrect, NaN if isMatch not provided */\n accuracy: {\n type: ParameterType.FLOAT,\n description: \"1 if response matches isMatch parameter, 0 otherwise (NaN if isMatch not provided)\"\n },\n /** Whether response occurred after duration expired */\n responded_after_duration: {\n type: ParameterType.BOOL,\n description: \"True if button clicked after duration expired, false otherwise\"\n },\n /** Time of late response (NaN if no late response) */\n rt_after_duration: {\n type: ParameterType.INT,\n description: \"Milliseconds between trial start and late button click (NaN if no late response)\"\n }\n },\n citations: \"\"\n};\n\ntype Info = typeof info;\n\n/**\n * **tangram-nback**\n *\n * A jsPsych plugin for n-back matching trials displaying a single tangram\n * with a response button.\n *\n * @author Justin Yang & Sean Paul Anderson\n * @see {@link https://github.com/cogtoolslab/tangram_construction.git/tree/main/experiments/jspsych-tangram-prep}\n */\nclass TangramNBackPlugin implements JsPsychPlugin<Info> {\n static info = info;\n\n constructor(private jsPsych: JsPsych) {}\n\n /**\n * Launches the trial by invoking startNBackTrial\n * with the display element, parameters, and jsPsych instance.\n */\n trial(display_element: HTMLElement, trial: TrialType<Info>) {\n // Wrap onTrialEnd to handle React cleanup and jsPsych trial completion\n const wrappedOnTrialEnd = (data: any) => {\n // Call user-provided callback if exists\n if (trial.onTrialEnd) {\n trial.onTrialEnd(data);\n }\n\n // Clean up React first (before clearing DOM)\n const reactContext = (display_element as any).__reactContext;\n if (reactContext?.root) {\n reactContext.root.unmount();\n }\n\n // Clear display after React cleanup\n display_element.innerHTML = '';\n\n // Finish jsPsych trial with data\n this.jsPsych.finishTrial(data);\n };\n\n // Create parameter object for wrapper\n const params: StartNBackTrialParams = {\n tangram: trial.tangram,\n isMatch: trial.isMatch,\n show_tangram_decomposition: trial.show_tangram_decomposition,\n instructions: trial.instructions,\n button_text: trial.button_text,\n duration: trial.duration,\n usePrimitiveColors: trial.use_primitive_colors,\n primitiveColorIndices: trial.primitive_color_indices,\n onTrialEnd: wrappedOnTrialEnd\n };\n\n // Use React wrapper to start the trial\n const { root, display_element: element, jsPsych } = startNBackTrial(display_element, params, this.jsPsych);\n\n // Store React context for cleanup\n (element as any).__reactContext = { root, jsPsych };\n }\n}\n\nexport default TangramNBackPlugin;\n"],"names":[],"mappings":"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"}
|
|
@@ -20835,14 +20835,20 @@ var TangramPrepPlugin = (function (jspsych) {
|
|
|
20835
20835
|
lineHeight: 1.5,
|
|
20836
20836
|
textAlign: "center"
|
|
20837
20837
|
};
|
|
20838
|
+
const scaleX = svgDimensions.width / viewBox.w;
|
|
20839
|
+
const rightEdgeOfSemicircleLogical = viewBox.w / 2 + layout.outerR;
|
|
20840
|
+
const distanceFromRightEdgeLogical = viewBox.w - rightEdgeOfSemicircleLogical;
|
|
20841
|
+
const distanceFromRightEdgePx = distanceFromRightEdgeLogical * scaleX;
|
|
20842
|
+
const charWidth = 24 * 0.6;
|
|
20843
|
+
const offsetLeft = charWidth * 5;
|
|
20838
20844
|
const timerStyle = {
|
|
20839
20845
|
position: "absolute",
|
|
20840
|
-
right:
|
|
20846
|
+
right: `${distanceFromRightEdgePx + offsetLeft}px`,
|
|
20841
20847
|
fontSize: "24px",
|
|
20842
20848
|
fontWeight: "bold",
|
|
20843
20849
|
fontFamily: "monospace",
|
|
20844
20850
|
color: "#333",
|
|
20845
|
-
|
|
20851
|
+
whiteSpace: "nowrap",
|
|
20846
20852
|
textAlign: "right"
|
|
20847
20853
|
};
|
|
20848
20854
|
const gameboardWrapperStyle = {
|