Element Snap Only Aligns to One Reference at a Time: The Euclidean Distance Problem
TLDR
An element being dragged near two reference elements snaps to only one of them. The other alignment is lost. The root cause: Math.hypot computes Euclidean distance, which has one winner. Separate Math.abs searches for X and Y each have an independent winner, enabling simultaneous two-element alignment.
Symptom
Dragging an element between two reference elements snaps to the closer one and ignores the other. If element A is 3px to the left of the snap threshold and element B is 2px above the snap threshold, the element snaps to B and loses alignment to A.
Users who need to align an element to both A's right edge and B's bottom edge cannot do it in one drag.
Why It Happens
The nearest-point algorithm uses Euclidean distance:
// finds single closest point โ one winner in 2D
candidates.forEach(bb => {
const centerX = bb.x + bb.width / 2;
const centerY = bb.y + bb.height / 2;
const d = Math.hypot(dragX - centerX, dragY - centerY);
if (d < bestDist) {
bestDist = d;
snapX = centerX;
snapY = centerY;
}
});
Math.hypot combines X and Y distance into a single scalar. The element with the smallest combined distance wins both axes. An element 3px away in X but 0px in Y may lose to an element 0px in X but 2px in Y, even if the first element is the better X candidate.
The Fix
// separate X and Y searches โ independent winners per axis
let snapX = dragX, snapY = dragY;
let minDistX = threshold, minDistY = threshold;
candidates.forEach(bb => { // X candidates: left edge, center, right edge [bb.x, bb.x + bb.width * 0.5, bb.x + bb.width].forEach(cx => { const d = Math.abs(dragX - cx); if (d < minDistX) { minDistX = d; snapX = cx; } });
// Y candidates: top edge, center, bottom edge (independent search) [bb.y, bb.y + bb.height * 0.5, bb.y + bb.height].forEach(cy => { const d = Math.abs(dragY - cy); if (d < minDistY) { minDistY = d; snapY = cy; } }); });
return { x: snapX, y: snapY };
The X search picks the element whose edge is closest horizontally. The Y search picks independently. The two results combine into a snap point that may align to two different elements simultaneously.
Scale the threshold by zoom to keep snap sensitivity constant in screen pixels:
const threshold = 8 / (currentZoom || 1);
How to Prevent It
When writing snap logic, ask: should X and Y be coupled or independent? If the user is drawing a shape near an element (snap to the element's exact position), coupling is correct. If the user is aligning an element to a layout grid of references (snap to the nearest edge per axis), independence is correct. Most interactive editor snap is the second case.
The Generalizable Lesson
Math.hypot for snap distance is a coupling decision: you are saying "the best X target and the best Y target must come from the same point." Math.abs per axis is a decoupling decision: you are saying "X and Y alignment are independent constraints." For multi-element layout alignment, decoupling is almost always the correct model.