terra-draw sketch binding (@honua/sdk-js/runtime)

Status: experimental (issue #492, Esri Widget Cliff workstream). Public entrypoint: @honua/sdk-js/runtimebindTerraDrawSketch, createTerraDrawSketch, createTerraDrawSnapping, terraDrawSketchToolCapabilities, editSketchToolForTerraDrawMode, TERRA_DRAW_SKETCH_TOOL_MODES.

Interactive drawing on the MapLibre path is provided by terra-draw, the engine-agnostic community successor to mapbox-gl-draw. The SDK does not fork or wrap terra-draw's rendering — it adapts terra-draw's mode events onto the renderer-neutral EditSketchWorkflowModel from @honua/sdk-js/contract, so undo/redo, validation, snapping configuration, and edit-session applyEdits submission are workflow features that apply to every sketch the same way.

Peer setup

terra-draw and terra-draw-maplibre-gl-adapter are optional peer dependencies. The SDK never imports them at module scope: bindTerraDrawSketch accepts a duck-typed instance your app constructed, and createTerraDrawSketch loads both packages lazily via dynamic import() (throwing a descriptive error when they are missing). Importing @honua/sdk-js/runtime without terra-draw installed stays side-effect free; the tree-shake:runtime-terra-draw-sketch bundle-budget fixture guards this.

npm i terra-draw terra-draw-maplibre-gl-adapter   # only if you use the binding

Modes and tools

terra-draw mode names map onto the contract EditSketchTool values:

terra-draw mode EditSketchTool
point, marker point
linestring, polyline, freehand-linestring line
polygon, freehand, sector, sensor polygon
rectangle, angled-rectangle rectangle
circle circle
select, render, static, custom modes — (not applied)

The contract defaults mark rectangle and circle as unsupported (they need renderer support); pass terraDrawSketchToolCapabilities() as the workflow's sketchTools to mark the terra-draw-backed tools supported. buffer stays unsupported — it needs geometry-service support, not a drawing engine.

One-call setup

import { createEditSketchWorkflow, createSnapIndex } from "@honua/sdk-js/contract";
import { createTerraDrawSketch, terraDrawSketchToolCapabilities } from "@honua/sdk-js/runtime";

const workflow = createEditSketchWorkflow({
  source,                                          // any editable contract Source
  kind: "create",
  feature: { attributes: {} },
  sketchTools: terraDrawSketchToolCapabilities(),
  snapping: { enabled: true, tolerance: 16 },
});

const index = createSnapIndex();
index.setSourceFeatures("parcels", parcels);

const sketch = await createTerraDrawSketch(map, {
  model: workflow,
  snapping: { index, model: workflow },
});

sketch.setTool("polygon");   // user draws on the map
sketch.undo();               // workflow history, mirrored back into terra-draw
await workflow.submit();     // edit-session applyEdits path, unchanged

Every terra-draw finish event — freshly drawn features and select-mode edits (dragged features, dragged/inserted/deleted coordinates) — flows through model.setSketchGeometry, so each is one undoable workflow step. Deleting the tracked feature stages an undoable null geometry. undo()/redo() on the handle drive the model and push the restored geometry back into terra-draw.

Apps that need custom terra-draw mode options construct the instance themselves and call bindTerraDrawSketch(draw, { model }) — the binding only attaches listeners (detached by handle.remove()).

Geometry round-trip and reprojection

terra-draw emits EPSG:4326 GeoJSON. When the edit source stores another CRS, pass the @honua/sdk-js/geometry reprojection helpers through the binding:

import { toWebMercator, toWgs84 } from "@honua/sdk-js/geometry";

bindTerraDrawSketch(draw, {
  model,
  transformGeometry: (g) => toWebMercator(g, 4326),  // model/applyEdits side
  restoreGeometry: (g) => toWgs84(g, 3857),          // terra-draw side (undo/redo sync)
});

Snapping

createTerraDrawSnapping({ index, model }) produces a function with terra-draw's Snapping.toCustom shape, backed by the SDK SnapIndex and the workflow's live snapping config — the same deterministic candidate ordering as bindEditSketchSnapping. createTerraDrawSketch wires it automatically.

The seam is honest: terra-draw exposes per-mode snapping options on its linestring, polygon, and polyline modes only. Point, rectangle, circle, and freehand modes have no custom-snapping hook in terra-draw's pointer pipeline, so those modes keep terra-draw-native behavior. If you need snapped point placement, resolve the position with SnapIndex.resolve() on click and set the geometry through the workflow directly.

esri-compat delegation

The Sketch shim (@honua/sdk-js/esri-compat) feature-detects a binding via its sketchBinding option: any object with a callable setTool (the bindTerraDrawSketch / createTerraDrawSketch handle qualifies) receives create tool-mode changes (polylineline), update() switches to select mode, and cancel/reset leave the drawing mode. Without a binding — including when terra-draw is not installed — the shim keeps its existing headless behavior and events.

Example

examples/sketch-editing demonstrates draw/edit/delete with undo/redo, snapping, and fixture-lane applyEdits (npm run demo:sketch-editing, Playwright smoke via npm run test:playwright:sketch-editing).