🚧 These docs are a work in progress — content is incomplete and may change without notice.
Skip to content

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():

ts
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:

ts
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),
  },
});
SourceDefaultSnaps to
graphicsonThe control points of rendered control measures, and the positions of point symbols and Images.
graphicGeometryoffThe 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.
externalnoneCandidates 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:

ts
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:

ts
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:

ts
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.