Snapping ​
Snapping pulls a point onto a nearby target while the user draws or edits, so graphics can share exact vertices or sit precisely on a line. It is off by default.
Turn snapping on ​
Pass snapping to the constructor, or change it at any time with setSnappingOptions():
const td = new TacticalDraw(adapter, {
snapping: { enabled: true },
});
// Later, for example from a toolbar toggle:
snapToggle.onchange = () => {
td.setSnappingOptions({ enabled: snapToggle.checked });
};setSnappingOptions() replaces the whole configuration, so pass every field you want to keep. Calling it with no argument turns snapping off.
Snapping applies when the user:
- places a point during a draw,
- drags a vertex or midpoint in reshape mode, or
- moves a graphic with the transform box (the grabbed point snaps, and the whole graphic follows).
Targets ​
sources selects what the pointer can snap to:
td.setSnappingOptions({
enabled: true,
tolerancePixels: 12,
sources: {
graphics: true, // control points and point-symbol/Image positions (default)
graphicGeometry: { edges: true, vertices: false }, // generated linework
external: (request) => gridIntersectionsNear(request.coordinate),
},
});| Source | Default | Snaps to |
|---|---|---|
graphics | on | The control points of rendered control measures, and the positions of point symbols and Images. |
graphicGeometry | off | The rendered lines and polygon outlines of control measures. true snaps to the nearest point on any edge; pass { vertices: true } to also snap to their corners. Labels are excluded. |
external | none | Candidates returned by your function, such as grid intersections or features from another layer. |
The graphic being edited never snaps to itself. During a reshape, however, its other control points remain valid targets, so you can close a shape exactly.
The nearest candidate within tolerancePixels (default 12) wins. When two are equally close, the one with the higher priority wins.
Supply your own candidates ​
An external provider receives the pointer coordinate and pixel, the interaction ("draw", "reshape", or "translate"), and, when relevant, the graphicId and controlPointIndex being moved. Return an array of candidates:
import type { SnapCandidateProvider } from "@orbat-mapper/tactical-draw";
const external: SnapCandidateProvider = (request) =>
referencePoints.map((point) => ({
id: point.id,
coordinate: point.position,
kind: "reference-point",
priority: 1, // prefer these over graphic control points at equal distance
}));The provider runs on every pointer move, so keep it fast. Return only candidates near request.coordinate when your data set is large.
Show a snap indicator ​
TacticalDraw moves the point but draws no indicator. Use onSnap to render your own. It receives the chosen target, or null when nothing is in range or the interaction ends:
td.setSnappingOptions({
enabled: true,
onSnap(result) {
if (!result) return hideSnapMarker();
showSnapMarker(result.pixel, result.candidate.kind);
},
});result contains the snapped coordinate and pixel, the matching candidate, and its distancePixels from the pointer.
Bypass snapping temporarily ​
Let users hold a key to place a point freely. bypass receives the engine's native event; return true to skip snapping for that event. Engines differ in what they pass, and some wrap the DOM event in their own object, so read the key defensively:
function ctrlPressed(event: unknown): boolean {
for (
let e = event;
e && typeof e === "object";
e = (e as { originalEvent?: unknown }).originalEvent
) {
if ((e as { ctrlKey?: boolean }).ctrlKey) return true;
}
return false;
}
td.setSnappingOptions({ enabled: true, bypass: ctrlPressed });Avoid Shift: it constrains angles for some draw rules, such as Sector.