@orbat-mapper/tactical-draw 0.13.2 → 0.14.1

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
@@ -519,19 +519,84 @@ declare function normalizeImageGraphic(image: ImageGraphic): ImageGraphic;
519
519
  declare function imageOpacity(image: ImageGraphic): number;
520
520
  declare function validateImageGraphic(image: ImageGraphic): void;
521
521
  //#endregion
522
+ //#region src/marker.d.ts
523
+ /** Displayed icon height; same pixel/meter contract as point-symbol sizes. */
524
+ type MarkerSize = PointSymbolSize;
525
+ interface MarkerStyle {
526
+ /** Host-interpreted tint, handed to {@link MarkerCapability.render}. */
527
+ color?: string;
528
+ /** 0–1 display multiplier applied by TacticalDraw. */
529
+ opacity?: number;
530
+ }
531
+ /**
532
+ * Persisted generic marker (e.g. a pin) whose icon is supplied by the
533
+ * host application. TacticalDraw treats `symbolId` as opaque (ADR-0059).
534
+ */
535
+ interface Marker {
536
+ id: string;
537
+ kind: "marker";
538
+ /** Host-defined icon identifier resolved by the {@link MarkerCapability}. */
539
+ symbolId: string;
540
+ position: Position;
541
+ /** Normalized radians clockwise from geographic north, around the icon anchor. */
542
+ rotation: number;
543
+ size: MarkerSize;
544
+ /** Optional text; the capability decides whether and how to draw it. */
545
+ label?: string;
546
+ style?: MarkerStyle;
547
+ properties?: Record<string, unknown>;
548
+ schemaVersion: 1;
549
+ }
550
+ interface MarkerAnchor {
551
+ x: number;
552
+ y: number;
553
+ }
554
+ /** Host-prepared marker icon in canonical SVG units. */
555
+ interface MarkerIcon {
556
+ /**
557
+ * Content-derived identity for adapter-wide icon reuse: equal ids must mean
558
+ * identical SVG. Include anything that changes the drawing (symbol, color, label).
559
+ */
560
+ resourceId: string;
561
+ /** Self-contained SVG markup. Producing safe content is the host's responsibility. */
562
+ svg: string;
563
+ width: number;
564
+ height: number;
565
+ /** Point placed at `Marker.position` (e.g. a pin's tip); rotation and scale pivot here. */
566
+ anchor: MarkerAnchor;
567
+ }
568
+ interface MarkerRender extends MarkerIcon {
569
+ /** False when the capability did not recognize `symbolId` and a placeholder is shown. */
570
+ available: boolean;
571
+ }
572
+ interface MarkerCapability {
573
+ /** Return `undefined` for an unknown `symbolId`; TacticalDraw renders a placeholder. */
574
+ render(marker: Marker): MarkerIcon | undefined;
575
+ /** Monotonic host-owned revision; bump it when icon resolution changes. */
576
+ readonly generation?: number;
577
+ }
578
+ declare function isMarker(graphic: unknown): graphic is Marker;
579
+ declare function cloneMarker(marker: Marker): Marker;
580
+ declare function markerOpacity(marker: Marker): number;
581
+ declare function validateMarker(marker: Marker): void;
582
+ declare function normalizeMarker(marker: Marker): Marker;
583
+ //#endregion
522
584
  //#region src/graphic.d.ts
523
- type Graphic = ControlMeasure | PointSymbol | ImageGraphic;
524
- type GraphicRender = ControlMeasureRender | PointSymbolRender | ImageRender;
585
+ type Graphic = ControlMeasure | PointSymbol | ImageGraphic | Marker;
586
+ type GraphicRender = ControlMeasureRender | PointSymbolRender | ImageRender | MarkerRender;
525
587
  interface GraphicSnapshot<G extends Graphic = Graphic> {
526
588
  graphic: G;
527
- render: G extends PointSymbol ? PointSymbolRender : G extends ImageGraphic ? ImageRender : ControlMeasureRender;
589
+ render: G extends PointSymbol ? PointSymbolRender : G extends ImageGraphic ? ImageRender : G extends Marker ? MarkerRender : ControlMeasureRender;
528
590
  }
529
591
  /**
530
592
  * A graphic whose identity is one geographic anchor + size + rotation,
531
- * rather than a control-point geometry: `PointSymbol` or `ImageGraphic`.
593
+ * rather than a control-point geometry: `PointSymbol`, `ImageGraphic`, or `Marker`.
532
594
  */
533
- type AnchoredGraphic = PointSymbol | ImageGraphic;
595
+ type AnchoredGraphic = PointSymbol | ImageGraphic | Marker;
534
596
  declare function isAnchoredGraphic(graphic: Graphic): graphic is AnchoredGraphic;
597
+ /** A graphic that can take view-owned pin presentation (ADR-0056): a point symbol or marker. */
598
+ type PinnableGraphic = PointSymbol | Marker;
599
+ declare function isPinnableGraphic(graphic: Graphic): graphic is PinnableGraphic;
535
600
  //#endregion
536
601
  //#region src/tactical-draw/abort.d.ts
537
602
  /**
@@ -1413,6 +1478,11 @@ type DrawPointSymbolDraft = Pick<PointSymbol, "kind" | "sidc"> & Partial<Omit<Po
1413
1478
  //#region src/tactical-draw/image-draw-controller.d.ts
1414
1479
  type DrawImageDraft = Pick<ImageGraphic, "kind" | "resourceId" | "intrinsicSize"> & Partial<Omit<ImageGraphic, "kind" | "resourceId" | "intrinsicSize">>;
1415
1480
  //#endregion
1481
+ //#region src/tactical-draw/marker-draw-controller.d.ts
1482
+ type DrawMarkerDraft = Pick<Marker, "kind" | "symbolId"> & Partial<Omit<Marker, "kind" | "symbolId">>;
1483
+ /** Default icon height for a drawn marker when the draft omits `size`. */
1484
+ declare const DEFAULT_MARKER_SIZE: MarkerSize;
1485
+ //#endregion
1416
1486
  //#region src/tactical-draw/tactical-draw.d.ts
1417
1487
  /**
1418
1488
  * Host-supplied layer slot ids. Any omitted slot is allocated by the façade and
@@ -1450,6 +1520,8 @@ interface TacticalDrawOptions {
1450
1520
  pointSymbols?: PointSymbolCapability;
1451
1521
  /** Synchronous host-supplied resolver for first-class Image resources. */
1452
1522
  images?: ImageCapability;
1523
+ /** Synchronous host-supplied icon resolver for generic Markers (ADR-0059). */
1524
+ markers?: MarkerCapability;
1453
1525
  /**
1454
1526
  * Façade-level default style passed through to `renderControlMeasure` as the
1455
1527
  * third (lowest priority) style layer per StyleResolver (#44). Shallow-merged
@@ -1483,7 +1555,7 @@ interface EditOptions {
1483
1555
  * Called synchronously once when the edit starts, before any pointer event
1484
1556
  * is processed. Lets a host wire commit / cancel buttons to the active edit.
1485
1557
  */
1486
- onSession?: (session: EditSession | GraphicEditSession<PointSymbol> | GraphicEditSession<ImageGraphic>) => void;
1558
+ onSession?: (session: EditSession | GraphicEditSession<PointSymbol> | GraphicEditSession<ImageGraphic> | GraphicEditSession<Marker>) => void;
1487
1559
  /**
1488
1560
  * Default `true`. When `true`, a click on empty map (or on another graphics
1489
1561
  * layer measure) closes the active edit. Clicks on preview / handle
@@ -1639,6 +1711,7 @@ declare class TacticalDraw {
1639
1711
  private offsetDragModifier;
1640
1712
  private readonly pointSymbols;
1641
1713
  private readonly images;
1714
+ private readonly markers;
1642
1715
  private snappingOptions;
1643
1716
  private readonly resolveSnap;
1644
1717
  private readonly graphicStack;
@@ -1717,6 +1790,7 @@ declare class TacticalDraw {
1717
1790
  */
1718
1791
  private lastSeenPointSymbolGeneration;
1719
1792
  private lastSeenImageGeneration;
1793
+ private lastSeenMarkerGeneration;
1720
1794
  /**
1721
1795
  * Ids of graphics currently carrying a passive selection-highlight outline
1722
1796
  * ({@link setHighlightedGraphics}). Declarative set-replace, no session —
@@ -1725,6 +1799,8 @@ declare class TacticalDraw {
1725
1799
  */
1726
1800
  private highlightedIds;
1727
1801
  private pinBodies;
1802
+ /** Resolved marker icons, so pin, highlight and view rebuilds don't re-ask the host. */
1803
+ private markerRenders;
1728
1804
  readonly pins: PinPresentationStore;
1729
1805
  private unsubscribePins;
1730
1806
  /**
@@ -1756,9 +1832,11 @@ declare class TacticalDraw {
1756
1832
  * handler and the adapter-capability change in `handleViewChange` need it.
1757
1833
  */
1758
1834
  private refreshActiveEditPresentation;
1835
+ /** The capability's icon for a marker snapshot, resolved once per snapshot. */
1836
+ private markerRender;
1759
1837
  /**
1760
- * Whether a point symbol should render as a billboard pin rather than an
1761
- * ordinary point symbol: its own presentation opts in, and the adapter
1838
+ * Whether a point symbol or marker should render as a billboard pin rather
1839
+ * than an ordinary icon: its own presentation opts in, and the adapter
1762
1840
  * currently supports rendering pins. Pass an already-resolved
1763
1841
  * `presentation` to skip re-resolving it when a caller has one in hand.
1764
1842
  */
@@ -1945,6 +2023,7 @@ declare class TacticalDraw {
1945
2023
  */
1946
2024
  draw(draft: DrawPointSymbolDraft, options?: DrawOptions): Promise<GraphicSnapshot<PointSymbol>>;
1947
2025
  draw(draft: DrawImageDraft, options?: DrawOptions): Promise<GraphicSnapshot<ImageGraphic>>;
2026
+ draw(draft: DrawMarkerDraft, options?: DrawOptions): Promise<GraphicSnapshot<Marker>>;
1948
2027
  draw<K extends ControlMeasureKind>(draft: DrawMeasureDraft<K>, options?: DrawOptions): Promise<GraphicSnapshot<ControlMeasure<K>>>;
1949
2028
  /**
1950
2029
  * Start an interactive edit on a single control measure. Returns a Promise
@@ -1963,6 +2042,7 @@ declare class TacticalDraw {
1963
2042
  */
1964
2043
  edit(graphic: PointSymbol, options?: EditOptions): Promise<GraphicSnapshot<PointSymbol>>;
1965
2044
  edit(graphic: ImageGraphic, options?: EditOptions): Promise<GraphicSnapshot<ImageGraphic>>;
2045
+ edit(graphic: Marker, options?: EditOptions): Promise<GraphicSnapshot<Marker>>;
1966
2046
  edit(measure: ControlMeasure, options?: EditOptions): Promise<GraphicSnapshot<ControlMeasure>>;
1967
2047
  /**
1968
2048
  * Start an interactive group transform (multi-select) over `measures`
@@ -2183,6 +2263,9 @@ declare class PointSymbolCapabilityRequiredError extends Error {
2183
2263
  declare class ImageCapabilityRequiredError extends Error {
2184
2264
  constructor();
2185
2265
  }
2266
+ declare class MarkerCapabilityRequiredError extends Error {
2267
+ constructor();
2268
+ }
2186
2269
  //#endregion
2187
2270
  //#region src/tactical-draw/transform-box.d.ts
2188
2271
  /**
@@ -2238,6 +2321,13 @@ interface ImageFootprint {
2238
2321
  }
2239
2322
  declare function imageFootprint(image: ImageGraphic, render: ImageRender, adapter: Pick<MapAdapter, "getBearing" | "getPixelFromCoordinate" | "getResolution">): ImageFootprint | null;
2240
2323
  //#endregion
2324
+ //#region src/tactical-draw/marker-rendering.d.ts
2325
+ type MarkerFootprint = Omit<PointSymbolFootprint, "connector">;
2326
+ /** Resolve a marker's icon, substituting the built-in placeholder for unknown symbols. */
2327
+ declare function resolveMarkerRender(capability: MarkerCapability, marker: Marker): MarkerRender;
2328
+ /** Complete rotated footprint for selection and transform chrome. */
2329
+ declare function markerFootprint(marker: Marker, render: MarkerRender, adapter: PointSymbolProjection): MarkerFootprint | null;
2330
+ //#endregion
2241
2331
  //#region src/world-wrap.d.ts
2242
2332
  /**
2243
2333
  * Shift `lng` by whole world widths (360°) onto the copy nearest `refLng`.
@@ -2252,4 +2342,4 @@ declare function nearestWorldCopyLng(lng: number, refLng: number): number;
2252
2342
  /** `nearestWorldCopyLng` applied to a lon/lat position. */
2253
2343
  declare function nearestWorldCopyPosition(position: Position, refLng: number): Position;
2254
2344
  //#endregion
2255
- export { type AnchoredGraphic, BaseMapAdapter, type CombinedAbort, DEFAULT_LABEL_SIZE_PIXELS, DEFAULT_PIN_PRESENTATION, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, type DrawImageDraft, type DrawMeasureDraft, type DrawOptions, type DrawPointSymbolDraft, type DrawSession, type EditChangeEvent, type EditManyOptions, type EditMode, type EditOptions, type EditPointerDriver, type EditSession, type FeatureGroup, type Graphic, type GraphicEditChangeEvent, type GraphicEditSession, type GraphicGeometrySnappingOptions, type GraphicRender, type GraphicSnapshot, type GuideStyle, HATCH_PATTERNS, type HandleStyle, type HatchPattern, type IconResource, type ImageCapability, ImageCapabilityRequiredError, type ImageFootprint, type ImageGraphic, type ImageGraphicStyle, type ImageIntrinsicSize, type ImageOverlayResource, type ImageRender, type ImageResource, type ImageSize, type InteractionHit, type InteractionStyle, type LabelExtent, type LayerId, MINE_PATTERN_TILE_SIZE, type MapAdapter, type MapCursor, type MapEvent, type MapEventHandler, type MergedPickFeature, type NativeEventEntry, type OffsetDragModifier, type PickCandidate, type PickCandidateHit, type PickCandidateHitKind, type PickCandidateInput, type PickEvaluationContext, type PickEvent, type PickHandler, type PickOptions, type PickPointerType, PinFootprint, PinOverridePatch, PinPresentation, PinPresentationCapability, PinPresentationPatch, PinPresentationState, PinPresentationStatus, PinPresentationStatusEvent, PinPresentationStore, PinRender, type PixelCoordinate, type PointSymbol, type PointSymbolAnchor, type PointSymbolCapability, PointSymbolCapabilityRequiredError, type PointSymbolColors, type PointSymbolConnector, type PointSymbolFootprint, type PointSymbolGraphicModifiers, type PointSymbolOffset, type PointSymbolOffsetControls, type PointSymbolOffsetLayout, type PointSymbolOffsetStyle, type PointSymbolPlacement, type PointSymbolProjection, type PointSymbolRender, type PointSymbolSize, type PointSymbolStyle, type PointSymbolView, type PointerCallback, PointerCallbackHub, type PointerEventInfo, type ResolvedPickPresentation, type Rgba, type RotatedScreenBounds, type ScreenBounds, type SessionChangeSource, type SessionHistory, type SessionHistoryState, type SettleReason, type SizeAnchor, type SnapCandidate, type SnapCandidateProvider, type SnapCandidateRequest, type SnapInteraction, type SnapResult, type SnappingOptions, type StyleOptions, TEXT_HALO_COLOR, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, type TacticalDrawAbortReason, TacticalDrawDestroyedError, type TacticalDrawLayerOptions, type TacticalDrawOptions, type TextSizeProps, type TextStyleOptions, type ToPixel, type TransformBox, type TransformChangeEvent, type TransformSession, type TransientChangeEvent, type TransientPhase, alignPointSymbolIndicator, bindAbortable, cloneImageGraphic, clonePointSymbol, coerceHandleStyle, combinePinScale, combineWithHostSignal, convertPointSymbolPlacement, effectivePointSymbolPixelSize, evaluatePickCandidates, fillPatternImageDimension, fillPatternImageId, getSizeAnchor, hitTestFeature, hitTestIconFeature, ignoreAbort, imageFootprint, imageOpacity, isAnchoredGraphic, isImageGraphic, isMinePattern, isPointSymbol, isTacticalDrawAbortError, isValidPinNumber, makeFillPatternImage, mergeNativeAndCachedPickFeatures, movePointSymbolLocation, nearestWorldCopyLng, nearestWorldCopyPosition, normalizeImageGraphic, parseColor, pickHitTolerance, pickPointerType, pinDistanceScale, pinZoomScale, placePointSymbol, pointSymbolFootprint, pointSymbolResource, renderOrderToVisualOrder, resizePointSymbolSize, resolveGroundTextSizePixels, resolvePickPresentation, resolvePinPresentation, resolvePointSymbolOffset, resolveTextHaloWidth, resolveTextSizePixels, scalePointSymbolLayout, tryControlMeasureIdFromFeature, validateImageGraphic };
2345
+ export { type AnchoredGraphic, BaseMapAdapter, type CombinedAbort, DEFAULT_LABEL_SIZE_PIXELS, DEFAULT_MARKER_SIZE, DEFAULT_PIN_PRESENTATION, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, type DrawImageDraft, type DrawMarkerDraft, type DrawMeasureDraft, type DrawOptions, type DrawPointSymbolDraft, type DrawSession, type EditChangeEvent, type EditManyOptions, type EditMode, type EditOptions, type EditPointerDriver, type EditSession, type FeatureGroup, type Graphic, type GraphicEditChangeEvent, type GraphicEditSession, type GraphicGeometrySnappingOptions, type GraphicRender, type GraphicSnapshot, type GuideStyle, HATCH_PATTERNS, type HandleStyle, type HatchPattern, type IconResource, type ImageCapability, ImageCapabilityRequiredError, type ImageFootprint, type ImageGraphic, type ImageGraphicStyle, type ImageIntrinsicSize, type ImageOverlayResource, type ImageRender, type ImageResource, type ImageSize, type InteractionHit, type InteractionStyle, type LabelExtent, type LayerId, MINE_PATTERN_TILE_SIZE, type MapAdapter, type MapCursor, type MapEvent, type MapEventHandler, type Marker, type MarkerAnchor, type MarkerCapability, MarkerCapabilityRequiredError, type MarkerFootprint, type MarkerIcon, type MarkerRender, type MarkerSize, type MarkerStyle, type MergedPickFeature, type NativeEventEntry, type OffsetDragModifier, type PickCandidate, type PickCandidateHit, type PickCandidateHitKind, type PickCandidateInput, type PickEvaluationContext, type PickEvent, type PickHandler, type PickOptions, type PickPointerType, PinFootprint, PinOverridePatch, PinPresentation, PinPresentationCapability, PinPresentationPatch, PinPresentationState, PinPresentationStatus, PinPresentationStatusEvent, PinPresentationStore, PinRender, type PinnableGraphic, type PixelCoordinate, type PointSymbol, type PointSymbolAnchor, type PointSymbolCapability, PointSymbolCapabilityRequiredError, type PointSymbolColors, type PointSymbolConnector, type PointSymbolFootprint, type PointSymbolGraphicModifiers, type PointSymbolOffset, type PointSymbolOffsetControls, type PointSymbolOffsetLayout, type PointSymbolOffsetStyle, type PointSymbolPlacement, type PointSymbolProjection, type PointSymbolRender, type PointSymbolSize, type PointSymbolStyle, type PointSymbolView, type PointerCallback, PointerCallbackHub, type PointerEventInfo, type ResolvedPickPresentation, type Rgba, type RotatedScreenBounds, type ScreenBounds, type SessionChangeSource, type SessionHistory, type SessionHistoryState, type SettleReason, type SizeAnchor, type SnapCandidate, type SnapCandidateProvider, type SnapCandidateRequest, type SnapInteraction, type SnapResult, type SnappingOptions, type StyleOptions, TEXT_HALO_COLOR, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, type TacticalDrawAbortReason, TacticalDrawDestroyedError, type TacticalDrawLayerOptions, type TacticalDrawOptions, type TextSizeProps, type TextStyleOptions, type ToPixel, type TransformBox, type TransformChangeEvent, type TransformSession, type TransientChangeEvent, type TransientPhase, alignPointSymbolIndicator, bindAbortable, cloneImageGraphic, cloneMarker, clonePointSymbol, coerceHandleStyle, combinePinScale, combineWithHostSignal, convertPointSymbolPlacement, effectivePointSymbolPixelSize, evaluatePickCandidates, fillPatternImageDimension, fillPatternImageId, getSizeAnchor, hitTestFeature, hitTestIconFeature, ignoreAbort, imageFootprint, imageOpacity, isAnchoredGraphic, isImageGraphic, isMarker, isMinePattern, isPinnableGraphic, isPointSymbol, isTacticalDrawAbortError, isValidPinNumber, makeFillPatternImage, markerFootprint, markerOpacity, mergeNativeAndCachedPickFeatures, movePointSymbolLocation, nearestWorldCopyLng, nearestWorldCopyPosition, normalizeImageGraphic, normalizeMarker, parseColor, pickHitTolerance, pickPointerType, pinDistanceScale, pinZoomScale, placePointSymbol, pointSymbolFootprint, pointSymbolResource, renderOrderToVisualOrder, resizePointSymbolSize, resolveGroundTextSizePixels, resolveMarkerRender, resolvePickPresentation, resolvePinPresentation, resolvePointSymbolOffset, resolveTextHaloWidth, resolveTextSizePixels, scalePointSymbolLayout, tryControlMeasureIdFromFeature, validateImageGraphic, validateMarker };
package/dist/index.mjs CHANGED
@@ -1448,6 +1448,10 @@ function pointInBox(box, pixel) {
1448
1448
  const [lx, ly] = rotate(pixel[0] - center[0], pixel[1] - center[1], -angle);
1449
1449
  return Math.abs(lx) <= halfWidth && Math.abs(ly) <= halfHeight;
1450
1450
  }
1451
+ /** Screen angle in radians from `anchor` to `pixel`, for rotate drags pivoting on an anchor. */
1452
+ function pointerAngle(pixel, anchor) {
1453
+ return Math.atan2(pixel[1] - anchor[1], pixel[0] - anchor[0]);
1454
+ }
1451
1455
  /** Plain 2D distance between two pixel coordinates. */
1452
1456
  function pixelDistance$1(a, b) {
1453
1457
  return Math.hypot(a[0] - b[0], a[1] - b[1]);
@@ -1716,9 +1720,55 @@ function normalizeImageRotation(value) {
1716
1720
  return (value % turn + turn) % turn;
1717
1721
  }
1718
1722
  //#endregion
1723
+ //#region src/tactical-draw/rotation.ts
1724
+ /** Excalidraw-style rotation increment used while Shift is held. */
1725
+ const CONSTRAINED_ROTATION_STEP = Math.PI / 12;
1726
+ /** Normalize radians to the canonical clockwise [0, 2π) range. */
1727
+ function normalizeRotation(angle) {
1728
+ const turn = Math.PI * 2;
1729
+ return (angle % turn + turn) % turn;
1730
+ }
1731
+ /** Snap an absolute rotation angle to the nearest 15-degree increment. */
1732
+ function constrainRotation(angle) {
1733
+ return Math.round(angle / CONSTRAINED_ROTATION_STEP) * CONSTRAINED_ROTATION_STEP;
1734
+ }
1735
+ //#endregion
1736
+ //#region src/marker.ts
1737
+ function isMarker(graphic) {
1738
+ return typeof graphic === "object" && graphic !== null && "kind" in graphic && graphic.kind === "marker";
1739
+ }
1740
+ function cloneMarker(marker) {
1741
+ return structuredClone(marker);
1742
+ }
1743
+ function markerOpacity(marker) {
1744
+ const opacity = marker.style?.opacity ?? 1;
1745
+ if (!Number.isFinite(opacity)) throw new TypeError("Marker.style.opacity must be finite");
1746
+ return Math.max(0, Math.min(1, opacity));
1747
+ }
1748
+ function validateMarker(marker) {
1749
+ if (!marker.id) throw new TypeError("Marker.id must not be empty");
1750
+ if (!marker.symbolId) throw new TypeError("Marker.symbolId must not be empty");
1751
+ if (!Number.isFinite(marker.size.value) || marker.size.value <= 0) throw new TypeError("Marker.size.value must be a positive finite number");
1752
+ if (marker.size.unit !== "pixels" && marker.size.unit !== "meters") throw new TypeError("Marker.size.unit must be \"pixels\" or \"meters\"");
1753
+ if (!Number.isFinite(marker.rotation)) throw new TypeError("Marker.rotation must be finite");
1754
+ if (marker.schemaVersion !== 1) throw new TypeError("Marker.schemaVersion must be 1");
1755
+ markerOpacity(marker);
1756
+ if (!Array.isArray(marker.position) || marker.position.length < 2 || !Number.isFinite(marker.position[0]) || !Number.isFinite(marker.position[1])) throw new TypeError("Marker.position must be a finite geographic position");
1757
+ }
1758
+ function normalizeMarker(marker) {
1759
+ validateMarker(marker);
1760
+ const next = cloneMarker(marker);
1761
+ next.rotation = normalizeRotation(next.rotation);
1762
+ if (next.style?.opacity !== void 0) next.style.opacity = markerOpacity(next);
1763
+ return next;
1764
+ }
1765
+ //#endregion
1719
1766
  //#region src/graphic.ts
1720
1767
  function isAnchoredGraphic(graphic) {
1721
- return isPointSymbol(graphic) || isImageGraphic(graphic);
1768
+ return isPointSymbol(graphic) || isImageGraphic(graphic) || isMarker(graphic);
1769
+ }
1770
+ function isPinnableGraphic(graphic) {
1771
+ return isPointSymbol(graphic) || isMarker(graphic);
1722
1772
  }
1723
1773
  //#endregion
1724
1774
  //#region src/tactical-draw/active-interaction.ts
@@ -3298,19 +3348,6 @@ function pickHandle(handles, pixel, tolerance, toPixel) {
3298
3348
  return null;
3299
3349
  }
3300
3350
  //#endregion
3301
- //#region src/tactical-draw/rotation.ts
3302
- /** Excalidraw-style rotation increment used while Shift is held. */
3303
- const CONSTRAINED_ROTATION_STEP = Math.PI / 12;
3304
- /** Normalize radians to the canonical clockwise [0, 2π) range. */
3305
- function normalizeRotation(angle) {
3306
- const turn = Math.PI * 2;
3307
- return (angle % turn + turn) % turn;
3308
- }
3309
- /** Snap an absolute rotation angle to the nearest 15-degree increment. */
3310
- function constrainRotation(angle) {
3311
- return Math.round(angle / CONSTRAINED_ROTATION_STEP) * CONSTRAINED_ROTATION_STEP;
3312
- }
3313
- //#endregion
3314
3351
  //#region src/tactical-draw/handle-drag.ts
3315
3352
  function clonePoints(points) {
3316
3353
  return points.map((p) => [...p]);
@@ -5687,6 +5724,13 @@ var ImageCapabilityRequiredError = class ImageCapabilityRequiredError extends Er
5687
5724
  Object.setPrototypeOf(this, ImageCapabilityRequiredError.prototype);
5688
5725
  }
5689
5726
  };
5727
+ var MarkerCapabilityRequiredError = class MarkerCapabilityRequiredError extends Error {
5728
+ constructor() {
5729
+ super("TacticalDraw: marker capability is not configured");
5730
+ this.name = "MarkerCapabilityRequiredError";
5731
+ Object.setPrototypeOf(this, MarkerCapabilityRequiredError.prototype);
5732
+ }
5733
+ };
5690
5734
  //#endregion
5691
5735
  //#region src/tactical-draw/point-symbol-rendering.ts
5692
5736
  /** Complete rotated logical footprint for selection and transform chrome. */
@@ -5910,6 +5954,147 @@ function missingResource(image) {
5910
5954
  };
5911
5955
  }
5912
5956
  //#endregion
5957
+ //#region src/tactical-draw/marker-rendering.ts
5958
+ const PLACEHOLDER_SIZE = 24;
5959
+ const PLACEHOLDER = {
5960
+ resourceId: "__td_missing_marker__",
5961
+ svg: `<svg xmlns="http://www.w3.org/2000/svg" width="${PLACEHOLDER_SIZE}" height="${PLACEHOLDER_SIZE}" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" fill="#f3f4f6" fill-opacity="0.85" stroke="#6b7280" stroke-width="2" stroke-dasharray="3 2"/><text x="12" y="16.5" text-anchor="middle" font-family="sans-serif" font-size="13" font-weight="bold" fill="#6b7280">?</text></svg>`,
5962
+ width: PLACEHOLDER_SIZE,
5963
+ height: PLACEHOLDER_SIZE,
5964
+ anchor: {
5965
+ x: PLACEHOLDER_SIZE / 2,
5966
+ y: PLACEHOLDER_SIZE / 2
5967
+ }
5968
+ };
5969
+ /** Resolve a marker's icon, substituting the built-in placeholder for unknown symbols. */
5970
+ function resolveMarkerRender(capability, marker) {
5971
+ validateMarker(marker);
5972
+ const icon = capability.render(marker);
5973
+ if (!icon) return {
5974
+ ...PLACEHOLDER,
5975
+ anchor: { ...PLACEHOLDER.anchor },
5976
+ available: false
5977
+ };
5978
+ validateMarkerIcon(icon);
5979
+ return {
5980
+ resourceId: icon.resourceId,
5981
+ svg: icon.svg,
5982
+ width: icon.width,
5983
+ height: icon.height,
5984
+ anchor: {
5985
+ x: icon.anchor.x,
5986
+ y: icon.anchor.y
5987
+ },
5988
+ available: true
5989
+ };
5990
+ }
5991
+ /**
5992
+ * Markers reuse the point-symbol icon path (ADR-0030) by presenting the icon
5993
+ * height as the "octagon" size, so `Marker.size` sizes the whole icon.
5994
+ */
5995
+ function asPointSymbol(marker, render) {
5996
+ return [{
5997
+ id: marker.id,
5998
+ kind: "point-symbol",
5999
+ sidc: "",
6000
+ position: marker.position,
6001
+ rotation: marker.rotation,
6002
+ size: marker.size,
6003
+ style: { opacity: markerOpacity(marker) },
6004
+ ...marker.properties ? { properties: marker.properties } : {}
6005
+ }, asPointSymbolRender(render)];
6006
+ }
6007
+ /**
6008
+ * Canonical icon-resource height. Engines rasterize an icon resource at its
6009
+ * declared size and then scale the bitmap, so a small host icon (e.g. a 44-unit
6010
+ * pin) would blur when enlarged. Presenting every marker at this height means
6011
+ * engines scale the bitmap down for typical sizes, like milsymbol's 100-unit
6012
+ * octagon does for point symbols.
6013
+ */
6014
+ const MARKER_RESOURCE_HEIGHT = 256;
6015
+ const symbolRenders = /* @__PURE__ */ new WeakMap();
6016
+ function asPointSymbolRender(render) {
6017
+ let symbolRender = symbolRenders.get(render);
6018
+ if (!symbolRender) {
6019
+ symbolRender = supersample(render);
6020
+ symbolRenders.set(render, symbolRender);
6021
+ }
6022
+ return symbolRender;
6023
+ }
6024
+ function supersample(render) {
6025
+ const k = Math.max(1, MARKER_RESOURCE_HEIGHT / render.height);
6026
+ const width = render.width * k;
6027
+ const height = render.height * k;
6028
+ const svg = k === 1 ? render.svg : `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${render.width} ${render.height}">${render.svg}</svg>`;
6029
+ return {
6030
+ resourceId: render.resourceId,
6031
+ svg,
6032
+ octagonSize: height,
6033
+ width,
6034
+ height,
6035
+ anchor: {
6036
+ x: render.anchor.x * k,
6037
+ y: render.anchor.y * k
6038
+ },
6039
+ octagonAnchor: {
6040
+ x: width / 2,
6041
+ y: height / 2
6042
+ },
6043
+ valid: render.available
6044
+ };
6045
+ }
6046
+ function markerFeature(marker, render, resolution) {
6047
+ const feature = pointSymbolFeature(...asPointSymbol(marker, render), resolution);
6048
+ feature.id = `${marker.id}:marker`;
6049
+ feature.properties.graphicKind = "marker";
6050
+ feature.properties.markerAvailable = render.available;
6051
+ return feature;
6052
+ }
6053
+ function markerResource(render) {
6054
+ return pointSymbolResource(asPointSymbolRender(render));
6055
+ }
6056
+ /**
6057
+ * The upright body a marker shows in pin presentation (ADR-0056): its icon
6058
+ * resource plus the canonical height the pin's `sizePixels` scales against.
6059
+ * The pin stem attaches below the icon, centred on the marker's anchor.
6060
+ */
6061
+ function markerPinBody(render) {
6062
+ const symbolRender = asPointSymbolRender(render);
6063
+ return {
6064
+ resource: pointSymbolResource(symbolRender),
6065
+ octagonSize: symbolRender.octagonSize
6066
+ };
6067
+ }
6068
+ /** Complete rotated footprint for selection and transform chrome. */
6069
+ function markerFootprint(marker, render, adapter) {
6070
+ const footprint = pointSymbolFootprint(...asPointSymbol(marker, render), adapter);
6071
+ return footprint ? {
6072
+ box: footprint.box,
6073
+ corners: footprint.corners
6074
+ } : null;
6075
+ }
6076
+ /**
6077
+ * Whether a marker's rendered icon depends on map resolution: meter sizes carry
6078
+ * a resolution-derived `iconScale` unless the engine scales ground icons itself.
6079
+ */
6080
+ function markerScalesWithResolution(marker, nativeGroundScaling) {
6081
+ return marker.size.unit === "meters" && !nativeGroundScaling;
6082
+ }
6083
+ /** Pass an already-resolved `render` to skip asking the capability again. */
6084
+ function renderMarkerForAdapter(adapter, capability, marker, render = resolveMarkerRender(capability, marker)) {
6085
+ return {
6086
+ render,
6087
+ features: [markerFeature(marker, render, adapter.getResolution())],
6088
+ resource: markerResource(render)
6089
+ };
6090
+ }
6091
+ function validateMarkerIcon(icon) {
6092
+ if (!icon.resourceId) throw new TypeError("MarkerIcon.resourceId must not be empty");
6093
+ if (typeof icon.svg !== "string" || !icon.svg) throw new TypeError("MarkerIcon.svg must be a non-empty SVG string");
6094
+ for (const [value, name] of [[icon.width, "width"], [icon.height, "height"]]) if (!Number.isFinite(value) || value <= 0) throw new TypeError(`MarkerIcon.${name} must be a positive finite number`);
6095
+ if (!Number.isFinite(icon.anchor?.x) || !Number.isFinite(icon.anchor?.y)) throw new TypeError("MarkerIcon.anchor must be finite");
6096
+ }
6097
+ //#endregion
5913
6098
  //#region src/tactical-draw/frame-coalescer.ts
5914
6099
  /**
5915
6100
  * @param run Callback invoked with the latest scheduled args, at most once per frame.
@@ -5990,9 +6175,9 @@ function defaultGroupSizeAnchor(graphics) {
5990
6175
  * a live session without a session boundary
5991
6176
  */
5992
6177
  function runGroupTransform(opts) {
5993
- const { adapter, capability, imageCapability, graphics, graphicsLayer, previewLayer, handlesLayer, graphicsStyle, symbolColors, signal, closeOnClickAway = true, interactionStyle, onSession, handleIdPrefix, publishWorkingGraphics, settleWorkingGraphics, isOnGraphicsLayer, onSettled, onClickAwayGraphicsPick } = opts;
6178
+ const { adapter, capability, imageCapability, markerCapability, graphics, graphicsLayer, previewLayer, handlesLayer, graphicsStyle, symbolColors, signal, closeOnClickAway = true, interactionStyle, onSession, handleIdPrefix, publishWorkingGraphics, settleWorkingGraphics, isOnGraphicsLayer, onSettled, onClickAwayGraphicsPick } = opts;
5994
6179
  assertCapabilityFor(graphics);
5995
- validateImageResources(graphics);
6180
+ validateHostResources(graphics);
5996
6181
  const renderOpts = {
5997
6182
  graphicsStyle,
5998
6183
  symbolColors
@@ -6002,10 +6187,14 @@ function runGroupTransform(opts) {
6002
6187
  function assertCapabilityFor(list) {
6003
6188
  if (!capability && list.some(isPointSymbol)) throw new PointSymbolCapabilityRequiredError();
6004
6189
  if (!imageCapability && list.some(isImageGraphic)) throw new ImageCapabilityRequiredError();
6190
+ if (!markerCapability && list.some(isMarker)) throw new MarkerCapabilityRequiredError();
6005
6191
  }
6006
6192
  /** Validate host integration before listeners, layers, or interaction state are created. */
6007
- function validateImageResources(list) {
6008
- for (const graphic of list) if (isImageGraphic(graphic)) renderImageForAdapter(adapter, imageCapability, graphic);
6193
+ function validateHostResources(list) {
6194
+ for (const graphic of list) {
6195
+ if (isImageGraphic(graphic)) renderImageForAdapter(adapter, imageCapability, graphic);
6196
+ if (isMarker(graphic)) resolveMarkerRender(markerCapability, graphic);
6197
+ }
6009
6198
  }
6010
6199
  function isPointSymbolMember(member) {
6011
6200
  return isPointSymbol(member.graphic);
@@ -6013,6 +6202,9 @@ function runGroupTransform(opts) {
6013
6202
  function isImageMember(member) {
6014
6203
  return isImageGraphic(member.graphic);
6015
6204
  }
6205
+ function isMarkerMember(member) {
6206
+ return isMarker(member.graphic);
6207
+ }
6016
6208
  function isAnchoredMember(member) {
6017
6209
  return isAnchoredGraphic(member.graphic);
6018
6210
  }
@@ -6093,6 +6285,13 @@ function runGroupTransform(opts) {
6093
6285
  image.rotation = member.workingRotation;
6094
6286
  return image;
6095
6287
  }
6288
+ function buildWorkingMarker(member) {
6289
+ const marker = cloneMarker(member.graphic);
6290
+ marker.position = [...member.workingPoints[0]];
6291
+ marker.size = resizePointSymbolSize(marker.size, member.workingSize);
6292
+ marker.rotation = member.workingRotation;
6293
+ return marker;
6294
+ }
6096
6295
  /** Build the working measure for member `i` with a given options override. */
6097
6296
  function buildWorkingMeasureWith(member, override) {
6098
6297
  const measure = member.graphic;
@@ -6120,6 +6319,13 @@ function runGroupTransform(opts) {
6120
6319
  render: renderImageForAdapter(adapter, imageCapability, image).render
6121
6320
  };
6122
6321
  }
6322
+ if (isMarkerMember(member)) {
6323
+ const marker = buildWorkingMarker(member);
6324
+ return {
6325
+ graphic: cloneMarker(marker),
6326
+ render: resolveMarkerRender(markerCapability, marker)
6327
+ };
6328
+ }
6123
6329
  const working = buildWorkingMeasureWith(member, override);
6124
6330
  const committed = getControlMeasureMetadata(working.kind).uniformSizing || sizeAnchor === "screen" ? working : bakePixelSizeToGround(working, adapter);
6125
6331
  return {
@@ -6132,6 +6338,7 @@ function runGroupTransform(opts) {
6132
6338
  const member = members[i];
6133
6339
  if (isPointSymbolMember(member)) return buildWorkingPointSymbol(member);
6134
6340
  if (isImageMember(member)) return buildWorkingImage(member);
6341
+ if (isMarkerMember(member)) return buildWorkingMarker(member);
6135
6342
  return buildWorkingMeasureWith(member, member.optionsOverride);
6136
6343
  }
6137
6344
  let dirty = false;
@@ -6276,6 +6483,24 @@ function runGroupTransform(opts) {
6276
6483
  features = [plan.feature];
6277
6484
  const footprint = imageFootprint(image, plan.render, adapter);
6278
6485
  if (footprint) boxPixels = footprint.corners;
6486
+ } else if (isMarkerMember(member)) {
6487
+ const marker = buildWorkingMarker(member);
6488
+ pinGroup = opts.renderPin?.(marker);
6489
+ const pinFootprint = pinGroup?.pin && adapter.getPinFootprint?.(marker.id);
6490
+ const render = pinFootprint ? void 0 : resolveMarkerRender(markerCapability, marker);
6491
+ if (pinGroup?.pin) {
6492
+ features = pinGroup.features;
6493
+ iconResources = pinGroup.iconResources;
6494
+ } else {
6495
+ const plan = renderMarkerForAdapter(adapter, markerCapability, marker, render);
6496
+ features = plan.features;
6497
+ iconResources = [plan.resource];
6498
+ }
6499
+ if (pinFootprint) boxPixels = boxCorners(boxFromPixelBounds(pinFootprint.min, pinFootprint.max));
6500
+ else {
6501
+ const footprint = markerFootprint(marker, render, adapter);
6502
+ if (footprint) boxPixels = footprint.corners;
6503
+ }
6279
6504
  } else try {
6280
6505
  if (isPointSymbolMember(member)) {
6281
6506
  const symbol = buildWorkingPointSymbol(member);
@@ -6835,7 +7060,7 @@ function runGroupTransform(opts) {
6835
7060
  }
6836
7061
  //#endregion
6837
7062
  //#region src/tactical-draw/point-symbol-draw-controller.ts
6838
- const PREVIEW_ID$1 = "__td_point_symbol_preview__";
7063
+ const PREVIEW_ID$2 = "__td_point_symbol_preview__";
6839
7064
  function runPointSymbolDraw(options) {
6840
7065
  const { adapter, capability, draft, previewLayer, generateId, signal, onSettled, snap } = options;
6841
7066
  let latestPosition = draft.position;
@@ -6852,9 +7077,9 @@ function runPointSymbolDraw(options) {
6852
7077
  });
6853
7078
  const renderPreview = () => {
6854
7079
  if (!latestPosition) return;
6855
- const plan = renderPointSymbolForAdapter(adapter, capability, build(PREVIEW_ID$1, latestPosition));
7080
+ const plan = renderPointSymbolForAdapter(adapter, capability, build(PREVIEW_ID$2, latestPosition));
6856
7081
  ctx.writeLayer(previewLayer, [{
6857
- id: PREVIEW_ID$1,
7082
+ id: PREVIEW_ID$2,
6858
7083
  features: plan.features,
6859
7084
  iconResources: [plan.resource]
6860
7085
  }]);
@@ -7130,7 +7355,6 @@ function runPointSymbolEdit(opts) {
7130
7355
  return settleEmitter.register(listener);
7131
7356
  }
7132
7357
  };
7133
- const pointerAngle = (pixel, anchor) => Math.atan2(pixel[1] - anchor[1], pixel[0] - anchor[0]);
7134
7358
  const snapTranslate = (position, fallbackPixel, info) => snap?.({
7135
7359
  coordinate: position,
7136
7360
  pixel: driver.toPixel(position) ?? fallbackPixel,
@@ -7326,7 +7550,7 @@ function runPointSymbolEdit(opts) {
7326
7550
  }
7327
7551
  //#endregion
7328
7552
  //#region src/tactical-draw/image-draw-controller.ts
7329
- const PREVIEW_ID = "__td_image_preview__";
7553
+ const PREVIEW_ID$1 = "__td_image_preview__";
7330
7554
  const DEFAULT_IMAGE_WIDTH_PIXELS = 128;
7331
7555
  function runImageDraw(options) {
7332
7556
  const { adapter, capability, draft, previewLayer, generateId, signal, onSettled, snap } = options;
@@ -7354,9 +7578,9 @@ function runImageDraw(options) {
7354
7578
  const planFor = (id, position) => renderImageForAdapter(adapter, capability, build(id, position));
7355
7579
  const renderPreview = () => {
7356
7580
  if (!latestPosition) return;
7357
- const plan = planFor(PREVIEW_ID, latestPosition);
7581
+ const plan = planFor(PREVIEW_ID$1, latestPosition);
7358
7582
  ctx.writeLayer(previewLayer, [{
7359
- id: PREVIEW_ID,
7583
+ id: PREVIEW_ID$1,
7360
7584
  features: [plan.feature],
7361
7585
  iconResources: [],
7362
7586
  ...plan.overlay ? { imageOverlays: [plan.overlay] } : {}
@@ -7656,6 +7880,382 @@ function runImageEdit(opts) {
7656
7880
  };
7657
7881
  }
7658
7882
  //#endregion
7883
+ //#region src/tactical-draw/marker-draw-controller.ts
7884
+ /** Default icon height for a drawn marker when the draft omits `size`. */
7885
+ const DEFAULT_MARKER_SIZE = {
7886
+ value: 32,
7887
+ unit: "pixels"
7888
+ };
7889
+ const PREVIEW_ID = "__td_marker_preview__";
7890
+ function runMarkerDraw(options) {
7891
+ const { adapter, capability, draft, previewLayer, generateId, signal, onSettled, snap } = options;
7892
+ let latestPosition = draft.position;
7893
+ let ctx;
7894
+ const savedCursor = "";
7895
+ const build = (id, position) => ({
7896
+ ...structuredClone(draft),
7897
+ id,
7898
+ kind: "marker",
7899
+ position: [...position],
7900
+ rotation: normalizeRotation(draft.rotation ?? 0),
7901
+ size: structuredClone(draft.size ?? DEFAULT_MARKER_SIZE),
7902
+ schemaVersion: 1
7903
+ });
7904
+ let previewRender;
7905
+ const renderPreview = () => {
7906
+ if (!latestPosition) return;
7907
+ const preview = build(PREVIEW_ID, latestPosition);
7908
+ previewRender ??= resolveMarkerRender(capability, preview);
7909
+ const plan = renderMarkerForAdapter(adapter, capability, preview, previewRender);
7910
+ ctx.writeLayer(previewLayer, [{
7911
+ id: PREVIEW_ID,
7912
+ features: plan.features,
7913
+ iconResources: [plan.resource]
7914
+ }]);
7915
+ };
7916
+ const handlePointerMove = (event) => {
7917
+ latestPosition = snappedDrawPosition(snap, event);
7918
+ renderPreview();
7919
+ };
7920
+ const handleClick = (event) => {
7921
+ latestPosition = snappedDrawPosition(snap, event);
7922
+ ctx.commit(() => {
7923
+ const graphic = build(draft.id ?? generateId(), latestPosition);
7924
+ return {
7925
+ graphic,
7926
+ render: resolveMarkerRender(capability, graphic)
7927
+ };
7928
+ });
7929
+ };
7930
+ return runInteraction({
7931
+ adapter,
7932
+ signal,
7933
+ layers: [previewLayer],
7934
+ onViewChange: renderPreview,
7935
+ onSettled,
7936
+ attach(interaction) {
7937
+ ctx = interaction;
7938
+ interaction.onTeardown(() => {
7939
+ adapter.off("pointermove", handlePointerMove);
7940
+ adapter.off("click", handleClick);
7941
+ adapter.setCursor(savedCursor);
7942
+ });
7943
+ adapter.setCursor("crosshair");
7944
+ adapter.on("pointermove", handlePointerMove);
7945
+ adapter.on("click", handleClick);
7946
+ renderPreview();
7947
+ }
7948
+ });
7949
+ }
7950
+ //#endregion
7951
+ //#region src/tactical-draw/marker-edit-controller.ts
7952
+ function normaliseMarkerModes(input) {
7953
+ const modes = input ?? ["transform"];
7954
+ for (const mode of modes) if (mode !== "transform" && mode !== "delete") throw new TypeError(`Marker editing does not support mode "${mode}"`);
7955
+ return modes.includes("delete") ? ["delete"] : [...new Set(modes)];
7956
+ }
7957
+ function runMarkerEdit(opts) {
7958
+ const { adapter, capability, marker, graphicsLayer, previewLayer, handlesLayer, signal, closeOnClickAway = true, interactionStyle, onSession, handleIdPrefix, settleToGraphics, publishWorkingGraphic, wasOnGraphicsLayer, onSettled, onClickAwayGraphicsPick, snap } = opts;
7959
+ validateMarker(marker);
7960
+ let modes = normaliseMarkerModes(opts.modes);
7961
+ let usesGraphicStack = wasOnGraphicsLayer;
7962
+ let pinEditing = !!opts.renderPin && opts.modes === void 0;
7963
+ let pinDrag = null;
7964
+ let working = cloneMarker(marker);
7965
+ let dirty = false;
7966
+ let box = null;
7967
+ let drag = null;
7968
+ let ctx;
7969
+ let driver;
7970
+ let shell;
7971
+ const changeEmitter = createChangeEmitter({
7972
+ listenerErrorsMessage: "GraphicEditSession.onChange errors",
7973
+ isSettled: () => ctx.settled,
7974
+ abort: () => ctx.fail(new TacticalDrawAbortError("session")),
7975
+ close: () => ctx.commit(snapshot)
7976
+ });
7977
+ const commitEmitter = createCommitEmitter("GraphicEditSession.onCommit listener errors");
7978
+ const settleEmitter = createSettleEmitter("GraphicEditSession.onSettled listener errors");
7979
+ const captureHistory = () => ({ working: cloneMarker(working) });
7980
+ const renders = /* @__PURE__ */ new WeakMap();
7981
+ const renderOf = (value) => {
7982
+ let render = renders.get(value);
7983
+ if (!render) {
7984
+ render = resolveMarkerRender(capability, value);
7985
+ renders.set(value, render);
7986
+ }
7987
+ return render;
7988
+ };
7989
+ const snapshotOf = (value) => ({
7990
+ graphic: cloneMarker(value),
7991
+ render: renderOf(value)
7992
+ });
7993
+ const snapshot = () => snapshotOf(working);
7994
+ const pinActive = () => pinEditing && !!opts.renderPin && opts.canRenderPin?.() === true;
7995
+ const render = (publishGraphic = true) => {
7996
+ if (ctx.settled) return;
7997
+ if (pinActive()) {
7998
+ const group = opts.renderPin(working);
7999
+ publishWorkingGraphic(group);
8000
+ ctx.writeLayer(previewLayer, usesGraphicStack ? [] : [group]);
8001
+ ctx.writeLayer(handlesLayer, group.pin ? [{
8002
+ id: `${handleIdPrefix}:pin`,
8003
+ features: [],
8004
+ iconResources: [],
8005
+ pin: {
8006
+ ...group.pin,
8007
+ highlightOnly: true
8008
+ }
8009
+ }] : []);
8010
+ box = null;
8011
+ return;
8012
+ }
8013
+ const plan = renderMarkerForAdapter(adapter, capability, working, renderOf(working));
8014
+ const footprint = markerFootprint(working, plan.render, adapter);
8015
+ const padding = interactionStyle?.boxPadding ?? 0;
8016
+ const nextBox = footprint ? {
8017
+ ...footprint.box,
8018
+ halfWidth: Math.max(1, footprint.box.halfWidth) + padding,
8019
+ halfHeight: Math.max(1, footprint.box.halfHeight) + padding
8020
+ } : null;
8021
+ const handles = [];
8022
+ if (modes.includes("transform") && nextBox) handles.push(boxOutlineFeature(nextBox, (pixel) => driver.fromPixel(pixel), `${handleIdPrefix}:box-outline`, interactionStyle?.boxOutline), ...toFeatures(boxToHandles(nextBox, (pixel) => driver.fromPixel(pixel)), handleIdPrefix, interactionStyle));
8023
+ const group = {
8024
+ id: working.id,
8025
+ features: plan.features,
8026
+ iconResources: [plan.resource]
8027
+ };
8028
+ if (publishGraphic) {
8029
+ publishWorkingGraphic(group);
8030
+ ctx.writeLayer(previewLayer, usesGraphicStack ? [] : [group]);
8031
+ }
8032
+ box = nextBox;
8033
+ ctx.writeLayer(handlesLayer, [{
8034
+ id: `__td_marker_edit_handles__:${working.id}`,
8035
+ features: handles,
8036
+ iconResources: []
8037
+ }]);
8038
+ };
8039
+ const emitChange = (previousState, source, onAccepted) => changeEmitter.emit({
8040
+ source,
8041
+ graphic: snapshot(),
8042
+ previous: snapshotOf(previousState.working),
8043
+ session
8044
+ }, {
8045
+ rollback() {
8046
+ working = cloneMarker(previousState.working);
8047
+ dirty = !deepEqual(working, marker);
8048
+ render();
8049
+ },
8050
+ onAccepted
8051
+ });
8052
+ const history = createSessionHistory({
8053
+ initial: captureHistory(),
8054
+ clone: (value) => ({ working: cloneMarker(value.working) }),
8055
+ equals: (a, b) => deepEqual(a.working, b.working),
8056
+ isCurrent: (value) => deepEqual(working, value.working),
8057
+ apply(from, to, source) {
8058
+ working = cloneMarker(to.working);
8059
+ dirty = !deepEqual(working, marker);
8060
+ render();
8061
+ return emitChange(from, source);
8062
+ }
8063
+ });
8064
+ /** `next` must be a fresh snapshot the caller no longer mutates. */
8065
+ const apply = (next, source, label) => {
8066
+ const previous = captureHistory();
8067
+ working = next;
8068
+ dirty = !deepEqual(working, marker);
8069
+ render();
8070
+ emitChange(previous, source, () => history.record(captureHistory(), label));
8071
+ };
8072
+ const session = {
8073
+ history,
8074
+ graphic: cloneMarker(marker),
8075
+ get workingGraphic() {
8076
+ return cloneMarker(working);
8077
+ },
8078
+ get dirty() {
8079
+ return dirty;
8080
+ },
8081
+ get modes() {
8082
+ return modes;
8083
+ },
8084
+ close: () => ctx.commit(snapshot),
8085
+ abort: () => ctx.fail(new TacticalDrawAbortError("session")),
8086
+ setModes(next) {
8087
+ if (ctx.settled) return;
8088
+ modes = normaliseMarkerModes(next);
8089
+ pinEditing = false;
8090
+ render();
8091
+ },
8092
+ updateGraphic(updater) {
8093
+ if (ctx.settled) return;
8094
+ const next = normalizeMarker(updater(cloneMarker(working)));
8095
+ if (next.kind !== "marker" || next.id !== marker.id) throw new TypeError("updateGraphic must preserve marker kind and id");
8096
+ renderOf(next);
8097
+ apply(next, "programmatic", "Update graphic");
8098
+ },
8099
+ onChange(listener) {
8100
+ if (ctx.settled) return () => {};
8101
+ return changeEmitter.register(listener);
8102
+ },
8103
+ onCommit(listener) {
8104
+ if (ctx.settled) return () => {};
8105
+ return commitEmitter.register(listener);
8106
+ },
8107
+ onSettled(listener) {
8108
+ return settleEmitter.register(listener);
8109
+ }
8110
+ };
8111
+ const snapTranslate = (position, pixel, info) => snap?.({
8112
+ coordinate: position,
8113
+ pixel,
8114
+ interaction: "translate",
8115
+ graphicId: marker.id,
8116
+ originalEvent: info?.native
8117
+ })?.coordinate ?? position;
8118
+ const transformed = (pixel, info) => {
8119
+ if (pinDrag) {
8120
+ const attachment = [pinDrag.attachment[0] + pixel[0] - pinDrag.down[0], pinDrag.attachment[1] + pixel[1] - pinDrag.down[1]];
8121
+ const position = adapter.movePinFromPixel?.(working.id, attachment);
8122
+ if (!position) return null;
8123
+ return {
8124
+ ...cloneMarker(pinDrag.start),
8125
+ position: snapTranslate(position, attachment, info)
8126
+ };
8127
+ }
8128
+ if (!drag) return null;
8129
+ const next = cloneMarker(drag.start);
8130
+ if (drag.kind === "body") {
8131
+ const position = driver.fromPixel([drag.anchor[0] + pixel[0] - drag.down[0], drag.anchor[1] + pixel[1] - drag.down[1]]);
8132
+ next.position = snapTranslate(position, driver.toPixel(position) ?? pixel, info);
8133
+ } else if (drag.kind === "rotate") {
8134
+ const rotation = drag.start.rotation + pointerAngle(pixel, drag.anchor) - drag.startPointerAngle;
8135
+ next.rotation = normalizeRotation(info?.shiftKey ? constrainRotation(rotation) : rotation);
8136
+ } else {
8137
+ const distance = pixelDistance$1(pixel, drag.anchor);
8138
+ next.size = resizePointSymbolSize(drag.start.size, Math.max(.1, drag.start.size.value * (distance / drag.startDistance)));
8139
+ }
8140
+ return next;
8141
+ };
8142
+ const onDown = (pixel, info) => {
8143
+ if (pinActive()) {
8144
+ if (ctx.settled) return;
8145
+ const footprint = adapter.getPinFootprint?.(working.id);
8146
+ if (footprint && adapter.pickPin?.(working.id, pixel, info?.hitTolerance ?? 12)) {
8147
+ pinDrag = {
8148
+ start: cloneMarker(working),
8149
+ down: [...pixel],
8150
+ attachment: footprint.attachment
8151
+ };
8152
+ driver.setDragPanEnabled(false);
8153
+ }
8154
+ return;
8155
+ }
8156
+ if (ctx.settled || !box || !modes.includes("transform")) return;
8157
+ const hit = pickBoxHandle(box, pixel, info?.hitTolerance ?? 12);
8158
+ if (!hit) return;
8159
+ const anchor = driver.toPixel(working.position);
8160
+ if (!anchor) return;
8161
+ drag = {
8162
+ kind: hit.kind,
8163
+ down: [...pixel],
8164
+ start: cloneMarker(working),
8165
+ anchor,
8166
+ startPointerAngle: pointerAngle(pixel, anchor),
8167
+ startDistance: Math.max(1, pixelDistance$1(pixel, anchor))
8168
+ };
8169
+ driver.setDragPanEnabled(false);
8170
+ };
8171
+ const onMove = (pixel, info) => {
8172
+ const next = transformed(pixel, info);
8173
+ if (!next) return;
8174
+ working = next;
8175
+ render();
8176
+ };
8177
+ const onUp = (pixel, info) => {
8178
+ if (pinDrag) {
8179
+ const next = transformed(pixel, info);
8180
+ const { start, down } = pinDrag;
8181
+ pinDrag = null;
8182
+ driver.setDragPanEnabled(true);
8183
+ shell.suppressNextClickAway();
8184
+ working = cloneMarker(start);
8185
+ if (next && !withinClickSlop(pixel, down)) apply(next, "gesture", "Move marker");
8186
+ else render();
8187
+ return;
8188
+ }
8189
+ if (!drag) return;
8190
+ const current = drag;
8191
+ const next = transformed(pixel, info);
8192
+ drag = null;
8193
+ shell.suppressNextClickAway();
8194
+ driver.setDragPanEnabled(true);
8195
+ working = cloneMarker(current.start);
8196
+ if (!next) return;
8197
+ if (current.kind === "body" && withinClickSlop(pixel, current.down)) {
8198
+ render();
8199
+ return;
8200
+ }
8201
+ apply(next, "gesture", `${current.kind[0].toUpperCase()}${current.kind.slice(1)}`);
8202
+ };
8203
+ const handle = runEditInteraction({
8204
+ adapter,
8205
+ signal,
8206
+ layers: [previewLayer, handlesLayer],
8207
+ graphicsLayer,
8208
+ previewLayer,
8209
+ handlesLayer,
8210
+ closeOnClickAway,
8211
+ onViewChange: () => {
8212
+ if (pinActive()) return;
8213
+ render(markerScalesWithResolution(working, adapter.scalesGroundPointSymbolsNatively === true));
8214
+ },
8215
+ onBeforeTeardown: (settlement) => settleToGraphics(settlement.status === "committed" ? settlement.value.graphic : void 0),
8216
+ onDown,
8217
+ onMove,
8218
+ onUp,
8219
+ ownsGraphic: (id) => id === marker.id,
8220
+ close: () => ctx.commit(snapshot),
8221
+ onForeignPick: onClickAwayGraphicsPick,
8222
+ historySettle: () => history.settle(),
8223
+ facadeOnSettled: onSettled,
8224
+ emitSettle: (settlement) => settleFromInteraction(settlement, commitEmitter, settleEmitter),
8225
+ attach(interaction, createdDriver, createdShell) {
8226
+ ctx = interaction;
8227
+ driver = createdDriver;
8228
+ shell = createdShell;
8229
+ render();
8230
+ onSession?.(session);
8231
+ }
8232
+ });
8233
+ return {
8234
+ promise: handle.promise,
8235
+ refreshPresentation: () => render(),
8236
+ get settled() {
8237
+ return handle.settled;
8238
+ },
8239
+ get usesGraphicStack() {
8240
+ return usesGraphicStack;
8241
+ },
8242
+ measureId: marker.id,
8243
+ graphicKind: "marker",
8244
+ abort: handle.abort,
8245
+ syncHostMembership(present) {
8246
+ if (handle.settled || usesGraphicStack === present) return;
8247
+ const previous = usesGraphicStack;
8248
+ usesGraphicStack = present;
8249
+ try {
8250
+ render();
8251
+ } catch (error) {
8252
+ usesGraphicStack = previous;
8253
+ throw error;
8254
+ }
8255
+ }
8256
+ };
8257
+ }
8258
+ //#endregion
7659
8259
  //#region src/tactical-draw/graphic-stack-coordinator.ts
7660
8260
  /**
7661
8261
  * Combines host-owned Graphic order with live or settled render overrides.
@@ -7720,6 +8320,7 @@ var TacticalDraw = class {
7720
8320
  offsetDragModifier;
7721
8321
  pointSymbols;
7722
8322
  images;
8323
+ markers;
7723
8324
  snappingOptions;
7724
8325
  resolveSnap;
7725
8326
  graphicStack = new GraphicStackCoordinator();
@@ -7798,6 +8399,7 @@ var TacticalDraw = class {
7798
8399
  */
7799
8400
  lastSeenPointSymbolGeneration;
7800
8401
  lastSeenImageGeneration;
8402
+ lastSeenMarkerGeneration;
7801
8403
  /**
7802
8404
  * Ids of graphics currently carrying a passive selection-highlight outline
7803
8405
  * ({@link setHighlightedGraphics}). Declarative set-replace, no session —
@@ -7806,6 +8408,8 @@ var TacticalDraw = class {
7806
8408
  */
7807
8409
  highlightedIds = [];
7808
8410
  pinBodies = /* @__PURE__ */ new WeakMap();
8411
+ /** Resolved marker icons, so pin, highlight and view rebuilds don't re-ask the host. */
8412
+ markerRenders = /* @__PURE__ */ new WeakMap();
7809
8413
  pins = new PinPresentationStore();
7810
8414
  unsubscribePins;
7811
8415
  /**
@@ -7836,6 +8440,8 @@ var TacticalDraw = class {
7836
8440
  this.images = options?.images;
7837
8441
  this.lastSeenPointSymbolGeneration = this.pointSymbols?.generation;
7838
8442
  this.lastSeenImageGeneration = this.images?.generation;
8443
+ this.markers = options?.markers;
8444
+ this.lastSeenMarkerGeneration = this.markers?.generation;
7839
8445
  this.snappingOptions = options?.snapping;
7840
8446
  this.resolveSnap = createSnapResolver(adapter, () => this.lastRenderedMeasures, () => this.snapGeometryFeatures, () => this.snappingOptions);
7841
8447
  const owned = /* @__PURE__ */ new Set();
@@ -7892,9 +8498,18 @@ var TacticalDraw = class {
7892
8498
  refreshActiveEditPresentation() {
7893
8499
  if (this.activeEdit && "refreshPresentation" in this.activeEdit) this.activeEdit.refreshPresentation();
7894
8500
  }
8501
+ /** The capability's icon for a marker snapshot, resolved once per snapshot. */
8502
+ markerRender(marker) {
8503
+ let render = this.markerRenders.get(marker);
8504
+ if (!render) {
8505
+ render = resolveMarkerRender(this.markers, marker);
8506
+ this.markerRenders.set(marker, render);
8507
+ }
8508
+ return render;
8509
+ }
7895
8510
  /**
7896
- * Whether a point symbol should render as a billboard pin rather than an
7897
- * ordinary point symbol: its own presentation opts in, and the adapter
8511
+ * Whether a point symbol or marker should render as a billboard pin rather
8512
+ * than an ordinary icon: its own presentation opts in, and the adapter
7898
8513
  * currently supports rendering pins. Pass an already-resolved
7899
8514
  * `presentation` to skip re-resolving it when a caller has one in hand.
7900
8515
  */
@@ -7939,7 +8554,7 @@ var TacticalDraw = class {
7939
8554
  if (cached.dependence === "none") return cached.group;
7940
8555
  if (cached.dependence === "resolution" && Object.is(cached.resolution, this.adapter.getResolution())) return cached.group;
7941
8556
  }
7942
- const presentation = isPointSymbol(graphic) ? this.pins.resolve(graphic.id) : void 0;
8557
+ const presentation = isPinnableGraphic(graphic) ? this.pins.resolve(graphic.id) : void 0;
7943
8558
  const dependence = presentation && this.graphicUsesPinPresentation(graphic, presentation) ? "none" : graphicViewDependence(graphic, this.adapter.scalesGroundPointSymbolsNatively === true);
7944
8559
  const resolution = dependence === "none" ? void 0 : this.adapter.getResolution();
7945
8560
  const group = this.buildGraphicGroup(graphic, presentation);
@@ -7969,34 +8584,13 @@ var TacticalDraw = class {
7969
8584
  }
7970
8585
  });
7971
8586
  this.pinBodies.set(graphic, body);
7972
- const resource = pointSymbolResource(body);
7973
- return {
7974
- id: graphic.id,
7975
- revision,
7976
- features: [{
7977
- type: "Feature",
7978
- id: `${graphic.id}:pin`,
7979
- geometry: {
7980
- type: "Point",
7981
- coordinates: [...graphic.position]
7982
- },
7983
- properties: {
7984
- graphicId: graphic.id,
7985
- graphicKind: "point-symbol",
7986
- pinPresentation: true
7987
- }
7988
- }],
7989
- iconResources: [],
7990
- pin: {
7991
- id: graphic.id,
7992
- position: graphic.position,
7993
- resource,
7994
- octagonSize: body.octagonSize,
7995
- presentation,
7996
- opacity: (graphic.style?.opacity ?? 1) * (graphic.style?.overallOpacity ?? 1),
7997
- ...graphic.graphicModifiers?.direction !== void 0 ? { direction: graphic.rotation + graphic.graphicModifiers.direction } : {}
7998
- }
7999
- };
8587
+ return pinGroup(graphic, revision, {
8588
+ resource: pointSymbolResource(body),
8589
+ octagonSize: body.octagonSize,
8590
+ presentation,
8591
+ opacity: (graphic.style?.opacity ?? 1) * (graphic.style?.overallOpacity ?? 1),
8592
+ ...graphic.graphicModifiers?.direction !== void 0 ? { direction: graphic.rotation + graphic.graphicModifiers.direction } : {}
8593
+ });
8000
8594
  }
8001
8595
  const plan = renderPointSymbolForAdapter(this.adapter, this.pointSymbols, graphic);
8002
8596
  return {
@@ -8017,6 +8611,23 @@ var TacticalDraw = class {
8017
8611
  revision
8018
8612
  };
8019
8613
  }
8614
+ if (isMarker(graphic)) {
8615
+ if (!this.markers) throw new MarkerCapabilityRequiredError();
8616
+ const presentation = resolved ?? this.pins.resolve(graphic.id);
8617
+ const render = this.markerRender(graphic);
8618
+ if (this.graphicUsesPinPresentation(graphic, presentation)) return pinGroup(graphic, revision, {
8619
+ ...markerPinBody(render),
8620
+ presentation,
8621
+ opacity: markerOpacity(graphic)
8622
+ });
8623
+ const plan = renderMarkerForAdapter(this.adapter, this.markers, graphic, render);
8624
+ return {
8625
+ id: graphic.id,
8626
+ features: plan.features,
8627
+ iconResources: [plan.resource],
8628
+ revision
8629
+ };
8630
+ }
8020
8631
  const render = renderMeasureForAdapter(this.adapter, graphic, {
8021
8632
  graphicsStyle: this.graphicsStyle,
8022
8633
  symbolColors: this.symbolColors
@@ -8034,17 +8645,22 @@ var TacticalDraw = class {
8034
8645
  * resolution-independent graphics remain cache hits.
8035
8646
  */
8036
8647
  invalidateGraphicGroupCache(clearBodies = true) {
8037
- if (clearBodies) this.pinBodies = /* @__PURE__ */ new WeakMap();
8648
+ if (clearBodies) {
8649
+ this.pinBodies = /* @__PURE__ */ new WeakMap();
8650
+ this.markerRenders = /* @__PURE__ */ new WeakMap();
8651
+ }
8038
8652
  this.graphicGroupCache = /* @__PURE__ */ new WeakMap();
8039
8653
  }
8040
8654
  /** Refresh capability-dependent renders before any caller-visible state is mutated. */
8041
8655
  refreshCapabilityGenerations() {
8042
8656
  const currentPointSymbolGeneration = this.pointSymbols?.generation;
8043
8657
  const currentImageGeneration = this.images?.generation;
8044
- if (currentPointSymbolGeneration !== this.lastSeenPointSymbolGeneration || currentImageGeneration !== this.lastSeenImageGeneration) {
8658
+ const currentMarkerGeneration = this.markers?.generation;
8659
+ if (currentPointSymbolGeneration !== this.lastSeenPointSymbolGeneration || currentImageGeneration !== this.lastSeenImageGeneration || currentMarkerGeneration !== this.lastSeenMarkerGeneration) {
8045
8660
  this.invalidateGraphicGroupCache();
8046
8661
  this.lastSeenPointSymbolGeneration = currentPointSymbolGeneration;
8047
8662
  this.lastSeenImageGeneration = currentImageGeneration;
8663
+ this.lastSeenMarkerGeneration = currentMarkerGeneration;
8048
8664
  }
8049
8665
  }
8050
8666
  /** Submit one complete grouped desired state for the host stack plus overrides. */
@@ -8161,7 +8777,7 @@ var TacticalDraw = class {
8161
8777
  seen.add(m.id);
8162
8778
  }
8163
8779
  this.refreshCapabilityGenerations();
8164
- for (const graphic of measures) if (isImageGraphic(graphic)) this.renderGraphicGroup(graphic);
8780
+ for (const graphic of measures) if (isImageGraphic(graphic) || isMarker(graphic)) this.renderGraphicGroup(graphic);
8165
8781
  const activeEdit = this.activeEdit;
8166
8782
  const constructingSession = activeEdit ? null : this.activeInteraction.session;
8167
8783
  const constructingGraphic = constructingSession && "workingGraphic" in constructingSession ? constructingSession.workingGraphic : void 0;
@@ -8251,7 +8867,7 @@ var TacticalDraw = class {
8251
8867
  if (excluded.has(id)) continue;
8252
8868
  const graphic = this.lastRenderedMeasuresById.get(id);
8253
8869
  if (!graphic) continue;
8254
- if (isPointSymbol(graphic)) {
8870
+ if (isPinnableGraphic(graphic)) {
8255
8871
  const pin = this.renderGraphicGroup(graphic).pin;
8256
8872
  if (pin) {
8257
8873
  pinGroups.push({
@@ -8302,6 +8918,10 @@ var TacticalDraw = class {
8302
8918
  if (!this.images) return null;
8303
8919
  return imageFootprint(graphic, renderImageForAdapter(this.adapter, this.images, graphic).render, this.adapter)?.box ?? null;
8304
8920
  }
8921
+ if (isMarker(graphic)) {
8922
+ if (!this.markers) return null;
8923
+ return markerFootprint(graphic, this.markerRender(graphic), this.adapter)?.box ?? null;
8924
+ }
8305
8925
  const features = this.renderGraphicGroup(graphic).features;
8306
8926
  if (graphic.kind === "text") return fitTextFootprintFromFeatures(features, (lonLat) => this.adapter.getPixelFromCoordinate(lonLat), {
8307
8927
  resolution: this.adapter.getResolution(),
@@ -8413,6 +9033,21 @@ var TacticalDraw = class {
8413
9033
  }
8414
9034
  draw(draft, options) {
8415
9035
  this.assertAlive();
9036
+ if (draft.kind === "marker") {
9037
+ if (!this.markers) throw new MarkerCapabilityRequiredError();
9038
+ const preempt = this.guardAndPreempt();
9039
+ if (preempt) return preempt;
9040
+ return this.runWithSlot((onSettled) => runMarkerDraw({
9041
+ adapter: this.adapter,
9042
+ capability: this.markers,
9043
+ draft,
9044
+ previewLayer: this.previewLayer,
9045
+ generateId: this.generateId,
9046
+ signal: options?.signal,
9047
+ onSettled,
9048
+ snap: this.resolveSnap
9049
+ }), null);
9050
+ }
8416
9051
  if (draft.kind === "image") {
8417
9052
  if (!this.images) throw new ImageCapabilityRequiredError();
8418
9053
  const preempt = this.guardAndPreempt();
@@ -8502,6 +9137,47 @@ var TacticalDraw = class {
8502
9137
  }
8503
9138
  edit(measure, options) {
8504
9139
  this.assertAlive();
9140
+ if (isMarker(measure)) {
9141
+ normaliseMarkerModes(options?.modes);
9142
+ if (!this.markers) throw new MarkerCapabilityRequiredError();
9143
+ const preempt = this.guardAndPreempt();
9144
+ if (preempt) return preempt;
9145
+ const wasOnGraphicsLayer = this.lastRenderedMeasures.some((g) => g.id === measure.id);
9146
+ const handleIdPrefix = `__td_edit_${++this.editCounter}__`;
9147
+ return this.runWithSlot((onSettled) => runMarkerEdit({
9148
+ adapter: this.adapter,
9149
+ capability: this.markers,
9150
+ marker: measure,
9151
+ ...this.graphicUsesPinPresentation(measure) ? {
9152
+ renderPin: (marker) => this.buildGraphicGroup(marker),
9153
+ canRenderPin: () => this.graphicUsesPinPresentation(measure)
9154
+ } : {},
9155
+ graphicsLayer: this.graphicsLayer,
9156
+ previewLayer: this.previewLayer,
9157
+ handlesLayer: this.handlesLayer,
9158
+ signal: options?.signal,
9159
+ closeOnClickAway: options?.closeOnClickAway,
9160
+ modes: options?.modes,
9161
+ interactionStyle: this.resolveInteractionStyle(options?.interactionStyle),
9162
+ onSession: this.trackSession(options?.onSession),
9163
+ handleIdPrefix,
9164
+ settleToGraphics: (settled) => this.settleWorkingGraphics([{
9165
+ id: measure.id,
9166
+ committed: settled
9167
+ }]),
9168
+ publishWorkingGraphic: (group) => this.publishWorkingGraphics([group]),
9169
+ wasOnGraphicsLayer,
9170
+ onSettled,
9171
+ onClickAwayGraphicsPick: (event) => this.dispatchHostPick(this.graphicsLayer, event),
9172
+ snap: this.resolveSnap
9173
+ }), (handle) => {
9174
+ handle.syncHostMembership(this.lastRenderedMeasures.some((graphic) => graphic.id === handle.measureId && isMarker(graphic)));
9175
+ this.activeEdit = handle;
9176
+ return () => {
9177
+ if (this.activeEdit === handle) this.activeEdit = null;
9178
+ };
9179
+ });
9180
+ }
8505
9181
  if (isImageGraphic(measure)) {
8506
9182
  normaliseImageModes(options?.modes);
8507
9183
  if (!this.images) throw new ImageCapabilityRequiredError();
@@ -8643,6 +9319,7 @@ var TacticalDraw = class {
8643
9319
  }
8644
9320
  if (measures.some(isPointSymbol) && !this.pointSymbols) throw new PointSymbolCapabilityRequiredError();
8645
9321
  if (measures.some(isImageGraphic) && !this.images) throw new ImageCapabilityRequiredError();
9322
+ if (measures.some(isMarker) && !this.markers) throw new MarkerCapabilityRequiredError();
8646
9323
  const preempt = this.guardAndPreempt();
8647
9324
  if (preempt) return preempt;
8648
9325
  const isOnGraphicsLayer = (id) => this.lastRenderedMeasures.some((m) => m.id === id);
@@ -8658,7 +9335,8 @@ var TacticalDraw = class {
8658
9335
  adapter: this.adapter,
8659
9336
  capability: this.pointSymbols,
8660
9337
  imageCapability: this.images,
8661
- ...this.pointSymbols ? { renderPin: (symbol) => this.buildGraphicGroup(symbol) } : {},
9338
+ markerCapability: this.markers,
9339
+ ...this.pointSymbols || this.markers ? { renderPin: (graphic) => this.buildGraphicGroup(graphic) } : {},
8662
9340
  graphics: measures,
8663
9341
  graphicsLayer: this.graphicsLayer,
8664
9342
  previewLayer: this.previewLayer,
@@ -9037,6 +9715,35 @@ var TacticalDraw = class {
9037
9715
  }
9038
9716
  };
9039
9717
  /**
9718
+ * The graphics-stack group for a pin-presented point symbol or marker
9719
+ * (ADR-0056): a pickable ground point plus the adapter-drawn billboard.
9720
+ */
9721
+ function pinGroup(graphic, revision, pin) {
9722
+ return {
9723
+ id: graphic.id,
9724
+ revision,
9725
+ features: [{
9726
+ type: "Feature",
9727
+ id: `${graphic.id}:pin`,
9728
+ geometry: {
9729
+ type: "Point",
9730
+ coordinates: [...graphic.position]
9731
+ },
9732
+ properties: {
9733
+ graphicId: graphic.id,
9734
+ graphicKind: graphic.kind,
9735
+ pinPresentation: true
9736
+ }
9737
+ }],
9738
+ iconResources: [],
9739
+ pin: {
9740
+ id: graphic.id,
9741
+ position: graphic.position,
9742
+ ...pin
9743
+ }
9744
+ };
9745
+ }
9746
+ /**
9040
9747
  * Classify what live view state a graphic's render reads. Offset point symbols
9041
9748
  * resolve their body and connector in pixel space, so they depend on the whole
9042
9749
  * camera rather than on scale alone.
@@ -9047,6 +9754,7 @@ function graphicViewDependence(graphic, nativePointSymbolScaling) {
9047
9754
  return graphic.size.unit === "meters" && !nativePointSymbolScaling ? "resolution" : "none";
9048
9755
  }
9049
9756
  if (isImageGraphic(graphic)) return "none";
9757
+ if (isMarker(graphic)) return markerScalesWithResolution(graphic, nativePointSymbolScaling) ? "resolution" : "none";
9050
9758
  return measureRendersFromViewState(graphic) ? "resolution" : "none";
9051
9759
  }
9052
9760
  function normalisePointEditModes(modes) {
@@ -9431,4 +10139,4 @@ function nearestWorldCopyPosition(position, refLng) {
9431
10139
  return [nearestWorldCopyLng(position[0], refLng), ...position.slice(1)];
9432
10140
  }
9433
10141
  //#endregion
9434
- export { BaseMapAdapter, DEFAULT_LABEL_SIZE_PIXELS, DEFAULT_PIN_PRESENTATION, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, HATCH_PATTERNS, ImageCapabilityRequiredError, MINE_PATTERN_TILE_SIZE, PinPresentationStore, PointSymbolCapabilityRequiredError, PointerCallbackHub, TEXT_HALO_COLOR, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, TacticalDrawDestroyedError, alignPointSymbolIndicator, bindAbortable, cloneImageGraphic, clonePointSymbol, coerceHandleStyle, combinePinScale, combineWithHostSignal, convertPointSymbolPlacement, effectivePointSymbolPixelSize, evaluatePickCandidates, fillPatternImageDimension, fillPatternImageId, getSizeAnchor, hitTestFeature, hitTestIconFeature, ignoreAbort, imageFootprint, imageOpacity, isAnchoredGraphic, isImageGraphic, isMinePattern, isPointSymbol, isTacticalDrawAbortError, isValidPinNumber, makeFillPatternImage, mergeNativeAndCachedPickFeatures, movePointSymbolLocation, nearestWorldCopyLng, nearestWorldCopyPosition, normalizeImageGraphic, parseColor, pickHitTolerance, pickPointerType, pinDistanceScale, pinZoomScale, placePointSymbol, pointSymbolFootprint, pointSymbolResource, renderOrderToVisualOrder, resizePointSymbolSize, resolveGroundTextSizePixels, resolvePickPresentation, resolvePinPresentation, resolvePointSymbolOffset, resolveTextHaloWidth, resolveTextSizePixels, scalePointSymbolLayout, tryControlMeasureIdFromFeature, validateImageGraphic };
10142
+ export { BaseMapAdapter, DEFAULT_LABEL_SIZE_PIXELS, DEFAULT_MARKER_SIZE, DEFAULT_PIN_PRESENTATION, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, HATCH_PATTERNS, ImageCapabilityRequiredError, MINE_PATTERN_TILE_SIZE, MarkerCapabilityRequiredError, PinPresentationStore, PointSymbolCapabilityRequiredError, PointerCallbackHub, TEXT_HALO_COLOR, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, TacticalDrawDestroyedError, alignPointSymbolIndicator, bindAbortable, cloneImageGraphic, cloneMarker, clonePointSymbol, coerceHandleStyle, combinePinScale, combineWithHostSignal, convertPointSymbolPlacement, effectivePointSymbolPixelSize, evaluatePickCandidates, fillPatternImageDimension, fillPatternImageId, getSizeAnchor, hitTestFeature, hitTestIconFeature, ignoreAbort, imageFootprint, imageOpacity, isAnchoredGraphic, isImageGraphic, isMarker, isMinePattern, isPinnableGraphic, isPointSymbol, isTacticalDrawAbortError, isValidPinNumber, makeFillPatternImage, markerFootprint, markerOpacity, mergeNativeAndCachedPickFeatures, movePointSymbolLocation, nearestWorldCopyLng, nearestWorldCopyPosition, normalizeImageGraphic, normalizeMarker, parseColor, pickHitTolerance, pickPointerType, pinDistanceScale, pinZoomScale, placePointSymbol, pointSymbolFootprint, pointSymbolResource, renderOrderToVisualOrder, resizePointSymbolSize, resolveGroundTextSizePixels, resolveMarkerRender, resolvePickPresentation, resolvePinPresentation, resolvePointSymbolOffset, resolveTextHaloWidth, resolveTextSizePixels, scalePointSymbolLayout, tryControlMeasureIdFromFeature, validateImageGraphic, validateMarker };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orbat-mapper/tactical-draw",
3
- "version": "0.13.2",
3
+ "version": "0.14.1",
4
4
  "description": "Engine-agnostic draw/edit core for tactical control measures (MIL-STD-2525 / APP-6). The MapAdapter ABI and bring-your-own-engine entry point for the tactical-draw package family.",
5
5
  "license": "MIT",
6
6
  "author": "Orbat Mapper",
@@ -39,7 +39,7 @@
39
39
  "access": "public"
40
40
  },
41
41
  "dependencies": {
42
- "@orbat-mapper/control-measures": "^0.32.0"
42
+ "@orbat-mapper/control-measures": "^0.33.0"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "vitest": ">=4.0.0"