@orbat-mapper/tactical-draw 0.2.0-alpha.13 → 0.2.0-alpha.14

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
@@ -57,9 +57,12 @@ interface StyleOptions {
57
57
  /**
58
58
  * Text (label) style for a layer. When `labelProperty` is set, adapters also
59
59
  * read per-feature label properties: `rotation` (radians, rendered as
60
- * `π − rotation`), `textSizePixels`/`textSizeZoom`/`textSizeResolution`
61
- * (ADR-0020), and `textAnchor` (`"start"` = text begins at the label point,
62
- * `"end"` = text ends there; absent = centered).
60
+ * `π − rotation`), the size anchor's `textSizeMeters` (ground) or
61
+ * `textSizePixels` (screen) (ADR-0028), and `textAnchor` (`"start"` = text
62
+ * begins at the label point, `"end"` = text ends there; absent = centered).
63
+ * A ground-anchored label renders at
64
+ * `clamp(textSizeMeters / resolution, textSizeMinPixels, textSizeMaxPixels)`;
65
+ * a screen-anchored one at a constant `textSizePixels`, unclamped.
63
66
  */
64
67
  interface TextStyleOptions {
65
68
  text?: string;
@@ -69,6 +72,18 @@ interface TextStyleOptions {
69
72
  strokeColor?: string;
70
73
  strokeWidth?: number;
71
74
  rotation?: number;
75
+ /**
76
+ * Lower edge of the ground-anchored label clamp band (ADR-0028): text never
77
+ * renders below this pixel size, however far the view zooms out.
78
+ * @default 8
79
+ */
80
+ textSizeMinPixels?: number;
81
+ /**
82
+ * Upper edge of the ground-anchored label clamp band (ADR-0028): text never
83
+ * renders above this pixel size, however far the view zooms in.
84
+ * @default 24
85
+ */
86
+ textSizeMaxPixels?: number;
72
87
  }
73
88
  /**
74
89
  * Per-feature style for edit handles (vertex / midpoint / translate / rotate).
@@ -333,6 +348,20 @@ declare function bindAbortable(signal: AbortSignal | undefined, attach: () => ()
333
348
  * when missing or shaped wrong so callers fall through to the layer style.
334
349
  */
335
350
  declare function coerceHandleStyle(raw: unknown): HandleStyle | undefined;
351
+ /** Default lower edge of the ground-anchored label clamp band (ADR-0028). */
352
+ declare const DEFAULT_TEXT_SIZE_MIN_PIXELS = 8;
353
+ /** Default upper edge of the ground-anchored label clamp band (ADR-0028). */
354
+ declare const DEFAULT_TEXT_SIZE_MAX_PIXELS = 24;
355
+ /**
356
+ * Rendered pixel size of a ground-anchored label (ADR-0028):
357
+ * `textSizeMeters / resolution`, clamped to the layer's text-style band.
358
+ * Within the band the text scales with its graphic; past either edge it pins
359
+ * to the bound, so zooming out never yields unreadable smudges and zooming in
360
+ * never yields screen-covering text. Shared by the adapters that resolve text
361
+ * size in code (OpenLayers, Leaflet); the MapLibre adapter evaluates the same
362
+ * clamp inside its per-zoom-stop style expression.
363
+ */
364
+ declare function resolveGroundTextSizePixels(textSizeMeters: number, resolution: number, text?: Pick<TextStyleOptions, "textSizeMinPixels" | "textSizeMaxPixels">): number;
336
365
  //#endregion
337
366
  //#region src/gestures/hit-test.d.ts
338
367
  type ToPixel = (lonLat: Position) => PixelCoordinate | null;
@@ -1487,4 +1516,4 @@ declare function nearestWorldCopyLng(lng: number, refLng: number): number;
1487
1516
  /** `nearestWorldCopyLng` applied to a lon/lat position. */
1488
1517
  declare function nearestWorldCopyPosition(position: Position, refLng: number): Position;
1489
1518
  //#endregion
1490
- export { BaseMapAdapter, type CombinedAbort, type DrawMeasureDraft, type DrawOptions, type DrawSession, type EditChangeEvent, type EditManyOptions, type EditMode, type EditOptions, type EditPointerDriver, type EditSession, type FeatureOps, type GuideStyle, type HandleStyle, type InteractionStyle, type LayerId, type MapAdapter, type MapCursor, type MapEvent, type MapEventHandler, type NativeEventEntry, type PickEvent, type PickHandler, type PickOptions, type PixelCoordinate, type PointerCallback, PointerCallbackHub, type PointerEventInfo, type SettleReason, type SizeAnchor, type StyleOptions, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, type TacticalDrawAbortReason, TacticalDrawDestroyedError, type TacticalDrawLayerOptions, type TacticalDrawOptions, type TextStyleOptions, type ToPixel, type TransformBox, type TransformChangeEvent, type TransformSession, bindAbortable, coerceHandleStyle, combineWithHostSignal, getSizeAnchor, hitTestFeature, ignoreAbort, isTacticalDrawAbortError, nearestWorldCopyLng, nearestWorldCopyPosition, pickHitTolerance, tryControlMeasureIdFromFeature };
1519
+ export { BaseMapAdapter, type CombinedAbort, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, type DrawMeasureDraft, type DrawOptions, type DrawSession, type EditChangeEvent, type EditManyOptions, type EditMode, type EditOptions, type EditPointerDriver, type EditSession, type FeatureOps, type GuideStyle, type HandleStyle, type InteractionStyle, type LayerId, type MapAdapter, type MapCursor, type MapEvent, type MapEventHandler, type NativeEventEntry, type PickEvent, type PickHandler, type PickOptions, type PixelCoordinate, type PointerCallback, PointerCallbackHub, type PointerEventInfo, type SettleReason, type SizeAnchor, type StyleOptions, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, type TacticalDrawAbortReason, TacticalDrawDestroyedError, type TacticalDrawLayerOptions, type TacticalDrawOptions, type TextStyleOptions, type ToPixel, type TransformBox, type TransformChangeEvent, type TransformSession, bindAbortable, coerceHandleStyle, combineWithHostSignal, getSizeAnchor, hitTestFeature, ignoreAbort, isTacticalDrawAbortError, nearestWorldCopyLng, nearestWorldCopyPosition, pickHitTolerance, resolveGroundTextSizePixels, tryControlMeasureIdFromFeature };
package/dist/index.mjs CHANGED
@@ -232,6 +232,24 @@ function coerceHandleStyle(raw) {
232
232
  if (!raw || typeof raw !== "object") return void 0;
233
233
  return raw;
234
234
  }
235
+ /** Default lower edge of the ground-anchored label clamp band (ADR-0028). */
236
+ const DEFAULT_TEXT_SIZE_MIN_PIXELS = 8;
237
+ /** Default upper edge of the ground-anchored label clamp band (ADR-0028). */
238
+ const DEFAULT_TEXT_SIZE_MAX_PIXELS = 24;
239
+ /**
240
+ * Rendered pixel size of a ground-anchored label (ADR-0028):
241
+ * `textSizeMeters / resolution`, clamped to the layer's text-style band.
242
+ * Within the band the text scales with its graphic; past either edge it pins
243
+ * to the bound, so zooming out never yields unreadable smudges and zooming in
244
+ * never yields screen-covering text. Shared by the adapters that resolve text
245
+ * size in code (OpenLayers, Leaflet); the MapLibre adapter evaluates the same
246
+ * clamp inside its per-zoom-stop style expression.
247
+ */
248
+ function resolveGroundTextSizePixels(textSizeMeters, resolution, text) {
249
+ const min = text?.textSizeMinPixels ?? 8;
250
+ const max = text?.textSizeMaxPixels ?? 24;
251
+ return Math.min(max, Math.max(min, textSizeMeters / resolution));
252
+ }
235
253
  //#endregion
236
254
  //#region src/gestures/hit-test.ts
237
255
  /** Squared distance from `pixel` to the segment p1→p2 in pixel space, or null if either endpoint fails to project or the segment has zero length. */
@@ -504,11 +522,14 @@ function combineWithHostSignal(hostSignal) {
504
522
  };
505
523
  }
506
524
  /**
507
- * Per-kind pixel-denominated size option and the meter option it bakes into —
508
- * the single source of which option makes a measure screen-anchored. Both the
509
- * resolution gate ([[measureUsesAdapterResolution]]) and the bake
510
- * ([[bakePixelSizeToGround]]) derive from this table, so a new pixel-sized kind
511
- * is one entry here (ADR-0020).
525
+ * Per-kind pixel-denominated *geometry* size option and the meter option it
526
+ * bakes into — the single source of which geometry option makes a measure
527
+ * screen-anchored. Label sizing is not listed here: every `capturesLabelSize`
528
+ * kind carries the shared `labelSizePixels` → `labelSize` pair implicitly
529
+ * (ADR-0028) — see [[sizeOptionSpecs]]. Both the resolution gate
530
+ * ([[measureUsesAdapterResolution]]) and the bake ([[bakePixelSizeToGround]])
531
+ * derive from this table, so a new pixel-sized kind is one entry here
532
+ * (ADR-0020).
512
533
  */
513
534
  const PIXEL_SIZE_OPTIONS = {
514
535
  boundary: {
@@ -523,6 +544,14 @@ const PIXEL_SIZE_OPTIONS = {
523
544
  pixels: "echelonSizePixels",
524
545
  meters: "echelonSize"
525
546
  },
547
+ "base-camp": {
548
+ pixels: "echelonSizePixels",
549
+ meters: "echelonSize"
550
+ },
551
+ "guerrilla-base": {
552
+ pixels: "echelonSizePixels",
553
+ meters: "echelonSize"
554
+ },
526
555
  flot: {
527
556
  pixels: "radiusPixels",
528
557
  meters: "radius"
@@ -548,34 +577,52 @@ const PIXEL_SIZE_OPTIONS = {
548
577
  meters: "toothHeight"
549
578
  }
550
579
  };
580
+ /** The shared label-size pair every `capturesLabelSize` kind carries (ADR-0028). */
581
+ const LABEL_SIZE_SPEC = {
582
+ pixels: "labelSizePixels",
583
+ meters: "labelSize"
584
+ };
585
+ /**
586
+ * Every pixel/meter size-option pair `kind` can carry: its geometry entry from
587
+ * [[PIXEL_SIZE_OPTIONS]] (if any) plus the shared label pair when the kind
588
+ * captures a label size — a kind like Boundary can carry both anchor-bearing
589
+ * dimensions at once (echelon glyph + labels).
590
+ */
591
+ function sizeOptionSpecs(kind) {
592
+ const specs = [];
593
+ const geometry = PIXEL_SIZE_OPTIONS[kind];
594
+ if (geometry) specs.push(geometry);
595
+ if (getControlMeasureMetadata(kind).capturesLabelSize) specs.push(LABEL_SIZE_SPEC);
596
+ return specs;
597
+ }
551
598
  /**
552
599
  * Whether `measure` carries a pixel-denominated size option whose resolution
553
600
  * to meters depends on the live map (`metersPerPixel`) — i.e. it is
554
601
  * *screen-anchored*. Used both to inject the override
555
602
  * ([[withAdapterMeasureOptions]]) and to let zoom-driven re-renders skip
556
- * measures whose geometry is resolution-independent. Accepts the minimal
603
+ * measures whose render is resolution-independent. Accepts the minimal
557
604
  * `{ kind, options }` shape so in-flight draw/edit handlers can gate against
558
605
  * working options without materialising a full measure.
559
606
  */
560
607
  function measureUsesAdapterResolution(measure) {
561
- const spec = PIXEL_SIZE_OPTIONS[measure.kind];
562
- if (!spec) return false;
563
- return measure.options?.[spec.pixels] !== void 0;
608
+ const options = measure.options;
609
+ return sizeOptionSpecs(measure.kind).some((spec) => options?.[spec.pixels] !== void 0);
564
610
  }
565
611
  /**
566
612
  * Whether rendering `measure` through [[renderMeasureForAdapter]] reads live
567
613
  * adapter view state — either a pixel-denominated size
568
614
  * ([[measureUsesAdapterResolution]]) or draw-time label sizing that
569
- * [[withCommittedAdapterMeasureOptions]] would still capture (a
570
- * `capturesLabelSize` kind not yet carrying `labelSizePixels`). The
571
- * view-change rerender gates key off this so such measures rescale live;
572
- * committed label-sized measures already carry `labelSizePixels`, stay
573
- * zoom-independent, and keep skipping the rebuild.
615
+ * [[withCommittedAdapterMeasureOptions]] would still stamp (a
616
+ * `capturesLabelSize` kind not yet carrying any label size). The view-change
617
+ * rerender gates key off this so such measures rescale live; committed
618
+ * ground-anchored measures carry `labelSize` meters, stay zoom-independent,
619
+ * and keep skipping the rebuild.
574
620
  */
575
621
  function measureRendersFromViewState(measure) {
576
622
  if (measureUsesAdapterResolution(measure)) return true;
577
623
  if (!getControlMeasureMetadata(measure.kind).capturesLabelSize) return false;
578
- return typeof measure.options?.labelSizePixels !== "number";
624
+ const options = measure.options;
625
+ return typeof options?.labelSizePixels !== "number" && typeof options?.labelSize !== "number";
579
626
  }
580
627
  /**
581
628
  * The [[SizeAnchor]] a measure currently encodes (ADR-0020): `screen` when it
@@ -597,16 +644,15 @@ function getSizeAnchor(measure) {
597
644
  * carries no pixel size or the adapter has no usable resolution.
598
645
  */
599
646
  function bakePixelSizeToGround(measure, adapter) {
600
- const spec = PIXEL_SIZE_OPTIONS[measure.kind];
601
- if (!spec) return measure;
602
647
  const options = measure.options ?? {};
603
- const pixels = options[spec.pixels];
604
- if (typeof pixels !== "number") return measure;
648
+ const specs = sizeOptionSpecs(measure.kind).filter((spec) => typeof options[spec.pixels] === "number");
649
+ if (specs.length === 0) return measure;
605
650
  const mpp = adapter.getResolution();
606
651
  if (mpp === void 0 || mpp <= 0) return measure;
652
+ const pixelKeys = new Set(specs.map((spec) => spec.pixels));
607
653
  const rest = {};
608
- for (const [key, value] of Object.entries(options)) if (key !== spec.pixels && key !== "metersPerPixel") rest[key] = value;
609
- rest[spec.meters] = pixels * mpp;
654
+ for (const [key, value] of Object.entries(options)) if (!pixelKeys.has(key) && key !== "metersPerPixel") rest[key] = value;
655
+ for (const spec of specs) rest[spec.meters] = options[spec.pixels] * mpp;
610
656
  return {
611
657
  ...measure,
612
658
  options: rest
@@ -632,24 +678,24 @@ function withAdapterMeasureOptions(measure, adapter) {
632
678
  };
633
679
  }
634
680
  /**
635
- * Capture draw-time label sizing options. These are stable feature style
636
- * inputs; adapters scale from them without TacticalDraw re-rendering on every
637
- * zoom. No-op if the measure already carries `labelSizePixels` (e.g. already
638
- * committed), so it's safe to call on every render as well as on commit
639
- * itself — see [[renderMeasureForAdapter]].
681
+ * Stamp the draw-time default label size, in its screen-anchored
682
+ * (`labelSizePixels`) form, onto a `capturesLabelSize` measure that carries no
683
+ * label size of its own (ADR-0028). The pixel form keeps the in-flight
684
+ * preview screen-anchored through the gesture; the commit's default `ground`
685
+ * anchor then bakes it to `labelSize` meters via [[bakePixelSizeToGround]],
686
+ * while a `screen` commit persists the pixels. No-op once either label-size
687
+ * option is present (e.g. already committed), so it's safe to call on every
688
+ * render as well as on commit itself — see [[renderMeasureForAdapter]].
640
689
  */
641
- function withCommittedAdapterMeasureOptions(measure, adapter) {
690
+ function withCommittedAdapterMeasureOptions(measure) {
642
691
  if (!getControlMeasureMetadata(measure.kind).capturesLabelSize) return measure;
643
- if (typeof measure.options?.labelSizePixels === "number") return measure;
644
- const zoom = adapter.getZoom();
645
- const resolution = adapter.getResolution();
692
+ const options = measure.options;
693
+ if (typeof options?.labelSizePixels === "number" || typeof options?.labelSize === "number") return measure;
646
694
  return {
647
695
  ...measure,
648
696
  options: {
649
697
  ...measure.options,
650
- labelSizePixels: 14,
651
- ...zoom !== void 0 ? { labelSizeZoom: zoom } : {},
652
- ...resolution !== void 0 && resolution > 0 ? { labelSizeResolution: resolution } : {}
698
+ labelSizePixels: 14
653
699
  }
654
700
  };
655
701
  }
@@ -668,7 +714,7 @@ function withCommittedAdapterMeasureOptions(measure, adapter) {
668
714
  * directly to produce the value they persist).
669
715
  */
670
716
  function renderMeasureForAdapter(adapter, measure, opts) {
671
- return renderControlMeasure(withAdapterMeasureOptions(withCommittedAdapterMeasureOptions(measure, adapter), adapter), opts);
717
+ return renderControlMeasure(withAdapterMeasureOptions(withCommittedAdapterMeasureOptions(measure), adapter), opts);
672
718
  }
673
719
  //#endregion
674
720
  //#region src/tactical-draw/run-interaction.ts
@@ -963,7 +1009,7 @@ function runFixedLengthDraw(options) {
963
1009
  ...measureStyle !== void 0 ? { style: measureStyle } : {},
964
1010
  ...measureProperties !== void 0 ? { properties: measureProperties } : {},
965
1011
  ...measureTextAmplifiers !== void 0 ? { textAmplifiers: measureTextAmplifiers } : {}
966
- }, adapter);
1012
+ });
967
1013
  const measure = sizeAnchor === "screen" ? captured : bakePixelSizeToGround(captured, adapter);
968
1014
  return {
969
1015
  measure: cloneControlMeasure(measure),
@@ -1335,7 +1381,7 @@ function runVariableLengthDraw(options) {
1335
1381
  ...measureStyle !== void 0 ? { style: measureStyle } : {},
1336
1382
  ...measureProperties !== void 0 ? { properties: measureProperties } : {},
1337
1383
  ...measureTextAmplifiers !== void 0 ? { textAmplifiers: measureTextAmplifiers } : {}
1338
- }, adapter);
1384
+ });
1339
1385
  const measure = sizeAnchor === "screen" ? built : bakePixelSizeToGround(built, adapter);
1340
1386
  return {
1341
1387
  measure: cloneControlMeasure(measure),
@@ -4336,4 +4382,4 @@ function nearestWorldCopyPosition(position, refLng) {
4336
4382
  return [nearestWorldCopyLng(position[0], refLng), ...position.slice(1)];
4337
4383
  }
4338
4384
  //#endregion
4339
- export { BaseMapAdapter, PointerCallbackHub, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, TacticalDrawDestroyedError, bindAbortable, coerceHandleStyle, combineWithHostSignal, getSizeAnchor, hitTestFeature, ignoreAbort, isTacticalDrawAbortError, nearestWorldCopyLng, nearestWorldCopyPosition, pickHitTolerance, tryControlMeasureIdFromFeature };
4385
+ export { BaseMapAdapter, 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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orbat-mapper/tactical-draw",
3
- "version": "0.2.0-alpha.13",
3
+ "version": "0.2.0-alpha.14",
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",
@@ -38,7 +38,7 @@
38
38
  "access": "public"
39
39
  },
40
40
  "dependencies": {
41
- "@orbat-mapper/control-measures": "^0.2.0-alpha.10"
41
+ "@orbat-mapper/control-measures": "^0.2.0-alpha.11"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@arethetypeswrong/core": "^0.18.3",