The Postmortem of One Snap Function Shared by Every Tool
TLDR
Routing all canvas drawing tools through a single globalsmartSnap(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 Milestone | Global Shared Dispatch (smartSnap) | Per-Tool Policy & Clamped Bounds |
|---|---|---|
| Freehand Pen Tool | Quantized points to grid (Staircase lines) | Raw high-rate inputs (Smooth curves) |
| Low-Zoom Precision (10%) | 80-unit giant snap grab radius | Clamped bounds (Math.min(8/zoom, 24)) |
| Wire & Shape Tools | Snapped to pins/grid | Snaps 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.