@orbat-mapper/tactical-draw 0.2.0-alpha.25 → 0.2.0-alpha.27

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
@@ -180,6 +180,12 @@ interface MapAdapter {
180
180
  * silently ignored.
181
181
  */
182
182
  removeFeatures(layerId: LayerId, ids: readonly string[]): void;
183
+ /**
184
+ * Defer an upcoming removal from `layerId` until the most recent update to
185
+ * `updatedLayerId` has completed in the engine. Optional because synchronous
186
+ * adapters already make cross-layer write order visible atomically.
187
+ */
188
+ deferLayerRemovalUntilLayerUpdate?(layerId: LayerId, updatedLayerId: LayerId): void;
183
189
  /**
184
190
  * Subscribe to click-style "pick" events for a specific layer. Basemap and
185
191
  * other-layer clicks never fire the handler.
@@ -544,6 +550,12 @@ interface PointSymbolCapability {
544
550
  }
545
551
  declare function isPointSymbol(graphic: unknown): graphic is PointSymbol;
546
552
  declare function clonePointSymbol(symbol: PointSymbol): PointSymbol;
553
+ /**
554
+ * Replace a point-symbol size value as a user-driven resize. Ground-size display
555
+ * bounds scale with the value so they continue to constrain zoom changes rather
556
+ * than preventing the resize itself.
557
+ */
558
+ declare function resizePointSymbolSize(size: PointSymbolSize, value: number): PointSymbolSize;
547
559
  //#endregion
548
560
  //#region src/graphic.d.ts
549
561
  type Graphic = ControlMeasure | PointSymbol;
@@ -1512,7 +1524,7 @@ declare class TacticalDraw {
1512
1524
  * Routes every measure through [[renderMeasureForAdapter]] so pixel sizing is
1513
1525
  * resolved against the live map. Callers own any edit-exclusion policy — see
1514
1526
  * `render()` (excludes the edited measure, owned by the preview layer) versus
1515
- * `restoreOriginalToGraphics` (no exclusion, the edit is ending).
1527
+ * `settleToGraphics` (no exclusion, the edit is ending).
1516
1528
  */
1517
1529
  private renderMeasuresToGraphics;
1518
1530
  /**
@@ -1880,6 +1892,7 @@ interface PointSymbolFootprint {
1880
1892
  box: TransformBox;
1881
1893
  corners: PixelCoordinate[];
1882
1894
  }
1895
+ declare function effectivePointSymbolPixelSize(symbol: PointSymbol, resolution: number | undefined): number;
1883
1896
  /** Complete rotated logical footprint for selection and transform chrome. */
1884
1897
  declare function pointSymbolFootprint(symbol: PointSymbol, render: PointSymbolRender, adapter: Pick<MapAdapter, "getBearing" | "getPixelFromCoordinate" | "getResolution">): PointSymbolFootprint | null;
1885
1898
  //#endregion
@@ -1897,4 +1910,4 @@ declare function nearestWorldCopyLng(lng: number, refLng: number): number;
1897
1910
  /** `nearestWorldCopyLng` applied to a lon/lat position. */
1898
1911
  declare function nearestWorldCopyPosition(position: Position, refLng: number): Position;
1899
1912
  //#endregion
1900
- 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 GraphicGeometrySnappingOptions, type GraphicRender, type GraphicSnapshot, 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 SessionChangeSource, type SessionHistory, type SessionHistoryState, type SettleReason, type SizeAnchor, type SnapCandidate, type SnapCandidateProvider, type SnapCandidateRequest, type SnapInteraction, type SnapResult, type SnappingOptions, 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 };
1913
+ 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 GraphicGeometrySnappingOptions, type GraphicRender, type GraphicSnapshot, 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 SessionChangeSource, type SessionHistory, type SessionHistoryState, type SettleReason, type SizeAnchor, type SnapCandidate, type SnapCandidateProvider, type SnapCandidateRequest, type SnapInteraction, type SnapResult, type SnappingOptions, 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, effectivePointSymbolPixelSize, getSizeAnchor, hitTestFeature, hitTestIconFeature, ignoreAbort, isPointSymbol, isTacticalDrawAbortError, nearestWorldCopyLng, nearestWorldCopyPosition, pickHitTolerance, pointSymbolFootprint, resizePointSymbolSize, resolveGroundTextSizePixels, tryControlMeasureIdFromFeature };
package/dist/index.mjs CHANGED
@@ -461,6 +461,28 @@ function isPointSymbol(graphic) {
461
461
  function clonePointSymbol(symbol) {
462
462
  return structuredClone(symbol);
463
463
  }
464
+ /**
465
+ * Replace a point-symbol size value as a user-driven resize. Ground-size display
466
+ * bounds scale with the value so they continue to constrain zoom changes rather
467
+ * than preventing the resize itself.
468
+ */
469
+ function resizePointSymbolSize(size, value) {
470
+ if (size.unit === "pixels") return {
471
+ value,
472
+ unit: "pixels"
473
+ };
474
+ const scale = value / size.value;
475
+ if (!Number.isFinite(scale) || scale <= 0) return {
476
+ ...size,
477
+ value
478
+ };
479
+ return {
480
+ ...size,
481
+ value,
482
+ ...size.minPixels !== void 0 ? { minPixels: size.minPixels * scale } : {},
483
+ ...size.maxPixels !== void 0 ? { maxPixels: size.maxPixels * scale } : {}
484
+ };
485
+ }
464
486
  //#endregion
465
487
  //#region src/tactical-draw/active-interaction.ts
466
488
  /**
@@ -1875,6 +1897,38 @@ function runEditInteraction(spec) {
1875
1897
  attach(interaction) {
1876
1898
  ctx = interaction;
1877
1899
  driver = adapter.createEditPointerDriver();
1900
+ let beforeTeardownRan = false;
1901
+ const runBeforeTeardown = (settlement) => {
1902
+ if (beforeTeardownRan) return;
1903
+ beforeTeardownRan = true;
1904
+ try {
1905
+ spec.onBeforeTeardown?.(settlement);
1906
+ } catch {}
1907
+ };
1908
+ const controllerContext = {
1909
+ get settled() {
1910
+ return interaction.settled;
1911
+ },
1912
+ commit(produce) {
1913
+ return interaction.commit(() => {
1914
+ const value = produce();
1915
+ runBeforeTeardown({
1916
+ status: "committed",
1917
+ value
1918
+ });
1919
+ return value;
1920
+ });
1921
+ },
1922
+ fail(error) {
1923
+ if (!interaction.settled) runBeforeTeardown({
1924
+ status: "rejected",
1925
+ error
1926
+ });
1927
+ interaction.fail(error);
1928
+ },
1929
+ writeLayer: interaction.writeLayer,
1930
+ onTeardown: interaction.onTeardown
1931
+ };
1878
1932
  const unsubDown = driver.onPointerDown(spec.onDown);
1879
1933
  const unsubMove = driver.onPointerMove(spec.onMove);
1880
1934
  const unsubUp = driver.onPointerUp(spec.onUp);
@@ -1883,6 +1937,10 @@ function runEditInteraction(spec) {
1883
1937
  const unsubGraphics = adapter.onPick(graphicsLayer, onGraphicsPick);
1884
1938
  adapter.on("click", onClick);
1885
1939
  interaction.onTeardown(() => {
1940
+ runBeforeTeardown({
1941
+ status: "rejected",
1942
+ error: void 0
1943
+ });
1886
1944
  adapter.off("click", onClick);
1887
1945
  unsubPreview();
1888
1946
  unsubHandles();
@@ -1893,7 +1951,7 @@ function runEditInteraction(spec) {
1893
1951
  spec.extraTeardown?.();
1894
1952
  driver.dispose();
1895
1953
  }, spec.onAbort);
1896
- spec.attach(interaction, driver, shell);
1954
+ spec.attach(controllerContext, driver, shell);
1897
1955
  }
1898
1956
  });
1899
1957
  }
@@ -2961,7 +3019,7 @@ const ALLOWED_EDIT_MODES = new Set([
2961
3019
  * - close / abort / signal / preempt / destroy paths
2962
3020
  */
2963
3021
  function runEdit(opts) {
2964
- const { adapter, measure, graphicsLayer, previewLayer, guideLayer, handlesLayer, graphicsStyle, signal, closeOnClickAway = true, modes: initialModes, interactionStyle, onSession, handleIdPrefix, guide: guideOpt, restoreOriginalToGraphics, wasOnGraphicsLayer, onSettled, onClickAwayGraphicsPick, snap } = opts;
3022
+ const { adapter, measure, graphicsLayer, previewLayer, guideLayer, handlesLayer, graphicsStyle, signal, closeOnClickAway = true, modes: initialModes, interactionStyle, onSession, handleIdPrefix, guide: guideOpt, settleToGraphics, wasOnGraphicsLayer, onSettled, onClickAwayGraphicsPick, snap } = opts;
2965
3023
  const renderOpts = graphicsStyle ? { graphicsStyle } : void 0;
2966
3024
  let sizeAnchor = opts.sizeAnchor ?? "ground";
2967
3025
  const metadata = getControlMeasureMetadata(measure.kind);
@@ -3836,6 +3894,9 @@ function runEdit(opts) {
3836
3894
  previewLayer,
3837
3895
  handlesLayer,
3838
3896
  closeOnClickAway,
3897
+ onBeforeTeardown: (settlement) => {
3898
+ settleToGraphics(settlement.status === "committed" ? settlement.value.graphic : void 0);
3899
+ },
3839
3900
  onViewChange: rescaleOnViewChange,
3840
3901
  onDown: onDriverDown,
3841
3902
  onMove: onDriverMove,
@@ -3857,13 +3918,15 @@ function runEdit(opts) {
3857
3918
  dragInFlight = false;
3858
3919
  }
3859
3920
  },
3860
- onAbort: wasOnGraphicsLayer ? restoreOriginalToGraphics : void 0,
3861
3921
  attach(interaction, createdDriver, createdShell) {
3862
3922
  ctx = interaction;
3863
3923
  driver = createdDriver;
3864
3924
  shell = createdShell;
3865
- liftOriginalFromGraphics();
3866
3925
  renderPreview();
3926
+ if (wasOnGraphicsLayer) {
3927
+ adapter.deferLayerRemovalUntilLayerUpdate?.(graphicsLayer, previewLayer);
3928
+ liftOriginalFromGraphics();
3929
+ }
3867
3930
  renderHandlesLayer();
3868
3931
  recomputeDirty();
3869
3932
  if (onSession) onSession(session);
@@ -4134,10 +4197,7 @@ function runGroupTransform(opts) {
4134
4197
  function buildWorkingPointSymbol(member) {
4135
4198
  const symbol = clonePointSymbol(member.graphic);
4136
4199
  symbol.position = [...member.workingPoints[0]];
4137
- symbol.size = {
4138
- ...symbol.size,
4139
- value: member.workingSize
4140
- };
4200
+ symbol.size = resizePointSymbolSize(symbol.size, member.workingSize);
4141
4201
  symbol.rotation = member.workingRotation;
4142
4202
  return symbol;
4143
4203
  }
@@ -4859,7 +4919,7 @@ function symbolBox(adapter, capability, symbol, padding = 0) {
4859
4919
  };
4860
4920
  }
4861
4921
  function runPointSymbolEdit(opts) {
4862
- const { adapter, capability, symbol, graphicsLayer, previewLayer, handlesLayer, signal, closeOnClickAway = true, interactionStyle, onSession, handleIdPrefix, restoreOriginalToGraphics, wasOnGraphicsLayer, onSettled, onClickAwayGraphicsPick, snap } = opts;
4922
+ const { adapter, capability, symbol, graphicsLayer, previewLayer, handlesLayer, signal, closeOnClickAway = true, interactionStyle, onSession, handleIdPrefix, settleToGraphics, wasOnGraphicsLayer, onSettled, onClickAwayGraphicsPick, snap } = opts;
4863
4923
  let modes = normalisePointModes(opts.modes);
4864
4924
  let working = clonePointSymbol(symbol);
4865
4925
  let dirty = false;
@@ -5010,7 +5070,7 @@ function runPointSymbolEdit(opts) {
5010
5070
  next.rotation = normalizeRotation(info?.shiftKey ? constrainRotation(rotation) : rotation);
5011
5071
  } else {
5012
5072
  const distance = Math.hypot(pixel[0] - drag.anchor[0], pixel[1] - drag.anchor[1]);
5013
- next.size.value = Math.max(.1, drag.start.size.value * (distance / drag.startDistance));
5073
+ next.size = resizePointSymbolSize(drag.start.size, Math.max(.1, drag.start.size.value * (distance / drag.startDistance)));
5014
5074
  }
5015
5075
  return next;
5016
5076
  };
@@ -5044,6 +5104,9 @@ function runPointSymbolEdit(opts) {
5044
5104
  handlesLayer,
5045
5105
  closeOnClickAway,
5046
5106
  onViewChange: render,
5107
+ onBeforeTeardown: (settlement) => {
5108
+ settleToGraphics(settlement.status === "committed" ? settlement.value.graphic : void 0);
5109
+ },
5047
5110
  onDown,
5048
5111
  onMove,
5049
5112
  onUp,
@@ -5056,15 +5119,15 @@ function runPointSymbolEdit(opts) {
5056
5119
  facadeOnSettled: onSettled,
5057
5120
  emitSettle: (settlement) => settleFromInteraction(settlement, commitEmitter, settleEmitter),
5058
5121
  extraTeardown: () => adapter.reconcileLayerIconResources(previewLayer, []),
5059
- onAbort: () => {
5060
- if (wasOnGraphicsLayer) restoreOriginalToGraphics();
5061
- },
5062
5122
  attach(interaction, createdDriver, createdShell) {
5063
5123
  ctx = interaction;
5064
5124
  driver = createdDriver;
5065
5125
  shell = createdShell;
5066
- if (wasOnGraphicsLayer) adapter.removeFeatures(graphicsLayer, [`${symbol.id}:point-symbol`]);
5067
5126
  render();
5127
+ if (wasOnGraphicsLayer) {
5128
+ adapter.deferLayerRemovalUntilLayerUpdate?.(graphicsLayer, previewLayer);
5129
+ adapter.removeFeatures(graphicsLayer, [`${symbol.id}:point-symbol`]);
5130
+ }
5068
5131
  onSession?.(session);
5069
5132
  }
5070
5133
  });
@@ -5186,7 +5249,7 @@ var TacticalDraw = class {
5186
5249
  * Routes every measure through [[renderMeasureForAdapter]] so pixel sizing is
5187
5250
  * resolved against the live map. Callers own any edit-exclusion policy — see
5188
5251
  * `render()` (excludes the edited measure, owned by the preview layer) versus
5189
- * `restoreOriginalToGraphics` (no exclusion, the edit is ending).
5252
+ * `settleToGraphics` (no exclusion, the edit is ending).
5190
5253
  */
5191
5254
  renderMeasuresToGraphics(graphics) {
5192
5255
  const features = [];
@@ -5482,7 +5545,12 @@ var TacticalDraw = class {
5482
5545
  const preempt = this.guardAndPreempt();
5483
5546
  if (preempt) return preempt;
5484
5547
  const wasOnGraphicsLayer = this.lastRenderedMeasures.some((m) => m.id === measure.id);
5485
- const restoreOriginalToGraphics = () => this.renderMeasuresToGraphics(this.lastRenderedMeasures);
5548
+ const settleToGraphics = (settledSymbol) => {
5549
+ if (!wasOnGraphicsLayer) return;
5550
+ const graphics = settledSymbol ? this.lastRenderedMeasures.map((graphic) => graphic.id === measure.id ? settledSymbol : graphic) : this.lastRenderedMeasures;
5551
+ this.renderMeasuresToGraphics(graphics);
5552
+ this.adapter.deferLayerRemovalUntilLayerUpdate?.(this.previewLayer, this.graphicsLayer);
5553
+ };
5486
5554
  const handleIdPrefix = `__td_edit_${++this.editCounter}__`;
5487
5555
  return this.runWithSlot((onSettled) => runPointSymbolEdit({
5488
5556
  adapter: this.adapter,
@@ -5497,7 +5565,7 @@ var TacticalDraw = class {
5497
5565
  interactionStyle: this.resolveInteractionStyle(options?.interactionStyle),
5498
5566
  onSession: options?.onSession,
5499
5567
  handleIdPrefix,
5500
- restoreOriginalToGraphics,
5568
+ settleToGraphics,
5501
5569
  wasOnGraphicsLayer,
5502
5570
  onSettled,
5503
5571
  onClickAwayGraphicsPick: (event) => this.dispatchHostPick(this.graphicsLayer, event),
@@ -5513,8 +5581,11 @@ var TacticalDraw = class {
5513
5581
  const preempt = this.guardAndPreempt();
5514
5582
  if (preempt) return preempt;
5515
5583
  const wasOnGraphicsLayer = this.lastRenderedMeasures.some((m) => m.id === measure.id);
5516
- const restoreOriginalToGraphics = () => {
5517
- this.renderMeasuresToGraphics(this.lastRenderedMeasures);
5584
+ const settleToGraphics = (settledMeasure) => {
5585
+ if (!wasOnGraphicsLayer) return;
5586
+ const graphics = settledMeasure ? this.lastRenderedMeasures.map((graphic) => graphic.id === measure.id ? settledMeasure : graphic) : this.lastRenderedMeasures;
5587
+ this.renderMeasuresToGraphics(graphics);
5588
+ this.adapter.deferLayerRemovalUntilLayerUpdate?.(this.previewLayer, this.graphicsLayer);
5518
5589
  };
5519
5590
  const handleIdPrefix = `__td_edit_${++this.editCounter}__`;
5520
5591
  const mergedInteractionStyle = this.resolveInteractionStyle(options?.interactionStyle);
@@ -5538,7 +5609,7 @@ var TacticalDraw = class {
5538
5609
  handleIdPrefix,
5539
5610
  guide: options?.guide,
5540
5611
  sizeAnchor: options?.sizeAnchor,
5541
- restoreOriginalToGraphics,
5612
+ settleToGraphics,
5542
5613
  wasOnGraphicsLayer,
5543
5614
  onSettled,
5544
5615
  onClickAwayGraphicsPick,
@@ -6040,4 +6111,4 @@ function nearestWorldCopyPosition(position, refLng) {
6040
6111
  return [nearestWorldCopyLng(position[0], refLng), ...position.slice(1)];
6041
6112
  }
6042
6113
  //#endregion
6043
- export { BaseMapAdapter, DEFAULT_LABEL_SIZE_PIXELS, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, PointSymbolCapabilityRequiredError, PointerCallbackHub, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, TacticalDrawDestroyedError, bindAbortable, clonePointSymbol, coerceHandleStyle, combineWithHostSignal, getSizeAnchor, hitTestFeature, hitTestIconFeature, ignoreAbort, isPointSymbol, isTacticalDrawAbortError, nearestWorldCopyLng, nearestWorldCopyPosition, pickHitTolerance, pointSymbolFootprint, resolveGroundTextSizePixels, tryControlMeasureIdFromFeature };
6114
+ export { BaseMapAdapter, DEFAULT_LABEL_SIZE_PIXELS, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, PointSymbolCapabilityRequiredError, PointerCallbackHub, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, TacticalDrawDestroyedError, bindAbortable, clonePointSymbol, coerceHandleStyle, combineWithHostSignal, effectivePointSymbolPixelSize, getSizeAnchor, hitTestFeature, hitTestIconFeature, ignoreAbort, isPointSymbol, isTacticalDrawAbortError, nearestWorldCopyLng, nearestWorldCopyPosition, pickHitTolerance, pointSymbolFootprint, resizePointSymbolSize, resolveGroundTextSizePixels, tryControlMeasureIdFromFeature };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orbat-mapper/tactical-draw",
3
- "version": "0.2.0-alpha.25",
3
+ "version": "0.2.0-alpha.27",
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",