# A4 smart guides (`a4/guides.js`) Pure functions, no React/DOM. `buildGuideCandidates(elements, excludeIds, pageSetup)` → `{V, H}`; `resolveSnap(draftRect, candidates, zoom)` → `{x, y, guides}`; plus `unionRect(boxes)` and `snapToleranceMm(zoom)`. ## The threshold is in SCREEN PIXELS, not mm `SNAP_PX = 6`, converted per-call via `SNAP_PX / (PX_PER_MM * zoom)`. This is load-bearing, not a detail: a fixed mm threshold feels sticky zoomed in and useless zoomed out. Do not "simplify" it to a constant mm value. The property worth asserting is `tolMm * PX_PER_MM * zoom === SNAP_PX` at every zoom. ## Integration seams in `A4Canvas.jsx` - Candidates are built **once** in `handleElementDragStart` into a `useRef`. Rebuilding per mousemove is the one thing that makes dragging laggy. - Multi-select snaps the **union rect**, then applies one delta to every member. Snapping members independently tears the group apart. - `hooks/useElementDrag.jsx` is shared with the matrix editor and forwards only pixel deltas — **never widen it**. Alt-to-suppress is tracked with a window-level `keydown`/`keyup` listener in A4Canvas, plus a `blur` reset (an alt-tab otherwise leaves snapping permanently off, because the keyup lands in another window). - Bands emit no candidates (their edges *are* the margins, already candidates), but band children do. ## Gotcha: snapping breaks drag tests that assume raw deltas Any pre-existing test that drags an element and asserts the exact px→mm result can fail once a candidate happens to sit within ~1.6mm of where it lands. This bit `a4Elements.test.js`'s rect case: it drifted 0.05mm from the 20mm left margin and legitimately snapped. The fix is to hold Alt around the drag (`withAltHeld` helper in that file), not to weaken the assertion — the test is about unit conversion, so switching the magnet off states its intent.