Reference โ
The complete public API. For task-oriented examples, see the recipes.
Constructor and options โ
const td = new TacticalDraw(adapter, {
generateId: () => crypto.randomUUID(),
graphicsStyle: { color: "#111827", strokeWidth: 2 },
interactionStyle: {
guide: { strokeColor: "#64748b", strokeWidth: 1, strokeDash: [8, 4] },
vertexHandle: { fillColor: "#ffffff", strokeColor: "#2563eb", strokeWidth: 2 },
midpointHandle: { fillColor: "#fbbf24", strokeColor: "#2563eb", strokeWidth: 2 },
},
});| Option | Default | Description |
|---|---|---|
layers | created automatically | Optional application layer IDs for graphics, highlight, preview, guide, and handles. Supplied IDs survive destroy(). |
graphicsStyle | { strokeWidth: 2 } | Lowest-priority default style for every control measure. Monocolor model: color paints stroke and fill; strokeColor / fillColor are per-channel overrides. Without a color, each kind's doctrinal default applies. |
symbolColors | doctrinal | Doctrinal default colors, such as { obstacle: MIL_STD_GREEN.dark }. See Doctrinal color defaults. |
interactionStyle | built-ins | Unified affordance style. Slots: guide, vertexHandle, midpointHandle, optionHandle, unsetOptionHandle, geometryHandle, labelHandle, scaleHandle, rotateHandle, boxOutline, highlightOutline, plus boxPadding (px, default 0). Per-call overrides merge on top. |
generateId | td-1, td-2, and so on | Used when draw() commits a graphic whose draft had no id. |
snapping | off | Snapping configuration. See Snapping. |
pointSymbols | none | Renderer capability for point symbols. See Optional point symbols. |
images | none | Resource resolver for Images. See Host-backed Images. |
offsetDragModifier | false | Key that turns a point-symbol body drag into an offset drag: "alt", "shift", "ctrl", "meta", or false. |
editModifiers | see Edit | Keys for the edit gestures: modifierDrag (Ctrl or โ), deleteOrReset (Alt), constrain (Shift), extendPartSelection (off). See Modifier keys. |
Layers โ
TacticalDraw uses five layer slots, listed bottom to top. It creates any slot you omit and removes that layer during destroy().
| Layer | Holds |
|---|---|
graphics | Authoritative rendered graphics from render(). |
highlight | Passive outlines from setHighlightedGraphics(). |
preview | Generated preview geometry during draw/edit. |
guide | Rubber-band polyline and optional area-closing segment. |
handles | Edit handles for reshape (vertex/midpoint) and transform (box outline, scale corners, rotate grip). |
Do not use layerIds
td.layerIds is internal and may change. Select committed graphics with onGraphicPick(), and use ownsInteractionAt to check whether TacticalDraw owns a pixel. Do not query internal layers directly.
Methods โ
| Method | Description |
|---|---|
render(graphics) | Reconciles the graphics layer with your list. Throws on duplicate IDs. |
draw(draft, options?) | Starts a draw. See Draw. |
edit(graphic, options?) | Starts an edit of one graphic. See Edit. |
editMany(graphics, options?) | Starts a group transform. See Multi-select. |
syncTransformGraphics(graphics, options?) | Updates or starts a group transform to match a selection. |
cancel(reason?) | Aborts the active interaction. Returns whether one was active. |
onGraphicPick(handler, options?) | Subscribes to clicks on committed graphics. Returns an unsubscribe function. |
onPick(layerId, handler, options?) | Subscribes to clicks on features in one of your own layers. |
ownsInteractionAt(pixel, options?) | Returns an InteractionHit if TacticalDraw owns the pixel, otherwise null. |
consumedPointerCycle() | Returns whether the active edit kept the current click. See Host click handling. |
setHighlightedGraphics(ids) | Replaces the set of passively outlined graphics. See Selection highlights. |
setSnappingOptions(options?) | Replaces the snapping configuration. See Snapping. |
setOffsetDragModifier(modifier) | Changes the point-symbol offset-drag key, including for an active edit. |
setEditModifiers(modifiers) | Changes the edit gesture keys, including for an active edit. See Modifier keys. |
getEditModifiers() | Returns the edit gesture keys with every role filled in. |
setPinPresentation(defaults) and related | Configure MapLibre billboard pins. See Billboard pins. |
destroy() | Aborts any interaction and removes owned layers and listeners. Idempotent. |
| Property | Description |
|---|---|
activeSession | The live DrawSession, EditSession, or GraphicEditSession, or null. |
activeTransformSession | The live TransformSession, or null. |
pins | The billboard-pin settings store. See Save settings. |
Data shapes โ
TacticalDraw uses the public data types from @orbat-mapper/control-measures. Your application owns the stored Graphic[]; TacticalDraw creates, edits, and renders entries from it. A Graphic is a ControlMeasure, a PointSymbol, or an ImageGraphic.
ControlMeasure<K> โ
import type { Position } from "geojson";
interface ControlMeasure<K extends ControlMeasureKind = ControlMeasureKind> {
id: string; // stable, unique; prefixes rendered feature ids
kind: K; // selects metadata, rules, option typing, generator
controlPoints: Position[]; // lon/lat order; gestures round to 6 decimals
options?: OptionsByKind[K]; // per-kind generator options
style?: ControlMeasureStyle; // participates in the render cascade
properties?: Record<string, unknown>; // your metadata (not copied onto preview features)
textAmplifiers?: TextAmplifiers; // doctrinal text fields, e.g. { T: "BLUE" }
amplifierPlacements?: AmplifierPlacements; // user-adjusted label positions/rotations
schemaVersion?: 1;
}See Labels and text amplifiers for the last two fields.
ControlMeasureStyle โ
interface ControlMeasureStyle {
color?: string; // Monocolor input. Resolves into stroke and fill.
strokeColor?: string; // per-channel override
strokeWidth?: number;
strokeDash?: number[];
fillColor?: string; // per-channel override
fillPattern?:
| "solid"
| "hatch"
| "reverse-hatch"
| "cross-hatch"
| "horizontal"
| "vertical"
| "dots"; // interior of filled parts
opacity?: number; // graphic opacity multiplier, 0 through 1
textHalo?: boolean; // contrasting halo behind label text
}Before an adapter draws GeoJSON, TacticalDraw resolves the layer style, graphicsStyle, measure style, and generator hints. See Styling for the priority sequence.
opacity is multiplied into each resolved color's alpha. It affects display only; selection and hit-testing still use the color's original alpha. See Graphic opacity.
textHalo adds a contrasting halo only to features with a text field. The renderer chooses the color and writes textHalo and textHaloColor at feature top level for adapters to read. See Text halo.
GraphicSnapshot<G> โ
interface GraphicSnapshot<G extends Graphic = Graphic> {
graphic: G; // Deep copy. You can retain it.
render: G extends PointSymbol
? PointSymbolRender
: G extends ImageGraphic
? ImageRender
: ControlMeasureRender;
}TacticalDraw copies graphic with structuredClone, so it remains valid after later session changes. Rendered feature IDs follow ${cmId}:${part}:${index}; recover the parent ID with tryControlMeasureIdFromFeature().
TransformBox โ
interface TransformBox {
center: PixelCoordinate; // box center, pixel space
halfWidth: number; // half-width along the box's local x-axis (pre-rotation), px
halfHeight: number; // half-height along the box's local y-axis (pre-rotation), px
angle: number; // radians, screen space, positive = clockwise on screen
}This exported type describes the oriented selection box, including its four corner scale handles and rotation handle, without exposing TacticalDraw's internal placement or hit-test geometry.
InteractionHit โ
interface InteractionHit {
layer: "handles" | "preview" | "graphics"; // topmost match wins
feature: Feature; // The selected feature. Do not change it.
measureId?: string; // set when the feature belongs to a control measure
}td.ownsInteractionAt(pixel, options?) returns this type or null. See Host click handling.
Sessions โ
Receive a session through onSession or read it from td.activeSession while an interaction is running. The property is null while idle and after the session settles.
DrawSession โ
| Member | Description |
|---|---|
controlPoints | Live snapshot of committed draw points. |
canCommit | true when the point count is inside the kind's valid range. |
minControlPoints / maxControlPoints | Point-count bounds for the active draw. |
commit() | Commits if possible, returns true; otherwise false. |
abort() | Rejects the draw promise with reason "session". |
onChange(listener) | Subscribes to committed point changes. It does not report rubber-band pointer movements. |
onTransientChange(listener) | Subscribes to live geometry during the draw. It supplies a TransientChangeEvent for each rubber-band pointer movement: the working points, the pointIndex of the live point, and a "start" / "move" / "end" phase. The "end" phase occurs one time. |
onCommit(handler) | Subscribes to the committed snapshot. The handler receives the same object that resolves the draw() promise. It runs synchronously one time. It does not run after an abort. It returns an unsubscribe function. A subscription after settlement has no effect. |
onSettled(handler) | Subscribes to session settlement. It runs one time after commit or abort and supplies a SettleReason. It runs after onCommit and after TacticalDraw clears td.activeSession. A subscription after settlement runs asynchronously one time. |
EditSession โ
The session for edit() on a control measure.
| Member | Description |
|---|---|
graphic | Immutable edit-start input. |
workingGraphic | Live working copy, as a snapshot. |
controlPoints / options / style | Live working-state snapshots. |
textAmplifiers / amplifierPlacements | Live working text fields and label placements. |
dirty | true after a meaningful mutation (tolerance-based for geometry). |
history | Session-local undo/redo for completed gestures and programmatic mutations. Exposes observable state (canUndo, canRedo), undo(), redo(), and subscribe(). Both stacks clear when the session settles. |
modes | Active edit modes. |
detailHandles | Whether reshape mode shows detail handles, such as the width grips on variable-width arrows. |
selectedPart | The primary selected part of a graphic made of parts, such as the index of the Flow Arrow's selected flow. undefined for other graphics. |
selectedParts | Every selected part, selectedPart included, ascending. Reshape mode shows the grips of each. Empty for graphics without parts. |
sizeAnchor | Current size anchor. The "ground" value converts geometry and label pixel sizes to meters at commit. The "screen" value keeps pixel sizes. It applies only to control measures that contain a pixel size. |
canResetGroundSizes | true when the ground-anchored measure has resettable geometry or label sizes and the adapter has a usable current resolution. |
close() | Commits: returns the GraphicSnapshot<ControlMeasure> of the current working state (the same reference the edit promise resolves with) and resolves the promise; undefined after the session has settled. |
abort() | Rejects with reason "session". |
setModes(modes) | Changes active modes. Queued during a drag (latest wins). "delete" collapses to delete-only. |
setDetailHandles(visible) | Shows or hides the detail handles. View state: no change event, no undo step. |
selectPart(part, { extend? }) | Selects a part alone, or with extend toggles it in or out of selectedParts. View state: no change event, no undo step. No-op for a part the graphic doesn't have. |
selectParts(parts, primary?) | Replaces selectedParts; primary (or else the last listed part) becomes selectedPart. View state, like selectPart(). |
onPartSelect(listener) | Calls listener(part, parts) once per change to the selection, from the pointer, selectPart(), selectParts(), or the reconcile after an edit or undo. |
setOptions(partial) | Shallow-merges options. |
setStyle(partial) | Shallow-merges style. |
setTextAmplifiers(partial) | Merges text amplifiers. undefined or an empty string removes a field. |
setAmplifierPlacements(partial) | Merges label placements field by field. undefined removes an entry. |
setSizeAnchor(anchor, options?) | Changes the size anchor, optionally merging a size-option patch in the same undo step. No-op for measures with no pixel size; the in-flight preview stays screen-locked until commit. |
resetGroundSizes() | Restores all supported sizes to their default on-screen appearance at the current zoom, stores them in meters, and records one undoable change. No-op when canResetGroundSizes is false. |
updateGraphic(updater) | Replaces the working graphic with updater(current) in one step. id and kind are preserved. |
onChange(listener) | Subscribes to completed changes: gestures, undo/redo, and programmatic mutations. |
onTransientChange(listener) | Subscribes to live geometry during a drag. It supplies a TransientChangeEvent for each pointer movement of a vertex, midpoint, or transform-box gesture. pointIndex is null when all points move together. The "end" phase occurs one time, on commit and on cancel, before onChange. |
onCommit(handler) | Subscribes to the committed snapshot. The handler receives the same object that close() returns and that resolves the edit() promise. It runs synchronously one time. It does not run after an abort. It returns an unsubscribe function. A subscription after settlement has no effect. |
onSettled(handler) | Subscribes to session settlement. It runs one time after commit or abort and supplies a SettleReason. It runs after onCommit and after TacticalDraw clears td.activeSession. A subscription after settlement runs asynchronously one time. |
All mutating methods rerender the preview, mark the session dirty, emit onChange, and record one undo step. They do nothing after the session has settled.
EditChangeEvent
Listeners receive graphic (the state after the change), previous, and session. The first two are GraphicSnapshot<ControlMeasure> values. source says what caused the change: "gesture", "undo", "redo", or "programmatic". Call reject(), close(), or abort() from the listener. Every mutation emits an event; setModes(), setDetailHandles(), selectPart() and selectParts() do not.
GraphicEditSession โ
The session for edit() on a point symbol or an Image. It shares the lifecycle of EditSession but has no control points, options, or text methods.
| Member | Description |
|---|---|
graphic / workingGraphic | Edit-start input and live working copy. |
dirty / history / modes | As on EditSession. |
offset | Point symbols only: offset commands (setPlacement, setConnector, align, reset). See Offset placement. |
setModes(modes) | Changes active modes. |
updateGraphic(updater) | Replaces the working graphic in one undoable step. |
close() / abort() | Commit or abort, as on EditSession. |
onChange / onCommit / onSettled | As on EditSession. |
To tell sessions apart in a shared handler, check for a member: only EditSession has setOptions, and only DrawSession has canCommit.
TransformSession โ
TransformSession is the group counterpart to EditSession, available through editMany or td.activeTransformSession. It applies scale, rotation, and translation to all members, but cannot reshape a member or change one member's options and style. Change membership with setGraphics() and key returned snapshots by snapshot.graphic.id, not array index.
| Member | Description |
|---|---|
graphics | Live view of the current members' join-time inputs (a fresh array per read). Grows/shrinks with setGraphics; key snapshots by graphic.id, not by index. |
workingGraphics | Live working copies of the current members, in the same order as graphics. |
controlPointsFor(id) | Live working control points for the member with that graphic id, or undefined if no member carries it. |
dirty | true when any member's working points differ from its edit-start input beyond tolerance. |
history | Session-local undo/redo for completed group transforms. A setGraphics() call establishes a new baseline and clears both stacks; membership changes are not undoable. |
sizeAnchor | Live size anchor, applied group-wide. |
setSizeAnchor(anchor) | Switches the group's size anchor; takes effect on the next snapshot and on close(). |
setGraphics(graphics) | Replaces the member set in place and resets session history to the resulting working state. Retained members carry their working state forward; removed members are committed and their exit snapshots returned (and delivered to onCommit); added members join at their input (lifted off the graphics layer if on it). Returns the removed members' GraphicSnapshot[] (empty when none removed), or undefined after the session has settled. setGraphics([]) delegates to close(). Duplicate ids throw TypeError. |
close() | Commits the members in the set at close time. Returns their GraphicSnapshot[]. The editMany promise and onCommit handler receive the same array. Returns undefined after settlement. |
abort() | Rejects with reason "session". |
onChange(listener) | Subscribes to completed group scale, rotation, and translation gestures. It does not report rubber-band pointer movements. |
onCommit(handler) | Subscribes to committed batches of exit snapshots. It reports members that setGraphics() removes and members that are present at close time. It runs synchronously one time for each batch. It does not run after an abort or rejection. It returns an unsubscribe function. A subscription after settlement has no effect. |
onSettled(handler) | Subscribes to session settlement. It runs one time and supplies a SettleReason. It reports "close", an abort reason, or "error". It runs after the close-batch onCommit and after TacticalDraw clears td.activeTransformSession. A subscription after settlement runs asynchronously one time. |
TransformChangeEvent
Listeners receive graphics, previous, source, and session. At emission time, both arrays follow the order of session.graphics. Call reject(), close(), or abort() from the listener, and key members by ID because indexes may change.
Removed members and render()
setGraphics() and close() do not put removed members back on the graphics layer. Save the returned snapshots, then call td.render(). onCommit receives the same arrays as those methods and the editMany() promise. Use onSettled when you also need to observe aborts and the final settlement reason.
Errors and abort reasons โ
| Error | When |
|---|---|
TacticalDrawAbortError | Normal interaction cancellation. Extends DOMException (name: "AbortError") with a closed reason. Test with isTacticalDrawAbortError(e). |
TacticalDrawDestroyedError | Synchronous throw when a public method is called after destroy(). |
PointSymbolCapabilityRequiredError | Drawing or editing a point symbol without a pointSymbols capability. |
ImageCapabilityRequiredError | Drawing or editing an Image without an images capability. |
Error | render() throws on duplicate graphic ids. |
TypeError | edit() throws synchronously on an unknown mode; editMany() throws on an empty or duplicate-id measure array (before any preemption); TransformSession.setGraphics() and syncTransformGraphics() throw on a duplicate-id array. |
Abort reasons are "escape", "signal", "preempted", "destroyed", "session", and "removed". Session onSettled handlers also receive "close" for a commit and "error" if snapshot creation fails during commit.
try {
await td.draw({ kind: "ambush" }, { signal: abort.signal });
} catch (error) {
if (isTacticalDrawAbortError(error)) {
console.info("draw cancelled:", error.reason);
} else {
throw error;
}
}Adapter surface โ
Applications normally use a ready-made engine package. To support another engine, extend BaseMapAdapter for shared layer IDs, caching, event tokens, reconciliation, and cleanup, then implement the remaining MapAdapter methods.
| Capability | Methods |
|---|---|
| Vector layers | addVectorLayer, removeLayer, setLayerStyle, setLayerFeatureGroups, getLayerFeatures |
| Map events | on/off for click, pointermove, dblclick |
| Pick events | onPick(layerId, handler, opts?); control-measure hits dispatch the measure id and non-measure features are ignored |
| Projection & view | toLonLat, fromLonLat, getPixelFromCoordinate, getCoordinateFromPixel (optional), getViewportSize, getResolution, getZoom, getBearing, onViewChange, offViewChange |
| Interaction controls | panByPixels, setCursor, setDoubleClickZoomEnabled, createEditPointerDriver |
| Billboard pins | Optional: getPinPresentationCapability, onPinPresentationStatusChange, getPinFootprint, pickPin, movePinFromPixel |
| Lifecycle | destroy() |
getResolution() returns Web Mercator meters per CSS pixel at the view center, without a latitude correction. Generators build pixel-sized geometry in the same units.
Feature groups โ
setLayerFeatureGroups(layerId, groups) replaces a layer with an ordered list of groups. Each group contains one graphic's features and icon resources:
interface FeatureGroup {
id: string;
features: readonly Feature[];
iconResources: readonly IconResource[];
imageOverlays?: readonly ImageOverlayResource[]; // four-corner rasters (Images)
revision?: number; // monotonic content stamp, when the producer supplies one
}The list is the layer's complete desired state, ordered bottom to top. Adapters must preserve it for both rendering and picking.
revision is an optional content stamp. Matching IDs and revisions guarantee identical features and icons, allowing the adapter to skip comparison and upload. A missing revision means โunknownโ; the adapter must compare content and must not treat two missing values as equal.
TacticalDraw caches rendered groups by graphic object identity. A new render gets a new stamp; a cache hit keeps the previous one. View changes and point-symbol generation changes clear the cache.
Supply a new object for a changed graphic
Pass render() a new object whenever a graphic changes. Mutating an existing object and reusing its reference leaves the previous cached render on screen.
Pick-before-click ordering
Dispatch onPick and "click" handlers from the same engine event, with pick handlers first. If the engine exposes separate native events, map the ABI "click" to the event used by onPick. Otherwise click-away handling may close an edit before the next graphic is picked, breaking close-then-pick and Shift-click selection. OpenLayers therefore maps ABI "click" to singleclick.
Adapter-author helpers โ
hitTestFeature(pixel, features, tol, toPixel): Does pixel-space hit tests for all geometry types.pickHitTolerance(originalEvent): Returns 4ย px for a mouse or pen and 12ย px for touch.TOUCH_HIT_TOLERANCE_PXis the handle constant.tryControlMeasureIdFromFeature(feature): Gets the control-measure ID from a rendered feature ID. Returnsnullif the feature is not part of a control measure.coerceHandleStyle(raw): Reads the handle style fromproperties.style.PointerCallbackHub: Supplies a subscriber registry for anEditPointerDriver.bindAbortable(signal, attach): Connects resource removal to anAbortSignaland a dispose function.nearestWorldCopyLng(lng, refLng)andnearestWorldCopyPosition(position, refLng): Keep a longitude or position continuous with a reference longitude across the antimeridian, allowing adapters to project onto the world copy in the current view.combineWithHostSignal(hostSignal?): Combines the TacticalDraw internal abort controller with an optional hostAbortSignal, returning aCombinedAbort(signal,abort(reason, cause?),dispose()).
Conformance tests โ
@orbat-mapper/tactical-draw/testing exports the Vitest suites that the built-in adapters pass. Call them from a spec file with a name and a factory that wraps your adapter:
import {
runFeatureGroupConformance,
runGroupedPickConformance,
} from "@orbat-mapper/tactical-draw/testing";
runFeatureGroupConformance("MyEngineAdapter", () => ({
adapter,
layerId,
// Engine probes: feature ids in paint order, icon/style checks, mutation countsโฆ
engineFeatureIds: () => readEngineFeatureIds(),
// โฆsee FeatureGroupConformanceSubject for the full list.
}));
runGroupedPickConformance("MyEngineAdapter", () => ({ adapter, layerId, pickAt }));runFeatureGroupConformance checks paint order, reconciliation, and revision handling for setLayerFeatureGroups. runGroupedPickConformance checks that picking follows the same order even when the engine's native hit order disagrees. The subject types are exported alongside the suites.
Known limitations โ
Current limitations and their workarounds:
| Edge | Today & workaround |
|---|---|
| Seed semantics for fixed-rule kinds | Draft controlPoints are documented as variable-length seed; fixed-rule kinds have subtler user-vs-canonical-point semantics. |