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

Edit a control measure ​

edit(measure, options?) edits one control measure. Closing the session commits the working state and resolves with its GraphicSnapshot<ControlMeasure>; aborting rejects the promise.

Draw an ambush, then select it to edit. Drag a vertex to reshape it, or use the box corners, rotation handle, and box interior to scale, rotate, and move it. Select elsewhere on the map to commit.

Edit modes:

Loading map…

ts
const { graphic: next } = await td.edit(measure, {
  modes: ["reshape", "transform"],
});

onCommit and onSettled ​

session.onCommit(handler) runs synchronously once when the edit closes, whether through close(), a click away, or selection of another graphic. It receives the same snapshot returned by close() and the edit() promise, and does not run after an abort.

session.onSettled(handler) runs once after commit or abort, after onCommit and after td.activeSession becomes null. A late subscription runs asynchronously. TransformSession uses the same timing for snapshot arrays. See the reference table for all session fields.

onTransientChange ​

session.onTransientChange(listener) reports live geometry during vertex, midpoint, and transform-box drags. Events contain the working points, the active pointIndex, and a phase: "start", "move", or "end". The final phase occurs on both commit and cancel, before the gesture's onChange event.

Transform-box gestures move all points, so pointIndex is null. Label drags do not emit this event because they leave control points unchanged.

Use it for live UI feedback, such as a radius that follows a circle handle. See Show a live radius for a full example.

Modes ​

ModeBehaviour
"reshape" (default)Shows a vertex handle at each control point. Shows midpoint handles on editable segments. Move a midpoint to insert a point. For a variable-length control measure, press Alt/Option and select a vertex to delete it.
"transform" (default)Shows an oriented box with four scale corners, a rotation handle, and a draggable interior. See The transform box.
"delete"Is exclusive. If you specify it, the active set becomes ["delete"]. Shows only vertex handles. Select a vertex to remove it. A fixed trailing slot or the minimum point quantity prevents removal.

reshape and transform can run in the same session and are both enabled by default.

The transform box ​

"transform" draws an oriented selection box around the graphic:

  • Corner handles: drag to scale uniformly around the opposite corner.
  • Rotation handle: drag the handle 28 pixels above the top edge to rotate around the box center.
  • Box interior: drag away from a handle to move the entire graphic. A 3-pixel threshold filters out accidental movement.

The box starts axis-aligned. Its angle exists only for the current session; the control measure stores rotated points, not a box angle. TacticalDraw refits the box after pan and zoom so it follows the current projection. Box gestures do not apply kind-specific draw rulesβ€”only vertex and midpoint drags in reshape mode do.

Set interactionStyle.boxPadding to add pixel spacing between the box and the rendered geometry. It defaults to 0 and is useful when handles overlap the graphic.

EditOptions ​

FieldDefaultWhat it does
modes["reshape", "transform"]Active edit modes (see above).
closeOnClickAwaytrueClicking outside preview/handles closes the edit.
onSessionNot setRuns once when the edit starts, before pointer events. The same session is available as td.activeSession.
signalNot setAborts the operation. The promise rejects with the reason "signal".
guidetrueOpt out of edit guide emission. Auto-suppressed for point kinds, and for fixed-length kinds unless the kind's draw rule opts back in via showGuide.
interactionStylector defaultPer-call style override (guide + handle slots), merged per-slot, per-property.
sizeAnchor"ground"Sets the anchor for geometry and label sizes. The "ground" value converts pixel sizes to meters at commit. The "screen" value keeps pixel sizes. The active preview stays screen-anchored. Use getSizeAnchor(measure) to get the current anchor. Use session.setSizeAnchor() to change it.

See Validate edits and Drive style & options from UI controls for common edit-session integrations.

Multi-select ​

editMany(measures, options?) places one transform box around a group. Group edits support scale, rotation, and translation, but not reshape.

ts
const next = await td.editMany([measureA, measureB], {
  onSession: (session) => {
    groupSession = session;
  },
});
// Key snapshots by measure id, not by input position.
next.forEach((snapshot) => upsert(snapshot.graphic));

The function resolves with one snapshot per member still selected at close time and uses the same abort behavior as edit. Because membership can change, key snapshots by snapshot.graphic.id, never by array position.

Session-local undo and redo ​

Every edit or transform session has its own session.history. A completed vertex, label, translation, rotation, or scale gesture creates one step; pointer-preview frames do not. Changes to the graphic, options, style, text, label placement, and size anchor are also undoable. Interaction settings such as setModes() are not.

ts
const unsubscribe = session.history.subscribe((state) => {
  undoButton.disabled = !state.canUndo;
  redoButton.disabled = !state.canRedo;
});

undoButton.onclick = () => session.history.undo();
redoButton.onclick = () => session.history.redo();

Undo and redo update the preview and handles without settling the session. Returning to the initial state clears dirty; making a change after undo clears redo history. close() commits the current state and abort() discards it. After settlement, undo and redo return false. History is capped at 100 completed steps.

Disable document-level undo while a session is active, even if its local history is empty. In group transforms, setGraphics() establishes a new baseline and clears both stacks. Membership changes and emitted exit snapshots cannot be undone within the session.

Mutable membership ​

Use session.setGraphics(measures) to replace the members of an active group without restarting the session or flickering the box:

ts
// Add or remove members without a session boundary.
const removed = groupSession.setGraphics(nextSelection);
// Add the exit snapshots in `removed` to your document.
// Then, call td.render(...).
removed.forEach((snapshot) => upsert(snapshot.graphic));

Retained members keep their working state, removed members commit and return exit snapshots, and added members start from their input state. TacticalDraw lifts added members off the graphics layer while active, but does not put removed members back. Save returned snapshots to your document and call td.render(). Passing [] closes the session. close() follows the same write-back contract.

The new member set becomes the history baseline, clearing undo and redo while preserving current geometry.

close() returns committed snapshots synchronously, and the promise resolves with the same array. You can therefore start another interaction immediately.

Use onCommit for exit state ​

setGraphics() returns snapshots for removed members, while the editMany promise returns members present at close. Subscribe to session.onCommit(handler) to handle both through one synchronous callback.

ts
// The promise also resolves with the close-time snapshots.
// A host that uses onCommit can ignore this value.
await td.editMany([measureA, measureB], {
  onSession: (session) => {
    session.onCommit((snapshots) => {
      snapshots.forEach((snapshot) => upsert(snapshot.graphic));
      td.render(documentMeasures());
    });
  },
});

Subscribe once in onSession. The handler receives each committed snapshot once, whether caused by removal, close(), setGraphics([]), or a click away. It does not run after an abort. You may call td.render() inside it; the direct return values and promise still contain the same data.

session.onSettled(handler) runs once after commit or abort, including Escape, preemption, destroy(), and abort signals. It runs after the closing onCommit batch and after td.activeTransformSession becomes null, making it a safe place to clear selection state. Late subscriptions run asynchronously once.

The dashed group box encloses the combined rendered geometry, with a thin outline around each member. Scale, rotation, and translation apply rigidly to the whole group; individual members cannot be reshaped during the transform.

EditManyOptions resembles EditOptions but omits modes and guide because groups always use the transform box. sizeAnchor applies to the whole group. onSession receives a TransformSession, also available as td.activeTransformSession. Group sessions do not expose setOptions() or setStyle() because those changes apply to individual kinds.

A one-element array behaves like edit(measure, { modes: ["transform"] }): it shows the box without reshape handles.

Use syncTransformGraphics to synchronize a group ​

Use td.syncTransformGraphics when selection may change during a transform. It updates an active session with setGraphics() or starts editMany() when none exists, while routing exit snapshots through one subscription:

ts
function setSelection(measures: ControlMeasure[]) {
  td.syncTransformGraphics(measures, {
    onSession: (session) => {
      session.onCommit((snapshots) => {
        snapshots.forEach((snapshot) => upsert(snapshot.graphic));
        td.render(documentMeasures());
      });
    },
  });
}

// For a Shift-click, call setSelection with the new ID set.
setSelection([alpha, bravo]);
setSelection([alpha]); // bravo's exit snapshot folds in via onCommit
setSelection([]); // closes the session; alpha's exit snapshot folds in

Add session.onSettled(...) in the same callback to handle completion and aborts from Escape, preemption, or destroy(). syncTransformGraphics owns the underlying promise, so you do not need to retain it.

Options, including onSession, are read only when a new transform starts. Later calls update membership without changing sizeAnchor, box angle, or click-away behavior. Pass [] to close an active session; it is a no-op when none exists.