Engineering Journal
Schema Editor
Schema Editor

The postmortem of one snap function shared by every tool

2026-07-17

TLDR

We routed every drawing tool through one shared snap helper. It felt like textbook reuse. It made the freehand pen quantize strokes to the grid mid-draw, and it made snap radii balloon into huge grab zones at low zoom. The lesson: snapping is a per-tool policy, not a shared utility, and zoom-relative thresholds need absolute clamps.

The assumption that seemed reasonable

Our diagram editor has a smartSnap(x, y) helper: snap to the grid, then check nearby element edges, centers, and pins, and snap to whichever is closest within a threshold. Wires want it. Lines want it. Rectangles want it. So the event layer applied it once, at the top, before dispatching to any tool:

const pt = screenToSVG(e.clientX, e.clientY);
const snapped = smartSnap(pt.x, pt.y);
switch (activeTool) {
  case 'pen':  penMove(snapped); break;
  case 'line': lineMove(snapped); break;
  case 'rect': rectMove(snapped); break;
  // ...
}

One coordinate transform, one snap call, every tool consistent. It reviewed well. Nobody flagged it.

When it failed

Users reported that the pen "only draws in boxes." Freehand strokes came out as staircases: short horizontal and vertical runs with right-angle turns. We first suspected the smoothing function, and it was indeed weak (midpoint quadratics, not a real spline), but rewriting it changed nothing. The staircase was in the input data.

Every sampled point of every stroke was being rounded to the nearest 20px grid intersection before the pen ever saw it. A freehand gesture through grid-snapped points is, by construction, a walk on a lattice. No downstream smoothing can undo that, because the information is gone.

A second failure surfaced at low zoom. The snap threshold was 8 / zoom world units, which is correct reasoning (keep the feel constant in screen pixels) with a missing bound. At 10% zoom that is an 80-unit world-space radius. Clicks landed nowhere near where users aimed, because some pin or edge 60 units away "won" the snap.

What was actually wrong

Two category errors.

First, snapping is semantics, not plumbing. A wire endpoint wants to land on a pin: snapping is the feature. A rectangle corner wants to align to the grid: snapping helps. A freehand stroke is a record of hand motion: snapping is destruction. These are different tools with different contracts, and hoisting the snap into the shared event path erased the difference. The reuse was real, but it reused a decision, not just code.

Second, zoom-relative values need absolute clamps. Dividing by zoom is right for feel and catastrophically wrong at the extremes. Any formula of the form k / zoom should ship as Math.min(k / zoom, cap).

What got deleted, what replaced it

The pen's mouse handlers left the shared dispatch entirely. It now binds its own pointer events (which it needed anyway for capture and coalesced samples) and consumes raw coordinates. The shared path still snaps for line, rect, ellipse, polygon, and wire, which all genuinely want it.

The thresholds got clamps: Math.min(8 / zoom, 24) for element snapping, Math.min(16 / zoom, 32) for pin snapping. Two-character-diff fixes that ended a whole class of low-zoom complaints.

The generalizable lesson

When you share a helper across N call sites, you are asserting that all N want the same policy, forever. For pure functions over data that assertion is usually cheap. For interaction policy (snapping, debouncing, gesture thresholds, auto-select behavior) it is usually false, and the failure is silent: nothing crashes, the tool just feels wrong, and users file bugs like "drawing feels broken" that point nowhere near the shared line of code that caused it.

The test we now apply: if two tools would ever legitimately want different values for it, it is not a utility, it is a per-tool setting with a default. And any threshold that scales with a user-controlled factor (zoom, DPI, velocity) gets a floor and a ceiling on the day it is written, not after the bug report.

Read this post in the full Engineering Journal →