@orbat-mapper/tactical-draw 0.2.0-alpha.19 → 0.2.0-alpha.20

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 { AmplifierPlacement, AmplifierPlacements, ControlMeasure, ControlMeasureKind, ControlMeasureSnapshot, ControlMeasureStyle, TextAmplifiers } 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
@@ -100,6 +100,14 @@ interface HandleStyle {
100
100
  pointRadius?: number;
101
101
  }
102
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
+ }
103
111
  /**
104
112
  * Click-style hit event delivered by {@link MapAdapter.onPick}.
105
113
  *
@@ -137,6 +145,8 @@ interface MapAdapter {
137
145
  * Set the style for a specific layer.
138
146
  */
139
147
  setLayerStyle(layerId: LayerId, style: StyleOptions): void;
148
+ /** Reconcile the layer's desired shared icon resources. */
149
+ reconcileLayerIconResources(layerId: LayerId, resources: readonly IconResource[]): void;
140
150
  /**
141
151
  * Set features on a vector layer.
142
152
  * Features should be standard GeoJSON Features.
@@ -298,6 +308,8 @@ declare abstract class BaseMapAdapter<TLayerHandle, TEventToken, TViewToken> imp
298
308
  protected viewChangeTokens: Map<() => void, TViewToken>;
299
309
  protected featureCache: Map<string, Feature<import("geojson").Geometry, import("geojson").GeoJsonProperties>[]>;
300
310
  protected featuresChangedListeners: Map<string, Set<(features: Feature[]) => void>>;
311
+ protected iconResources: Map<string, IconResource>;
312
+ private layerIconResourceIds;
301
313
  private layerCounter;
302
314
  private destroyed;
303
315
  protected nextLayerId(): LayerId;
@@ -305,6 +317,7 @@ declare abstract class BaseMapAdapter<TLayerHandle, TEventToken, TViewToken> imp
305
317
  style?: StyleOptions;
306
318
  }): LayerId;
307
319
  removeLayer(layerId: LayerId): void;
320
+ reconcileLayerIconResources(layerId: LayerId, resources: readonly IconResource[]): void;
308
321
  setLayerFeatures(layerId: LayerId, features: readonly Feature[]): void;
309
322
  updateLayerFeatures(layerId: LayerId, features: readonly Feature[]): void;
310
323
  removeFeatures(layerId: LayerId, ids: readonly string[]): void;
@@ -319,6 +332,10 @@ declare abstract class BaseMapAdapter<TLayerHandle, TEventToken, TViewToken> imp
319
332
  protected emitLayerFeaturesChanged(layerId: LayerId, features: Feature[]): void;
320
333
  protected onLayerFeaturesChanged(layerId: LayerId, listener: (features: Feature[]) => void): () => void;
321
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;
322
339
  protected abstract createLayer(layerId: LayerId, style?: StyleOptions): TLayerHandle;
323
340
  protected abstract destroyLayer(handle: TLayerHandle): void;
324
341
  protected abstract applyFeatureOps(layerId: LayerId, ops: FeatureOps): void;
@@ -396,6 +413,13 @@ type ToPixel = (lonLat: Position) => PixelCoordinate | null;
396
413
  * Returns the first matching feature in iteration order, or null.
397
414
  */
398
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;
399
423
  //#endregion
400
424
  //#region src/gestures/hit-tolerance.d.ts
401
425
  declare const TOUCH_HIT_TOLERANCE_PX = 22;
@@ -430,6 +454,87 @@ declare class PointerCallbackHub {
430
454
  clear(): void;
431
455
  }
432
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
433
538
  //#region src/tactical-draw/abort.d.ts
434
539
  /**
435
540
  * AbortPlumbing for the TacticalDraw façade.
@@ -557,7 +662,7 @@ type EditMode = "reshape" | "transform" | "labeldrag" | "delete";
557
662
  * `TransformSession.close()` / `EditSession.close()` / `DrawSession.commit()`,
558
663
  * a click-away close, a gesture commit (dblclick, a second click/tap on the
559
664
  * finishing vertex, a click on another graphics-layer measure), or
560
- * `TransformSession.setMembers([])`. `"error"` is the commit-time snapshot
665
+ * `TransformSession.setGraphics([])`. `"error"` is the commit-time snapshot
561
666
  * build throwing (the outer promise rejects with that error, not a
562
667
  * `TacticalDrawAbortError`). The remaining values replay
563
668
  * `TacticalDrawAbortError.reason` verbatim: `"escape"`, `"signal"`,
@@ -579,7 +684,9 @@ type SettleReason = "close" | "error" | TacticalDrawAbortReason;
579
684
  */
580
685
  interface EditSession {
581
686
  /** Immutable edit-start input. */
582
- readonly measure: ControlMeasure;
687
+ readonly graphic: ControlMeasure;
688
+ /** Live complete working graphic. Copy on read. */
689
+ readonly workingGraphic: ControlMeasure;
583
690
  /** Live working state. Copy on read. */
584
691
  readonly controlPoints: readonly Position[];
585
692
  /** Live working options. Copy on read. */
@@ -605,12 +712,12 @@ interface EditSession {
605
712
  */
606
713
  readonly sizeAnchor: SizeAnchor;
607
714
  /**
608
- * Commit: returns the `ControlMeasureSnapshot` of the current working state
715
+ * Commit: returns the `GraphicSnapshot<ControlMeasure>` of the current working state
609
716
  * and resolves the outer `edit` promise with the same value (the same
610
717
  * reference — no double-bake of ground-anchored sizes). Returns `undefined`
611
718
  * after the session has settled (idempotent no-op).
612
719
  */
613
- close(): ControlMeasureSnapshot | undefined;
720
+ close(): GraphicSnapshot<ControlMeasure> | undefined;
614
721
  /** Reject the outer `edit` promise with reason `"session"`. */
615
722
  abort(): void;
616
723
  /**
@@ -621,6 +728,8 @@ interface EditSession {
621
728
  * is rejected with `TypeError`.
622
729
  */
623
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;
624
733
  /** Shallow-merge per-measure generator options into the live working copy. */
625
734
  setOptions(partial: ControlMeasure["options"]): void;
626
735
  /**
@@ -662,7 +771,7 @@ interface EditSession {
662
771
  onChange(listener: (event: EditChangeEvent) => void): () => void;
663
772
  /**
664
773
  * Subscribe to the committed snapshot — the single delivery channel for the
665
- * `ControlMeasureSnapshot` this session ever commits (#197, mirroring
774
+ * `GraphicSnapshot<ControlMeasure>` this session ever commits (#197, mirroring
666
775
  * `TransformSession.onCommit` #191). Fires exactly once, synchronously from
667
776
  * within the [[close]] call that produced it (`session.close()`,
668
777
  * `event.close()` from [[onChange]], a click-away close, or a click on
@@ -675,7 +784,7 @@ interface EditSession {
675
784
  * session has settled returns a no-op unsubscribe and the listener never
676
785
  * fires.
677
786
  */
678
- onCommit(handler: (snapshot: ControlMeasureSnapshot) => void): () => void;
787
+ onCommit(handler: (snapshot: GraphicSnapshot<ControlMeasure>) => void): () => void;
679
788
  /**
680
789
  * Subscribe to the session's settlement — the lifecycle complement of
681
790
  * [[onCommit]]: `onCommit` delivers *what* was committed, `onSettled` says
@@ -712,9 +821,9 @@ interface EditSession {
712
821
  */
713
822
  interface EditChangeEvent {
714
823
  /** Working state after the completed gesture. */
715
- readonly measure: ControlMeasureSnapshot;
824
+ readonly graphic: GraphicSnapshot<ControlMeasure>;
716
825
  /** State immediately before this gesture. */
717
- readonly previous: ControlMeasureSnapshot;
826
+ readonly previous: GraphicSnapshot<ControlMeasure>;
718
827
  /** The session the event belongs to. */
719
828
  readonly session: EditSession;
720
829
  /**
@@ -729,38 +838,65 @@ interface EditChangeEvent {
729
838
  abort(): void;
730
839
  }
731
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
732
868
  //#region src/tactical-draw/transform-session.d.ts
733
869
  /**
734
- * Surface delivered via `td.editMany(measures, opts).onSession` and
870
+ * Surface delivered via `td.editMany(graphics, opts).onSession` and
735
871
  * synchronously available from `td.activeTransformSession` while the group
736
872
  * transform is live. The group sibling of `EditSession` (`edit-session.ts`):
737
873
  * no reshape, no per-measure `setOptions`/`setStyle` — only the shared
738
874
  * transform box's corner-scale / rotate / body-translate gestures, applied to
739
875
  * every member at once. See the Phase 3 ("Multi-select") slice of ADR-0017.
740
876
  *
741
- * The member set is mutable via [[setMembers]] (#187): members can join or
877
+ * The member set is mutable via [[setGraphics]] (#187): members can join or
742
878
  * leave a live session without a session boundary. Because membership changes
743
879
  * over the session's lifetime, snapshot arrays are keyed by **measure id**, not
744
- * by index — a snapshot's identity is `snapshot.measure.id`. `measures` is a
880
+ * by index — a snapshot's identity is `snapshot.graphic.id`. `graphics` is a
745
881
  * live view of the current members' join-time inputs; a `TransformChangeEvent`'s
746
- * arrays are index-aligned with `session.measures` *as of that emission*. After
747
- * the session settles (`close` / `abort` / an emptying `setMembers`), every
882
+ * arrays are index-aligned with `session.graphics` *as of that emission*. After
883
+ * the session settles (`close` / `abort` / an emptying `setGraphics`), every
748
884
  * method is a silent no-op (except [[onSettled]] — late registration still
749
885
  * delivers, asynchronously).
750
886
  */
751
887
  interface TransformSession {
752
888
  /**
753
- * Live view of the current members' join-time input measures — a fresh array
754
- * on each read, in current membership order. Grows/shrinks as [[setMembers]]
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]]
755
891
  * adds or removes members. Not stable across membership changes: key
756
- * snapshots by `snapshot.measure.id` rather than relying on index alignment.
892
+ * snapshots by `snapshot.graphic.id` rather than relying on index alignment.
757
893
  */
758
- readonly measures: readonly ControlMeasure[];
894
+ readonly graphics: readonly ControlMeasure[];
759
895
  /**
760
- * Live working control points for the member whose id is `measureId`, or
896
+ * Live working control points for the member whose id is `graphicId`, or
761
897
  * `undefined` when no member carries that id. Copy on read.
762
898
  */
763
- controlPointsFor(measureId: string): readonly Position[] | undefined;
899
+ controlPointsFor(graphicId: string): readonly Position[] | undefined;
764
900
  /** `true` when any member's working points differ from its edit-start input beyond tolerance. */
765
901
  readonly dirty: boolean;
766
902
  /**
@@ -803,22 +939,22 @@ interface TransformSession {
803
939
  * resolution through a single subscription. Returns `undefined` (a silent
804
940
  * no-op) after the session has settled.
805
941
  *
806
- * `setMembers([])` on a live session delegates to [[close]]: it commits every
942
+ * `setGraphics([])` on a live session delegates to [[close]]: it commits every
807
943
  * current member, resolves the promise with their snapshots, and returns that
808
- * same array. Duplicate ids in `measures` throw `TypeError` synchronously,
944
+ * same array. Duplicate ids in `graphics` throw `TypeError` synchronously,
809
945
  * without mutating session state.
810
946
  */
811
- setMembers(measures: readonly ControlMeasure[]): ControlMeasureSnapshot[] | undefined;
947
+ setGraphics(graphics: readonly ControlMeasure[]): GraphicSnapshot<ControlMeasure>[] | undefined;
812
948
  /**
813
- * Commit: returns the members' `ControlMeasureSnapshot[]` (the members in the
949
+ * Commit: returns the members' `GraphicSnapshot<ControlMeasure>[]` (the members in the
814
950
  * set at close time) and resolves the outer `editMany` promise with the same
815
951
  * value (the same reference — no double-bake of ground-anchored sizes). A
816
- * member removed mid-session via [[setMembers]] is not included here; its exit
817
- * snapshot was returned from that `setMembers` call (and delivered to
952
+ * member removed mid-session via [[setGraphics]] is not included here; its exit
953
+ * snapshot was returned from that `setGraphics` call (and delivered to
818
954
  * [[onCommit]] at that time). Returns `undefined` after the session has
819
955
  * settled (idempotent no-op).
820
956
  */
821
- close(): ControlMeasureSnapshot[] | undefined;
957
+ close(): GraphicSnapshot<ControlMeasure>[] | undefined;
822
958
  /** Reject the outer `editMany` promise with reason `"session"`. */
823
959
  abort(): void;
824
960
  /**
@@ -836,22 +972,22 @@ interface TransformSession {
836
972
  onChange(listener: (event: TransformChangeEvent) => void): () => void;
837
973
  /**
838
974
  * Subscribe to committed exit-snapshot batches — the single delivery channel
839
- * for every `ControlMeasureSnapshot[]` this session ever commits (#191). Fires
975
+ * for every `GraphicSnapshot<ControlMeasure>[]` this session ever commits (#191). Fires
840
976
  * exactly once per batch:
841
977
  *
842
- * - The members [[setMembers]] removes, but only when that call actually
978
+ * - The members [[setGraphics]] removes, but only when that call actually
843
979
  * removed at least one member.
844
- * - The close-time members on any commit path: [[close]], `setMembers([])`,
980
+ * - The close-time members on any commit path: [[close]], `setGraphics([])`,
845
981
  * a click-away close, or `TransformChangeEvent.close()`.
846
982
  *
847
983
  * Never fires on an abort/reject path — pair it with [[onSettled]] to also
848
984
  * observe abort settles. Fires synchronously from within the
849
- * `setMembers`/`close` call that produced the batch, before that call
985
+ * `setGraphics`/`close` call that produced the batch, before that call
850
986
  * returns, and the array delivered here is the same reference that call
851
987
  * returns (and, for the close batch, the same reference the `editMany`
852
988
  * promise resolves with). This is the recommended single fold path for exit
853
989
  * state — subscribe once in `onSession` and every committed snapshot, from
854
- * either delivery site, arrives here exactly once; the `setMembers` return
990
+ * either delivery site, arrives here exactly once; the `setGraphics` return
855
991
  * value and the promise resolution are unchanged and remain valid for
856
992
  * backward compatibility.
857
993
  *
@@ -861,7 +997,7 @@ interface TransformSession {
861
997
  * session has settled returns a no-op unsubscribe and the listener never
862
998
  * fires.
863
999
  */
864
- onCommit(handler: (snapshots: ControlMeasureSnapshot[]) => void): () => void;
1000
+ onCommit(handler: (snapshots: GraphicSnapshot<ControlMeasure>[]) => void): () => void;
865
1001
  /**
866
1002
  * Subscribe to the session's settlement — the lifecycle complement of
867
1003
  * [[onCommit]]: `onCommit` delivers *what* was committed, `onSettled` says
@@ -891,26 +1027,26 @@ interface TransformSession {
891
1027
  }
892
1028
  /**
893
1029
  * Delivered to `TransformSession.onChange` listeners once per completed group
894
- * gesture. Both `measures` and `previous` are index-aligned with
895
- * `session.measures` *as of this emission*. Key by `measure.id` rather than
896
- * relying on index alignment across emissions. A `setMembers` call from within
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
897
1033
  * a listener takes effect immediately and supersedes this emission's
898
1034
  * `reject()` (the rollback baseline no longer matches the member set);
899
1035
  * `close()` and `abort()` still apply, against the new membership. See
900
1036
  * `EditChangeEvent` for the identical emission contract this mirrors.
901
1037
  */
902
1038
  interface TransformChangeEvent {
903
- /** Working state after the completed gesture, index-aligned with `session.measures` as of this emission. */
904
- readonly measures: ControlMeasureSnapshot[];
905
- /** State immediately before this gesture, index-aligned with `session.measures` as of this emission. */
906
- readonly previous: ControlMeasureSnapshot[];
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>[];
907
1043
  /** The session the event belongs to. */
908
1044
  readonly session: TransformSession;
909
1045
  /**
910
1046
  * Roll every member's working preview back to `previous` after all
911
1047
  * listeners have run. Multiple calls collapse to a single rollback.
912
1048
  * Overridden by `close()` or `abort()` if either is also called during this
913
- * emission, and dropped if a listener called `session.setMembers` (the
1049
+ * emission, and dropped if a listener called `session.setGraphics` (the
914
1050
  * rollback baseline no longer matches the member set).
915
1051
  */
916
1052
  reject(): void;
@@ -920,6 +1056,26 @@ interface TransformChangeEvent {
920
1056
  abort(): void;
921
1057
  }
922
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
923
1079
  //#region src/tactical-draw/session.d.ts
924
1080
  /**
925
1081
  * Scoped surface delivered via `td.draw({ onSession })` for variable-length
@@ -951,7 +1107,7 @@ interface DrawSession {
951
1107
  onChange(listener: (session: DrawSession) => void): () => void;
952
1108
  /**
953
1109
  * Subscribe to the committed snapshot — the single delivery channel for the
954
- * `ControlMeasureSnapshot` this session ever commits (#197, mirroring
1110
+ * `GraphicSnapshot<ControlMeasure>` this session ever commits (#197, mirroring
955
1111
  * `TransformSession.onCommit` #191). Fires exactly once, synchronously from
956
1112
  * within the commit that produced it (`session.commit()`, dblclick, or a
957
1113
  * second click/tap on the finishing vertex), with the same reference the
@@ -964,7 +1120,7 @@ interface DrawSession {
964
1120
  * session has settled returns a no-op unsubscribe and the listener never
965
1121
  * fires.
966
1122
  */
967
- onCommit(handler: (snapshot: ControlMeasureSnapshot) => void): () => void;
1123
+ onCommit(handler: (snapshot: GraphicSnapshot<ControlMeasure>) => void): () => void;
968
1124
  /**
969
1125
  * Subscribe to the session's settlement — the lifecycle complement of
970
1126
  * [[onCommit]]: `onCommit` delivers *what* was committed, `onSettled` says
@@ -990,6 +1146,9 @@ interface DrawSession {
990
1146
  onSettled(handler: (reason: SettleReason) => void): () => void;
991
1147
  }
992
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
993
1152
  //#region src/tactical-draw/tactical-draw.d.ts
994
1153
  /**
995
1154
  * Host-supplied layer slot ids. Any omitted slot is allocated by the façade and
@@ -1015,6 +1174,8 @@ interface InteractionHit {
1015
1174
  }
1016
1175
  interface TacticalDrawOptions {
1017
1176
  layers?: TacticalDrawLayerOptions;
1177
+ /** Optional renderer capability for first-class point symbols (ADR-0029). */
1178
+ pointSymbols?: PointSymbolCapability;
1018
1179
  /**
1019
1180
  * Façade-level default style passed through to `renderControlMeasure` as the
1020
1181
  * third (lowest priority) style layer per StyleResolver (#44). Shallow-merged
@@ -1042,7 +1203,7 @@ interface EditOptions {
1042
1203
  * Called synchronously once when the edit starts, before any pointer event
1043
1204
  * is processed. Lets a host wire commit / cancel buttons to the active edit.
1044
1205
  */
1045
- onSession?: (session: EditSession) => void;
1206
+ onSession?: (session: EditSession | GraphicEditSession<PointSymbol>) => void;
1046
1207
  /**
1047
1208
  * Default `true`. When `true`, a click on empty map (or on another graphics
1048
1209
  * layer measure) closes the active edit. Clicks on preview / handle
@@ -1101,7 +1262,7 @@ interface EditManyOptions {
1101
1262
  * pointer event is processed. Lets a host wire Done/Cancel/Delete buttons to
1102
1263
  * the active session.
1103
1264
  */
1104
- onSession?: (session: TransformSession) => void;
1265
+ onSession?: (session: TransformSession | GraphicTransformSession) => void;
1105
1266
  /**
1106
1267
  * Default `true`. When `true`, a click on empty map (or on a graphics-layer
1107
1268
  * measure outside the group) closes the active transform. Clicks on preview
@@ -1182,6 +1343,7 @@ declare class TacticalDraw {
1182
1343
  private readonly graphicsStyle;
1183
1344
  private readonly defaultInteractionStyle;
1184
1345
  private readonly generateId;
1346
+ private readonly pointSymbols;
1185
1347
  private destroyed;
1186
1348
  private hasRendered;
1187
1349
  private readonly activeInteraction;
@@ -1196,7 +1358,7 @@ declare class TacticalDraw {
1196
1358
  * Tracked alongside `activeInteraction` whenever the interaction is a group
1197
1359
  * transform (`editMany`). The `render()` / view-change paths need every
1198
1360
  * member's id (to exclude all of them from reconcile) — mirrors `activeEdit`
1199
- * but for `TransformHandle`'s `measureIds` array.
1361
+ * but for `TransformHandle`'s `graphicIds` array.
1200
1362
  */
1201
1363
  private activeTransform;
1202
1364
  /**
@@ -1207,7 +1369,7 @@ declare class TacticalDraw {
1207
1369
  */
1208
1370
  private activeTransformSessionRef;
1209
1371
  /**
1210
- * Host pick handlers registered via `td.onPick` / `td.onMeasurePick`. The
1372
+ * Host pick handlers registered via `td.onPick` / `td.onGraphicPick`. The
1211
1373
  * façade subscribes once to `adapter.onPick` per layer and routes events
1212
1374
  * through this map so that close-then-pick ordering during an active edit can
1213
1375
  * be enforced (issue #52).
@@ -1291,7 +1453,7 @@ declare class TacticalDraw {
1291
1453
  * layers (the only edit-facing side effect, `notifyExternalMeasure`, is a
1292
1454
  * pure style replay).
1293
1455
  */
1294
- render(measures: readonly ControlMeasure[]): void;
1456
+ render(measures: readonly Graphic[]): void;
1295
1457
  /**
1296
1458
  * Nested-call guard + preempt, shared by `draw` and `edit`. Returns a
1297
1459
  * pre-rejected promise when called re-entrantly from inside another
@@ -1338,10 +1500,11 @@ declare class TacticalDraw {
1338
1500
  * they make the returned promise non-load-bearing for hosts that adopt them.
1339
1501
  * Fixed-length kinds have no session and settle only through the promise.
1340
1502
  */
1341
- 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>>>;
1342
1505
  /**
1343
1506
  * Start an interactive edit on a single control measure. Returns a Promise
1344
- * that resolves with a `ControlMeasureSnapshot` of the working state on
1507
+ * that resolves with a `GraphicSnapshot<ControlMeasure>` of the working state on
1345
1508
  * `session.close()` and rejects with `TacticalDrawAbortError` on
1346
1509
  * signal / Escape / destroy / preempt / `session.abort()`.
1347
1510
  *
@@ -1354,51 +1517,55 @@ declare class TacticalDraw {
1354
1517
  * session is over and why (#197) — together they make the returned promise
1355
1518
  * non-load-bearing for hosts that adopt them.
1356
1519
  */
1357
- edit(measure: ControlMeasure, options?: EditOptions): Promise<ControlMeasureSnapshot>;
1520
+ edit(graphic: PointSymbol, options?: EditOptions): Promise<GraphicSnapshot<PointSymbol>>;
1521
+ edit(measure: ControlMeasure, options?: EditOptions): Promise<GraphicSnapshot<ControlMeasure>>;
1358
1522
  /**
1359
1523
  * Start an interactive group transform (multi-select) over `measures`
1360
1524
  * (ADR-0017 "Multi-select" slice). Every member is lifted onto the shared
1361
1525
  * preview layer; the box (corner scale, rotate grip, drag-anywhere body
1362
1526
  * translate) applies rigidly to all of them at once. Returns a Promise that
1363
- * resolves with `ControlMeasureSnapshot[]` on `session.close()` and rejects
1527
+ * resolves with `GraphicSnapshot<ControlMeasure>[]` on `session.close()` and rejects
1364
1528
  * with `TacticalDrawAbortError` on signal / Escape / destroy / preempt /
1365
- * `session.abort()`. Key the resolved snapshots by `snapshot.measure.id`:
1366
- * the member set is mutable (`session.setMembers`, #187) so snapshot arrays
1529
+ * `session.abort()`. Key the resolved snapshots by `snapshot.graphic.id`:
1530
+ * the member set is mutable (`session.setGraphics`, #187) so snapshot arrays
1367
1531
  * are id-keyed, not index-aligned with this input. The promise resolves with
1368
1532
  * the members in the set at close time; a member removed mid-session had its
1369
- * exit snapshot returned from the `setMembers` call that removed it.
1533
+ * exit snapshot returned from the `setGraphics` call that removed it.
1370
1534
  *
1371
1535
  * `measures.length` must be >= 1; a single-element array behaves like an
1372
1536
  * `edit` with only `transform` mode. Throws `TypeError` synchronously (before
1373
1537
  * any preemption) for an empty array or duplicate ids.
1374
1538
  *
1375
1539
  * For a selection that changes over time (shift-click toggle, marquee
1376
- * replace), prefer [[syncTransformMembers]] over hand-rolling the
1540
+ * replace), prefer [[syncTransformGraphics]] over hand-rolling the
1377
1541
  * live-session-vs-fresh-`editMany` branch, and subscribe to
1378
1542
  * `TransformSession.onCommit` as the single fold path for exit snapshots
1379
1543
  * and `TransformSession.onSettled` as the lifecycle signal for when the
1380
1544
  * session is over and why.
1381
1545
  */
1382
- editMany(measures: ControlMeasure[], options?: EditManyOptions): Promise<ControlMeasureSnapshot[]>;
1546
+ editMany(measures: ControlMeasure[], options?: EditManyOptions): Promise<GraphicSnapshot<ControlMeasure>[]>;
1547
+ editMany(graphics: Graphic[], options?: EditManyOptions): Promise<GraphicSnapshot[]>;
1383
1548
  /**
1384
- * One-call selection-sync: mutate the live group transform's member set in
1385
- * place, or start a fresh `editMany` when no transform is live (#191). This
1386
- * owns the "live session → `setMembers`, else → `editMany`" branch that every
1387
- * host otherwise hand-rolls against `activeTransformSession`.
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.
1388
1555
  *
1389
1556
  * The facade knows session liveness synchronously — `activeTransformSessionRef`
1390
1557
  * is cleared inside the settle path, before control returns to host code — so
1391
- * unlike a host's own `activeTransformSession ? setMembers : editMany` check,
1558
+ * unlike a host's own `activeTransformSession ? setGraphics : editMany` check,
1392
1559
  * this can never observe a stale ref pointing at an already-settled session.
1393
1560
  * The settled-race fall-through hosts had to hand-roll for that gap is owned
1394
- * here instead: a `setMembers` call that (implausibly) still returns
1561
+ * here instead: a `setGraphics` call that (implausibly) still returns
1395
1562
  * `undefined` falls through to a fresh `editMany` rather than silently
1396
1563
  * dropping the selection.
1397
1564
  *
1398
- * `measures: []` closes any live session (committing every member) and is a
1565
+ * `graphics: []` closes any live session (committing every member) and is a
1399
1566
  * no-op while idle. With a live session, `options` is ignored — session-scoped
1400
1567
  * state (`sizeAnchor`, box angle, close-on-click-away policy) is sticky per
1401
- * `setMembers`'s contract; `options` applies only when this call starts a
1568
+ * `setGraphics`'s contract; `options` applies only when this call starts a
1402
1569
  * fresh `editMany`. Exit snapshots are delivered via the session's
1403
1570
  * `onCommit` — subscribe once in `options.onSession`. Session lifecycle —
1404
1571
  * including an Escape / preempt / destroy abort the host did not itself
@@ -1410,7 +1577,7 @@ declare class TacticalDraw {
1410
1577
  * either path. Starting a fresh transform preempts any other in-flight
1411
1578
  * draw/edit exactly like `editMany`.
1412
1579
  */
1413
- syncTransformMembers(measures: readonly ControlMeasure[], options?: EditManyOptions): void;
1580
+ syncTransformGraphics(graphics: readonly Graphic[], options?: EditManyOptions): void;
1414
1581
  /**
1415
1582
  * Subscribe to picks on committed control measures.
1416
1583
  *
@@ -1422,7 +1589,7 @@ declare class TacticalDraw {
1422
1589
  * Returns an idempotent unsubscribe function. An optional `signal` removes
1423
1590
  * the handler when aborted.
1424
1591
  */
1425
- onMeasurePick(handler: PickHandler, opts?: PickOptions): () => void;
1592
+ onGraphicPick(handler: PickHandler, opts?: PickOptions): () => void;
1426
1593
  /**
1427
1594
  * Subscribe to pick events on `layerId`. Routed through the façade so the
1428
1595
  * close-then-pick contract (issue #52) is observable: when an active edit
@@ -1486,14 +1653,14 @@ declare class TacticalDraw {
1486
1653
  * The live variable-draw or edit session, or `null` while idle and during
1487
1654
  * fixed-length draws. Safe to read after `destroy()`.
1488
1655
  */
1489
- get activeSession(): DrawSession | EditSession | null;
1656
+ get activeSession(): DrawSession | EditSession | GraphicEditSession<PointSymbol> | null;
1490
1657
  /**
1491
1658
  * The live `editMany` group-transform session, or `null` while idle (and
1492
1659
  * during any other interaction). Separate from `activeSession` because
1493
1660
  * `TransformSession` isn't part of its `DrawSession | EditSession` union —
1494
1661
  * see `activeTransformSessionRef`. Safe to read after `destroy()`.
1495
1662
  */
1496
- get activeTransformSession(): TransformSession | null;
1663
+ get activeTransformSession(): TransformSession | GraphicTransformSession | null;
1497
1664
  /**
1498
1665
  * Tear down the façade. Releases only layer slots the façade created; host
1499
1666
  * supplied layer ids are left untouched. Safe to call multiple times. After
@@ -1555,6 +1722,9 @@ declare class TacticalDraw {
1555
1722
  declare class TacticalDrawDestroyedError extends Error {
1556
1723
  constructor(message?: string);
1557
1724
  }
1725
+ declare class PointSymbolCapabilityRequiredError extends Error {
1726
+ constructor();
1727
+ }
1558
1728
  //#endregion
1559
1729
  //#region src/tactical-draw/transform-box.d.ts
1560
1730
  /**
@@ -1590,6 +1760,14 @@ interface TransformBox {
1590
1760
  angle: number;
1591
1761
  }
1592
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
1593
1771
  //#region src/world-wrap.d.ts
1594
1772
  /**
1595
1773
  * Shift `lng` by whole world widths (360°) onto the copy nearest `refLng`.
@@ -1604,4 +1782,4 @@ declare function nearestWorldCopyLng(lng: number, refLng: number): number;
1604
1782
  /** `nearestWorldCopyLng` applied to a lon/lat position. */
1605
1783
  declare function nearestWorldCopyPosition(position: Position, refLng: number): Position;
1606
1784
  //#endregion
1607
- export { BaseMapAdapter, type CombinedAbort, DEFAULT_LABEL_SIZE_PIXELS, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, type DrawMeasureDraft, type DrawOptions, type DrawSession, type EditChangeEvent, type EditManyOptions, type EditMode, type EditOptions, type EditPointerDriver, type EditSession, type FeatureOps, type GuideStyle, type HandleStyle, 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 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, coerceHandleStyle, combineWithHostSignal, getSizeAnchor, hitTestFeature, ignoreAbort, isTacticalDrawAbortError, nearestWorldCopyLng, nearestWorldCopyPosition, pickHitTolerance, resolveGroundTextSizePixels, 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 };