class SnapDetector { /** * Check all pieces in the held group against their grid neighbours. * * @param {PieceObject} heldPieceObj * @param {Map} allPieceObjects pieceId → PieceObject * @param {GroupManager} groupManager * @param {number} cols * @param {number} pieceW * @param {number} pieceH * @param {number} snapRadius — distance threshold for snapping * @returns {Array<{heldId, neighborId, shiftX, shiftY, dist}>} sorted closest-first */ static check(heldPieceObj, allPieceObjects, groupManager, cols, pieceW, pieceH, snapRadius) { return SnapDetector._scan( heldPieceObj, allPieceObjects, groupManager, cols, pieceW, pieceH, snapRadius ); } /** * Same scan but with a larger radius, returning edge segment world coords for glow rendering. * * @returns {Array<{x0,y0,x1,y1}>} — line segments in world space to draw the glow along */ static getGlowSegments(heldPieceObj, allPieceObjects, groupManager, cols, pieceW, pieceH, glowRadius) { const candidates = SnapDetector._scan( heldPieceObj, allPieceObjects, groupManager, cols, pieceW, pieceH, glowRadius ); return candidates.map(({ heldId, neighborId }) => { const hp = allPieceObjects.get(heldId).data; const np = allPieceObjects.get(neighborId).data; return SnapDetector._sharedEdgeSegment(hp, np, pieceW, pieceH); }); } // ───────────────────────────────────────────────────────────────────── // Internal helpers // ───────────────────────────────────────────────────────────────────── static _scan(heldPieceObj, allPieceObjects, groupManager, cols, pieceW, pieceH, radius) { const results = []; const heldGroupId = groupManager.getGroupId(heldPieceObj.data.id); const heldGroup = groupManager.getPeersOf(heldPieceObj.data.id); const DIRS = [ { dc: 0, dr: -1 }, // top { dc: 1, dr: 0 }, // right { dc: 0, dr: 1 }, // bottom { dc: -1, dr: 0 }, // left ]; heldGroup.forEach(heldId => { const hp = allPieceObjects.get(heldId); if (!hp) return; const hd = hp.data; DIRS.forEach(({ dc, dr }) => { const nRow = hd.gridRow + dr; const nCol = hd.gridCol + dc; if (nRow < 0 || nCol < 0) return; const neighborId = nRow * cols + nCol; const np = allPieceObjects.get(neighborId); if (!np) return; if (groupManager.getGroupId(neighborId) === heldGroupId) return; // Ideal position of the held piece if it were snapped to the neighbor const idealHeldX = np.data.x - dc * pieceW; const idealHeldY = np.data.y - dr * pieceH; const dist = Math.hypot(hd.x - idealHeldX, hd.y - idealHeldY); if (dist < radius) { results.push({ heldId, neighborId, shiftX: idealHeldX - hd.x, shiftY: idealHeldY - hd.y, dist }); } }); }); return results.sort((a, b) => a.dist - b.dist); } /** * Compute the world-space line segment for the shared edge between two adjacent pieces. * Used to position the glow effect on the correct edge. */ static _sharedEdgeSegment(hp, np, pieceW, pieceH) { const dc = np.gridCol - hp.gridCol; const dr = np.gridRow - hp.gridRow; if (dc === 1) { // hp is left of np — right edge of hp return { x0: hp.x + pieceW / 2, y0: hp.y - pieceH / 2, x1: hp.x + pieceW / 2, y1: hp.y + pieceH / 2 }; } else if (dc === -1) { // hp is right of np — left edge of hp return { x0: hp.x - pieceW / 2, y0: hp.y - pieceH / 2, x1: hp.x - pieceW / 2, y1: hp.y + pieceH / 2 }; } else if (dr === 1) { // hp is above np — bottom edge of hp return { x0: hp.x - pieceW / 2, y0: hp.y + pieceH / 2, x1: hp.x + pieceW / 2, y1: hp.y + pieceH / 2 }; } else { // hp is below np — top edge of hp return { x0: hp.x - pieceW / 2, y0: hp.y - pieceH / 2, x1: hp.x + pieceW / 2, y1: hp.y - pieceH / 2 }; } } }