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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -15,6 +15,8 @@ var BaseMapAdapter = class {
15
15
  viewChangeTokens = /* @__PURE__ */ new Map();
16
16
  featureCache = /* @__PURE__ */ new Map();
17
17
  featuresChangedListeners = /* @__PURE__ */ new Map();
18
+ iconResources = /* @__PURE__ */ new Map();
19
+ layerIconResourceIds = /* @__PURE__ */ new Map();
18
20
  layerCounter = 0;
19
21
  destroyed = false;
20
22
  nextLayerId() {
@@ -34,6 +36,19 @@ var BaseMapAdapter = class {
34
36
  this.layers.delete(layerId);
35
37
  this.featureCache.delete(layerId);
36
38
  this.featuresChangedListeners.delete(layerId);
39
+ this.layerIconResourceIds.delete(layerId);
40
+ this.pruneIconResources();
41
+ }
42
+ reconcileLayerIconResources(layerId, resources) {
43
+ if (!this.layers.has(layerId)) return;
44
+ const ids = /* @__PURE__ */ new Set();
45
+ for (const resource of resources) {
46
+ ids.add(resource.id);
47
+ this.iconResources.set(resource.id, resource);
48
+ }
49
+ this.layerIconResourceIds.set(layerId, ids);
50
+ this.applyIconResources(resources);
51
+ this.pruneIconResources();
37
52
  }
38
53
  setLayerFeatures(layerId, features) {
39
54
  if (!this.layers.has(layerId)) return;
@@ -148,6 +163,9 @@ var BaseMapAdapter = class {
148
163
  this.layers.clear();
149
164
  this.featureCache.clear();
150
165
  this.featuresChangedListeners.clear();
166
+ for (const id of this.iconResources.keys()) this.removeIconResource(id);
167
+ this.iconResources.clear();
168
+ this.layerIconResourceIds.clear();
151
169
  this.destroyEngineResources();
152
170
  } finally {
153
171
  this.destroyed = true;
@@ -173,6 +191,22 @@ var BaseMapAdapter = class {
173
191
  };
174
192
  }
175
193
  destroyEngineResources() {}
194
+ applyIconResources(resources) {}
195
+ getIconResource(id) {
196
+ return this.iconResources.get(id);
197
+ }
198
+ removeIconResource(id) {}
199
+ pruneIconResources() {
200
+ const active = /* @__PURE__ */ new Set();
201
+ for (const ids of this.layerIconResourceIds.values()) for (const id of ids) active.add(id);
202
+ if (this.iconResources.size <= active.size + 32) return;
203
+ for (const id of this.iconResources.keys()) {
204
+ if (active.has(id)) continue;
205
+ this.iconResources.delete(id);
206
+ this.removeIconResource(id);
207
+ if (this.iconResources.size <= active.size + 32) break;
208
+ }
209
+ }
176
210
  /**
177
211
  * Default for engines/views that don't rotate (Leaflet). Rotating engines
178
212
  * (MapLibre, OpenLayers) override this.
@@ -206,6 +240,8 @@ function mapEventHitTolerance(originalEvent) {
206
240
  return direct;
207
241
  }
208
242
  function tryControlMeasureIdFromFeature(feature) {
243
+ const graphicId = feature.properties?.graphicId;
244
+ if (typeof graphicId === "string" && graphicId.length > 0) return graphicId;
209
245
  try {
210
246
  return controlMeasureIdFromFeature(feature);
211
247
  } catch {
@@ -335,6 +371,33 @@ function hitTestFeature(pixel, features, hitTolerance, toPixel) {
335
371
  }
336
372
  return null;
337
373
  }
374
+ /**
375
+ * Test a rendered icon-backed Point feature against its actual pixel footprint.
376
+ * The feature carries the adapter-neutral `iconScale` / `iconRotation` values
377
+ * emitted by TacticalDraw; `resource.anchor` is the placement anchor within the
378
+ * unscaled SVG viewport. A positive tolerance pads all four sides.
379
+ */
380
+ function hitTestIconFeature(pixel, feature, resource, hitTolerance, toPixel, bearing = 0) {
381
+ if (feature.geometry.type !== "Point") return false;
382
+ const anchor = toPixel(feature.geometry.coordinates);
383
+ if (!anchor) return false;
384
+ const rawScale = feature.properties?.iconScale;
385
+ const scale = typeof rawScale === "number" ? rawScale : 1;
386
+ if (!Number.isFinite(scale) || scale <= 0) return false;
387
+ const rawRotation = feature.properties?.iconRotation;
388
+ const angle = (typeof rawRotation === "number" && Number.isFinite(rawRotation) ? rawRotation : 0) - bearing;
389
+ const cos = Math.cos(angle);
390
+ const sin = Math.sin(angle);
391
+ const dx = pixel[0] - anchor[0];
392
+ const dy = pixel[1] - anchor[1];
393
+ const localX = dx * cos + dy * sin;
394
+ const localY = -dx * sin + dy * cos;
395
+ const minX = -resource.anchor[0] * scale - hitTolerance;
396
+ const maxX = (resource.width - resource.anchor[0]) * scale + hitTolerance;
397
+ const minY = -resource.anchor[1] * scale - hitTolerance;
398
+ const maxY = (resource.height - resource.anchor[1]) * scale + hitTolerance;
399
+ return localX >= minX && localX <= maxX && localY >= minY && localY <= maxY;
400
+ }
338
401
  //#endregion
339
402
  //#region src/gestures/hit-tolerance.ts
340
403
  const TOUCH_HIT_TOLERANCE_PX = 22;
@@ -388,6 +451,14 @@ var PointerCallbackHub = class {
388
451
  }
389
452
  };
390
453
  //#endregion
454
+ //#region src/point-symbol.ts
455
+ function isPointSymbol(graphic) {
456
+ return typeof graphic === "object" && graphic !== null && "kind" in graphic && graphic.kind === "point-symbol";
457
+ }
458
+ function clonePointSymbol(symbol) {
459
+ return structuredClone(symbol);
460
+ }
461
+ //#endregion
391
462
  //#region src/tactical-draw/active-interaction.ts
392
463
  /**
393
464
  * Owns the facade's public interaction state: the abort handle used by
@@ -1025,7 +1096,7 @@ function runFixedLengthDraw(options) {
1025
1096
  });
1026
1097
  const measure = sizeAnchor === "screen" ? captured : bakePixelSizeToGround(captured, adapter);
1027
1098
  return {
1028
- measure: cloneControlMeasure(measure),
1099
+ graphic: cloneControlMeasure(measure),
1029
1100
  render: renderMeasureForAdapter(adapter, measure, renderOpts)
1030
1101
  };
1031
1102
  }
@@ -1175,7 +1246,7 @@ function createSettleEmitter(listenerErrorsMessage) {
1175
1246
  * `EditSession.onCommit`, and `DrawSession.onCommit`: a listener set with
1176
1247
  * synchronous, exception-collecting emission (single rethrow, `AggregateError`
1177
1248
  * otherwise). Unlike `createSettleEmitter`, `emit` may be called more than
1178
- * once — a transform's `setMembers` can commit several removal batches before
1249
+ * once — a transform's `setGraphics` can commit several removal batches before
1179
1250
  * the close batch — and there is no latch or late-registration path:
1180
1251
  * registration-time `ctx.settled` guards stay in the controllers, mirroring
1181
1252
  * `onChange`.
@@ -1397,7 +1468,7 @@ function runVariableLengthDraw(options) {
1397
1468
  });
1398
1469
  const measure = sizeAnchor === "screen" ? built : bakePixelSizeToGround(built, adapter);
1399
1470
  return {
1400
- measure: cloneControlMeasure(measure),
1471
+ graphic: cloneControlMeasure(measure),
1401
1472
  render: renderMeasureForAdapter(adapter, measure, renderOpts)
1402
1473
  };
1403
1474
  }
@@ -2188,7 +2259,7 @@ const LABEL_HANDLE_OFFSET_PX = 14;
2188
2259
  * Corner/rotate drags commit even at zero movement (matches the pre-ADR-0017
2189
2260
  * translate/rotate seams).
2190
2261
  */
2191
- const CLICK_SLOP_PX$1 = 3;
2262
+ const CLICK_SLOP_PX$3 = 3;
2192
2263
  const ALLOWED_EDIT_MODES = new Set([
2193
2264
  "reshape",
2194
2265
  "transform",
@@ -2464,7 +2535,7 @@ function runEdit(opts) {
2464
2535
  else delete working.amplifierPlacements;
2465
2536
  const committed = sizeAnchor === "screen" ? working : bakePixelSizeToGround(working, adapter);
2466
2537
  return {
2467
- measure: cloneControlMeasure(committed),
2538
+ graphic: cloneControlMeasure(committed),
2468
2539
  render: renderMeasureForAdapter(adapter, committed, renderOpts)
2469
2540
  };
2470
2541
  }
@@ -2519,7 +2590,7 @@ function runEdit(opts) {
2519
2590
  let closeRequested = false;
2520
2591
  let abortRequested = false;
2521
2592
  const event = {
2522
- measure: currentRendered,
2593
+ graphic: currentRendered,
2523
2594
  previous: previousRendered,
2524
2595
  session,
2525
2596
  reject() {
@@ -2785,7 +2856,17 @@ function runEdit(opts) {
2785
2856
  applyModes(next);
2786
2857
  }
2787
2858
  const session = {
2788
- measure,
2859
+ graphic: measure,
2860
+ get workingGraphic() {
2861
+ return cloneControlMeasure({
2862
+ ...measure,
2863
+ controlPoints: workingPoints.map((point) => [...point]),
2864
+ options: { ...workingOptions ?? {} },
2865
+ style: currentStyle ? { ...currentStyle } : void 0,
2866
+ textAmplifiers: workingTextAmplifiers ? { ...workingTextAmplifiers } : void 0,
2867
+ amplifierPlacements: snapshotAmplifierPlacements()
2868
+ });
2869
+ },
2789
2870
  get controlPoints() {
2790
2871
  return workingPoints.map((p) => [...p]);
2791
2872
  },
@@ -2826,6 +2907,26 @@ function runEdit(opts) {
2826
2907
  }
2827
2908
  applyModes(normalised);
2828
2909
  },
2910
+ updateGraphic(updater) {
2911
+ if (ctx.settled) return;
2912
+ const previous = rebuildRendered();
2913
+ const previousNonGeometryDirty = nonGeometryDirty;
2914
+ const previousSizeAnchor = sizeAnchor;
2915
+ const current = session.workingGraphic;
2916
+ const next = updater(current);
2917
+ if (next.id !== measure.id || next.kind !== measure.kind) throw new TypeError("updateGraphic must preserve control-measure id and kind");
2918
+ renderMeasureForAdapter(adapter, next, renderOpts);
2919
+ workingPoints.splice(0, workingPoints.length, ...next.controlPoints.map((point) => [...point]));
2920
+ workingOptions = { ...next.options ?? {} };
2921
+ currentStyle = next.style ? { ...next.style } : void 0;
2922
+ workingTextAmplifiers = next.textAmplifiers ? { ...next.textAmplifiers } : void 0;
2923
+ workingAmplifierPlacements = next.amplifierPlacements ? structuredClone(next.amplifierPlacements) : void 0;
2924
+ nonGeometryDirty = true;
2925
+ renderPreview();
2926
+ renderHandlesLayer();
2927
+ recomputeDirty();
2928
+ emitChange(previous, previous.graphic.controlPoints.map((point) => [...point]), previous.graphic.options, previous.graphic.style, previousNonGeometryDirty, previousSizeAnchor, rebuildRendered(), previous.graphic.textAmplifiers, previous.graphic.amplifierPlacements);
2929
+ },
2829
2930
  setOptions(partial) {
2830
2931
  setOptions(partial);
2831
2932
  },
@@ -3189,7 +3290,7 @@ function runEdit(opts) {
3189
3290
  activeLabelRotateDrag = null;
3190
3291
  workingAmplifierPlacements = drag.previous;
3191
3292
  endDragBookkeeping();
3192
- if (Math.hypot(pixel[0] - drag.downPixel[0], pixel[1] - drag.downPixel[1]) > CLICK_SLOP_PX$1) {
3293
+ if (Math.hypot(pixel[0] - drag.downPixel[0], pixel[1] - drag.downPixel[1]) > CLICK_SLOP_PX$3) {
3193
3294
  const delta = pointerAngleAround(pixel, drag.anchorPixel) - drag.startPointerAngle;
3194
3295
  const rotation = drag.startRotation - delta;
3195
3296
  setAmplifierPlacements({ [drag.key]: { rotation } });
@@ -3206,7 +3307,7 @@ function runEdit(opts) {
3206
3307
  const position = labelAnchorFromPointer(pixel, grabOffset);
3207
3308
  workingAmplifierPlacements = previous;
3208
3309
  endDragBookkeeping();
3209
- if (position && Math.hypot(pixel[0] - downPixel[0], pixel[1] - downPixel[1]) > CLICK_SLOP_PX$1) setAmplifierPlacements({ [field]: position });
3310
+ if (position && Math.hypot(pixel[0] - downPixel[0], pixel[1] - downPixel[1]) > CLICK_SLOP_PX$3) setAmplifierPlacements({ [field]: position });
3210
3311
  else {
3211
3312
  selectedLabelKey = selectedLabelKey === field ? null : field;
3212
3313
  renderPreview();
@@ -3241,7 +3342,7 @@ function runEdit(opts) {
3241
3342
  activeBoxDrag = null;
3242
3343
  driver.setDragPanEnabled(true);
3243
3344
  const distance = Math.hypot(pixel[0] - downPixel[0], pixel[1] - downPixel[1]);
3244
- if (fsm.kind === "body" && distance <= CLICK_SLOP_PX$1) {
3345
+ if (fsm.kind === "body" && distance <= CLICK_SLOP_PX$3) {
3245
3346
  replaceWorkingPoints(preDragPoints);
3246
3347
  workingAmplifierPlacements = preDragAmplifierPlacements;
3247
3348
  renderPreview();
@@ -3414,7 +3515,7 @@ const HANDLE_HIT_TOLERANCE_PX = 12;
3414
3515
  * click, not a gesture — same rationale as `edit-controller.ts`'s constant of
3415
3516
  * the same name.
3416
3517
  */
3417
- const CLICK_SLOP_PX = 3;
3518
+ const CLICK_SLOP_PX$2 = 3;
3418
3519
  /**
3419
3520
  * Drive an interactive group transform against a `MapAdapter` — the leaner,
3420
3521
  * multi-select sibling of `runEdit` (ADR-0017 "Multi-select" slice). No
@@ -3430,7 +3531,7 @@ const CLICK_SLOP_PX = 3;
3430
3531
  * thin outline (its own fit at the same angle)
3431
3532
  * - dirty tracking, close/abort/signal/preempt/destroy, click-away — same
3432
3533
  * contracts as `runEdit`, scoped to the group
3433
- * - membership is mutable via `session.setMembers` (#187): members join/leave
3534
+ * - membership is mutable via `session.setGraphics` (#187): members join/leave
3434
3535
  * a live session without a session boundary
3435
3536
  */
3436
3537
  function runGroupTransform(opts) {
@@ -3454,7 +3555,7 @@ function runGroupTransform(opts) {
3454
3555
  let members = measures.map(createMemberState);
3455
3556
  /**
3456
3557
  * `true` once any member has been lifted off the graphics layer (initial or a
3457
- * mid-session `setMembers` add). Gates the abort-path restore so a session
3558
+ * mid-session `setGraphics` add). Gates the abort-path restore so a session
3458
3559
  * that never touched the graphics layer doesn't rewrite it on abort.
3459
3560
  */
3460
3561
  let anyLiftedFromGraphics = false;
@@ -3481,7 +3582,7 @@ function runGroupTransform(opts) {
3481
3582
  };
3482
3583
  const committed = sizeAnchor === "screen" ? working : bakePixelSizeToGround(working, adapter);
3483
3584
  return {
3484
- measure: cloneControlMeasure(committed),
3585
+ graphic: cloneControlMeasure(committed),
3485
3586
  render: renderMeasureForAdapter(adapter, committed, renderOpts)
3486
3587
  };
3487
3588
  }
@@ -3711,7 +3812,7 @@ function runGroupTransform(opts) {
3711
3812
  let closeRequested = false;
3712
3813
  let abortRequested = false;
3713
3814
  const event = {
3714
- measures: currentRendered,
3815
+ graphics: currentRendered,
3715
3816
  previous: previousRendered,
3716
3817
  session,
3717
3818
  reject() {
@@ -3808,7 +3909,7 @@ function runGroupTransform(opts) {
3808
3909
  /**
3809
3910
  * Cancel an in-flight box drag without committing it: restore the pre-drag
3810
3911
  * geometry and option overrides, re-enable drag pan, and clear the drag. Used
3811
- * by `setMembers` so a membership change never bakes a half-finished drag; the
3912
+ * by `setGraphics` so a membership change never bakes a half-finished drag; the
3812
3913
  * caller is responsible for the follow-up repaint (`renderAllPreviews` /
3813
3914
  * `renderHandlesLayer` / `recomputeDirty`), so this leaves the paint to it
3814
3915
  * rather than rendering the outgoing member set only to redo it immediately.
@@ -3823,11 +3924,11 @@ function runGroupTransform(opts) {
3823
3924
  applyOptionsOverrides(preDragOptionsOverrideByMember);
3824
3925
  dragInFlight = false;
3825
3926
  }
3826
- function setMembers(next) {
3927
+ function setGraphics(next) {
3827
3928
  if (ctx.settled) return void 0;
3828
3929
  const seen = /* @__PURE__ */ new Set();
3829
3930
  for (const m of next) {
3830
- if (seen.has(m.id)) throw new TypeError(`TransformSession.setMembers: duplicate control measure id "${m.id}"`);
3931
+ if (seen.has(m.id)) throw new TypeError(`TransformSession.setGraphics: duplicate control measure id "${m.id}"`);
3831
3932
  seen.add(m.id);
3832
3933
  }
3833
3934
  if (next.length === 0) return close();
@@ -3857,11 +3958,11 @@ function runGroupTransform(opts) {
3857
3958
  return commitClose();
3858
3959
  }
3859
3960
  const session = {
3860
- get measures() {
3961
+ get graphics() {
3861
3962
  return members.map((m) => m.measure);
3862
3963
  },
3863
- controlPointsFor(measureId) {
3864
- const member = members.find((m) => m.measure.id === measureId);
3964
+ controlPointsFor(graphicId) {
3965
+ const member = members.find((m) => m.measure.id === graphicId);
3865
3966
  if (!member) return void 0;
3866
3967
  return member.workingPoints.map((p) => [...p]);
3867
3968
  },
@@ -3874,7 +3975,7 @@ function runGroupTransform(opts) {
3874
3975
  setSizeAnchor(next) {
3875
3976
  setSizeAnchor(next);
3876
3977
  },
3877
- setMembers,
3978
+ setGraphics,
3878
3979
  close,
3879
3980
  abort() {
3880
3981
  if (ctx.settled) return;
@@ -3958,7 +4059,7 @@ function runGroupTransform(opts) {
3958
4059
  activeBoxDrag = null;
3959
4060
  driver.setDragPanEnabled(true);
3960
4061
  const distance = Math.hypot(pixel[0] - downPixel[0], pixel[1] - downPixel[1]);
3961
- if (fsm.kind === "body" && distance <= CLICK_SLOP_PX) {
4062
+ if (fsm.kind === "body" && distance <= CLICK_SLOP_PX$2) {
3962
4063
  applyConcatenatedPoints(preDragPoints);
3963
4064
  applyAmplifierPlacementsByMember(preDragAmplifierPlacementsByMember);
3964
4065
  renderAllPreviews();
@@ -4060,7 +4161,7 @@ function runGroupTransform(opts) {
4060
4161
  get settled() {
4061
4162
  return core.settled;
4062
4163
  },
4063
- get measureIds() {
4164
+ get graphicIds() {
4064
4165
  return members.map((m) => m.measure.id);
4065
4166
  },
4066
4167
  abort(reason, cause) {
@@ -4069,6 +4170,342 @@ function runGroupTransform(opts) {
4069
4170
  };
4070
4171
  }
4071
4172
  //#endregion
4173
+ //#region src/tactical-draw/point-symbol-rendering.ts
4174
+ function effectivePointSymbolPixelSize(symbol, resolution) {
4175
+ if (symbol.size.unit === "pixels") return symbol.size.value;
4176
+ return resolution !== void 0 && resolution > 0 ? symbol.size.value / resolution : symbol.size.value;
4177
+ }
4178
+ function pointSymbolPixelScale(symbol, render, resolution) {
4179
+ if (!Number.isFinite(render.octagonSize) || render.octagonSize <= 0) throw new TypeError("PointSymbolRender.octagonSize must be a positive finite number");
4180
+ return effectivePointSymbolPixelSize(symbol, resolution) / render.octagonSize;
4181
+ }
4182
+ /** Complete rotated logical footprint for selection and transform chrome. */
4183
+ function pointSymbolFootprint(symbol, render, adapter) {
4184
+ const anchor = adapter.getPixelFromCoordinate(symbol.position);
4185
+ if (!anchor) return null;
4186
+ const scale = pointSymbolPixelScale(symbol, render, adapter.getResolution());
4187
+ const angle = symbol.rotation - adapter.getBearing();
4188
+ const centerOffsetX = (render.width / 2 - render.anchor.x) * scale;
4189
+ const centerOffsetY = (render.height / 2 - render.anchor.y) * scale;
4190
+ const cos = Math.cos(angle);
4191
+ const sin = Math.sin(angle);
4192
+ const box = {
4193
+ center: [anchor[0] + centerOffsetX * cos - centerOffsetY * sin, anchor[1] + centerOffsetX * sin + centerOffsetY * cos],
4194
+ halfWidth: render.width * scale / 2,
4195
+ halfHeight: render.height * scale / 2,
4196
+ angle
4197
+ };
4198
+ return {
4199
+ box,
4200
+ corners: boxCorners(box)
4201
+ };
4202
+ }
4203
+ function renderPointSymbolForAdapter(adapter, capability, symbol) {
4204
+ const render = capability.render(symbol);
4205
+ return {
4206
+ render,
4207
+ resource: {
4208
+ id: render.resourceId,
4209
+ svg: render.svg,
4210
+ width: render.width,
4211
+ height: render.height,
4212
+ anchor: [render.anchor.x, render.anchor.y]
4213
+ },
4214
+ feature: pointSymbolFeature(symbol, render, adapter.getResolution())
4215
+ };
4216
+ }
4217
+ function pointSymbolFeature(symbol, render, resolution) {
4218
+ return {
4219
+ type: "Feature",
4220
+ id: `${symbol.id}:point-symbol`,
4221
+ geometry: {
4222
+ type: "Point",
4223
+ coordinates: [...symbol.position]
4224
+ },
4225
+ properties: {
4226
+ ...symbol.properties ?? {},
4227
+ graphicId: symbol.id,
4228
+ graphicKind: "point-symbol",
4229
+ iconId: render.resourceId,
4230
+ iconScale: pointSymbolPixelScale(symbol, render, resolution),
4231
+ iconRotation: symbol.rotation,
4232
+ iconOpacity: symbol.style?.opacity ?? 1
4233
+ }
4234
+ };
4235
+ }
4236
+ //#endregion
4237
+ //#region src/tactical-draw/rotation.ts
4238
+ /** Normalize radians to the canonical clockwise [0, 2π) range. */
4239
+ function normalizeRotation(angle) {
4240
+ const turn = Math.PI * 2;
4241
+ return (angle % turn + turn) % turn;
4242
+ }
4243
+ //#endregion
4244
+ //#region src/tactical-draw/graphic-transform-controller.ts
4245
+ const HIT_TOLERANCE_PX$1 = 12;
4246
+ const CLICK_SLOP_PX$1 = 3;
4247
+ function cloneGraphic(graphic) {
4248
+ return isPointSymbol(graphic) ? clonePointSymbol(graphic) : cloneControlMeasure(graphic);
4249
+ }
4250
+ function runGraphicTransform(opts) {
4251
+ const { adapter, capability, graphics, graphicsLayer, previewLayer, handlesLayer, graphicsStyle, signal, closeOnClickAway = true, interactionStyle, onSession, handleIdPrefix, restoreOriginalToGraphics, isOnGraphicsLayer, onSettled, onClickAwayGraphicsPick } = opts;
4252
+ const original = graphics.map(cloneGraphic);
4253
+ let working = original.map(cloneGraphic);
4254
+ let dirty = false;
4255
+ let boxAngle = 0;
4256
+ let box = null;
4257
+ let ctx;
4258
+ let driver;
4259
+ let drag = null;
4260
+ const listeners = /* @__PURE__ */ new Set();
4261
+ const commitEmitter = createCommitEmitter("GraphicTransformSession.onCommit listener errors");
4262
+ const settleEmitter = createSettleEmitter("GraphicTransformSession.onSettled listener errors");
4263
+ const renderOpts = graphicsStyle ? { graphicsStyle } : void 0;
4264
+ const snapshots = () => working.map((graphic) => isPointSymbol(graphic) ? {
4265
+ graphic: clonePointSymbol(graphic),
4266
+ render: capability.render(graphic)
4267
+ } : {
4268
+ graphic: cloneControlMeasure(graphic),
4269
+ render: renderMeasureForAdapter(adapter, graphic, renderOpts)
4270
+ });
4271
+ function renderAll() {
4272
+ if (ctx.settled) return;
4273
+ const features = [];
4274
+ const resources = /* @__PURE__ */ new Map();
4275
+ const pixels = [];
4276
+ for (const graphic of working) if (isPointSymbol(graphic)) {
4277
+ const plan = renderPointSymbolForAdapter(adapter, capability, graphic);
4278
+ resources.set(plan.resource.id, plan.resource);
4279
+ features.push(plan.feature);
4280
+ const footprint = pointSymbolFootprint(graphic, plan.render, adapter);
4281
+ if (footprint) pixels.push(...footprint.corners);
4282
+ } else {
4283
+ const rendered = renderMeasureForAdapter(adapter, graphic, renderOpts);
4284
+ features.push(...rendered.features);
4285
+ for (const feature of rendered.features) {
4286
+ const positions = [];
4287
+ if (feature.geometry) collectFeaturePositions(feature.geometry, positions);
4288
+ for (const position of positions) {
4289
+ const pixel = driver.toPixel(position);
4290
+ if (pixel) pixels.push(pixel);
4291
+ }
4292
+ }
4293
+ }
4294
+ adapter.reconcileLayerIconResources(previewLayer, [...resources.values()]);
4295
+ ctx.writeLayer(previewLayer, features);
4296
+ if (!drag) box = fitTransformBox(pixels, boxAngle, interactionStyle?.boxPadding);
4297
+ const liveBox = drag?.liveBox ?? box;
4298
+ const handles = [];
4299
+ if (liveBox) {
4300
+ handles.push(boxOutlineFeature(liveBox, (pixel) => driver.fromPixel(pixel), `${handleIdPrefix}:box-outline`, interactionStyle?.boxOutline));
4301
+ handles.push(...toFeatures(boxToHandles(liveBox, (pixel) => driver.fromPixel(pixel)), handleIdPrefix, interactionStyle));
4302
+ }
4303
+ ctx.writeLayer(handlesLayer, handles);
4304
+ }
4305
+ function anchorPoints(source) {
4306
+ const points = [];
4307
+ for (const graphic of source) if (isPointSymbol(graphic)) points.push([...graphic.position]);
4308
+ else points.push(...graphic.controlPoints.map((point) => [...point]));
4309
+ return points;
4310
+ }
4311
+ function applyPoints(source, points, scale, rotation) {
4312
+ let offset = 0;
4313
+ return source.map((graphic) => {
4314
+ if (isPointSymbol(graphic)) {
4315
+ const next = clonePointSymbol(graphic);
4316
+ next.position = [...points[offset++]];
4317
+ next.size.value = Math.max(.1, next.size.value * scale);
4318
+ next.rotation = normalizeRotation(next.rotation + rotation);
4319
+ return next;
4320
+ }
4321
+ const next = cloneControlMeasure(graphic);
4322
+ const count = next.controlPoints.length;
4323
+ next.controlPoints = points.slice(offset, offset + count).map((point) => [...point]);
4324
+ offset += count;
4325
+ return next;
4326
+ });
4327
+ }
4328
+ function emit(previous, previousGraphics) {
4329
+ const current = snapshots();
4330
+ let reject = false;
4331
+ let close = false;
4332
+ let abort = false;
4333
+ const event = {
4334
+ graphics: current,
4335
+ previous,
4336
+ session,
4337
+ reject: () => reject = true,
4338
+ close: () => close = true,
4339
+ abort: () => abort = true
4340
+ };
4341
+ const errors = [];
4342
+ for (const listener of [...listeners]) try {
4343
+ listener(event);
4344
+ } catch (error) {
4345
+ errors.push(error);
4346
+ }
4347
+ if (abort) ctx.fail(new TacticalDrawAbortError("session"));
4348
+ else if (close) ctx.commit(snapshots);
4349
+ else if (reject && !ctx.settled) {
4350
+ working = previousGraphics.map(cloneGraphic);
4351
+ dirty = JSON.stringify(working) !== JSON.stringify(original);
4352
+ renderAll();
4353
+ }
4354
+ if (errors.length === 1) throw errors[0];
4355
+ if (errors.length > 1) throw new AggregateError(errors, "GraphicTransformSession.onChange errors");
4356
+ }
4357
+ const session = {
4358
+ get graphics() {
4359
+ return original.map(cloneGraphic);
4360
+ },
4361
+ get workingGraphics() {
4362
+ return working.map(cloneGraphic);
4363
+ },
4364
+ get dirty() {
4365
+ return dirty;
4366
+ },
4367
+ close: () => ctx.commit(snapshots),
4368
+ abort: () => ctx.fail(new TacticalDrawAbortError("session")),
4369
+ onChange(listener) {
4370
+ if (ctx.settled) return () => {};
4371
+ listeners.add(listener);
4372
+ return () => listeners.delete(listener);
4373
+ },
4374
+ onCommit(listener) {
4375
+ if (ctx.settled) return () => {};
4376
+ return commitEmitter.register(listener);
4377
+ },
4378
+ onSettled: (listener) => settleEmitter.register(listener)
4379
+ };
4380
+ const onDown = (pixel, info) => {
4381
+ if (!box || ctx.settled) return;
4382
+ const hit = pickBoxHandle(box, pixel, info?.hitTolerance ?? HIT_TOLERANCE_PX$1);
4383
+ if (!hit) return;
4384
+ const start = working.map(cloneGraphic);
4385
+ drag = {
4386
+ fsm: beginBoxDrag({
4387
+ kind: hit.kind,
4388
+ downPixel: pixel,
4389
+ points: anchorPoints(start),
4390
+ projection: {
4391
+ toPixel: (p) => driver.toPixel(p),
4392
+ fromPixel: (p) => driver.fromPixel(p)
4393
+ },
4394
+ box,
4395
+ cornerIndex: hit.kind === "scale" ? hit.index : void 0
4396
+ }),
4397
+ start,
4398
+ startBox: box,
4399
+ liveBox: box,
4400
+ down: [...pixel]
4401
+ };
4402
+ driver.setDragPanEnabled(false);
4403
+ };
4404
+ const dragResult = (pixel) => {
4405
+ if (!drag) return null;
4406
+ const result = drag.fsm.move(pixel);
4407
+ const scale = drag.fsm.kind === "scale" ? Math.max(result.box.halfWidth / drag.startBox.halfWidth, result.box.halfHeight / drag.startBox.halfHeight) : 1;
4408
+ const rotation = drag.fsm.kind === "rotate" ? result.box.angle - drag.startBox.angle : 0;
4409
+ return {
4410
+ result,
4411
+ graphics: applyPoints(drag.start, result.points, scale, rotation)
4412
+ };
4413
+ };
4414
+ const onMove = (pixel) => {
4415
+ const active = drag;
4416
+ const next = dragResult(pixel);
4417
+ if (!active || !next) return;
4418
+ working = next.graphics;
4419
+ active.liveBox = next.result.box;
4420
+ renderAll();
4421
+ };
4422
+ const onUp = (pixel) => {
4423
+ if (!drag) return;
4424
+ const active = drag;
4425
+ const next = dragResult(pixel);
4426
+ drag = null;
4427
+ suppressClickAway = true;
4428
+ driver.setDragPanEnabled(true);
4429
+ working = active.start.map(cloneGraphic);
4430
+ if (!next) return;
4431
+ if (active.fsm.kind === "body" && Math.hypot(pixel[0] - active.down[0], pixel[1] - active.down[1]) <= CLICK_SLOP_PX$1) {
4432
+ renderAll();
4433
+ return;
4434
+ }
4435
+ const previous = snapshots();
4436
+ const previousGraphics = working.map(cloneGraphic);
4437
+ working = next.graphics;
4438
+ if (active.fsm.kind === "rotate") boxAngle += next.result.box.angle - active.startBox.angle;
4439
+ dirty = JSON.stringify(working) !== JSON.stringify(original);
4440
+ renderAll();
4441
+ emit(previous, previousGraphics);
4442
+ };
4443
+ let previewPick;
4444
+ let handlesPick;
4445
+ let graphicsPick;
4446
+ let suppressClickAway = false;
4447
+ const onClick = () => {
4448
+ const keepOpen = previewPick || handlesPick;
4449
+ const deferred = graphicsPick;
4450
+ previewPick = handlesPick = graphicsPick = void 0;
4451
+ if (suppressClickAway) {
4452
+ suppressClickAway = false;
4453
+ return;
4454
+ }
4455
+ if (!closeOnClickAway || keepOpen || drag) return;
4456
+ if (ctx.commit(snapshots) && deferred) onClickAwayGraphicsPick?.(deferred);
4457
+ };
4458
+ const core = runInteraction({
4459
+ adapter,
4460
+ signal,
4461
+ layers: [previewLayer, handlesLayer],
4462
+ onViewChange: renderAll,
4463
+ onSettled(settlement) {
4464
+ onSettled?.();
4465
+ settleFromInteraction(settlement, commitEmitter, settleEmitter);
4466
+ },
4467
+ attach(interaction) {
4468
+ ctx = interaction;
4469
+ driver = adapter.createEditPointerDriver();
4470
+ const unsubs = [
4471
+ driver.onPointerDown(onDown),
4472
+ driver.onPointerMove(onMove),
4473
+ driver.onPointerUp(onUp),
4474
+ adapter.onPick(previewLayer, (event) => previewPick = event),
4475
+ adapter.onPick(handlesLayer, (event) => handlesPick = event),
4476
+ adapter.onPick(graphicsLayer, (event) => graphicsPick = event)
4477
+ ];
4478
+ interaction.onTeardown(() => {
4479
+ unsubs.forEach((unsubscribe) => unsubscribe());
4480
+ adapter.off("click", onClick);
4481
+ driver.dispose();
4482
+ adapter.reconcileLayerIconResources(previewLayer, []);
4483
+ }, restoreOriginalToGraphics);
4484
+ for (const graphic of working) {
4485
+ if (!isOnGraphicsLayer(graphic.id)) continue;
4486
+ if (isPointSymbol(graphic)) adapter.removeFeatures(graphicsLayer, [`${graphic.id}:point-symbol`]);
4487
+ else {
4488
+ const ids = renderMeasureForAdapter(adapter, graphic, renderOpts).features.map((feature) => feature.id).filter((id) => typeof id === "string");
4489
+ adapter.removeFeatures(graphicsLayer, ids);
4490
+ }
4491
+ }
4492
+ adapter.on("click", onClick);
4493
+ renderAll();
4494
+ onSession?.(session);
4495
+ }
4496
+ });
4497
+ return {
4498
+ promise: core.promise,
4499
+ get settled() {
4500
+ return core.settled;
4501
+ },
4502
+ get graphicIds() {
4503
+ return working.map((graphic) => graphic.id);
4504
+ },
4505
+ abort: core.abort
4506
+ };
4507
+ }
4508
+ //#endregion
4072
4509
  //#region src/tactical-draw/errors.ts
4073
4510
  /**
4074
4511
  * Error thrown when any `TacticalDraw` method is called after `destroy()`.
@@ -4081,6 +4518,308 @@ var TacticalDrawDestroyedError = class TacticalDrawDestroyedError extends Error
4081
4518
  Object.setPrototypeOf(this, TacticalDrawDestroyedError.prototype);
4082
4519
  }
4083
4520
  };
4521
+ var PointSymbolCapabilityRequiredError = class extends Error {
4522
+ constructor() {
4523
+ super("TacticalDraw: point-symbol capability is not configured");
4524
+ this.name = "PointSymbolCapabilityRequiredError";
4525
+ }
4526
+ };
4527
+ //#endregion
4528
+ //#region src/tactical-draw/point-symbol-draw-controller.ts
4529
+ const PREVIEW_ID = "__td_point_symbol_preview__";
4530
+ function runPointSymbolDraw(options) {
4531
+ const { adapter, capability, draft, previewLayer, generateId, signal, onSettled } = options;
4532
+ let latestPosition = draft.position;
4533
+ let ctx;
4534
+ const savedCursor = "";
4535
+ const build = (id, position) => ({
4536
+ ...structuredClone(draft),
4537
+ id,
4538
+ kind: "point-symbol",
4539
+ sidc: draft.sidc,
4540
+ position: [...position],
4541
+ rotation: normalizeRotation(draft.rotation ?? capability.defaultRotation),
4542
+ size: structuredClone(draft.size ?? capability.defaultSize)
4543
+ });
4544
+ const renderPreview = () => {
4545
+ if (!latestPosition) return;
4546
+ const plan = renderPointSymbolForAdapter(adapter, capability, build(PREVIEW_ID, latestPosition));
4547
+ adapter.reconcileLayerIconResources(previewLayer, [plan.resource]);
4548
+ ctx.writeLayer(previewLayer, [plan.feature]);
4549
+ };
4550
+ const handlePointerMove = (event) => {
4551
+ latestPosition = event.coordinate;
4552
+ renderPreview();
4553
+ };
4554
+ const handleClick = (event) => {
4555
+ latestPosition = event.coordinate;
4556
+ ctx.commit(() => {
4557
+ const graphic = build(draft.id ?? generateId(), event.coordinate);
4558
+ return {
4559
+ graphic: clonePointSymbol(graphic),
4560
+ render: capability.render(graphic)
4561
+ };
4562
+ });
4563
+ };
4564
+ return runInteraction({
4565
+ adapter,
4566
+ signal,
4567
+ layers: [previewLayer],
4568
+ onViewChange: renderPreview,
4569
+ onSettled,
4570
+ attach(interaction) {
4571
+ ctx = interaction;
4572
+ interaction.onTeardown(() => {
4573
+ adapter.off("pointermove", handlePointerMove);
4574
+ adapter.off("click", handleClick);
4575
+ adapter.reconcileLayerIconResources(previewLayer, []);
4576
+ adapter.setCursor(savedCursor);
4577
+ });
4578
+ adapter.setCursor("crosshair");
4579
+ adapter.on("pointermove", handlePointerMove);
4580
+ adapter.on("click", handleClick);
4581
+ renderPreview();
4582
+ }
4583
+ });
4584
+ }
4585
+ //#endregion
4586
+ //#region src/tactical-draw/point-symbol-edit-controller.ts
4587
+ const HIT_TOLERANCE_PX = 12;
4588
+ const CLICK_SLOP_PX = 3;
4589
+ function normalisePointModes(input) {
4590
+ const modes = input ?? ["transform"];
4591
+ for (const mode of modes) if (mode !== "transform" && mode !== "delete") throw new TypeError(`Point-symbol editing does not support mode "${mode}"`);
4592
+ return modes.includes("delete") ? ["delete"] : [...new Set(modes)];
4593
+ }
4594
+ function symbolBox(adapter, capability, symbol, padding = 0) {
4595
+ const footprint = pointSymbolFootprint(symbol, capability.render(symbol), adapter);
4596
+ if (!footprint) return null;
4597
+ return {
4598
+ ...footprint.box,
4599
+ halfWidth: Math.max(1, footprint.box.halfWidth) + padding,
4600
+ halfHeight: Math.max(1, footprint.box.halfHeight) + padding
4601
+ };
4602
+ }
4603
+ function runPointSymbolEdit(opts) {
4604
+ const { adapter, capability, symbol, graphicsLayer, previewLayer, handlesLayer, signal, closeOnClickAway = true, interactionStyle, onSession, handleIdPrefix, restoreOriginalToGraphics, wasOnGraphicsLayer, onSettled, onClickAwayGraphicsPick } = opts;
4605
+ let modes = normalisePointModes(opts.modes);
4606
+ let working = clonePointSymbol(symbol);
4607
+ let dirty = false;
4608
+ let box = null;
4609
+ let drag = null;
4610
+ let ctx;
4611
+ let driver;
4612
+ const listeners = /* @__PURE__ */ new Set();
4613
+ const commitEmitter = createCommitEmitter("GraphicEditSession.onCommit listener errors");
4614
+ const settleEmitter = createSettleEmitter("GraphicEditSession.onSettled listener errors");
4615
+ const snapshot = () => ({
4616
+ graphic: clonePointSymbol(working),
4617
+ render: capability.render(working)
4618
+ });
4619
+ const render = () => {
4620
+ if (ctx.settled) return;
4621
+ const plan = renderPointSymbolForAdapter(adapter, capability, working);
4622
+ adapter.reconcileLayerIconResources(previewLayer, [plan.resource]);
4623
+ ctx.writeLayer(previewLayer, [plan.feature]);
4624
+ box = symbolBox(adapter, capability, working, interactionStyle?.boxPadding);
4625
+ const handles = [];
4626
+ if (modes.includes("transform") && box) {
4627
+ handles.push(boxOutlineFeature(box, (pixel) => driver.fromPixel(pixel), `${handleIdPrefix}:box-outline`, interactionStyle?.boxOutline));
4628
+ handles.push(...toFeatures(boxToHandles(box, (pixel) => driver.fromPixel(pixel)), handleIdPrefix, interactionStyle));
4629
+ }
4630
+ ctx.writeLayer(handlesLayer, handles);
4631
+ };
4632
+ const apply = (next, previous = snapshot()) => {
4633
+ working = clonePointSymbol(next);
4634
+ dirty = JSON.stringify(working) !== JSON.stringify(symbol);
4635
+ render();
4636
+ const current = snapshot();
4637
+ let reject = false;
4638
+ let close = false;
4639
+ let abort = false;
4640
+ const event = {
4641
+ graphic: current,
4642
+ previous,
4643
+ session,
4644
+ reject() {
4645
+ reject = true;
4646
+ },
4647
+ close() {
4648
+ close = true;
4649
+ },
4650
+ abort() {
4651
+ abort = true;
4652
+ }
4653
+ };
4654
+ const errors = [];
4655
+ for (const listener of [...listeners]) try {
4656
+ listener(event);
4657
+ } catch (error) {
4658
+ errors.push(error);
4659
+ }
4660
+ if (abort) ctx.fail(new TacticalDrawAbortError("session"));
4661
+ else if (close) ctx.commit(snapshot);
4662
+ else if (reject && !ctx.settled) {
4663
+ working = clonePointSymbol(previous.graphic);
4664
+ dirty = JSON.stringify(working) !== JSON.stringify(symbol);
4665
+ render();
4666
+ }
4667
+ if (errors.length === 1) throw errors[0];
4668
+ if (errors.length > 1) throw new AggregateError(errors, "GraphicEditSession.onChange errors");
4669
+ };
4670
+ const session = {
4671
+ graphic: clonePointSymbol(symbol),
4672
+ get workingGraphic() {
4673
+ return clonePointSymbol(working);
4674
+ },
4675
+ get dirty() {
4676
+ return dirty;
4677
+ },
4678
+ get modes() {
4679
+ return modes;
4680
+ },
4681
+ close() {
4682
+ return ctx.commit(snapshot);
4683
+ },
4684
+ abort() {
4685
+ ctx.fail(new TacticalDrawAbortError("session"));
4686
+ },
4687
+ setModes(next) {
4688
+ if (ctx.settled) return;
4689
+ modes = normalisePointModes(next);
4690
+ render();
4691
+ },
4692
+ updateGraphic(updater) {
4693
+ if (ctx.settled) return;
4694
+ const previous = snapshot();
4695
+ const next = updater(clonePointSymbol(working));
4696
+ if (next.kind !== "point-symbol" || next.id !== symbol.id) throw new TypeError("updateGraphic must preserve point-symbol kind and id");
4697
+ capability.render(next);
4698
+ apply(next, previous);
4699
+ },
4700
+ onChange(listener) {
4701
+ if (ctx.settled) return () => {};
4702
+ listeners.add(listener);
4703
+ return () => listeners.delete(listener);
4704
+ },
4705
+ onCommit(listener) {
4706
+ if (ctx.settled) return () => {};
4707
+ return commitEmitter.register(listener);
4708
+ },
4709
+ onSettled(listener) {
4710
+ return settleEmitter.register(listener);
4711
+ }
4712
+ };
4713
+ const pointerAngle = (pixel, anchor) => Math.atan2(pixel[1] - anchor[1], pixel[0] - anchor[0]);
4714
+ const onDown = (pixel, info) => {
4715
+ if (ctx.settled || !box || !modes.includes("transform")) return;
4716
+ const hit = pickBoxHandle(box, pixel, info?.hitTolerance ?? HIT_TOLERANCE_PX);
4717
+ if (!hit) return;
4718
+ const anchor = driver.toPixel(working.position);
4719
+ if (!anchor) return;
4720
+ drag = {
4721
+ kind: hit.kind,
4722
+ down: [...pixel],
4723
+ start: clonePointSymbol(working),
4724
+ anchor,
4725
+ startPointerAngle: pointerAngle(pixel, anchor),
4726
+ startDistance: Math.max(1, Math.hypot(pixel[0] - anchor[0], pixel[1] - anchor[1]))
4727
+ };
4728
+ driver.setDragPanEnabled(false);
4729
+ };
4730
+ const transformed = (pixel) => {
4731
+ if (!drag) return null;
4732
+ const next = clonePointSymbol(drag.start);
4733
+ if (drag.kind === "body") next.position = driver.fromPixel([drag.anchor[0] + pixel[0] - drag.down[0], drag.anchor[1] + pixel[1] - drag.down[1]]);
4734
+ else if (drag.kind === "rotate") next.rotation = normalizeRotation(drag.start.rotation + pointerAngle(pixel, drag.anchor) - drag.startPointerAngle);
4735
+ else {
4736
+ const distance = Math.hypot(pixel[0] - drag.anchor[0], pixel[1] - drag.anchor[1]);
4737
+ next.size.value = Math.max(.1, drag.start.size.value * (distance / drag.startDistance));
4738
+ }
4739
+ return next;
4740
+ };
4741
+ const onMove = (pixel) => {
4742
+ const next = transformed(pixel);
4743
+ if (!next) return;
4744
+ working = next;
4745
+ render();
4746
+ };
4747
+ const onUp = (pixel) => {
4748
+ if (!drag) return;
4749
+ const currentDrag = drag;
4750
+ const next = transformed(pixel);
4751
+ drag = null;
4752
+ suppressClickAway = true;
4753
+ driver.setDragPanEnabled(true);
4754
+ working = clonePointSymbol(currentDrag.start);
4755
+ if (!next) return;
4756
+ if (currentDrag.kind === "body" && Math.hypot(pixel[0] - currentDrag.down[0], pixel[1] - currentDrag.down[1]) <= CLICK_SLOP_PX) {
4757
+ render();
4758
+ return;
4759
+ }
4760
+ apply(next);
4761
+ };
4762
+ let previewPick;
4763
+ let handlesPick;
4764
+ let graphicsPick;
4765
+ let suppressClickAway = false;
4766
+ const onPreviewPick = (event) => previewPick = event;
4767
+ const onHandlesPick = (event) => handlesPick = event;
4768
+ const onGraphicsPick = (event) => graphicsPick = event;
4769
+ const onClick = () => {
4770
+ const keepOpen = previewPick || handlesPick;
4771
+ const deferred = graphicsPick;
4772
+ previewPick = handlesPick = graphicsPick = void 0;
4773
+ if (suppressClickAway) {
4774
+ suppressClickAway = false;
4775
+ return;
4776
+ }
4777
+ if (!closeOnClickAway || keepOpen || drag) return;
4778
+ if (ctx.commit(snapshot) && deferred) onClickAwayGraphicsPick?.(deferred);
4779
+ };
4780
+ const handle = runInteraction({
4781
+ adapter,
4782
+ signal,
4783
+ layers: [previewLayer, handlesLayer],
4784
+ onViewChange: render,
4785
+ onSettled(settlement) {
4786
+ onSettled?.();
4787
+ settleFromInteraction(settlement, commitEmitter, settleEmitter);
4788
+ },
4789
+ attach(interaction) {
4790
+ ctx = interaction;
4791
+ driver = adapter.createEditPointerDriver();
4792
+ const unsubs = [
4793
+ driver.onPointerDown(onDown),
4794
+ driver.onPointerMove(onMove),
4795
+ driver.onPointerUp(onUp),
4796
+ adapter.onPick(previewLayer, onPreviewPick),
4797
+ adapter.onPick(handlesLayer, onHandlesPick),
4798
+ adapter.onPick(graphicsLayer, onGraphicsPick)
4799
+ ];
4800
+ interaction.onTeardown(() => {
4801
+ unsubs.forEach((unsubscribe) => unsubscribe());
4802
+ adapter.off("click", onClick);
4803
+ driver.dispose();
4804
+ adapter.reconcileLayerIconResources(previewLayer, []);
4805
+ }, () => {
4806
+ if (wasOnGraphicsLayer) restoreOriginalToGraphics();
4807
+ });
4808
+ if (wasOnGraphicsLayer) adapter.removeFeatures(graphicsLayer, [`${symbol.id}:point-symbol`]);
4809
+ adapter.on("click", onClick);
4810
+ render();
4811
+ onSession?.(session);
4812
+ }
4813
+ });
4814
+ return {
4815
+ promise: handle.promise,
4816
+ get settled() {
4817
+ return handle.settled;
4818
+ },
4819
+ measureId: symbol.id,
4820
+ abort: handle.abort
4821
+ };
4822
+ }
4084
4823
  //#endregion
4085
4824
  //#region src/tactical-draw/tactical-draw.ts
4086
4825
  /**
@@ -4100,6 +4839,7 @@ var TacticalDraw = class {
4100
4839
  graphicsStyle;
4101
4840
  defaultInteractionStyle;
4102
4841
  generateId;
4842
+ pointSymbols;
4103
4843
  destroyed = false;
4104
4844
  hasRendered = false;
4105
4845
  activeInteraction = new ActiveInteractionTracker();
@@ -4114,7 +4854,7 @@ var TacticalDraw = class {
4114
4854
  * Tracked alongside `activeInteraction` whenever the interaction is a group
4115
4855
  * transform (`editMany`). The `render()` / view-change paths need every
4116
4856
  * member's id (to exclude all of them from reconcile) — mirrors `activeEdit`
4117
- * but for `TransformHandle`'s `measureIds` array.
4857
+ * but for `TransformHandle`'s `graphicIds` array.
4118
4858
  */
4119
4859
  activeTransform = null;
4120
4860
  /**
@@ -4125,7 +4865,7 @@ var TacticalDraw = class {
4125
4865
  */
4126
4866
  activeTransformSessionRef = null;
4127
4867
  /**
4128
- * Host pick handlers registered via `td.onPick` / `td.onMeasurePick`. The
4868
+ * Host pick handlers registered via `td.onPick` / `td.onGraphicPick`. The
4129
4869
  * façade subscribes once to `adapter.onPick` per layer and routes events
4130
4870
  * through this map so that close-then-pick ordering during an active edit can
4131
4871
  * be enforced (issue #52).
@@ -4155,6 +4895,7 @@ var TacticalDraw = class {
4155
4895
  this.graphicsStyle = resolveStyleHints(BUILT_IN_GRAPHICS_STYLE, options?.graphicsStyle, void 0);
4156
4896
  this.defaultInteractionStyle = options?.interactionStyle;
4157
4897
  this.generateId = options?.generateId ?? (() => `td-${++this.idCounter}`);
4898
+ this.pointSymbols = options?.pointSymbols;
4158
4899
  const owned = /* @__PURE__ */ new Set();
4159
4900
  const provided = options?.layers ?? {};
4160
4901
  this.graphicsLayer = resolveSlot(adapter, provided.graphics, owned);
@@ -4175,13 +4916,20 @@ var TacticalDraw = class {
4175
4916
  * `render()` (excludes the edited measure, owned by the preview layer) versus
4176
4917
  * `restoreOriginalToGraphics` (no exclusion, the edit is ending).
4177
4918
  */
4178
- renderMeasuresToGraphics(measures) {
4919
+ renderMeasuresToGraphics(graphics) {
4179
4920
  const features = [];
4921
+ const resources = /* @__PURE__ */ new Map();
4180
4922
  const renderOpts = { graphicsStyle: this.graphicsStyle };
4181
- for (const m of measures) {
4182
- const render = renderMeasureForAdapter(this.adapter, m, renderOpts);
4183
- for (const f of render.features) features.push(f);
4923
+ for (const graphic of graphics) if (isPointSymbol(graphic)) {
4924
+ if (!this.pointSymbols) throw new PointSymbolCapabilityRequiredError();
4925
+ const plan = renderPointSymbolForAdapter(this.adapter, this.pointSymbols, graphic);
4926
+ resources.set(plan.resource.id, plan.resource);
4927
+ features.push(plan.feature);
4928
+ } else {
4929
+ const render = renderMeasureForAdapter(this.adapter, graphic, renderOpts);
4930
+ for (const feature of render.features) features.push(feature);
4184
4931
  }
4932
+ this.adapter.reconcileLayerIconResources(this.graphicsLayer, [...resources.values()]);
4185
4933
  this.adapter.updateLayerFeatures(this.graphicsLayer, features);
4186
4934
  }
4187
4935
  /**
@@ -4197,7 +4945,7 @@ var TacticalDraw = class {
4197
4945
  if (this.destroyed || !this.hasRendered) return;
4198
4946
  const excluded = this.excludedInteractionIds();
4199
4947
  const visible = excluded.size > 0 ? this.lastRenderedMeasures.filter((m) => !excluded.has(m.id)) : this.lastRenderedMeasures;
4200
- if (!visible.some(measureRendersFromViewState)) return;
4948
+ if (!visible.some(graphicRendersFromViewState)) return;
4201
4949
  this.renderMeasuresToGraphics(visible);
4202
4950
  };
4203
4951
  /**
@@ -4220,8 +4968,10 @@ var TacticalDraw = class {
4220
4968
  * `render()` from inside `onSession` still sees live membership.
4221
4969
  */
4222
4970
  transformMemberIds() {
4223
- if (this.activeTransform) return this.activeTransform.measureIds;
4224
- return this.activeTransformSessionRef?.measures.map((m) => m.id);
4971
+ if (this.activeTransform) return this.activeTransform.graphicIds;
4972
+ const session = this.activeTransformSessionRef;
4973
+ if (!session) return void 0;
4974
+ return session.graphics.map((graphic) => graphic.id);
4225
4975
  }
4226
4976
  /**
4227
4977
  * Reconcile the graphics layer against the authoritative list of measures.
@@ -4252,14 +5002,15 @@ var TacticalDraw = class {
4252
5002
  this.assertAlive();
4253
5003
  const seen = /* @__PURE__ */ new Set();
4254
5004
  for (const m of measures) {
4255
- if (seen.has(m.id)) throw new Error(`TacticalDraw.render: duplicate control measure id "${m.id}"`);
5005
+ if (seen.has(m.id)) throw new Error(`TacticalDraw.render: duplicate graphic id "${m.id}"`);
4256
5006
  seen.add(m.id);
4257
5007
  }
4258
5008
  const editedId = this.activeEdit?.measureId;
4259
5009
  let externalEdited;
4260
5010
  let editedRemoved = false;
4261
5011
  if (editedId !== void 0) {
4262
- externalEdited = measures.find((m) => m.id === editedId);
5012
+ const candidate = measures.find((m) => m.id === editedId);
5013
+ externalEdited = candidate && !isPointSymbol(candidate) ? candidate : void 0;
4263
5014
  editedRemoved = externalEdited === void 0;
4264
5015
  }
4265
5016
  const transformIds = this.transformMemberIds();
@@ -4271,7 +5022,7 @@ var TacticalDraw = class {
4271
5022
  this.hasRendered = true;
4272
5023
  }
4273
5024
  this.lastRenderedMeasures = measures.slice();
4274
- if (externalEdited && this.activeEdit) this.activeEdit.notifyExternalMeasure(externalEdited);
5025
+ if (externalEdited && this.activeEdit && "notifyExternalMeasure" in this.activeEdit) this.activeEdit.notifyExternalMeasure(externalEdited);
4275
5026
  if (editedRemoved && this.activeEdit) {
4276
5027
  const editHandle = this.activeEdit;
4277
5028
  queueMicrotask(() => {
@@ -4285,7 +5036,7 @@ var TacticalDraw = class {
4285
5036
  if (!handle || handle.settled) return;
4286
5037
  if (this.activeTransformSessionRef !== sessionRef) return;
4287
5038
  const rendered = new Set(this.lastRenderedMeasures.map((m) => m.id));
4288
- if (handle.measureIds.some((id) => !rendered.has(id))) handle.abort("removed");
5039
+ if (handle.graphicIds.some((id) => !rendered.has(id))) handle.abort("removed");
4289
5040
  });
4290
5041
  }
4291
5042
  }
@@ -4379,22 +5130,25 @@ var TacticalDraw = class {
4379
5130
  hostOnSession?.(session);
4380
5131
  };
4381
5132
  }
4382
- /**
4383
- * Start an interactive draw. Fixed-length kinds commit automatically on the
4384
- * last required click. Variable-length kinds surface a `DrawSession` via
4385
- * `opts.onSession` and commit on `session.commit()`, `dblclick`, or a second
4386
- * click on the last added control point. All paths reject with
4387
- * `TacticalDrawAbortError` on signal / Escape / destroy / `session.abort()`.
4388
- *
4389
- * For a variable-length draw, `DrawSession.onCommit` is the single delivery
4390
- * channel for the committed snapshot and `DrawSession.onSettled` is the
4391
- * lifecycle signal for when the session is over and why (#197) — together
4392
- * they make the returned promise non-load-bearing for hosts that adopt them.
4393
- * Fixed-length kinds have no session and settle only through the promise.
4394
- */
4395
5133
  draw(draft, options) {
4396
5134
  this.assertAlive();
4397
- const kind = draft.kind;
5135
+ if (draft.kind === "point-symbol") {
5136
+ if (!this.pointSymbols) throw new PointSymbolCapabilityRequiredError();
5137
+ const preempt = this.guardAndPreempt();
5138
+ if (preempt) return preempt;
5139
+ const pointDraft = draft;
5140
+ return this.runWithSlot((onSettled) => runPointSymbolDraw({
5141
+ adapter: this.adapter,
5142
+ capability: this.pointSymbols,
5143
+ draft: pointDraft,
5144
+ previewLayer: this.previewLayer,
5145
+ generateId: this.generateId,
5146
+ signal: options?.signal,
5147
+ onSettled
5148
+ }), null);
5149
+ }
5150
+ const measureDraft = draft;
5151
+ const kind = measureDraft.kind;
4398
5152
  const preempt = this.guardAndPreempt();
4399
5153
  if (preempt) return preempt;
4400
5154
  const fixedLength = isFixedLength(getControlMeasureMetadata(kind));
@@ -4414,12 +5168,12 @@ var TacticalDraw = class {
4414
5168
  previewLayer: this.previewLayer,
4415
5169
  guideLayer: this.guideLayer,
4416
5170
  graphicsStyle: this.graphicsStyle,
4417
- generateId: () => draft.id ?? this.generateId(),
5171
+ generateId: () => measureDraft.id ?? this.generateId(),
4418
5172
  signal: options?.signal,
4419
- measureOptions: draft.options,
4420
- measureStyle: draft.style,
4421
- measureProperties: draft.properties,
4422
- measureTextAmplifiers: draft.textAmplifiers,
5173
+ measureOptions: measureDraft.options,
5174
+ measureStyle: measureDraft.style,
5175
+ measureProperties: measureDraft.properties,
5176
+ measureTextAmplifiers: measureDraft.textAmplifiers,
4423
5177
  guide: options?.guide,
4424
5178
  sizeAnchor: options?.sizeAnchor,
4425
5179
  onSettled
@@ -4429,36 +5183,53 @@ var TacticalDraw = class {
4429
5183
  previewLayer: this.previewLayer,
4430
5184
  guideLayer: this.guideLayer,
4431
5185
  graphicsStyle: this.graphicsStyle,
4432
- generateId: () => draft.id ?? this.generateId(),
5186
+ generateId: () => measureDraft.id ?? this.generateId(),
4433
5187
  signal: options?.signal,
4434
5188
  onSession: this.trackSession(options?.onSession),
4435
- seed: draft.controlPoints,
4436
- measureOptions: draft.options,
4437
- measureStyle: draft.style,
4438
- measureProperties: draft.properties,
4439
- measureTextAmplifiers: draft.textAmplifiers,
5189
+ seed: measureDraft.controlPoints,
5190
+ measureOptions: measureDraft.options,
5191
+ measureStyle: measureDraft.style,
5192
+ measureProperties: measureDraft.properties,
5193
+ measureTextAmplifiers: measureDraft.textAmplifiers,
4440
5194
  guide: options?.guide,
4441
5195
  sizeAnchor: options?.sizeAnchor,
4442
5196
  onSettled
4443
5197
  }), null, extraSettle);
4444
5198
  }
4445
- /**
4446
- * Start an interactive edit on a single control measure. Returns a Promise
4447
- * that resolves with a `ControlMeasureSnapshot` of the working state on
4448
- * `session.close()` and rejects with `TacticalDrawAbortError` on
4449
- * signal / Escape / destroy / preempt / `session.abort()`.
4450
- *
4451
- * Default click-away (`closeOnClickAway: true`) closes the edit on a click
4452
- * outside the preview / handle features. Hosts that own close policy pass
4453
- * `false`.
4454
- *
4455
- * `EditSession.onCommit` is the single delivery channel for the committed
4456
- * snapshot and `EditSession.onSettled` is the lifecycle signal for when the
4457
- * session is over and why (#197) — together they make the returned promise
4458
- * non-load-bearing for hosts that adopt them.
4459
- */
4460
5199
  edit(measure, options) {
4461
5200
  this.assertAlive();
5201
+ if (isPointSymbol(measure)) {
5202
+ normalisePointEditModes(options?.modes);
5203
+ if (!this.pointSymbols) throw new PointSymbolCapabilityRequiredError();
5204
+ const preempt = this.guardAndPreempt();
5205
+ if (preempt) return preempt;
5206
+ const wasOnGraphicsLayer = this.lastRenderedMeasures.some((m) => m.id === measure.id);
5207
+ const restoreOriginalToGraphics = () => this.renderMeasuresToGraphics(this.lastRenderedMeasures);
5208
+ const handleIdPrefix = `__td_edit_${++this.editCounter}__`;
5209
+ return this.runWithSlot((onSettled) => runPointSymbolEdit({
5210
+ adapter: this.adapter,
5211
+ capability: this.pointSymbols,
5212
+ symbol: measure,
5213
+ graphicsLayer: this.graphicsLayer,
5214
+ previewLayer: this.previewLayer,
5215
+ handlesLayer: this.handlesLayer,
5216
+ signal: options?.signal,
5217
+ closeOnClickAway: options?.closeOnClickAway,
5218
+ modes: options?.modes,
5219
+ interactionStyle: this.resolveInteractionStyle(options?.interactionStyle),
5220
+ onSession: options?.onSession,
5221
+ handleIdPrefix,
5222
+ restoreOriginalToGraphics,
5223
+ wasOnGraphicsLayer,
5224
+ onSettled,
5225
+ onClickAwayGraphicsPick: (event) => this.dispatchHostPick(this.graphicsLayer, event)
5226
+ }), (handle) => {
5227
+ this.activeEdit = handle;
5228
+ return () => {
5229
+ if (this.activeEdit === handle) this.activeEdit = null;
5230
+ };
5231
+ });
5232
+ }
4462
5233
  normaliseModes(options?.modes);
4463
5234
  const preempt = this.guardAndPreempt();
4464
5235
  if (preempt) return preempt;
@@ -4499,38 +5270,16 @@ var TacticalDraw = class {
4499
5270
  };
4500
5271
  }, revertGuideStyle);
4501
5272
  }
4502
- /**
4503
- * Start an interactive group transform (multi-select) over `measures`
4504
- * (ADR-0017 "Multi-select" slice). Every member is lifted onto the shared
4505
- * preview layer; the box (corner scale, rotate grip, drag-anywhere body
4506
- * translate) applies rigidly to all of them at once. Returns a Promise that
4507
- * resolves with `ControlMeasureSnapshot[]` on `session.close()` and rejects
4508
- * with `TacticalDrawAbortError` on signal / Escape / destroy / preempt /
4509
- * `session.abort()`. Key the resolved snapshots by `snapshot.measure.id`:
4510
- * the member set is mutable (`session.setMembers`, #187) so snapshot arrays
4511
- * are id-keyed, not index-aligned with this input. The promise resolves with
4512
- * the members in the set at close time; a member removed mid-session had its
4513
- * exit snapshot returned from the `setMembers` call that removed it.
4514
- *
4515
- * `measures.length` must be >= 1; a single-element array behaves like an
4516
- * `edit` with only `transform` mode. Throws `TypeError` synchronously (before
4517
- * any preemption) for an empty array or duplicate ids.
4518
- *
4519
- * For a selection that changes over time (shift-click toggle, marquee
4520
- * replace), prefer [[syncTransformMembers]] over hand-rolling the
4521
- * live-session-vs-fresh-`editMany` branch, and subscribe to
4522
- * `TransformSession.onCommit` as the single fold path for exit snapshots
4523
- * and `TransformSession.onSettled` as the lifecycle signal for when the
4524
- * session is over and why.
4525
- */
4526
5273
  editMany(measures, options) {
4527
5274
  this.assertAlive();
4528
5275
  if (measures.length === 0) throw new TypeError("td.editMany: measures must not be empty");
4529
5276
  const seenIds = /* @__PURE__ */ new Set();
4530
5277
  for (const m of measures) {
4531
- if (seenIds.has(m.id)) throw new TypeError(`td.editMany: duplicate control measure id "${m.id}"`);
5278
+ if (seenIds.has(m.id)) throw new TypeError(`td.editMany: duplicate graphic id "${m.id}"`);
4532
5279
  seenIds.add(m.id);
4533
5280
  }
5281
+ const hasPointSymbols = measures.some(isPointSymbol);
5282
+ if (hasPointSymbols && !this.pointSymbols) throw new PointSymbolCapabilityRequiredError();
4534
5283
  const preempt = this.guardAndPreempt();
4535
5284
  if (preempt) return preempt;
4536
5285
  const isOnGraphicsLayer = (id) => this.lastRenderedMeasures.some((m) => m.id === id);
@@ -4545,6 +5294,29 @@ var TacticalDraw = class {
4545
5294
  const extraSettle = () => {
4546
5295
  this.activeTransformSessionRef = null;
4547
5296
  };
5297
+ if (hasPointSymbols) return this.runWithSlot((onSettled) => runGraphicTransform({
5298
+ adapter: this.adapter,
5299
+ capability: this.pointSymbols,
5300
+ graphics: measures,
5301
+ graphicsLayer: this.graphicsLayer,
5302
+ previewLayer: this.previewLayer,
5303
+ handlesLayer: this.handlesLayer,
5304
+ graphicsStyle: this.graphicsStyle,
5305
+ signal: options?.signal,
5306
+ closeOnClickAway: options?.closeOnClickAway,
5307
+ interactionStyle: mergedInteractionStyle,
5308
+ onSession: this.trackTransformSession(options?.onSession),
5309
+ handleIdPrefix,
5310
+ restoreOriginalToGraphics,
5311
+ isOnGraphicsLayer,
5312
+ onSettled,
5313
+ onClickAwayGraphicsPick
5314
+ }), (handle) => {
5315
+ this.activeTransform = handle;
5316
+ return () => {
5317
+ if (this.activeTransform === handle) this.activeTransform = null;
5318
+ };
5319
+ }, extraSettle);
4548
5320
  return this.runWithSlot((onSettled) => runGroupTransform({
4549
5321
  adapter: this.adapter,
4550
5322
  measures,
@@ -4570,24 +5342,26 @@ var TacticalDraw = class {
4570
5342
  }, extraSettle);
4571
5343
  }
4572
5344
  /**
4573
- * One-call selection-sync: mutate the live group transform's member set in
4574
- * place, or start a fresh `editMany` when no transform is live (#191). This
4575
- * owns the "live session → `setMembers`, else → `editMany`" branch that every
4576
- * host otherwise hand-rolls against `activeTransformSession`.
5345
+ * One-call selection-sync: mutate a live control-measure-only transform's
5346
+ * member set in place, or start a fresh `editMany` when no compatible
5347
+ * transform is live (#191). A selection containing a point symbol uses the
5348
+ * unified graphic transform session, which does not yet support mutable
5349
+ * membership; changing that selection preempts the current session and starts
5350
+ * a new one, resetting its box angle and other session-local state.
4577
5351
  *
4578
5352
  * The facade knows session liveness synchronously — `activeTransformSessionRef`
4579
5353
  * is cleared inside the settle path, before control returns to host code — so
4580
- * unlike a host's own `activeTransformSession ? setMembers : editMany` check,
5354
+ * unlike a host's own `activeTransformSession ? setGraphics : editMany` check,
4581
5355
  * this can never observe a stale ref pointing at an already-settled session.
4582
5356
  * The settled-race fall-through hosts had to hand-roll for that gap is owned
4583
- * here instead: a `setMembers` call that (implausibly) still returns
5357
+ * here instead: a `setGraphics` call that (implausibly) still returns
4584
5358
  * `undefined` falls through to a fresh `editMany` rather than silently
4585
5359
  * dropping the selection.
4586
5360
  *
4587
- * `measures: []` closes any live session (committing every member) and is a
5361
+ * `graphics: []` closes any live session (committing every member) and is a
4588
5362
  * no-op while idle. With a live session, `options` is ignored — session-scoped
4589
5363
  * state (`sizeAnchor`, box angle, close-on-click-away policy) is sticky per
4590
- * `setMembers`'s contract; `options` applies only when this call starts a
5364
+ * `setGraphics`'s contract; `options` applies only when this call starts a
4591
5365
  * fresh `editMany`. Exit snapshots are delivered via the session's
4592
5366
  * `onCommit` — subscribe once in `options.onSession`. Session lifecycle —
4593
5367
  * including an Escape / preempt / destroy abort the host did not itself
@@ -4599,14 +5373,17 @@ var TacticalDraw = class {
4599
5373
  * either path. Starting a fresh transform preempts any other in-flight
4600
5374
  * draw/edit exactly like `editMany`.
4601
5375
  */
4602
- syncTransformMembers(measures, options) {
5376
+ syncTransformGraphics(graphics, options) {
4603
5377
  this.assertAlive();
4604
5378
  const session = this.activeTransformSessionRef;
4605
- if (session) {
4606
- if (session.setMembers(measures) !== void 0) return;
5379
+ if (session && "setGraphics" in session && graphics.every((graphic) => !isPointSymbol(graphic))) {
5380
+ if (session.setGraphics(graphics) !== void 0) return;
4607
5381
  }
4608
- if (measures.length === 0) return;
4609
- this.editMany([...measures], options).catch(ignoreAbort);
5382
+ if (graphics.length === 0) {
5383
+ session?.close();
5384
+ return;
5385
+ }
5386
+ this.editMany([...graphics], options).catch(ignoreAbort);
4610
5387
  }
4611
5388
  /**
4612
5389
  * Subscribe to picks on committed control measures.
@@ -4619,7 +5396,7 @@ var TacticalDraw = class {
4619
5396
  * Returns an idempotent unsubscribe function. An optional `signal` removes
4620
5397
  * the handler when aborted.
4621
5398
  */
4622
- onMeasurePick(handler, opts) {
5399
+ onGraphicPick(handler, opts) {
4623
5400
  this.assertAlive();
4624
5401
  return this.onPick(this.graphicsLayer, handler, opts);
4625
5402
  }
@@ -4873,6 +5650,12 @@ var TacticalDraw = class {
4873
5650
  if (this.destroyed) throw new TacticalDrawDestroyedError();
4874
5651
  }
4875
5652
  };
5653
+ function graphicRendersFromViewState(graphic) {
5654
+ return isPointSymbol(graphic) ? graphic.size.unit === "meters" : measureRendersFromViewState(graphic);
5655
+ }
5656
+ function normalisePointEditModes(modes) {
5657
+ for (const mode of modes ?? ["transform"]) if (mode !== "transform" && mode !== "delete") throw new TypeError(`Point-symbol editing does not support mode "${mode}"`);
5658
+ }
4876
5659
  /**
4877
5660
  * Built-in default graphics style. Ensures OL / MapLibre / Leaflet render
4878
5661
  * identically when neither host nor measure supplies a style — without it,
@@ -4997,4 +5780,4 @@ function nearestWorldCopyPosition(position, refLng) {
4997
5780
  return [nearestWorldCopyLng(position[0], refLng), ...position.slice(1)];
4998
5781
  }
4999
5782
  //#endregion
5000
- export { BaseMapAdapter, DEFAULT_LABEL_SIZE_PIXELS, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, PointerCallbackHub, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, TacticalDrawDestroyedError, bindAbortable, coerceHandleStyle, combineWithHostSignal, getSizeAnchor, hitTestFeature, ignoreAbort, isTacticalDrawAbortError, nearestWorldCopyLng, nearestWorldCopyPosition, pickHitTolerance, resolveGroundTextSizePixels, tryControlMeasureIdFromFeature };
5783
+ 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 };