Engineering Journal
Schema Editor
Schema Editor

The Postmortem of One Snap Function Shared by Every Tool

2026-07-17

TLDR

Routing all canvas drawing tools through a single global smartSnap(x, y) helper at the event-dispatch layer seemed like clean code reuse. In practice, it forced the freehand pen tool to quantize mouse coordinates onto a 20px grid mid-draw (producing staircase lines) and caused snap radii to balloon into massive grab zones at low zoom levels. Snapping is a per-tool interaction policy, not a global event-dispatcher utility, and zoom-scaled thresholds must always be clamped with absolute bounds.
Incident MilestoneGlobal Shared Dispatch (smartSnap)Per-Tool Policy & Clamped Bounds
Freehand Pen ToolQuantized points to grid (Staircase lines)Raw high-rate inputs (Smooth curves)
Low-Zoom Precision (10%)80-unit giant snap grab radiusClamped bounds (Math.min(8/zoom, 24))
Wire & Shape ToolsSnapped to pins/gridSnaps cleanly to pins/grid

Problem statement: the staircase line defect

Our editor featured a global smartSnap(x, y) helper that snapped world coordinates to the nearest grid interval or component pin.

To ensure consistency, the top-level event dispatcher applied smartSnap before routing input to active tools:

// DEPRECATED: Global snap applied at top-level event dispatcher
const rawPt = screenToSVG(e.clientX, e.clientY);
const snappedPt = smartSnap(rawPt.x, rawPt.y); // Applied to EVERY tool!

switch (activeTool) { case 'pen': penMove(snappedPt); break; case 'line': lineMove(snappedPt); break; case 'rect': rectMove(snappedPt); break; }

Users reported that the freehand pen tool "only draws in boxes." Freehand strokes produced staircases of right-angle segments because every mouse point was rounded to grid intersections before reaching the pen handler.


Technical failure mode: unclamped zoom threshold scaling

A secondary failure occurred at low zoom levels ($0.1\text{x}$ zoom).

The snap threshold formula was threshold = 8 / zoom. At $0.1\text{x}$ zoom, the threshold ballooned to 80 world units. Clicking anywhere near a cluster of components triggered unexpected snaps to pins located far across the canvas.


The fix & architecture: per-tool policy & absolute threshold clamps

1. Decoupling the freehand pen tool

Remove global snapping from the top-level dispatcher. Allow tools to specify their own snapping policies:
// REFACTORED: Per-tool interaction handling
function onCanvasPointerMove(e) {
  const rawPt = screenToSVG(e.clientX, e.clientY);

if (activeTool === 'pen') { penMove(rawPt); // Freehand pen consumes raw, un-quantized input! } else if (['line', 'rect', 'wire'].includes(activeTool)) { const snappedPt = smartSnap(rawPt.x, rawPt.y); toolMove(snappedPt); } }

2. Clamping zoom-scaled thresholds

Apply absolute clamps (Math.min) to all zoom-scaled threshold calculations:
// REFACTORED: Clamped zoom threshold calculation
function getClampedSnapThreshold(currentZoom) {
  const elementSnapThreshold = Math.min(8 / currentZoom, 24);  // Max 24 world units
  const pinSnapThreshold     = Math.min(16 / currentZoom, 32); // Max 32 world units
  
  return { elementSnapThreshold, pinSnapThreshold };
}
Rule of thumb: Snapping is an interaction policy specific to each tool, not a universal event dispatcher utility. Always clamp zoom-scaled spatial thresholds using Math.min() bounds.
Read this post in the full Engineering Journal →