@orbat-mapper/tactical-draw 0.2.0-alpha.2 → 0.2.0-alpha.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { ControlMeasure, ControlMeasureKind, ControlMeasureSnapshot, ControlMeasureStyle } from "@orbat-mapper/control-measures";
1
+ import { AmplifierPlacement, AmplifierPlacements, ControlMeasure, ControlMeasureKind, ControlMeasureRender, ControlMeasureStyle, TextAmplifiers } from "@orbat-mapper/control-measures";
2
2
  import { Feature, Position } from "geojson";
3
3
 
4
4
  //#region src/gestures/types.d.ts
@@ -54,6 +54,16 @@ interface StyleOptions {
54
54
  pointRadius?: number;
55
55
  text?: TextStyleOptions;
56
56
  }
57
+ /**
58
+ * Text (label) style for a layer. When `labelProperty` is set, adapters also
59
+ * read per-feature label properties: `rotation` (radians, rendered as
60
+ * `π − rotation`), the size anchor's `textSizeMeters` (ground) or
61
+ * `textSizePixels` (screen) (ADR-0028), and `textAnchor` (`"start"` = text
62
+ * begins at the label point, `"end"` = text ends there; absent = centered).
63
+ * A ground-anchored label renders at
64
+ * `clamp(textSizeMeters / resolution, textSizeMinPixels, textSizeMaxPixels)`;
65
+ * a screen-anchored one at a constant `textSizePixels`, unclamped.
66
+ */
57
67
  interface TextStyleOptions {
58
68
  text?: string;
59
69
  labelProperty?: string;
@@ -62,6 +72,18 @@ interface TextStyleOptions {
62
72
  strokeColor?: string;
63
73
  strokeWidth?: number;
64
74
  rotation?: number;
75
+ /**
76
+ * Lower edge of the ground-anchored label clamp band (ADR-0028): text never
77
+ * renders below this pixel size, however far the view zooms out.
78
+ * @default 8
79
+ */
80
+ textSizeMinPixels?: number;
81
+ /**
82
+ * Upper edge of the ground-anchored label clamp band (ADR-0028): text never
83
+ * renders above this pixel size, however far the view zooms in.
84
+ * @default 24
85
+ */
86
+ textSizeMaxPixels?: number;
65
87
  }
66
88
  /**
67
89
  * Per-feature style for edit handles (vertex / midpoint / translate / rotate).
@@ -78,6 +100,14 @@ interface HandleStyle {
78
100
  pointRadius?: number;
79
101
  }
80
102
  type LayerId = string;
103
+ /** Canonical SVG resource shared by point features through `iconId`. */
104
+ interface IconResource {
105
+ id: string;
106
+ svg: string;
107
+ width: number;
108
+ height: number;
109
+ anchor: [number, number];
110
+ }
81
111
  /**
82
112
  * Click-style hit event delivered by {@link MapAdapter.onPick}.
83
113
  *
@@ -115,6 +145,8 @@ interface MapAdapter {
115
145
  * Set the style for a specific layer.
116
146
  */
117
147
  setLayerStyle(layerId: LayerId, style: StyleOptions): void;
148
+ /** Reconcile the layer's desired shared icon resources. */
149
+ reconcileLayerIconResources(layerId: LayerId, resources: readonly IconResource[]): void;
118
150
  /**
119
151
  * Set features on a vector layer.
120
152
  * Features should be standard GeoJSON Features.
@@ -165,6 +197,15 @@ interface MapAdapter {
165
197
  onPick(layerId: LayerId, handler: PickHandler, opts?: PickOptions): () => void;
166
198
  /**
167
199
  * Map Event Listeners
200
+ *
201
+ * Ordering contract: a `"click"` handler and the `onPick` handlers for the
202
+ * same user click MUST observe the pick first — adapters deliver picks and
203
+ * `"click"` from the same underlying engine event, in registration order.
204
+ * The draw/edit controllers register their pick subscriptions before their
205
+ * click handler and read the stashed pick inside it (click-away close /
206
+ * close-then-pick re-dispatch); an adapter that fires `"click"` from an
207
+ * earlier engine event than its picks breaks that contract (e.g. OL
208
+ * `click` vs `singleclick`).
168
209
  */
169
210
  on(eventType: "click" | "pointermove" | "dblclick", handler: MapEventHandler): void;
170
211
  off(eventType: "click" | "pointermove" | "dblclick", handler: MapEventHandler): void;
@@ -207,6 +248,12 @@ interface MapAdapter {
207
248
  * Get current map zoom level
208
249
  */
209
250
  getZoom(): number | undefined;
251
+ /**
252
+ * Get current view bearing, radians — the clockwise-from-north compass
253
+ * direction that screen-up points to (MapLibre-bearing semantics). `0` for
254
+ * engines/views that don't rotate.
255
+ */
256
+ getBearing(): number;
210
257
  /**
211
258
  * Listen for view changes (zoom/resolution)
212
259
  */
@@ -223,6 +270,13 @@ interface MapAdapter {
223
270
  * destruction.
224
271
  */
225
272
  destroy(): void;
273
+ /**
274
+ * Returns the features currently set on the layer — the adapter-side cache
275
+ * of the last `setLayerFeatures`/`updateLayerFeatures`/`removeFeatures`/
276
+ * `clearLayer` state. Empty array for an unknown layer. Callers must not
277
+ * mutate the returned array or features.
278
+ */
279
+ getLayerFeatures(layerId: LayerId): readonly Feature[];
226
280
  }
227
281
  //#endregion
228
282
  //#region src/base-map-adapter.d.ts
@@ -254,6 +308,8 @@ declare abstract class BaseMapAdapter<TLayerHandle, TEventToken, TViewToken> imp
254
308
  protected viewChangeTokens: Map<() => void, TViewToken>;
255
309
  protected featureCache: Map<string, Feature<import("geojson").Geometry, import("geojson").GeoJsonProperties>[]>;
256
310
  protected featuresChangedListeners: Map<string, Set<(features: Feature[]) => void>>;
311
+ protected iconResources: Map<string, IconResource>;
312
+ private layerIconResourceIds;
257
313
  private layerCounter;
258
314
  private destroyed;
259
315
  protected nextLayerId(): LayerId;
@@ -261,6 +317,7 @@ declare abstract class BaseMapAdapter<TLayerHandle, TEventToken, TViewToken> imp
261
317
  style?: StyleOptions;
262
318
  }): LayerId;
263
319
  removeLayer(layerId: LayerId): void;
320
+ reconcileLayerIconResources(layerId: LayerId, resources: readonly IconResource[]): void;
264
321
  setLayerFeatures(layerId: LayerId, features: readonly Feature[]): void;
265
322
  updateLayerFeatures(layerId: LayerId, features: readonly Feature[]): void;
266
323
  removeFeatures(layerId: LayerId, ids: readonly string[]): void;
@@ -275,6 +332,10 @@ declare abstract class BaseMapAdapter<TLayerHandle, TEventToken, TViewToken> imp
275
332
  protected emitLayerFeaturesChanged(layerId: LayerId, features: Feature[]): void;
276
333
  protected onLayerFeaturesChanged(layerId: LayerId, listener: (features: Feature[]) => void): () => void;
277
334
  protected destroyEngineResources(): void;
335
+ protected applyIconResources(resources: readonly IconResource[]): void;
336
+ protected getIconResource(id: string): IconResource | undefined;
337
+ protected removeIconResource(id: string): void;
338
+ private pruneIconResources;
278
339
  protected abstract createLayer(layerId: LayerId, style?: StyleOptions): TLayerHandle;
279
340
  protected abstract destroyLayer(handle: TLayerHandle): void;
280
341
  protected abstract applyFeatureOps(layerId: LayerId, ops: FeatureOps): void;
@@ -297,6 +358,12 @@ declare abstract class BaseMapAdapter<TLayerHandle, TEventToken, TViewToken> imp
297
358
  abstract setDoubleClickZoomEnabled(enabled: boolean): void;
298
359
  abstract getResolution(): number | undefined;
299
360
  abstract getZoom(): number | undefined;
361
+ /**
362
+ * Default for engines/views that don't rotate (Leaflet). Rotating engines
363
+ * (MapLibre, OpenLayers) override this.
364
+ */
365
+ getBearing(): number;
366
+ getLayerFeatures(layerId: LayerId): readonly Feature[];
300
367
  }
301
368
  //#endregion
302
369
  //#region src/pick-utils.d.ts
@@ -317,6 +384,20 @@ declare function bindAbortable(signal: AbortSignal | undefined, attach: () => ()
317
384
  * when missing or shaped wrong so callers fall through to the layer style.
318
385
  */
319
386
  declare function coerceHandleStyle(raw: unknown): HandleStyle | undefined;
387
+ /** Default lower edge of the ground-anchored label clamp band (ADR-0028). */
388
+ declare const DEFAULT_TEXT_SIZE_MIN_PIXELS = 8;
389
+ /** Default upper edge of the ground-anchored label clamp band (ADR-0028). */
390
+ declare const DEFAULT_TEXT_SIZE_MAX_PIXELS = 24;
391
+ /**
392
+ * Rendered pixel size of a ground-anchored label (ADR-0028):
393
+ * `textSizeMeters / resolution`, clamped to the layer's text-style band.
394
+ * Within the band the text scales with its graphic; past either edge it pins
395
+ * to the bound, so zooming out never yields unreadable smudges and zooming in
396
+ * never yields screen-covering text. Shared by the adapters that resolve text
397
+ * size in code (OpenLayers, Leaflet); the MapLibre adapter evaluates the same
398
+ * clamp inside its per-zoom-stop style expression.
399
+ */
400
+ declare function resolveGroundTextSizePixels(textSizeMeters: number, resolution: number, text?: Pick<TextStyleOptions, "textSizeMinPixels" | "textSizeMaxPixels">): number;
320
401
  //#endregion
321
402
  //#region src/gestures/hit-test.d.ts
322
403
  type ToPixel = (lonLat: Position) => PixelCoordinate | null;
@@ -331,7 +412,14 @@ type ToPixel = (lonLat: Position) => PixelCoordinate | null;
331
412
  *
332
413
  * Returns the first matching feature in iteration order, or null.
333
414
  */
334
- declare function hitTestFeature(pixel: PixelCoordinate, features: Feature[], hitTolerance: number, toPixel: ToPixel): Feature | null;
415
+ declare function hitTestFeature(pixel: PixelCoordinate, features: readonly Feature[], hitTolerance: number, toPixel: ToPixel): Feature | null;
416
+ /**
417
+ * Test a rendered icon-backed Point feature against its actual pixel footprint.
418
+ * The feature carries the adapter-neutral `iconScale` / `iconRotation` values
419
+ * emitted by TacticalDraw; `resource.anchor` is the placement anchor within the
420
+ * unscaled SVG viewport. A positive tolerance pads all four sides.
421
+ */
422
+ declare function hitTestIconFeature(pixel: PixelCoordinate, feature: Feature, resource: IconResource, hitTolerance: number, toPixel: ToPixel, bearing?: number): boolean;
335
423
  //#endregion
336
424
  //#region src/gestures/hit-tolerance.d.ts
337
425
  declare const TOUCH_HIT_TOLERANCE_PX = 22;
@@ -366,6 +454,87 @@ declare class PointerCallbackHub {
366
454
  clear(): void;
367
455
  }
368
456
  //#endregion
457
+ //#region src/point-symbol.d.ts
458
+ type PointSymbolSize = {
459
+ value: number;
460
+ unit: "pixels";
461
+ } | {
462
+ value: number;
463
+ unit: "meters";
464
+ };
465
+ interface PointSymbolGraphicModifiers {
466
+ /** Radians clockwise, relative to the point symbol's own orientation. */
467
+ direction?: number;
468
+ }
469
+ interface PointSymbolColors {
470
+ fill?: string;
471
+ frame?: string;
472
+ icon?: string;
473
+ info?: string;
474
+ outline?: string;
475
+ }
476
+ interface PointSymbolStyle {
477
+ colors?: PointSymbolColors;
478
+ monoColor?: string;
479
+ opacity?: number;
480
+ outlineWidth?: number;
481
+ fill?: boolean;
482
+ frame?: boolean;
483
+ icon?: boolean;
484
+ }
485
+ interface PointSymbol {
486
+ id: string;
487
+ kind: "point-symbol";
488
+ sidc: string;
489
+ position: Position;
490
+ /** Normalized radians clockwise from geographic north. */
491
+ rotation: number;
492
+ /** Milsymbol's octagon-based L dimension. */
493
+ size: PointSymbolSize;
494
+ textAmplifiers?: TextAmplifiers;
495
+ graphicModifiers?: PointSymbolGraphicModifiers;
496
+ style?: PointSymbolStyle;
497
+ rendererOptions?: {
498
+ milsymbol?: Record<string, unknown>;
499
+ };
500
+ properties?: Record<string, unknown>;
501
+ schemaVersion?: 1;
502
+ }
503
+ interface PointSymbolAnchor {
504
+ x: number;
505
+ y: number;
506
+ }
507
+ /** Canonical-size, renderer-neutral point-symbol output. */
508
+ interface PointSymbolRender {
509
+ /** Content-derived identity for adapter-wide native-resource reuse. */
510
+ resourceId: string;
511
+ svg: string;
512
+ /** Renderer-space octagon size used to interpret persisted symbol size. */
513
+ octagonSize: number;
514
+ width: number;
515
+ height: number;
516
+ anchor: PointSymbolAnchor;
517
+ octagonAnchor: PointSymbolAnchor;
518
+ valid: boolean;
519
+ validation?: unknown;
520
+ }
521
+ interface PointSymbolCapability {
522
+ readonly defaultSize: PointSymbolSize;
523
+ readonly defaultRotation: number;
524
+ render(symbol: PointSymbol): PointSymbolRender;
525
+ invalidate(): void;
526
+ }
527
+ declare function isPointSymbol(graphic: unknown): graphic is PointSymbol;
528
+ declare function clonePointSymbol(symbol: PointSymbol): PointSymbol;
529
+ //#endregion
530
+ //#region src/graphic.d.ts
531
+ type Graphic = ControlMeasure | PointSymbol;
532
+ type GraphicRender = ControlMeasureRender | PointSymbolRender;
533
+ interface GraphicSnapshot<G extends Graphic = Graphic> {
534
+ graphic: G;
535
+ render: G extends PointSymbol ? PointSymbolRender : ControlMeasureRender;
536
+ }
537
+ //#endregion
369
538
  //#region src/tactical-draw/abort.d.ts
370
539
  /**
371
540
  * AbortPlumbing for the TacticalDraw façade.
@@ -416,6 +585,7 @@ interface CombinedAbort {
416
585
  declare function combineWithHostSignal(hostSignal?: AbortSignal): CombinedAbort;
417
586
  //#endregion
418
587
  //#region src/tactical-draw/measure-options.d.ts
588
+ declare const DEFAULT_LABEL_SIZE_PIXELS = 14;
419
589
  /**
420
590
  * Where a control measure's size is anchored (ADR-0020). `ground` (the
421
591
  * default) means a meter size fixed to the terrain — static, never re-rendered
@@ -437,9 +607,10 @@ declare function getSizeAnchor(measure: Pick<ControlMeasure, "kind" | "options">
437
607
  //#endregion
438
608
  //#region src/tactical-draw/handles.d.ts
439
609
  /**
440
- * Line-only style surface for the rubber-band guide (ADR-0004). Slot inside
441
- * `InteractionStyle.guide`. Applied as the guide layer's layer style by the
442
- * façade; not per-feature.
610
+ * Line-only style surface for the rubber-band guide (ADR-0004) and the
611
+ * transform-box outline. Slot inside `InteractionStyle.guide` /
612
+ * `InteractionStyle.boxOutline`. Applied as a layer style for the guide, and
613
+ * per-feature (`properties.style`) for the box outline — see ADR-0017.
443
614
  */
444
615
  interface GuideStyle {
445
616
  strokeColor?: string;
@@ -447,25 +618,59 @@ interface GuideStyle {
447
618
  strokeDash?: readonly number[];
448
619
  }
449
620
  /**
450
- * Unified interaction-affordance style surface (ADR-0006). One option object
451
- * for both the guide line and every handle slot. `guide` is delivered as a
452
- * layer style; handle slots ride per-feature on `properties.style`.
621
+ * Unified interaction-affordance style surface (ADR-0006, extended by
622
+ * ADR-0017 for the transform box). One option object for the guide line,
623
+ * the box outline, and every handle slot. `guide` and `boxOutline` are line
624
+ * styles; handle slots ride per-feature on `properties.style`.
453
625
  */
454
626
  interface InteractionStyle {
455
627
  guide?: GuideStyle;
456
628
  vertexHandle?: HandleStyle;
457
629
  midpointHandle?: HandleStyle;
458
- translateHandle?: HandleStyle;
630
+ /** Visible grab target for a draggable rendered text label. */
631
+ labelHandle?: HandleStyle;
632
+ scaleHandle?: HandleStyle;
459
633
  rotateHandle?: HandleStyle;
634
+ boxOutline?: GuideStyle;
635
+ /**
636
+ * Pixel padding added on every side of the fitted transform box
637
+ * (`fitTransformBox`, ADR-0017) so the box doesn't tightly hug the rendered
638
+ * geometry. Default `0`. A scalar, not a style object, but it participates
639
+ * in the same built-in → façade → per-call resolution slot system as every
640
+ * other `InteractionStyle` field (see `TacticalDraw.resolveInteractionStyle`).
641
+ */
642
+ boxPadding?: number;
460
643
  }
461
644
  /**
462
- * Reserved enum from PRD #40. `reshape`, `translate`, and `rotate` produce
463
- * handles. `delete` is an explicit destructive mode: it renders the vertex
464
- * handles (so they can be tapped to remove) but no midpoint/translate/rotate
465
- * grips, and the edit controller deletes the tapped vertex instead of dragging
466
- * it. `scale` is reserved and throws elsewhere.
645
+ * Reserved enum from PRD #40, replaced under ADR-0017 by the transform-box UI.
646
+ * `reshape` renders vertex/midpoint handles; `transform` renders the oriented
647
+ * selection box (4 corner scale handles + a rotate grip + drag-anywhere
648
+ * body translate) via `transform-box.ts`. The two can be combined freely —
649
+ * single-measure edits show both by default. `labeldrag` enables dragging
650
+ * rendered text labels. `delete` is an explicit
651
+ * destructive mode: it renders the vertex handles (so they can be tapped to
652
+ * remove) but no midpoints or box, and the edit controller deletes the tapped
653
+ * vertex instead of dragging it.
654
+ */
655
+ type EditMode = "reshape" | "transform" | "labeldrag" | "delete";
656
+ //#endregion
657
+ //#region src/tactical-draw/session-events.d.ts
658
+ /**
659
+ * The reason a session (`TransformSession`, `EditSession`, or `DrawSession`)
660
+ * settled, delivered to that session's `onSettled` handler (#191 / #194 /
661
+ * #197). `"close"` covers every commit path of the owning session —
662
+ * `TransformSession.close()` / `EditSession.close()` / `DrawSession.commit()`,
663
+ * a click-away close, a gesture commit (dblclick, a second click/tap on the
664
+ * finishing vertex, a click on another graphics-layer measure), or
665
+ * `TransformSession.setGraphics([])`. `"error"` is the commit-time snapshot
666
+ * build throwing (the outer promise rejects with that error, not a
667
+ * `TacticalDrawAbortError`). The remaining values replay
668
+ * `TacticalDrawAbortError.reason` verbatim: `"escape"`, `"signal"`,
669
+ * `"session"` (`session.abort()` / `event.abort()`), `"preempted"`,
670
+ * `"destroyed"`, and `"removed"` (edit / transform only — a measure dropped
671
+ * from `td.render()`'s measure set).
467
672
  */
468
- type EditMode = "reshape" | "translate" | "rotate" | "scale" | "delete";
673
+ type SettleReason = "close" | "error" | TacticalDrawAbortReason;
469
674
  //#endregion
470
675
  //#region src/tactical-draw/edit-session.d.ts
471
676
  /**
@@ -474,17 +679,29 @@ type EditMode = "reshape" | "translate" | "rotate" | "scale" | "delete";
474
679
  *
475
680
  * `measure` is the immutable edit-start input. `controlPoints` is live working
476
681
  * state. After the session settles (`close` / `abort`), every method is a
477
- * silent no-op.
682
+ * silent no-op (except [[onSettled]] — late registration still delivers,
683
+ * asynchronously).
478
684
  */
479
685
  interface EditSession {
480
686
  /** Immutable edit-start input. */
481
- readonly measure: ControlMeasure;
687
+ readonly graphic: ControlMeasure;
688
+ /** Live complete working graphic. Copy on read. */
689
+ readonly workingGraphic: ControlMeasure;
482
690
  /** Live working state. Copy on read. */
483
691
  readonly controlPoints: readonly Position[];
484
692
  /** Live working options. Copy on read. */
485
693
  readonly options: ControlMeasure["options"];
486
694
  /** Live working style. Copy on read. */
487
695
  readonly style: ControlMeasureStyle | undefined;
696
+ /**
697
+ * Live working text amplifiers (MIL-STD-2525E Appendix L Table L-II). Copy
698
+ * on read. `undefined` when the edit-start measure carried none and none
699
+ * have been set since. Keys are stored as given — canonicalisation to
700
+ * doctrinal field codes happens in the core render seam, not here.
701
+ */
702
+ readonly textAmplifiers: TextAmplifiers | undefined;
703
+ /** Live user-adjusted anchors for individually rendered text amplifiers. */
704
+ readonly amplifierPlacements: AmplifierPlacements | undefined;
488
705
  /** `true` after the first working-copy mutation; back to `false` within tolerance. */
489
706
  readonly dirty: boolean;
490
707
  readonly modes: readonly EditMode[];
@@ -495,18 +712,24 @@ interface EditSession {
495
712
  */
496
713
  readonly sizeAnchor: SizeAnchor;
497
714
  /**
498
- * Resolve the outer `edit` promise with a `ControlMeasureSnapshot` of the
499
- * current working state.
715
+ * Commit: returns the `GraphicSnapshot<ControlMeasure>` of the current working state
716
+ * and resolves the outer `edit` promise with the same value (the same
717
+ * reference — no double-bake of ground-anchored sizes). Returns `undefined`
718
+ * after the session has settled (idempotent no-op).
500
719
  */
501
- close(): void;
720
+ close(): GraphicSnapshot<ControlMeasure> | undefined;
502
721
  /** Reject the outer `edit` promise with reason `"session"`. */
503
722
  abort(): void;
504
723
  /**
505
- * Switch the active edit-mode set. `"reshape"`, `"translate"`, and `"rotate"`
506
- * produce handles; `"delete"` is exclusive (vertex-tap removal) and collapses
507
- * the set to delete-only. `"scale"` is rejected with `TypeError` per PRD #40.
724
+ * Switch the active edit-mode set. `"reshape"` produces vertex/midpoint
725
+ * handles; `"transform"` renders the oriented selection box (corner scale
726
+ * handles, rotate grip, body translate — ADR-0017); `"delete"` is exclusive
727
+ * (vertex-tap removal) and collapses the set to delete-only. An unknown mode
728
+ * is rejected with `TypeError`.
508
729
  */
509
730
  setModes(modes: readonly EditMode[]): void;
731
+ /** Atomically replace the live working graphic while preserving id and kind. */
732
+ updateGraphic(updater: (graphic: ControlMeasure) => ControlMeasure): void;
510
733
  /** Shallow-merge per-measure generator options into the live working copy. */
511
734
  setOptions(partial: ControlMeasure["options"]): void;
512
735
  /**
@@ -518,6 +741,22 @@ interface EditSession {
518
741
  setSizeAnchor(anchor: SizeAnchor): void;
519
742
  /** Shallow-merge per-measure style into the live working copy. */
520
743
  setStyle(partial: ControlMeasureStyle): void;
744
+ /**
745
+ * Shallow-merge into the live working text amplifiers. A value of
746
+ * `undefined` or an empty/whitespace-only string deletes that key; the
747
+ * working record collapses back to `undefined` once it holds no keys. A
748
+ * silent no-op after the session has settled (like [[setOptions]]).
749
+ */
750
+ setTextAmplifiers(partial: Record<string, string | undefined>): void;
751
+ /**
752
+ * Merge field-keyed label overrides (position and/or rotation). The merge is
753
+ * **field-wise per key**: a bare `Position` value means `{ position }`;
754
+ * fields absent from the incoming value (e.g. `rotation` when only
755
+ * `position` is supplied) are preserved from the existing entry; an
756
+ * explicit `undefined` value deletes the key entirely — a full reset to the
757
+ * doctrinal placement (both position and rotation).
758
+ */
759
+ setAmplifierPlacements(partial: Record<string, AmplifierPlacement | undefined>): void;
521
760
  /**
522
761
  * Subscribe to working-copy changes. The listener receives an
523
762
  * [[EditChangeEvent]] for each completed gesture (drag end, insert/delete
@@ -530,6 +769,47 @@ interface EditSession {
530
769
  * have run.
531
770
  */
532
771
  onChange(listener: (event: EditChangeEvent) => void): () => void;
772
+ /**
773
+ * Subscribe to the committed snapshot — the single delivery channel for the
774
+ * `GraphicSnapshot<ControlMeasure>` this session ever commits (#197, mirroring
775
+ * `TransformSession.onCommit` #191). Fires exactly once, synchronously from
776
+ * within the [[close]] call that produced it (`session.close()`,
777
+ * `event.close()` from [[onChange]], a click-away close, or a click on
778
+ * another graphics-layer measure), with the same reference [[close]]
779
+ * returns and the outer `edit` promise resolves with. Never fires on abort.
780
+ *
781
+ * Listener exceptions are collected; after every listener has run, they are
782
+ * rethrown (one if single, `AggregateError` otherwise) — mirrors
783
+ * [[onChange]]. Returns an unsubscribe function; registering after the
784
+ * session has settled returns a no-op unsubscribe and the listener never
785
+ * fires.
786
+ */
787
+ onCommit(handler: (snapshot: GraphicSnapshot<ControlMeasure>) => void): () => void;
788
+ /**
789
+ * Subscribe to the session's settlement — the lifecycle complement of
790
+ * [[onCommit]]: `onCommit` delivers *what* was committed, `onSettled` says
791
+ * the session is *over* and why (#197, mirroring
792
+ * `TransformSession.onSettled` #194). Fires exactly once, when the session
793
+ * settles by any path, commit or abort, with a `SettleReason`.
794
+ *
795
+ * - On the commit path it fires **after** [[onCommit]]'s emission, so a
796
+ * listener that clears selection state observes a document that already
797
+ * contains the committed snapshot.
798
+ * - On every path it fires **after** the facade has already cleared
799
+ * `td.activeSession` — so fold-then-render works from inside the handler.
800
+ * - `reason` distinguishes commit from abort but is deliberately coarse —
801
+ * no payload; the snapshot was already delivered through [[onCommit]].
802
+ *
803
+ * Registering after the session has already settled is **not** a no-op
804
+ * (unlike [[onChange]] / [[onCommit]]): the handler is invoked
805
+ * asynchronously (on a microtask) exactly once with the settle reason;
806
+ * unsubscribing before that microtask runs cancels the delivery.
807
+ *
808
+ * Listener exceptions are collected; after every listener has run, they are
809
+ * rethrown (one if single, `AggregateError` otherwise) — mirrors
810
+ * [[onChange]] / [[onCommit]]. Returns an unsubscribe function.
811
+ */
812
+ onSettled(handler: (reason: SettleReason) => void): () => void;
533
813
  }
534
814
  /**
535
815
  * Delivered to `EditSession.onChange` listeners once per completed gesture.
@@ -541,9 +821,9 @@ interface EditSession {
541
821
  */
542
822
  interface EditChangeEvent {
543
823
  /** Working state after the completed gesture. */
544
- readonly measure: ControlMeasureSnapshot;
824
+ readonly graphic: GraphicSnapshot<ControlMeasure>;
545
825
  /** State immediately before this gesture. */
546
- readonly previous: ControlMeasureSnapshot;
826
+ readonly previous: GraphicSnapshot<ControlMeasure>;
547
827
  /** The session the event belongs to. */
548
828
  readonly session: EditSession;
549
829
  /**
@@ -558,13 +838,252 @@ interface EditChangeEvent {
558
838
  abort(): void;
559
839
  }
560
840
  //#endregion
841
+ //#region src/tactical-draw/graphic-edit-session.d.ts
842
+ interface GraphicEditChangeEvent<G extends Graphic = Graphic> {
843
+ readonly graphic: GraphicSnapshot<G>;
844
+ readonly previous: GraphicSnapshot<G>;
845
+ readonly session: GraphicEditSession<G>;
846
+ reject(): void;
847
+ close(): void;
848
+ abort(): void;
849
+ }
850
+ /** Common selection/editing contract shared by control measures and point symbols. */
851
+ interface GraphicEditSession<G extends Graphic = Graphic> {
852
+ /** Immutable edit-start input. */
853
+ readonly graphic: G;
854
+ /** Live working graphic. Copy on read. */
855
+ readonly workingGraphic: G;
856
+ readonly dirty: boolean;
857
+ readonly modes: readonly EditMode[];
858
+ close(): GraphicSnapshot<G> | undefined;
859
+ abort(): void;
860
+ setModes(modes: readonly EditMode[]): void;
861
+ /** Atomically replace any part of the live working graphic. */
862
+ updateGraphic(updater: (graphic: G) => G): void;
863
+ onChange(listener: (event: GraphicEditChangeEvent<G>) => void): () => void;
864
+ onCommit(handler: (snapshot: GraphicSnapshot<G>) => void): () => void;
865
+ onSettled(handler: (reason: SettleReason) => void): () => void;
866
+ }
867
+ //#endregion
868
+ //#region src/tactical-draw/transform-session.d.ts
869
+ /**
870
+ * Surface delivered via `td.editMany(graphics, opts).onSession` and
871
+ * synchronously available from `td.activeTransformSession` while the group
872
+ * transform is live. The group sibling of `EditSession` (`edit-session.ts`):
873
+ * no reshape, no per-measure `setOptions`/`setStyle` — only the shared
874
+ * transform box's corner-scale / rotate / body-translate gestures, applied to
875
+ * every member at once. See the Phase 3 ("Multi-select") slice of ADR-0017.
876
+ *
877
+ * The member set is mutable via [[setGraphics]] (#187): members can join or
878
+ * leave a live session without a session boundary. Because membership changes
879
+ * over the session's lifetime, snapshot arrays are keyed by **measure id**, not
880
+ * by index — a snapshot's identity is `snapshot.graphic.id`. `graphics` is a
881
+ * live view of the current members' join-time inputs; a `TransformChangeEvent`'s
882
+ * arrays are index-aligned with `session.graphics` *as of that emission*. After
883
+ * the session settles (`close` / `abort` / an emptying `setGraphics`), every
884
+ * method is a silent no-op (except [[onSettled]] — late registration still
885
+ * delivers, asynchronously).
886
+ */
887
+ interface TransformSession {
888
+ /**
889
+ * Live view of the current members' join-time input graphics — a fresh array
890
+ * on each read, in current membership order. Grows/shrinks as [[setGraphics]]
891
+ * adds or removes members. Not stable across membership changes: key
892
+ * snapshots by `snapshot.graphic.id` rather than relying on index alignment.
893
+ */
894
+ readonly graphics: readonly ControlMeasure[];
895
+ /**
896
+ * Live working control points for the member whose id is `graphicId`, or
897
+ * `undefined` when no member carries that id. Copy on read.
898
+ */
899
+ controlPointsFor(graphicId: string): readonly Position[] | undefined;
900
+ /** `true` when any member's working points differ from its edit-start input beyond tolerance. */
901
+ readonly dirty: boolean;
902
+ /**
903
+ * Live size anchor, applied group-wide (ADR-0020). `"ground"` bakes each
904
+ * member's pixel size to meters on commit; `"screen"` keeps it zoom-aware.
905
+ * Initialised from `EditManyOptions.sizeAnchor` and mutated by [[setSizeAnchor]].
906
+ */
907
+ readonly sizeAnchor: SizeAnchor;
908
+ /** Switch the group's size anchor (ADR-0020). Takes effect on the next emitted snapshot and on `close()`. */
909
+ setSizeAnchor(anchor: SizeAnchor): void;
910
+ /**
911
+ * Replace the member set in place — the primitive behind shift-click toggle,
912
+ * marquee replace, and single-edit → group folding, without tearing the
913
+ * session down (#187). Members are matched to the current set by measure id:
914
+ *
915
+ * - **Retained** members (id in both the current and next sets) carry their
916
+ * current working state forward; the passed measure object is ignored for
917
+ * them. Their `dirty` baseline stays their original join-time input, so a
918
+ * session that has already moved things stays dirty across the call.
919
+ * - **Removed** members (id in the current set, absent from the next) are
920
+ * committed at their current working state.
921
+ * - **Added** members (id only in the next set) join at their passed input
922
+ * state; if currently on the graphics layer they are lifted off it exactly
923
+ * like initial members.
924
+ *
925
+ * The transform box re-derives from the next member set at the session's
926
+ * current box angle. Session-scoped state (`sizeAnchor`, box angle,
927
+ * non-geometry dirty) is unchanged.
928
+ *
929
+ * **Return value** — the committed exit snapshots of the members removed *by
930
+ * this call*, built exactly like [[close]]'s snapshots (pixel size baked to
931
+ * ground unless `sizeAnchor` is `"screen"`, then cloned and rendered); an
932
+ * empty array when nothing was removed. The outer `editMany` promise resolves
933
+ * with the snapshots of the members in the set **at close time only**.
934
+ * Removed members are NOT written back to the graphics layer by the
935
+ * controller — the host folds the returned snapshots into its document and
936
+ * calls `td.render()`, the same contract as [[close]]. A removed member's
937
+ * exit snapshot is also delivered through [[onCommit]] — the recommended
938
+ * channel, since it covers this return value and the close-time promise
939
+ * resolution through a single subscription. Returns `undefined` (a silent
940
+ * no-op) after the session has settled.
941
+ *
942
+ * `setGraphics([])` on a live session delegates to [[close]]: it commits every
943
+ * current member, resolves the promise with their snapshots, and returns that
944
+ * same array. Duplicate ids in `graphics` throw `TypeError` synchronously,
945
+ * without mutating session state.
946
+ */
947
+ setGraphics(graphics: readonly ControlMeasure[]): GraphicSnapshot<ControlMeasure>[] | undefined;
948
+ /**
949
+ * Commit: returns the members' `GraphicSnapshot<ControlMeasure>[]` (the members in the
950
+ * set at close time) and resolves the outer `editMany` promise with the same
951
+ * value (the same reference — no double-bake of ground-anchored sizes). A
952
+ * member removed mid-session via [[setGraphics]] is not included here; its exit
953
+ * snapshot was returned from that `setGraphics` call (and delivered to
954
+ * [[onCommit]] at that time). Returns `undefined` after the session has
955
+ * settled (idempotent no-op).
956
+ */
957
+ close(): GraphicSnapshot<ControlMeasure>[] | undefined;
958
+ /** Reject the outer `editMany` promise with reason `"session"`. */
959
+ abort(): void;
960
+ /**
961
+ * Subscribe to working-copy changes. The listener receives a
962
+ * [[TransformChangeEvent]] for each completed group gesture (corner scale,
963
+ * rotate, or body-translate drag end) — never on rubber-band ticks. All
964
+ * currently-registered listeners receive the same event instance, in
965
+ * registration order.
966
+ *
967
+ * Listener exceptions are collected; after every listener has run, they are
968
+ * rethrown (one if single, aggregated otherwise). `event.reject()` /
969
+ * `event.close()` / `event.abort()` take effect only after all listeners
970
+ * have run — mirrors `EditSession.onChange` (issue #49).
971
+ */
972
+ onChange(listener: (event: TransformChangeEvent) => void): () => void;
973
+ /**
974
+ * Subscribe to committed exit-snapshot batches — the single delivery channel
975
+ * for every `GraphicSnapshot<ControlMeasure>[]` this session ever commits (#191). Fires
976
+ * exactly once per batch:
977
+ *
978
+ * - The members [[setGraphics]] removes, but only when that call actually
979
+ * removed at least one member.
980
+ * - The close-time members on any commit path: [[close]], `setGraphics([])`,
981
+ * a click-away close, or `TransformChangeEvent.close()`.
982
+ *
983
+ * Never fires on an abort/reject path — pair it with [[onSettled]] to also
984
+ * observe abort settles. Fires synchronously from within the
985
+ * `setGraphics`/`close` call that produced the batch, before that call
986
+ * returns, and the array delivered here is the same reference that call
987
+ * returns (and, for the close batch, the same reference the `editMany`
988
+ * promise resolves with). This is the recommended single fold path for exit
989
+ * state — subscribe once in `onSession` and every committed snapshot, from
990
+ * either delivery site, arrives here exactly once; the `setGraphics` return
991
+ * value and the promise resolution are unchanged and remain valid for
992
+ * backward compatibility.
993
+ *
994
+ * Listener exceptions are collected; after every listener has run, they are
995
+ * rethrown (one if single, `AggregateError` otherwise) — mirrors
996
+ * [[onChange]]. Returns an unsubscribe function; registering after the
997
+ * session has settled returns a no-op unsubscribe and the listener never
998
+ * fires.
999
+ */
1000
+ onCommit(handler: (snapshots: GraphicSnapshot<ControlMeasure>[]) => void): () => void;
1001
+ /**
1002
+ * Subscribe to the session's settlement — the lifecycle complement of
1003
+ * [[onCommit]]: `onCommit` delivers *what* was committed, `onSettled` says
1004
+ * the session is *over* and why (#194). Fires exactly once, when the
1005
+ * session settles by any path, commit or abort, with a [[SettleReason]].
1006
+ *
1007
+ * - On commit paths it fires **after** the close batch's `onCommit`
1008
+ * emission, so a listener that clears selection state observes a document
1009
+ * that already contains the exit snapshots.
1010
+ * - On every path it fires **after** the facade has already cleared its
1011
+ * slot — `td.activeTransformSession` is `null` and the members are no
1012
+ * longer excluded from `td.render()` reconciliation, so fold-then-render
1013
+ * works from inside the handler.
1014
+ * - `reason` distinguishes commit from abort but is deliberately coarse —
1015
+ * no payload; snapshots were already delivered through [[onCommit]].
1016
+ *
1017
+ * Registering after the session has already settled is **not** a no-op
1018
+ * (unlike [[onChange]] / [[onCommit]]): the handler is invoked
1019
+ * asynchronously (on a microtask) exactly once with the settle reason;
1020
+ * unsubscribing before that microtask runs cancels the delivery.
1021
+ *
1022
+ * Listener exceptions are collected; after every listener has run, they are
1023
+ * rethrown (one if single, `AggregateError` otherwise) — mirrors
1024
+ * [[onChange]] / [[onCommit]]. Returns an unsubscribe function.
1025
+ */
1026
+ onSettled(handler: (reason: SettleReason) => void): () => void;
1027
+ }
1028
+ /**
1029
+ * Delivered to `TransformSession.onChange` listeners once per completed group
1030
+ * gesture. Both `graphics` and `previous` are index-aligned with
1031
+ * `session.graphics` *as of this emission*. Key by `graphic.id` rather than
1032
+ * relying on index alignment across emissions. A `setGraphics` call from within
1033
+ * a listener takes effect immediately and supersedes this emission's
1034
+ * `reject()` (the rollback baseline no longer matches the member set);
1035
+ * `close()` and `abort()` still apply, against the new membership. See
1036
+ * `EditChangeEvent` for the identical emission contract this mirrors.
1037
+ */
1038
+ interface TransformChangeEvent {
1039
+ /** Working state after the completed gesture, index-aligned with `session.graphics` as of this emission. */
1040
+ readonly graphics: GraphicSnapshot<ControlMeasure>[];
1041
+ /** State immediately before this gesture, index-aligned with `session.graphics` as of this emission. */
1042
+ readonly previous: GraphicSnapshot<ControlMeasure>[];
1043
+ /** The session the event belongs to. */
1044
+ readonly session: TransformSession;
1045
+ /**
1046
+ * Roll every member's working preview back to `previous` after all
1047
+ * listeners have run. Multiple calls collapse to a single rollback.
1048
+ * Overridden by `close()` or `abort()` if either is also called during this
1049
+ * emission, and dropped if a listener called `session.setGraphics` (the
1050
+ * rollback baseline no longer matches the member set).
1051
+ */
1052
+ reject(): void;
1053
+ /** Shortcut for `event.session.close()`, deferred until the emission ends. */
1054
+ close(): void;
1055
+ /** Shortcut for `event.session.abort()`, deferred until the emission ends. */
1056
+ abort(): void;
1057
+ }
1058
+ //#endregion
1059
+ //#region src/tactical-draw/graphic-transform-session.d.ts
1060
+ interface GraphicTransformChangeEvent {
1061
+ readonly graphics: GraphicSnapshot[];
1062
+ readonly previous: GraphicSnapshot[];
1063
+ readonly session: GraphicTransformSession;
1064
+ reject(): void;
1065
+ close(): void;
1066
+ abort(): void;
1067
+ }
1068
+ interface GraphicTransformSession {
1069
+ readonly graphics: readonly Graphic[];
1070
+ readonly workingGraphics: readonly Graphic[];
1071
+ readonly dirty: boolean;
1072
+ close(): GraphicSnapshot[] | undefined;
1073
+ abort(): void;
1074
+ onChange(listener: (event: GraphicTransformChangeEvent) => void): () => void;
1075
+ onCommit(listener: (snapshots: GraphicSnapshot[]) => void): () => void;
1076
+ onSettled(listener: (reason: SettleReason) => void): () => void;
1077
+ }
1078
+ //#endregion
561
1079
  //#region src/tactical-draw/session.d.ts
562
1080
  /**
563
1081
  * Scoped surface delivered via `td.draw({ onSession })` for variable-length
564
1082
  * kinds. Also available from `td.activeSession` while the draw is live.
565
1083
  *
566
1084
  * After the draw has settled — either via `commit()` returning `true` or
567
- * `abort()` resolving — every method is a silent no-op.
1085
+ * `abort()` resolving — every method is a silent no-op (except [[onSettled]]
1086
+ * — late registration still delivers, asynchronously).
568
1087
  */
569
1088
  interface DrawSession {
570
1089
  /** Live snapshot of the committed control points. Copy on read. */
@@ -586,8 +1105,50 @@ interface DrawSession {
586
1105
  * are collected and rethrown after all listeners run.
587
1106
  */
588
1107
  onChange(listener: (session: DrawSession) => void): () => void;
1108
+ /**
1109
+ * Subscribe to the committed snapshot — the single delivery channel for the
1110
+ * `GraphicSnapshot<ControlMeasure>` this session ever commits (#197, mirroring
1111
+ * `TransformSession.onCommit` #191). Fires exactly once, synchronously from
1112
+ * within the commit that produced it (`session.commit()`, dblclick, or a
1113
+ * second click/tap on the finishing vertex), with the same reference the
1114
+ * outer `draw` promise resolves with — the committed snapshot, not just the
1115
+ * measure. Never fires on abort.
1116
+ *
1117
+ * Listener exceptions are collected; after every listener has run, they are
1118
+ * rethrown (one if single, `AggregateError` otherwise) — mirrors
1119
+ * [[onChange]]. Returns an unsubscribe function; registering after the
1120
+ * session has settled returns a no-op unsubscribe and the listener never
1121
+ * fires.
1122
+ */
1123
+ onCommit(handler: (snapshot: GraphicSnapshot<ControlMeasure>) => void): () => void;
1124
+ /**
1125
+ * Subscribe to the session's settlement — the lifecycle complement of
1126
+ * [[onCommit]]: `onCommit` delivers *what* was committed, `onSettled` says
1127
+ * the session is *over* and why (#197, mirroring
1128
+ * `TransformSession.onSettled` #194). Fires exactly once, when the session
1129
+ * settles by any path, commit or abort, with a `SettleReason`.
1130
+ *
1131
+ * - On the commit path it fires **after** [[onCommit]]'s emission.
1132
+ * - On every path it fires **after** the facade has already cleared
1133
+ * `td.activeSession` — so fold-then-render works from inside the handler.
1134
+ * - `reason` distinguishes commit from abort but is deliberately coarse —
1135
+ * no payload; the snapshot was already delivered through [[onCommit]].
1136
+ *
1137
+ * Registering after the session has already settled is **not** a no-op
1138
+ * (unlike [[onChange]] / [[onCommit]]): the handler is invoked
1139
+ * asynchronously (on a microtask) exactly once with the settle reason;
1140
+ * unsubscribing before that microtask runs cancels the delivery.
1141
+ *
1142
+ * Listener exceptions are collected; after every listener has run, they are
1143
+ * rethrown (one if single, `AggregateError` otherwise) — mirrors
1144
+ * [[onChange]] / [[onCommit]]. Returns an unsubscribe function.
1145
+ */
1146
+ onSettled(handler: (reason: SettleReason) => void): () => void;
589
1147
  }
590
1148
  //#endregion
1149
+ //#region src/tactical-draw/point-symbol-draw-controller.d.ts
1150
+ type DrawPointSymbolDraft = Pick<PointSymbol, "kind" | "sidc"> & Partial<Omit<PointSymbol, "kind" | "sidc">>;
1151
+ //#endregion
591
1152
  //#region src/tactical-draw/tactical-draw.d.ts
592
1153
  /**
593
1154
  * Host-supplied layer slot ids. Any omitted slot is allocated by the façade and
@@ -599,8 +1160,22 @@ interface TacticalDrawLayerOptions {
599
1160
  guide?: LayerId;
600
1161
  handles?: LayerId;
601
1162
  }
1163
+ /**
1164
+ * Result of {@link TacticalDraw.ownsInteractionAt} — which layer claimed the
1165
+ * pixel and the feature that matched.
1166
+ */
1167
+ interface InteractionHit {
1168
+ /** Which TacticalDraw layer owns the pixel — topmost match wins. */
1169
+ layer: "handles" | "preview" | "graphics";
1170
+ /** The hit feature (do not mutate). */
1171
+ feature: Feature;
1172
+ /** Control-measure id when the hit feature belongs to one (graphics/preview parts). */
1173
+ measureId?: string;
1174
+ }
602
1175
  interface TacticalDrawOptions {
603
1176
  layers?: TacticalDrawLayerOptions;
1177
+ /** Optional renderer capability for first-class point symbols (ADR-0029). */
1178
+ pointSymbols?: PointSymbolCapability;
604
1179
  /**
605
1180
  * Façade-level default style passed through to `renderControlMeasure` as the
606
1181
  * third (lowest priority) style layer per StyleResolver (#44). Shallow-merged
@@ -628,7 +1203,7 @@ interface EditOptions {
628
1203
  * Called synchronously once when the edit starts, before any pointer event
629
1204
  * is processed. Lets a host wire commit / cancel buttons to the active edit.
630
1205
  */
631
- onSession?: (session: EditSession) => void;
1206
+ onSession?: (session: EditSession | GraphicEditSession<PointSymbol>) => void;
632
1207
  /**
633
1208
  * Default `true`. When `true`, a click on empty map (or on another graphics
634
1209
  * layer measure) closes the active edit. Clicks on preview / handle
@@ -637,11 +1212,13 @@ interface EditOptions {
637
1212
  */
638
1213
  closeOnClickAway?: boolean;
639
1214
  /**
640
- * Active edit modes. Defaults to `["reshape"]`. `reshape` renders vertex /
641
- * midpoint handles; `translate` renders a grip on the centroid pivot; and
642
- * `rotate` renders a grip offset just outside the shape (so it has a lever
643
- * arm and orbits with the geometry). The three can be combined freely.
644
- * `"scale"` is reserved and throws.
1215
+ * Active edit modes. Defaults to `["reshape", "transform"]`. `reshape`
1216
+ * renders vertex / midpoint handles; `transform` renders the oriented
1217
+ * selection box — 4 corner scale handles, a rotate grip above the top edge,
1218
+ * and drag-anywhere-inside-the-box translate (ADR-0017). The two can be
1219
+ * combined freely (the default does). `"labeldrag"` opt-in enables repositioning
1220
+ * rendered text labels. `"delete"` is exclusive: it collapses
1221
+ * the set to vertex-tap removal only. An unknown mode throws `TypeError`.
645
1222
  */
646
1223
  modes?: readonly EditMode[];
647
1224
  /**
@@ -655,8 +1232,10 @@ interface EditOptions {
655
1232
  * polyline sourced from the working control points alongside the working
656
1233
  * geometry preview. For `geometry === "area"` kinds, a separate
657
1234
  * closing-segment `LineString` is also emitted once `workingPoints.length >= 3`.
658
- * The guide is suppressed mechanically when `geometry === "point"` or
659
- * `minCoordinates === maxCoordinates`, regardless of this flag. See ADR-0004.
1235
+ * The guide is suppressed mechanically when `geometry === "point"`, and for
1236
+ * fixed-length kinds (`minCoordinates === maxCoordinates` or `canonicalPointCount`
1237
+ * set) unless the kind's draw rule opts back in via `showGuide`, regardless of
1238
+ * this flag. See ADR-0004.
660
1239
  */
661
1240
  guide?: boolean;
662
1241
  /**
@@ -669,6 +1248,32 @@ interface EditOptions {
669
1248
  */
670
1249
  sizeAnchor?: SizeAnchor;
671
1250
  }
1251
+ /**
1252
+ * Options for `TacticalDraw.editMany` — the multi-select group transform
1253
+ * (ADR-0017 "Multi-select" slice). Leaner than `EditOptions`: there is no
1254
+ * `modes` (a group transform is always the oriented box — no reshape), no
1255
+ * `guide` (no rubber-band content for a group), and no per-measure `setOptions`
1256
+ * / `setStyle` surface on the resulting session.
1257
+ */
1258
+ interface EditManyOptions {
1259
+ signal?: AbortSignal;
1260
+ /**
1261
+ * Called synchronously once when the group transform starts, before any
1262
+ * pointer event is processed. Lets a host wire Done/Cancel/Delete buttons to
1263
+ * the active session.
1264
+ */
1265
+ onSession?: (session: TransformSession | GraphicTransformSession) => void;
1266
+ /**
1267
+ * Default `true`. When `true`, a click on empty map (or on a graphics-layer
1268
+ * measure outside the group) closes the active transform. Clicks on preview
1269
+ * / handle features are consumed and keep it open.
1270
+ */
1271
+ closeOnClickAway?: boolean;
1272
+ /** Per-call interaction style override scoped to this transform. See ADR-0006. */
1273
+ interactionStyle?: InteractionStyle;
1274
+ /** Size anchor for the committed measures (ADR-0020), applied group-wide. Default `"ground"`. */
1275
+ sizeAnchor?: SizeAnchor;
1276
+ }
672
1277
  /**
673
1278
  * Draft control measure handed to `TacticalDraw.draw`. `kind` is required up
674
1279
  * front so the façade can resolve metadata, draw rule, and controller choice
@@ -699,8 +1304,10 @@ interface DrawOptions {
699
1304
  * polyline alongside the kind's generator preview. For `geometry === "area"`
700
1305
  * kinds, a separate closing-segment `LineString` is also emitted once the
701
1306
  * total point count (cursor included) is ≥ 3. The guide is suppressed
702
- * mechanically when `geometry === "point"` or `minCoordinates === maxCoordinates`,
703
- * regardless of this flag. See ADR-0004.
1307
+ * mechanically when `geometry === "point"`, and for fixed-length kinds
1308
+ * (`minCoordinates === maxCoordinates` or `canonicalPointCount` set) unless
1309
+ * the kind's draw rule opts back in via `showGuide`, regardless of this
1310
+ * flag. See ADR-0004.
704
1311
  */
705
1312
  guide?: boolean;
706
1313
  /**
@@ -736,6 +1343,7 @@ declare class TacticalDraw {
736
1343
  private readonly graphicsStyle;
737
1344
  private readonly defaultInteractionStyle;
738
1345
  private readonly generateId;
1346
+ private readonly pointSymbols;
739
1347
  private destroyed;
740
1348
  private hasRendered;
741
1349
  private readonly activeInteraction;
@@ -747,7 +1355,21 @@ declare class TacticalDraw {
747
1355
  */
748
1356
  private activeEdit;
749
1357
  /**
750
- * Host pick handlers registered via `td.onPick` / `td.onMeasurePick`. The
1358
+ * Tracked alongside `activeInteraction` whenever the interaction is a group
1359
+ * transform (`editMany`). The `render()` / view-change paths need every
1360
+ * member's id (to exclude all of them from reconcile) — mirrors `activeEdit`
1361
+ * but for `TransformHandle`'s `graphicIds` array.
1362
+ */
1363
+ private activeTransform;
1364
+ /**
1365
+ * The live `TransformSession`, exposed via `activeTransformSession`. Set
1366
+ * directly by `editMany`'s `onSession` wrapper (not routed through
1367
+ * `ActiveInteractionTracker.session`, whose type is `DrawSession | EditSession`)
1368
+ * and cleared on settle.
1369
+ */
1370
+ private activeTransformSessionRef;
1371
+ /**
1372
+ * Host pick handlers registered via `td.onPick` / `td.onGraphicPick`. The
751
1373
  * façade subscribes once to `adapter.onPick` per layer and routes events
752
1374
  * through this map so that close-then-pick ordering during an active edit can
753
1375
  * be enforced (issue #52).
@@ -764,6 +1386,7 @@ declare class TacticalDraw {
764
1386
  private deferredGraphicsPick;
765
1387
  private idCounter;
766
1388
  private editCounter;
1389
+ private transformCounter;
767
1390
  /**
768
1391
  * Snapshot of the host's last `render()` argument. The edit slice uses it
769
1392
  * to recompute the graphics-layer state during a lift / restore — the
@@ -782,13 +1405,29 @@ declare class TacticalDraw {
782
1405
  private renderMeasuresToGraphics;
783
1406
  /**
784
1407
  * Re-render the graphics layer on a view (zoom) change so pixel-denominated
785
- * measures rescale live. Skipped entirely when nothing rendered carries a
786
- * resolution-dependent size — meter geometry is zoom-independent, so a rebuild
787
- * would be wasted layer writes. During an active edit the edited measure lives
788
- * on the preview layer (lifted from graphics), so it is excluded here exactly
789
- * as in `render()`; the preview's own view-change subscription rescales it.
1408
+ * and not-yet-committed label-sized measures rescale live. Skipped entirely
1409
+ * when nothing rendered still reads live view state — meter geometry with a
1410
+ * captured label size is zoom-independent, so a rebuild would be wasted
1411
+ * layer writes. During an active edit the edited measure lives on the
1412
+ * preview layer (lifted from graphics), so it is excluded here exactly as
1413
+ * in `render()`; the preview's own view-change subscription rescales it.
790
1414
  */
791
1415
  private readonly handleViewChange;
1416
+ /**
1417
+ * Every measure id currently lifted onto the preview layer by an active
1418
+ * edit or group transform — excluded from `render()` / view-change
1419
+ * reconciliation because the preview layer (not graphics) owns their
1420
+ * working copy. Empty while idle.
1421
+ */
1422
+ private excludedInteractionIds;
1423
+ /**
1424
+ * Live member ids of the active group transform, or `undefined` when none.
1425
+ * Falls back to the session ref because `onSession` fires from inside the
1426
+ * controller's attach, before `runWithSlot` has assigned `activeTransform` —
1427
+ * the session ref is registered first (`trackTransformSession`), so a
1428
+ * `render()` from inside `onSession` still sees live membership.
1429
+ */
1430
+ private transformMemberIds;
792
1431
  /**
793
1432
  * Reconcile the graphics layer against the authoritative list of measures.
794
1433
  * Idempotent. Throws synchronously on duplicate measure ids *before*
@@ -814,7 +1453,7 @@ declare class TacticalDraw {
814
1453
  * layers (the only edit-facing side effect, `notifyExternalMeasure`, is a
815
1454
  * pure style replay).
816
1455
  */
817
- render(measures: readonly ControlMeasure[]): void;
1456
+ render(measures: readonly Graphic[]): void;
818
1457
  /**
819
1458
  * Nested-call guard + preempt, shared by `draw` and `edit`. Returns a
820
1459
  * pre-rejected promise when called re-entrantly from inside another
@@ -841,25 +1480,104 @@ declare class TacticalDraw {
841
1480
  * (exposing it via `activeSession`) before the host's own handler runs.
842
1481
  */
843
1482
  private trackSession;
1483
+ /**
1484
+ * Wrap a host `onSession` callback for `editMany`, recording the session on
1485
+ * `activeTransformSessionRef` (exposed via `activeTransformSession`) before
1486
+ * the host's own handler runs. Separate from `trackSession` because
1487
+ * `ActiveInteractionTracker.session` is typed `DrawSession | EditSession`.
1488
+ */
1489
+ private trackTransformSession;
844
1490
  /**
845
1491
  * Start an interactive draw. Fixed-length kinds commit automatically on the
846
1492
  * last required click. Variable-length kinds surface a `DrawSession` via
847
1493
  * `opts.onSession` and commit on `session.commit()`, `dblclick`, or a second
848
1494
  * click on the last added control point. All paths reject with
849
1495
  * `TacticalDrawAbortError` on signal / Escape / destroy / `session.abort()`.
1496
+ *
1497
+ * For a variable-length draw, `DrawSession.onCommit` is the single delivery
1498
+ * channel for the committed snapshot and `DrawSession.onSettled` is the
1499
+ * lifecycle signal for when the session is over and why (#197) — together
1500
+ * they make the returned promise non-load-bearing for hosts that adopt them.
1501
+ * Fixed-length kinds have no session and settle only through the promise.
850
1502
  */
851
- draw<K extends ControlMeasureKind>(draft: DrawMeasureDraft<K>, options?: DrawOptions): Promise<ControlMeasureSnapshot<K>>;
1503
+ draw(draft: DrawPointSymbolDraft, options?: DrawOptions): Promise<GraphicSnapshot<PointSymbol>>;
1504
+ draw<K extends ControlMeasureKind>(draft: DrawMeasureDraft<K>, options?: DrawOptions): Promise<GraphicSnapshot<ControlMeasure<K>>>;
852
1505
  /**
853
1506
  * Start an interactive edit on a single control measure. Returns a Promise
854
- * that resolves with a `ControlMeasureSnapshot` of the working state on
1507
+ * that resolves with a `GraphicSnapshot<ControlMeasure>` of the working state on
855
1508
  * `session.close()` and rejects with `TacticalDrawAbortError` on
856
1509
  * signal / Escape / destroy / preempt / `session.abort()`.
857
1510
  *
858
1511
  * Default click-away (`closeOnClickAway: true`) closes the edit on a click
859
1512
  * outside the preview / handle features. Hosts that own close policy pass
860
1513
  * `false`.
1514
+ *
1515
+ * `EditSession.onCommit` is the single delivery channel for the committed
1516
+ * snapshot and `EditSession.onSettled` is the lifecycle signal for when the
1517
+ * session is over and why (#197) — together they make the returned promise
1518
+ * non-load-bearing for hosts that adopt them.
1519
+ */
1520
+ edit(graphic: PointSymbol, options?: EditOptions): Promise<GraphicSnapshot<PointSymbol>>;
1521
+ edit(measure: ControlMeasure, options?: EditOptions): Promise<GraphicSnapshot<ControlMeasure>>;
1522
+ /**
1523
+ * Start an interactive group transform (multi-select) over `measures`
1524
+ * (ADR-0017 "Multi-select" slice). Every member is lifted onto the shared
1525
+ * preview layer; the box (corner scale, rotate grip, drag-anywhere body
1526
+ * translate) applies rigidly to all of them at once. Returns a Promise that
1527
+ * resolves with `GraphicSnapshot<ControlMeasure>[]` on `session.close()` and rejects
1528
+ * with `TacticalDrawAbortError` on signal / Escape / destroy / preempt /
1529
+ * `session.abort()`. Key the resolved snapshots by `snapshot.graphic.id`:
1530
+ * the member set is mutable (`session.setGraphics`, #187) so snapshot arrays
1531
+ * are id-keyed, not index-aligned with this input. The promise resolves with
1532
+ * the members in the set at close time; a member removed mid-session had its
1533
+ * exit snapshot returned from the `setGraphics` call that removed it.
1534
+ *
1535
+ * `measures.length` must be >= 1; a single-element array behaves like an
1536
+ * `edit` with only `transform` mode. Throws `TypeError` synchronously (before
1537
+ * any preemption) for an empty array or duplicate ids.
1538
+ *
1539
+ * For a selection that changes over time (shift-click toggle, marquee
1540
+ * replace), prefer [[syncTransformGraphics]] over hand-rolling the
1541
+ * live-session-vs-fresh-`editMany` branch, and subscribe to
1542
+ * `TransformSession.onCommit` as the single fold path for exit snapshots
1543
+ * and `TransformSession.onSettled` as the lifecycle signal for when the
1544
+ * session is over and why.
1545
+ */
1546
+ editMany(measures: ControlMeasure[], options?: EditManyOptions): Promise<GraphicSnapshot<ControlMeasure>[]>;
1547
+ editMany(graphics: Graphic[], options?: EditManyOptions): Promise<GraphicSnapshot[]>;
1548
+ /**
1549
+ * One-call selection-sync: mutate a live control-measure-only transform's
1550
+ * member set in place, or start a fresh `editMany` when no compatible
1551
+ * transform is live (#191). A selection containing a point symbol uses the
1552
+ * unified graphic transform session, which does not yet support mutable
1553
+ * membership; changing that selection preempts the current session and starts
1554
+ * a new one, resetting its box angle and other session-local state.
1555
+ *
1556
+ * The facade knows session liveness synchronously — `activeTransformSessionRef`
1557
+ * is cleared inside the settle path, before control returns to host code — so
1558
+ * unlike a host's own `activeTransformSession ? setGraphics : editMany` check,
1559
+ * this can never observe a stale ref pointing at an already-settled session.
1560
+ * The settled-race fall-through hosts had to hand-roll for that gap is owned
1561
+ * here instead: a `setGraphics` call that (implausibly) still returns
1562
+ * `undefined` falls through to a fresh `editMany` rather than silently
1563
+ * dropping the selection.
1564
+ *
1565
+ * `graphics: []` closes any live session (committing every member) and is a
1566
+ * no-op while idle. With a live session, `options` is ignored — session-scoped
1567
+ * state (`sizeAnchor`, box angle, close-on-click-away policy) is sticky per
1568
+ * `setGraphics`'s contract; `options` applies only when this call starts a
1569
+ * fresh `editMany`. Exit snapshots are delivered via the session's
1570
+ * `onCommit` — subscribe once in `options.onSession`. Session lifecycle —
1571
+ * including an Escape / preempt / destroy abort the host did not itself
1572
+ * initiate — is observed via the session's `onSettled` (#194); together
1573
+ * `onCommit` and `onSettled` make the underlying `editMany` promise
1574
+ * irrelevant to hosts. That promise is owned by the facade; its abort
1575
+ * rejections are swallowed here (non-abort errors surface as unhandled
1576
+ * rejections). Throws `TypeError` synchronously on duplicate ids, from
1577
+ * either path. Starting a fresh transform preempts any other in-flight
1578
+ * draw/edit exactly like `editMany`.
861
1579
  */
862
- edit(measure: ControlMeasure, options?: EditOptions): Promise<ControlMeasureSnapshot>;
1580
+ syncTransformGraphics(graphics: readonly Graphic[], options?: EditManyOptions): void;
863
1581
  /**
864
1582
  * Subscribe to picks on committed control measures.
865
1583
  *
@@ -871,7 +1589,7 @@ declare class TacticalDraw {
871
1589
  * Returns an idempotent unsubscribe function. An optional `signal` removes
872
1590
  * the handler when aborted.
873
1591
  */
874
- onMeasurePick(handler: PickHandler, opts?: PickOptions): () => void;
1592
+ onGraphicPick(handler: PickHandler, opts?: PickOptions): () => void;
875
1593
  /**
876
1594
  * Subscribe to pick events on `layerId`. Routed through the façade so the
877
1595
  * close-then-pick contract (issue #52) is observable: when an active edit
@@ -883,6 +1601,47 @@ declare class TacticalDraw {
883
1601
  * Returns an unsubscribe function. Optional `signal` mirrors `adapter.onPick`.
884
1602
  */
885
1603
  onPick(layerId: LayerId, handler: PickHandler, opts?: PickOptions): () => void;
1604
+ /**
1605
+ * Stateless hit test answering "does TacticalDraw own the pixel at
1606
+ * `pixel`?" — for hosts composing their own click-away / deselection logic
1607
+ * (issue #216) alongside `TacticalDraw`'s draw/edit sessions. Today hosts
1608
+ * hack this together from the `@internal` `layerIds` plus engine-specific
1609
+ * layer queries; this is the supported replacement.
1610
+ *
1611
+ * Tests, topmost first, the three interactive layers — `handlesLayer`,
1612
+ * then `previewLayer`, then `graphicsLayer` — against each layer's current
1613
+ * `adapter.getLayerFeatures(...)` cache. The `guideLayer` is deliberately
1614
+ * excluded: it is non-interactive draw-guide scaffolding, never a
1615
+ * legitimate "owned" click target.
1616
+ *
1617
+ * `preview` and `graphics` each get a label text-extent fallback after the
1618
+ * geometry pass misses on that layer: for every `Point` feature carrying a
1619
+ * string `properties.text`, the anchor is projected and
1620
+ * `fitLabelBoxFromFeature` (shared with the edit controller's `pickLabel`,
1621
+ * see commit f908434) estimates the rendered text's box; a pixel inside
1622
+ * that box counts as owned even though it's off the anchor point itself.
1623
+ * This matters most for the active preview, since a click can land
1624
+ * anywhere across a rotated/offset label during an edit.
1625
+ *
1626
+ * `tolerance` defaults to `mapEventHitTolerance(options?.originalEvent)` —
1627
+ * pass the host's raw click event as `originalEvent` to get the same
1628
+ * pointer-type-aware tolerance (4px mouse/pen, 12px touch) `onPick` uses;
1629
+ * pass an explicit `tolerance` to override it outright.
1630
+ *
1631
+ * `measureId` is set when the hit feature resolves to a control-measure id
1632
+ * via `tryControlMeasureIdFromFeature` — present for graphics/preview
1633
+ * parts, generally absent for handles.
1634
+ *
1635
+ * This is a pure synchronous query over adapter-cached state: it does not
1636
+ * depend on `onPick`/`"click"` dispatch having run first, so it is safe to
1637
+ * call from any host click handler regardless of ordering relative to
1638
+ * TacticalDraw's own pick/click pipeline.
1639
+ */
1640
+ ownsInteractionAt(pixel: PixelCoordinate, options?: {
1641
+ tolerance?: number;
1642
+ originalEvent?: unknown;
1643
+ }): InteractionHit | null;
1644
+ private toInteractionHit;
886
1645
  /**
887
1646
  * Abort the in-flight draw or edit. The default `"session"` reason matches
888
1647
  * `DrawSession.abort()` / `EditSession.abort()`.
@@ -894,7 +1653,14 @@ declare class TacticalDraw {
894
1653
  * The live variable-draw or edit session, or `null` while idle and during
895
1654
  * fixed-length draws. Safe to read after `destroy()`.
896
1655
  */
897
- get activeSession(): DrawSession | EditSession | null;
1656
+ get activeSession(): DrawSession | EditSession | GraphicEditSession<PointSymbol> | null;
1657
+ /**
1658
+ * The live `editMany` group-transform session, or `null` while idle (and
1659
+ * during any other interaction). Separate from `activeSession` because
1660
+ * `TransformSession` isn't part of its `DrawSession | EditSession` union —
1661
+ * see `activeTransformSessionRef`. Safe to read after `destroy()`.
1662
+ */
1663
+ get activeTransformSession(): TransformSession | GraphicTransformSession | null;
898
1664
  /**
899
1665
  * Tear down the façade. Releases only layer slots the façade created; host
900
1666
  * supplied layer ids are left untouched. Safe to call multiple times. After
@@ -926,9 +1692,19 @@ declare class TacticalDraw {
926
1692
  private applyGuideStyle;
927
1693
  private baseGuideStyle;
928
1694
  /**
929
- * Resolve handle slots across built-in → façade → per-call with per-slot,
930
- * per-property shallow merge. Explicit `undefined` at a higher-priority
931
- * layer unsets the corresponding lower-priority field (ADR-0006).
1695
+ * Resolve handle slots (plus `boxOutline`) across built-in → façade →
1696
+ * per-call with per-slot, per-property shallow merge. Explicit `undefined`
1697
+ * at a higher-priority layer unsets the corresponding lower-priority field
1698
+ * (ADR-0006). `boxOutline` rides this same per-feature merge path (not the
1699
+ * `guide` layer-style path) — the edit controller reads it off
1700
+ * `properties.style` on the box outline `LineString` (ADR-0017).
1701
+ *
1702
+ * `boxPadding` is a scalar, not a mergeable style object, so it resolves
1703
+ * separately with the same façade → per-call precedence and the same
1704
+ * explicit-undefined-wins rule: a `boxPadding` key present at a
1705
+ * higher-priority layer (even set to `undefined`) wins over a lower layer's
1706
+ * value. There is no built-in `boxPadding` — an absent value at every layer
1707
+ * falls through to the controllers' own `?? 0` default.
932
1708
  *
933
1709
  * Returns `undefined` only when no slot would carry any value — keeps the
934
1710
  * downstream "no style on the feature" path intact for tests that assert
@@ -946,5 +1722,64 @@ declare class TacticalDraw {
946
1722
  declare class TacticalDrawDestroyedError extends Error {
947
1723
  constructor(message?: string);
948
1724
  }
1725
+ declare class PointSymbolCapabilityRequiredError extends Error {
1726
+ constructor();
1727
+ }
1728
+ //#endregion
1729
+ //#region src/tactical-draw/transform-box.d.ts
1730
+ /**
1731
+ * The Excalidraw/Felt-style transform box: the oriented bounding box drawn
1732
+ * around a selection, with 4 corner scale handles, a rotate grip above the
1733
+ * top edge, and drag-anywhere-inside-the-box translate. This module is the
1734
+ * single source of truth for box *placement* (`fitTransformBox`,
1735
+ * `boxCorners`, `rotateGripPixel`) AND box *picking* (`pointInBox`,
1736
+ * `pickBoxHandle`) — mirroring the ADR-0016 discipline that keeps
1737
+ * `handles.ts` owning vertex/midpoint placement/pick. Everything here is pure
1738
+ * pixel-space math: no projection, no rendering, no session state.
1739
+ *
1740
+ * **Angle convention**: `angle` is in radians, measured in screen pixel space
1741
+ * (y grows downward). A positive angle rotates the box clockwise as drawn on
1742
+ * screen — the same convention `rotatePointsAround` (`handle-drag.ts`) uses,
1743
+ * so box rotation and control-point rotation stay in lockstep.
1744
+ *
1745
+ * **Corner ordering**: `boxCorners` always returns
1746
+ * `[topLeft, topRight, bottomRight, bottomLeft]` in the box's own (unrotated)
1747
+ * local frame, then rotated into place. "Top" means the smaller-y edge before
1748
+ * rotation — i.e. the edge the rotate grip floats above. The order is stable
1749
+ * across calls so a scale handle's `index` (0–3) always means the same
1750
+ * corner, and its opposite corner is always `(index + 2) % 4`.
1751
+ */
1752
+ interface TransformBox {
1753
+ /** Box center, pixel space. */
1754
+ center: PixelCoordinate;
1755
+ /** Half-width along the box's local x-axis (pre-rotation), in pixels. */
1756
+ halfWidth: number;
1757
+ /** Half-height along the box's local y-axis (pre-rotation), in pixels. */
1758
+ halfHeight: number;
1759
+ /** Radians, screen space, positive = clockwise on screen. */
1760
+ angle: number;
1761
+ }
1762
+ //#endregion
1763
+ //#region src/tactical-draw/point-symbol-rendering.d.ts
1764
+ interface PointSymbolFootprint {
1765
+ box: TransformBox;
1766
+ corners: PixelCoordinate[];
1767
+ }
1768
+ /** Complete rotated logical footprint for selection and transform chrome. */
1769
+ declare function pointSymbolFootprint(symbol: PointSymbol, render: PointSymbolRender, adapter: Pick<MapAdapter, "getBearing" | "getPixelFromCoordinate" | "getResolution">): PointSymbolFootprint | null;
1770
+ //#endregion
1771
+ //#region src/world-wrap.d.ts
1772
+ /**
1773
+ * Shift `lng` by whole world widths (360°) onto the copy nearest `refLng`.
1774
+ *
1775
+ * Map engines that wrap pointer longitudes into [-180, 180] (OpenLayers'
1776
+ * `toLonLat`, MapLibre's globe unproject) make positions jump ~360° when a
1777
+ * gesture crosses the antimeridian. Adapters use this to keep delivered
1778
+ * longitudes continuous with the view center, and to project coordinates on
1779
+ * the world copy the view is actually looking at.
1780
+ */
1781
+ declare function nearestWorldCopyLng(lng: number, refLng: number): number;
1782
+ /** `nearestWorldCopyLng` applied to a lon/lat position. */
1783
+ declare function nearestWorldCopyPosition(position: Position, refLng: number): Position;
949
1784
  //#endregion
950
- export { BaseMapAdapter, type CombinedAbort, type DrawMeasureDraft, type DrawOptions, type DrawSession, type EditChangeEvent, type EditMode, type EditOptions, type EditPointerDriver, type EditSession, type FeatureOps, type GuideStyle, type HandleStyle, type InteractionStyle, type LayerId, type MapAdapter, type MapCursor, type MapEvent, type MapEventHandler, type NativeEventEntry, type PickEvent, type PickHandler, type PickOptions, type PixelCoordinate, type PointerCallback, PointerCallbackHub, type PointerEventInfo, type SizeAnchor, type StyleOptions, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, type TacticalDrawAbortReason, TacticalDrawDestroyedError, type TacticalDrawLayerOptions, type TacticalDrawOptions, type TextStyleOptions, type ToPixel, bindAbortable, coerceHandleStyle, combineWithHostSignal, getSizeAnchor, hitTestFeature, ignoreAbort, isTacticalDrawAbortError, pickHitTolerance, tryControlMeasureIdFromFeature };
1785
+ export { BaseMapAdapter, type CombinedAbort, DEFAULT_LABEL_SIZE_PIXELS, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, type DrawMeasureDraft, type DrawOptions, type DrawPointSymbolDraft, type DrawSession, type EditChangeEvent, type EditManyOptions, type EditMode, type EditOptions, type EditPointerDriver, type EditSession, type FeatureOps, type Graphic, type GraphicEditChangeEvent, type GraphicEditSession, type GraphicRender, type GraphicSnapshot, type GraphicTransformChangeEvent, type GraphicTransformSession, type GuideStyle, type HandleStyle, type IconResource, type InteractionHit, type InteractionStyle, type LayerId, type MapAdapter, type MapCursor, type MapEvent, type MapEventHandler, type NativeEventEntry, type PickEvent, type PickHandler, type PickOptions, type PixelCoordinate, type PointSymbol, type PointSymbolAnchor, type PointSymbolCapability, PointSymbolCapabilityRequiredError, type PointSymbolColors, type PointSymbolFootprint, type PointSymbolGraphicModifiers, type PointSymbolRender, type PointSymbolSize, type PointSymbolStyle, type PointerCallback, PointerCallbackHub, type PointerEventInfo, type SettleReason, type SizeAnchor, type StyleOptions, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, type TacticalDrawAbortReason, TacticalDrawDestroyedError, type TacticalDrawLayerOptions, type TacticalDrawOptions, type TextStyleOptions, type ToPixel, type TransformBox, type TransformChangeEvent, type TransformSession, bindAbortable, clonePointSymbol, coerceHandleStyle, combineWithHostSignal, getSizeAnchor, hitTestFeature, hitTestIconFeature, ignoreAbort, isPointSymbol, isTacticalDrawAbortError, nearestWorldCopyLng, nearestWorldCopyPosition, pickHitTolerance, pointSymbolFootprint, resolveGroundTextSizePixels, tryControlMeasureIdFromFeature };