@orbat-mapper/control-measures 0.6.0 → 0.7.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.
@@ -689,6 +689,81 @@ const staticPointDrawRule = {
689
689
  id: "point2:static-anchor"
690
690
  };
691
691
  //#endregion
692
+ //#region src/draw-rules/sector.ts
693
+ const SECTOR_ANGLE_SNAP_RADIANS = Math.PI / 36;
694
+ /**
695
+ * Sector — anchor + inner-left edge + outer-right edge. The two radial points
696
+ * independently control a radius and a true-north azimuth. Dragging the anchor
697
+ * translates both edge points so the sector retains its size and orientation.
698
+ */
699
+ function derive$9(points) {
700
+ return clonePositions(points.slice(0, 3));
701
+ }
702
+ function guidePoints$1(points) {
703
+ return points.length < 3 ? clonePositions(points) : [];
704
+ }
705
+ /** Snap one radial Sector handle to a true-north angular increment. */
706
+ function snapSectorAngle(points, activePointIndex, incrementRadians) {
707
+ const next = clonePositions(points);
708
+ if (activePointIndex !== 1 && activePointIndex !== 2) return next;
709
+ const anchor = next[0];
710
+ const point = next[activePointIndex];
711
+ if (!anchor || !point) return next;
712
+ const range = haversineDistance(anchor, point);
713
+ const bearing = sphericalBearing(anchor, point);
714
+ next[activePointIndex] = [...destinationPoint(anchor, range, Math.round(bearing / incrementRadians) * incrementRadians), ...point.slice(2)];
715
+ return next;
716
+ }
717
+ function constrainAngles(points, activePointIndex) {
718
+ return snapSectorAngle(points, activePointIndex, SECTOR_ANGLE_SNAP_RADIANS);
719
+ }
720
+ const sectorDrawRule = {
721
+ id: "sector:anchor-radii",
722
+ minimumUserPoints: 3,
723
+ canonicalPointCount: 3,
724
+ showGuide: true,
725
+ guidePoints: guidePoints$1,
726
+ constrainAngles,
727
+ derive: derive$9,
728
+ transform(event) {
729
+ const { previous, next, activePointIndex } = event;
730
+ if (activePointIndex === 0 && previous.length >= 3 && next.length >= 3) {
731
+ const dx = next[0][0] - previous[0][0];
732
+ const dy = next[0][1] - previous[0][1];
733
+ return [
734
+ clonePosition(next[0]),
735
+ [previous[1][0] + dx, previous[1][1] + dy],
736
+ [previous[2][0] + dx, previous[2][1] + dy]
737
+ ];
738
+ }
739
+ return derive$9(next);
740
+ }
741
+ };
742
+ //#endregion
743
+ //#region src/draw-rules/point18.ts
744
+ const WHOLE_DEGREE_RADIANS = Math.PI / 180;
745
+ function snapToWholeDegrees(points) {
746
+ let snapped = snapSectorAngle(points, 1, WHOLE_DEGREE_RADIANS);
747
+ if (snapped.length >= 3) snapped = snapSectorAngle(snapped, 2, WHOLE_DEGREE_RADIANS);
748
+ return snapped;
749
+ }
750
+ /**
751
+ * Point18 — doctrinally a numeric, single-anchor symbol. The interactive
752
+ * authoring model exposes those dimensions as Sector-style radial handles:
753
+ * P1 is the anchor, P2 fixes the start range/left bearing, and P3 fixes the
754
+ * stop range/right bearing.
755
+ */
756
+ const dynamicPointDrawRule = {
757
+ ...sectorDrawRule,
758
+ id: "point18:anchor-radii",
759
+ derive(points) {
760
+ return snapToWholeDegrees(sectorDrawRule.derive(points));
761
+ },
762
+ transform(event) {
763
+ return snapSectorAngle(sectorDrawRule.transform(event), event.activePointIndex, WHOLE_DEGREE_RADIANS);
764
+ }
765
+ };
766
+ //#endregion
692
767
  //#region src/draw-rules/area7.ts
693
768
  /**
694
769
  * Area7 anchor draw rule for Attack By Fire.
@@ -716,20 +791,20 @@ const ambushDrawRule = createMidpointPerpendicularDrawRule({
716
791
  });
717
792
  //#endregion
718
793
  //#region src/draw-rules/line1.ts
719
- function derive$9(points) {
794
+ function derive$8(points) {
720
795
  return clonePositions(points);
721
796
  }
722
797
  const line1DrawRule = {
723
798
  id: "line1",
724
799
  minimumUserPoints: 2,
725
- derive: derive$9,
800
+ derive: derive$8,
726
801
  transform(event) {
727
- return derive$9(event.next);
802
+ return derive$8(event.next);
728
803
  }
729
804
  };
730
805
  //#endregion
731
806
  //#region src/draw-rules/line3.ts
732
- function derive$8(points) {
807
+ function derive$7(points) {
733
808
  return clonePositions(points);
734
809
  }
735
810
  /**
@@ -744,23 +819,23 @@ const line3DrawRule = {
744
819
  id: "line3",
745
820
  minimumUserPoints: 3,
746
821
  canonicalPointCount: 3,
747
- derive: derive$8,
822
+ derive: derive$7,
748
823
  transform(event) {
749
- return derive$8(event.next);
824
+ return derive$7(event.next);
750
825
  }
751
826
  };
752
827
  //#endregion
753
828
  //#region src/draw-rules/line9.ts
754
- function derive$7(points) {
829
+ function derive$6(points) {
755
830
  return clonePositions(points);
756
831
  }
757
832
  const line9DrawRule = {
758
833
  id: "line9",
759
834
  minimumUserPoints: 2,
760
835
  canonicalPointCount: 2,
761
- derive: derive$7,
836
+ derive: derive$6,
762
837
  transform(event) {
763
- return derive$7(event.next);
838
+ return derive$6(event.next);
764
839
  }
765
840
  };
766
841
  //#endregion
@@ -867,7 +942,7 @@ const line24DrawRule = createMidpointPerpendicularDrawRule({
867
942
  });
868
943
  //#endregion
869
944
  //#region src/draw-rules/line26.ts
870
- function derive$6(points) {
945
+ function derive$5(points) {
871
946
  if (points.length === 2) {
872
947
  const [p1, p2] = points;
873
948
  const dx = p2[0] - p1[0];
@@ -906,9 +981,9 @@ const line26DrawRule = {
906
981
  minimumUserPoints: 4,
907
982
  minimumPreviewPoints: 2,
908
983
  canonicalPointCount: 4,
909
- derive: derive$6,
984
+ derive: derive$5,
910
985
  transform(event) {
911
- return derive$6(event.next);
986
+ return derive$5(event.next);
912
987
  }
913
988
  };
914
989
  //#endregion
@@ -921,7 +996,7 @@ function deriveArcPoint(p1, p2, p4) {
921
996
  if (!arc) return [(p2[0] + p4[0]) / 2, (p2[1] + p4[1]) / 2];
922
997
  return unproject(arc.midpoint[0], arc.midpoint[1]);
923
998
  }
924
- function derive$5(points) {
999
+ function derive$4(points) {
925
1000
  if (points.length < 2) return clonePositions(points);
926
1001
  const [p1, p2] = points;
927
1002
  if (points.length === 2) {
@@ -960,7 +1035,7 @@ const line27DrawRule = {
960
1035
  minimumUserPoints: 3,
961
1036
  minimumPreviewPoints: 2,
962
1037
  canonicalPointCount: 4,
963
- derive: derive$5,
1038
+ derive: derive$4,
964
1039
  transform(event) {
965
1040
  const { previous, next, activePointIndex } = event;
966
1041
  if (activePointIndex === 0 && previous.length >= 4 && next.length >= 4) {
@@ -993,7 +1068,7 @@ const line27DrawRule = {
993
1068
  };
994
1069
  //#endregion
995
1070
  //#region src/draw-rules/area8.ts
996
- function derive$4(points) {
1071
+ function derive$3(points) {
997
1072
  if (points.length < 2) return clonePositions(points);
998
1073
  if (points.length !== 2) return clonePositions(points.slice(0, 4));
999
1074
  const p1 = clonePosition(points[0]);
@@ -1017,16 +1092,16 @@ const supportByFireDrawRule = {
1017
1092
  id: "area8:support-by-fire",
1018
1093
  minimumUserPoints: 2,
1019
1094
  canonicalPointCount: 4,
1020
- derive: derive$4,
1095
+ derive: derive$3,
1021
1096
  transform(event) {
1022
1097
  const { next } = event;
1023
1098
  if (next.length === 4) return clonePositions(next);
1024
- return derive$4(next);
1099
+ return derive$3(next);
1025
1100
  }
1026
1101
  };
1027
1102
  //#endregion
1028
1103
  //#region src/draw-rules/axis1.ts
1029
- function derive$3(points) {
1104
+ function derive$2(points) {
1030
1105
  if (points.length < 2) return clonePositions(points);
1031
1106
  if (points.length === 2) {
1032
1107
  const tip = clonePosition(points[0]);
@@ -1064,10 +1139,10 @@ const axis1DrawRule = {
1064
1139
  id: "axis1",
1065
1140
  minimumUserPoints: 2,
1066
1141
  trailingFixedSlots: 1,
1067
- derive: derive$3,
1142
+ derive: derive$2,
1068
1143
  transform(event) {
1069
1144
  const { previous, next, activePointIndex } = event;
1070
- if (next.length < 3 || previous.length < 3) return derive$3(next);
1145
+ if (next.length < 3 || previous.length < 3) return derive$2(next);
1071
1146
  if (!(activePointIndex === 0 || activePointIndex === 1)) return clonePositions(next);
1072
1147
  const metrics = calculateMetrics(previous);
1073
1148
  if (!metrics) return clonePositions(next);
@@ -1120,7 +1195,7 @@ const counterattackByFireDrawRule = {
1120
1195
  };
1121
1196
  //#endregion
1122
1197
  //#region src/draw-rules/area21.ts
1123
- function derive$2(points) {
1198
+ function derive$1(points) {
1124
1199
  if (points.length === 2) return clonePositions([
1125
1200
  points[0],
1126
1201
  points[1],
@@ -1133,9 +1208,9 @@ const searchAreaDrawRule = {
1133
1208
  minimumUserPoints: 3,
1134
1209
  minimumPreviewPoints: 2,
1135
1210
  canonicalPointCount: 3,
1136
- derive: derive$2,
1211
+ derive: derive$1,
1137
1212
  transform(event) {
1138
- return derive$2(event.next);
1213
+ return derive$1(event.next);
1139
1214
  }
1140
1215
  };
1141
1216
  //#endregion
@@ -1145,7 +1220,7 @@ const searchAreaDrawRule = {
1145
1220
  * edge. P3 is the next corner, constrained to the perpendicular through P2.
1146
1221
  * The generator derives the fourth corner.
1147
1222
  */
1148
- function derive$1(points) {
1223
+ function derive(points) {
1149
1224
  if (points.length < 2) return clonePositions(points);
1150
1225
  const p1 = clonePosition(points[0]);
1151
1226
  const p2 = clonePosition(points[1]);
@@ -1164,8 +1239,8 @@ function derive$1(points) {
1164
1239
  frame?.pointAtNormalDistance(frame.signedNormalDistance(points[2])) ?? clonePosition(points[2])
1165
1240
  ];
1166
1241
  }
1167
- function guidePoints$1(points) {
1168
- const canonical = derive$1(points);
1242
+ function guidePoints(points) {
1243
+ const canonical = derive(points);
1169
1244
  if (canonical.length < 3) return canonical;
1170
1245
  const [c1, c2, c3] = canonical;
1171
1246
  if (!c1 || !c2 || !c3) return canonical;
@@ -1191,16 +1266,16 @@ const rectangleDrawRule = {
1191
1266
  minimumPreviewPoints: 2,
1192
1267
  canonicalPointCount: 3,
1193
1268
  showGuide: true,
1194
- guidePoints: guidePoints$1,
1195
- derive: derive$1,
1269
+ guidePoints,
1270
+ derive,
1196
1271
  transform(event) {
1197
1272
  const { previous, next, activePointIndex } = event;
1198
- if (previous.length < 3 || next.length < 3) return derive$1(next);
1273
+ if (previous.length < 3 || next.length < 3) return derive(next);
1199
1274
  const frame = createBaselineFrame(previous[0], previous[1], {
1200
1275
  origin: "p2",
1201
1276
  normal: "right"
1202
1277
  });
1203
- if (!frame) return derive$1(next);
1278
+ if (!frame) return derive(next);
1204
1279
  if (activePointIndex === 0) return [
1205
1280
  snapToAxis(next[0], frame.p2, frame.direction),
1206
1281
  clonePosition(previous[1]),
@@ -1225,53 +1300,6 @@ const rectangleDrawRule = {
1225
1300
  p3
1226
1301
  ];
1227
1302
  }
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
1303
  return derive(next);
1276
1304
  }
1277
1305
  };
@@ -2103,6 +2131,43 @@ function buildGappedLine(verts, gaps) {
2103
2131
  return parts;
2104
2132
  }
2105
2133
  //#endregion
2134
+ //#region src/style.ts
2135
+ /**
2136
+ * Per-feature style hint pinning a filled part to a solid interior. Attach to
2137
+ * a generator's intrinsic doctrinal accents — arrowheads, teeth, echelon
2138
+ * glyphs, barbs, blades — so a patterned (`hatch`, `dots`, …) fill set at the graphicsStyle
2139
+ * or measure layer never bleeds into a small silhouette and ruins its
2140
+ * legibility. The renderer only reads style hints, so one shared instance is
2141
+ * safe to reuse across every accent feature.
2142
+ */
2143
+ const SOLID_ACCENT_FILL = { fillPattern: "solid" };
2144
+ /**
2145
+ * Ultimate fallback for the symbol color when no `color` or per-channel
2146
+ * override is supplied at any layer. Keeps a zero-config render monocolor
2147
+ * black and guarantees filled parts still render filled. See ADR-0011.
2148
+ */
2149
+ const DEFAULT_SYMBOL_COLOR = "#000000";
2150
+ //#endregion
2151
+ //#region src/portrayal.ts
2152
+ const DEFAULT_STROKE_WIDTH_CSS_PIXELS = 2;
2153
+ const DEFAULT_STROKE_DASH_CSS_PIXELS = Object.freeze([]);
2154
+ const DEFAULT_LINE_CAP = "round";
2155
+ const DEFAULT_LINE_JOIN = "round";
2156
+ const DEFAULT_LABEL_HEIGHT_CSS_PIXELS = 14;
2157
+ const DEFAULT_LABEL_SIZE_CLAMP_CSS_PIXELS = {
2158
+ min: 8,
2159
+ max: 24
2160
+ };
2161
+ /** Shared absent-value portrayal defaults; render output remains sparse. */
2162
+ const DEFAULT_PORTRAYAL = Object.freeze({
2163
+ symbolColor: DEFAULT_SYMBOL_COLOR,
2164
+ strokeWidthCssPixels: 2,
2165
+ strokeDashCssPixels: DEFAULT_STROKE_DASH_CSS_PIXELS,
2166
+ lineCap: DEFAULT_LINE_CAP,
2167
+ lineJoin: DEFAULT_LINE_JOIN,
2168
+ labelHeightCssPixels: 14
2169
+ });
2170
+ //#endregion
2106
2171
  //#region src/internal/angle-utils.ts
2107
2172
  /**
2108
2173
  * Wraps an angle (radians) into the half-open range (-π, π].
@@ -2613,6 +2678,39 @@ const LABEL_CHAR_ASPECT_RATIO = .6;
2613
2678
  function estimatedTextWidth(text, textHeightMeters) {
2614
2679
  return text.length * LABEL_CHAR_ASPECT_RATIO * textHeightMeters;
2615
2680
  }
2681
+ /** Resolve the label's final display size, then measure and convert it to construction metres. */
2682
+ function resolveTextMetrics(text, options, context, style = "regular") {
2683
+ const legacyHeight = resolveLabelOffsetMeters(options, 1);
2684
+ const constructionScale = context?.constructionMetersPerCssPixel;
2685
+ if (!(constructionScale !== void 0 && constructionScale > 0)) return {
2686
+ width: estimatedTextWidth(text, legacyHeight),
2687
+ height: legacyHeight
2688
+ };
2689
+ let sizeCssPixels;
2690
+ if (options.labelSizePixels !== void 0) sizeCssPixels = options.labelSizePixels;
2691
+ else if (options.labelSize !== void 0) {
2692
+ const requested = options.labelSize / constructionScale;
2693
+ const requestedBand = context?.labelSizeClampCssPixels;
2694
+ const band = requestedBand && Number.isFinite(requestedBand.min) && requestedBand.min >= 0 && Number.isFinite(requestedBand.max) && requestedBand.max >= 0 ? requestedBand : DEFAULT_LABEL_SIZE_CLAMP_CSS_PIXELS;
2695
+ const min = Math.min(band.min, band.max);
2696
+ const max = Math.max(band.min, band.max);
2697
+ sizeCssPixels = Math.min(max, Math.max(min, requested));
2698
+ } else sizeCssPixels = 14;
2699
+ const fallbackHeight = sizeCssPixels * constructionScale;
2700
+ const measured = context?.measureText?.({
2701
+ text,
2702
+ style,
2703
+ sizeCssPixels
2704
+ });
2705
+ if (!measured || !Number.isFinite(measured.widthCssPixels) || measured.widthCssPixels < 0 || !Number.isFinite(measured.heightCssPixels) || measured.heightCssPixels < 0) return {
2706
+ width: estimatedTextWidth(text, fallbackHeight),
2707
+ height: fallbackHeight
2708
+ };
2709
+ return {
2710
+ width: measured.widthCssPixels * constructionScale,
2711
+ height: measured.heightCssPixels * constructionScale
2712
+ };
2713
+ }
2616
2714
  /** The end-label text for a phase-line naming, or `""` when unnamed. */
2617
2715
  function phaseLineLabelText(name, includePrefix) {
2618
2716
  if (!name) return "";
@@ -2624,17 +2722,17 @@ function phaseLineLabelText(name, includePrefix) {
2624
2722
  * {@link pushEndLabels}. When another beyond-tip label is present, its
2625
2723
  * estimated width is added so the hostile marker remains outermost.
2626
2724
  */
2627
- function pushHostileEndLabels(out, verts, hostileText, options = {}, occupiedTextForEnd = "") {
2725
+ function pushHostileEndLabels(out, verts, hostileText, options = {}, occupiedTextForEnd = "", context) {
2628
2726
  if (!hostileText) return;
2629
2727
  const frames = endFrames(verts);
2630
2728
  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
2729
  for (const end of ["start", "end"]) {
2634
2730
  const frame = frames[end];
2635
2731
  if (!frame) continue;
2636
2732
  const occupiedText = typeof occupiedTextForEnd === "string" ? occupiedTextForEnd : occupiedTextForEnd(end);
2637
- const distance = clearance + (occupiedText ? estimatedTextWidth(occupiedText, textHeight) + clearance : 0);
2733
+ const metrics = resolveTextMetrics(occupiedText || hostileText, options, context);
2734
+ const clearance = (END_LABEL_CLEARANCE_RATIO + Math.max(0, options.labelPadding ?? 0)) * metrics.height;
2735
+ const distance = clearance + (occupiedText ? metrics.width + clearance : 0);
2638
2736
  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
2737
  }
2640
2738
  }
@@ -2693,7 +2791,7 @@ function pushEndLabelsAbove(out, verts, textForEnd, options = {}) {
2693
2791
  * (via {@link pushEndLabels} or {@link pushEndLabelsAbove}, per `placement`)
2694
2792
  * as a complete FeatureCollection.
2695
2793
  */
2696
- function fixedLabelLineFeatures(positions, options, textForEnd, hostileText) {
2794
+ function fixedLabelLineFeatures(positions, options, textForEnd, hostileText, context) {
2697
2795
  const { part, placement = "beyond", phaseLineName, includePrefix = true, smooth, smoothResolution, ...sizeOptions } = options;
2698
2796
  const verts = smoothLineVerts(positions.map((p) => project(p[0], p[1])), {
2699
2797
  smooth,
@@ -2713,7 +2811,7 @@ function fixedLabelLineFeatures(positions, options, textForEnd, hostileText) {
2713
2811
  else pushEndLabels(labelFeatures, verts, textForEnd, sizeOptions);
2714
2812
  const nameText = phaseLineLabelText(phaseLineName?.trim(), includePrefix);
2715
2813
  if (nameText) pushEndLabels(labelFeatures, verts, nameText, sizeOptions);
2716
- pushHostileEndLabels(labelFeatures, verts, hostileText, sizeOptions, nameText || (placement === "beyond" ? textForEnd : ""));
2814
+ pushHostileEndLabels(labelFeatures, verts, hostileText, sizeOptions, nameText || (placement === "beyond" ? textForEnd : ""), context);
2717
2815
  features.push(...labelFeatures);
2718
2816
  return {
2719
2817
  type: "FeatureCollection",
@@ -2879,10 +2977,9 @@ function nonNegative$1(value, fallback) {
2879
2977
  * later `buildGappedLine` walk); `segments`/`totalLength` describe the full
2880
2978
  * route that anchors the default label placement.
2881
2979
  */
2882
- function directionLabelGap(verts, segments, totalLength, fraction, text, options, placement = {}) {
2980
+ function directionLabelGap(verts, segments, totalLength, fraction, text, options, placement = {}, context) {
2883
2981
  if (!text) return void 0;
2884
- const textHeight = resolveLabelOffsetMeters(options, 1);
2885
- const textWidth = estimatedTextWidth(text, textHeight);
2982
+ const { width: textWidth, height: textHeight } = resolveTextMetrics(text, options, context);
2886
2983
  const clearance = (LABEL_GAP_CLEARANCE_RATIO + Math.max(0, options.labelPadding ?? 0)) * textHeight;
2887
2984
  const frame = pointAlongPolyline(segments, totalLength, fraction);
2888
2985
  const rotation = placement.rotation ?? (frame ? labelRotationAlong(frame.along) : 0);
@@ -2943,7 +3040,7 @@ function createDirectionOfAttackLine(coordinates, options, textAmplifiers, confi
2943
3040
  shaftVerts.push(innerTip);
2944
3041
  }
2945
3042
  const labelPosition = clampLinePosition(options.labelPosition, DEFAULT_DIRECTION_LABEL_POSITION);
2946
- const labelGap = directionLabelGap(shaftVerts, segments, totalLength, labelPosition, textAmplifiers.T, options, resolveAmplifierPlacement(context.amplifierPlacements?.T));
3043
+ const labelGap = directionLabelGap(shaftVerts, segments, totalLength, labelPosition, textAmplifiers.T, options, resolveAmplifierPlacement(context.amplifierPlacements?.T), context);
2947
3044
  const lines = [...buildGappedLine(shaftVerts, labelGap ? [labelGap] : []), arrowhead.map((point) => unproject(point[0], point[1]))];
2948
3045
  const features = [{
2949
3046
  type: "Feature",
@@ -3227,7 +3324,7 @@ function createDirectionOfAttackAviation(coordinates, options = {}, textAmplifie
3227
3324
  const bowTieB = vecSub(bowTieStart, vecScale(bowTiePerp, bowTieHalfWidth));
3228
3325
  const bowTieC = vecAdd(bowTieEnd, vecScale(bowTiePerp, bowTieHalfWidth));
3229
3326
  const bowTieD = vecSub(bowTieEnd, vecScale(bowTiePerp, bowTieHalfWidth));
3230
- const labelGap = directionLabelGap(verts, segments, totalLength, labelPosition, textAmplifiers.T, resolved, resolveAmplifierPlacement(context.amplifierPlacements?.T));
3327
+ const labelGap = directionLabelGap(verts, segments, totalLength, labelPosition, textAmplifiers.T, resolved, resolveAmplifierPlacement(context.amplifierPlacements?.T), context);
3231
3328
  const features = [{
3232
3329
  type: "Feature",
3233
3330
  properties: { part: "direction-of-attack-aviation" },
@@ -3807,9 +3904,12 @@ function ringLabelAnchor(verts) {
3807
3904
  * overlaps the finite contacted edge; intrinsic markers always mask. Callers
3808
3905
  * own marker names, anchors, rotation, clearance, placement keys, and fields.
3809
3906
  */
3810
- function pushBoundaryMarkers(out, maskVerts, text, anchors, options, amplifierPlacements) {
3907
+ function pushBoundaryMarkers(out, maskVerts, text, anchors, options, amplifierPlacements, context, labelOptions) {
3811
3908
  const { rotation, textHeight, clearance, sizeProps } = options;
3812
- const textWidth = estimatedTextWidth(text, textHeight);
3909
+ const metrics = resolveTextMetrics(text, labelOptions ?? { labelSize: textHeight }, context);
3910
+ const textWidth = metrics.width;
3911
+ const resolvedTextHeight = metrics.height;
3912
+ const resolvedClearance = textHeight > 0 ? clearance * (resolvedTextHeight / textHeight) : clearance;
3813
3913
  const gaps = [];
3814
3914
  for (const anchor of anchors) {
3815
3915
  const labelPlacementKey = options.placementKeyPrefix ? `${options.placementKeyPrefix}:${anchor.side}` : void 0;
@@ -3819,21 +3919,21 @@ function pushBoundaryMarkers(out, maskVerts, text, anchors, options, amplifierPl
3819
3919
  pushLabel(out, labelPoint, text, options.followPlacementRotation ? boxRotation : rotation, sizeProps, void 0, options.amplifierField, labelPlacementKey);
3820
3920
  const contact = placement.position !== void 0 || options.rescanDefaultAnchors === true || anchor.contact === void 0 ? nearestPointOnPolyline(maskVerts, labelPoint) : anchor.contact;
3821
3921
  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;
3922
+ if (placement.position && !labelBoxIntersectsSegment(labelPoint, ...contact.edge, textWidth, resolvedTextHeight, boxRotation)) continue;
3923
+ const tangentExtent = labelHalfExtent(contact.along, textWidth, resolvedTextHeight, boxRotation) + resolvedClearance;
3824
3924
  pushWrappedGap(gaps, contact.arc - tangentExtent, contact.arc + tangentExtent, contact.totalArc);
3825
3925
  }
3826
3926
  return gaps;
3827
3927
  }
3828
3928
  /** Emits movable Field N markers using the generic boundary-marker knockout path. */
3829
- function pushHostileMarkers(out, maskVerts, text, anchors, options, amplifierPlacements) {
3929
+ function pushHostileMarkers(out, maskVerts, text, anchors, options, amplifierPlacements, context, labelOptions) {
3830
3930
  return pushBoundaryMarkers(out, maskVerts, text, anchors, {
3831
3931
  ...options,
3832
3932
  placementKeyPrefix: "label:N",
3833
3933
  amplifierField: "N"
3834
- }, amplifierPlacements);
3934
+ }, amplifierPlacements, context, labelOptions);
3835
3935
  }
3836
- function pushAreaLabels(out, ringVerts, texts, options = {}, amplifierPlacements, maskBoundary) {
3936
+ function pushAreaLabels(out, ringVerts, texts, options = {}, amplifierPlacements, maskBoundary, context) {
3837
3937
  if (ringVerts.length < 2) return [];
3838
3938
  const sizeProps = labelSizeProps(options);
3839
3939
  const padding = Math.max(0, options.labelPadding ?? 0);
@@ -3876,7 +3976,7 @@ function pushAreaLabels(out, ringVerts, texts, options = {}, amplifierPlacements
3876
3976
  clearance: (ENY_CLEARANCE_RATIO + padding) * textHeight + Math.max(0, maskBoundary?.extraClearanceMeters ?? 0),
3877
3977
  sizeProps,
3878
3978
  rescanDefaultAnchors: maskRingVerts !== ringVerts
3879
- }, amplifierPlacements);
3979
+ }, amplifierPlacements, context, options);
3880
3980
  }
3881
3981
  /**
3882
3982
  * Boundary + fill features for a pattern-filled area (e.g. Limited Access Area,
@@ -3955,11 +4055,11 @@ function boundaryFeature(verts, gaps) {
3955
4055
  * echelon-masked boundary, ADR-0023) instead of a closed `Polygon` — the
3956
4056
  * common (no `N`) case is unaffected.
3957
4057
  */
3958
- function labeledAreaFeatures(positions, options, texts, amplifierPlacements) {
4058
+ function labeledAreaFeatures(positions, options, texts, amplifierPlacements, context) {
3959
4059
  const { smooth = false, smoothResolution = DEFAULT_SMOOTH_RESOLUTION$9, ...labelOptions } = options;
3960
4060
  const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$9);
3961
4061
  const labelFeatures = [];
3962
- const features = [boundaryFeature(verts, pushAreaLabels(labelFeatures, verts, texts, labelOptions, amplifierPlacements))];
4062
+ const features = [boundaryFeature(verts, pushAreaLabels(labelFeatures, verts, texts, labelOptions, amplifierPlacements, void 0, context))];
3963
4063
  features.push(...labelFeatures);
3964
4064
  return {
3965
4065
  type: "FeatureCollection",
@@ -4003,7 +4103,7 @@ const AREA_METADATA = {
4003
4103
  * marker, per {@link labeledAreaFeatures}.
4004
4104
  */
4005
4105
  function createArea(positions, options = {}, textAmplifiers = {}, context = {}) {
4006
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, ""), context.amplifierPlacements);
4106
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, ""), context.amplifierPlacements, context);
4007
4107
  }
4008
4108
  const AREA = defineControlMeasure({
4009
4109
  metadata: AREA_METADATA,
@@ -4022,23 +4122,6 @@ const AREA = defineControlMeasure({
4022
4122
  }
4023
4123
  });
4024
4124
  //#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
4125
  //#region src/generators/cm15-maneuver-areas/area-defense.ts
4043
4126
  const DEFAULT_AREA_DEFENSE_OPTIONS = {
4044
4127
  arrowOpeningAngle: 30,
@@ -4283,7 +4366,7 @@ const AREA_OF_OPERATIONS_METADATA = {
4283
4366
  * `H`/`W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
4284
4367
  */
4285
4368
  function createAreaOfOperations(positions, options = {}, textAmplifiers = {}, context = {}) {
4286
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "AO"), context.amplifierPlacements);
4369
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "AO"), context.amplifierPlacements, context);
4287
4370
  }
4288
4371
  const AREA_OF_OPERATIONS = defineControlMeasure({
4289
4372
  metadata: AREA_OF_OPERATIONS_METADATA,
@@ -4342,7 +4425,7 @@ const NAMED_AREA_OF_INTEREST_METADATA = {
4342
4425
  * an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
4343
4426
  */
4344
4427
  function createNamedAreaOfInterest(positions, options = {}, textAmplifiers = {}, context = {}) {
4345
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "NAI"), context.amplifierPlacements);
4428
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "NAI"), context.amplifierPlacements, context);
4346
4429
  }
4347
4430
  const NAMED_AREA_OF_INTEREST = defineControlMeasure({
4348
4431
  metadata: NAMED_AREA_OF_INTEREST_METADATA,
@@ -4401,7 +4484,7 @@ const TARGET_AREA_OF_INTEREST_METADATA = {
4401
4484
  * an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
4402
4485
  */
4403
4486
  function createTargetAreaOfInterest(positions, options = {}, textAmplifiers = {}, context = {}) {
4404
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "TAI"), context.amplifierPlacements);
4487
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "TAI"), context.amplifierPlacements, context);
4405
4488
  }
4406
4489
  const TARGET_AREA_OF_INTEREST = defineControlMeasure({
4407
4490
  metadata: TARGET_AREA_OF_INTEREST_METADATA,
@@ -4496,7 +4579,7 @@ function createAirfieldZone(positions, options = {}, textAmplifiers = {}, contex
4496
4579
  const { smooth = DEFAULT_AIRFIELD_ZONE_OPTIONS.smooth, smoothResolution = DEFAULT_AIRFIELD_ZONE_OPTIONS.smoothResolution, glyphSizeRatio = DEFAULT_AIRFIELD_ZONE_OPTIONS.glyphSizeRatio, ...labelOptions } = options;
4497
4580
  const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$8);
4498
4581
  const enyLabels = [];
4499
- const features = [boundaryFeature(verts, textAmplifiers.N ? pushAreaLabels(enyLabels, verts, { hostile: textAmplifiers.N }, labelOptions, context.amplifierPlacements) : [])];
4582
+ const features = [boundaryFeature(verts, textAmplifiers.N ? pushAreaLabels(enyLabels, verts, { hostile: textAmplifiers.N }, labelOptions, context.amplifierPlacements, void 0, context) : [])];
4500
4583
  let minX = Infinity;
4501
4584
  let maxX = -Infinity;
4502
4585
  for (const [x] of verts) {
@@ -4572,13 +4655,13 @@ const DEFAULT_ECHELON_LABELED_AREA_OPTIONS = {
4572
4655
  * before carving the boundary, so either (or both) opens it into a gapped
4573
4656
  * `MultiLineString`; with neither, the boundary is a closed `Polygon`.
4574
4657
  */
4575
- function echelonLabeledAreaFeatures(positions, options, texts, amplifierPlacements) {
4658
+ function echelonLabeledAreaFeatures(positions, options, texts, amplifierPlacements, context) {
4576
4659
  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
4660
  const h = resolveEchelonHeight(echelonSize, echelonSizePixels, metersPerPixel);
4578
4661
  const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$7);
4579
4662
  const { strokes, fills, gaps: echelonGaps } = placeEchelon(buildAreaPerimeter(verts), h, echelon, echelonPadding, echelonPosition, echelonAnchor);
4580
4663
  const labelFeatures = [];
4581
- const enyGaps = pushAreaLabels(labelFeatures, verts, texts, labelOptions, amplifierPlacements);
4664
+ const enyGaps = pushAreaLabels(labelFeatures, verts, texts, labelOptions, amplifierPlacements, void 0, context);
4582
4665
  const features = [boundaryFeature(verts, [...echelonGaps, ...enyGaps])];
4583
4666
  if (strokes.length > 0) features.push({
4584
4667
  type: "Feature",
@@ -4654,7 +4737,7 @@ const BASE_CAMP_METADATA = {
4654
4737
  * boundary, per {@link echelonLabeledAreaFeatures}.
4655
4738
  */
4656
4739
  function createBaseCamp(positions, options = {}, textAmplifiers = {}, context = {}) {
4657
- return echelonLabeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "BC"), context.amplifierPlacements);
4740
+ return echelonLabeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "BC"), context.amplifierPlacements, context);
4658
4741
  }
4659
4742
  const BASE_CAMP = defineControlMeasure({
4660
4743
  metadata: BASE_CAMP_METADATA,
@@ -4712,7 +4795,7 @@ const GUERRILLA_BASE_METADATA = {
4712
4795
  * straddling the boundary, per {@link echelonLabeledAreaFeatures}.
4713
4796
  */
4714
4797
  function createGuerrillaBase(positions, options = {}, textAmplifiers = {}, context = {}) {
4715
- return echelonLabeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "GB"), context.amplifierPlacements);
4798
+ return echelonLabeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "GB"), context.amplifierPlacements, context);
4716
4799
  }
4717
4800
  const GUERRILLA_BASE = defineControlMeasure({
4718
4801
  metadata: GUERRILLA_BASE_METADATA,
@@ -4774,7 +4857,7 @@ const GENERIC_C2_AREA_METADATA = {
4774
4857
  * `H`/`W`/`W1`/`N` amplifier rows — no prefix, per {@link labeledAreaFeatures}.
4775
4858
  */
4776
4859
  function createGenericC2Area(positions, options = {}, textAmplifiers = {}, context = {}) {
4777
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers), context.amplifierPlacements);
4860
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers), context.amplifierPlacements, context);
4778
4861
  }
4779
4862
  const GENERIC_C2_AREA = defineControlMeasure({
4780
4863
  metadata: GENERIC_C2_AREA_METADATA,
@@ -4829,7 +4912,7 @@ const ASSEMBLY_AREA_METADATA = {
4829
4912
  * `H`/`W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
4830
4913
  */
4831
4914
  function createAssemblyArea(positions, options = {}, textAmplifiers = {}, context = {}) {
4832
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "AA"), context.amplifierPlacements);
4915
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "AA"), context.amplifierPlacements, context);
4833
4916
  }
4834
4917
  const ASSEMBLY_AREA = defineControlMeasure({
4835
4918
  metadata: ASSEMBLY_AREA_METADATA,
@@ -5021,13 +5104,13 @@ function ditchPaths(projectedPoints, options) {
5021
5104
  function buildAntitankDitchTriangles(basePath, options) {
5022
5105
  return generateAntitankDitchTriangles(basePath, calculateAntitankDitchHeight(options), options.toothWidthRatio ?? DEFAULT_TOOTH_WIDTH_RATIO$1);
5023
5106
  }
5024
- function createAntitankDitchUnderConstruction(positions, options = {}, textAmplifiers = {}) {
5107
+ function createAntitankDitchUnderConstruction(positions, options = {}, textAmplifiers = {}, context = {}) {
5025
5108
  const { smoothedPath, basePath } = ditchPaths(positions.map((position) => project(position[0], position[1])), options);
5026
5109
  const triangles = buildAntitankDitchTriangles(basePath, options);
5027
5110
  const baseLine = options.smooth ? basePath.map((point) => unproject(point[0], point[1])) : positions.map((position) => [position[0], position[1]]);
5028
5111
  const triangleLines = triangles.map((triangle) => triangle.map((point) => unproject(point[0], point[1])));
5029
5112
  const labels = [];
5030
- pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options);
5113
+ pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options, "", context);
5031
5114
  return {
5032
5115
  type: "FeatureCollection",
5033
5116
  features: [{
@@ -5040,13 +5123,13 @@ function createAntitankDitchUnderConstruction(positions, options = {}, textAmpli
5040
5123
  }, ...labels]
5041
5124
  };
5042
5125
  }
5043
- function createAntitankDitchCompleted(positions, options = {}, textAmplifiers = {}) {
5126
+ function createAntitankDitchCompleted(positions, options = {}, textAmplifiers = {}, context = {}) {
5044
5127
  const { smoothedPath, basePath } = ditchPaths(positions.map((position) => project(position[0], position[1])), options);
5045
5128
  const polygons = buildAntitankDitchTriangles(basePath, options).map((triangle) => {
5046
5129
  return [[...triangle, triangle[0]].map((point) => unproject(point[0], point[1]))];
5047
5130
  });
5048
5131
  const labels = [];
5049
- pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options);
5132
+ pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options, "", context);
5050
5133
  return {
5051
5134
  type: "FeatureCollection",
5052
5135
  features: [{
@@ -5136,7 +5219,7 @@ function generateAntitankWallPoints(projectedPoints, toothHeight, toothWidthRati
5136
5219
  }
5137
5220
  return wallPoints;
5138
5221
  }
5139
- function createAntitankWall(positions, options = {}, textAmplifiers = {}) {
5222
+ function createAntitankWall(positions, options = {}, textAmplifiers = {}, context = {}) {
5140
5223
  const toothHeight = calculateAntitankDitchHeight(options);
5141
5224
  const toothWidthRatio = options.toothWidthRatio ?? DEFAULT_TOOTH_WIDTH_RATIO;
5142
5225
  const toothSpacingRatio = options.toothSpacingRatio ?? DEFAULT_TOOTH_SPACING_RATIO;
@@ -5144,7 +5227,7 @@ function createAntitankWall(positions, options = {}, textAmplifiers = {}) {
5144
5227
  const smoothedPath = smoothLineVerts(projectedPoints, options, 16);
5145
5228
  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
5229
  const labels = [];
5147
- pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options);
5230
+ pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options, "", context);
5148
5231
  return {
5149
5232
  type: "FeatureCollection",
5150
5233
  features: [{
@@ -5591,7 +5674,7 @@ function createBattlePosition(positions, options = {}, textAmplifiers = {}, cont
5591
5674
  const enyGaps = pushAreaLabels(labelFeatures, verts, {
5592
5675
  name: textAmplifiers.T,
5593
5676
  hostile: textAmplifiers.N
5594
- }, options, context.amplifierPlacements);
5677
+ }, options, context.amplifierPlacements, void 0, context);
5595
5678
  const boundaryCoords = buildGappedLine(verts, [...gaps, ...enyGaps]);
5596
5679
  const features = [];
5597
5680
  if (boundaryCoords.length > 0) features.push({
@@ -6189,7 +6272,7 @@ const BOUNDARY_METADATA = {
6189
6272
  * @param textAmplifiers - Normalized text amplifiers (ADR-0027): `T`/`AS` label
6190
6273
  * unit 1 (left-of-travel side), `T1`/`AS1` label unit 2 (right-of-travel side).
6191
6274
  */
6192
- function createBoundary(positions, options = {}, textAmplifiers = {}) {
6275
+ function createBoundary(positions, options = {}, textAmplifiers = {}, context = {}) {
6193
6276
  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
6277
  const unit1Designator = textAmplifiers.T ?? "";
6195
6278
  const unit1Country = textAmplifiers.AS ?? "";
@@ -6273,7 +6356,7 @@ function createBoundary(positions, options = {}, textAmplifiers = {}) {
6273
6356
  labelSize: h,
6274
6357
  labelPadding
6275
6358
  };
6276
- pushHostileEndLabels(labelFeatures, verts, textAmplifiers.N, hostileLabelOptions);
6359
+ pushHostileEndLabels(labelFeatures, verts, textAmplifiers.N, hostileLabelOptions, "", context);
6277
6360
  const boundaryCoords = buildGappedLine(verts, gaps);
6278
6361
  const features = [];
6279
6362
  if (boundaryCoords.length > 0) features.push({
@@ -6389,12 +6472,12 @@ const LIGHT_LINE_METADATA = {
6389
6472
  * Creates a Light line control measure: a polyline through `positions`
6390
6473
  * labeled "LL" above the line near each end.
6391
6474
  */
6392
- function createLightLine(positions, options = {}, textAmplifiers = {}) {
6475
+ function createLightLine(positions, options = {}, textAmplifiers = {}, context = {}) {
6393
6476
  return fixedLabelLineFeatures(positions, {
6394
6477
  part: "light-line",
6395
6478
  placement: "above",
6396
6479
  ...options
6397
- }, "LL", textAmplifiers.N);
6480
+ }, "LL", textAmplifiers.N, context);
6398
6481
  }
6399
6482
  const LIGHT_LINE = defineControlMeasure({
6400
6483
  metadata: LIGHT_LINE_METADATA,
@@ -6500,12 +6583,12 @@ const CENTER_LABEL_OFFSET_RATIO = .7;
6500
6583
  * above the line, T1/AS1 below). No center group is emitted when all four
6501
6584
  * amplifiers are empty.
6502
6585
  */
6503
- function createEngineerWorkLine(positions, options = {}, textAmplifiers = {}) {
6586
+ function createEngineerWorkLine(positions, options = {}, textAmplifiers = {}, context = {}) {
6504
6587
  const features = [...fixedLabelLineFeatures(positions, {
6505
6588
  part: "engineer-work-line",
6506
6589
  placement: "above",
6507
6590
  ...options
6508
- }, "EWL", textAmplifiers.N).features];
6591
+ }, "EWL", textAmplifiers.N, context).features];
6509
6592
  const { segments, totalLength } = polylineSegments(smoothLineVerts(positions.map((p) => project(p[0], p[1])), options, 12));
6510
6593
  const frame = pointAlongPolyline(segments, totalLength, .5);
6511
6594
  if (frame) {
@@ -6642,7 +6725,7 @@ const GENERIC_C2_LINE_METADATA = {
6642
6725
  * (inset from the tip, anchored toward the line's interior), one at
6643
6726
  * `base + perp·offset` and the other at `base − perp·offset`.
6644
6727
  */
6645
- function createGenericC2Line(positions, options = {}, textAmplifiers = {}) {
6728
+ function createGenericC2Line(positions, options = {}, textAmplifiers = {}, context = {}) {
6646
6729
  const { phaseLineName, includePrefix = true, smooth, smoothResolution, ...sizeOptions } = options;
6647
6730
  const verts = smoothLineVerts(positions.map((p) => project(p[0], p[1])), {
6648
6731
  smooth,
@@ -6674,7 +6757,7 @@ function createGenericC2Line(positions, options = {}, textAmplifiers = {}) {
6674
6757
  }
6675
6758
  const nameText = phaseLineLabelText(trimmedPhaseLineName, includePrefix);
6676
6759
  if (nameText) pushEndLabels(labelFeatures, verts, nameText, sizeOptions);
6677
- pushHostileEndLabels(labelFeatures, verts, textAmplifiers.N, sizeOptions, nameText);
6760
+ pushHostileEndLabels(labelFeatures, verts, textAmplifiers.N, sizeOptions, nameText, context);
6678
6761
  features.push(...labelFeatures);
6679
6762
  }
6680
6763
  return {
@@ -7526,6 +7609,8 @@ const ARC_RESOLUTION_PARAMS = [{
7526
7609
  max: 360,
7527
7610
  step: 1
7528
7611
  }];
7612
+ //#endregion
7613
+ //#region src/generators/cm99-generic-graphics/params.ts
7529
7614
  const SMOOTH_PATH_PARAMS = [{
7530
7615
  key: "smooth",
7531
7616
  label: "Smooth",
@@ -7862,6 +7947,41 @@ const GENERIC_RECTANGLE = defineControlMeasure({
7862
7947
  ] }
7863
7948
  });
7864
7949
  //#endregion
7950
+ //#region src/internal/annular-sector.ts
7951
+ /**
7952
+ * Clockwise sweep in degrees from `leftAzimuth` to `rightAzimuth`. A non-zero
7953
+ * raw difference that normalizes to 0 means a full 360° fan.
7954
+ *
7955
+ * `collapseToleranceDegrees` (default 0, i.e. off) treats a near-zero or
7956
+ * near-360 sweep as a collapsed sector. Callers whose handle coordinates are
7957
+ * rounded need this: two handles placed on the same ray can recover bearings a
7958
+ * few thousandths of a degree apart, which would otherwise read as an
7959
+ * almost-360° fan.
7960
+ */
7961
+ function clockwiseSweepDegrees(leftAzimuth, rightAzimuth, collapseToleranceDegrees = 0) {
7962
+ const rawSweep = rightAzimuth - leftAzimuth;
7963
+ const sweep = normalizeDegrees(rawSweep);
7964
+ if (collapseToleranceDegrees > 0 && Math.min(sweep, 360 - sweep) < collapseToleranceDegrees) return 0;
7965
+ return sweep === 0 && rawSweep !== 0 ? 360 : sweep;
7966
+ }
7967
+ /** Samples an arc of `sweepDegrees` at `radius`, starting from `leftAzimuth`. */
7968
+ function sampleArc(anchor, radius, leftAzimuth, sweepDegrees, resolution) {
7969
+ const segmentCount = Math.max(1, Math.ceil(sweepDegrees / 360 * resolution));
7970
+ return Array.from({ length: segmentCount + 1 }, (_, index) => {
7971
+ return destinationPoint(anchor, radius, (leftAzimuth + index / segmentCount * sweepDegrees) * Math.PI / 180);
7972
+ });
7973
+ }
7974
+ /** Closed ring tracing the outer arc, back along the inner arc, and closed. */
7975
+ function annularSectorRing(anchor, innerRadius, outerRadius, leftAzimuth, sweepDegrees, resolution) {
7976
+ const outerArc = sampleArc(anchor, outerRadius, leftAzimuth, sweepDegrees, resolution);
7977
+ const innerArc = sampleArc(anchor, innerRadius, leftAzimuth, sweepDegrees, resolution).reverse();
7978
+ return [
7979
+ ...outerArc,
7980
+ ...innerArc,
7981
+ outerArc[0]
7982
+ ];
7983
+ }
7984
+ //#endregion
7865
7985
  //#region src/generators/cm99-generic-graphics/sector.ts
7866
7986
  const DEFAULT_GENERIC_SECTOR_OPTIONS = {
7867
7987
  filled: false,
@@ -7887,17 +8007,6 @@ const GENERIC_SECTOR_METADATA = {
7887
8007
  drawRule: "Sector",
7888
8008
  params: [...FILLED_AREA_PARAMS, ...ARC_RESOLUTION_PARAMS]
7889
8009
  };
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
8010
  function createGenericSector(coordinates, options = {}) {
7902
8011
  const anchor = coordinates[0];
7903
8012
  const innerLeftPoint = coordinates[1];
@@ -7906,11 +8015,13 @@ function createGenericSector(coordinates, options = {}) {
7906
8015
  type: "FeatureCollection",
7907
8016
  features: []
7908
8017
  };
7909
- const innerRadius = haversineDistance(anchor, innerLeftPoint);
7910
- const outerRadius = haversineDistance(anchor, outerRightPoint);
8018
+ const firstRadius = haversineDistance(anchor, innerLeftPoint);
8019
+ const secondRadius = haversineDistance(anchor, outerRightPoint);
8020
+ const innerRadius = Math.min(firstRadius, secondRadius);
8021
+ const outerRadius = Math.max(firstRadius, secondRadius);
7911
8022
  const leftAzimuth = normalizeDegrees(sphericalBearing(anchor, innerLeftPoint) * 180 / Math.PI);
7912
8023
  const rightAzimuth = normalizeDegrees(sphericalBearing(anchor, outerRightPoint) * 180 / Math.PI);
7913
- if (outerRadius <= innerRadius || innerRadius < 1e-6 || outerRadius < 1e-6) return {
8024
+ if (outerRadius - innerRadius < 1e-6 || innerRadius < 1e-6) return {
7914
8025
  type: "FeatureCollection",
7915
8026
  features: []
7916
8027
  };
@@ -7919,13 +8030,7 @@ function createGenericSector(coordinates, options = {}) {
7919
8030
  type: "FeatureCollection",
7920
8031
  features: []
7921
8032
  };
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
- ];
8033
+ const ring = annularSectorRing(anchor, innerRadius, outerRadius, leftAzimuth, sweepDegrees, normalizeArcResolution(options.resolution));
7929
8034
  return {
7930
8035
  type: "FeatureCollection",
7931
8036
  features: [{
@@ -8128,6 +8233,114 @@ const GENERIC_TEXT = defineControlMeasure({
8128
8233
  }
8129
8234
  });
8130
8235
  //#endregion
8236
+ //#region src/generators/cm20-maritime-control-areas/radar-search-doctrine.ts
8237
+ const DEFAULT_RADAR_SEARCH_DOCTRINE_OPTIONS = { resolution: 64 };
8238
+ /** Doctrinal RSD colors from the symbol specification. */
8239
+ const RADAR_SEARCH_DOCTRINE_STROKE_COLOR = "rgb(51, 136, 136)";
8240
+ const RADAR_SEARCH_DOCTRINE_FILL_COLOR = "rgba(51, 136, 136, 0.25)";
8241
+ const RADAR_SEARCH_DOCTRINE_METADATA = {
8242
+ id: "radar-search-doctrine",
8243
+ name: "Radar Search Doctrine",
8244
+ description: "A maritime radar search area defined by an axis and annular-sector limits.",
8245
+ entity: "Maritime Control Areas",
8246
+ entityType: "Radar Search Doctrine",
8247
+ value: "200700",
8248
+ minCoordinates: 3,
8249
+ maxCoordinates: 3,
8250
+ geometry: "area",
8251
+ geometryTypes: ["Polygon", "Point"],
8252
+ paints: {
8253
+ stroke: true,
8254
+ fill: "fixed",
8255
+ text: true
8256
+ },
8257
+ drawRule: "Point18",
8258
+ capturesLabelSize: true,
8259
+ params: ARC_RESOLUTION_PARAMS,
8260
+ textAmplifiers: [{
8261
+ key: "T",
8262
+ label: "Unique designation",
8263
+ description: "Field T — designation centered in the search area along its axis.",
8264
+ placeholder: "FF",
8265
+ maxLength: 20
8266
+ }]
8267
+ };
8268
+ /**
8269
+ * GeoJSON positions are rounded by `destinationPoint`, so two handles placed on
8270
+ * the same ray can recover bearings a few thousandths of a degree apart. Treat
8271
+ * that numerical noise as a collapsed sector, not an almost-360° fan.
8272
+ */
8273
+ const SWEEP_COLLAPSE_TOLERANCE_DEGREES = .01;
8274
+ function createRadarSearchDoctrine(coordinates, options = {}, textAmplifiers = {}) {
8275
+ const anchor = coordinates[0];
8276
+ const startLimit = coordinates[1];
8277
+ const stopLimit = coordinates[2];
8278
+ const firstRange = haversineDistance(anchor, startLimit);
8279
+ const secondRange = haversineDistance(anchor, stopLimit);
8280
+ const startRange = Math.min(firstRange, secondRange);
8281
+ const stopRange = Math.max(firstRange, secondRange);
8282
+ const leftAzimuth = normalizeDegrees(sphericalBearing(anchor, startLimit) * 180 / Math.PI);
8283
+ const sweepDegrees = clockwiseSweepDegrees(leftAzimuth, normalizeDegrees(sphericalBearing(anchor, stopLimit) * 180 / Math.PI), SWEEP_COLLAPSE_TOLERANCE_DEGREES);
8284
+ if (stopRange - startRange < 1e-6 || sweepDegrees < 1e-6) return {
8285
+ type: "FeatureCollection",
8286
+ features: []
8287
+ };
8288
+ const axis = normalizeDegrees(leftAzimuth + sweepDegrees / 2) * Math.PI / 180;
8289
+ const ring = annularSectorRing(anchor, startRange, stopRange, leftAzimuth, sweepDegrees, normalizeArcResolution(options.resolution));
8290
+ const features = [{
8291
+ type: "Feature",
8292
+ properties: {
8293
+ part: "search-area",
8294
+ fill: true,
8295
+ style: {
8296
+ strokeColor: RADAR_SEARCH_DOCTRINE_STROKE_COLOR,
8297
+ fillColor: RADAR_SEARCH_DOCTRINE_FILL_COLOR,
8298
+ fillPattern: "solid"
8299
+ }
8300
+ },
8301
+ geometry: {
8302
+ type: "Polygon",
8303
+ coordinates: [ring]
8304
+ }
8305
+ }];
8306
+ if (textAmplifiers.T) {
8307
+ const labelAnchor = destinationPoint(anchor, (startRange + stopRange) / 2, axis);
8308
+ features.push({
8309
+ type: "Feature",
8310
+ properties: {
8311
+ part: "label",
8312
+ text: textAmplifiers.T,
8313
+ amplifierField: "T",
8314
+ labelPlacementKey: "T",
8315
+ ...labelSizeProps(options),
8316
+ rotation: labelRotationAlong([Math.cos(axis), -Math.sin(axis)])
8317
+ },
8318
+ geometry: {
8319
+ type: "Point",
8320
+ coordinates: labelAnchor
8321
+ }
8322
+ });
8323
+ }
8324
+ return {
8325
+ type: "FeatureCollection",
8326
+ features
8327
+ };
8328
+ }
8329
+ const RADAR_SEARCH_DOCTRINE = defineControlMeasure({
8330
+ metadata: RADAR_SEARCH_DOCTRINE_METADATA,
8331
+ generator: createRadarSearchDoctrine,
8332
+ defaultOptions: DEFAULT_RADAR_SEARCH_DOCTRINE_OPTIONS,
8333
+ rule: dynamicPointDrawRule,
8334
+ previewSample: {
8335
+ controlPoints: [
8336
+ [0, -1],
8337
+ [-.55, -.15],
8338
+ [1, .45]
8339
+ ],
8340
+ textAmplifiers: { T: "FF" }
8341
+ }
8342
+ });
8343
+ //#endregion
8131
8344
  //#region src/generators/cm34-mission-tasks/clear.ts
8132
8345
  /**
8133
8346
  * Default options for the CLEAR symbol.
@@ -9699,7 +9912,7 @@ const DROP_ZONE_METADATA = {
9699
9912
  * (Field N) ENY marker, per {@link labeledAreaFeatures}.
9700
9913
  */
9701
9914
  function createDropZone(positions, options = {}, textAmplifiers = {}, context = {}) {
9702
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "DZ"), context.amplifierPlacements);
9915
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "DZ"), context.amplifierPlacements, context);
9703
9916
  }
9704
9917
  const DROP_ZONE = defineControlMeasure({
9705
9918
  metadata: DROP_ZONE_METADATA,
@@ -9835,7 +10048,7 @@ function createEncirclement(positions, options = {}, textAmplifiers = {}, contex
9835
10048
  const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$2);
9836
10049
  const perimeter = buildAreaPerimeter(verts);
9837
10050
  const labelFeatures = [];
9838
- const gaps = pushAreaLabels(labelFeatures, verts, { hostile: textAmplifiers.N }, options, context.amplifierPlacements);
10051
+ const gaps = pushAreaLabels(labelFeatures, verts, { hostile: textAmplifiers.N }, options, context.amplifierPlacements, void 0, context);
9839
10052
  const radius = meanRadius(verts);
9840
10053
  const barbLength = radius * Math.max(0, barbLengthRatio);
9841
10054
  const barbs = buildBarbs(perimeter, radius * Math.max(0, barbSpacingRatio), barbLength, gaps);
@@ -9916,7 +10129,7 @@ const EXTRACTION_ZONE_METADATA = {
9916
10129
  * an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
9917
10130
  */
9918
10131
  function createExtractionZone(positions, options = {}, textAmplifiers = {}, context = {}) {
9919
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "EZ"), context.amplifierPlacements);
10132
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "EZ"), context.amplifierPlacements, context);
9920
10133
  }
9921
10134
  const EXTRACTION_ZONE = defineControlMeasure({
9922
10135
  metadata: EXTRACTION_ZONE_METADATA,
@@ -10588,7 +10801,7 @@ function processSegment(p1, p2, radius, arcSegments, isFirstSegment) {
10588
10801
  * );
10589
10802
  * ```
10590
10803
  */
10591
- function createFLOT(positions, options = {}, textAmplifiers = {}) {
10804
+ function createFLOT(positions, options = {}, textAmplifiers = {}, context = {}) {
10592
10805
  const { radius = DEFAULT_RADIUS, radiusPixels, metersPerPixel, arcSegments = DEFAULT_ARC_SEGMENTS, smooth = DEFAULT_FLOT_OPTIONS.smooth, smoothResolution = DEFAULT_FLOT_OPTIONS.smoothResolution } = options;
10593
10806
  let effectiveRadius = radius;
10594
10807
  if (radiusPixels !== void 0 && metersPerPixel !== void 0 && metersPerPixel > 0) effectiveRadius = radiusPixels * metersPerPixel;
@@ -10626,7 +10839,7 @@ function createFLOT(positions, options = {}, textAmplifiers = {}) {
10626
10839
  }
10627
10840
  };
10628
10841
  const labels = [];
10629
- pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options);
10842
+ pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options, "", context);
10630
10843
  return {
10631
10844
  type: "FeatureCollection",
10632
10845
  features: [feature, ...labels]
@@ -10695,14 +10908,14 @@ const PHASE_LINE_METADATA = {
10695
10908
  * Creates a Phase line control measure: a polyline through `positions`
10696
10909
  * labeled at each end with `T` (optionally "PL"-prefixed).
10697
10910
  */
10698
- function createPhaseLine(positions, options = {}, textAmplifiers = {}) {
10911
+ function createPhaseLine(positions, options = {}, textAmplifiers = {}, context = {}) {
10699
10912
  const { includePrefix = true, ...sizeOptions } = options;
10700
10913
  const designation = textAmplifiers.T ?? "";
10701
10914
  const text = includePrefix ? designation.length > 0 ? `PL ${designation}` : "PL" : designation;
10702
10915
  return fixedLabelLineFeatures(positions, {
10703
10916
  part: "phase-line",
10704
10917
  ...sizeOptions
10705
- }, text, textAmplifiers.N);
10918
+ }, text, textAmplifiers.N, context);
10706
10919
  }
10707
10920
  const PHASE_LINE = defineControlMeasure({
10708
10921
  metadata: PHASE_LINE_METADATA,
@@ -10774,12 +10987,12 @@ const BRIDGEHEAD_LINE_METADATA = {
10774
10987
  textAmplifiers: [HOSTILE_LINE_AMPLIFIER]
10775
10988
  };
10776
10989
  /** Creates a Bridgehead line labeled "BL" above both ends. */
10777
- function createBridgeheadLine(positions, options = {}, textAmplifiers = {}) {
10990
+ function createBridgeheadLine(positions, options = {}, textAmplifiers = {}, context = {}) {
10778
10991
  return fixedLabelLineFeatures(positions, {
10779
10992
  part: "bridgehead-line",
10780
10993
  placement: "above",
10781
10994
  ...options
10782
- }, "BL", textAmplifiers.N);
10995
+ }, "BL", textAmplifiers.N, context);
10783
10996
  }
10784
10997
  const BRIDGEHEAD_LINE = defineControlMeasure({
10785
10998
  metadata: BRIDGEHEAD_LINE_METADATA,
@@ -10850,12 +11063,12 @@ const HOLDING_LINE_METADATA = {
10850
11063
  textAmplifiers: [HOSTILE_LINE_AMPLIFIER]
10851
11064
  };
10852
11065
  /** Creates a Holding line labeled "HL" above both ends. */
10853
- function createHoldingLine(positions, options = {}, textAmplifiers = {}) {
11066
+ function createHoldingLine(positions, options = {}, textAmplifiers = {}, context = {}) {
10854
11067
  return fixedLabelLineFeatures(positions, {
10855
11068
  part: "holding-line",
10856
11069
  placement: "above",
10857
11070
  ...options
10858
- }, "HL", textAmplifiers.N);
11071
+ }, "HL", textAmplifiers.N, context);
10859
11072
  }
10860
11073
  const HOLDING_LINE = defineControlMeasure({
10861
11074
  metadata: HOLDING_LINE_METADATA,
@@ -10926,12 +11139,12 @@ const RELEASE_LINE_METADATA = {
10926
11139
  textAmplifiers: [HOSTILE_LINE_AMPLIFIER]
10927
11140
  };
10928
11141
  /** Creates a Release line labeled "RL" above both ends. */
10929
- function createReleaseLine(positions, options = {}, textAmplifiers = {}) {
11142
+ function createReleaseLine(positions, options = {}, textAmplifiers = {}, context = {}) {
10930
11143
  return fixedLabelLineFeatures(positions, {
10931
11144
  part: "release-line",
10932
11145
  placement: "above",
10933
11146
  ...options
10934
- }, "RL", textAmplifiers.N);
11147
+ }, "RL", textAmplifiers.N, context);
10935
11148
  }
10936
11149
  const RELEASE_LINE = defineControlMeasure({
10937
11150
  metadata: RELEASE_LINE_METADATA,
@@ -11005,12 +11218,12 @@ const FORWARD_EDGE_OF_BATTLE_AREA_METADATA = {
11005
11218
  * Creates a Forward edge of the battle area control measure: a polyline
11006
11219
  * through `positions` labeled "FEBA" above the line near each end.
11007
11220
  */
11008
- function createForwardEdgeOfBattleArea(positions, options = {}, textAmplifiers = {}) {
11221
+ function createForwardEdgeOfBattleArea(positions, options = {}, textAmplifiers = {}, context = {}) {
11009
11222
  return fixedLabelLineFeatures(positions, {
11010
11223
  part: "forward-edge-of-battle-area",
11011
11224
  placement: "above",
11012
11225
  ...options
11013
- }, "FEBA", textAmplifiers.N);
11226
+ }, "FEBA", textAmplifiers.N, context);
11014
11227
  }
11015
11228
  const FORWARD_EDGE_OF_BATTLE_AREA = defineControlMeasure({
11016
11229
  metadata: FORWARD_EDGE_OF_BATTLE_AREA_METADATA,
@@ -11084,12 +11297,12 @@ const HANDOVER_LINE_METADATA = {
11084
11297
  * Creates a Handover line control measure: a polyline through `positions`
11085
11298
  * labeled "HOL" above the line near each end.
11086
11299
  */
11087
- function createHandoverLine(positions, options = {}, textAmplifiers = {}) {
11300
+ function createHandoverLine(positions, options = {}, textAmplifiers = {}, context = {}) {
11088
11301
  return fixedLabelLineFeatures(positions, {
11089
11302
  part: "handover-line",
11090
11303
  placement: "above",
11091
11304
  ...options
11092
- }, "HOL", textAmplifiers.N);
11305
+ }, "HOL", textAmplifiers.N, context);
11093
11306
  }
11094
11307
  const HANDOVER_LINE = defineControlMeasure({
11095
11308
  metadata: HANDOVER_LINE_METADATA,
@@ -11163,12 +11376,12 @@ const BATTLE_HANDOVER_LINE_METADATA = {
11163
11376
  * Creates a Battle handover line control measure: a polyline through
11164
11377
  * `positions` labeled "BHL" above the line near each end.
11165
11378
  */
11166
- function createBattleHandoverLine(positions, options = {}, textAmplifiers = {}) {
11379
+ function createBattleHandoverLine(positions, options = {}, textAmplifiers = {}, context = {}) {
11167
11380
  return fixedLabelLineFeatures(positions, {
11168
11381
  part: "battle-handover-line",
11169
11382
  placement: "above",
11170
11383
  ...options
11171
- }, "BHL", textAmplifiers.N);
11384
+ }, "BHL", textAmplifiers.N, context);
11172
11385
  }
11173
11386
  const BATTLE_HANDOVER_LINE = defineControlMeasure({
11174
11387
  metadata: BATTLE_HANDOVER_LINE_METADATA,
@@ -11320,7 +11533,7 @@ function generateFortifiedPoints(projectedPoints, effectiveSize) {
11320
11533
  * @param options - Configuration options for the graphic
11321
11534
  * @returns A GeoJSON FeatureCollection containing the castellated LineString
11322
11535
  */
11323
- function createFortifiedLine(positions, options = {}, textAmplifiers = {}) {
11536
+ function createFortifiedLine(positions, options = {}, textAmplifiers = {}, context = {}) {
11324
11537
  const { smooth = DEFAULT_FORTIFIED_LINE_OPTIONS.smooth, smoothResolution = DEFAULT_FORTIFIED_LINE_OPTIONS.smoothResolution } = options;
11325
11538
  const safeSize = calculateEffectiveSize(options);
11326
11539
  const projectedPoints = positions.map((p) => project(p[0], p[1]));
@@ -11334,7 +11547,7 @@ function createFortifiedLine(positions, options = {}, textAmplifiers = {}) {
11334
11547
  }
11335
11548
  };
11336
11549
  const labels = [];
11337
- pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options);
11550
+ pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options, "", context);
11338
11551
  return {
11339
11552
  type: "FeatureCollection",
11340
11553
  features: [feature, ...labels]
@@ -11418,7 +11631,7 @@ function createFortifiedArea(positions, options = {}, textAmplifiers = {}, conte
11418
11631
  }, options, context.amplifierPlacements, {
11419
11632
  ringVerts: boundaryPoints,
11420
11633
  extraClearanceMeters: 3 * safeSize
11421
- })),
11634
+ }, context)),
11422
11635
  properties: {}
11423
11636
  }];
11424
11637
  features.push(...labelFeatures);
@@ -12343,7 +12556,7 @@ const JOINT_TACTICAL_ACTION_AREA_METADATA = {
12343
12556
  * {@link labeledAreaFeatures}.
12344
12557
  */
12345
12558
  function createJointTacticalActionArea(positions, options = {}, textAmplifiers = {}, context = {}) {
12346
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "JTAA -", ""), context.amplifierPlacements);
12559
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "JTAA -", ""), context.amplifierPlacements, context);
12347
12560
  }
12348
12561
  const JOINT_TACTICAL_ACTION_AREA = defineControlMeasure({
12349
12562
  metadata: JOINT_TACTICAL_ACTION_AREA_METADATA,
@@ -12402,7 +12615,7 @@ const LANDING_ZONE_METADATA = {
12402
12615
  * amplifier rows, per {@link labeledAreaFeatures}.
12403
12616
  */
12404
12617
  function createLandingZone(positions, options = {}, textAmplifiers = {}, context = {}) {
12405
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "LZ"), context.amplifierPlacements);
12618
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "LZ"), context.amplifierPlacements, context);
12406
12619
  }
12407
12620
  const LANDING_ZONE = defineControlMeasure({
12408
12621
  metadata: LANDING_ZONE_METADATA,
@@ -12473,7 +12686,7 @@ function createLimitedAccessArea(positions, options = {}, textAmplifiers = {}, c
12473
12686
  }];
12474
12687
  return {
12475
12688
  type: "FeatureCollection",
12476
- features: [...patternedAreaFeatures(verts, pushAreaLabels(labelFeatures, verts, { hostile: textAmplifiers.N }, options, context.amplifierPlacements), "reverse-hatch"), ...labelFeatures]
12689
+ features: [...patternedAreaFeatures(verts, pushAreaLabels(labelFeatures, verts, { hostile: textAmplifiers.N }, options, context.amplifierPlacements, void 0, context), "reverse-hatch"), ...labelFeatures]
12477
12690
  };
12478
12691
  }
12479
12692
  const LIMITED_ACCESS_AREA = defineControlMeasure({
@@ -12584,7 +12797,7 @@ function createNoFireAreaIrregular(positions, options = {}, textAmplifiers = {},
12584
12797
  }
12585
12798
  return {
12586
12799
  type: "FeatureCollection",
12587
- features: [...patternedAreaFeatures(verts, pushAreaLabels(labels, verts, { hostile: textAmplifiers.N }, options, context.amplifierPlacements), "reverse-hatch"), ...labels]
12800
+ features: [...patternedAreaFeatures(verts, pushAreaLabels(labels, verts, { hostile: textAmplifiers.N }, options, context.amplifierPlacements, void 0, context), "reverse-hatch"), ...labels]
12588
12801
  };
12589
12802
  }
12590
12803
  const NO_FIRE_AREA_IRREGULAR = defineControlMeasure({
@@ -12929,7 +13142,7 @@ function createMinefield(coordinates, options = {}, textAmplifiers = {}, context
12929
13142
  clearance: ENY_CLEARANCE_RATIO * labelSize,
12930
13143
  sizeProps,
12931
13144
  followPlacementRotation: true
12932
- }, context.amplifierPlacements);
13145
+ }, context.amplifierPlacements, context, { labelSize });
12933
13146
  if (textAmplifiers.W) pushLabel(labelFeatures, offset(center, 0, -halfHeight - width * LABEL_CLEARANCE_RATIO), textAmplifiers.W, labelRotation, sizeProps, void 0, "W");
12934
13147
  const features = [{
12935
13148
  type: "Feature",
@@ -13146,7 +13359,7 @@ function createMineArea(positions, options, minedArea, textAmplifiers, context)
13146
13359
  textHeight: resolveLabelOffsetMeters(options, 1),
13147
13360
  clearance: resolveLabelOffsetMeters(options, .12 + labelPadding),
13148
13361
  sizeProps: labelSizeProps(options)
13149
- }) : [], ...pushAreaLabels(labelFeatures, ring, { hostile: textAmplifiers.N }, options, context.amplifierPlacements)], mineFillPattern(mineType)), ...labelFeatures]
13362
+ }, void 0, context, options) : [], ...pushAreaLabels(labelFeatures, ring, { hostile: textAmplifiers.N }, options, context.amplifierPlacements, void 0, context)], mineFillPattern(mineType)), ...labelFeatures]
13150
13363
  };
13151
13364
  }
13152
13365
  function createDynamicMinefield(positions, options = {}, textAmplifiers = {}, context = {}) {
@@ -13649,7 +13862,7 @@ const PICKUP_ZONE_METADATA = {
13649
13862
  * amplifier rows, per {@link labeledAreaFeatures}.
13650
13863
  */
13651
13864
  function createPickupZone(positions, options = {}, textAmplifiers = {}, context = {}) {
13652
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "PZ"), context.amplifierPlacements);
13865
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "PZ"), context.amplifierPlacements, context);
13653
13866
  }
13654
13867
  const PICKUP_ZONE = defineControlMeasure({
13655
13868
  metadata: PICKUP_ZONE_METADATA,
@@ -13704,7 +13917,7 @@ const ASSAULT_POSITION_METADATA = {
13704
13917
  * an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
13705
13918
  */
13706
13919
  function createAssaultPosition(positions, options = {}, textAmplifiers = {}, context = {}) {
13707
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "ASLT"), context.amplifierPlacements);
13920
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "ASLT"), context.amplifierPlacements, context);
13708
13921
  }
13709
13922
  const ASSAULT_POSITION = defineControlMeasure({
13710
13923
  metadata: ASSAULT_POSITION_METADATA,
@@ -13759,7 +13972,7 @@ const ATTACK_POSITION_METADATA = {
13759
13972
  * an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
13760
13973
  */
13761
13974
  function createAttackPosition(positions, options = {}, textAmplifiers = {}, context = {}) {
13762
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "ATK"), context.amplifierPlacements);
13975
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "ATK"), context.amplifierPlacements, context);
13763
13976
  }
13764
13977
  const ATTACK_POSITION = defineControlMeasure({
13765
13978
  metadata: ATTACK_POSITION_METADATA,
@@ -13814,7 +14027,7 @@ const OBJECTIVE_AREA_METADATA = {
13814
14027
  * an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
13815
14028
  */
13816
14029
  function createObjectiveArea(positions, options = {}, textAmplifiers = {}, context = {}) {
13817
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "OBJ"), context.amplifierPlacements);
14030
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "OBJ"), context.amplifierPlacements, context);
13818
14031
  }
13819
14032
  const OBJECTIVE_AREA = defineControlMeasure({
13820
14033
  metadata: OBJECTIVE_AREA_METADATA,
@@ -14097,7 +14310,7 @@ function createStrongPoint(positions, options = {}, textAmplifiers = {}, context
14097
14310
  const enyGaps = pushAreaLabels(labelFeatures, verts, {
14098
14311
  name: textAmplifiers.T,
14099
14312
  hostile: textAmplifiers.N
14100
- }, options, context.amplifierPlacements);
14313
+ }, options, context.amplifierPlacements, void 0, context);
14101
14314
  const allGaps = [...gaps, ...enyGaps];
14102
14315
  const boundaryCoords = buildGappedLine(verts, allGaps);
14103
14316
  const tics = buildStrongPointTics(perimeter, allGaps, h);
@@ -14203,7 +14416,7 @@ const SUBMARINE_ACTION_AREA_METADATA = {
14203
14416
  * `W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
14204
14417
  */
14205
14418
  function createSubmarineActionArea(positions, options = {}, textAmplifiers = {}, context = {}) {
14206
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "SAA -", ""), context.amplifierPlacements);
14419
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "SAA -", ""), context.amplifierPlacements, context);
14207
14420
  }
14208
14421
  const SUBMARINE_ACTION_AREA = defineControlMeasure({
14209
14422
  metadata: SUBMARINE_ACTION_AREA_METADATA,
@@ -14263,7 +14476,7 @@ const SUBMARINE_GENERATED_ACTION_AREA_METADATA = {
14263
14476
  * (Field T) plus `W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
14264
14477
  */
14265
14478
  function createSubmarineGeneratedActionArea(positions, options = {}, textAmplifiers = {}, context = {}) {
14266
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "SGSA -", ""), context.amplifierPlacements);
14479
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "SGSA -", ""), context.amplifierPlacements, context);
14267
14480
  }
14268
14481
  const SUBMARINE_GENERATED_ACTION_AREA = defineControlMeasure({
14269
14482
  metadata: SUBMARINE_GENERATED_ACTION_AREA_METADATA,
@@ -14669,6 +14882,7 @@ const DEFINITIONS = {
14669
14882
  circle: GENERIC_CIRCLE,
14670
14883
  sector: GENERIC_SECTOR,
14671
14884
  text: GENERIC_TEXT,
14885
+ "radar-search-doctrine": RADAR_SEARCH_DOCTRINE,
14672
14886
  "airborne-attack": AIRBORNE_ATTACK,
14673
14887
  "attack-helicopter": ATTACK_HELICOPTER,
14674
14888
  "support-by-fire": SUPPORT_BY_FIRE,
@@ -14960,7 +15174,31 @@ function dispatchControlMeasure(cm, opts) {
14960
15174
  if (!validateInputContract(cm.controlPoints, definition.metadata, opts?.validationMode)) return EMPTY_COLLECTION;
14961
15175
  const generator = definition.generator;
14962
15176
  const textAmplifiers = normalizeTextAmplifiers(cm.textAmplifiers);
14963
- return generator(cm.controlPoints, cm.options ?? {}, textAmplifiers, { amplifierPlacements: cm.amplifierPlacements });
15177
+ const constructionMetersPerCssPixel = resolveConstructionScale(cm.controlPoints, opts?.context?.groundMetersPerCssPixel);
15178
+ const authoredOptions = cm.options ?? {};
15179
+ const generatorOptions = constructionMetersPerCssPixel === void 0 ? authoredOptions : {
15180
+ ...authoredOptions,
15181
+ metersPerPixel: constructionMetersPerCssPixel
15182
+ };
15183
+ return generator(cm.controlPoints, generatorOptions, textAmplifiers, {
15184
+ amplifierPlacements: cm.amplifierPlacements,
15185
+ constructionMetersPerCssPixel,
15186
+ labelSizeClampCssPixels: opts?.context?.labelSizeClampCssPixels,
15187
+ measureText: opts?.context?.measureText
15188
+ });
15189
+ }
15190
+ /** Convert true-ground scale to the Web Mercator metres legacy generators construct in. */
15191
+ function resolveConstructionScale(points, groundMetersPerCssPixel) {
15192
+ if (!(groundMetersPerCssPixel !== void 0 && groundMetersPerCssPixel > 0)) return void 0;
15193
+ let minLatitude = Infinity;
15194
+ let maxLatitude = -Infinity;
15195
+ for (const point of points) {
15196
+ minLatitude = Math.min(minLatitude, point[1]);
15197
+ maxLatitude = Math.max(maxLatitude, point[1]);
15198
+ }
15199
+ const midpointLatitude = (minLatitude + maxLatitude) / 2;
15200
+ const mercatorScale = Math.cos(midpointLatitude * Math.PI / 180);
15201
+ return groundMetersPerCssPixel / Math.max(mercatorScale, Number.EPSILON);
14964
15202
  }
14965
15203
  /**
14966
15204
  * The control-point *input contract*: `controlPoints` must be present and an
@@ -15099,4 +15337,4 @@ function assertNever(value) {
15099
15337
  throw new Error(`Unhandled control measure kind: ${String(value)}`);
15100
15338
  }
15101
15339
  //#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 };
15340
+ export { DEFAULT_TACTICAL_ARROW_OPTIONS as $, line26DrawRule as $t, DEFAULT_REARWARD_PASSAGE_OF_LINES_OPTIONS as A, DEFAULT_AREA_OF_OPERATIONS_OPTIONS as At, DEFAULT_BRIDGEHEAD_LINE_OPTIONS as B, DEFAULT_LABEL_SIZE_CLAMP_CSS_PIXELS as Bt, DEFAULT_LANDING_ZONE_OPTIONS as C, project as Cn, DEFAULT_BLOCK_OPTIONS as Ct, DEFAULT_SEIZE_OPTIONS as D, getMetersPerPixel as Dn, DEFAULT_ANTITANK_WALL_OPTIONS as Dt, DEFAULT_WITHDRAW_OPTIONS as E, EPSILON as En, DEFAULT_ATTACK_BY_FIRE_OPTIONS as Et, DEFAULT_BATTLE_HANDOVER_LINE_OPTIONS as F, TEXT_AMPLIFIER_FIELDS as Ft, DEFAULT_FINAL_PROTECTIVE_FIRE_OPTIONS as G, DEFAULT_STROKE_WIDTH_CSS_PIXELS as Gt, DEFAULT_FLOT_OPTIONS as H, DEFAULT_LINE_JOIN as Ht, DEFAULT_HANDOVER_LINE_OPTIONS as I, canonicalTextAmplifierKey as It, DEFAULT_GUARD_OPTIONS as J, DEFAULT_AIRBORNE_ATTACK_OPTIONS as Jt, DEFAULT_ENCIRCLEMENT_OPTIONS as K, DEFAULT_SYMBOL_COLOR as Kt, DEFAULT_FORWARD_EDGE_OF_BATTLE_AREA_OPTIONS as L, normalizeTextAmplifiers as Lt, DEFAULT_FRONTAL_ATTACK_OPTIONS as M, DEFAULT_DIRECTION_OF_SUPPORTING_ATTACK_OPTIONS as Mt, DEFAULT_FORTIFIED_AREA_OPTIONS as N, DEFAULT_DIRECTION_OF_MAIN_ATTACK_OPTIONS as Nt, DEFAULT_SCREEN_OPTIONS as O, roundToFixed as On, DEFAULT_ANTITANK_DITCH_OPTIONS as Ot, DEFAULT_FORTIFIED_LINE_OPTIONS as P, DEFAULT_DIRECTION_OF_ATTACK_AVIATION_OPTIONS as Pt, DEFAULT_COVER_OPTIONS as Q, line27DrawRule as Qt, DEFAULT_RELEASE_LINE_OPTIONS as R, resolveAmplifierPlacement as Rt, DEFAULT_MAIN_ATTACK_OPTIONS as S, haversineDistance as Sn, DEFAULT_BLOCK_ARROW_OPTIONS as St, DEFAULT_WITHDRAW_UNDER_PRESSURE_OPTIONS as T, unproject as Tn, DEFAULT_ATTACK_HELICOPTER_OPTIONS as Tt, DEFAULT_FIX_MISSION_TASK_OPTIONS as U, DEFAULT_PORTRAYAL as Ut, DEFAULT_PHASE_LINE_OPTIONS as V, DEFAULT_LINE_CAP as Vt, DEFAULT_FIX_OPTIONS as W, DEFAULT_STROKE_DASH_CSS_PIXELS as Wt, DEFAULT_DISENGAGE_OPTIONS as X, axis1DrawRule as Xt, DEFAULT_DISRUPT_MISSION_TASK_OPTIONS as Y, rectangleDrawRule as Yt, DEFAULT_DELAY_OPTIONS as Z, supportByFireDrawRule as Zt, DEFAULT_PICKUP_ZONE_OPTIONS as _, pointOnMidpointPerpendicularAxis as _n, DEFAULT_BLOCK_MISSION_TASK_OPTIONS as _t, DEFINITIONS as a, attackByFireDrawRule as an, RADAR_SEARCH_DOCTRINE_FILL_COLOR as at, DEFAULT_OBSTACLE_BYPASS_DIFFICULT_OPTIONS as b, calculateMetrics as bn, DEFAULT_LIGHT_LINE_OPTIONS as bt, getDefaultOptions as c, staticPointDrawRule as cn, DEFAULT_GENERIC_SECTOR_OPTIONS as ct, DEFAULT_TURN_OPTIONS as d, centerRadiusDrawRule as dn, DEFAULT_GENERIC_LINE_OPTIONS as dt, line24DrawRule as en, DEFAULT_COUNTERATTACK_BY_FIRE_OPTIONS as et, DEFAULT_SUPPORT_BY_FIRE_OPTIONS as f, disruptDrawRule as fn, DEFAULT_GENERIC_CIRCLE_OPTIONS as ft, DEFAULT_PRINCIPAL_DIRECTION_OF_FIRE_OPTIONS as g, getMidpointPerpendicularSignedDistance as gn, DEFAULT_BREACH_OPTIONS as gt, SECONDARY_DIRECTION_OF_FIRE_DASH as h, createMidpointPerpendicularDrawRule as hn, DEFAULT_BYPASS_OPTIONS as ht, CONTROL_MEASURE_METADATA as i, ambushDrawRule as in, DEFAULT_RADAR_SEARCH_DOCTRINE_OPTIONS as it, DEFAULT_PENETRATE_OPTIONS as j, DEFAULT_AREA_DEFENSE_OPTIONS as jt, DEFAULT_RETIRE_OPTIONS as k, DEFAULT_ASSEMBLY_AREA_OPTIONS as kt, listControlMeasureMetadata as l, point12DrawRule as ln, DEFAULT_GENERIC_RECTANGLE_OPTIONS as lt, DEFAULT_SECONDARY_DIRECTION_OF_FIRE_OPTIONS as m, computeDefaultMidpointPerpendicularPoint as mn, DEFAULT_CANALIZE_OPTIONS as mt, resolveStyleHints as n, turnDrawRule as nn, DEFAULT_SUPPORTING_ATTACK_OPTIONS as nt, getControlMeasureMetadata as o, dynamicPointDrawRule as on, RADAR_SEARCH_DOCTRINE_STROKE_COLOR as ot, DEFAULT_STRONG_POINT_OPTIONS as p, blockDrawRule as pn, DEFAULT_CLASSIC_ARROW_OPTIONS as pt, DEFAULT_DISRUPT_OPTIONS as q, DEFAULT_AMBUSH_OPTIONS as qt, CONTROL_MEASURE_IDS as r, line1DrawRule as rn, DEFAULT_CLEAR_OPTIONS as rt, getControlMeasureMetadataByValue as s, sectorDrawRule as sn, DEFAULT_GENERIC_TEXT_OPTIONS as st, renderControlMeasure as t, line23DrawRule as tn, DEFAULT_COUNTERATTACK_OPTIONS as tt, DEFAULT_TURNING_MOVEMENT_OPTIONS as u, penetrateDrawRule as un, DEFAULT_GENERIC_POLYGON_OPTIONS as ut, DEFAULT_OBSTACLE_BYPASS_IMPOSSIBLE_OPTIONS as v, snapToMidpointPerpendicular as vn, DEFAULT_GENERIC_C2_LINE_OPTIONS as vt, DEFAULT_JOINT_TACTICAL_ACTION_AREA_OPTIONS as w, sphericalBearing as wn, DEFAULT_BATTLE_POSITION_OPTIONS as wt, DEFAULT_MINEFIELD_OPTIONS as x, computeInitialWidthPoint as xn, DEFAULT_BOUNDARY_OPTIONS as xt, DEFAULT_OBSTACLE_BYPASS_EASY_OPTIONS as y, createBaselineFrame as yn, DEFAULT_ENGINEER_WORK_LINE_OPTIONS as yt, DEFAULT_HOLDING_LINE_OPTIONS as z, DEFAULT_LABEL_HEIGHT_CSS_PIXELS as zt };