@orbat-mapper/control-measures 0.6.0 → 0.8.0

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.
@@ -586,20 +586,16 @@ const area1DrawRule = {
586
586
  }
587
587
  };
588
588
  //#endregion
589
- //#region src/draw-rules/area11.ts
590
- const blockDrawRule = createMidpointPerpendicularDrawRule({ id: "area11:block" });
591
- //#endregion
592
- //#region src/draw-rules/area12.ts
589
+ //#region src/draw-rules/area5.ts
593
590
  /**
594
- * Area12 anchor draw rule for Disrupt.
595
- *
596
- * P1/P2 are the free spine endpoints. P3 is the longest-arrow tip, constrained
597
- * to the perpendicular axis through P1 (not the baseline midpoint).
591
+ * Area5 — PT. 1/PT. 2 define the open diameter and PT. 3 defines the arrow end.
592
+ * The arrow stays on the perpendicular bisector of the diameter.
598
593
  */
599
- const disruptDrawRule = createMidpointPerpendicularDrawRule({
600
- id: "area12:disrupt",
601
- origin: "p1",
602
- normal: "left"
594
+ const containDrawRule = createMidpointPerpendicularDrawRule({
595
+ id: "area5:contain",
596
+ minimumUserPoints: 3,
597
+ defaultDistanceRatio: -1.5,
598
+ minimumPreviewPoints: 2
603
599
  });
604
600
  //#endregion
605
601
  //#region src/draw-rules/area15.ts
@@ -630,6 +626,34 @@ const centerRadiusDrawRule = {
630
626
  /** Backward-compatible doctrinal name for Area15's shared center-radius rule. */
631
627
  const isolateDrawRule = centerRadiusDrawRule;
632
628
  //#endregion
629
+ //#region src/draw-rules/area6.ts
630
+ /**
631
+ * Area6 — center + start point, mechanically identical to Area15. It re-labels
632
+ * the shared behaviour rather than aliasing it because a definition's `rule.id`
633
+ * must carry its `metadata.drawRule` as a prefix (see the registry invariant in
634
+ * `__tests__/metadata-registry.spec.ts`, ADR-0013).
635
+ */
636
+ const retainDrawRule = {
637
+ ...centerRadiusDrawRule,
638
+ id: "area6:center-radius"
639
+ };
640
+ //#endregion
641
+ //#region src/draw-rules/area11.ts
642
+ const blockDrawRule = createMidpointPerpendicularDrawRule({ id: "area11:block" });
643
+ //#endregion
644
+ //#region src/draw-rules/area12.ts
645
+ /**
646
+ * Area12 anchor draw rule for Disrupt.
647
+ *
648
+ * P1/P2 are the free spine endpoints. P3 is the longest-arrow tip, constrained
649
+ * to the perpendicular axis through P1 (not the baseline midpoint).
650
+ */
651
+ const disruptDrawRule = createMidpointPerpendicularDrawRule({
652
+ id: "area12:disrupt",
653
+ origin: "p1",
654
+ normal: "left"
655
+ });
656
+ //#endregion
633
657
  //#region src/draw-rules/area17.ts
634
658
  /**
635
659
  * Area17 anchor draw rule for Penetrate.
@@ -689,6 +713,81 @@ const staticPointDrawRule = {
689
713
  id: "point2:static-anchor"
690
714
  };
691
715
  //#endregion
716
+ //#region src/draw-rules/sector.ts
717
+ const SECTOR_ANGLE_SNAP_RADIANS = Math.PI / 36;
718
+ /**
719
+ * Sector — anchor + inner-left edge + outer-right edge. The two radial points
720
+ * independently control a radius and a true-north azimuth. Dragging the anchor
721
+ * translates both edge points so the sector retains its size and orientation.
722
+ */
723
+ function derive$9(points) {
724
+ return clonePositions(points.slice(0, 3));
725
+ }
726
+ function guidePoints$1(points) {
727
+ return points.length < 3 ? clonePositions(points) : [];
728
+ }
729
+ /** Snap one radial Sector handle to a true-north angular increment. */
730
+ function snapSectorAngle(points, activePointIndex, incrementRadians) {
731
+ const next = clonePositions(points);
732
+ if (activePointIndex !== 1 && activePointIndex !== 2) return next;
733
+ const anchor = next[0];
734
+ const point = next[activePointIndex];
735
+ if (!anchor || !point) return next;
736
+ const range = haversineDistance(anchor, point);
737
+ const bearing = sphericalBearing(anchor, point);
738
+ next[activePointIndex] = [...destinationPoint(anchor, range, Math.round(bearing / incrementRadians) * incrementRadians), ...point.slice(2)];
739
+ return next;
740
+ }
741
+ function constrainAngles(points, activePointIndex) {
742
+ return snapSectorAngle(points, activePointIndex, SECTOR_ANGLE_SNAP_RADIANS);
743
+ }
744
+ const sectorDrawRule = {
745
+ id: "sector:anchor-radii",
746
+ minimumUserPoints: 3,
747
+ canonicalPointCount: 3,
748
+ showGuide: true,
749
+ guidePoints: guidePoints$1,
750
+ constrainAngles,
751
+ derive: derive$9,
752
+ transform(event) {
753
+ const { previous, next, activePointIndex } = event;
754
+ if (activePointIndex === 0 && previous.length >= 3 && next.length >= 3) {
755
+ const dx = next[0][0] - previous[0][0];
756
+ const dy = next[0][1] - previous[0][1];
757
+ return [
758
+ clonePosition(next[0]),
759
+ [previous[1][0] + dx, previous[1][1] + dy],
760
+ [previous[2][0] + dx, previous[2][1] + dy]
761
+ ];
762
+ }
763
+ return derive$9(next);
764
+ }
765
+ };
766
+ //#endregion
767
+ //#region src/draw-rules/point18.ts
768
+ const WHOLE_DEGREE_RADIANS = Math.PI / 180;
769
+ function snapToWholeDegrees(points) {
770
+ let snapped = snapSectorAngle(points, 1, WHOLE_DEGREE_RADIANS);
771
+ if (snapped.length >= 3) snapped = snapSectorAngle(snapped, 2, WHOLE_DEGREE_RADIANS);
772
+ return snapped;
773
+ }
774
+ /**
775
+ * Point18 — doctrinally a numeric, single-anchor symbol. The interactive
776
+ * authoring model exposes those dimensions as Sector-style radial handles:
777
+ * P1 is the anchor, P2 fixes the start range/left bearing, and P3 fixes the
778
+ * stop range/right bearing.
779
+ */
780
+ const dynamicPointDrawRule = {
781
+ ...sectorDrawRule,
782
+ id: "point18:anchor-radii",
783
+ derive(points) {
784
+ return snapToWholeDegrees(sectorDrawRule.derive(points));
785
+ },
786
+ transform(event) {
787
+ return snapSectorAngle(sectorDrawRule.transform(event), event.activePointIndex, WHOLE_DEGREE_RADIANS);
788
+ }
789
+ };
790
+ //#endregion
692
791
  //#region src/draw-rules/area7.ts
693
792
  /**
694
793
  * Area7 anchor draw rule for Attack By Fire.
@@ -716,20 +815,20 @@ const ambushDrawRule = createMidpointPerpendicularDrawRule({
716
815
  });
717
816
  //#endregion
718
817
  //#region src/draw-rules/line1.ts
719
- function derive$9(points) {
818
+ function derive$8(points) {
720
819
  return clonePositions(points);
721
820
  }
722
821
  const line1DrawRule = {
723
822
  id: "line1",
724
823
  minimumUserPoints: 2,
725
- derive: derive$9,
824
+ derive: derive$8,
726
825
  transform(event) {
727
- return derive$9(event.next);
826
+ return derive$8(event.next);
728
827
  }
729
828
  };
730
829
  //#endregion
731
830
  //#region src/draw-rules/line3.ts
732
- function derive$8(points) {
831
+ function derive$7(points) {
733
832
  return clonePositions(points);
734
833
  }
735
834
  /**
@@ -744,23 +843,23 @@ const line3DrawRule = {
744
843
  id: "line3",
745
844
  minimumUserPoints: 3,
746
845
  canonicalPointCount: 3,
747
- derive: derive$8,
846
+ derive: derive$7,
748
847
  transform(event) {
749
- return derive$8(event.next);
848
+ return derive$7(event.next);
750
849
  }
751
850
  };
752
851
  //#endregion
753
852
  //#region src/draw-rules/line9.ts
754
- function derive$7(points) {
853
+ function derive$6(points) {
755
854
  return clonePositions(points);
756
855
  }
757
856
  const line9DrawRule = {
758
857
  id: "line9",
759
858
  minimumUserPoints: 2,
760
859
  canonicalPointCount: 2,
761
- derive: derive$7,
860
+ derive: derive$6,
762
861
  transform(event) {
763
- return derive$7(event.next);
862
+ return derive$6(event.next);
764
863
  }
765
864
  };
766
865
  //#endregion
@@ -867,7 +966,7 @@ const line24DrawRule = createMidpointPerpendicularDrawRule({
867
966
  });
868
967
  //#endregion
869
968
  //#region src/draw-rules/line26.ts
870
- function derive$6(points) {
969
+ function derive$5(points) {
871
970
  if (points.length === 2) {
872
971
  const [p1, p2] = points;
873
972
  const dx = p2[0] - p1[0];
@@ -906,9 +1005,9 @@ const line26DrawRule = {
906
1005
  minimumUserPoints: 4,
907
1006
  minimumPreviewPoints: 2,
908
1007
  canonicalPointCount: 4,
909
- derive: derive$6,
1008
+ derive: derive$5,
910
1009
  transform(event) {
911
- return derive$6(event.next);
1010
+ return derive$5(event.next);
912
1011
  }
913
1012
  };
914
1013
  //#endregion
@@ -921,7 +1020,7 @@ function deriveArcPoint(p1, p2, p4) {
921
1020
  if (!arc) return [(p2[0] + p4[0]) / 2, (p2[1] + p4[1]) / 2];
922
1021
  return unproject(arc.midpoint[0], arc.midpoint[1]);
923
1022
  }
924
- function derive$5(points) {
1023
+ function derive$4(points) {
925
1024
  if (points.length < 2) return clonePositions(points);
926
1025
  const [p1, p2] = points;
927
1026
  if (points.length === 2) {
@@ -960,7 +1059,7 @@ const line27DrawRule = {
960
1059
  minimumUserPoints: 3,
961
1060
  minimumPreviewPoints: 2,
962
1061
  canonicalPointCount: 4,
963
- derive: derive$5,
1062
+ derive: derive$4,
964
1063
  transform(event) {
965
1064
  const { previous, next, activePointIndex } = event;
966
1065
  if (activePointIndex === 0 && previous.length >= 4 && next.length >= 4) {
@@ -993,7 +1092,7 @@ const line27DrawRule = {
993
1092
  };
994
1093
  //#endregion
995
1094
  //#region src/draw-rules/area8.ts
996
- function derive$4(points) {
1095
+ function derive$3(points) {
997
1096
  if (points.length < 2) return clonePositions(points);
998
1097
  if (points.length !== 2) return clonePositions(points.slice(0, 4));
999
1098
  const p1 = clonePosition(points[0]);
@@ -1017,16 +1116,16 @@ const supportByFireDrawRule = {
1017
1116
  id: "area8:support-by-fire",
1018
1117
  minimumUserPoints: 2,
1019
1118
  canonicalPointCount: 4,
1020
- derive: derive$4,
1119
+ derive: derive$3,
1021
1120
  transform(event) {
1022
1121
  const { next } = event;
1023
1122
  if (next.length === 4) return clonePositions(next);
1024
- return derive$4(next);
1123
+ return derive$3(next);
1025
1124
  }
1026
1125
  };
1027
1126
  //#endregion
1028
1127
  //#region src/draw-rules/axis1.ts
1029
- function derive$3(points) {
1128
+ function derive$2(points) {
1030
1129
  if (points.length < 2) return clonePositions(points);
1031
1130
  if (points.length === 2) {
1032
1131
  const tip = clonePosition(points[0]);
@@ -1064,10 +1163,10 @@ const axis1DrawRule = {
1064
1163
  id: "axis1",
1065
1164
  minimumUserPoints: 2,
1066
1165
  trailingFixedSlots: 1,
1067
- derive: derive$3,
1166
+ derive: derive$2,
1068
1167
  transform(event) {
1069
1168
  const { previous, next, activePointIndex } = event;
1070
- if (next.length < 3 || previous.length < 3) return derive$3(next);
1169
+ if (next.length < 3 || previous.length < 3) return derive$2(next);
1071
1170
  if (!(activePointIndex === 0 || activePointIndex === 1)) return clonePositions(next);
1072
1171
  const metrics = calculateMetrics(previous);
1073
1172
  if (!metrics) return clonePositions(next);
@@ -1120,7 +1219,7 @@ const counterattackByFireDrawRule = {
1120
1219
  };
1121
1220
  //#endregion
1122
1221
  //#region src/draw-rules/area21.ts
1123
- function derive$2(points) {
1222
+ function derive$1(points) {
1124
1223
  if (points.length === 2) return clonePositions([
1125
1224
  points[0],
1126
1225
  points[1],
@@ -1133,9 +1232,9 @@ const searchAreaDrawRule = {
1133
1232
  minimumUserPoints: 3,
1134
1233
  minimumPreviewPoints: 2,
1135
1234
  canonicalPointCount: 3,
1136
- derive: derive$2,
1235
+ derive: derive$1,
1137
1236
  transform(event) {
1138
- return derive$2(event.next);
1237
+ return derive$1(event.next);
1139
1238
  }
1140
1239
  };
1141
1240
  //#endregion
@@ -1145,7 +1244,7 @@ const searchAreaDrawRule = {
1145
1244
  * edge. P3 is the next corner, constrained to the perpendicular through P2.
1146
1245
  * The generator derives the fourth corner.
1147
1246
  */
1148
- function derive$1(points) {
1247
+ function derive(points) {
1149
1248
  if (points.length < 2) return clonePositions(points);
1150
1249
  const p1 = clonePosition(points[0]);
1151
1250
  const p2 = clonePosition(points[1]);
@@ -1164,8 +1263,8 @@ function derive$1(points) {
1164
1263
  frame?.pointAtNormalDistance(frame.signedNormalDistance(points[2])) ?? clonePosition(points[2])
1165
1264
  ];
1166
1265
  }
1167
- function guidePoints$1(points) {
1168
- const canonical = derive$1(points);
1266
+ function guidePoints(points) {
1267
+ const canonical = derive(points);
1169
1268
  if (canonical.length < 3) return canonical;
1170
1269
  const [c1, c2, c3] = canonical;
1171
1270
  if (!c1 || !c2 || !c3) return canonical;
@@ -1191,16 +1290,16 @@ const rectangleDrawRule = {
1191
1290
  minimumPreviewPoints: 2,
1192
1291
  canonicalPointCount: 3,
1193
1292
  showGuide: true,
1194
- guidePoints: guidePoints$1,
1195
- derive: derive$1,
1293
+ guidePoints,
1294
+ derive,
1196
1295
  transform(event) {
1197
1296
  const { previous, next, activePointIndex } = event;
1198
- if (previous.length < 3 || next.length < 3) return derive$1(next);
1297
+ if (previous.length < 3 || next.length < 3) return derive(next);
1199
1298
  const frame = createBaselineFrame(previous[0], previous[1], {
1200
1299
  origin: "p2",
1201
1300
  normal: "right"
1202
1301
  });
1203
- if (!frame) return derive$1(next);
1302
+ if (!frame) return derive(next);
1204
1303
  if (activePointIndex === 0) return [
1205
1304
  snapToAxis(next[0], frame.p2, frame.direction),
1206
1305
  clonePosition(previous[1]),
@@ -1225,53 +1324,6 @@ const rectangleDrawRule = {
1225
1324
  p3
1226
1325
  ];
1227
1326
  }
1228
- return derive$1(next);
1229
- }
1230
- };
1231
- //#endregion
1232
- //#region src/draw-rules/sector.ts
1233
- const SECTOR_ANGLE_SNAP_RADIANS = Math.PI / 36;
1234
- /**
1235
- * Sector — anchor + inner-left edge + outer-right edge. The two radial points
1236
- * independently control a radius and a true-north azimuth. Dragging the anchor
1237
- * translates both edge points so the sector retains its size and orientation.
1238
- */
1239
- function derive(points) {
1240
- return clonePositions(points.slice(0, 3));
1241
- }
1242
- function guidePoints(points) {
1243
- return points.length < 3 ? clonePositions(points) : [];
1244
- }
1245
- function constrainAngles(points, activePointIndex) {
1246
- const next = clonePositions(points);
1247
- if (activePointIndex !== 1 && activePointIndex !== 2) return next;
1248
- const anchor = next[0];
1249
- const point = next[activePointIndex];
1250
- if (!anchor || !point) return next;
1251
- const range = haversineDistance(anchor, point);
1252
- const bearing = sphericalBearing(anchor, point);
1253
- next[activePointIndex] = [...destinationPoint(anchor, range, Math.round(bearing / SECTOR_ANGLE_SNAP_RADIANS) * SECTOR_ANGLE_SNAP_RADIANS), ...point.slice(2)];
1254
- return next;
1255
- }
1256
- const sectorDrawRule = {
1257
- id: "sector:anchor-radii",
1258
- minimumUserPoints: 3,
1259
- canonicalPointCount: 3,
1260
- showGuide: true,
1261
- guidePoints,
1262
- constrainAngles,
1263
- derive,
1264
- transform(event) {
1265
- const { previous, next, activePointIndex } = event;
1266
- if (activePointIndex === 0 && previous.length >= 3 && next.length >= 3) {
1267
- const dx = next[0][0] - previous[0][0];
1268
- const dy = next[0][1] - previous[0][1];
1269
- return [
1270
- clonePosition(next[0]),
1271
- [previous[1][0] + dx, previous[1][1] + dy],
1272
- [previous[2][0] + dx, previous[2][1] + dy]
1273
- ];
1274
- }
1275
1327
  return derive(next);
1276
1328
  }
1277
1329
  };
@@ -2103,6 +2155,43 @@ function buildGappedLine(verts, gaps) {
2103
2155
  return parts;
2104
2156
  }
2105
2157
  //#endregion
2158
+ //#region src/style.ts
2159
+ /**
2160
+ * Per-feature style hint pinning a filled part to a solid interior. Attach to
2161
+ * a generator's intrinsic doctrinal accents — arrowheads, teeth, echelon
2162
+ * glyphs, barbs, blades — so a patterned (`hatch`, `dots`, …) fill set at the graphicsStyle
2163
+ * or measure layer never bleeds into a small silhouette and ruins its
2164
+ * legibility. The renderer only reads style hints, so one shared instance is
2165
+ * safe to reuse across every accent feature.
2166
+ */
2167
+ const SOLID_ACCENT_FILL = { fillPattern: "solid" };
2168
+ /**
2169
+ * Ultimate fallback for the symbol color when no `color` or per-channel
2170
+ * override is supplied at any layer. Keeps a zero-config render monocolor
2171
+ * black and guarantees filled parts still render filled. See ADR-0011.
2172
+ */
2173
+ const DEFAULT_SYMBOL_COLOR = "#000000";
2174
+ //#endregion
2175
+ //#region src/portrayal.ts
2176
+ const DEFAULT_STROKE_WIDTH_CSS_PIXELS = 2;
2177
+ const DEFAULT_STROKE_DASH_CSS_PIXELS = Object.freeze([]);
2178
+ const DEFAULT_LINE_CAP = "round";
2179
+ const DEFAULT_LINE_JOIN = "round";
2180
+ const DEFAULT_LABEL_HEIGHT_CSS_PIXELS = 14;
2181
+ const DEFAULT_LABEL_SIZE_CLAMP_CSS_PIXELS = {
2182
+ min: 8,
2183
+ max: 24
2184
+ };
2185
+ /** Shared absent-value portrayal defaults; render output remains sparse. */
2186
+ const DEFAULT_PORTRAYAL = Object.freeze({
2187
+ symbolColor: DEFAULT_SYMBOL_COLOR,
2188
+ strokeWidthCssPixels: 2,
2189
+ strokeDashCssPixels: DEFAULT_STROKE_DASH_CSS_PIXELS,
2190
+ lineCap: DEFAULT_LINE_CAP,
2191
+ lineJoin: DEFAULT_LINE_JOIN,
2192
+ labelHeightCssPixels: 14
2193
+ });
2194
+ //#endregion
2106
2195
  //#region src/internal/angle-utils.ts
2107
2196
  /**
2108
2197
  * Wraps an angle (radians) into the half-open range (-π, π].
@@ -2609,10 +2698,50 @@ function pushEndLabels(out, verts, textForEnd, options = {}) {
2609
2698
  * measurement is available at generator time.
2610
2699
  */
2611
2700
  const LABEL_CHAR_ASPECT_RATIO = .6;
2701
+ /**
2702
+ * Half a single glyph's width, as a ratio of its height — the base boundary
2703
+ * gap a measure reserves for one intrinsic capital letter (Retain's R,
2704
+ * Contain's C). Wider than half `LABEL_CHAR_ASPECT_RATIO` because these
2705
+ * glyphs are stamped alone rather than averaged over a word.
2706
+ */
2707
+ const LABEL_HALF_GLYPH_RATIO = .45;
2612
2708
  /** Estimated rendered width of `text` given its resolved label height in meters. */
2613
2709
  function estimatedTextWidth(text, textHeightMeters) {
2614
2710
  return text.length * LABEL_CHAR_ASPECT_RATIO * textHeightMeters;
2615
2711
  }
2712
+ /** Resolve the label's final display size, then measure and convert it to construction metres. */
2713
+ function resolveTextMetrics(text, options, context, style = "regular") {
2714
+ const legacyHeight = resolveLabelOffsetMeters(options, 1);
2715
+ const constructionScale = context?.constructionMetersPerCssPixel;
2716
+ if (!(constructionScale !== void 0 && constructionScale > 0)) return {
2717
+ width: estimatedTextWidth(text, legacyHeight),
2718
+ height: legacyHeight
2719
+ };
2720
+ let sizeCssPixels;
2721
+ if (options.labelSizePixels !== void 0) sizeCssPixels = options.labelSizePixels;
2722
+ else if (options.labelSize !== void 0) {
2723
+ const requested = options.labelSize / constructionScale;
2724
+ const requestedBand = context?.labelSizeClampCssPixels;
2725
+ const band = requestedBand && Number.isFinite(requestedBand.min) && requestedBand.min >= 0 && Number.isFinite(requestedBand.max) && requestedBand.max >= 0 ? requestedBand : DEFAULT_LABEL_SIZE_CLAMP_CSS_PIXELS;
2726
+ const min = Math.min(band.min, band.max);
2727
+ const max = Math.max(band.min, band.max);
2728
+ sizeCssPixels = Math.min(max, Math.max(min, requested));
2729
+ } else sizeCssPixels = 14;
2730
+ const fallbackHeight = sizeCssPixels * constructionScale;
2731
+ const measured = context?.measureText?.({
2732
+ text,
2733
+ style,
2734
+ sizeCssPixels
2735
+ });
2736
+ if (!measured || !Number.isFinite(measured.widthCssPixels) || measured.widthCssPixels < 0 || !Number.isFinite(measured.heightCssPixels) || measured.heightCssPixels < 0) return {
2737
+ width: estimatedTextWidth(text, fallbackHeight),
2738
+ height: fallbackHeight
2739
+ };
2740
+ return {
2741
+ width: measured.widthCssPixels * constructionScale,
2742
+ height: measured.heightCssPixels * constructionScale
2743
+ };
2744
+ }
2616
2745
  /** The end-label text for a phase-line naming, or `""` when unnamed. */
2617
2746
  function phaseLineLabelText(name, includePrefix) {
2618
2747
  if (!name) return "";
@@ -2624,17 +2753,17 @@ function phaseLineLabelText(name, includePrefix) {
2624
2753
  * {@link pushEndLabels}. When another beyond-tip label is present, its
2625
2754
  * estimated width is added so the hostile marker remains outermost.
2626
2755
  */
2627
- function pushHostileEndLabels(out, verts, hostileText, options = {}, occupiedTextForEnd = "") {
2756
+ function pushHostileEndLabels(out, verts, hostileText, options = {}, occupiedTextForEnd = "", context) {
2628
2757
  if (!hostileText) return;
2629
2758
  const frames = endFrames(verts);
2630
2759
  const sizeProps = labelSizeProps(options);
2631
- const textHeight = resolveLabelOffsetMeters(options, 1);
2632
- const clearance = resolveLabelOffsetMeters(options, END_LABEL_CLEARANCE_RATIO + Math.max(0, options.labelPadding ?? 0));
2633
2760
  for (const end of ["start", "end"]) {
2634
2761
  const frame = frames[end];
2635
2762
  if (!frame) continue;
2636
2763
  const occupiedText = typeof occupiedTextForEnd === "string" ? occupiedTextForEnd : occupiedTextForEnd(end);
2637
- const distance = clearance + (occupiedText ? estimatedTextWidth(occupiedText, textHeight) + clearance : 0);
2764
+ const metrics = resolveTextMetrics(occupiedText || hostileText, options, context);
2765
+ const clearance = (END_LABEL_CLEARANCE_RATIO + Math.max(0, options.labelPadding ?? 0)) * metrics.height;
2766
+ const distance = clearance + (occupiedText ? metrics.width + clearance : 0);
2638
2767
  pushLabel(out, vecAdd(frame.point, vecScale(frame.along, distance)), hostileText, labelRotationAlong(frame.along), sizeProps, frame.along[0] >= 0 ? "start" : "end", "N", `label:N:${end}`);
2639
2768
  }
2640
2769
  }
@@ -2693,7 +2822,7 @@ function pushEndLabelsAbove(out, verts, textForEnd, options = {}) {
2693
2822
  * (via {@link pushEndLabels} or {@link pushEndLabelsAbove}, per `placement`)
2694
2823
  * as a complete FeatureCollection.
2695
2824
  */
2696
- function fixedLabelLineFeatures(positions, options, textForEnd, hostileText) {
2825
+ function fixedLabelLineFeatures(positions, options, textForEnd, hostileText, context) {
2697
2826
  const { part, placement = "beyond", phaseLineName, includePrefix = true, smooth, smoothResolution, ...sizeOptions } = options;
2698
2827
  const verts = smoothLineVerts(positions.map((p) => project(p[0], p[1])), {
2699
2828
  smooth,
@@ -2713,7 +2842,7 @@ function fixedLabelLineFeatures(positions, options, textForEnd, hostileText) {
2713
2842
  else pushEndLabels(labelFeatures, verts, textForEnd, sizeOptions);
2714
2843
  const nameText = phaseLineLabelText(phaseLineName?.trim(), includePrefix);
2715
2844
  if (nameText) pushEndLabels(labelFeatures, verts, nameText, sizeOptions);
2716
- pushHostileEndLabels(labelFeatures, verts, hostileText, sizeOptions, nameText || (placement === "beyond" ? textForEnd : ""));
2845
+ pushHostileEndLabels(labelFeatures, verts, hostileText, sizeOptions, nameText || (placement === "beyond" ? textForEnd : ""), context);
2717
2846
  features.push(...labelFeatures);
2718
2847
  return {
2719
2848
  type: "FeatureCollection",
@@ -2879,10 +3008,9 @@ function nonNegative$1(value, fallback) {
2879
3008
  * later `buildGappedLine` walk); `segments`/`totalLength` describe the full
2880
3009
  * route that anchors the default label placement.
2881
3010
  */
2882
- function directionLabelGap(verts, segments, totalLength, fraction, text, options, placement = {}) {
3011
+ function directionLabelGap(verts, segments, totalLength, fraction, text, options, placement = {}, context) {
2883
3012
  if (!text) return void 0;
2884
- const textHeight = resolveLabelOffsetMeters(options, 1);
2885
- const textWidth = estimatedTextWidth(text, textHeight);
3013
+ const { width: textWidth, height: textHeight } = resolveTextMetrics(text, options, context);
2886
3014
  const clearance = (LABEL_GAP_CLEARANCE_RATIO + Math.max(0, options.labelPadding ?? 0)) * textHeight;
2887
3015
  const frame = pointAlongPolyline(segments, totalLength, fraction);
2888
3016
  const rotation = placement.rotation ?? (frame ? labelRotationAlong(frame.along) : 0);
@@ -2943,7 +3071,7 @@ function createDirectionOfAttackLine(coordinates, options, textAmplifiers, confi
2943
3071
  shaftVerts.push(innerTip);
2944
3072
  }
2945
3073
  const labelPosition = clampLinePosition(options.labelPosition, DEFAULT_DIRECTION_LABEL_POSITION);
2946
- const labelGap = directionLabelGap(shaftVerts, segments, totalLength, labelPosition, textAmplifiers.T, options, resolveAmplifierPlacement(context.amplifierPlacements?.T));
3074
+ const labelGap = directionLabelGap(shaftVerts, segments, totalLength, labelPosition, textAmplifiers.T, options, resolveAmplifierPlacement(context.amplifierPlacements?.T), context);
2947
3075
  const lines = [...buildGappedLine(shaftVerts, labelGap ? [labelGap] : []), arrowhead.map((point) => unproject(point[0], point[1]))];
2948
3076
  const features = [{
2949
3077
  type: "Feature",
@@ -3227,7 +3355,7 @@ function createDirectionOfAttackAviation(coordinates, options = {}, textAmplifie
3227
3355
  const bowTieB = vecSub(bowTieStart, vecScale(bowTiePerp, bowTieHalfWidth));
3228
3356
  const bowTieC = vecAdd(bowTieEnd, vecScale(bowTiePerp, bowTieHalfWidth));
3229
3357
  const bowTieD = vecSub(bowTieEnd, vecScale(bowTiePerp, bowTieHalfWidth));
3230
- const labelGap = directionLabelGap(verts, segments, totalLength, labelPosition, textAmplifiers.T, resolved, resolveAmplifierPlacement(context.amplifierPlacements?.T));
3358
+ const labelGap = directionLabelGap(verts, segments, totalLength, labelPosition, textAmplifiers.T, resolved, resolveAmplifierPlacement(context.amplifierPlacements?.T), context);
3231
3359
  const features = [{
3232
3360
  type: "Feature",
3233
3361
  properties: { part: "direction-of-attack-aviation" },
@@ -3807,9 +3935,12 @@ function ringLabelAnchor(verts) {
3807
3935
  * overlaps the finite contacted edge; intrinsic markers always mask. Callers
3808
3936
  * own marker names, anchors, rotation, clearance, placement keys, and fields.
3809
3937
  */
3810
- function pushBoundaryMarkers(out, maskVerts, text, anchors, options, amplifierPlacements) {
3938
+ function pushBoundaryMarkers(out, maskVerts, text, anchors, options, amplifierPlacements, context, labelOptions) {
3811
3939
  const { rotation, textHeight, clearance, sizeProps } = options;
3812
- const textWidth = estimatedTextWidth(text, textHeight);
3940
+ const metrics = resolveTextMetrics(text, labelOptions ?? { labelSize: textHeight }, context);
3941
+ const textWidth = metrics.width;
3942
+ const resolvedTextHeight = metrics.height;
3943
+ const resolvedClearance = textHeight > 0 ? clearance * (resolvedTextHeight / textHeight) : clearance;
3813
3944
  const gaps = [];
3814
3945
  for (const anchor of anchors) {
3815
3946
  const labelPlacementKey = options.placementKeyPrefix ? `${options.placementKeyPrefix}:${anchor.side}` : void 0;
@@ -3819,21 +3950,21 @@ function pushBoundaryMarkers(out, maskVerts, text, anchors, options, amplifierPl
3819
3950
  pushLabel(out, labelPoint, text, options.followPlacementRotation ? boxRotation : rotation, sizeProps, void 0, options.amplifierField, labelPlacementKey);
3820
3951
  const contact = placement.position !== void 0 || options.rescanDefaultAnchors === true || anchor.contact === void 0 ? nearestPointOnPolyline(maskVerts, labelPoint) : anchor.contact;
3821
3952
  if (!contact) continue;
3822
- if (placement.position && !labelBoxIntersectsSegment(labelPoint, ...contact.edge, textWidth, textHeight, boxRotation)) continue;
3823
- const tangentExtent = labelHalfExtent(contact.along, textWidth, textHeight, boxRotation) + clearance;
3953
+ if (placement.position && !labelBoxIntersectsSegment(labelPoint, ...contact.edge, textWidth, resolvedTextHeight, boxRotation)) continue;
3954
+ const tangentExtent = labelHalfExtent(contact.along, textWidth, resolvedTextHeight, boxRotation) + resolvedClearance;
3824
3955
  pushWrappedGap(gaps, contact.arc - tangentExtent, contact.arc + tangentExtent, contact.totalArc);
3825
3956
  }
3826
3957
  return gaps;
3827
3958
  }
3828
3959
  /** Emits movable Field N markers using the generic boundary-marker knockout path. */
3829
- function pushHostileMarkers(out, maskVerts, text, anchors, options, amplifierPlacements) {
3960
+ function pushHostileMarkers(out, maskVerts, text, anchors, options, amplifierPlacements, context, labelOptions) {
3830
3961
  return pushBoundaryMarkers(out, maskVerts, text, anchors, {
3831
3962
  ...options,
3832
3963
  placementKeyPrefix: "label:N",
3833
3964
  amplifierField: "N"
3834
- }, amplifierPlacements);
3965
+ }, amplifierPlacements, context, labelOptions);
3835
3966
  }
3836
- function pushAreaLabels(out, ringVerts, texts, options = {}, amplifierPlacements, maskBoundary) {
3967
+ function pushAreaLabels(out, ringVerts, texts, options = {}, amplifierPlacements, maskBoundary, context) {
3837
3968
  if (ringVerts.length < 2) return [];
3838
3969
  const sizeProps = labelSizeProps(options);
3839
3970
  const padding = Math.max(0, options.labelPadding ?? 0);
@@ -3876,7 +4007,7 @@ function pushAreaLabels(out, ringVerts, texts, options = {}, amplifierPlacements
3876
4007
  clearance: (ENY_CLEARANCE_RATIO + padding) * textHeight + Math.max(0, maskBoundary?.extraClearanceMeters ?? 0),
3877
4008
  sizeProps,
3878
4009
  rescanDefaultAnchors: maskRingVerts !== ringVerts
3879
- }, amplifierPlacements);
4010
+ }, amplifierPlacements, context, options);
3880
4011
  }
3881
4012
  /**
3882
4013
  * Boundary + fill features for a pattern-filled area (e.g. Limited Access Area,
@@ -3955,11 +4086,11 @@ function boundaryFeature(verts, gaps) {
3955
4086
  * echelon-masked boundary, ADR-0023) instead of a closed `Polygon` — the
3956
4087
  * common (no `N`) case is unaffected.
3957
4088
  */
3958
- function labeledAreaFeatures(positions, options, texts, amplifierPlacements) {
4089
+ function labeledAreaFeatures(positions, options, texts, amplifierPlacements, context) {
3959
4090
  const { smooth = false, smoothResolution = DEFAULT_SMOOTH_RESOLUTION$9, ...labelOptions } = options;
3960
4091
  const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$9);
3961
4092
  const labelFeatures = [];
3962
- const features = [boundaryFeature(verts, pushAreaLabels(labelFeatures, verts, texts, labelOptions, amplifierPlacements))];
4093
+ const features = [boundaryFeature(verts, pushAreaLabels(labelFeatures, verts, texts, labelOptions, amplifierPlacements, void 0, context))];
3963
4094
  features.push(...labelFeatures);
3964
4095
  return {
3965
4096
  type: "FeatureCollection",
@@ -4003,7 +4134,7 @@ const AREA_METADATA = {
4003
4134
  * marker, per {@link labeledAreaFeatures}.
4004
4135
  */
4005
4136
  function createArea(positions, options = {}, textAmplifiers = {}, context = {}) {
4006
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, ""), context.amplifierPlacements);
4137
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, ""), context.amplifierPlacements, context);
4007
4138
  }
4008
4139
  const AREA = defineControlMeasure({
4009
4140
  metadata: AREA_METADATA,
@@ -4022,23 +4153,6 @@ const AREA = defineControlMeasure({
4022
4153
  }
4023
4154
  });
4024
4155
  //#endregion
4025
- //#region src/style.ts
4026
- /**
4027
- * Per-feature style hint pinning a filled part to a solid interior. Attach to
4028
- * a generator's intrinsic doctrinal accents — arrowheads, teeth, echelon
4029
- * glyphs, barbs, blades — so a patterned (`hatch`, `dots`, …) fill set at the graphicsStyle
4030
- * or measure layer never bleeds into a small silhouette and ruins its
4031
- * legibility. The renderer only reads style hints, so one shared instance is
4032
- * safe to reuse across every accent feature.
4033
- */
4034
- const SOLID_ACCENT_FILL = { fillPattern: "solid" };
4035
- /**
4036
- * Ultimate fallback for the symbol color when no `color` or per-channel
4037
- * override is supplied at any layer. Keeps a zero-config render monocolor
4038
- * black and guarantees filled parts still render filled. See ADR-0011.
4039
- */
4040
- const DEFAULT_SYMBOL_COLOR = "#000000";
4041
- //#endregion
4042
4156
  //#region src/generators/cm15-maneuver-areas/area-defense.ts
4043
4157
  const DEFAULT_AREA_DEFENSE_OPTIONS = {
4044
4158
  arrowOpeningAngle: 30,
@@ -4122,13 +4236,13 @@ const AREA_DEFENSE_METADATA = {
4122
4236
  }
4123
4237
  ]
4124
4238
  };
4125
- function pointOnCircle(center, radius, angle) {
4239
+ function pointOnCircle$1(center, radius, angle) {
4126
4240
  return vecAdd(center, [radius * Math.cos(angle), radius * Math.sin(angle)]);
4127
4241
  }
4128
- function sampleArc$1(center, radius, start, end) {
4242
+ function sampleArc$2(center, radius, start, end) {
4129
4243
  const span = end - start;
4130
4244
  const segments = Math.max(8, Math.round(48 * Math.abs(span) / Math.PI));
4131
- return Array.from({ length: segments + 1 }, (_, index) => pointOnCircle(center, radius, start + span * index / segments));
4245
+ return Array.from({ length: segments + 1 }, (_, index) => pointOnCircle$1(center, radius, start + span * index / segments));
4132
4246
  }
4133
4247
  function openArrowhead(tip, direction, length) {
4134
4248
  const base = vecSub(tip, [direction[0] * length, direction[1] * length]);
@@ -4159,8 +4273,8 @@ function createAreaDefense(coordinates, options = {}) {
4159
4273
  const upperEnd = upperTipAngle;
4160
4274
  const lowerStart = labelCenter - labelHalf;
4161
4275
  const lowerEnd = upperTipAngle + arrowHalf * 2 - Math.PI * 2;
4162
- const upperArc = sampleArc$1(center, radius, upperStart, upperEnd);
4163
- const lowerArc = sampleArc$1(center, radius, lowerStart, lowerEnd);
4276
+ const upperArc = sampleArc$2(center, radius, upperStart, upperEnd);
4277
+ const lowerArc = sampleArc$2(center, radius, lowerStart, lowerEnd);
4164
4278
  const arrowLength = radius * Math.max(0, resolved.arrowheadLengthRatio);
4165
4279
  const upperDirection = [-Math.sin(upperEnd), Math.cos(upperEnd)];
4166
4280
  const lowerDirection = [Math.sin(lowerEnd), -Math.cos(lowerEnd)];
@@ -4185,17 +4299,17 @@ function createAreaDefense(coordinates, options = {}) {
4185
4299
  const barbPolygons = [];
4186
4300
  for (const arc of barbArcs) for (let ordinal = 1; ordinal <= arc.count; ordinal++) {
4187
4301
  const angle = arc.start + (arc.end - arc.start) * ordinal / (arc.count + 1);
4188
- const left = pointOnCircle(center, radius, angle - barbHalfAngle);
4189
- const right = pointOnCircle(center, radius, angle + barbHalfAngle);
4302
+ const left = pointOnCircle$1(center, radius, angle - barbHalfAngle);
4303
+ const right = pointOnCircle$1(center, radius, angle + barbHalfAngle);
4190
4304
  const ring = [
4191
4305
  left,
4192
- pointOnCircle(center, radius + barbLength, angle),
4306
+ pointOnCircle$1(center, radius + barbLength, angle),
4193
4307
  right,
4194
4308
  left
4195
4309
  ].map((point) => unproject(point[0], point[1]));
4196
4310
  barbPolygons.push([ring]);
4197
4311
  }
4198
- const labelPoint = pointOnCircle(center, radius, labelCenter);
4312
+ const labelPoint = pointOnCircle$1(center, radius, labelCenter);
4199
4313
  return {
4200
4314
  type: "FeatureCollection",
4201
4315
  features: [
@@ -4283,7 +4397,7 @@ const AREA_OF_OPERATIONS_METADATA = {
4283
4397
  * `H`/`W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
4284
4398
  */
4285
4399
  function createAreaOfOperations(positions, options = {}, textAmplifiers = {}, context = {}) {
4286
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "AO"), context.amplifierPlacements);
4400
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "AO"), context.amplifierPlacements, context);
4287
4401
  }
4288
4402
  const AREA_OF_OPERATIONS = defineControlMeasure({
4289
4403
  metadata: AREA_OF_OPERATIONS_METADATA,
@@ -4342,7 +4456,7 @@ const NAMED_AREA_OF_INTEREST_METADATA = {
4342
4456
  * an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
4343
4457
  */
4344
4458
  function createNamedAreaOfInterest(positions, options = {}, textAmplifiers = {}, context = {}) {
4345
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "NAI"), context.amplifierPlacements);
4459
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "NAI"), context.amplifierPlacements, context);
4346
4460
  }
4347
4461
  const NAMED_AREA_OF_INTEREST = defineControlMeasure({
4348
4462
  metadata: NAMED_AREA_OF_INTEREST_METADATA,
@@ -4401,7 +4515,7 @@ const TARGET_AREA_OF_INTEREST_METADATA = {
4401
4515
  * an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
4402
4516
  */
4403
4517
  function createTargetAreaOfInterest(positions, options = {}, textAmplifiers = {}, context = {}) {
4404
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "TAI"), context.amplifierPlacements);
4518
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "TAI"), context.amplifierPlacements, context);
4405
4519
  }
4406
4520
  const TARGET_AREA_OF_INTEREST = defineControlMeasure({
4407
4521
  metadata: TARGET_AREA_OF_INTEREST_METADATA,
@@ -4496,7 +4610,7 @@ function createAirfieldZone(positions, options = {}, textAmplifiers = {}, contex
4496
4610
  const { smooth = DEFAULT_AIRFIELD_ZONE_OPTIONS.smooth, smoothResolution = DEFAULT_AIRFIELD_ZONE_OPTIONS.smoothResolution, glyphSizeRatio = DEFAULT_AIRFIELD_ZONE_OPTIONS.glyphSizeRatio, ...labelOptions } = options;
4497
4611
  const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$8);
4498
4612
  const enyLabels = [];
4499
- const features = [boundaryFeature(verts, textAmplifiers.N ? pushAreaLabels(enyLabels, verts, { hostile: textAmplifiers.N }, labelOptions, context.amplifierPlacements) : [])];
4613
+ const features = [boundaryFeature(verts, textAmplifiers.N ? pushAreaLabels(enyLabels, verts, { hostile: textAmplifiers.N }, labelOptions, context.amplifierPlacements, void 0, context) : [])];
4500
4614
  let minX = Infinity;
4501
4615
  let maxX = -Infinity;
4502
4616
  for (const [x] of verts) {
@@ -4572,13 +4686,13 @@ const DEFAULT_ECHELON_LABELED_AREA_OPTIONS = {
4572
4686
  * before carving the boundary, so either (or both) opens it into a gapped
4573
4687
  * `MultiLineString`; with neither, the boundary is a closed `Polygon`.
4574
4688
  */
4575
- function echelonLabeledAreaFeatures(positions, options, texts, amplifierPlacements) {
4689
+ function echelonLabeledAreaFeatures(positions, options, texts, amplifierPlacements, context) {
4576
4690
  const { echelon = DEFAULT_ECHELON_LABELED_AREA_OPTIONS.echelon, echelonSize = DEFAULT_ECHELON_LABELED_AREA_OPTIONS.echelonSize, echelonSizePixels, echelonPadding = DEFAULT_ECHELON_LABELED_AREA_OPTIONS.echelonPadding, echelonPosition = DEFAULT_ECHELON_LABELED_AREA_OPTIONS.echelonPosition, echelonAnchor, metersPerPixel, smooth = DEFAULT_ECHELON_LABELED_AREA_OPTIONS.smooth, smoothResolution = DEFAULT_ECHELON_LABELED_AREA_OPTIONS.smoothResolution, ...labelOptions } = options;
4577
4691
  const h = resolveEchelonHeight(echelonSize, echelonSizePixels, metersPerPixel);
4578
4692
  const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$7);
4579
4693
  const { strokes, fills, gaps: echelonGaps } = placeEchelon(buildAreaPerimeter(verts), h, echelon, echelonPadding, echelonPosition, echelonAnchor);
4580
4694
  const labelFeatures = [];
4581
- const enyGaps = pushAreaLabels(labelFeatures, verts, texts, labelOptions, amplifierPlacements);
4695
+ const enyGaps = pushAreaLabels(labelFeatures, verts, texts, labelOptions, amplifierPlacements, void 0, context);
4582
4696
  const features = [boundaryFeature(verts, [...echelonGaps, ...enyGaps])];
4583
4697
  if (strokes.length > 0) features.push({
4584
4698
  type: "Feature",
@@ -4654,7 +4768,7 @@ const BASE_CAMP_METADATA = {
4654
4768
  * boundary, per {@link echelonLabeledAreaFeatures}.
4655
4769
  */
4656
4770
  function createBaseCamp(positions, options = {}, textAmplifiers = {}, context = {}) {
4657
- return echelonLabeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "BC"), context.amplifierPlacements);
4771
+ return echelonLabeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "BC"), context.amplifierPlacements, context);
4658
4772
  }
4659
4773
  const BASE_CAMP = defineControlMeasure({
4660
4774
  metadata: BASE_CAMP_METADATA,
@@ -4712,7 +4826,7 @@ const GUERRILLA_BASE_METADATA = {
4712
4826
  * straddling the boundary, per {@link echelonLabeledAreaFeatures}.
4713
4827
  */
4714
4828
  function createGuerrillaBase(positions, options = {}, textAmplifiers = {}, context = {}) {
4715
- return echelonLabeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "GB"), context.amplifierPlacements);
4829
+ return echelonLabeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "GB"), context.amplifierPlacements, context);
4716
4830
  }
4717
4831
  const GUERRILLA_BASE = defineControlMeasure({
4718
4832
  metadata: GUERRILLA_BASE_METADATA,
@@ -4774,7 +4888,7 @@ const GENERIC_C2_AREA_METADATA = {
4774
4888
  * `H`/`W`/`W1`/`N` amplifier rows — no prefix, per {@link labeledAreaFeatures}.
4775
4889
  */
4776
4890
  function createGenericC2Area(positions, options = {}, textAmplifiers = {}, context = {}) {
4777
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers), context.amplifierPlacements);
4891
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers), context.amplifierPlacements, context);
4778
4892
  }
4779
4893
  const GENERIC_C2_AREA = defineControlMeasure({
4780
4894
  metadata: GENERIC_C2_AREA_METADATA,
@@ -4829,7 +4943,7 @@ const ASSEMBLY_AREA_METADATA = {
4829
4943
  * `H`/`W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
4830
4944
  */
4831
4945
  function createAssemblyArea(positions, options = {}, textAmplifiers = {}, context = {}) {
4832
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "AA"), context.amplifierPlacements);
4946
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "AA"), context.amplifierPlacements, context);
4833
4947
  }
4834
4948
  const ASSEMBLY_AREA = defineControlMeasure({
4835
4949
  metadata: ASSEMBLY_AREA_METADATA,
@@ -5021,13 +5135,13 @@ function ditchPaths(projectedPoints, options) {
5021
5135
  function buildAntitankDitchTriangles(basePath, options) {
5022
5136
  return generateAntitankDitchTriangles(basePath, calculateAntitankDitchHeight(options), options.toothWidthRatio ?? DEFAULT_TOOTH_WIDTH_RATIO$1);
5023
5137
  }
5024
- function createAntitankDitchUnderConstruction(positions, options = {}, textAmplifiers = {}) {
5138
+ function createAntitankDitchUnderConstruction(positions, options = {}, textAmplifiers = {}, context = {}) {
5025
5139
  const { smoothedPath, basePath } = ditchPaths(positions.map((position) => project(position[0], position[1])), options);
5026
5140
  const triangles = buildAntitankDitchTriangles(basePath, options);
5027
5141
  const baseLine = options.smooth ? basePath.map((point) => unproject(point[0], point[1])) : positions.map((position) => [position[0], position[1]]);
5028
5142
  const triangleLines = triangles.map((triangle) => triangle.map((point) => unproject(point[0], point[1])));
5029
5143
  const labels = [];
5030
- pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options);
5144
+ pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options, "", context);
5031
5145
  return {
5032
5146
  type: "FeatureCollection",
5033
5147
  features: [{
@@ -5040,13 +5154,13 @@ function createAntitankDitchUnderConstruction(positions, options = {}, textAmpli
5040
5154
  }, ...labels]
5041
5155
  };
5042
5156
  }
5043
- function createAntitankDitchCompleted(positions, options = {}, textAmplifiers = {}) {
5157
+ function createAntitankDitchCompleted(positions, options = {}, textAmplifiers = {}, context = {}) {
5044
5158
  const { smoothedPath, basePath } = ditchPaths(positions.map((position) => project(position[0], position[1])), options);
5045
5159
  const polygons = buildAntitankDitchTriangles(basePath, options).map((triangle) => {
5046
5160
  return [[...triangle, triangle[0]].map((point) => unproject(point[0], point[1]))];
5047
5161
  });
5048
5162
  const labels = [];
5049
- pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options);
5163
+ pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options, "", context);
5050
5164
  return {
5051
5165
  type: "FeatureCollection",
5052
5166
  features: [{
@@ -5136,7 +5250,7 @@ function generateAntitankWallPoints(projectedPoints, toothHeight, toothWidthRati
5136
5250
  }
5137
5251
  return wallPoints;
5138
5252
  }
5139
- function createAntitankWall(positions, options = {}, textAmplifiers = {}) {
5253
+ function createAntitankWall(positions, options = {}, textAmplifiers = {}, context = {}) {
5140
5254
  const toothHeight = calculateAntitankDitchHeight(options);
5141
5255
  const toothWidthRatio = options.toothWidthRatio ?? DEFAULT_TOOTH_WIDTH_RATIO;
5142
5256
  const toothSpacingRatio = options.toothSpacingRatio ?? DEFAULT_TOOTH_SPACING_RATIO;
@@ -5144,7 +5258,7 @@ function createAntitankWall(positions, options = {}, textAmplifiers = {}) {
5144
5258
  const smoothedPath = smoothLineVerts(projectedPoints, options, 16);
5145
5259
  const wallLine = generateAntitankWallPoints(options.smooth && projectedPoints.length >= 3 ? evenlySpacePath(smoothedPath, Math.max(EPSILON, toothWidthRatio) * toothHeight) : smoothedPath, toothHeight, toothWidthRatio, toothSpacingRatio).map((point) => unproject(point[0], point[1]));
5146
5260
  const labels = [];
5147
- pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options);
5261
+ pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options, "", context);
5148
5262
  return {
5149
5263
  type: "FeatureCollection",
5150
5264
  features: [{
@@ -5591,7 +5705,7 @@ function createBattlePosition(positions, options = {}, textAmplifiers = {}, cont
5591
5705
  const enyGaps = pushAreaLabels(labelFeatures, verts, {
5592
5706
  name: textAmplifiers.T,
5593
5707
  hostile: textAmplifiers.N
5594
- }, options, context.amplifierPlacements);
5708
+ }, options, context.amplifierPlacements, void 0, context);
5595
5709
  const boundaryCoords = buildGappedLine(verts, [...gaps, ...enyGaps]);
5596
5710
  const features = [];
5597
5711
  if (boundaryCoords.length > 0) features.push({
@@ -5628,41 +5742,253 @@ function createBattlePosition(positions, options = {}, textAmplifiers = {}, cont
5628
5742
  features
5629
5743
  };
5630
5744
  }
5631
- /**
5632
- * `freezeOrientation` hook (ADR-0023) shared by Battle Position and Strong
5633
- * Point: pins the echelon anchor to its current bottom-edge fraction so a
5634
- * subsequent rigid rotation of `controlPoints` (e.g. the transform box's
5635
- * rotate grip) does not re-derive a different "bottom edge" against the
5636
- * rotated geometry. Delegates to {@link makeFreezeEchelonAnchor}, the shared
5637
- * factory every echelon-carrying area measure (Battle Position, Strong
5638
- * Point, Base Camp, Guerrilla Base) builds its own instance from — this
5639
- * export just binds Battle Position's own defaults, keeping strongPoint.ts's
5640
- * existing import (and any other importer) unchanged.
5641
- */
5642
- const freezeEchelonAnchor = makeFreezeEchelonAnchor({
5643
- echelon: DEFAULT_BATTLE_POSITION_OPTIONS.echelon,
5644
- smooth: DEFAULT_BATTLE_POSITION_OPTIONS.smooth,
5645
- smoothResolution: DEFAULT_BATTLE_POSITION_OPTIONS.smoothResolution
5646
- });
5647
- const BATTLE_POSITION = defineControlMeasure({
5648
- metadata: BATTLE_POSITION_METADATA,
5649
- generator: createBattlePosition,
5650
- defaultOptions: DEFAULT_BATTLE_POSITION_OPTIONS,
5651
- rule: area1DrawRule,
5652
- freezeOrientation: freezeEchelonAnchor,
5745
+ /**
5746
+ * `freezeOrientation` hook (ADR-0023) shared by Battle Position and Strong
5747
+ * Point: pins the echelon anchor to its current bottom-edge fraction so a
5748
+ * subsequent rigid rotation of `controlPoints` (e.g. the transform box's
5749
+ * rotate grip) does not re-derive a different "bottom edge" against the
5750
+ * rotated geometry. Delegates to {@link makeFreezeEchelonAnchor}, the shared
5751
+ * factory every echelon-carrying area measure (Battle Position, Strong
5752
+ * Point, Base Camp, Guerrilla Base) builds its own instance from — this
5753
+ * export just binds Battle Position's own defaults, keeping strongPoint.ts's
5754
+ * existing import (and any other importer) unchanged.
5755
+ */
5756
+ const freezeEchelonAnchor = makeFreezeEchelonAnchor({
5757
+ echelon: DEFAULT_BATTLE_POSITION_OPTIONS.echelon,
5758
+ smooth: DEFAULT_BATTLE_POSITION_OPTIONS.smooth,
5759
+ smoothResolution: DEFAULT_BATTLE_POSITION_OPTIONS.smoothResolution
5760
+ });
5761
+ const BATTLE_POSITION = defineControlMeasure({
5762
+ metadata: BATTLE_POSITION_METADATA,
5763
+ generator: createBattlePosition,
5764
+ defaultOptions: DEFAULT_BATTLE_POSITION_OPTIONS,
5765
+ rule: area1DrawRule,
5766
+ freezeOrientation: freezeEchelonAnchor,
5767
+ previewSample: {
5768
+ controlPoints: [
5769
+ [-1, -.5],
5770
+ [1, -.5],
5771
+ [1, .5],
5772
+ [-1, .5]
5773
+ ],
5774
+ textAmplifiers: { T: "ALFA" },
5775
+ options: {
5776
+ echelonSize: 200,
5777
+ smooth: true,
5778
+ labelSize: 96
5779
+ }
5780
+ }
5781
+ });
5782
+ //#endregion
5783
+ //#region src/internal/arrowhead.ts
5784
+ /**
5785
+ * Builds a closed, filled arrowhead triangle ring in unprojected coordinates.
5786
+ *
5787
+ * `tip` is the point of the arrow, `dir` the unit direction the arrow points,
5788
+ * `length` the distance from tip back to the base, and `width` the base width.
5789
+ * The returned ring is `[left, tip, right, left]`.
5790
+ */
5791
+ function createArrowHeadRing(tip, dir, length, width) {
5792
+ const perp = [-dir[1], dir[0]];
5793
+ const base = vecSub(tip, vecScale(dir, length));
5794
+ const left = vecAdd(base, vecScale(perp, width / 2));
5795
+ const right = vecSub(base, vecScale(perp, width / 2));
5796
+ return [
5797
+ unproject(left[0], left[1]),
5798
+ unproject(tip[0], tip[1]),
5799
+ unproject(right[0], right[1]),
5800
+ unproject(left[0], left[1])
5801
+ ];
5802
+ }
5803
+ //#endregion
5804
+ //#region src/internal/projected-arc.ts
5805
+ /** The arc point at `angle`, in projected meters. */
5806
+ function arcPointAt(center, startRadius, bulgeRadius, angle) {
5807
+ return vecAdd(center, vecAdd(vecScale(startRadius, Math.cos(angle)), vecScale(bulgeRadius, Math.sin(angle))));
5808
+ }
5809
+ /**
5810
+ * Samples the arc from `start` to `end` radians as unprojected positions.
5811
+ * `resolution` is the segment count a full half-turn would use, so the density
5812
+ * of a partial arc stays proportional to how much of the circle it covers.
5813
+ */
5814
+ function sampleProjectedArc(center, startRadius, bulgeRadius, start, end, resolution) {
5815
+ const segments = Math.max(2, Math.ceil(resolution * Math.abs(end - start) / Math.PI));
5816
+ return Array.from({ length: segments + 1 }, (_, index) => {
5817
+ const point = arcPointAt(center, startRadius, bulgeRadius, start + (end - start) * index / segments);
5818
+ return unproject(point[0], point[1]);
5819
+ });
5820
+ }
5821
+ //#endregion
5822
+ //#region src/generators/cm15-maneuver-areas/contain.ts
5823
+ const DEFAULT_CONTAIN_OPTIONS = {
5824
+ labelPadding: .2,
5825
+ resolution: 32
5826
+ };
5827
+ const CONTAIN_METADATA = {
5828
+ id: "contain",
5829
+ name: "Contain",
5830
+ description: "Stops, holds, or surrounds enemy forces and prevents them from withdrawing or using them elsewhere.",
5831
+ entity: "Maneuver Areas",
5832
+ entityType: "Battle Position",
5833
+ entitySubtype: "Contain",
5834
+ value: "151204",
5835
+ minCoordinates: 3,
5836
+ maxCoordinates: 3,
5837
+ geometry: "area",
5838
+ geometryTypes: ["MultiLineString", "Point"],
5839
+ paints: {
5840
+ stroke: true,
5841
+ fill: "none",
5842
+ text: true
5843
+ },
5844
+ drawRule: "Area5",
5845
+ capturesLabelSize: true,
5846
+ textAmplifiers: [AREA_HOSTILE_AMPLIFIER],
5847
+ params: [{
5848
+ key: "labelPadding",
5849
+ presentationTier: "advanced",
5850
+ label: "Label padding",
5851
+ description: "Extra clearance around C and ENY, as a ratio of label height",
5852
+ type: "number",
5853
+ min: 0,
5854
+ max: 2,
5855
+ step: .05
5856
+ }, {
5857
+ key: "resolution",
5858
+ presentationTier: "advanced",
5859
+ label: "Resolution",
5860
+ description: "Number of segments used to approximate the semicircle",
5861
+ type: "number",
5862
+ min: 8,
5863
+ max: 128,
5864
+ step: 1
5865
+ }]
5866
+ };
5867
+ /** Where the ENY label sits along the arrow, as a ratio of its full length. */
5868
+ const HOSTILE_LABEL_POSITION_RATIO = .55;
5869
+ /** Creates the Contain semicircle, inward tics, center-pointing arrow, and C label. */
5870
+ function createContain(coordinates, options = {}, textAmplifiers = {}, context = {}) {
5871
+ const p1 = project(coordinates[0][0], coordinates[0][1]);
5872
+ const p2 = project(coordinates[1][0], coordinates[1][1]);
5873
+ const p3 = project(coordinates[2][0], coordinates[2][1]);
5874
+ const diameterLength = vecMag(vecSub(p2, p1));
5875
+ if (diameterLength < 1e-6) return {
5876
+ type: "FeatureCollection",
5877
+ features: []
5878
+ };
5879
+ const center = [(p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2];
5880
+ const arrowVector = vecSub(p3, center);
5881
+ const arrowLength = vecMag(arrowVector);
5882
+ if (arrowLength < 1e-6) return {
5883
+ type: "FeatureCollection",
5884
+ features: []
5885
+ };
5886
+ const radius = diameterLength / 2;
5887
+ const openingDirection = vecNorm(arrowVector);
5888
+ const labelRotation = labelRotationAlong(openingDirection);
5889
+ const startRadius = vecSub(p1, center);
5890
+ const bulgeRadius = vecScale(openingDirection, -radius);
5891
+ const h = Math.min(radius * .45, Math.max(EPSILON, resolveLabelOffsetMeters(options, 1, radius * .12)));
5892
+ const labelPadding = Math.max(0, options.labelPadding ?? DEFAULT_CONTAIN_OPTIONS.labelPadding);
5893
+ const resolution = Math.round(Math.min(128, Math.max(8, options.resolution ?? DEFAULT_CONTAIN_OPTIONS.resolution)));
5894
+ const hostileText = textAmplifiers.N ?? "ENY";
5895
+ const labelHalfAngle = Math.min(Math.PI / 3, h * (LABEL_HALF_GLYPH_RATIO + labelPadding) / radius);
5896
+ const labelAngle = Math.PI / 2;
5897
+ const boundary = [sampleProjectedArc(center, startRadius, bulgeRadius, 0, labelAngle - labelHalfAngle, resolution), sampleProjectedArc(center, startRadius, bulgeRadius, labelAngle + labelHalfAngle, Math.PI, resolution)];
5898
+ const tics = [];
5899
+ const spacingAngle = h / radius;
5900
+ for (let angle = spacingAngle / 2; angle < Math.PI; angle += spacingAngle) {
5901
+ if (Math.abs(angle - labelAngle) <= labelHalfAngle) continue;
5902
+ const outer = arcPointAt(center, startRadius, bulgeRadius, angle);
5903
+ const inner = vecAdd(outer, vecScale(vecNorm(vecSub(center, outer)), h));
5904
+ tics.push([unproject(outer[0], outer[1]), unproject(inner[0], inner[1])]);
5905
+ }
5906
+ const arrowhead = createArrowHeadRing(center, vecScale(openingDirection, -1), h, h).slice(0, 3);
5907
+ const arrow = [];
5908
+ const hostilePoint = vecAdd(center, vecScale(arrowVector, HOSTILE_LABEL_POSITION_RATIO));
5909
+ if (hostileText) {
5910
+ const metrics = resolveTextMetrics(hostileText, options, context);
5911
+ const maximumHalfGap = arrowLength * Math.min(HOSTILE_LABEL_POSITION_RATIO, 1 - HOSTILE_LABEL_POSITION_RATIO);
5912
+ const halfGap = Math.min(maximumHalfGap, metrics.width / 2 + labelPadding * metrics.height);
5913
+ const farGapEdge = vecAdd(hostilePoint, vecScale(openingDirection, halfGap));
5914
+ const nearGapEdge = vecAdd(hostilePoint, vecScale(openingDirection, -halfGap));
5915
+ arrow.push([unproject(p3[0], p3[1]), unproject(farGapEdge[0], farGapEdge[1])], [unproject(nearGapEdge[0], nearGapEdge[1]), unproject(center[0], center[1])]);
5916
+ } else arrow.push([unproject(p3[0], p3[1]), unproject(center[0], center[1])]);
5917
+ arrow.push(arrowhead);
5918
+ const labelPoint = arcPointAt(center, startRadius, bulgeRadius, labelAngle);
5919
+ const sizeProps = labelSizeProps(options);
5920
+ const features = [
5921
+ {
5922
+ type: "Feature",
5923
+ properties: { part: "boundary" },
5924
+ geometry: {
5925
+ type: "MultiLineString",
5926
+ coordinates: boundary
5927
+ }
5928
+ },
5929
+ {
5930
+ type: "Feature",
5931
+ properties: { part: "tics" },
5932
+ geometry: {
5933
+ type: "MultiLineString",
5934
+ coordinates: tics
5935
+ }
5936
+ },
5937
+ {
5938
+ type: "Feature",
5939
+ properties: { part: "arrow" },
5940
+ geometry: {
5941
+ type: "MultiLineString",
5942
+ coordinates: arrow
5943
+ }
5944
+ },
5945
+ {
5946
+ type: "Feature",
5947
+ properties: {
5948
+ part: "label",
5949
+ labelPlacement: false,
5950
+ text: "C",
5951
+ rotation: labelRotation,
5952
+ ...sizeProps
5953
+ },
5954
+ geometry: {
5955
+ type: "Point",
5956
+ coordinates: unproject(labelPoint[0], labelPoint[1])
5957
+ }
5958
+ }
5959
+ ];
5960
+ if (hostileText) features.push({
5961
+ type: "Feature",
5962
+ properties: {
5963
+ part: "label",
5964
+ labelPlacement: false,
5965
+ amplifierField: "N",
5966
+ text: hostileText,
5967
+ rotation: labelRotation,
5968
+ ...sizeProps
5969
+ },
5970
+ geometry: {
5971
+ type: "Point",
5972
+ coordinates: unproject(hostilePoint[0], hostilePoint[1])
5973
+ }
5974
+ });
5975
+ return {
5976
+ type: "FeatureCollection",
5977
+ features
5978
+ };
5979
+ }
5980
+ const CONTAIN = defineControlMeasure({
5981
+ metadata: CONTAIN_METADATA,
5982
+ generator: createContain,
5983
+ defaultOptions: DEFAULT_CONTAIN_OPTIONS,
5984
+ rule: containDrawRule,
5653
5985
  previewSample: {
5654
5986
  controlPoints: [
5655
- [-1, -.5],
5656
- [1, -.5],
5657
- [1, .5],
5658
- [-1, .5]
5987
+ [0, .7],
5988
+ [0, -.7],
5989
+ [1.5, 0]
5659
5990
  ],
5660
- textAmplifiers: { T: "ALFA" },
5661
- options: {
5662
- echelonSize: 200,
5663
- smooth: true,
5664
- labelSize: 96
5665
- }
5991
+ options: { labelSize: 90 }
5666
5992
  }
5667
5993
  });
5668
5994
  //#endregion
@@ -6189,7 +6515,7 @@ const BOUNDARY_METADATA = {
6189
6515
  * @param textAmplifiers - Normalized text amplifiers (ADR-0027): `T`/`AS` label
6190
6516
  * unit 1 (left-of-travel side), `T1`/`AS1` label unit 2 (right-of-travel side).
6191
6517
  */
6192
- function createBoundary(positions, options = {}, textAmplifiers = {}) {
6518
+ function createBoundary(positions, options = {}, textAmplifiers = {}, context = {}) {
6193
6519
  const { echelon = DEFAULT_BOUNDARY_OPTIONS.echelon, echelonSize = DEFAULT_BOUNDARY_OPTIONS.echelonSize, echelonSizePixels, echelonPadding = DEFAULT_BOUNDARY_OPTIONS.echelonPadding, metersPerPixel, labelRepetitions = DEFAULT_BOUNDARY_OPTIONS.labelRepetitions, labelSpacing = DEFAULT_BOUNDARY_OPTIONS.labelSpacing, labelPosition = DEFAULT_BOUNDARY_OPTIONS.labelPosition, labelPadding = DEFAULT_BOUNDARY_OPTIONS.labelPadding, labelOrientation = DEFAULT_BOUNDARY_OPTIONS.labelOrientation, smooth = DEFAULT_BOUNDARY_OPTIONS.smooth, smoothResolution = DEFAULT_BOUNDARY_OPTIONS.smoothResolution } = options;
6194
6520
  const unit1Designator = textAmplifiers.T ?? "";
6195
6521
  const unit1Country = textAmplifiers.AS ?? "";
@@ -6273,7 +6599,7 @@ function createBoundary(positions, options = {}, textAmplifiers = {}) {
6273
6599
  labelSize: h,
6274
6600
  labelPadding
6275
6601
  };
6276
- pushHostileEndLabels(labelFeatures, verts, textAmplifiers.N, hostileLabelOptions);
6602
+ pushHostileEndLabels(labelFeatures, verts, textAmplifiers.N, hostileLabelOptions, "", context);
6277
6603
  const boundaryCoords = buildGappedLine(verts, gaps);
6278
6604
  const features = [];
6279
6605
  if (boundaryCoords.length > 0) features.push({
@@ -6389,12 +6715,12 @@ const LIGHT_LINE_METADATA = {
6389
6715
  * Creates a Light line control measure: a polyline through `positions`
6390
6716
  * labeled "LL" above the line near each end.
6391
6717
  */
6392
- function createLightLine(positions, options = {}, textAmplifiers = {}) {
6718
+ function createLightLine(positions, options = {}, textAmplifiers = {}, context = {}) {
6393
6719
  return fixedLabelLineFeatures(positions, {
6394
6720
  part: "light-line",
6395
6721
  placement: "above",
6396
6722
  ...options
6397
- }, "LL", textAmplifiers.N);
6723
+ }, "LL", textAmplifiers.N, context);
6398
6724
  }
6399
6725
  const LIGHT_LINE = defineControlMeasure({
6400
6726
  metadata: LIGHT_LINE_METADATA,
@@ -6500,12 +6826,12 @@ const CENTER_LABEL_OFFSET_RATIO = .7;
6500
6826
  * above the line, T1/AS1 below). No center group is emitted when all four
6501
6827
  * amplifiers are empty.
6502
6828
  */
6503
- function createEngineerWorkLine(positions, options = {}, textAmplifiers = {}) {
6829
+ function createEngineerWorkLine(positions, options = {}, textAmplifiers = {}, context = {}) {
6504
6830
  const features = [...fixedLabelLineFeatures(positions, {
6505
6831
  part: "engineer-work-line",
6506
6832
  placement: "above",
6507
6833
  ...options
6508
- }, "EWL", textAmplifiers.N).features];
6834
+ }, "EWL", textAmplifiers.N, context).features];
6509
6835
  const { segments, totalLength } = polylineSegments(smoothLineVerts(positions.map((p) => project(p[0], p[1])), options, 12));
6510
6836
  const frame = pointAlongPolyline(segments, totalLength, .5);
6511
6837
  if (frame) {
@@ -6642,7 +6968,7 @@ const GENERIC_C2_LINE_METADATA = {
6642
6968
  * (inset from the tip, anchored toward the line's interior), one at
6643
6969
  * `base + perp·offset` and the other at `base − perp·offset`.
6644
6970
  */
6645
- function createGenericC2Line(positions, options = {}, textAmplifiers = {}) {
6971
+ function createGenericC2Line(positions, options = {}, textAmplifiers = {}, context = {}) {
6646
6972
  const { phaseLineName, includePrefix = true, smooth, smoothResolution, ...sizeOptions } = options;
6647
6973
  const verts = smoothLineVerts(positions.map((p) => project(p[0], p[1])), {
6648
6974
  smooth,
@@ -6674,7 +7000,7 @@ function createGenericC2Line(positions, options = {}, textAmplifiers = {}) {
6674
7000
  }
6675
7001
  const nameText = phaseLineLabelText(trimmedPhaseLineName, includePrefix);
6676
7002
  if (nameText) pushEndLabels(labelFeatures, verts, nameText, sizeOptions);
6677
- pushHostileEndLabels(labelFeatures, verts, textAmplifiers.N, sizeOptions, nameText);
7003
+ pushHostileEndLabels(labelFeatures, verts, textAmplifiers.N, sizeOptions, nameText, context);
6678
7004
  features.push(...labelFeatures);
6679
7005
  }
6680
7006
  return {
@@ -7526,6 +7852,8 @@ const ARC_RESOLUTION_PARAMS = [{
7526
7852
  max: 360,
7527
7853
  step: 1
7528
7854
  }];
7855
+ //#endregion
7856
+ //#region src/generators/cm99-generic-graphics/params.ts
7529
7857
  const SMOOTH_PATH_PARAMS = [{
7530
7858
  key: "smooth",
7531
7859
  label: "Smooth",
@@ -7862,6 +8190,41 @@ const GENERIC_RECTANGLE = defineControlMeasure({
7862
8190
  ] }
7863
8191
  });
7864
8192
  //#endregion
8193
+ //#region src/internal/annular-sector.ts
8194
+ /**
8195
+ * Clockwise sweep in degrees from `leftAzimuth` to `rightAzimuth`. A non-zero
8196
+ * raw difference that normalizes to 0 means a full 360° fan.
8197
+ *
8198
+ * `collapseToleranceDegrees` (default 0, i.e. off) treats a near-zero or
8199
+ * near-360 sweep as a collapsed sector. Callers whose handle coordinates are
8200
+ * rounded need this: two handles placed on the same ray can recover bearings a
8201
+ * few thousandths of a degree apart, which would otherwise read as an
8202
+ * almost-360° fan.
8203
+ */
8204
+ function clockwiseSweepDegrees(leftAzimuth, rightAzimuth, collapseToleranceDegrees = 0) {
8205
+ const rawSweep = rightAzimuth - leftAzimuth;
8206
+ const sweep = normalizeDegrees(rawSweep);
8207
+ if (collapseToleranceDegrees > 0 && Math.min(sweep, 360 - sweep) < collapseToleranceDegrees) return 0;
8208
+ return sweep === 0 && rawSweep !== 0 ? 360 : sweep;
8209
+ }
8210
+ /** Samples an arc of `sweepDegrees` at `radius`, starting from `leftAzimuth`. */
8211
+ function sampleArc$1(anchor, radius, leftAzimuth, sweepDegrees, resolution) {
8212
+ const segmentCount = Math.max(1, Math.ceil(sweepDegrees / 360 * resolution));
8213
+ return Array.from({ length: segmentCount + 1 }, (_, index) => {
8214
+ return destinationPoint(anchor, radius, (leftAzimuth + index / segmentCount * sweepDegrees) * Math.PI / 180);
8215
+ });
8216
+ }
8217
+ /** Closed ring tracing the outer arc, back along the inner arc, and closed. */
8218
+ function annularSectorRing(anchor, innerRadius, outerRadius, leftAzimuth, sweepDegrees, resolution) {
8219
+ const outerArc = sampleArc$1(anchor, outerRadius, leftAzimuth, sweepDegrees, resolution);
8220
+ const innerArc = sampleArc$1(anchor, innerRadius, leftAzimuth, sweepDegrees, resolution).reverse();
8221
+ return [
8222
+ ...outerArc,
8223
+ ...innerArc,
8224
+ outerArc[0]
8225
+ ];
8226
+ }
8227
+ //#endregion
7865
8228
  //#region src/generators/cm99-generic-graphics/sector.ts
7866
8229
  const DEFAULT_GENERIC_SECTOR_OPTIONS = {
7867
8230
  filled: false,
@@ -7887,17 +8250,6 @@ const GENERIC_SECTOR_METADATA = {
7887
8250
  drawRule: "Sector",
7888
8251
  params: [...FILLED_AREA_PARAMS, ...ARC_RESOLUTION_PARAMS]
7889
8252
  };
7890
- function clockwiseSweepDegrees(leftAzimuth, rightAzimuth) {
7891
- const rawSweep = rightAzimuth - leftAzimuth;
7892
- const sweep = normalizeDegrees(rawSweep);
7893
- return sweep === 0 && rawSweep !== 0 ? 360 : sweep;
7894
- }
7895
- function sampleArc(anchor, radius, leftAzimuth, sweepDegrees, resolution) {
7896
- const segmentCount = Math.max(1, Math.ceil(sweepDegrees / 360 * resolution));
7897
- return Array.from({ length: segmentCount + 1 }, (_, index) => {
7898
- return destinationPoint(anchor, radius, (leftAzimuth + index / segmentCount * sweepDegrees) * Math.PI / 180);
7899
- });
7900
- }
7901
8253
  function createGenericSector(coordinates, options = {}) {
7902
8254
  const anchor = coordinates[0];
7903
8255
  const innerLeftPoint = coordinates[1];
@@ -7906,11 +8258,13 @@ function createGenericSector(coordinates, options = {}) {
7906
8258
  type: "FeatureCollection",
7907
8259
  features: []
7908
8260
  };
7909
- const innerRadius = haversineDistance(anchor, innerLeftPoint);
7910
- const outerRadius = haversineDistance(anchor, outerRightPoint);
8261
+ const firstRadius = haversineDistance(anchor, innerLeftPoint);
8262
+ const secondRadius = haversineDistance(anchor, outerRightPoint);
8263
+ const innerRadius = Math.min(firstRadius, secondRadius);
8264
+ const outerRadius = Math.max(firstRadius, secondRadius);
7911
8265
  const leftAzimuth = normalizeDegrees(sphericalBearing(anchor, innerLeftPoint) * 180 / Math.PI);
7912
8266
  const rightAzimuth = normalizeDegrees(sphericalBearing(anchor, outerRightPoint) * 180 / Math.PI);
7913
- if (outerRadius <= innerRadius || innerRadius < 1e-6 || outerRadius < 1e-6) return {
8267
+ if (outerRadius - innerRadius < 1e-6 || innerRadius < 1e-6) return {
7914
8268
  type: "FeatureCollection",
7915
8269
  features: []
7916
8270
  };
@@ -7919,13 +8273,7 @@ function createGenericSector(coordinates, options = {}) {
7919
8273
  type: "FeatureCollection",
7920
8274
  features: []
7921
8275
  };
7922
- const resolution = normalizeArcResolution(options.resolution);
7923
- const outerArc = sampleArc(anchor, outerRadius, leftAzimuth, sweepDegrees, resolution);
7924
- const ring = [
7925
- ...outerArc,
7926
- ...sampleArc(anchor, innerRadius, leftAzimuth, sweepDegrees, resolution).reverse(),
7927
- outerArc[0]
7928
- ];
8276
+ const ring = annularSectorRing(anchor, innerRadius, outerRadius, leftAzimuth, sweepDegrees, normalizeArcResolution(options.resolution));
7929
8277
  return {
7930
8278
  type: "FeatureCollection",
7931
8279
  features: [{
@@ -8128,6 +8476,114 @@ const GENERIC_TEXT = defineControlMeasure({
8128
8476
  }
8129
8477
  });
8130
8478
  //#endregion
8479
+ //#region src/generators/cm20-maritime-control-areas/radar-search-doctrine.ts
8480
+ const DEFAULT_RADAR_SEARCH_DOCTRINE_OPTIONS = { resolution: 64 };
8481
+ /** Doctrinal RSD colors from the symbol specification. */
8482
+ const RADAR_SEARCH_DOCTRINE_STROKE_COLOR = "rgb(51, 136, 136)";
8483
+ const RADAR_SEARCH_DOCTRINE_FILL_COLOR = "rgba(51, 136, 136, 0.25)";
8484
+ const RADAR_SEARCH_DOCTRINE_METADATA = {
8485
+ id: "radar-search-doctrine",
8486
+ name: "Radar Search Doctrine",
8487
+ description: "A maritime radar search area defined by an axis and annular-sector limits.",
8488
+ entity: "Maritime Control Areas",
8489
+ entityType: "Radar Search Doctrine",
8490
+ value: "200700",
8491
+ minCoordinates: 3,
8492
+ maxCoordinates: 3,
8493
+ geometry: "area",
8494
+ geometryTypes: ["Polygon", "Point"],
8495
+ paints: {
8496
+ stroke: true,
8497
+ fill: "fixed",
8498
+ text: true
8499
+ },
8500
+ drawRule: "Point18",
8501
+ capturesLabelSize: true,
8502
+ params: ARC_RESOLUTION_PARAMS,
8503
+ textAmplifiers: [{
8504
+ key: "T",
8505
+ label: "Unique designation",
8506
+ description: "Field T — designation centered in the search area along its axis.",
8507
+ placeholder: "FF",
8508
+ maxLength: 20
8509
+ }]
8510
+ };
8511
+ /**
8512
+ * GeoJSON positions are rounded by `destinationPoint`, so two handles placed on
8513
+ * the same ray can recover bearings a few thousandths of a degree apart. Treat
8514
+ * that numerical noise as a collapsed sector, not an almost-360° fan.
8515
+ */
8516
+ const SWEEP_COLLAPSE_TOLERANCE_DEGREES = .01;
8517
+ function createRadarSearchDoctrine(coordinates, options = {}, textAmplifiers = {}) {
8518
+ const anchor = coordinates[0];
8519
+ const startLimit = coordinates[1];
8520
+ const stopLimit = coordinates[2];
8521
+ const firstRange = haversineDistance(anchor, startLimit);
8522
+ const secondRange = haversineDistance(anchor, stopLimit);
8523
+ const startRange = Math.min(firstRange, secondRange);
8524
+ const stopRange = Math.max(firstRange, secondRange);
8525
+ const leftAzimuth = normalizeDegrees(sphericalBearing(anchor, startLimit) * 180 / Math.PI);
8526
+ const sweepDegrees = clockwiseSweepDegrees(leftAzimuth, normalizeDegrees(sphericalBearing(anchor, stopLimit) * 180 / Math.PI), SWEEP_COLLAPSE_TOLERANCE_DEGREES);
8527
+ if (stopRange - startRange < 1e-6 || sweepDegrees < 1e-6) return {
8528
+ type: "FeatureCollection",
8529
+ features: []
8530
+ };
8531
+ const axis = normalizeDegrees(leftAzimuth + sweepDegrees / 2) * Math.PI / 180;
8532
+ const ring = annularSectorRing(anchor, startRange, stopRange, leftAzimuth, sweepDegrees, normalizeArcResolution(options.resolution));
8533
+ const features = [{
8534
+ type: "Feature",
8535
+ properties: {
8536
+ part: "search-area",
8537
+ fill: true,
8538
+ style: {
8539
+ strokeColor: RADAR_SEARCH_DOCTRINE_STROKE_COLOR,
8540
+ fillColor: RADAR_SEARCH_DOCTRINE_FILL_COLOR,
8541
+ fillPattern: "solid"
8542
+ }
8543
+ },
8544
+ geometry: {
8545
+ type: "Polygon",
8546
+ coordinates: [ring]
8547
+ }
8548
+ }];
8549
+ if (textAmplifiers.T) {
8550
+ const labelAnchor = destinationPoint(anchor, (startRange + stopRange) / 2, axis);
8551
+ features.push({
8552
+ type: "Feature",
8553
+ properties: {
8554
+ part: "label",
8555
+ text: textAmplifiers.T,
8556
+ amplifierField: "T",
8557
+ labelPlacementKey: "T",
8558
+ ...labelSizeProps(options),
8559
+ rotation: labelRotationAlong([Math.cos(axis), -Math.sin(axis)])
8560
+ },
8561
+ geometry: {
8562
+ type: "Point",
8563
+ coordinates: labelAnchor
8564
+ }
8565
+ });
8566
+ }
8567
+ return {
8568
+ type: "FeatureCollection",
8569
+ features
8570
+ };
8571
+ }
8572
+ const RADAR_SEARCH_DOCTRINE = defineControlMeasure({
8573
+ metadata: RADAR_SEARCH_DOCTRINE_METADATA,
8574
+ generator: createRadarSearchDoctrine,
8575
+ defaultOptions: DEFAULT_RADAR_SEARCH_DOCTRINE_OPTIONS,
8576
+ rule: dynamicPointDrawRule,
8577
+ previewSample: {
8578
+ controlPoints: [
8579
+ [0, -1],
8580
+ [-.55, -.15],
8581
+ [1, .45]
8582
+ ],
8583
+ textAmplifiers: { T: "FF" }
8584
+ }
8585
+ });
8586
+ //#endregion
8131
8587
  //#region src/generators/cm34-mission-tasks/clear.ts
8132
8588
  /**
8133
8589
  * Default options for the CLEAR symbol.
@@ -8467,27 +8923,6 @@ const COUNTERATTACK = defineControlMeasure({
8467
8923
  }
8468
8924
  });
8469
8925
  //#endregion
8470
- //#region src/internal/arrowhead.ts
8471
- /**
8472
- * Builds a closed, filled arrowhead triangle ring in unprojected coordinates.
8473
- *
8474
- * `tip` is the point of the arrow, `dir` the unit direction the arrow points,
8475
- * `length` the distance from tip back to the base, and `width` the base width.
8476
- * The returned ring is `[left, tip, right, left]`.
8477
- */
8478
- function createArrowHeadRing(tip, dir, length, width) {
8479
- const perp = [-dir[1], dir[0]];
8480
- const base = vecSub(tip, vecScale(dir, length));
8481
- const left = vecAdd(base, vecScale(perp, width / 2));
8482
- const right = vecSub(base, vecScale(perp, width / 2));
8483
- return [
8484
- unproject(left[0], left[1]),
8485
- unproject(tip[0], tip[1]),
8486
- unproject(right[0], right[1]),
8487
- unproject(left[0], left[1])
8488
- ];
8489
- }
8490
- //#endregion
8491
8926
  //#region src/generators/cm34-mission-tasks/counterattack-by-fire.ts
8492
8927
  const DEFAULT_COUNTERATTACK_BY_FIRE_OPTIONS = { ...DEFAULT_COUNTERATTACK_OPTIONS };
8493
8928
  /** Length of the solid fire arrowhead relative to the recessed P1 extension. */
@@ -9699,7 +10134,7 @@ const DROP_ZONE_METADATA = {
9699
10134
  * (Field N) ENY marker, per {@link labeledAreaFeatures}.
9700
10135
  */
9701
10136
  function createDropZone(positions, options = {}, textAmplifiers = {}, context = {}) {
9702
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "DZ"), context.amplifierPlacements);
10137
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "DZ"), context.amplifierPlacements, context);
9703
10138
  }
9704
10139
  const DROP_ZONE = defineControlMeasure({
9705
10140
  metadata: DROP_ZONE_METADATA,
@@ -9835,7 +10270,7 @@ function createEncirclement(positions, options = {}, textAmplifiers = {}, contex
9835
10270
  const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$2);
9836
10271
  const perimeter = buildAreaPerimeter(verts);
9837
10272
  const labelFeatures = [];
9838
- const gaps = pushAreaLabels(labelFeatures, verts, { hostile: textAmplifiers.N }, options, context.amplifierPlacements);
10273
+ const gaps = pushAreaLabels(labelFeatures, verts, { hostile: textAmplifiers.N }, options, context.amplifierPlacements, void 0, context);
9839
10274
  const radius = meanRadius(verts);
9840
10275
  const barbLength = radius * Math.max(0, barbLengthRatio);
9841
10276
  const barbs = buildBarbs(perimeter, radius * Math.max(0, barbSpacingRatio), barbLength, gaps);
@@ -9916,7 +10351,7 @@ const EXTRACTION_ZONE_METADATA = {
9916
10351
  * an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
9917
10352
  */
9918
10353
  function createExtractionZone(positions, options = {}, textAmplifiers = {}, context = {}) {
9919
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "EZ"), context.amplifierPlacements);
10354
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "EZ"), context.amplifierPlacements, context);
9920
10355
  }
9921
10356
  const EXTRACTION_ZONE = defineControlMeasure({
9922
10357
  metadata: EXTRACTION_ZONE_METADATA,
@@ -10588,7 +11023,7 @@ function processSegment(p1, p2, radius, arcSegments, isFirstSegment) {
10588
11023
  * );
10589
11024
  * ```
10590
11025
  */
10591
- function createFLOT(positions, options = {}, textAmplifiers = {}) {
11026
+ function createFLOT(positions, options = {}, textAmplifiers = {}, context = {}) {
10592
11027
  const { radius = DEFAULT_RADIUS, radiusPixels, metersPerPixel, arcSegments = DEFAULT_ARC_SEGMENTS, smooth = DEFAULT_FLOT_OPTIONS.smooth, smoothResolution = DEFAULT_FLOT_OPTIONS.smoothResolution } = options;
10593
11028
  let effectiveRadius = radius;
10594
11029
  if (radiusPixels !== void 0 && metersPerPixel !== void 0 && metersPerPixel > 0) effectiveRadius = radiusPixels * metersPerPixel;
@@ -10626,7 +11061,7 @@ function createFLOT(positions, options = {}, textAmplifiers = {}) {
10626
11061
  }
10627
11062
  };
10628
11063
  const labels = [];
10629
- pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options);
11064
+ pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options, "", context);
10630
11065
  return {
10631
11066
  type: "FeatureCollection",
10632
11067
  features: [feature, ...labels]
@@ -10695,14 +11130,14 @@ const PHASE_LINE_METADATA = {
10695
11130
  * Creates a Phase line control measure: a polyline through `positions`
10696
11131
  * labeled at each end with `T` (optionally "PL"-prefixed).
10697
11132
  */
10698
- function createPhaseLine(positions, options = {}, textAmplifiers = {}) {
11133
+ function createPhaseLine(positions, options = {}, textAmplifiers = {}, context = {}) {
10699
11134
  const { includePrefix = true, ...sizeOptions } = options;
10700
11135
  const designation = textAmplifiers.T ?? "";
10701
11136
  const text = includePrefix ? designation.length > 0 ? `PL ${designation}` : "PL" : designation;
10702
11137
  return fixedLabelLineFeatures(positions, {
10703
11138
  part: "phase-line",
10704
11139
  ...sizeOptions
10705
- }, text, textAmplifiers.N);
11140
+ }, text, textAmplifiers.N, context);
10706
11141
  }
10707
11142
  const PHASE_LINE = defineControlMeasure({
10708
11143
  metadata: PHASE_LINE_METADATA,
@@ -10774,12 +11209,12 @@ const BRIDGEHEAD_LINE_METADATA = {
10774
11209
  textAmplifiers: [HOSTILE_LINE_AMPLIFIER]
10775
11210
  };
10776
11211
  /** Creates a Bridgehead line labeled "BL" above both ends. */
10777
- function createBridgeheadLine(positions, options = {}, textAmplifiers = {}) {
11212
+ function createBridgeheadLine(positions, options = {}, textAmplifiers = {}, context = {}) {
10778
11213
  return fixedLabelLineFeatures(positions, {
10779
11214
  part: "bridgehead-line",
10780
11215
  placement: "above",
10781
11216
  ...options
10782
- }, "BL", textAmplifiers.N);
11217
+ }, "BL", textAmplifiers.N, context);
10783
11218
  }
10784
11219
  const BRIDGEHEAD_LINE = defineControlMeasure({
10785
11220
  metadata: BRIDGEHEAD_LINE_METADATA,
@@ -10850,12 +11285,12 @@ const HOLDING_LINE_METADATA = {
10850
11285
  textAmplifiers: [HOSTILE_LINE_AMPLIFIER]
10851
11286
  };
10852
11287
  /** Creates a Holding line labeled "HL" above both ends. */
10853
- function createHoldingLine(positions, options = {}, textAmplifiers = {}) {
11288
+ function createHoldingLine(positions, options = {}, textAmplifiers = {}, context = {}) {
10854
11289
  return fixedLabelLineFeatures(positions, {
10855
11290
  part: "holding-line",
10856
11291
  placement: "above",
10857
11292
  ...options
10858
- }, "HL", textAmplifiers.N);
11293
+ }, "HL", textAmplifiers.N, context);
10859
11294
  }
10860
11295
  const HOLDING_LINE = defineControlMeasure({
10861
11296
  metadata: HOLDING_LINE_METADATA,
@@ -10926,12 +11361,12 @@ const RELEASE_LINE_METADATA = {
10926
11361
  textAmplifiers: [HOSTILE_LINE_AMPLIFIER]
10927
11362
  };
10928
11363
  /** Creates a Release line labeled "RL" above both ends. */
10929
- function createReleaseLine(positions, options = {}, textAmplifiers = {}) {
11364
+ function createReleaseLine(positions, options = {}, textAmplifiers = {}, context = {}) {
10930
11365
  return fixedLabelLineFeatures(positions, {
10931
11366
  part: "release-line",
10932
11367
  placement: "above",
10933
11368
  ...options
10934
- }, "RL", textAmplifiers.N);
11369
+ }, "RL", textAmplifiers.N, context);
10935
11370
  }
10936
11371
  const RELEASE_LINE = defineControlMeasure({
10937
11372
  metadata: RELEASE_LINE_METADATA,
@@ -11005,12 +11440,12 @@ const FORWARD_EDGE_OF_BATTLE_AREA_METADATA = {
11005
11440
  * Creates a Forward edge of the battle area control measure: a polyline
11006
11441
  * through `positions` labeled "FEBA" above the line near each end.
11007
11442
  */
11008
- function createForwardEdgeOfBattleArea(positions, options = {}, textAmplifiers = {}) {
11443
+ function createForwardEdgeOfBattleArea(positions, options = {}, textAmplifiers = {}, context = {}) {
11009
11444
  return fixedLabelLineFeatures(positions, {
11010
11445
  part: "forward-edge-of-battle-area",
11011
11446
  placement: "above",
11012
11447
  ...options
11013
- }, "FEBA", textAmplifiers.N);
11448
+ }, "FEBA", textAmplifiers.N, context);
11014
11449
  }
11015
11450
  const FORWARD_EDGE_OF_BATTLE_AREA = defineControlMeasure({
11016
11451
  metadata: FORWARD_EDGE_OF_BATTLE_AREA_METADATA,
@@ -11084,12 +11519,12 @@ const HANDOVER_LINE_METADATA = {
11084
11519
  * Creates a Handover line control measure: a polyline through `positions`
11085
11520
  * labeled "HOL" above the line near each end.
11086
11521
  */
11087
- function createHandoverLine(positions, options = {}, textAmplifiers = {}) {
11522
+ function createHandoverLine(positions, options = {}, textAmplifiers = {}, context = {}) {
11088
11523
  return fixedLabelLineFeatures(positions, {
11089
11524
  part: "handover-line",
11090
11525
  placement: "above",
11091
11526
  ...options
11092
- }, "HOL", textAmplifiers.N);
11527
+ }, "HOL", textAmplifiers.N, context);
11093
11528
  }
11094
11529
  const HANDOVER_LINE = defineControlMeasure({
11095
11530
  metadata: HANDOVER_LINE_METADATA,
@@ -11163,12 +11598,12 @@ const BATTLE_HANDOVER_LINE_METADATA = {
11163
11598
  * Creates a Battle handover line control measure: a polyline through
11164
11599
  * `positions` labeled "BHL" above the line near each end.
11165
11600
  */
11166
- function createBattleHandoverLine(positions, options = {}, textAmplifiers = {}) {
11601
+ function createBattleHandoverLine(positions, options = {}, textAmplifiers = {}, context = {}) {
11167
11602
  return fixedLabelLineFeatures(positions, {
11168
11603
  part: "battle-handover-line",
11169
11604
  placement: "above",
11170
11605
  ...options
11171
- }, "BHL", textAmplifiers.N);
11606
+ }, "BHL", textAmplifiers.N, context);
11172
11607
  }
11173
11608
  const BATTLE_HANDOVER_LINE = defineControlMeasure({
11174
11609
  metadata: BATTLE_HANDOVER_LINE_METADATA,
@@ -11320,7 +11755,7 @@ function generateFortifiedPoints(projectedPoints, effectiveSize) {
11320
11755
  * @param options - Configuration options for the graphic
11321
11756
  * @returns A GeoJSON FeatureCollection containing the castellated LineString
11322
11757
  */
11323
- function createFortifiedLine(positions, options = {}, textAmplifiers = {}) {
11758
+ function createFortifiedLine(positions, options = {}, textAmplifiers = {}, context = {}) {
11324
11759
  const { smooth = DEFAULT_FORTIFIED_LINE_OPTIONS.smooth, smoothResolution = DEFAULT_FORTIFIED_LINE_OPTIONS.smoothResolution } = options;
11325
11760
  const safeSize = calculateEffectiveSize(options);
11326
11761
  const projectedPoints = positions.map((p) => project(p[0], p[1]));
@@ -11334,7 +11769,7 @@ function createFortifiedLine(positions, options = {}, textAmplifiers = {}) {
11334
11769
  }
11335
11770
  };
11336
11771
  const labels = [];
11337
- pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options);
11772
+ pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options, "", context);
11338
11773
  return {
11339
11774
  type: "FeatureCollection",
11340
11775
  features: [feature, ...labels]
@@ -11418,7 +11853,7 @@ function createFortifiedArea(positions, options = {}, textAmplifiers = {}, conte
11418
11853
  }, options, context.amplifierPlacements, {
11419
11854
  ringVerts: boundaryPoints,
11420
11855
  extraClearanceMeters: 3 * safeSize
11421
- })),
11856
+ }, context)),
11422
11857
  properties: {}
11423
11858
  }];
11424
11859
  features.push(...labelFeatures);
@@ -12343,7 +12778,7 @@ const JOINT_TACTICAL_ACTION_AREA_METADATA = {
12343
12778
  * {@link labeledAreaFeatures}.
12344
12779
  */
12345
12780
  function createJointTacticalActionArea(positions, options = {}, textAmplifiers = {}, context = {}) {
12346
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "JTAA -", ""), context.amplifierPlacements);
12781
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "JTAA -", ""), context.amplifierPlacements, context);
12347
12782
  }
12348
12783
  const JOINT_TACTICAL_ACTION_AREA = defineControlMeasure({
12349
12784
  metadata: JOINT_TACTICAL_ACTION_AREA_METADATA,
@@ -12402,7 +12837,7 @@ const LANDING_ZONE_METADATA = {
12402
12837
  * amplifier rows, per {@link labeledAreaFeatures}.
12403
12838
  */
12404
12839
  function createLandingZone(positions, options = {}, textAmplifiers = {}, context = {}) {
12405
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "LZ"), context.amplifierPlacements);
12840
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "LZ"), context.amplifierPlacements, context);
12406
12841
  }
12407
12842
  const LANDING_ZONE = defineControlMeasure({
12408
12843
  metadata: LANDING_ZONE_METADATA,
@@ -12473,7 +12908,7 @@ function createLimitedAccessArea(positions, options = {}, textAmplifiers = {}, c
12473
12908
  }];
12474
12909
  return {
12475
12910
  type: "FeatureCollection",
12476
- features: [...patternedAreaFeatures(verts, pushAreaLabels(labelFeatures, verts, { hostile: textAmplifiers.N }, options, context.amplifierPlacements), "reverse-hatch"), ...labelFeatures]
12911
+ features: [...patternedAreaFeatures(verts, pushAreaLabels(labelFeatures, verts, { hostile: textAmplifiers.N }, options, context.amplifierPlacements, void 0, context), "reverse-hatch"), ...labelFeatures]
12477
12912
  };
12478
12913
  }
12479
12914
  const LIMITED_ACCESS_AREA = defineControlMeasure({
@@ -12584,7 +13019,7 @@ function createNoFireAreaIrregular(positions, options = {}, textAmplifiers = {},
12584
13019
  }
12585
13020
  return {
12586
13021
  type: "FeatureCollection",
12587
- features: [...patternedAreaFeatures(verts, pushAreaLabels(labels, verts, { hostile: textAmplifiers.N }, options, context.amplifierPlacements), "reverse-hatch"), ...labels]
13022
+ features: [...patternedAreaFeatures(verts, pushAreaLabels(labels, verts, { hostile: textAmplifiers.N }, options, context.amplifierPlacements, void 0, context), "reverse-hatch"), ...labels]
12588
13023
  };
12589
13024
  }
12590
13025
  const NO_FIRE_AREA_IRREGULAR = defineControlMeasure({
@@ -12929,7 +13364,7 @@ function createMinefield(coordinates, options = {}, textAmplifiers = {}, context
12929
13364
  clearance: ENY_CLEARANCE_RATIO * labelSize,
12930
13365
  sizeProps,
12931
13366
  followPlacementRotation: true
12932
- }, context.amplifierPlacements);
13367
+ }, context.amplifierPlacements, context, { labelSize });
12933
13368
  if (textAmplifiers.W) pushLabel(labelFeatures, offset(center, 0, -halfHeight - width * LABEL_CLEARANCE_RATIO), textAmplifiers.W, labelRotation, sizeProps, void 0, "W");
12934
13369
  const features = [{
12935
13370
  type: "Feature",
@@ -13146,7 +13581,7 @@ function createMineArea(positions, options, minedArea, textAmplifiers, context)
13146
13581
  textHeight: resolveLabelOffsetMeters(options, 1),
13147
13582
  clearance: resolveLabelOffsetMeters(options, .12 + labelPadding),
13148
13583
  sizeProps: labelSizeProps(options)
13149
- }) : [], ...pushAreaLabels(labelFeatures, ring, { hostile: textAmplifiers.N }, options, context.amplifierPlacements)], mineFillPattern(mineType)), ...labelFeatures]
13584
+ }, void 0, context, options) : [], ...pushAreaLabels(labelFeatures, ring, { hostile: textAmplifiers.N }, options, context.amplifierPlacements, void 0, context)], mineFillPattern(mineType)), ...labelFeatures]
13150
13585
  };
13151
13586
  }
13152
13587
  function createDynamicMinefield(positions, options = {}, textAmplifiers = {}, context = {}) {
@@ -13649,7 +14084,7 @@ const PICKUP_ZONE_METADATA = {
13649
14084
  * amplifier rows, per {@link labeledAreaFeatures}.
13650
14085
  */
13651
14086
  function createPickupZone(positions, options = {}, textAmplifiers = {}, context = {}) {
13652
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "PZ"), context.amplifierPlacements);
14087
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "PZ"), context.amplifierPlacements, context);
13653
14088
  }
13654
14089
  const PICKUP_ZONE = defineControlMeasure({
13655
14090
  metadata: PICKUP_ZONE_METADATA,
@@ -13704,7 +14139,7 @@ const ASSAULT_POSITION_METADATA = {
13704
14139
  * an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
13705
14140
  */
13706
14141
  function createAssaultPosition(positions, options = {}, textAmplifiers = {}, context = {}) {
13707
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "ASLT"), context.amplifierPlacements);
14142
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "ASLT"), context.amplifierPlacements, context);
13708
14143
  }
13709
14144
  const ASSAULT_POSITION = defineControlMeasure({
13710
14145
  metadata: ASSAULT_POSITION_METADATA,
@@ -13759,7 +14194,7 @@ const ATTACK_POSITION_METADATA = {
13759
14194
  * an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
13760
14195
  */
13761
14196
  function createAttackPosition(positions, options = {}, textAmplifiers = {}, context = {}) {
13762
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "ATK"), context.amplifierPlacements);
14197
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "ATK"), context.amplifierPlacements, context);
13763
14198
  }
13764
14199
  const ATTACK_POSITION = defineControlMeasure({
13765
14200
  metadata: ATTACK_POSITION_METADATA,
@@ -13814,7 +14249,7 @@ const OBJECTIVE_AREA_METADATA = {
13814
14249
  * an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
13815
14250
  */
13816
14251
  function createObjectiveArea(positions, options = {}, textAmplifiers = {}, context = {}) {
13817
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "OBJ"), context.amplifierPlacements);
14252
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "OBJ"), context.amplifierPlacements, context);
13818
14253
  }
13819
14254
  const OBJECTIVE_AREA = defineControlMeasure({
13820
14255
  metadata: OBJECTIVE_AREA_METADATA,
@@ -14097,7 +14532,7 @@ function createStrongPoint(positions, options = {}, textAmplifiers = {}, context
14097
14532
  const enyGaps = pushAreaLabels(labelFeatures, verts, {
14098
14533
  name: textAmplifiers.T,
14099
14534
  hostile: textAmplifiers.N
14100
- }, options, context.amplifierPlacements);
14535
+ }, options, context.amplifierPlacements, void 0, context);
14101
14536
  const allGaps = [...gaps, ...enyGaps];
14102
14537
  const boundaryCoords = buildGappedLine(verts, allGaps);
14103
14538
  const tics = buildStrongPointTics(perimeter, allGaps, h);
@@ -14166,6 +14601,180 @@ const STRONG_POINT = defineControlMeasure({
14166
14601
  }
14167
14602
  });
14168
14603
  //#endregion
14604
+ //#region src/generators/cm15-maneuver-areas/retain.ts
14605
+ const DEFAULT_RETAIN_OPTIONS = {
14606
+ openingAngle: 30,
14607
+ arrowheadLengthRatio: .1,
14608
+ arrowheadWidthRatio: .12,
14609
+ labelPadding: 0
14610
+ };
14611
+ const RETAIN_METADATA = {
14612
+ id: "retain",
14613
+ name: "Retain",
14614
+ description: "Ensures that a terrain feature controlled by a friendly force remains secure.",
14615
+ entity: "Maneuver Areas",
14616
+ entityType: "Battle Position",
14617
+ entitySubtype: "Retain",
14618
+ value: "151205",
14619
+ minCoordinates: 2,
14620
+ maxCoordinates: 2,
14621
+ geometry: "area",
14622
+ geometryTypes: ["MultiLineString", "Point"],
14623
+ paints: {
14624
+ stroke: true,
14625
+ fill: "none",
14626
+ text: true
14627
+ },
14628
+ drawRule: "Area6",
14629
+ capturesLabelSize: true,
14630
+ params: [
14631
+ {
14632
+ key: "openingAngle",
14633
+ presentationTier: "advanced",
14634
+ label: "Opening",
14635
+ description: "Angular opening on the friendly side, in degrees",
14636
+ type: "number",
14637
+ min: 5,
14638
+ max: 120,
14639
+ step: 1
14640
+ },
14641
+ {
14642
+ key: "arrowheadLengthRatio",
14643
+ presentationTier: "advanced",
14644
+ label: "Arrowhead length",
14645
+ description: "Open-arrowhead length as a ratio of the symbol radius",
14646
+ type: "number",
14647
+ min: .02,
14648
+ max: .5,
14649
+ step: .01
14650
+ },
14651
+ {
14652
+ key: "arrowheadWidthRatio",
14653
+ presentationTier: "advanced",
14654
+ label: "Arrowhead width",
14655
+ description: "Open-arrowhead base width as a ratio of the symbol radius",
14656
+ type: "number",
14657
+ min: .02,
14658
+ max: .5,
14659
+ step: .01
14660
+ },
14661
+ {
14662
+ key: "labelPadding",
14663
+ presentationTier: "advanced",
14664
+ label: "R padding",
14665
+ description: "Extra boundary clearance on each side of R, as a ratio of its height",
14666
+ type: "number",
14667
+ min: 0,
14668
+ max: 2,
14669
+ step: .05
14670
+ }
14671
+ ]
14672
+ };
14673
+ const TAU = Math.PI * 2;
14674
+ /** Smallest visible boundary gap, in projected meters, for tiny label heights. */
14675
+ const MIN_LABEL_GAP_METERS = 1;
14676
+ function pointOnCircle(center, radius, angle) {
14677
+ return vecAdd(center, [radius * Math.cos(angle), radius * Math.sin(angle)]);
14678
+ }
14679
+ function sampleArc(center, radius, start, end) {
14680
+ const segments = Math.max(8, Math.ceil(48 * Math.abs(end - start) / Math.PI));
14681
+ return Array.from({ length: segments + 1 }, (_, index) => {
14682
+ const point = pointOnCircle(center, radius, start + (end - start) * index / segments);
14683
+ return unproject(point[0], point[1]);
14684
+ });
14685
+ }
14686
+ /** Creates the Retain boundary, outward tics, and intrinsic R label. */
14687
+ function createRetain(coordinates, options = {}) {
14688
+ const center = project(coordinates[0][0], coordinates[0][1]);
14689
+ const radiusVector = vecSub(project(coordinates[1][0], coordinates[1][1]), center);
14690
+ const radius = vecMag(radiusVector);
14691
+ if (radius < 1e-6) return {
14692
+ type: "FeatureCollection",
14693
+ features: []
14694
+ };
14695
+ const resolved = {
14696
+ ...DEFAULT_RETAIN_OPTIONS,
14697
+ ...options
14698
+ };
14699
+ const startAngle = Math.atan2(radiusVector[1], radiusVector[0]);
14700
+ const opening = Math.min(120, Math.max(5, resolved.openingAngle)) * Math.PI / 180;
14701
+ const h = resolveLabelOffsetMeters(options, 1, radius * .1);
14702
+ const labelPadding = Math.max(0, resolved.labelPadding);
14703
+ const boundarySpan = TAU - opening;
14704
+ const labelDistance = Math.PI - opening / 2;
14705
+ const labelCenter = startAngle - labelDistance;
14706
+ const labelHalfGap = Math.min(Math.PI / 2, Math.max(h * (LABEL_HALF_GLYPH_RATIO + labelPadding), MIN_LABEL_GAP_METERS) / radius);
14707
+ const labelStart = startAngle - labelDistance + labelHalfGap;
14708
+ const labelEnd = startAngle - labelDistance - labelHalfGap;
14709
+ const boundaryEnd = startAngle - boundarySpan;
14710
+ const arrowTip = pointOnCircle(center, radius, boundaryEnd);
14711
+ const arrowheadLength = radius * Math.max(0, resolved.arrowheadLengthRatio);
14712
+ const arrowheadWidth = radius * Math.max(0, resolved.arrowheadWidthRatio);
14713
+ const boundary = [
14714
+ sampleArc(center, radius, startAngle, labelStart),
14715
+ sampleArc(center, radius, labelEnd, boundaryEnd),
14716
+ createArrowHeadRing(arrowTip, [Math.sin(boundaryEnd), -Math.cos(boundaryEnd)], arrowheadLength, arrowheadWidth).slice(0, 3)
14717
+ ];
14718
+ const tics = [];
14719
+ const spacingAngle = Math.max(h / radius, Math.PI / 180);
14720
+ const arrowClearanceAngle = (arrowheadLength + arrowheadWidth / 2 + h / 2) / radius;
14721
+ for (let distance = spacingAngle / 2; distance < boundarySpan - arrowClearanceAngle; distance += spacingAngle) {
14722
+ if (distance > labelDistance - labelHalfGap && distance < labelDistance + labelHalfGap) continue;
14723
+ const angle = startAngle - distance;
14724
+ const cos = Math.cos(angle);
14725
+ const sin = Math.sin(angle);
14726
+ const innerX = center[0] + radius * cos;
14727
+ const innerY = center[1] + radius * sin;
14728
+ tics.push([unproject(innerX, innerY), unproject(innerX + h * cos, innerY + h * sin)]);
14729
+ }
14730
+ const labelPoint = pointOnCircle(center, radius, labelCenter);
14731
+ return {
14732
+ type: "FeatureCollection",
14733
+ features: [
14734
+ {
14735
+ type: "Feature",
14736
+ properties: { part: "boundary" },
14737
+ geometry: {
14738
+ type: "MultiLineString",
14739
+ coordinates: boundary
14740
+ }
14741
+ },
14742
+ {
14743
+ type: "Feature",
14744
+ properties: { part: "tics" },
14745
+ geometry: {
14746
+ type: "MultiLineString",
14747
+ coordinates: tics
14748
+ }
14749
+ },
14750
+ {
14751
+ type: "Feature",
14752
+ properties: {
14753
+ part: "label",
14754
+ labelPlacement: false,
14755
+ text: "R",
14756
+ rotation: Math.PI,
14757
+ ...labelSizeProps(options)
14758
+ },
14759
+ geometry: {
14760
+ type: "Point",
14761
+ coordinates: unproject(labelPoint[0], labelPoint[1])
14762
+ }
14763
+ }
14764
+ ]
14765
+ };
14766
+ }
14767
+ const RETAIN = defineControlMeasure({
14768
+ metadata: RETAIN_METADATA,
14769
+ generator: createRetain,
14770
+ defaultOptions: DEFAULT_RETAIN_OPTIONS,
14771
+ rule: retainDrawRule,
14772
+ previewSample: {
14773
+ controlPoints: [[0, 0], [-.8, .2]],
14774
+ options: { labelSize: 90 }
14775
+ }
14776
+ });
14777
+ //#endregion
14169
14778
  //#region src/generators/cm15-maneuver-areas/submarineActionArea.ts
14170
14779
  /** Default options for the Submarine action area control measure. */
14171
14780
  const DEFAULT_SUBMARINE_ACTION_AREA_OPTIONS = {
@@ -14203,7 +14812,7 @@ const SUBMARINE_ACTION_AREA_METADATA = {
14203
14812
  * `W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
14204
14813
  */
14205
14814
  function createSubmarineActionArea(positions, options = {}, textAmplifiers = {}, context = {}) {
14206
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "SAA -", ""), context.amplifierPlacements);
14815
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "SAA -", ""), context.amplifierPlacements, context);
14207
14816
  }
14208
14817
  const SUBMARINE_ACTION_AREA = defineControlMeasure({
14209
14818
  metadata: SUBMARINE_ACTION_AREA_METADATA,
@@ -14263,7 +14872,7 @@ const SUBMARINE_GENERATED_ACTION_AREA_METADATA = {
14263
14872
  * (Field T) plus `W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
14264
14873
  */
14265
14874
  function createSubmarineGeneratedActionArea(positions, options = {}, textAmplifiers = {}, context = {}) {
14266
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "SGSA -", ""), context.amplifierPlacements);
14875
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "SGSA -", ""), context.amplifierPlacements, context);
14267
14876
  }
14268
14877
  const SUBMARINE_GENERATED_ACTION_AREA = defineControlMeasure({
14269
14878
  metadata: SUBMARINE_GENERATED_ACTION_AREA_METADATA,
@@ -14624,7 +15233,9 @@ const DEFINITIONS = {
14624
15233
  "engineer-work-line": ENGINEER_WORK_LINE,
14625
15234
  "generic-c2-line": GENERIC_C2_LINE,
14626
15235
  "battle-position": BATTLE_POSITION,
15236
+ contain: CONTAIN,
14627
15237
  "strong-point": STRONG_POINT,
15238
+ retain: RETAIN,
14628
15239
  ambush: AMBUSH,
14629
15240
  "direction-of-attack-aviation": DIRECTION_OF_ATTACK_AVIATION,
14630
15241
  "direction-of-main-attack": DIRECTION_OF_MAIN_ATTACK,
@@ -14669,6 +15280,7 @@ const DEFINITIONS = {
14669
15280
  circle: GENERIC_CIRCLE,
14670
15281
  sector: GENERIC_SECTOR,
14671
15282
  text: GENERIC_TEXT,
15283
+ "radar-search-doctrine": RADAR_SEARCH_DOCTRINE,
14672
15284
  "airborne-attack": AIRBORNE_ATTACK,
14673
15285
  "attack-helicopter": ATTACK_HELICOPTER,
14674
15286
  "support-by-fire": SUPPORT_BY_FIRE,
@@ -14960,7 +15572,31 @@ function dispatchControlMeasure(cm, opts) {
14960
15572
  if (!validateInputContract(cm.controlPoints, definition.metadata, opts?.validationMode)) return EMPTY_COLLECTION;
14961
15573
  const generator = definition.generator;
14962
15574
  const textAmplifiers = normalizeTextAmplifiers(cm.textAmplifiers);
14963
- return generator(cm.controlPoints, cm.options ?? {}, textAmplifiers, { amplifierPlacements: cm.amplifierPlacements });
15575
+ const constructionMetersPerCssPixel = resolveConstructionScale(cm.controlPoints, opts?.context?.groundMetersPerCssPixel);
15576
+ const authoredOptions = cm.options ?? {};
15577
+ const generatorOptions = constructionMetersPerCssPixel === void 0 ? authoredOptions : {
15578
+ ...authoredOptions,
15579
+ metersPerPixel: constructionMetersPerCssPixel
15580
+ };
15581
+ return generator(cm.controlPoints, generatorOptions, textAmplifiers, {
15582
+ amplifierPlacements: cm.amplifierPlacements,
15583
+ constructionMetersPerCssPixel,
15584
+ labelSizeClampCssPixels: opts?.context?.labelSizeClampCssPixels,
15585
+ measureText: opts?.context?.measureText
15586
+ });
15587
+ }
15588
+ /** Convert true-ground scale to the Web Mercator metres legacy generators construct in. */
15589
+ function resolveConstructionScale(points, groundMetersPerCssPixel) {
15590
+ if (!(groundMetersPerCssPixel !== void 0 && groundMetersPerCssPixel > 0)) return void 0;
15591
+ let minLatitude = Infinity;
15592
+ let maxLatitude = -Infinity;
15593
+ for (const point of points) {
15594
+ minLatitude = Math.min(minLatitude, point[1]);
15595
+ maxLatitude = Math.max(maxLatitude, point[1]);
15596
+ }
15597
+ const midpointLatitude = (minLatitude + maxLatitude) / 2;
15598
+ const mercatorScale = Math.cos(midpointLatitude * Math.PI / 180);
15599
+ return groundMetersPerCssPixel / Math.max(mercatorScale, Number.EPSILON);
14964
15600
  }
14965
15601
  /**
14966
15602
  * The control-point *input contract*: `controlPoints` must be present and an
@@ -15099,4 +15735,4 @@ function assertNever(value) {
15099
15735
  throw new Error(`Unhandled control measure kind: ${String(value)}`);
15100
15736
  }
15101
15737
  //#endregion
15102
- export { DEFAULT_TACTICAL_ARROW_OPTIONS as $, centerRadiusDrawRule as $t, DEFAULT_REARWARD_PASSAGE_OF_LINES_OPTIONS as A, DEFAULT_DIRECTION_OF_MAIN_ATTACK_OPTIONS as At, DEFAULT_BRIDGEHEAD_LINE_OPTIONS as B, axis1DrawRule as Bt, DEFAULT_LANDING_ZONE_OPTIONS as C, DEFAULT_ATTACK_BY_FIRE_OPTIONS as Ct, DEFAULT_SEIZE_OPTIONS as D, DEFAULT_AREA_OF_OPERATIONS_OPTIONS as Dt, DEFAULT_WITHDRAW_OPTIONS as E, DEFAULT_ASSEMBLY_AREA_OPTIONS as Et, DEFAULT_BATTLE_HANDOVER_LINE_OPTIONS as F, resolveAmplifierPlacement as Ft, DEFAULT_FINAL_PROTECTIVE_FIRE_OPTIONS as G, line23DrawRule as Gt, DEFAULT_FLOT_OPTIONS as H, line27DrawRule as Ht, DEFAULT_HANDOVER_LINE_OPTIONS as I, DEFAULT_AMBUSH_OPTIONS as It, DEFAULT_GUARD_OPTIONS as J, ambushDrawRule as Jt, DEFAULT_ENCIRCLEMENT_OPTIONS as K, turnDrawRule as Kt, DEFAULT_FORWARD_EDGE_OF_BATTLE_AREA_OPTIONS as L, DEFAULT_AIRBORNE_ATTACK_OPTIONS as Lt, DEFAULT_FRONTAL_ATTACK_OPTIONS as M, TEXT_AMPLIFIER_FIELDS as Mt, DEFAULT_FORTIFIED_AREA_OPTIONS as N, canonicalTextAmplifierKey as Nt, DEFAULT_SCREEN_OPTIONS as O, DEFAULT_AREA_DEFENSE_OPTIONS as Ot, DEFAULT_FORTIFIED_LINE_OPTIONS as P, normalizeTextAmplifiers as Pt, DEFAULT_COVER_OPTIONS as Q, penetrateDrawRule as Qt, DEFAULT_RELEASE_LINE_OPTIONS as R, sectorDrawRule as Rt, DEFAULT_MAIN_ATTACK_OPTIONS as S, DEFAULT_ATTACK_HELICOPTER_OPTIONS as St, DEFAULT_WITHDRAW_UNDER_PRESSURE_OPTIONS as T, DEFAULT_ANTITANK_DITCH_OPTIONS as Tt, DEFAULT_FIX_MISSION_TASK_OPTIONS as U, line26DrawRule as Ut, DEFAULT_PHASE_LINE_OPTIONS as V, supportByFireDrawRule as Vt, DEFAULT_FIX_OPTIONS as W, line24DrawRule as Wt, DEFAULT_DISENGAGE_OPTIONS as X, staticPointDrawRule as Xt, DEFAULT_DISRUPT_MISSION_TASK_OPTIONS as Y, attackByFireDrawRule as Yt, DEFAULT_DELAY_OPTIONS as Z, point12DrawRule as Zt, DEFAULT_PICKUP_ZONE_OPTIONS as _, DEFAULT_LIGHT_LINE_OPTIONS as _t, DEFINITIONS as a, pointOnMidpointPerpendicularAxis as an, DEFAULT_GENERIC_SECTOR_OPTIONS as at, DEFAULT_OBSTACLE_BYPASS_DIFFICULT_OPTIONS as b, DEFAULT_BLOCK_OPTIONS as bt, getDefaultOptions as c, calculateMetrics as cn, DEFAULT_GENERIC_LINE_OPTIONS as ct, DEFAULT_TURN_OPTIONS as d, project as dn, DEFAULT_CANALIZE_OPTIONS as dt, disruptDrawRule as en, DEFAULT_COUNTERATTACK_BY_FIRE_OPTIONS as et, DEFAULT_SUPPORT_BY_FIRE_OPTIONS as f, sphericalBearing as fn, DEFAULT_BYPASS_OPTIONS as ft, DEFAULT_PRINCIPAL_DIRECTION_OF_FIRE_OPTIONS as g, roundToFixed as gn, DEFAULT_ENGINEER_WORK_LINE_OPTIONS as gt, SECONDARY_DIRECTION_OF_FIRE_DASH as h, getMetersPerPixel as hn, DEFAULT_GENERIC_C2_LINE_OPTIONS as ht, CONTROL_MEASURE_METADATA as i, getMidpointPerpendicularSignedDistance as in, DEFAULT_GENERIC_TEXT_OPTIONS as it, DEFAULT_PENETRATE_OPTIONS as j, DEFAULT_DIRECTION_OF_ATTACK_AVIATION_OPTIONS as jt, DEFAULT_RETIRE_OPTIONS as k, DEFAULT_DIRECTION_OF_SUPPORTING_ATTACK_OPTIONS as kt, listControlMeasureMetadata as l, computeInitialWidthPoint as ln, DEFAULT_GENERIC_CIRCLE_OPTIONS as lt, DEFAULT_SECONDARY_DIRECTION_OF_FIRE_OPTIONS as m, EPSILON as mn, DEFAULT_BLOCK_MISSION_TASK_OPTIONS as mt, resolveStyleHints as n, computeDefaultMidpointPerpendicularPoint as nn, DEFAULT_SUPPORTING_ATTACK_OPTIONS as nt, getControlMeasureMetadata as o, snapToMidpointPerpendicular as on, DEFAULT_GENERIC_RECTANGLE_OPTIONS as ot, DEFAULT_STRONG_POINT_OPTIONS as p, unproject as pn, DEFAULT_BREACH_OPTIONS as pt, DEFAULT_DISRUPT_OPTIONS as q, line1DrawRule as qt, CONTROL_MEASURE_IDS as r, createMidpointPerpendicularDrawRule as rn, DEFAULT_CLEAR_OPTIONS as rt, getControlMeasureMetadataByValue as s, createBaselineFrame as sn, DEFAULT_GENERIC_POLYGON_OPTIONS as st, renderControlMeasure as t, blockDrawRule as tn, DEFAULT_COUNTERATTACK_OPTIONS as tt, DEFAULT_TURNING_MOVEMENT_OPTIONS as u, haversineDistance as un, DEFAULT_CLASSIC_ARROW_OPTIONS as ut, DEFAULT_OBSTACLE_BYPASS_IMPOSSIBLE_OPTIONS as v, DEFAULT_BOUNDARY_OPTIONS as vt, DEFAULT_JOINT_TACTICAL_ACTION_AREA_OPTIONS as w, DEFAULT_ANTITANK_WALL_OPTIONS as wt, DEFAULT_MINEFIELD_OPTIONS as x, DEFAULT_BATTLE_POSITION_OPTIONS as xt, DEFAULT_OBSTACLE_BYPASS_EASY_OPTIONS as y, DEFAULT_BLOCK_ARROW_OPTIONS as yt, DEFAULT_HOLDING_LINE_OPTIONS as z, rectangleDrawRule as zt };
15738
+ export { DEFAULT_COVER_OPTIONS as $, supportByFireDrawRule as $t, DEFAULT_RETIRE_OPTIONS as A, getMetersPerPixel as An, DEFAULT_ANTITANK_DITCH_OPTIONS as At, DEFAULT_HOLDING_LINE_OPTIONS as B, resolveAmplifierPlacement as Bt, DEFAULT_MAIN_ATTACK_OPTIONS as C, calculateMetrics as Cn, DEFAULT_BLOCK_ARROW_OPTIONS as Ct, DEFAULT_WITHDRAW_OPTIONS as D, sphericalBearing as Dn, DEFAULT_ATTACK_HELICOPTER_OPTIONS as Dt, DEFAULT_WITHDRAW_UNDER_PRESSURE_OPTIONS as E, project as En, DEFAULT_BATTLE_POSITION_OPTIONS as Et, DEFAULT_FORTIFIED_LINE_OPTIONS as F, DEFAULT_DIRECTION_OF_MAIN_ATTACK_OPTIONS as Ft, DEFAULT_FIX_OPTIONS as G, DEFAULT_PORTRAYAL as Gt, DEFAULT_PHASE_LINE_OPTIONS as H, DEFAULT_LABEL_SIZE_CLAMP_CSS_PIXELS as Ht, DEFAULT_BATTLE_HANDOVER_LINE_OPTIONS as I, DEFAULT_DIRECTION_OF_ATTACK_AVIATION_OPTIONS as It, DEFAULT_DISRUPT_OPTIONS as J, DEFAULT_SYMBOL_COLOR as Jt, DEFAULT_FINAL_PROTECTIVE_FIRE_OPTIONS as K, DEFAULT_STROKE_DASH_CSS_PIXELS as Kt, DEFAULT_HANDOVER_LINE_OPTIONS as L, TEXT_AMPLIFIER_FIELDS as Lt, DEFAULT_PENETRATE_OPTIONS as M, DEFAULT_AREA_OF_OPERATIONS_OPTIONS as Mt, DEFAULT_FRONTAL_ATTACK_OPTIONS as N, DEFAULT_AREA_DEFENSE_OPTIONS as Nt, DEFAULT_SEIZE_OPTIONS as O, unproject as On, DEFAULT_ATTACK_BY_FIRE_OPTIONS as Ot, DEFAULT_FORTIFIED_AREA_OPTIONS as P, DEFAULT_DIRECTION_OF_SUPPORTING_ATTACK_OPTIONS as Pt, DEFAULT_DELAY_OPTIONS as Q, axis1DrawRule as Qt, DEFAULT_FORWARD_EDGE_OF_BATTLE_AREA_OPTIONS as R, canonicalTextAmplifierKey as Rt, DEFAULT_MINEFIELD_OPTIONS as S, createBaselineFrame as Sn, DEFAULT_BOUNDARY_OPTIONS as St, DEFAULT_JOINT_TACTICAL_ACTION_AREA_OPTIONS as T, haversineDistance as Tn, DEFAULT_CONTAIN_OPTIONS as Tt, DEFAULT_FLOT_OPTIONS as U, DEFAULT_LINE_CAP as Ut, DEFAULT_BRIDGEHEAD_LINE_OPTIONS as V, DEFAULT_LABEL_HEIGHT_CSS_PIXELS as Vt, DEFAULT_FIX_MISSION_TASK_OPTIONS as W, DEFAULT_LINE_JOIN as Wt, DEFAULT_DISRUPT_MISSION_TASK_OPTIONS as X, DEFAULT_AIRBORNE_ATTACK_OPTIONS as Xt, DEFAULT_GUARD_OPTIONS as Y, DEFAULT_AMBUSH_OPTIONS as Yt, DEFAULT_DISENGAGE_OPTIONS as Z, rectangleDrawRule as Zt, DEFAULT_PRINCIPAL_DIRECTION_OF_FIRE_OPTIONS as _, computeDefaultMidpointPerpendicularPoint as _n, DEFAULT_BREACH_OPTIONS as _t, DEFINITIONS as a, line1DrawRule as an, DEFAULT_RADAR_SEARCH_DOCTRINE_OPTIONS as at, DEFAULT_OBSTACLE_BYPASS_EASY_OPTIONS as b, pointOnMidpointPerpendicularAxis as bn, DEFAULT_ENGINEER_WORK_LINE_OPTIONS as bt, getDefaultOptions as c, dynamicPointDrawRule as cn, DEFAULT_GENERIC_TEXT_OPTIONS as ct, DEFAULT_TURN_OPTIONS as d, point12DrawRule as dn, DEFAULT_GENERIC_POLYGON_OPTIONS as dt, line27DrawRule as en, DEFAULT_TACTICAL_ARROW_OPTIONS as et, DEFAULT_SUPPORT_BY_FIRE_OPTIONS as f, penetrateDrawRule as fn, DEFAULT_GENERIC_LINE_OPTIONS as ft, SECONDARY_DIRECTION_OF_FIRE_DASH as g, containDrawRule as gn, DEFAULT_BYPASS_OPTIONS as gt, DEFAULT_SECONDARY_DIRECTION_OF_FIRE_OPTIONS as h, centerRadiusDrawRule as hn, DEFAULT_CANALIZE_OPTIONS as ht, CONTROL_MEASURE_METADATA as i, turnDrawRule as in, DEFAULT_CLEAR_OPTIONS as it, DEFAULT_REARWARD_PASSAGE_OF_LINES_OPTIONS as j, roundToFixed as jn, DEFAULT_ASSEMBLY_AREA_OPTIONS as jt, DEFAULT_SCREEN_OPTIONS as k, EPSILON as kn, DEFAULT_ANTITANK_WALL_OPTIONS as kt, listControlMeasureMetadata as l, sectorDrawRule as ln, DEFAULT_GENERIC_SECTOR_OPTIONS as lt, DEFAULT_STRONG_POINT_OPTIONS as m, blockDrawRule as mn, DEFAULT_CLASSIC_ARROW_OPTIONS as mt, resolveStyleHints as n, line24DrawRule as nn, DEFAULT_COUNTERATTACK_OPTIONS as nt, getControlMeasureMetadata as o, ambushDrawRule as on, RADAR_SEARCH_DOCTRINE_FILL_COLOR as ot, DEFAULT_RETAIN_OPTIONS as p, disruptDrawRule as pn, DEFAULT_GENERIC_CIRCLE_OPTIONS as pt, DEFAULT_ENCIRCLEMENT_OPTIONS as q, DEFAULT_STROKE_WIDTH_CSS_PIXELS as qt, CONTROL_MEASURE_IDS as r, line23DrawRule as rn, DEFAULT_SUPPORTING_ATTACK_OPTIONS as rt, getControlMeasureMetadataByValue as s, attackByFireDrawRule as sn, RADAR_SEARCH_DOCTRINE_STROKE_COLOR as st, renderControlMeasure as t, line26DrawRule as tn, DEFAULT_COUNTERATTACK_BY_FIRE_OPTIONS as tt, DEFAULT_TURNING_MOVEMENT_OPTIONS as u, staticPointDrawRule as un, DEFAULT_GENERIC_RECTANGLE_OPTIONS as ut, DEFAULT_PICKUP_ZONE_OPTIONS as v, createMidpointPerpendicularDrawRule as vn, DEFAULT_BLOCK_MISSION_TASK_OPTIONS as vt, DEFAULT_LANDING_ZONE_OPTIONS as w, computeInitialWidthPoint as wn, DEFAULT_BLOCK_OPTIONS as wt, DEFAULT_OBSTACLE_BYPASS_DIFFICULT_OPTIONS as x, snapToMidpointPerpendicular as xn, DEFAULT_LIGHT_LINE_OPTIONS as xt, DEFAULT_OBSTACLE_BYPASS_IMPOSSIBLE_OPTIONS as y, getMidpointPerpendicularSignedDistance as yn, DEFAULT_GENERIC_C2_LINE_OPTIONS as yt, DEFAULT_RELEASE_LINE_OPTIONS as z, normalizeTextAmplifiers as zt };