iPuzzle/js/puzzle/SnapDetector.js

125 lines
4.4 KiB
JavaScript

class SnapDetector {
/**
* Check all pieces in the held group against their grid neighbours.
*
* @param {PieceObject} heldPieceObj
* @param {Map<number, PieceObject>} 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, dist }) => {
const hp = allPieceObjects.get(heldId).data;
const np = allPieceObjects.get(neighborId).data;
const seg = SnapDetector._sharedEdgeSegment(hp, np, pieceW, pieceH);
return { ...seg, dist, neighborId, heldId, glowRadius };
});
}
// ─────────────────────────────────────────────────────────────────────
// 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
};
}
}
}