Postmortem: Our Snap Algorithm Could Only Align to One Element at a Time
TLDR
The snap algorithm found the single closest point across all candidates. This prevented simultaneous alignment to two different elements. Users reported that layout operations requiring two-axis alignment took twice as many steps as expected. Axis-independent search for X and Y fixed both dimensions independently.
The Assumption That Seemed Reasonable
Snap-to-nearest means finding the nearest element. Euclidean distance is the correct measure of nearness. The algorithm finds the closest point in 2D space and snaps to it.
This is how most tutorial implementations of snap work. It is how the snap was first implemented. It produced correct results for all single-element alignment cases.
When It Failed
A user was laying out a schematic with multiple components. They wanted to align a new component's left edge to the right edge of component A, and simultaneously align the top edge to the bottom edge of component B. Components A and B were at different positions.
With the nearest-point snap, dragging the new component would snap to either A or B depending on which was closer. It could not snap to both at once. The user had to:
- Drag near A until it snapped to A's right edge.
- Note the X position.
- Drag near B until it snapped to B's bottom edge.
- The element had moved horizontally during step 3.
- Manually type the correct X position in the property panel.
The feedback: "snap only works when I'm aligning to one thing. If I need two alignments it's faster to use the coordinates panel."
What Was Actually Wrong
The nearest-Euclidean-point algorithm finds one closest point in 2D space. That point belongs to one element. It is the mathematically closest point, but it is not what the user wants when they want to align to two elements.
The user's intent for two-axis alignment is: "make my X position match this element's edge, and make my Y position match that other element's edge." These are two independent alignment constraints. A single-point search cannot satisfy two independent constraints.
The algorithm was solving a different problem than the user was posing. The user asked: "align me to the nearest relevant target for each axis." The algorithm answered: "snap to the single globally nearest point."
What Got Deleted
The Euclidean nearest-point search: the loop that computed Math.hypot(x - pt.x, y - pt.y) and tracked a single bestDist. This produced one snap candidate that covered both axes.
What Replaced It
Two independent nearest-candidate searches, one for X and one for Y. Each uses Math.abs along a single axis:
// X search: find closest horizontal alignment target
[bb.x, bb.x + bb.width / 2, bb.x + bb.width].forEach(cx => {
const d = Math.abs(x - cx);
if (d < minDistX) { minDistX = d; snapX = cx; }
});
// Y search: find closest vertical alignment target (independent of X) [bb.y, bb.y + bb.height / 2, bb.y + bb.height].forEach(cy => { const d = Math.abs(y - cy); if (d < minDistY) { minDistY = d; snapY = cy; } });
The X and Y results are combined into the snap point. They can come from different elements. The user's layout operation that previously required four steps now requires one drag.
The Lesson
When users describe an operation as taking more steps than expected, the algorithm is solving a different problem than they are posing. Snap-to-nearest solves "find the closest point." Snap-to-align solves "satisfy the closest alignment constraint per axis." These are different problems with different algorithms.