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

Reference ​

The full surface, for when the recipes aren't enough.

Constructor & options ​

ts
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 },
  },
});
OptionDefaultDescription
layersfacade allocatesOptional host layer ids for graphics, preview, guide, handles. Host-supplied ids survive destroy().
graphicsStyle{ color: "#000000", strokeWidth: 2 }Lowest-priority default style. Monocolor model: color paints stroke and fill; strokeColor / fillColor are per-channel overrides.
interactionStylebuilt-insUnified affordance style. Slots: guide, vertexHandle, midpointHandle, scaleHandle, rotateHandle, boxOutline, plus boxPadding (px, default 0) to inflate the fitted transform box. Per-call overrides merge on top.
generateIdtd-1, td-2, …Used when draw() commits a control measure whose draft had no id.

Layers ​

Four slots; omitted slots are facade-allocated and removed on destroy.

LayerHolds
graphicsAuthoritative rendered measures from render().
previewGenerated preview geometry during draw/edit.
guideRubber-band polyline and optional area-closing segment.
handlesEdit handles for reshape (vertex/midpoint) and transform (box outline, scale corners, rotate grip).

layerIds is @internal

td.layerIds is exposed for tests and internal controllers but may move in a future release. Use onGraphicPick() for committed-measure picks; hosts should not depend on the graphics layer id. For "does TacticalDraw own this pixel?" (host click-away / deselection logic), use ownsInteractionAt instead of querying layers directly.

Data shapes ​

TacticalDraw stores and returns the public @orbat-mapper/control-measures shapes. You own the persisted ControlMeasure[]; the facade creates, edits, and renders them.

ControlMeasure<K> ​

ts
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)
  schemaVersion?: 1;
}

ControlMeasureStyle ​

ts
interface ControlMeasureStyle {
  color?: string; // monocolor input β€” resolved into stroke + fill
  strokeColor?: string; // per-channel override
  strokeWidth?: number;
  strokeDash?: number[];
  fillColor?: string; // per-channel override
}

Layer style, facade graphicsStyle, measure style, and generator pattern hints merge before adapters draw the GeoJSON. See Styling for the full cascade.

GraphicSnapshot<G> β€” what draw/edit return ​

ts
interface GraphicSnapshot<G extends Graphic = Graphic> {
  graphic: G; // deep-cloned β€” safe to retain
  render: G extends PointSymbol ? PointSymbolRender : ControlMeasureRender;
}

The graphic is cloned with structuredClone, so held references survive later session changes. Rendered feature ids are ${cmId}:${part}:${index} β€” recover the measure id with tryControlMeasureIdFromFeature().

TransformBox ​

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

The oriented selection box drawn around scale/rotate/translate handles β€” 4 corner scale handles, a rotate grip above the top edge. Exported so callers can describe box state (e.g. for custom rendering) without reaching into the internal placement/pick geometry.

InteractionHit β€” ownsInteractionAt's return value ​

ts
interface InteractionHit {
  layer: "handles" | "preview" | "graphics"; // topmost match wins
  feature: Feature; // the hit feature β€” do not mutate
  measureId?: string; // set when the feature belongs to a control measure
}

td.ownsInteractionAt(pixel, options?) returns this (or null) β€” see Composing with host click handling.

Sessions ​

onSession hands you a live session for imperative control while an interaction runs. Also available as td.activeSession β€” returns null while idle, after settlement, and during fixed-length draws.

DrawSession ​

MemberDescription
controlPointsLive snapshot of committed draw points.
canCommittrue when the point count is inside the kind's valid range.
minControlPoints / maxControlPointsPoint-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 β€” not rubber-band pointer ticks.
onCommit(handler)Subscribes to the committed snapshot β€” the same reference the draw() promise resolves with. Fires synchronously, exactly once, from the producing call; never on abort. Returns an unsubscribe function; subscribing after settle is a no-op.
onSettled(handler)Subscribes to the session's settlement β€” the lifecycle complement of onCommit: fires exactly once, on any settle path (commit or abort), with a SettleReason. Fires after onCommit and after td.activeSession has already been cleared. Unlike onChange/onCommit, registering after the session has settled still delivers β€” asynchronously, exactly once.

EditSession ​

MemberDescription
measureImmutable edit-start input.
controlPoints / options / styleLive working-state snapshots.
dirtytrue after a meaningful mutation (tolerance-based for geometry).
historySession-local undo/redo for completed gestures and authored programmatic mutations. Exposes observable state, undo(), redo(), and subscribe(). Both stacks clear when the session settles.
modesActive edit modes.
sizeAnchorLive size anchor for the measure under edit β€” "ground" bakes pixel sizes (geometry and label) to meters on commit, "screen" keeps them zoom-aware. Only meaningful for measures carrying a pixel size.
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.
setOptions(partial)Shallow-merges options, re-renders preview, marks dirty, emits onChange.
setStyle(partial)Shallow-merges style, re-renders preview, marks dirty, emits onChange.
setSizeAnchor(anchor)Flips the size anchor, re-renders the emitted snapshot, marks dirty, emits onChange. No-op for measures with no pixel size; the in-flight preview stays screen-locked until commit.
onChange(listener)Subscribes to completed gesture changes.
onCommit(handler)Subscribes to the committed snapshot β€” the same reference close() returns and the edit() promise resolves with. Fires synchronously, exactly once, from the producing call; never on abort. Returns an unsubscribe function; subscribing after settle is a no-op.
onSettled(handler)Subscribes to the session's settlement β€” the lifecycle complement of onCommit: fires exactly once, on any settle path (commit or abort), with a SettleReason. Fires after onCommit and after td.activeSession has already been cleared. Unlike onChange/onCommit, registering after the session has settled still delivers β€” asynchronously, exactly once.

EditChangeEvent

Listeners receive measure and previous (each a GraphicSnapshot<ControlMeasure>) plus the owning session, and may call reject(), close(), or abort(). Geometry drags, setOptions(), setStyle(), and setSizeAnchor() each emit one event; setModes() does not.

TransformSession ​

The group counterpart of EditSession, delivered by editMany's onSession and available as td.activeTransformSession. No reshape, no per-member setOptions / setStyle β€” only the shared box's scale / rotate / translate gestures, applied to every member at once. The member set is mutable via setGraphics, so snapshot arrays are keyed by snapshot.graphic.id rather than by index.

MemberDescription
measuresLive view of the current members' join-time inputs (a fresh array per read). Grows/shrinks with setGraphics; key snapshots by measure.id, not by index.
controlPointsFor(id)Live working control points for the member with that measure id, or undefined if no member carries it.
dirtytrue when any member's working points differ from its edit-start input beyond tolerance.
historySession-local undo/redo for completed group transforms. A setGraphics() call establishes a new baseline and clears both stacks; membership changes are not undoable.
sizeAnchorLive size anchor, applied group-wide.
setSizeAnchor(anchor)Switches the group's size anchor; takes effect on the next snapshot and on close().
setGraphics(measures)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<ControlMeasure>[] (empty when none removed), or undefined after the session has settled. setGraphics([]) delegates to close(). Duplicate ids throw TypeError.
close()Commits: returns the members' GraphicSnapshot<ControlMeasure>[] (those in the set at close time β€” the same reference the editMany promise resolves with and delivered to onCommit) and resolves the promise; undefined after the session has settled.
abort()Rejects with reason "session".
onChange(listener)Subscribes to completed group gestures (corner scale, rotate, or body-translate drag end) β€” never rubber-band ticks.
onCommit(handler)Subscribes to committed exit-snapshot batches β€” the single delivery channel for every snapshot array this session commits: the members removed by a setGraphics call (only when at least one was removed) and the close-time members on any commit path. Fires synchronously, exactly once per batch, before the triggering call returns; never on abort/reject. Returns an unsubscribe function; subscribing after settle is a no-op.
onSettled(handler)Subscribes to the session's settlement β€” the lifecycle complement of onCommit: fires exactly once, on any settle path (commit or abort), with a SettleReason ("close" on commit paths, otherwise the abort reason, or "error"). Fires after the close batch's onCommit and after td.activeTransformSession has already been cleared, so fold-then-render works from inside the handler. Unlike onChange/onCommit, registering after the session has settled still delivers β€” asynchronously, exactly once.

TransformChangeEvent

Mirrors EditChangeEvent: listeners receive measures and previous arrays (index-aligned with session.graphics as of that emission) plus the owning session, and may call reject(), close(), or abort(). Key by measure.id rather than relying on index alignment across membership changes.

Removed members and render()

setGraphics and close() do not write removed members back to the graphics layer β€” fold the returned snapshots into your document and call td.render(), the same contract for both. onCommit is the recommended single subscription for this fold: it delivers the same arrays as both the setGraphics return value and the close()/editMany promise resolution, so a host only needs one handler instead of folding at two call sites. Pair it with onSettled as the lifecycle signal β€” onCommit says what was committed, onSettled says the session is over and why, including abort paths onCommit never sees.

Errors & abort reasons ​

ErrorWhen
TacticalDrawAbortErrorNormal interaction cancellation. Extends DOMException (name: "AbortError") with a closed reason. Test with isTacticalDrawAbortError(e).
TacticalDrawDestroyedErrorSynchronous throw when a public method is called after destroy().
Errorrender() throws on duplicate measure ids.
TypeErroredit() 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: "escape", "signal", "preempted", "destroyed", "session", "removed". DrawSession.onSettled, EditSession.onSettled, and TransformSession.onSettled all report these same reason strings, plus "close" (any commit path) and "error" (the commit-time snapshot build threw).

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

Most hosts consume an engine package and never touch this. Adapter authors implement MapAdapter, usually by extending BaseMapAdapter, which handles layer ids, feature caching, event/view tokens, reconciliation, and teardown.

CapabilityMethods
Vector layersaddVectorLayer, removeLayer, setLayerStyle, setLayerFeatures, updateLayerFeatures, removeFeatures, clearLayer, getLayerFeatures
Map eventson/off for click, pointermove, dblclick
Pick eventsonPick(layerId, handler, opts?); control-measure hits dispatch the measure id and non-measure features are ignored
Projection & viewtoLonLat, fromLonLat, getPixelFromCoordinate, getViewportSize, getResolution, getZoom, onViewChange, offViewChange
Interaction controlspanByPixels, setCursor, setDoubleClickZoomEnabled, createEditPointerDriver
Lifecycledestroy()

Pick-before-click ordering

For a given user click, onPick handlers and "click" handlers must be delivered from the same engine event, picks first. If your engine fires its own pick-ish event and a generic click event separately (with different timing β€” e.g. OpenLayers' click fires before the singleclick that hit detection needs to debounce dblclick), map the ABI "click" to whichever native event onPick also uses. Otherwise the click-away logic can see an empty-map click and close an active edit before the pick on another measure arrives, breaking close-then-pick re-dispatch and shift-click multi-select. The OpenLayers adapter maps ABI "click" to OL singleclick for exactly this reason.

Adapter-author helpers ​

  • hitTestFeature(pixel, features, tol, toPixel) β€” pixel-space hit-testing for all geometry types.
  • pickHitTolerance(originalEvent) β€” 4Β px for mouse/pen, 12Β px for touch. TOUCH_HIT_TOLERANCE_PX is the handle constant.
  • tryControlMeasureIdFromFeature(feature) β€” recovers the measure id from a rendered feature's id, or null if the feature isn't a control measure part.
  • coerceHandleStyle(raw) β€” reads per-feature handle style from properties.style.
  • PointerCallbackHub β€” subscriber registry for building an EditPointerDriver.
  • bindAbortable(signal, attach) β€” wires teardown to both an AbortSignal and a dispose fn.
  • nearestWorldCopyLng(lng, refLng) / nearestWorldCopyPosition(position, refLng) β€” keep a longitude (or position) continuous with a reference longitude across the antimeridian, so adapters can project onto the world copy the view is actually looking at.
  • combineWithHostSignal(hostSignal?) β€” composes the faΓ§ade's internal abort controller with an optional host AbortSignal, returning a CombinedAbort (signal, abort(reason, cause?), dispose()).

Known rough edges ​

Honest notes on the current surface β€” not blockers, just things to expect.

EdgeToday & workaround
Seed semantics for fixed-rule kindsDraft controlPoints are documented as variable-length seed; fixed-rule kinds have subtler user-vs-canonical-point semantics.