Engineering Journal
Schema Editor
Schema Editor

Element Snap Only Aligns to One Reference at a Time: The Euclidean Distance Problem

2026-06-04

TLDR

When dragging canvas elements near multiple reference items, using Math.hypot causes the snap engine to lock onto whichever candidate element is 1 pixel closer in combined Euclidean space, completely ignoring alignment targets along the secondary axis. Replacing Euclidean calculations with independent Math.abs distance checks along X and Y axes restores dual-target snapping.
Search MetricMathematical ExpressionBehaviorResult
Coupled 2D DistanceMath.hypot(dx, dy)Single target wins both axesIgnored secondary alignment
Decoupled Axis DistanceMath.abs(dx) & Math.abs(dy)Independent X/Y candidate winnersSimultaneous dual alignment

Problem statement: the one-target snapping glitch

Users attempting to position a schematic component between two existing components experienced a frustrating defect: when trying to align component C's left edge to component A and its top edge to component B, the editor snapped to B but completely ignored A.

The user was forced to drag once to align vertically, release, drag again horizontally, and then manually adjust coordinates in the properties inspector when the second drag broke the first alignment.


Technical failure mode: scalar coupling in Math.hypot

The root cause was using Euclidean 2D distance to evaluate snap priority:

// DEFECTIVE: Combines X and Y into a single scalar distance
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;
  }
});

Because Math.hypot evaluates 2D space as a single scalar, an element 0px away in Y but 4px away in X loses to an element 1px away in both X and Y. The algorithm couples X and Y constraints together when they should be evaluated independently.


The fix: independent Math.abs searches per axis

We decoupled the search into two separate scalar comparisons scaled by canvas zoom:

// REFACTORED: Independent per-axis candidate evaluation
const threshold = 8 / (currentZoom || 1); // Zoom-adjusted threshold in screen pixels
let snapX = dragX, snapY = dragY;
let minDistX = threshold, minDistY = threshold;

candidates.forEach(bb => { // 1. Horizontal Snap Candidates (Left, Center, Right) [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; } });

// 2. Vertical Snap Candidates (Top, Center, Bottom) [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 };

Rule of thumb: Use Math.hypot only when snapping to exact point targets (such as component pins or vertex nodes). Use independent Math.abs checks per axis when aligning element bounding boxes across a canvas layout.
Read this post in the full Engineering Journal →