@orbat-mapper/tactical-draw 0.2.0-alpha.32 → 0.2.0-alpha.33

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
@@ -877,6 +877,15 @@ interface InteractionStyle {
877
877
  scaleHandle?: HandleStyle;
878
878
  rotateHandle?: HandleStyle;
879
879
  boxOutline?: GuideStyle;
880
+ /**
881
+ * Line style for the passive, non-interactive selection highlight rendered
882
+ * by {@link TacticalDraw.setHighlightedGraphics}. A dashed, muted variant of
883
+ * `boxOutline` so a locked/highlighted selection reads as visually distinct
884
+ * from an active edit's oriented transform box. Delivered per-feature via
885
+ * `properties.style` on the highlight outline `LineString`, resolved through
886
+ * the same built-in → façade → per-call slot system as every other field.
887
+ */
888
+ highlightOutline?: GuideStyle;
880
889
  /**
881
890
  * Pixel padding added on every side of the fitted transform box
882
891
  * (`fitTransformBox`, ADR-0017) so the box doesn't tightly hug the rendered
@@ -1547,6 +1556,12 @@ type DrawPointSymbolDraft = Pick<PointSymbol, "kind" | "sidc"> & Partial<Omit<Po
1547
1556
  */
1548
1557
  interface TacticalDrawLayerOptions {
1549
1558
  graphics?: LayerId;
1559
+ /**
1560
+ * Passive selection-highlight outlines ({@link TacticalDraw.setHighlightedGraphics}).
1561
+ * Resolved immediately above `graphics` so highlight boxes stack over base
1562
+ * graphics but below the preview / guide / handles interaction chrome.
1563
+ */
1564
+ highlight?: LayerId;
1550
1565
  preview?: LayerId;
1551
1566
  guide?: LayerId;
1552
1567
  handles?: LayerId;
@@ -1733,6 +1748,7 @@ interface DrawOptions {
1733
1748
  declare class TacticalDraw {
1734
1749
  private readonly adapter;
1735
1750
  private readonly graphicsLayer;
1751
+ private readonly highlightLayer;
1736
1752
  private readonly previewLayer;
1737
1753
  private readonly guideLayer;
1738
1754
  private readonly handlesLayer;
@@ -1794,6 +1810,20 @@ declare class TacticalDraw {
1794
1810
  */
1795
1811
  private lastRenderedMeasures;
1796
1812
  private snapGeometryFeatures;
1813
+ /**
1814
+ * Ids of graphics currently carrying a passive selection-highlight outline
1815
+ * ({@link setHighlightedGraphics}). Declarative set-replace, no session —
1816
+ * re-projected on every view change and re-evaluated at the end of every
1817
+ * `render()` so outlines track geometry, style, and dropped graphics.
1818
+ */
1819
+ private highlightedIds;
1820
+ /**
1821
+ * Whether the highlight layer currently holds any outline. Lets
1822
+ * `renderHighlights` skip a redundant empty write on every view change /
1823
+ * render while no highlights are set, but still emit the one clearing write
1824
+ * that removes outlines when the set transitions to empty.
1825
+ */
1826
+ private hasHighlightOutlines;
1797
1827
  constructor(adapter: MapAdapter, options?: TacticalDrawOptions);
1798
1828
  /** Replace snapping configuration without recreating the map adapter or façade. */
1799
1829
  setSnappingOptions(options?: SnappingOptions): void;
@@ -1858,6 +1888,34 @@ declare class TacticalDraw {
1858
1888
  * pure style replay).
1859
1889
  */
1860
1890
  render(measures: readonly Graphic[]): void;
1891
+ /**
1892
+ * Replace the passive selection-highlight set with `ids`. Declarative and
1893
+ * idempotent (mirrors {@link render}): a non-interactive outline box is drawn
1894
+ * around each still-rendered graphic on the dedicated highlight layer — no
1895
+ * draw / edit / transform session, no promise, no pick subscription. Passing
1896
+ * `[]` clears every highlight. Unknown ids and ids currently lifted into an
1897
+ * active edit / transform session are skipped (the live session's own visuals
1898
+ * supersede the passive outline). Hosts use it for Excalidraw-style
1899
+ * "locked selection" chrome.
1900
+ */
1901
+ setHighlightedGraphics(ids: readonly string[]): void;
1902
+ /**
1903
+ * Project the current {@link highlightedIds} into one outline `LineString`
1904
+ * per graphic and reconcile them onto the highlight layer. Idempotent and
1905
+ * id-keyed via `adapter.updateLayerFeatures`, so a set-replace removes stale
1906
+ * outlines and an unchanged set is a cheap no-op write. Skips ids that are
1907
+ * unknown (not in the last render) or lifted into an active session, and
1908
+ * bails gracefully when the adapter cannot invert pixels to coordinates
1909
+ * (`getCoordinateFromPixel` is optional in the MapAdapter ABI).
1910
+ */
1911
+ private renderHighlights;
1912
+ /**
1913
+ * Fit the passive-highlight transform box for one graphic: the point-symbol
1914
+ * footprint for symbols, the snug text footprint for Text graphics, and the
1915
+ * projected-geometry bounding box (at the current bearing) for every other
1916
+ * control measure. Returns `null` when nothing projects this frame.
1917
+ */
1918
+ private highlightBoxForGraphic;
1861
1919
  /**
1862
1920
  * Nested-call guard + preempt, shared by `draw` and `edit`. Returns a
1863
1921
  * pre-rejected promise when called re-entrantly from inside another
@@ -2093,6 +2151,7 @@ declare class TacticalDraw {
2093
2151
  /** @internal — exposed for tests and future controllers. */
2094
2152
  get layerIds(): {
2095
2153
  graphics: LayerId;
2154
+ highlight: LayerId;
2096
2155
  preview: LayerId;
2097
2156
  guide: LayerId;
2098
2157
  handles: LayerId;
@@ -2104,6 +2163,7 @@ declare class TacticalDraw {
2104
2163
  */
2105
2164
  private applyGuideStyle;
2106
2165
  private baseGuideStyle;
2166
+ private baseHighlightStyle;
2107
2167
  /**
2108
2168
  * Resolve handle slots (plus `boxOutline`) across built-in → façade →
2109
2169
  * per-call with per-slot, per-property shallow merge. Explicit `undefined`
package/dist/index.mjs CHANGED
@@ -5843,6 +5843,7 @@ function runPointSymbolEdit(opts) {
5843
5843
  var TacticalDraw = class {
5844
5844
  adapter;
5845
5845
  graphicsLayer;
5846
+ highlightLayer;
5846
5847
  previewLayer;
5847
5848
  guideLayer;
5848
5849
  handlesLayer;
@@ -5904,6 +5905,20 @@ var TacticalDraw = class {
5904
5905
  */
5905
5906
  lastRenderedMeasures = [];
5906
5907
  snapGeometryFeatures = [];
5908
+ /**
5909
+ * Ids of graphics currently carrying a passive selection-highlight outline
5910
+ * ({@link setHighlightedGraphics}). Declarative set-replace, no session —
5911
+ * re-projected on every view change and re-evaluated at the end of every
5912
+ * `render()` so outlines track geometry, style, and dropped graphics.
5913
+ */
5914
+ highlightedIds = [];
5915
+ /**
5916
+ * Whether the highlight layer currently holds any outline. Lets
5917
+ * `renderHighlights` skip a redundant empty write on every view change /
5918
+ * render while no highlights are set, but still emit the one clearing write
5919
+ * that removes outlines when the set transitions to empty.
5920
+ */
5921
+ hasHighlightOutlines = false;
5907
5922
  constructor(adapter, options) {
5908
5923
  this.adapter = adapter;
5909
5924
  this.graphicsStyle = resolveStyleHints(BUILT_IN_GRAPHICS_STYLE, options?.graphicsStyle, void 0);
@@ -5915,6 +5930,7 @@ var TacticalDraw = class {
5915
5930
  const owned = /* @__PURE__ */ new Set();
5916
5931
  const provided = options?.layers ?? {};
5917
5932
  this.graphicsLayer = resolveSlot(adapter, provided.graphics, owned);
5933
+ this.highlightLayer = resolveSlot(adapter, provided.highlight, owned);
5918
5934
  this.previewLayer = resolveSlot(adapter, provided.preview, owned);
5919
5935
  this.guideLayer = resolveSlot(adapter, provided.guide, owned);
5920
5936
  this.handlesLayer = resolveSlot(adapter, provided.handles, owned);
@@ -5923,6 +5939,7 @@ var TacticalDraw = class {
5923
5939
  this.adapter.setLayerStyle(this.graphicsLayer, graphicsLayerStyle);
5924
5940
  this.adapter.setLayerStyle(this.previewLayer, graphicsLayerStyle);
5925
5941
  this.adapter.setLayerStyle(this.guideLayer, guideToStyleOptions(this.baseGuideStyle()));
5942
+ this.adapter.setLayerStyle(this.highlightLayer, guideToStyleOptions(this.baseHighlightStyle()));
5926
5943
  this.adapter.onViewChange(this.handleViewChange);
5927
5944
  }
5928
5945
  /** Replace snapping configuration without recreating the map adapter or façade. */
@@ -5970,6 +5987,7 @@ var TacticalDraw = class {
5970
5987
  */
5971
5988
  handleViewChange = () => {
5972
5989
  if (this.destroyed || !this.hasRendered) return;
5990
+ this.renderHighlights();
5973
5991
  const excluded = this.excludedInteractionIds();
5974
5992
  const visible = excluded.size > 0 ? this.lastRenderedMeasures.filter((m) => !excluded.has(m.id)) : this.lastRenderedMeasures;
5975
5993
  const nativeGroundIconScaling = this.adapter.scalesGroundPointSymbolsNatively === true;
@@ -6067,6 +6085,88 @@ var TacticalDraw = class {
6067
6085
  if (handle.graphicIds.some((id) => !rendered.has(id))) handle.abort("removed");
6068
6086
  });
6069
6087
  }
6088
+ this.renderHighlights();
6089
+ }
6090
+ /**
6091
+ * Replace the passive selection-highlight set with `ids`. Declarative and
6092
+ * idempotent (mirrors {@link render}): a non-interactive outline box is drawn
6093
+ * around each still-rendered graphic on the dedicated highlight layer — no
6094
+ * draw / edit / transform session, no promise, no pick subscription. Passing
6095
+ * `[]` clears every highlight. Unknown ids and ids currently lifted into an
6096
+ * active edit / transform session are skipped (the live session's own visuals
6097
+ * supersede the passive outline). Hosts use it for Excalidraw-style
6098
+ * "locked selection" chrome.
6099
+ */
6100
+ setHighlightedGraphics(ids) {
6101
+ this.assertAlive();
6102
+ this.highlightedIds = ids.slice();
6103
+ this.renderHighlights();
6104
+ }
6105
+ /**
6106
+ * Project the current {@link highlightedIds} into one outline `LineString`
6107
+ * per graphic and reconcile them onto the highlight layer. Idempotent and
6108
+ * id-keyed via `adapter.updateLayerFeatures`, so a set-replace removes stale
6109
+ * outlines and an unchanged set is a cheap no-op write. Skips ids that are
6110
+ * unknown (not in the last render) or lifted into an active session, and
6111
+ * bails gracefully when the adapter cannot invert pixels to coordinates
6112
+ * (`getCoordinateFromPixel` is optional in the MapAdapter ABI).
6113
+ */
6114
+ renderHighlights() {
6115
+ if (this.destroyed) return;
6116
+ if (this.highlightedIds.length === 0 && !this.hasHighlightOutlines) return;
6117
+ const fromPixel = this.adapter.getCoordinateFromPixel?.bind(this.adapter);
6118
+ if (!fromPixel) return;
6119
+ const resolved = this.resolveInteractionStyle(void 0);
6120
+ const style = resolved.highlightOutline;
6121
+ const padding = resolved.boxPadding ?? 0;
6122
+ const excluded = this.excludedInteractionIds();
6123
+ const bearing = this.adapter.getBearing();
6124
+ const features = [];
6125
+ for (const id of this.highlightedIds) {
6126
+ if (excluded.has(id)) continue;
6127
+ const graphic = this.lastRenderedMeasures.find((g) => g.id === id);
6128
+ if (!graphic) continue;
6129
+ const box = this.highlightBoxForGraphic(graphic, bearing, padding);
6130
+ if (!box) continue;
6131
+ let invertFailed = false;
6132
+ const invert = (pixel) => {
6133
+ const coord = fromPixel(pixel);
6134
+ if (!coord) {
6135
+ invertFailed = true;
6136
+ return [pixel[0], pixel[1]];
6137
+ }
6138
+ return coord;
6139
+ };
6140
+ const feature = boxOutlineFeature(box, invert, `__td_highlight__:${id}`, style);
6141
+ if (!invertFailed) features.push(feature);
6142
+ }
6143
+ this.adapter.updateLayerFeatures(this.highlightLayer, features);
6144
+ this.hasHighlightOutlines = features.length > 0;
6145
+ }
6146
+ /**
6147
+ * Fit the passive-highlight transform box for one graphic: the point-symbol
6148
+ * footprint for symbols, the snug text footprint for Text graphics, and the
6149
+ * projected-geometry bounding box (at the current bearing) for every other
6150
+ * control measure. Returns `null` when nothing projects this frame.
6151
+ */
6152
+ highlightBoxForGraphic(graphic, bearing, padding) {
6153
+ if (isPointSymbol(graphic)) {
6154
+ if (!this.pointSymbols) return null;
6155
+ return pointSymbolFootprint(graphic, this.pointSymbols.render(graphic), this.adapter)?.box ?? null;
6156
+ }
6157
+ const render = renderMeasureForAdapter(this.adapter, graphic, { graphicsStyle: this.graphicsStyle });
6158
+ if (graphic.kind === "text") return fitTextFootprintFromFeatures(render.features, (lonLat) => this.adapter.getPixelFromCoordinate(lonLat), {
6159
+ resolution: this.adapter.getResolution(),
6160
+ bearing
6161
+ });
6162
+ const positions = [];
6163
+ for (const feature of render.features) if (feature.geometry) collectFeaturePositions(feature.geometry, positions);
6164
+ const pixels = [];
6165
+ for (const position of positions) {
6166
+ const pixel = this.adapter.getPixelFromCoordinate(position);
6167
+ if (pixel) pixels.push(pixel);
6168
+ }
6169
+ return fitTransformBox(pixels, bearing, padding);
6070
6170
  }
6071
6171
  /**
6072
6172
  * Nested-call guard + preempt, shared by `draw` and `edit`. Returns a
@@ -6121,6 +6221,7 @@ var TacticalDraw = class {
6121
6221
  clearSlot();
6122
6222
  this.clearSnapResult();
6123
6223
  if (extraSettle) extraSettle();
6224
+ this.renderHighlights();
6124
6225
  };
6125
6226
  this.activeInteraction.begin();
6126
6227
  try {
@@ -6132,7 +6233,10 @@ var TacticalDraw = class {
6132
6233
  }
6133
6234
  if (!settledDuringCreate) {
6134
6235
  this.activeInteraction.set(handle);
6135
- if (activate && !handle.settled) deactivate = activate(handle) ?? null;
6236
+ if (activate && !handle.settled) {
6237
+ deactivate = activate(handle) ?? null;
6238
+ this.renderHighlights();
6239
+ }
6136
6240
  }
6137
6241
  handle.promise.then(clearSlot, clearSlot);
6138
6242
  return handle.promise;
@@ -6648,6 +6752,7 @@ var TacticalDraw = class {
6648
6752
  get layerIds() {
6649
6753
  return {
6650
6754
  graphics: this.graphicsLayer,
6755
+ highlight: this.highlightLayer,
6651
6756
  preview: this.previewLayer,
6652
6757
  guide: this.guideLayer,
6653
6758
  handles: this.handlesLayer
@@ -6670,6 +6775,9 @@ var TacticalDraw = class {
6670
6775
  baseGuideStyle() {
6671
6776
  return mergeSlot(BUILT_IN_INTERACTION_STYLE.guide, this.defaultInteractionStyle?.guide);
6672
6777
  }
6778
+ baseHighlightStyle() {
6779
+ return mergeSlot(BUILT_IN_INTERACTION_STYLE.highlightOutline, this.defaultInteractionStyle?.highlightOutline);
6780
+ }
6673
6781
  /**
6674
6782
  * Resolve handle slots (plus `boxOutline`) across built-in → façade →
6675
6783
  * per-call with per-slot, per-property shallow merge. Explicit `undefined`
@@ -6697,6 +6805,8 @@ var TacticalDraw = class {
6697
6805
  }
6698
6806
  const boxOutline = mergeSlot(mergeSlot(BUILT_IN_INTERACTION_STYLE.boxOutline, this.defaultInteractionStyle?.boxOutline), perCall?.boxOutline);
6699
6807
  if (boxOutline) out.boxOutline = boxOutline;
6808
+ const highlightOutline = mergeSlot(mergeSlot(BUILT_IN_INTERACTION_STYLE.highlightOutline, this.defaultInteractionStyle?.highlightOutline), perCall?.highlightOutline);
6809
+ if (highlightOutline) out.highlightOutline = highlightOutline;
6700
6810
  const boxPadding = perCall && "boxPadding" in perCall ? perCall.boxPadding : this.defaultInteractionStyle && "boxPadding" in this.defaultInteractionStyle ? this.defaultInteractionStyle.boxPadding : void 0;
6701
6811
  if (typeof boxPadding === "number") out.boxPadding = boxPadding;
6702
6812
  return out;
@@ -6737,6 +6847,11 @@ const BUILT_IN_INTERACTION_STYLE = {
6737
6847
  strokeColor: "#4f7cff",
6738
6848
  strokeWidth: 1
6739
6849
  },
6850
+ highlightOutline: {
6851
+ strokeColor: "#8ea3c9",
6852
+ strokeWidth: 1.5,
6853
+ strokeDash: [4, 4]
6854
+ },
6740
6855
  vertexHandle: {
6741
6856
  pointRadius: 6,
6742
6857
  fillColor: "#ffffff",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orbat-mapper/tactical-draw",
3
- "version": "0.2.0-alpha.32",
3
+ "version": "0.2.0-alpha.33",
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",