Engineering Journal
Schema Editor
Schema Editor

Postmortem: Our Snap Algorithm Could Only Align to One Element at a Time

2026-06-04

TLDR

Our original canvas snapping engine used Euclidean distance (Math.hypot) to find the single closest candidate point across the canvas. While this worked for single-element alignment, it prevented users from aligning to two different reference components simultaneously. We refactored the snapping core to perform independent scalar searches for X and Y axes, reducing a 4-step manual alignment workaround down to a single drag gesture.
Incident AspectInitial ImplementationEngineering Root CauseProduction Remediation
Snapping BehaviorLocked onto 1 element onlySingle bestDist Euclidean calculationSplit into independent X & Y searches
User WorkaroundManual coordinate entrySingle winner took both axesIndependent Math.abs per axis
User FrictionHigh (4 steps per alignment)Coupled spatial distanceLow (1 drag gesture)

Problem statement: the 4-step layout alignment workaround

Users reported that aligning a new schematic symbol to existing canvas components was taking twice as long as expected:

  1. Drag component near Component A to align left edges.
  2. Note the resulting X coordinate.
  3. Drag component near Component B to align top edges (which broke the X alignment to Component A).
  4. Open the Property Panel and manually type the saved X coordinate back in.
User feedback was clear: "Snapping only works when I'm aligning to one thing. If I need to align to two things, it's faster to type coordinates manually."

Technical failure mode: coupled Euclidean distance search

The original implementation tracked a single bestDist variable across 2D Euclidean space:

// DEPRECATED: Euclidean single-winner calculation (~15 lines deleted)
let bestDist = threshold;
candidates.forEach(bb => {
  const points = [ { x: bb.x, y: bb.y }, / ... / ];
  points.forEach(pt => {
    const d = Math.hypot(x - pt.x, y - pt.y); // Coupled distance!
    if (d < bestDist) { bestDist = d; best = pt; }
  });
});

Because bestDist was a single scalar value, the algorithm picked one reference point to satisfy both axes, completely ignoring the second candidate element.


The fix & architecture: decoupled per-axis search

We replaced the Euclidean search with two independent candidate sweeps:

// REFACTORED: Decoupled per-axis search
let snapX = x, snapY = y;
let minDistX = threshold, minDistY = threshold;

// 1. Independent Horizontal Search [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; } });

// 2. Independent Vertical Search [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; } });

return { x: snapX, y: snapY };

Rule of thumb: If users report that a basic canvas operation requires manual property panel tweaks, check whether your underlying algorithm is trying to solve two independent constraints with a single coupled calculation.
Read this post in the full Engineering Journal →