@orbat-mapper/control-measures 0.17.0 → 0.18.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.
@@ -673,7 +673,7 @@ function frameFor(p1, p2) {
673
673
  }
674
674
  //#endregion
675
675
  //#region src/draw-rules/area1.ts
676
- function derive$14(points) {
676
+ function derive$15(points) {
677
677
  return clonePositions(points);
678
678
  }
679
679
  /**
@@ -693,9 +693,9 @@ const area1DrawRule = {
693
693
  id: "area1",
694
694
  minimumUserPoints: 3,
695
695
  closedRing: true,
696
- derive: derive$14,
696
+ derive: derive$15,
697
697
  transform(event) {
698
- return derive$14(event.next);
698
+ return derive$15(event.next);
699
699
  }
700
700
  };
701
701
  //#endregion
@@ -735,14 +735,14 @@ const containDrawRule = createMidpointPerpendicularDrawRule({
735
735
  * that fixes both the radius and the bearing of the symbol's opening. Both
736
736
  * points are user-clicked, so the canonical array is just the (clamped) input.
737
737
  */
738
- function derive$13(points) {
738
+ function derive$14(points) {
739
739
  return clonePositions(points.slice(0, 2));
740
740
  }
741
741
  const centerRadiusDrawRule = {
742
742
  id: "area15:center-radius",
743
743
  minimumUserPoints: 2,
744
744
  canonicalPointCount: 2,
745
- derive: derive$13,
745
+ derive: derive$14,
746
746
  transform(event) {
747
747
  const { previous, next, activePointIndex } = event;
748
748
  if (activePointIndex === 0 && previous.length >= 2 && next.length >= 2) {
@@ -751,7 +751,7 @@ const centerRadiusDrawRule = {
751
751
  const origin = previous[1];
752
752
  return [clonePosition(next[0]), [origin[0] + dx, origin[1] + dy]];
753
753
  }
754
- return derive$13(next);
754
+ return derive$14(next);
755
755
  }
756
756
  };
757
757
  /** Backward-compatible doctrinal name for Area15's shared center-radius rule. */
@@ -810,6 +810,72 @@ const penetrateDrawRule = createMidpointPerpendicularDrawRule({
810
810
  minimumPreviewPoints: 2
811
811
  });
812
812
  //#endregion
813
+ //#region src/draw-rules/area18.ts
814
+ const DEFAULT_OFFSET_RATIO = .6;
815
+ function createArea18DrawRule(id, normal) {
816
+ function derive(points) {
817
+ if (points.length < 2) return clonePositions(points);
818
+ const p1 = points[0];
819
+ const p2 = points[1];
820
+ if (points.length >= 4) return clonePositions(points.slice(0, 4));
821
+ const firstTip = project(p1[0], p1[1]);
822
+ const firstEnd = project(p2[0], p2[1]);
823
+ const frame = createProjectedBaselineFrame(firstTip, firstEnd, {
824
+ origin: "p1",
825
+ normal
826
+ });
827
+ let point4;
828
+ let p4;
829
+ if (points.length === 3) {
830
+ p4 = points[2];
831
+ point4 = project(p4[0], p4[1]);
832
+ } else if (frame) {
833
+ point4 = vecAdd(frame.origin, vecScale(frame.normal, frame.length * DEFAULT_OFFSET_RATIO));
834
+ p4 = unproject(point4[0], point4[1]);
835
+ } else {
836
+ point4 = firstTip;
837
+ p4 = [...p1];
838
+ }
839
+ const point3 = vecAdd(point4, vecSub(firstEnd, firstTip));
840
+ return clonePositions([
841
+ p1,
842
+ p2,
843
+ unproject(point3[0], point3[1]),
844
+ p4
845
+ ]);
846
+ }
847
+ return {
848
+ id,
849
+ minimumUserPoints: 2,
850
+ minimumPreviewPoints: 2,
851
+ canonicalPointCount: 4,
852
+ derive,
853
+ transform(event) {
854
+ return clonePositions(event.next.slice(0, 4));
855
+ }
856
+ };
857
+ }
858
+ /**
859
+ * Area18 anchor draw rule for Relief in Place.
860
+ *
861
+ * The user clicks PT.1 and PT.2. Those points derive the complete default
862
+ * hairpin for commit. Once committed, PT.1 through PT.4 are all independent,
863
+ * draggable anchors.
864
+ */
865
+ const reliefInPlaceDrawRule = createArea18DrawRule("area18:relief-in-place", "right");
866
+ /** Demonstrate uses the mirrored Area18 default, with its turn above P1→P2. */
867
+ const demonstrateDrawRule = createArea18DrawRule("area18:demonstrate", "left");
868
+ //#endregion
869
+ //#region src/draw-rules/area19.ts
870
+ /**
871
+ * Area19 — center + start/radius point, mechanically identical to Area15.
872
+ * The distinct id preserves the doctrinal draw-rule identity in metadata.
873
+ */
874
+ const secureDrawRule = {
875
+ ...centerRadiusDrawRule,
876
+ id: "area19:secure"
877
+ };
878
+ //#endregion
813
879
  //#region src/draw-rules/point12.ts
814
880
  /**
815
881
  * Point12 anchor draw rule for the obstacle-bypass family.
@@ -827,16 +893,16 @@ const point12DrawRule = createMidpointPerpendicularDrawRule({ id: "point12:obsta
827
893
  * Point1 — a single anchor point (e.g. Text's center). One user click commits
828
894
  * the measure; dragging the point simply translates it.
829
895
  */
830
- function derive$12(points) {
896
+ function derive$13(points) {
831
897
  return clonePositions(points.slice(0, 1));
832
898
  }
833
899
  const pointDrawRule = {
834
900
  id: "point1:anchor",
835
901
  minimumUserPoints: 1,
836
902
  canonicalPointCount: 1,
837
- derive: derive$12,
903
+ derive: derive$13,
838
904
  transform(event) {
839
- return derive$12(event.next);
905
+ return derive$13(event.next);
840
906
  }
841
907
  };
842
908
  //#endregion
@@ -863,7 +929,7 @@ const SECTOR_ANGLE_SNAP_RADIANS = Math.PI / 36;
863
929
  * independently control a radius and a true-north azimuth. Dragging the anchor
864
930
  * translates both edge points so the sector retains its size and orientation.
865
931
  */
866
- function derive$11(points) {
932
+ function derive$12(points) {
867
933
  return clonePositions(points.slice(0, 3));
868
934
  }
869
935
  function guidePoints$2(points) {
@@ -891,7 +957,7 @@ const sectorDrawRule = {
891
957
  showGuide: true,
892
958
  guidePoints: guidePoints$2,
893
959
  constrainAngles,
894
- derive: derive$11,
960
+ derive: derive$12,
895
961
  transform(event) {
896
962
  const { previous, next, activePointIndex } = event;
897
963
  if (activePointIndex === 0 && previous.length >= 3 && next.length >= 3) {
@@ -903,7 +969,7 @@ const sectorDrawRule = {
903
969
  [previous[2][0] + dx, previous[2][1] + dy]
904
970
  ];
905
971
  }
906
- return derive$11(next);
972
+ return derive$12(next);
907
973
  }
908
974
  };
909
975
  //#endregion
@@ -957,6 +1023,56 @@ const ambushDrawRule = createMidpointPerpendicularDrawRule({
957
1023
  defaultDistanceRatio: -.5
958
1024
  });
959
1025
  //#endregion
1026
+ //#region src/draw-rules/line30.ts
1027
+ const DEFAULT_FIN_LENGTH_RATIO = .25;
1028
+ /** PT. 3 on the 45-degree fin ray leaving `p2`, `side` picking which fin. */
1029
+ function finAnchor(p2, direction, normal, length, side) {
1030
+ const anchor = vecAdd(p2, vecScale(vecNorm(vecAdd(vecScale(direction, -1), vecScale(normal, side))), length));
1031
+ return unproject(anchor[0], anchor[1]);
1032
+ }
1033
+ /**
1034
+ * Snaps PT. 3 onto the 45-degree fin ray on whichever side of the PT. 1 - PT. 2
1035
+ * axis the raw point falls. `finRatio` replaces the distance carried by the raw
1036
+ * PT. 3 with a share of the axis length, which is how dragging PT. 1 or PT. 2
1037
+ * keeps the fin proportional instead of pinning its absolute size.
1038
+ */
1039
+ function derive$11(points, finRatio) {
1040
+ const derived = clonePositions(points.slice(0, 3));
1041
+ if (derived.length < 2) return derived;
1042
+ const p1 = project(derived[0][0], derived[0][1]);
1043
+ const p2 = project(derived[1][0], derived[1][1]);
1044
+ const axis = vecSub(p1, p2);
1045
+ const axisLength = vecMag(axis);
1046
+ if (axisLength === 0) return derived;
1047
+ const direction = vecNorm(axis);
1048
+ const normal = [-direction[1], direction[0]];
1049
+ if (derived.length === 2) {
1050
+ const length = axisLength * (finRatio ?? DEFAULT_FIN_LENGTH_RATIO);
1051
+ derived.push(finAnchor(p2, direction, normal, length, 1));
1052
+ return derived;
1053
+ }
1054
+ const offset = vecSub(project(derived[2][0], derived[2][1]), p2);
1055
+ const side = vecDot(offset, normal) < 0 ? -1 : 1;
1056
+ derived[2] = finAnchor(p2, direction, normal, finRatio === void 0 ? vecMag(offset) : axisLength * finRatio, side);
1057
+ return derived;
1058
+ }
1059
+ /** Line30 anchors for Exploit/Exploitation; PT. 3 controls the 45-degree fin length. */
1060
+ const line30DrawRule = {
1061
+ id: "line30:exploitation",
1062
+ minimumUserPoints: 2,
1063
+ minimumPreviewPoints: 2,
1064
+ canonicalPointCount: 3,
1065
+ derive: derive$11,
1066
+ transform(event) {
1067
+ const { activePointIndex, next, previous } = event;
1068
+ if (!(activePointIndex === 0 || activePointIndex === 1) || next.length < 3 || previous.length < 3) return derive$11(next);
1069
+ const oldP2 = project(previous[1][0], previous[1][1]);
1070
+ const oldAxisLength = vecMag(vecSub(project(previous[0][0], previous[0][1]), oldP2));
1071
+ const oldFinLength = vecMag(vecSub(project(previous[2][0], previous[2][1]), oldP2));
1072
+ return derive$11(next, oldAxisLength === 0 ? DEFAULT_FIN_LENGTH_RATIO : oldFinLength / oldAxisLength);
1073
+ }
1074
+ };
1075
+ //#endregion
960
1076
  //#region src/draw-rules/line1.ts
961
1077
  function derive$10(points) {
962
1078
  return clonePositions(points);
@@ -3015,6 +3131,70 @@ function labelBoxIntersectsSegment(anchor, a, b, textWidth, textHeight, rotation
3015
3131
  }
3016
3132
  return true;
3017
3133
  }
3134
+ /**
3135
+ * Splits the polyline `points` into the pieces that fall outside a
3136
+ * `2*halfWidth × 2*halfHeight` label box centered at `anchor` with stored
3137
+ * `rotation` — the clipping counterpart to {@link labelBoxIntersectsSegment},
3138
+ * for ornament geometry (barbs, ticks) that must break around a glyph rather
3139
+ * than merely decide whether it collides.
3140
+ *
3141
+ * Each segment is clipped in the box's own frame with a two-slab parametric
3142
+ * test; pieces that rejoin across a shared vertex are merged so a polyline
3143
+ * crossing the box mid-span stays one line string per surviving run instead of
3144
+ * fragmenting at every interior vertex.
3145
+ */
3146
+ function clipPolylineOutsideLabelBox(points, anchor, halfWidth, halfHeight, rotation) {
3147
+ const [widthAxis, heightAxis] = labelBoxAxes(rotation);
3148
+ const out = [];
3149
+ const append = (piece) => {
3150
+ if (piece.length < 2 || vecMag(vecSub(piece[1], piece[0])) < 1e-6) return;
3151
+ const previous = out.at(-1);
3152
+ if (previous && vecMag(vecSub(previous.at(-1), piece[0])) < 1e-6) previous.push(...piece.slice(1));
3153
+ else out.push(piece);
3154
+ };
3155
+ for (let i = 1; i < points.length; i++) {
3156
+ const a = points[i - 1];
3157
+ const b = points[i];
3158
+ const delta = vecSub(b, a);
3159
+ const relative = vecSub(a, anchor);
3160
+ const origin = [vecDot(relative, widthAxis), vecDot(relative, heightAxis)];
3161
+ const direction = [vecDot(delta, widthAxis), vecDot(delta, heightAxis)];
3162
+ let enter = 0;
3163
+ let exit = 1;
3164
+ let intersects = true;
3165
+ for (const [coordinate, rate, halfExtent] of [[
3166
+ origin[0],
3167
+ direction[0],
3168
+ halfWidth
3169
+ ], [
3170
+ origin[1],
3171
+ direction[1],
3172
+ halfHeight
3173
+ ]]) {
3174
+ if (Math.abs(rate) < 1e-6) {
3175
+ if (Math.abs(coordinate) > halfExtent) intersects = false;
3176
+ continue;
3177
+ }
3178
+ const first = (-halfExtent - coordinate) / rate;
3179
+ const second = (halfExtent - coordinate) / rate;
3180
+ enter = Math.max(enter, Math.min(first, second));
3181
+ exit = Math.min(exit, Math.max(first, second));
3182
+ if (enter > exit) intersects = false;
3183
+ }
3184
+ if (!intersects || exit < 0 || enter > 1) {
3185
+ append([a, b]);
3186
+ continue;
3187
+ }
3188
+ const clippedEnter = Math.max(0, enter);
3189
+ const clippedExit = Math.min(1, exit);
3190
+ const pointAt = (t) => t <= 0 ? a : t >= 1 ? b : vecAdd(a, vecScale(delta, t));
3191
+ if (clippedEnter > 0) append([a, pointAt(clippedEnter)]);
3192
+ if (clippedExit < 1) append([pointAt(clippedExit), b]);
3193
+ }
3194
+ return out;
3195
+ }
3196
+ /** Default extra clearance for an intrinsic label that interrupts a stroke. */
3197
+ const DEFAULT_LABEL_KNOCKOUT_PADDING = .3;
3018
3198
  /** Field N descriptor shared by Line1 graphics that opt into hostile endpoint markers. */
3019
3199
  const HOSTILE_LINE_AMPLIFIER = {
3020
3200
  key: "N",
@@ -3023,12 +3203,12 @@ const HOSTILE_LINE_AMPLIFIER = {
3023
3203
  placeholder: "ENY",
3024
3204
  maxLength: 3
3025
3205
  };
3026
- /** Label-padding control shared by newly label-bearing Line1 graphics. */
3027
- const LINE_LABEL_PADDING_PARAM = {
3206
+ /** Label-padding control shared by label-bearing graphics. */
3207
+ const LABEL_PADDING_PARAM = {
3028
3208
  key: "labelPadding",
3029
3209
  presentationTier: "advanced",
3030
3210
  label: "Label padding",
3031
- description: "Extra clearance between the line and its labels, as a ratio of the label height",
3211
+ description: "Extra clearance around labels, as a ratio of the label height",
3032
3212
  type: "number",
3033
3213
  min: 0,
3034
3214
  max: 2,
@@ -3136,8 +3316,8 @@ function estimatedTextWidth(text, textHeightMeters) {
3136
3316
  return text.length * LABEL_CHAR_ASPECT_RATIO * textHeightMeters;
3137
3317
  }
3138
3318
  /** Resolve the label's final display size, then measure and convert it to construction metres. */
3139
- function resolveTextMetrics(text, options, context, style = "regular") {
3140
- const legacyHeight = resolveLabelOffsetMeters(options, 1);
3319
+ function resolveTextMetrics(text, options, context, style = "regular", fallbackHeightMeters = 50) {
3320
+ const legacyHeight = resolveLabelOffsetMeters(options, 1, fallbackHeightMeters);
3141
3321
  const constructionScale = context?.constructionMetersPerCssPixel;
3142
3322
  if (!(constructionScale !== void 0 && constructionScale > 0)) return {
3143
3323
  width: estimatedTextWidth(text, legacyHeight),
@@ -3168,6 +3348,28 @@ function resolveTextMetrics(text, options, context, style = "regular") {
3168
3348
  height: measured.heightCssPixels * constructionScale
3169
3349
  };
3170
3350
  }
3351
+ /**
3352
+ * Half-length of the knockout required for a label along `direction`.
3353
+ *
3354
+ * The text box itself is always cleared; `labelPadding` adds symmetric clearance expressed in
3355
+ * resolved label heights. Keeping this calculation here gives every generator the same public
3356
+ * label-clearance interface while hiding font measurement and rotated-box projection details.
3357
+ */
3358
+ function labelKnockoutHalfExtent(text, direction, rotation, options, context, defaultPadding = 0) {
3359
+ const metrics = resolveTextMetrics(text, options, context);
3360
+ const padding = Math.max(0, options.labelPadding ?? defaultPadding) * metrics.height;
3361
+ return labelHalfExtent(direction, metrics.width, metrics.height, rotation) + padding;
3362
+ }
3363
+ /**
3364
+ * {@link labelKnockoutHalfExtent} expressed as a fraction of an arc's angular
3365
+ * sweep, for generators that carve their knockout in arc-progress space rather
3366
+ * than in meters. Clamped to `0.5` so the gap can never exceed the arc.
3367
+ */
3368
+ function arcKnockoutHalfFraction(halfExtentMeters, radius, sweepAngle) {
3369
+ const arcLength = radius * Math.abs(sweepAngle);
3370
+ if (!(arcLength > 1e-6)) return .5;
3371
+ return Math.min(.5, Math.max(0, halfExtentMeters) / arcLength);
3372
+ }
3171
3373
  /** The end-label text for a phase-line naming, or `""` when unnamed. */
3172
3374
  function phaseLineLabelText(name, includePrefix) {
3173
3375
  if (!name) return "";
@@ -3632,6 +3834,7 @@ const DIRECTION_OF_ATTACK_AVIATION_METADATA = {
3632
3834
  },
3633
3835
  drawRule: "Line1",
3634
3836
  capturesLabelSize: true,
3837
+ labelGeometryUsesResolvedMetrics: ["T"],
3635
3838
  params: [
3636
3839
  ...SMOOTH_LINE_PARAMS,
3637
3840
  {
@@ -3870,6 +4073,7 @@ const DIRECTION_OF_MAIN_ATTACK_METADATA = {
3870
4073
  },
3871
4074
  drawRule: "Line1",
3872
4075
  capturesLabelSize: true,
4076
+ labelGeometryUsesResolvedMetrics: ["T"],
3873
4077
  params: [
3874
4078
  ...SMOOTH_LINE_PARAMS,
3875
4079
  ...DIRECTION_OF_ATTACK_PARAMS,
@@ -3938,6 +4142,7 @@ const DIRECTION_OF_SUPPORTING_ATTACK_METADATA = {
3938
4142
  },
3939
4143
  drawRule: "Line1",
3940
4144
  capturesLabelSize: true,
4145
+ labelGeometryUsesResolvedMetrics: ["T"],
3941
4146
  params: [...SMOOTH_LINE_PARAMS, ...DIRECTION_OF_ATTACK_PARAMS],
3942
4147
  textAmplifiers: DIRECTION_OF_ATTACK_TEXT_AMPLIFIERS
3943
4148
  };
@@ -4568,6 +4773,7 @@ const AREA_METADATA = {
4568
4773
  },
4569
4774
  drawRule: "Area1",
4570
4775
  capturesLabelSize: true,
4776
+ labelGeometryUsesResolvedMetrics: ["N"],
4571
4777
  params: LABELED_AREA_PARAMS,
4572
4778
  textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
4573
4779
  };
@@ -4831,6 +5037,7 @@ const ENGAGEMENT_AREA_METADATA = {
4831
5037
  },
4832
5038
  drawRule: "Area1",
4833
5039
  capturesLabelSize: true,
5040
+ labelGeometryUsesResolvedMetrics: ["N"],
4834
5041
  params: PREFIXED_LABELED_AREA_PARAMS,
4835
5042
  textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
4836
5043
  };
@@ -4897,6 +5104,26 @@ function sampleProjectedArc(center, startRadius, bulgeRadius, start, end, resolu
4897
5104
  return unproject(point[0], point[1]);
4898
5105
  });
4899
5106
  }
5107
+ /**
5108
+ * Samples the 180-degree arc standing on the diameter `start`→`end`, bulging
5109
+ * toward `bulgeDir`. Shared by the mission tasks whose shaft turns back on
5110
+ * itself (Delay, Relief in Place, Demonstrate); `requestedSegments` is the
5111
+ * segment count for the half-turn.
5112
+ */
5113
+ function createSemicircleArc(start, end, bulgeDir, requestedSegments) {
5114
+ const diameterVector = vecSub(end, start);
5115
+ const diameter = vecMag(diameterVector);
5116
+ if (diameter < 1e-6) return [unproject(start[0], start[1])];
5117
+ const radius = diameter / 2;
5118
+ const center = vecAdd(start, vecScale(diameterVector, .5));
5119
+ const startRadius = vecScale(diameterVector, -.5);
5120
+ const bulgeRadius = vecScale(bulgeDir, radius);
5121
+ const segments = Math.max(4, Math.round(requestedSegments));
5122
+ return Array.from({ length: segments + 1 }, (_, index) => {
5123
+ const point = arcPointAt(center, startRadius, bulgeRadius, index / segments * Math.PI);
5124
+ return unproject(point[0], point[1]);
5125
+ });
5126
+ }
4900
5127
  //#endregion
4901
5128
  //#region src/generators/cm15-maneuver-areas/mobile-defense.ts
4902
5129
  const DEFAULT_MOBILE_DEFENSE_OPTIONS = { labelPadding: 0 };
@@ -4922,6 +5149,7 @@ const MOBILE_DEFENSE_METADATA = {
4922
5149
  },
4923
5150
  drawRule: "Area27",
4924
5151
  capturesLabelSize: true,
5152
+ labelGeometryUsesResolvedMetrics: true,
4925
5153
  params: [{
4926
5154
  key: "labelPadding",
4927
5155
  presentationTier: "advanced",
@@ -5098,6 +5326,7 @@ const AREA_OF_OPERATIONS_METADATA = {
5098
5326
  },
5099
5327
  drawRule: "Area1",
5100
5328
  capturesLabelSize: true,
5329
+ labelGeometryUsesResolvedMetrics: ["N"],
5101
5330
  params: PREFIXED_LABELED_AREA_PARAMS,
5102
5331
  textAmplifiers: AREA_TEXT_AMPLIFIERS
5103
5332
  };
@@ -5160,6 +5389,7 @@ const NAMED_AREA_OF_INTEREST_METADATA = {
5160
5389
  },
5161
5390
  drawRule: "Area1",
5162
5391
  capturesLabelSize: true,
5392
+ labelGeometryUsesResolvedMetrics: ["N"],
5163
5393
  params: PREFIXED_LABELED_AREA_PARAMS,
5164
5394
  textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
5165
5395
  };
@@ -5219,6 +5449,7 @@ const TARGET_AREA_OF_INTEREST_METADATA = {
5219
5449
  },
5220
5450
  drawRule: "Area1",
5221
5451
  capturesLabelSize: true,
5452
+ labelGeometryUsesResolvedMetrics: ["N"],
5222
5453
  params: PREFIXED_LABELED_AREA_PARAMS,
5223
5454
  textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
5224
5455
  };
@@ -5287,6 +5518,7 @@ const AIRFIELD_ZONE_METADATA = {
5287
5518
  },
5288
5519
  drawRule: "Area1",
5289
5520
  capturesLabelSize: true,
5521
+ labelGeometryUsesResolvedMetrics: ["N"],
5290
5522
  params: [
5291
5523
  ...AREA_SMOOTH_PARAMS,
5292
5524
  {
@@ -5471,6 +5703,7 @@ const BASE_CAMP_METADATA = {
5471
5703
  },
5472
5704
  drawRule: "Area1",
5473
5705
  capturesLabelSize: true,
5706
+ labelGeometryUsesResolvedMetrics: ["N"],
5474
5707
  params: PREFIXED_ECHELON_AREA_PARAMS,
5475
5708
  textAmplifiers: AREA_TEXT_AMPLIFIERS
5476
5709
  };
@@ -5529,6 +5762,7 @@ const GUERRILLA_BASE_METADATA = {
5529
5762
  },
5530
5763
  drawRule: "Area1",
5531
5764
  capturesLabelSize: true,
5765
+ labelGeometryUsesResolvedMetrics: ["N"],
5532
5766
  params: PREFIXED_ECHELON_AREA_PARAMS,
5533
5767
  textAmplifiers: AREA_TEXT_AMPLIFIERS
5534
5768
  };
@@ -5592,6 +5826,7 @@ const GENERIC_C2_AREA_METADATA = {
5592
5826
  },
5593
5827
  drawRule: "Area1",
5594
5828
  capturesLabelSize: true,
5829
+ labelGeometryUsesResolvedMetrics: ["N"],
5595
5830
  params: LABELED_AREA_PARAMS,
5596
5831
  textAmplifiers: AREA_TEXT_AMPLIFIERS
5597
5832
  };
@@ -5647,6 +5882,7 @@ const ASSEMBLY_AREA_METADATA = {
5647
5882
  },
5648
5883
  drawRule: "Area1",
5649
5884
  capturesLabelSize: true,
5885
+ labelGeometryUsesResolvedMetrics: ["N"],
5650
5886
  params: PREFIXED_LABELED_AREA_PARAMS,
5651
5887
  textAmplifiers: AREA_TEXT_AMPLIFIERS
5652
5888
  };
@@ -5698,7 +5934,7 @@ const FORTIFIED_PARAMS = [
5698
5934
  step: 1,
5699
5935
  unit: "m"
5700
5936
  },
5701
- LINE_LABEL_PADDING_PARAM
5937
+ LABEL_PADDING_PARAM
5702
5938
  ];
5703
5939
  const ANTITANK_DITCH_PARAMS = [
5704
5940
  ...SMOOTH_LINE_PARAMS,
@@ -5733,7 +5969,7 @@ const ANTITANK_DITCH_PARAMS = [
5733
5969
  step: .05,
5734
5970
  unit: "x"
5735
5971
  },
5736
- LINE_LABEL_PADDING_PARAM
5972
+ LABEL_PADDING_PARAM
5737
5973
  ];
5738
5974
  const ANTITANK_WALL_PARAMS = [...ANTITANK_DITCH_PARAMS, {
5739
5975
  key: "toothSpacingRatio",
@@ -5776,6 +6012,7 @@ const ANTITANK_DITCH_UNDER_CONSTRUCTION_METADATA = {
5776
6012
  },
5777
6013
  drawRule: "Line1",
5778
6014
  capturesLabelSize: true,
6015
+ labelGeometryUsesResolvedMetrics: ["N"],
5779
6016
  params: ANTITANK_DITCH_PARAMS,
5780
6017
  textAmplifiers: [HOSTILE_LINE_AMPLIFIER]
5781
6018
  };
@@ -5797,6 +6034,7 @@ const ANTITANK_DITCH_COMPLETED_METADATA = {
5797
6034
  },
5798
6035
  drawRule: "Line1",
5799
6036
  capturesLabelSize: true,
6037
+ labelGeometryUsesResolvedMetrics: ["N"],
5800
6038
  params: ANTITANK_DITCH_PARAMS,
5801
6039
  textAmplifiers: [HOSTILE_LINE_AMPLIFIER]
5802
6040
  };
@@ -5929,6 +6167,7 @@ const ANTITANK_WALL_METADATA = {
5929
6167
  },
5930
6168
  drawRule: "Line1",
5931
6169
  capturesLabelSize: true,
6170
+ labelGeometryUsesResolvedMetrics: ["N"],
5932
6171
  params: ANTITANK_WALL_PARAMS,
5933
6172
  textAmplifiers: [HOSTILE_LINE_AMPLIFIER]
5934
6173
  };
@@ -6395,6 +6634,7 @@ const BATTLE_POSITION_METADATA = {
6395
6634
  },
6396
6635
  drawRule: "Area1",
6397
6636
  capturesLabelSize: true,
6637
+ labelGeometryUsesResolvedMetrics: ["N"],
6398
6638
  params: ECHELON_AREA_PARAMS,
6399
6639
  textAmplifiers: AREA_UNIT_DESIGNATION_HOSTILE_AMPLIFIERS
6400
6640
  };
@@ -6538,6 +6778,7 @@ const BATTLE_POSITION_PREPARED_METADATA = {
6538
6778
  },
6539
6779
  drawRule: "Area1",
6540
6780
  capturesLabelSize: true,
6781
+ labelGeometryUsesResolvedMetrics: ["N"],
6541
6782
  params: PREFIXED_ECHELON_AREA_PARAMS,
6542
6783
  textAmplifiers: AREA_UNIT_DESIGNATION_HOSTILE_AMPLIFIERS
6543
6784
  };
@@ -6606,6 +6847,7 @@ const CONTAIN_METADATA = {
6606
6847
  },
6607
6848
  drawRule: "Area5",
6608
6849
  capturesLabelSize: true,
6850
+ labelGeometryUsesResolvedMetrics: true,
6609
6851
  textAmplifiers: [AREA_HOSTILE_AMPLIFIER],
6610
6852
  params: [{
6611
6853
  key: "labelPadding",
@@ -7501,6 +7743,7 @@ const BOUNDARY_METADATA = {
7501
7743
  text: true
7502
7744
  },
7503
7745
  drawRule: "Line1",
7746
+ labelGeometryUsesResolvedMetrics: ["N"],
7504
7747
  params: [
7505
7748
  {
7506
7749
  key: "smooth",
@@ -7919,6 +8162,7 @@ const GENERIC_C2_LINE_METADATA = {
7919
8162
  },
7920
8163
  drawRule: "Line1",
7921
8164
  capturesLabelSize: true,
8165
+ labelGeometryUsesResolvedMetrics: ["N"],
7922
8166
  params: [
7923
8167
  ...SMOOTH_LINE_PARAMS,
7924
8168
  ...echelonLineParams("line"),
@@ -7940,7 +8184,7 @@ const GENERIC_C2_LINE_METADATA = {
7940
8184
  type: "boolean",
7941
8185
  visibleWhen: (opts) => Boolean(String(opts.phaseLineName ?? "").trim())
7942
8186
  },
7943
- LINE_LABEL_PADDING_PARAM
8187
+ LABEL_PADDING_PARAM
7944
8188
  ],
7945
8189
  textAmplifiers: [
7946
8190
  {
@@ -8167,7 +8411,7 @@ function createLabelFeature(point, text, rotation, labelSize = {}) {
8167
8411
  * @param config - Per-measure end caps, label glyph, and sizing
8168
8412
  * @returns GeoJSON FeatureCollection containing MultiLineString and Point features
8169
8413
  */
8170
- function createBracketSymbol(coordinates, config) {
8414
+ function createBracketSymbol(coordinates, config, context) {
8171
8415
  const [c1, c2, c3] = coordinates;
8172
8416
  const p1 = project(c1[0], c1[1]);
8173
8417
  const p2 = project(c2[0], c2[1]);
@@ -8187,7 +8431,10 @@ function createBracketSymbol(coordinates, config) {
8187
8431
  const rearMidpoint = vecAdd(frontMidpoint, vecScale(perpDir, perpDistance));
8188
8432
  const rearTop = vecSub(rearMidpoint, halfFront);
8189
8433
  const rearBottom = vecAdd(rearMidpoint, halfFront);
8190
- const gapHalf = frontLength * config.labelGapRatio / 2;
8434
+ const rearLineRotation = Math.atan2(dirFront[1], dirFront[0]);
8435
+ const renderedLabelRotation = Math.PI - (rearLineRotation - Math.PI / 2);
8436
+ const labelRotation = normalizeRadians(Math.PI - keepTextLeftToRight(renderedLabelRotation));
8437
+ const gapHalf = Math.min(frontLength / 2, labelKnockoutHalfExtent(config.labelText, dirFront, labelRotation, config.labelOptions ?? {}, context));
8191
8438
  const gapPt1 = vecAdd(rearMidpoint, vecScale(dirFront, -gapHalf));
8192
8439
  const gapPt2 = vecAdd(rearMidpoint, vecScale(dirFront, gapHalf));
8193
8440
  const [topCap, bottomCap] = config.buildEndCaps({
@@ -8198,39 +8445,35 @@ function createBracketSymbol(coordinates, config) {
8198
8445
  frontLength,
8199
8446
  rearTop
8200
8447
  });
8201
- const mainFeature = {
8202
- type: "Feature",
8203
- properties: { part: config.part },
8204
- geometry: {
8205
- type: "MultiLineString",
8206
- coordinates: [
8207
- topCap,
8208
- [
8209
- unproject(p1[0], p1[1]),
8210
- unproject(rearTop[0], rearTop[1]),
8211
- unproject(gapPt1[0], gapPt1[1])
8212
- ],
8213
- [
8214
- unproject(gapPt2[0], gapPt2[1]),
8215
- unproject(rearBottom[0], rearBottom[1]),
8216
- unproject(p2[0], p2[1])
8217
- ],
8218
- bottomCap
8219
- ]
8220
- }
8221
- };
8222
- const rearLineRotation = Math.atan2(dirFront[1], dirFront[0]);
8223
- const renderedLabelRotation = Math.PI - (rearLineRotation - Math.PI / 2);
8224
- const labelRotation = normalizeRadians(Math.PI - keepTextLeftToRight(renderedLabelRotation));
8225
8448
  return {
8226
8449
  type: "FeatureCollection",
8227
- features: [mainFeature, createLabelFeature(rearMidpoint, config.labelText, labelRotation, config.labelSize)]
8450
+ features: [{
8451
+ type: "Feature",
8452
+ properties: { part: config.part },
8453
+ geometry: {
8454
+ type: "MultiLineString",
8455
+ coordinates: [
8456
+ topCap,
8457
+ [
8458
+ unproject(p1[0], p1[1]),
8459
+ unproject(rearTop[0], rearTop[1]),
8460
+ unproject(gapPt1[0], gapPt1[1])
8461
+ ],
8462
+ [
8463
+ unproject(gapPt2[0], gapPt2[1]),
8464
+ unproject(rearBottom[0], rearBottom[1]),
8465
+ unproject(p2[0], p2[1])
8466
+ ],
8467
+ bottomCap
8468
+ ]
8469
+ }
8470
+ }, createLabelFeature(rearMidpoint, config.labelText, labelRotation, config.labelOptions)]
8228
8471
  };
8229
8472
  }
8230
8473
  //#endregion
8231
8474
  //#region src/generators/cm34-mission-tasks/block.ts
8232
8475
  const DEFAULT_STEM_RATIO = 1.5;
8233
- const DEFAULT_BLOCK_MISSION_TASK_OPTIONS = { labelGapRatio: .2 };
8476
+ const DEFAULT_BLOCK_MISSION_TASK_OPTIONS = { labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING };
8234
8477
  const BLOCK_MISSION_TASK_METADATA = {
8235
8478
  id: "block-mission-task",
8236
8479
  name: "Block",
@@ -8248,16 +8491,8 @@ const BLOCK_MISSION_TASK_METADATA = {
8248
8491
  },
8249
8492
  drawRule: "Area11",
8250
8493
  capturesLabelSize: true,
8251
- params: [{
8252
- key: "labelGapRatio",
8253
- presentationTier: "advanced",
8254
- label: "Label gap",
8255
- description: "Size of the shaft gap holding the 'B' label, as a ratio of the front length",
8256
- type: "number",
8257
- min: 0,
8258
- max: 1,
8259
- step: .01
8260
- }]
8494
+ labelGeometryUsesResolvedMetrics: true,
8495
+ params: [LABEL_PADDING_PARAM]
8261
8496
  };
8262
8497
  /**
8263
8498
  * Generates a GeoJSON FeatureCollection for a BLOCK mission task symbol (340100).
@@ -8269,8 +8504,7 @@ const BLOCK_MISSION_TASK_METADATA = {
8269
8504
  * @param options - Configuration options
8270
8505
  * @returns GeoJSON FeatureCollection containing MultiLineString and Point features
8271
8506
  */
8272
- function createBlockMissionTaskSymbol(coordinates, options = {}) {
8273
- const labelGapRatio = options.labelGapRatio ?? DEFAULT_BLOCK_MISSION_TASK_OPTIONS.labelGapRatio;
8507
+ function createBlockMissionTaskSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
8274
8508
  const p1 = project(coordinates[0][0], coordinates[0][1]);
8275
8509
  const p2 = project(coordinates[1][0], coordinates[1][1]);
8276
8510
  const frame = createProjectedBaselineFrame(p1, p2, {
@@ -8287,12 +8521,12 @@ function createBlockMissionTaskSymbol(coordinates, options = {}) {
8287
8521
  const pStemEnd = vecAdd(mid, vecScale(frame.normal, stemDistance));
8288
8522
  const labelPoint = vecAdd(mid, vecScale(frame.normal, stemDistance / 2));
8289
8523
  const stemDirection = vecScale(frame.normal, stemDistance < 0 ? -1 : 1);
8290
- const gapHalf = Math.min(frame.length * labelGapRatio / 2, Math.abs(stemDistance) / 2);
8291
- const gapStart = vecAdd(labelPoint, vecScale(stemDirection, -gapHalf));
8292
- const gapEnd = vecAdd(labelPoint, vecScale(stemDirection, gapHalf));
8293
8524
  const shaftRotation = Math.atan2(frame.normal[1], frame.normal[0]);
8294
8525
  const renderedLabelRotation = Math.PI - shaftRotation;
8295
8526
  const labelRotation = normalizeRadians(Math.PI - keepTextLeftToRight(renderedLabelRotation));
8527
+ const gapHalf = Math.min(labelKnockoutHalfExtent("B", stemDirection, labelRotation, options, context, DEFAULT_BLOCK_MISSION_TASK_OPTIONS.labelPadding), Math.abs(stemDistance) / 2);
8528
+ const gapStart = vecAdd(labelPoint, vecScale(stemDirection, -gapHalf));
8529
+ const gapEnd = vecAdd(labelPoint, vecScale(stemDirection, gapHalf));
8296
8530
  return {
8297
8531
  type: "FeatureCollection",
8298
8532
  features: [{
@@ -8327,7 +8561,7 @@ const BLOCK_MISSION_TASK = defineControlMeasure({
8327
8561
  const DEFAULT_BREACH_OPTIONS = {
8328
8562
  tickLengthRatio: .15,
8329
8563
  tickAngle: 45,
8330
- labelGapRatio: .2
8564
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING
8331
8565
  };
8332
8566
  const BREACH_METADATA = {
8333
8567
  id: "breach",
@@ -8347,6 +8581,7 @@ const BREACH_METADATA = {
8347
8581
  },
8348
8582
  drawRule: "Point12",
8349
8583
  capturesLabelSize: true,
8584
+ labelGeometryUsesResolvedMetrics: true,
8350
8585
  params: [
8351
8586
  {
8352
8587
  key: "tickLengthRatio",
@@ -8368,16 +8603,7 @@ const BREACH_METADATA = {
8368
8603
  max: 90,
8369
8604
  step: 1
8370
8605
  },
8371
- {
8372
- key: "labelGapRatio",
8373
- presentationTier: "advanced",
8374
- label: "Label gap",
8375
- description: "Size of the rear-line gap holding the 'B' label, as a ratio of the front length",
8376
- type: "number",
8377
- min: 0,
8378
- max: 1,
8379
- step: .01
8380
- }
8606
+ LABEL_PADDING_PARAM
8381
8607
  ]
8382
8608
  };
8383
8609
  /**
@@ -8391,16 +8617,15 @@ const BREACH_METADATA = {
8391
8617
  * @param options - Configuration options
8392
8618
  * @returns GeoJSON FeatureCollection containing MultiLineString and Point features
8393
8619
  */
8394
- function createBreachSymbol(coordinates, options = {}) {
8620
+ function createBreachSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
8395
8621
  const tickLengthRatio = options.tickLengthRatio ?? DEFAULT_BREACH_OPTIONS.tickLengthRatio;
8396
8622
  const tickAngle = options.tickAngle ?? DEFAULT_BREACH_OPTIONS.tickAngle;
8397
8623
  return createBracketSymbol(coordinates, {
8398
8624
  part: "breach",
8399
8625
  labelText: "B",
8400
- labelGapRatio: options.labelGapRatio ?? DEFAULT_BREACH_OPTIONS.labelGapRatio,
8401
- labelSize: {
8402
- labelSize: options.labelSize,
8403
- labelSizePixels: options.labelSizePixels
8626
+ labelOptions: {
8627
+ ...options,
8628
+ labelPadding: options.labelPadding ?? DEFAULT_BREACH_OPTIONS.labelPadding
8404
8629
  },
8405
8630
  buildEndCaps: ({ p1, p2, dirFront, perpDir, frontLength }) => {
8406
8631
  const halfTick = frontLength * tickLengthRatio / 2;
@@ -8416,7 +8641,7 @@ function createBreachSymbol(coordinates, options = {}) {
8416
8641
  const tick2End = vecAdd(p2, vecScale(tick2Dir, halfTick));
8417
8642
  return [[unproject(tick1Start[0], tick1Start[1]), unproject(tick1End[0], tick1End[1])], [unproject(tick2Start[0], tick2Start[1]), unproject(tick2End[0], tick2End[1])]];
8418
8643
  }
8419
- });
8644
+ }, context);
8420
8645
  }
8421
8646
  const BREACH = defineControlMeasure({
8422
8647
  metadata: BREACH_METADATA,
@@ -8431,7 +8656,7 @@ const BREACH = defineControlMeasure({
8431
8656
  */
8432
8657
  const DEFAULT_BYPASS_OPTIONS = {
8433
8658
  arrowheadLengthRatio: .12,
8434
- labelGapRatio: .2
8659
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING
8435
8660
  };
8436
8661
  const BYPASS_METADATA = {
8437
8662
  id: "bypass",
@@ -8451,6 +8676,7 @@ const BYPASS_METADATA = {
8451
8676
  },
8452
8677
  drawRule: "Point12",
8453
8678
  capturesLabelSize: true,
8679
+ labelGeometryUsesResolvedMetrics: true,
8454
8680
  params: [{
8455
8681
  key: "arrowheadLengthRatio",
8456
8682
  presentationTier: "advanced",
@@ -8460,16 +8686,7 @@ const BYPASS_METADATA = {
8460
8686
  min: .01,
8461
8687
  max: 1,
8462
8688
  step: .01
8463
- }, {
8464
- key: "labelGapRatio",
8465
- presentationTier: "advanced",
8466
- label: "Label gap",
8467
- description: "Size of the rear-line gap holding the 'B' label, as a ratio of the front length",
8468
- type: "number",
8469
- min: 0,
8470
- max: 1,
8471
- step: .01
8472
- }]
8689
+ }, LABEL_PADDING_PARAM]
8473
8690
  };
8474
8691
  /**
8475
8692
  * Generates a GeoJSON FeatureCollection for a BYPASS mission task symbol (340300).
@@ -8482,22 +8699,21 @@ const BYPASS_METADATA = {
8482
8699
  * @param options - Configuration options
8483
8700
  * @returns GeoJSON FeatureCollection containing MultiLineString and Point features
8484
8701
  */
8485
- function createBypassSymbol(coordinates, options = {}) {
8702
+ function createBypassSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
8486
8703
  const arrowheadLengthRatio = options.arrowheadLengthRatio ?? DEFAULT_BYPASS_OPTIONS.arrowheadLengthRatio;
8487
8704
  return createBracketSymbol(coordinates, {
8488
8705
  part: "bypass",
8489
8706
  labelText: "B",
8490
- labelGapRatio: options.labelGapRatio ?? DEFAULT_BYPASS_OPTIONS.labelGapRatio,
8491
- labelSize: {
8492
- labelSize: options.labelSize,
8493
- labelSizePixels: options.labelSizePixels
8707
+ labelOptions: {
8708
+ ...options,
8709
+ labelPadding: options.labelPadding ?? DEFAULT_BYPASS_OPTIONS.labelPadding
8494
8710
  },
8495
8711
  buildEndCaps: ({ p1, p2, frontLength, rearTop }) => {
8496
8712
  const arrowheadLength = frontLength * arrowheadLengthRatio;
8497
8713
  const arrowDir = vecNorm(vecSub(p1, rearTop));
8498
8714
  return [createArrowHead$1(p1, arrowDir, arrowheadLength, arrowheadLength), createArrowHead$1(p2, arrowDir, arrowheadLength, arrowheadLength)];
8499
8715
  }
8500
- });
8716
+ }, context);
8501
8717
  }
8502
8718
  const BYPASS = defineControlMeasure({
8503
8719
  metadata: BYPASS_METADATA,
@@ -8513,7 +8729,7 @@ const BYPASS = defineControlMeasure({
8513
8729
  const DEFAULT_CANALIZE_OPTIONS = {
8514
8730
  tickLengthRatio: .15,
8515
8731
  tickAngle: 45,
8516
- labelGapRatio: .2
8732
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING
8517
8733
  };
8518
8734
  const CANALIZE_METADATA = {
8519
8735
  id: "canalize",
@@ -8533,6 +8749,7 @@ const CANALIZE_METADATA = {
8533
8749
  },
8534
8750
  drawRule: "Point12",
8535
8751
  capturesLabelSize: true,
8752
+ labelGeometryUsesResolvedMetrics: true,
8536
8753
  params: [
8537
8754
  {
8538
8755
  key: "tickLengthRatio",
@@ -8554,16 +8771,7 @@ const CANALIZE_METADATA = {
8554
8771
  max: 90,
8555
8772
  step: 1
8556
8773
  },
8557
- {
8558
- key: "labelGapRatio",
8559
- presentationTier: "advanced",
8560
- label: "Label gap",
8561
- description: "Size of the rear-line gap holding the 'C' label, as a ratio of the front length",
8562
- type: "number",
8563
- min: 0,
8564
- max: 1,
8565
- step: .01
8566
- }
8774
+ LABEL_PADDING_PARAM
8567
8775
  ]
8568
8776
  };
8569
8777
  /**
@@ -8577,16 +8785,15 @@ const CANALIZE_METADATA = {
8577
8785
  * @param options - Configuration options
8578
8786
  * @returns GeoJSON FeatureCollection containing MultiLineString and Point features
8579
8787
  */
8580
- function createCanalizeSymbol(coordinates, options = {}) {
8788
+ function createCanalizeSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
8581
8789
  const tickLengthRatio = options.tickLengthRatio ?? DEFAULT_CANALIZE_OPTIONS.tickLengthRatio;
8582
8790
  const tickAngle = options.tickAngle ?? DEFAULT_CANALIZE_OPTIONS.tickAngle;
8583
8791
  return createBracketSymbol(coordinates, {
8584
8792
  part: "canalize",
8585
8793
  labelText: "C",
8586
- labelGapRatio: options.labelGapRatio ?? DEFAULT_CANALIZE_OPTIONS.labelGapRatio,
8587
- labelSize: {
8588
- labelSize: options.labelSize,
8589
- labelSizePixels: options.labelSizePixels
8794
+ labelOptions: {
8795
+ ...options,
8796
+ labelPadding: options.labelPadding ?? DEFAULT_CANALIZE_OPTIONS.labelPadding
8590
8797
  },
8591
8798
  buildEndCaps: ({ p1, p2, dirFront, perpDir, frontLength }) => {
8592
8799
  const halfTick = frontLength * tickLengthRatio / 2;
@@ -8601,7 +8808,7 @@ function createCanalizeSymbol(coordinates, options = {}) {
8601
8808
  const tick2End = vecAdd(p2, vecScale(tick2Dir, halfTick));
8602
8809
  return [[unproject(tick1Start[0], tick1Start[1]), unproject(tick1End[0], tick1End[1])], [unproject(tick2Start[0], tick2Start[1]), unproject(tick2End[0], tick2End[1])]];
8603
8810
  }
8604
- });
8811
+ }, context);
8605
8812
  }
8606
8813
  const CANALIZE = defineControlMeasure({
8607
8814
  metadata: CANALIZE_METADATA,
@@ -9670,7 +9877,7 @@ const DEFAULT_CLEAR_OPTIONS = {
9670
9877
  arrowInsetRatio: .15,
9671
9878
  arrowheadLengthRatio: .15,
9672
9879
  arrowheadWidthRatio: .2,
9673
- labelGapRatio: .2
9880
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING
9674
9881
  };
9675
9882
  const CLEAR_METADATA = {
9676
9883
  id: "clear",
@@ -9690,6 +9897,7 @@ const CLEAR_METADATA = {
9690
9897
  },
9691
9898
  drawRule: "Line23",
9692
9899
  capturesLabelSize: true,
9900
+ labelGeometryUsesResolvedMetrics: true,
9693
9901
  params: [
9694
9902
  {
9695
9903
  key: "arrowInsetRatio",
@@ -9721,16 +9929,7 @@ const CLEAR_METADATA = {
9721
9929
  max: 1,
9722
9930
  step: .01
9723
9931
  },
9724
- {
9725
- key: "labelGapRatio",
9726
- presentationTier: "advanced",
9727
- label: "Label gap",
9728
- description: "Size of the shaft gap holding the 'C' label, as a ratio of the shaft length",
9729
- type: "number",
9730
- min: 0,
9731
- max: 1,
9732
- step: .01
9733
- }
9932
+ LABEL_PADDING_PARAM
9734
9933
  ]
9735
9934
  };
9736
9935
  /**
@@ -9754,11 +9953,10 @@ const CLEAR_METADATA = {
9754
9953
  * @param options - Configuration options
9755
9954
  * @returns GeoJSON FeatureCollection containing MultiLineString and Point features
9756
9955
  */
9757
- function createClearSymbol(coordinates, options = {}) {
9956
+ function createClearSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
9758
9957
  const arrowInsetRatio = Math.min(.49, Math.max(0, options.arrowInsetRatio ?? DEFAULT_CLEAR_OPTIONS.arrowInsetRatio));
9759
9958
  const arrowheadLengthRatio = options.arrowheadLengthRatio ?? DEFAULT_CLEAR_OPTIONS.arrowheadLengthRatio;
9760
9959
  const arrowheadWidthRatio = options.arrowheadWidthRatio ?? DEFAULT_CLEAR_OPTIONS.arrowheadWidthRatio;
9761
- const labelGapRatio = options.labelGapRatio ?? DEFAULT_CLEAR_OPTIONS.labelGapRatio;
9762
9960
  const [c1, c2, c3] = coordinates;
9763
9961
  const p1 = project(c1[0], c1[1]);
9764
9962
  const p2 = project(c2[0], c2[1]);
@@ -9777,6 +9975,9 @@ function createClearSymbol(coordinates, options = {}) {
9777
9975
  const arrowDir = vecScale(perpToRear, -1);
9778
9976
  const arrowheadLength = height * arrowheadLengthRatio;
9779
9977
  const arrowheadWidth = height * arrowheadWidthRatio;
9978
+ const lineRotation = Math.atan2(dirLine[1], dirLine[0]);
9979
+ const renderedLabelRotation = Math.PI - (lineRotation - Math.PI / 2);
9980
+ const labelRotation = normalizeRadians(Math.PI - keepTextLeftToRight(renderedLabelRotation));
9780
9981
  const tipAt = (t) => vecAdd(p1, vecScale(dirLine, t * height));
9781
9982
  const lines = [[unproject(p1[0], p1[1]), unproject(p2[0], p2[1])]];
9782
9983
  const fullTs = [
@@ -9790,7 +9991,7 @@ function createClearSymbol(coordinates, options = {}) {
9790
9991
  const rearEnd = vecAdd(tip, vecScale(perpToRear, shaftLength));
9791
9992
  if (i === labelIndex && shaftLength > 1e-6) {
9792
9993
  const labelCenter = vecAdd(tip, vecScale(perpToRear, shaftLength * .5));
9793
- const gapHalf = shaftLength * labelGapRatio / 2;
9994
+ const gapHalf = Math.min(shaftLength / 2, labelKnockoutHalfExtent("C", perpToRear, labelRotation, options, context, DEFAULT_CLEAR_OPTIONS.labelPadding));
9794
9995
  const gapRear = vecAdd(labelCenter, vecScale(perpToRear, gapHalf));
9795
9996
  const gapTip = vecAdd(labelCenter, vecScale(perpToRear, -gapHalf));
9796
9997
  lines.push([unproject(rearEnd[0], rearEnd[1]), unproject(gapRear[0], gapRear[1])]);
@@ -9806,9 +10007,6 @@ function createClearSymbol(coordinates, options = {}) {
9806
10007
  coordinates: lines
9807
10008
  }
9808
10009
  };
9809
- const lineRotation = Math.atan2(dirLine[1], dirLine[0]);
9810
- const renderedLabelRotation = Math.PI - (lineRotation - Math.PI / 2);
9811
- const labelRotation = normalizeRadians(Math.PI - keepTextLeftToRight(renderedLabelRotation));
9812
10010
  const labelTip = tipAt(fullTs[labelIndex]);
9813
10011
  return {
9814
10012
  type: "FeatureCollection",
@@ -9825,61 +10023,311 @@ const CLEAR = defineControlMeasure({
9825
10023
  rule: line23DrawRule
9826
10024
  });
9827
10025
  //#endregion
9828
- //#region src/generators/cm15-maneuver-areas/maneuver-arrow-task-shared.ts
9829
- const DEFAULT_MANEUVER_ARROW_TASK_AMPLIFIER_OPTIONS = {
9830
- labelPosition: .25,
9831
- dtgPosition: .62,
9832
- labelPadding: 0
9833
- };
9834
- const DEFAULT_MANEUVER_ARROW_TASK_OPTIONS = {
9835
- shaftWidthRatio: DEFAULT_SHAFT_WIDTH_RATIO$1,
9836
- rearWidthRatio: DEFAULT_REAR_WIDTH_RATIO,
9837
- smooth: false,
9838
- smoothResolution: 5,
9839
- crossbarLengthRatio: 1.1,
9840
- ...DEFAULT_MANEUVER_ARROW_TASK_AMPLIFIER_OPTIONS
10026
+ //#region src/generators/cm34-mission-tasks/isolate.ts
10027
+ /**
10028
+ * Default options for the ISOLATE symbol.
10029
+ */
10030
+ const DEFAULT_ISOLATE_OPTIONS = {
10031
+ openingAngle: 30,
10032
+ barbCount: 9,
10033
+ barbLengthRatio: .18,
10034
+ labelPadding: .2
9841
10035
  };
9842
- function createManeuverArrowTaskAmplifierLabels(geometry, options, textAmplifiers) {
9843
- const resolved = {
9844
- ...DEFAULT_MANEUVER_ARROW_TASK_AMPLIFIER_OPTIONS,
9845
- ...options
9846
- };
9847
- const centerline = geometry.shaftCenterline;
9848
- const { segments, totalLength } = polylineSegments(centerline);
9849
- if (segments.length === 0) return [];
9850
- const labels = [];
9851
- const sizeProps = labelSizeProps(resolved);
9852
- const labelFrame = pointAlongPolyline(segments, totalLength, clamp01(resolved.labelPosition));
9853
- if (labelFrame && textAmplifiers.T) pushLabel(labels, labelFrame.point, textAmplifiers.T, labelRotationAlong(labelFrame.along), sizeProps, void 0, "T");
9854
- const dtgFrame = pointAlongPolyline(segments, totalLength, clamp01(resolved.dtgPosition));
9855
- if (dtgFrame) {
9856
- const rowOffset = resolveLabelOffsetMeters(resolved, .55 + Math.max(0, resolved.labelPadding));
9857
- const rotation = labelRotationAlong(dtgFrame.along);
9858
- if (textAmplifiers.W) pushLabel(labels, vecAdd(dtgFrame.point, vecScale(dtgFrame.perp, rowOffset)), textAmplifiers.W, rotation, sizeProps, void 0, "W");
9859
- if (textAmplifiers.W1) pushLabel(labels, vecSub(dtgFrame.point, vecScale(dtgFrame.perp, rowOffset)), textAmplifiers.W1, rotation, sizeProps, void 0, "W1");
10036
+ const ISOLATE_VARIANT_PARAMS = [
10037
+ {
10038
+ key: "openingAngle",
10039
+ presentationTier: "advanced",
10040
+ label: "Opening",
10041
+ description: "Angular width of the gap on the friendly side, in degrees",
10042
+ type: "number",
10043
+ min: 0,
10044
+ max: 180,
10045
+ step: 1
10046
+ },
10047
+ {
10048
+ key: "barbCount",
10049
+ presentationTier: "advanced",
10050
+ label: "Barbs",
10051
+ description: "Number of inward-pointing barbs spaced along the closed arc",
10052
+ type: "number",
10053
+ min: 1,
10054
+ max: 24,
10055
+ step: 1
10056
+ },
10057
+ {
10058
+ key: "barbLengthRatio",
10059
+ presentationTier: "advanced",
10060
+ label: "Barb length",
10061
+ description: "Length of each inward barb, as a ratio of the symbol radius",
10062
+ type: "number",
10063
+ min: .01,
10064
+ max: 1,
10065
+ step: .01
9860
10066
  }
9861
- return labels;
10067
+ ];
10068
+ function isolateVariantParams(labelText) {
10069
+ return [...ISOLATE_VARIANT_PARAMS, {
10070
+ ...LABEL_PADDING_PARAM,
10071
+ label: `${labelText} padding`,
10072
+ description: `Extra boundary clearance on each side of ${labelText}, as a ratio of its height`
10073
+ }];
9862
10074
  }
9863
- function createManeuverArrowTask(coordinates, options, textAmplifiers, config) {
9864
- const merged = {
9865
- ...DEFAULT_MANEUVER_ARROW_TASK_OPTIONS,
9866
- ...options
9867
- };
9868
- const resolved = {
9869
- ...merged,
9870
- rearWidthRatio: resolveRearWidthRatio(options, merged.shaftWidthRatio)
9871
- };
9872
- const { geometry } = processAttackGeometry(coordinates, resolved);
9873
- if (!geometry) return {
9874
- type: "FeatureCollection",
9875
- features: []
9876
- };
9877
- const outline = [
9878
- ...geometry.shaftLeft,
9879
- geometry.headRing[0],
9880
- geometry.headRing[1],
9881
- geometry.headRing[2],
9882
- ...[...geometry.shaftRight].reverse()
10075
+ const ISOLATE_METADATA = {
10076
+ id: "isolate",
10077
+ name: "Isolate",
10078
+ description: "Seals an enemy off from support, denies freedom of movement, and prevents contact with other enemy forces while offensive action continues against it.",
10079
+ entity: "Mission Tasks",
10080
+ entityType: "Isolate",
10081
+ value: "341500",
10082
+ minCoordinates: 2,
10083
+ maxCoordinates: 2,
10084
+ geometry: "line",
10085
+ geometryTypes: ["MultiLineString", "Point"],
10086
+ paints: {
10087
+ stroke: true,
10088
+ fill: "none",
10089
+ text: true
10090
+ },
10091
+ drawRule: "Area15",
10092
+ capturesLabelSize: true,
10093
+ labelGeometryUsesResolvedMetrics: true,
10094
+ params: isolateVariantParams("I")
10095
+ };
10096
+ const LABEL_ROTATION = Math.PI;
10097
+ /**
10098
+ * Generates a GeoJSON FeatureCollection for an ISOLATE mission task symbol (341500).
10099
+ *
10100
+ * A near-complete circle centered on PT. 1 whose radius reaches PT. 2. The arc
10101
+ * starts (bare) at PT. 2's bearing and sweeps clockwise around the closed
10102
+ * portion to a single arrow tip at the far end; the wedge-shaped gap (default
10103
+ * 30°) between that arrow tip and PT. 2 is the opening — the "friendly side".
10104
+ * Inward-pointing arrowhead barbs are spaced along the closed arc, set in from
10105
+ * both ends so they never touch PT. 2 or the arrow tip, conveying the
10106
+ * sealing-off of the enclosed force. A doctrinal "I" interrupts the closed arc
10107
+ * opposite the friendly-side opening.
10108
+ *
10109
+ * Input Points:
10110
+ * - PT. 1: Center point
10111
+ * - PT. 2: Start point — fixes the radius and the bearing of the opening
10112
+ *
10113
+ * @param coordinates - [PT. 1, PT. 2] as Position arrays
10114
+ * @param options - Configuration options
10115
+ * @returns GeoJSON FeatureCollection containing MultiLineString and Point features
10116
+ */
10117
+ function createIsolateSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
10118
+ return createIsolateVariantSymbol(coordinates, options, {
10119
+ part: "isolate",
10120
+ labelText: "I"
10121
+ }, context);
10122
+ }
10123
+ /** Shared geometry for Isolate and its doctrinal cordon variants. */
10124
+ function createIsolateVariantSymbol(coordinates, options, config, context = {}) {
10125
+ const openingAngle = options.openingAngle ?? DEFAULT_ISOLATE_OPTIONS.openingAngle;
10126
+ const barbCount = options.barbCount ?? DEFAULT_ISOLATE_OPTIONS.barbCount;
10127
+ const barbLengthRatio = options.barbLengthRatio ?? DEFAULT_ISOLATE_OPTIONS.barbLengthRatio;
10128
+ const center = project(coordinates[0][0], coordinates[0][1]);
10129
+ const vRadius = vecSub(project(coordinates[1][0], coordinates[1][1]), center);
10130
+ const radius = vecMag(vRadius);
10131
+ if (radius < 1e-6) return {
10132
+ type: "FeatureCollection",
10133
+ features: []
10134
+ };
10135
+ const startAngle = Math.atan2(vRadius[1], vRadius[0]);
10136
+ const openingRad = Math.min(Math.max(openingAngle, 0), 359) * Math.PI / 180;
10137
+ const arcSpan = 2 * Math.PI - openingRad;
10138
+ const endAngle = startAngle - arcSpan;
10139
+ const pointOnCircle = (angle, r = radius) => vecAdd(center, [r * Math.cos(angle), r * Math.sin(angle)]);
10140
+ const labelSweep = arcSpan / 2;
10141
+ const labelMetrics = resolveTextMetrics(config.labelText, options, context, "regular", radius * .12);
10142
+ const labelPadding = Math.max(0, options.labelPadding ?? DEFAULT_ISOLATE_OPTIONS.labelPadding);
10143
+ const labelHalfWidth = Math.max(labelMetrics.width / 2, labelMetrics.height * LABEL_HALF_GLYPH_RATIO);
10144
+ const labelHalfGap = Math.min(arcSpan / 4, Math.max(labelHalfWidth + labelMetrics.height * labelPadding, 1) / radius);
10145
+ const labelPoint = pointOnCircle(startAngle - labelSweep);
10146
+ const labelBoxHalfWidth = labelMetrics.width / 2 + labelMetrics.height * labelPadding;
10147
+ const labelBoxHalfHeight = labelMetrics.height / 2 + labelMetrics.height * labelPadding;
10148
+ const sampleArc = (from, to) => {
10149
+ const span = to - from;
10150
+ const segments = Math.max(2, Math.round(64 * span / (2 * Math.PI)));
10151
+ const arc = [];
10152
+ for (let i = 0; i <= segments; i++) {
10153
+ const p = pointOnCircle(startAngle - from - span * i / segments);
10154
+ arc.push(unproject(p[0], p[1]));
10155
+ }
10156
+ return arc;
10157
+ };
10158
+ const barbLength = radius * barbLengthRatio;
10159
+ const lines = [sampleArc(0, labelSweep - labelHalfGap), sampleArc(labelSweep + labelHalfGap, arcSpan)];
10160
+ const tipDir = [Math.sin(endAngle), -Math.cos(endAngle)];
10161
+ lines.push(createArrowHead$1(pointOnCircle(endAngle), tipDir, barbLength, barbLength));
10162
+ const halfWidth = Math.asin(Math.min(1, barbLength / 2 / radius));
10163
+ const tipRadius = Math.max(0, radius - barbLength);
10164
+ const steps = Math.max(1, barbCount);
10165
+ for (let k = 1; k <= steps; k++) {
10166
+ const angle = startAngle - arcSpan * k / (steps + 1);
10167
+ const left = pointOnCircle(angle - halfWidth);
10168
+ const right = pointOnCircle(angle + halfWidth);
10169
+ const tip = pointOnCircle(angle, tipRadius);
10170
+ const barbLines = config.knockoutBarbs ? clipPolylineOutsideLabelBox([
10171
+ left,
10172
+ tip,
10173
+ right
10174
+ ], labelPoint, labelBoxHalfWidth, labelBoxHalfHeight, LABEL_ROTATION) : [[
10175
+ left,
10176
+ tip,
10177
+ right
10178
+ ]];
10179
+ for (const barb of barbLines) lines.push(barb.map((point) => unproject(point[0], point[1])));
10180
+ }
10181
+ return {
10182
+ type: "FeatureCollection",
10183
+ features: [{
10184
+ type: "Feature",
10185
+ properties: { part: config.part },
10186
+ geometry: {
10187
+ type: "MultiLineString",
10188
+ coordinates: lines
10189
+ }
10190
+ }, createLabelFeature(labelPoint, config.labelText, LABEL_ROTATION, options)]
10191
+ };
10192
+ }
10193
+ const ISOLATE = defineControlMeasure({
10194
+ metadata: ISOLATE_METADATA,
10195
+ generator: createIsolateSymbol,
10196
+ defaultOptions: DEFAULT_ISOLATE_OPTIONS,
10197
+ rule: isolateDrawRule
10198
+ });
10199
+ //#endregion
10200
+ //#region src/generators/cm34-mission-tasks/cordon-and-knock.ts
10201
+ const DEFAULT_CORDON_AND_KNOCK_OPTIONS = DEFAULT_ISOLATE_OPTIONS;
10202
+ const CORDON_AND_KNOCK_METADATA = {
10203
+ id: "cordon-and-knock",
10204
+ name: "Cordon and Knock",
10205
+ description: "Isolates an area while forces call on its occupants to cooperate before conducting further action.",
10206
+ entity: "Mission Tasks",
10207
+ entityType: "Cordon and Knock",
10208
+ value: "342600",
10209
+ minCoordinates: 2,
10210
+ maxCoordinates: 2,
10211
+ geometry: "line",
10212
+ geometryTypes: ["MultiLineString", "Point"],
10213
+ paints: {
10214
+ stroke: true,
10215
+ fill: "none",
10216
+ text: true
10217
+ },
10218
+ drawRule: "Area15",
10219
+ capturesLabelSize: true,
10220
+ labelGeometryUsesResolvedMetrics: true,
10221
+ params: isolateVariantParams("C/K")
10222
+ };
10223
+ /** Generates the Cordon and Knock mission-task symbol (342600). */
10224
+ function createCordonAndKnockSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
10225
+ return createIsolateVariantSymbol(coordinates, options, {
10226
+ part: "cordon-and-knock",
10227
+ labelText: "C/K",
10228
+ knockoutBarbs: true
10229
+ }, context);
10230
+ }
10231
+ const CORDON_AND_KNOCK = defineControlMeasure({
10232
+ metadata: CORDON_AND_KNOCK_METADATA,
10233
+ generator: createCordonAndKnockSymbol,
10234
+ defaultOptions: DEFAULT_CORDON_AND_KNOCK_OPTIONS,
10235
+ rule: isolateDrawRule
10236
+ });
10237
+ //#endregion
10238
+ //#region src/generators/cm34-mission-tasks/cordon-and-search.ts
10239
+ const DEFAULT_CORDON_AND_SEARCH_OPTIONS = DEFAULT_ISOLATE_OPTIONS;
10240
+ const CORDON_AND_SEARCH_METADATA = {
10241
+ id: "cordon-and-search",
10242
+ name: "Cordon and Search",
10243
+ description: "Isolates an area so forces can systematically search it and its occupants.",
10244
+ entity: "Mission Tasks",
10245
+ entityType: "Cordon and Search",
10246
+ value: "342700",
10247
+ minCoordinates: 2,
10248
+ maxCoordinates: 2,
10249
+ geometry: "line",
10250
+ geometryTypes: ["MultiLineString", "Point"],
10251
+ paints: {
10252
+ stroke: true,
10253
+ fill: "none",
10254
+ text: true
10255
+ },
10256
+ drawRule: "Area15",
10257
+ capturesLabelSize: true,
10258
+ labelGeometryUsesResolvedMetrics: true,
10259
+ params: isolateVariantParams("C/S")
10260
+ };
10261
+ /** Generates the Cordon and Search mission-task symbol (342700). */
10262
+ function createCordonAndSearchSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
10263
+ return createIsolateVariantSymbol(coordinates, options, {
10264
+ part: "cordon-and-search",
10265
+ labelText: "C/S",
10266
+ knockoutBarbs: true
10267
+ }, context);
10268
+ }
10269
+ const CORDON_AND_SEARCH = defineControlMeasure({
10270
+ metadata: CORDON_AND_SEARCH_METADATA,
10271
+ generator: createCordonAndSearchSymbol,
10272
+ defaultOptions: DEFAULT_CORDON_AND_SEARCH_OPTIONS,
10273
+ rule: isolateDrawRule
10274
+ });
10275
+ //#endregion
10276
+ //#region src/generators/cm15-maneuver-areas/maneuver-arrow-task-shared.ts
10277
+ const DEFAULT_MANEUVER_ARROW_TASK_AMPLIFIER_OPTIONS = {
10278
+ labelPosition: .25,
10279
+ dtgPosition: .62,
10280
+ labelPadding: 0
10281
+ };
10282
+ const DEFAULT_MANEUVER_ARROW_TASK_OPTIONS = {
10283
+ shaftWidthRatio: DEFAULT_SHAFT_WIDTH_RATIO$1,
10284
+ rearWidthRatio: DEFAULT_REAR_WIDTH_RATIO,
10285
+ smooth: false,
10286
+ smoothResolution: 5,
10287
+ crossbarLengthRatio: 1.1,
10288
+ ...DEFAULT_MANEUVER_ARROW_TASK_AMPLIFIER_OPTIONS
10289
+ };
10290
+ function createManeuverArrowTaskAmplifierLabels(geometry, options, textAmplifiers) {
10291
+ const resolved = {
10292
+ ...DEFAULT_MANEUVER_ARROW_TASK_AMPLIFIER_OPTIONS,
10293
+ ...options
10294
+ };
10295
+ const centerline = geometry.shaftCenterline;
10296
+ const { segments, totalLength } = polylineSegments(centerline);
10297
+ if (segments.length === 0) return [];
10298
+ const labels = [];
10299
+ const sizeProps = labelSizeProps(resolved);
10300
+ const labelFrame = pointAlongPolyline(segments, totalLength, clamp01(resolved.labelPosition));
10301
+ if (labelFrame && textAmplifiers.T) pushLabel(labels, labelFrame.point, textAmplifiers.T, labelRotationAlong(labelFrame.along), sizeProps, void 0, "T");
10302
+ const dtgFrame = pointAlongPolyline(segments, totalLength, clamp01(resolved.dtgPosition));
10303
+ if (dtgFrame) {
10304
+ const rowOffset = resolveLabelOffsetMeters(resolved, .55 + Math.max(0, resolved.labelPadding));
10305
+ const rotation = labelRotationAlong(dtgFrame.along);
10306
+ if (textAmplifiers.W) pushLabel(labels, vecAdd(dtgFrame.point, vecScale(dtgFrame.perp, rowOffset)), textAmplifiers.W, rotation, sizeProps, void 0, "W");
10307
+ if (textAmplifiers.W1) pushLabel(labels, vecSub(dtgFrame.point, vecScale(dtgFrame.perp, rowOffset)), textAmplifiers.W1, rotation, sizeProps, void 0, "W1");
10308
+ }
10309
+ return labels;
10310
+ }
10311
+ function createManeuverArrowTask(coordinates, options, textAmplifiers, config) {
10312
+ const merged = {
10313
+ ...DEFAULT_MANEUVER_ARROW_TASK_OPTIONS,
10314
+ ...options
10315
+ };
10316
+ const resolved = {
10317
+ ...merged,
10318
+ rearWidthRatio: resolveRearWidthRatio(options, merged.shaftWidthRatio)
10319
+ };
10320
+ const { geometry } = processAttackGeometry(coordinates, resolved);
10321
+ if (!geometry) return {
10322
+ type: "FeatureCollection",
10323
+ features: []
10324
+ };
10325
+ const outline = [
10326
+ ...geometry.shaftLeft,
10327
+ geometry.headRing[0],
10328
+ geometry.headRing[1],
10329
+ geometry.headRing[2],
10330
+ ...[...geometry.shaftRight].reverse()
9883
10331
  ];
9884
10332
  const centerline = geometry.shaftCenterline;
9885
10333
  const { segments, totalLength } = polylineSegments(centerline);
@@ -10631,7 +11079,7 @@ const COVER = defineControlMeasure({
10631
11079
  const DEFAULT_DELAY_OPTIONS = {
10632
11080
  arrowheadLengthRatio: .12,
10633
11081
  arrowheadWidthRatio: .12,
10634
- labelGapRatio: .18,
11082
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING,
10635
11083
  arcSegments: 32
10636
11084
  };
10637
11085
  const DELAY_PARAMS = [
@@ -10655,16 +11103,7 @@ const DELAY_PARAMS = [
10655
11103
  max: 1,
10656
11104
  step: .01
10657
11105
  },
10658
- {
10659
- key: "labelGapRatio",
10660
- presentationTier: "advanced",
10661
- label: "Label gap",
10662
- description: "Size of the shaft gap holding the task label, as a ratio of the shaft length",
10663
- type: "number",
10664
- min: 0,
10665
- max: 1,
10666
- step: .01
10667
- },
11106
+ LABEL_PADDING_PARAM,
10668
11107
  {
10669
11108
  key: "arcSegments",
10670
11109
  presentationTier: "advanced",
@@ -10694,9 +11133,16 @@ const DELAY_METADATA = {
10694
11133
  },
10695
11134
  drawRule: "Line24",
10696
11135
  capturesLabelSize: true,
11136
+ labelGeometryUsesResolvedMetrics: true,
10697
11137
  params: DELAY_PARAMS
10698
11138
  };
10699
- function createDelaySymbol(coordinates, options = {}) {
11139
+ function createDelaySymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
11140
+ return createDelayTaskSymbol(coordinates, {
11141
+ glyph: "D",
11142
+ part: "delay"
11143
+ }, options, context);
11144
+ }
11145
+ function createDelayTaskSymbol(coordinates, config, options, context) {
10700
11146
  const [c1, c2, c3] = coordinates;
10701
11147
  const p1 = project(c1[0], c1[1]);
10702
11148
  const p2 = project(c2[0], c2[1]);
@@ -10712,8 +11158,9 @@ function createDelaySymbol(coordinates, options = {}) {
10712
11158
  const signedDiameter = vecDot(vecSub(p3, p2), diameterAxis);
10713
11159
  const diameterEnd = vecAdd(p2, vecScale(diameterAxis, signedDiameter));
10714
11160
  const diameter = Math.abs(signedDiameter);
10715
- const gapHalf = lineLength * Math.min(1, Math.max(0, options.labelGapRatio ?? DEFAULT_DELAY_OPTIONS.labelGapRatio)) / 2;
10716
11161
  const labelPoint = vecAdd(p1, vecScale(dirLine, lineLength / 2));
11162
+ const labelRotation = labelRotationAlong(dirLine);
11163
+ const gapHalf = Math.min(lineLength / 2, labelKnockoutHalfExtent(config.glyph, dirLine, labelRotation, options, context, DEFAULT_DELAY_OPTIONS.labelPadding));
10717
11164
  const gapStart = vecAdd(labelPoint, vecScale(dirLine, -gapHalf));
10718
11165
  const gapEnd = vecAdd(labelPoint, vecScale(dirLine, gapHalf));
10719
11166
  const lines = [
@@ -10726,52 +11173,23 @@ function createDelaySymbol(coordinates, options = {}) {
10726
11173
  type: "FeatureCollection",
10727
11174
  features: [{
10728
11175
  type: "Feature",
10729
- properties: { part: "delay" },
11176
+ properties: { part: config.part },
10730
11177
  geometry: {
10731
11178
  type: "MultiLineString",
10732
11179
  coordinates: lines
10733
11180
  }
10734
- }, createLabelFeature(labelPoint, "D", labelRotationAlong(dirLine), {
11181
+ }, createLabelFeature(labelPoint, config.glyph, labelRotation, {
10735
11182
  labelSize: options.labelSize,
10736
11183
  labelSizePixels: options.labelSizePixels
10737
11184
  })]
10738
11185
  };
10739
11186
  }
10740
- function createSemicircleArc(start, end, bulgeDir, requestedSegments) {
10741
- const diameterVector = vecSub(end, start);
10742
- const diameter = vecMag(diameterVector);
10743
- if (diameter < 1e-6) return [unproject(start[0], start[1])];
10744
- const diameterDir = vecNorm(diameterVector);
10745
- const center = vecAdd(start, vecScale(diameterVector, .5));
10746
- const radius = diameter / 2;
10747
- const segments = Math.max(4, Math.round(requestedSegments));
10748
- const points = [];
10749
- for (let i = 0; i <= segments; i += 1) {
10750
- const theta = i / segments * Math.PI;
10751
- const point = vecAdd(center, vecAdd(vecScale(diameterDir, -Math.cos(theta) * radius), vecScale(bulgeDir, Math.sin(theta) * radius)));
10752
- points.push(unproject(point[0], point[1]));
10753
- }
10754
- return points;
10755
- }
10756
11187
  /**
10757
11188
  * Line24 mission tasks that share Delay's geometry and differ only in the
10758
11189
  * glyph shown in the shaft gap and the `part` tag on the line feature.
10759
11190
  */
10760
- function createDelayVariantSymbol(coordinates, config, options = {}) {
10761
- return {
10762
- type: "FeatureCollection",
10763
- features: createDelaySymbol(coordinates, options).features.map((feature) => ({
10764
- ...feature,
10765
- properties: feature.geometry.type === "Point" ? {
10766
- ...feature.properties,
10767
- part: "label",
10768
- text: config.glyph
10769
- } : {
10770
- ...feature.properties,
10771
- part: config.part
10772
- }
10773
- }))
10774
- };
11191
+ function createDelayVariantSymbol(coordinates, config, options = {}, context = {}) {
11192
+ return createDelayTaskSymbol(coordinates, config, options, context);
10775
11193
  }
10776
11194
  const DELAY = defineControlMeasure({
10777
11195
  metadata: DELAY_METADATA,
@@ -10780,6 +11198,122 @@ const DELAY = defineControlMeasure({
10780
11198
  rule: line24DrawRule
10781
11199
  });
10782
11200
  //#endregion
11201
+ //#region src/generators/cm34-mission-tasks/hairpin.ts
11202
+ /**
11203
+ * Shared scaffold for the Area18 "hairpin" mission tasks (Relief in Place,
11204
+ * Demonstrate). Owns the doctrinal geometry both measures share: two parallel
11205
+ * arms with an arrow tip at each open end (PT. 1 and PT. 3), joined by a
11206
+ * semicircular turn across PT. 2 → PT. 3.
11207
+ *
11208
+ * Both tips use the same option ratios, each scaled by the length of its own
11209
+ * arm (PT. 1/PT. 2 and PT. 3/PT. 4). Only glyph placement differs between the
11210
+ * measures, which each supply via {@link HairpinSymbolConfig.buildLabel}.
11211
+ */
11212
+ function createHairpinSymbol(coordinates, config, options = {}) {
11213
+ const [c1, c2, c3, c4] = coordinates;
11214
+ const p1 = project(c1[0], c1[1]);
11215
+ const p2 = project(c2[0], c2[1]);
11216
+ const p3 = project(c3[0], c3[1]);
11217
+ const p4 = project(c4[0], c4[1]);
11218
+ const upperShaft = vecSub(p2, p1);
11219
+ const upperLength = vecMag(upperShaft);
11220
+ const lowerShaft = vecSub(p3, p4);
11221
+ const lowerLength = vecMag(lowerShaft);
11222
+ if (upperLength < 1e-6 || lowerLength < 1e-6) return {
11223
+ type: "FeatureCollection",
11224
+ features: []
11225
+ };
11226
+ const upperDirection = vecNorm(upperShaft);
11227
+ const lowerDirection = vecNorm(lowerShaft);
11228
+ const arrowheadLengthRatio = Math.max(0, options.arrowheadLengthRatio ?? DEFAULT_DELAY_OPTIONS.arrowheadLengthRatio);
11229
+ const arrowheadWidthRatio = Math.max(0, options.arrowheadWidthRatio ?? DEFAULT_DELAY_OPTIONS.arrowheadWidthRatio);
11230
+ const { label, upperArm } = config.buildLabel({
11231
+ points: [
11232
+ p1,
11233
+ p2,
11234
+ p3,
11235
+ p4
11236
+ ],
11237
+ upperDirection,
11238
+ upperLength,
11239
+ upperArm: [[unproject(p1[0], p1[1]), unproject(p2[0], p2[1])]]
11240
+ });
11241
+ return {
11242
+ type: "FeatureCollection",
11243
+ features: [{
11244
+ type: "Feature",
11245
+ properties: { part: config.part },
11246
+ geometry: {
11247
+ type: "MultiLineString",
11248
+ coordinates: [
11249
+ ...upperArm,
11250
+ createSemicircleArc(p2, p3, upperDirection, options.arcSegments ?? DEFAULT_DELAY_OPTIONS.arcSegments),
11251
+ [unproject(p4[0], p4[1]), unproject(p3[0], p3[1])],
11252
+ createArrowHead$1(p1, vecScale(upperDirection, -1), upperLength * arrowheadLengthRatio, upperLength * arrowheadWidthRatio),
11253
+ createArrowHead$1(p3, lowerDirection, lowerLength * arrowheadLengthRatio, lowerLength * arrowheadWidthRatio)
11254
+ ]
11255
+ }
11256
+ }, label]
11257
+ };
11258
+ }
11259
+ //#endregion
11260
+ //#region src/generators/cm34-mission-tasks/demonstrate.ts
11261
+ const DEFAULT_DEMONSTRATE_OPTIONS = DEFAULT_DELAY_OPTIONS;
11262
+ const DEMONSTRATE_METADATA = {
11263
+ id: "demonstrate",
11264
+ name: "Demonstrate/Demonstration",
11265
+ description: "Uses a show of force to deceive the enemy without seeking decisive engagement.",
11266
+ entity: "Mission Tasks",
11267
+ entityType: "Demonstrate/Demonstration",
11268
+ value: "343300",
11269
+ minCoordinates: 4,
11270
+ maxCoordinates: 4,
11271
+ geometry: "line",
11272
+ geometryTypes: ["MultiLineString", "Point"],
11273
+ paints: {
11274
+ stroke: true,
11275
+ fill: "none",
11276
+ text: true
11277
+ },
11278
+ drawRule: "Area18",
11279
+ capturesLabelSize: true,
11280
+ labelGeometryUsesResolvedMetrics: true,
11281
+ params: DELAY_PARAMS
11282
+ };
11283
+ /** Generates the DEMONSTRATE/DEMONSTRATION mission task symbol (343300). */
11284
+ function createDemonstrateSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
11285
+ return createHairpinSymbol(coordinates, {
11286
+ part: "demonstrate",
11287
+ buildLabel: ({ points, upperDirection, upperLength }) => {
11288
+ const [p1, p2] = points;
11289
+ const labelPoint = vecAdd(p1, vecScale(upperDirection, upperLength / 2));
11290
+ const rotation = labelRotationAlong(upperDirection);
11291
+ const gapHalf = Math.min(upperLength / 2, labelKnockoutHalfExtent("DEM", upperDirection, rotation, options, context, DEFAULT_DEMONSTRATE_OPTIONS.labelPadding));
11292
+ const gapStart = vecAdd(labelPoint, vecScale(upperDirection, -gapHalf));
11293
+ const gapEnd = vecAdd(labelPoint, vecScale(upperDirection, gapHalf));
11294
+ return {
11295
+ upperArm: [[unproject(p1[0], p1[1]), unproject(gapStart[0], gapStart[1])], [unproject(gapEnd[0], gapEnd[1]), unproject(p2[0], p2[1])]],
11296
+ label: createLabelFeature(labelPoint, "DEM", rotation, {
11297
+ labelSize: options.labelSize,
11298
+ labelSizePixels: options.labelSizePixels
11299
+ })
11300
+ };
11301
+ }
11302
+ }, options);
11303
+ }
11304
+ const DEMONSTRATE = defineControlMeasure({
11305
+ metadata: DEMONSTRATE_METADATA,
11306
+ generator: createDemonstrateSymbol,
11307
+ defaultOptions: DEFAULT_DEMONSTRATE_OPTIONS,
11308
+ rule: demonstrateDrawRule,
11309
+ previewSample: { controlPoints: [
11310
+ [-1, -.45],
11311
+ [.45, -.45],
11312
+ [.45, .45],
11313
+ [-1, .45]
11314
+ ] }
11315
+ });
11316
+ //#endregion
10783
11317
  //#region src/generators/cm34-mission-tasks/disengage.ts
10784
11318
  const DEFAULT_DISENGAGE_OPTIONS = DEFAULT_DELAY_OPTIONS;
10785
11319
  const DISENGAGE_METADATA = {
@@ -10800,27 +11334,25 @@ const DISENGAGE_METADATA = {
10800
11334
  },
10801
11335
  drawRule: "Line24",
10802
11336
  capturesLabelSize: true,
11337
+ labelGeometryUsesResolvedMetrics: true,
10803
11338
  params: DELAY_PARAMS
10804
11339
  };
10805
- function createDisengageSymbol(coordinates, options = {}) {
11340
+ function createDisengageSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
10806
11341
  return createDelayVariantSymbol(coordinates, {
10807
11342
  glyph: "DIS",
10808
11343
  part: "disengage"
10809
- }, options);
11344
+ }, options, context);
10810
11345
  }
10811
11346
  const DISENGAGE = defineControlMeasure({
10812
11347
  metadata: DISENGAGE_METADATA,
10813
11348
  generator: createDisengageSymbol,
10814
11349
  defaultOptions: DEFAULT_DISENGAGE_OPTIONS,
10815
11350
  rule: line24DrawRule,
10816
- previewSample: {
10817
- controlPoints: [
10818
- [-1, -.4],
10819
- [1, -.4],
10820
- [0, .6]
10821
- ],
10822
- options: { labelGapRatio: .36 }
10823
- }
11351
+ previewSample: { controlPoints: [
11352
+ [-1, -.4],
11353
+ [1, -.4],
11354
+ [0, .6]
11355
+ ] }
10824
11356
  });
10825
11357
  //#endregion
10826
11358
  //#region src/generators/cm34-mission-tasks/disrupt.ts
@@ -10830,7 +11362,7 @@ const DEFAULT_DISRUPT_MISSION_TASK_OPTIONS = {
10830
11362
  shaftLengthRatio: .25,
10831
11363
  arrowheadLengthRatio: .15,
10832
11364
  arrowheadWidthRatio: .2,
10833
- labelGapRatio: .2
11365
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING
10834
11366
  };
10835
11367
  const DISRUPT_MISSION_TASK_METADATA = {
10836
11368
  id: "disrupt-mission-task",
@@ -10850,6 +11382,7 @@ const DISRUPT_MISSION_TASK_METADATA = {
10850
11382
  },
10851
11383
  drawRule: "Area12",
10852
11384
  capturesLabelSize: true,
11385
+ labelGeometryUsesResolvedMetrics: true,
10853
11386
  params: [
10854
11387
  {
10855
11388
  key: "middleArrowLengthRatio",
@@ -10901,20 +11434,11 @@ const DISRUPT_MISSION_TASK_METADATA = {
10901
11434
  max: 1,
10902
11435
  step: .01
10903
11436
  },
10904
- {
10905
- key: "labelGapRatio",
10906
- presentationTier: "advanced",
10907
- label: "Label gap",
10908
- description: "Size of the middle-shaft gap holding the 'D' label",
10909
- type: "number",
10910
- min: 0,
10911
- max: 1,
10912
- step: .01
10913
- }
11437
+ LABEL_PADDING_PARAM
10914
11438
  ]
10915
11439
  };
10916
11440
  /** Generates the DISRUPT mission task symbol (341000). */
10917
- function createDisruptMissionTaskSymbol(coordinates, options = {}) {
11441
+ function createDisruptMissionTaskSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
10918
11442
  const p1 = project(coordinates[0][0], coordinates[0][1]);
10919
11443
  const p2 = project(coordinates[1][0], coordinates[1][1]);
10920
11444
  const frame = createProjectedBaselineFrame(p1, p2, {
@@ -10946,8 +11470,8 @@ function createDisruptMissionTaskSymbol(coordinates, options = {}) {
10946
11470
  const bottomTip = vecAdd(p2, vecScale(arrowDirection, bottomLength));
10947
11471
  const rearEnd = vecAdd(middle, vecScale(arrowDirection, -rearLength));
10948
11472
  const labelPoint = vecScale(vecAdd(middle, middleTip), .5);
10949
- const labelGapRatio = clamp01(options.labelGapRatio ?? DEFAULT_DISRUPT_MISSION_TASK_OPTIONS.labelGapRatio);
10950
- const gapHalf = Math.min(topLength * labelGapRatio / 2, middleLength / 2);
11473
+ const labelRotation = labelRotationAlong(arrowDirection);
11474
+ const gapHalf = Math.min(labelKnockoutHalfExtent("D", arrowDirection, labelRotation, options, context, DEFAULT_DISRUPT_MISSION_TASK_OPTIONS.labelPadding), middleLength / 2);
10951
11475
  const gapStart = vecAdd(labelPoint, vecScale(arrowDirection, -gapHalf));
10952
11476
  const gapEnd = vecAdd(labelPoint, vecScale(arrowDirection, gapHalf));
10953
11477
  const p1LonLat = unproject(p1[0], p1[1]);
@@ -10970,7 +11494,7 @@ function createDisruptMissionTaskSymbol(coordinates, options = {}) {
10970
11494
  createArrowHead$1(bottomTip, arrowDirection, headLength, headWidth)
10971
11495
  ]
10972
11496
  }
10973
- }, createLabelFeature(labelPoint, "D", labelRotationAlong(arrowDirection), {
11497
+ }, createLabelFeature(labelPoint, "D", labelRotation, {
10974
11498
  labelSize: options.labelSize,
10975
11499
  labelSizePixels: options.labelSizePixels
10976
11500
  })]
@@ -10989,7 +11513,7 @@ const DEFAULT_ENVELOPMENT_OPTIONS = {
10989
11513
  arrowheadLengthRatio: .1,
10990
11514
  arrowheadWidthRatio: .12,
10991
11515
  labelPosition: .6,
10992
- labelGapRatio: .18,
11516
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING,
10993
11517
  resolution: 64
10994
11518
  };
10995
11519
  const ENVELOPMENT_METADATA = {
@@ -11010,6 +11534,7 @@ const ENVELOPMENT_METADATA = {
11010
11534
  },
11011
11535
  drawRule: "Line31",
11012
11536
  capturesLabelSize: true,
11537
+ labelGeometryUsesResolvedMetrics: true,
11013
11538
  params: [
11014
11539
  {
11015
11540
  key: "arrowheadLengthRatio",
@@ -11041,21 +11566,12 @@ const ENVELOPMENT_METADATA = {
11041
11566
  max: 1,
11042
11567
  step: .01
11043
11568
  },
11044
- {
11045
- key: "labelGapRatio",
11046
- presentationTier: "advanced",
11047
- label: "Label gap",
11048
- description: "Size of the straight-line cutout holding the E label",
11049
- type: "number",
11050
- min: 0,
11051
- max: 1,
11052
- step: .01
11053
- },
11569
+ LABEL_PADDING_PARAM,
11054
11570
  ...ARC_RESOLUTION_PARAMS
11055
11571
  ]
11056
11572
  };
11057
11573
  /** Generates the Envelopment mission task symbol (343500). */
11058
- function createEnvelopment(coordinates, options = {}) {
11574
+ function createEnvelopment(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
11059
11575
  const [c1, c2, c3, c4] = coordinates;
11060
11576
  const p1 = project(c1[0], c1[1]);
11061
11577
  const p2 = project(c2[0], c2[1]);
@@ -11070,8 +11586,10 @@ function createEnvelopment(coordinates, options = {}) {
11070
11586
  };
11071
11587
  const straightDirection = vecNorm(straight);
11072
11588
  const p3 = vecAdd(p2, vecScale(straightDirection, diameter));
11073
- const labelPoint = vecAdd(p1, vecScale(straight, clamp01(options.labelPosition ?? DEFAULT_ENVELOPMENT_OPTIONS.labelPosition)));
11074
- const gapHalf = straightLength * clamp01(options.labelGapRatio ?? DEFAULT_ENVELOPMENT_OPTIONS.labelGapRatio) / 2;
11589
+ const labelPosition = clamp01(options.labelPosition ?? DEFAULT_ENVELOPMENT_OPTIONS.labelPosition);
11590
+ const labelPoint = vecAdd(p1, vecScale(straight, labelPosition));
11591
+ const labelRotation = labelRotationAlong(straightDirection);
11592
+ const gapHalf = Math.min(straightLength * Math.min(labelPosition, 1 - labelPosition), labelKnockoutHalfExtent("E", straightDirection, labelRotation, options, context, DEFAULT_ENVELOPMENT_OPTIONS.labelPadding));
11075
11593
  const gapStart = vecAdd(labelPoint, vecScale(straightDirection, -gapHalf));
11076
11594
  const gapEnd = vecAdd(labelPoint, vecScale(straightDirection, gapHalf));
11077
11595
  const center = midpoint(p2, p3);
@@ -11094,7 +11612,7 @@ function createEnvelopment(coordinates, options = {}) {
11094
11612
  arrowhead
11095
11613
  ]
11096
11614
  }
11097
- }, createLabelFeature(labelPoint, "E", labelRotationAlong(straightDirection), options)]
11615
+ }, createLabelFeature(labelPoint, "E", labelRotation, options)]
11098
11616
  };
11099
11617
  }
11100
11618
  const ENVELOPMENT = defineControlMeasure({
@@ -11113,12 +11631,83 @@ const ENVELOPMENT = defineControlMeasure({
11113
11631
  }
11114
11632
  });
11115
11633
  //#endregion
11634
+ //#region src/generators/cm34-mission-tasks/exploitation.ts
11635
+ const EXPLOITATION_FIN_DASH = [6, 4];
11636
+ const DEFAULT_EXPLOITATION_OPTIONS = {};
11637
+ const EXPLOITATION_METADATA = {
11638
+ id: "exploitation",
11639
+ name: "Exploit/Exploitation",
11640
+ description: "The arrow points in the direction of the action; its tip may indicate where the action is to conclude, and its base marks the tasked unit’s current or projected location.",
11641
+ entity: "Mission Tasks",
11642
+ entityType: "Exploit/Exploitation",
11643
+ value: "343100",
11644
+ minCoordinates: 3,
11645
+ maxCoordinates: 3,
11646
+ geometry: "line",
11647
+ geometryTypes: ["MultiLineString"],
11648
+ paints: {
11649
+ stroke: true,
11650
+ fill: "none",
11651
+ text: false
11652
+ },
11653
+ drawRule: "Line30"
11654
+ };
11655
+ /** Generates the Exploit/Exploitation mission task symbol (343100). */
11656
+ function createExploitation(coordinates, _options = {}) {
11657
+ const [c1, c2, c3] = coordinates;
11658
+ const p1 = project(c1[0], c1[1]);
11659
+ const p2 = project(c2[0], c2[1]);
11660
+ const p3 = project(c3[0], c3[1]);
11661
+ const axis = vecSub(p1, p2);
11662
+ const axisLength = vecMag(axis);
11663
+ const finLength = vecMag(vecSub(p3, p2));
11664
+ if (axisLength < 1e-6 || finLength < 1e-6) return {
11665
+ type: "FeatureCollection",
11666
+ features: []
11667
+ };
11668
+ const direction = vecNorm(axis);
11669
+ const chevron = (tip) => createArrowHead$1(tip, direction, finLength * Math.SQRT1_2, finLength * Math.SQRT2);
11670
+ const [finLeft, , finRight] = chevron(p2);
11671
+ return {
11672
+ type: "FeatureCollection",
11673
+ features: [{
11674
+ type: "Feature",
11675
+ properties: { part: "exploitation" },
11676
+ geometry: {
11677
+ type: "MultiLineString",
11678
+ coordinates: [[c2, c1], chevron(p1)]
11679
+ }
11680
+ }, {
11681
+ type: "Feature",
11682
+ properties: {
11683
+ part: "fins",
11684
+ style: { strokeDash: [...EXPLOITATION_FIN_DASH] }
11685
+ },
11686
+ geometry: {
11687
+ type: "MultiLineString",
11688
+ coordinates: [[c2, finLeft], [c2, finRight]]
11689
+ }
11690
+ }]
11691
+ };
11692
+ }
11693
+ const EXPLOITATION = defineControlMeasure({
11694
+ metadata: EXPLOITATION_METADATA,
11695
+ generator: createExploitation,
11696
+ defaultOptions: DEFAULT_EXPLOITATION_OPTIONS,
11697
+ rule: line30DrawRule,
11698
+ previewSample: { controlPoints: [
11699
+ [.8, 0],
11700
+ [-.6, 0],
11701
+ [-.9, .3]
11702
+ ] }
11703
+ });
11704
+ //#endregion
11116
11705
  //#region src/generators/cm34-mission-tasks/infiltration.ts
11117
11706
  const DEFAULT_INFILTRATION_OPTIONS = {
11118
11707
  arrowheadLengthRatio: .1,
11119
11708
  arrowheadWidthRatio: .1,
11120
11709
  labelPosition: .6,
11121
- labelGapRatio: .24,
11710
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING,
11122
11711
  resolution: 64
11123
11712
  };
11124
11713
  const INFILTRATION_METADATA = {
@@ -11139,6 +11728,7 @@ const INFILTRATION_METADATA = {
11139
11728
  },
11140
11729
  drawRule: "Line32",
11141
11730
  capturesLabelSize: true,
11731
+ labelGeometryUsesResolvedMetrics: true,
11142
11732
  params: [
11143
11733
  {
11144
11734
  key: "arrowheadLengthRatio",
@@ -11170,16 +11760,7 @@ const INFILTRATION_METADATA = {
11170
11760
  max: 1,
11171
11761
  step: .01
11172
11762
  },
11173
- {
11174
- key: "labelGapRatio",
11175
- presentationTier: "advanced",
11176
- label: "Label gap",
11177
- description: "Size of the initial-line cutout holding the IN label",
11178
- type: "number",
11179
- min: 0,
11180
- max: 1,
11181
- step: .01
11182
- },
11763
+ LABEL_PADDING_PARAM,
11183
11764
  ...ARC_RESOLUTION_PARAMS
11184
11765
  ]
11185
11766
  };
@@ -11205,7 +11786,7 @@ function tangentDirection(p1, p2, p3) {
11205
11786
  return direction;
11206
11787
  }
11207
11788
  /** Generates the Infiltration mission task symbol (343800). */
11208
- function createInfiltration(coordinates, options = {}) {
11789
+ function createInfiltration(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
11209
11790
  const [c1, c2, c3] = coordinates;
11210
11791
  const p1 = project(c1[0], c1[1]);
11211
11792
  const p2 = project(c2[0], c2[1]);
@@ -11238,8 +11819,10 @@ function createInfiltration(coordinates, options = {}) {
11238
11819
  const arcResolution = normalizeArcResolution(options.resolution) / 2;
11239
11820
  const firstArc = sampleProjectedArc(firstCenter, sideRadius, alongRadius, 0, Math.PI / 2, arcResolution);
11240
11821
  const secondArc = sampleProjectedArc(secondCenter, vecScale(alongRadius, -1), vecScale(sideRadius, -1), 0, Math.PI / 2, arcResolution);
11241
- const labelPoint = vecAdd(p1, vecScale(straight, clamp01(options.labelPosition ?? DEFAULT_INFILTRATION_OPTIONS.labelPosition)));
11242
- const gapHalf = straightLength * clamp01(options.labelGapRatio ?? DEFAULT_INFILTRATION_OPTIONS.labelGapRatio) / 2;
11822
+ const labelPosition = clamp01(options.labelPosition ?? DEFAULT_INFILTRATION_OPTIONS.labelPosition);
11823
+ const labelPoint = vecAdd(p1, vecScale(straight, labelPosition));
11824
+ const labelRotation = labelRotationAlong(along);
11825
+ const gapHalf = Math.min(straightLength * Math.min(labelPosition, 1 - labelPosition), labelKnockoutHalfExtent("IN", along, labelRotation, options, context, DEFAULT_INFILTRATION_OPTIONS.labelPadding));
11243
11826
  const gapStart = vecAdd(labelPoint, vecScale(along, -gapHalf));
11244
11827
  const gapEnd = vecAdd(labelPoint, vecScale(along, gapHalf));
11245
11828
  const arrowhead = createArrowHead$1(p3, along, spanLength * Math.max(0, options.arrowheadLengthRatio ?? DEFAULT_INFILTRATION_OPTIONS.arrowheadLengthRatio), spanLength * Math.max(0, options.arrowheadWidthRatio ?? DEFAULT_INFILTRATION_OPTIONS.arrowheadWidthRatio));
@@ -11259,7 +11842,7 @@ function createInfiltration(coordinates, options = {}) {
11259
11842
  arrowhead
11260
11843
  ]
11261
11844
  }
11262
- }, createLabelFeature(labelPoint, "IN", labelRotationAlong(along), options)]
11845
+ }, createLabelFeature(labelPoint, "IN", labelRotation, options)]
11263
11846
  };
11264
11847
  }
11265
11848
  const INFILTRATION = defineControlMeasure({
@@ -11546,6 +12129,7 @@ const DROP_ZONE_METADATA = {
11546
12129
  },
11547
12130
  drawRule: "Area1",
11548
12131
  capturesLabelSize: true,
12132
+ labelGeometryUsesResolvedMetrics: ["N"],
11549
12133
  params: PREFIXED_LABELED_AREA_PARAMS,
11550
12134
  textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
11551
12135
  };
@@ -11610,6 +12194,7 @@ const ENCIRCLEMENT_METADATA = {
11610
12194
  },
11611
12195
  drawRule: "Area1",
11612
12196
  capturesLabelSize: true,
12197
+ labelGeometryUsesResolvedMetrics: ["N"],
11613
12198
  params: ENCIRCLEMENT_PARAMS,
11614
12199
  textAmplifiers: [{
11615
12200
  key: "N",
@@ -11763,6 +12348,7 @@ const EXTRACTION_ZONE_METADATA = {
11763
12348
  },
11764
12349
  drawRule: "Area1",
11765
12350
  capturesLabelSize: true,
12351
+ labelGeometryUsesResolvedMetrics: ["N"],
11766
12352
  params: PREFIXED_LABELED_AREA_PARAMS,
11767
12353
  textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
11768
12354
  };
@@ -12127,7 +12713,7 @@ const DEFAULT_FIX_MISSION_TASK_OPTIONS = {
12127
12713
  zigzagEndRatio: .8,
12128
12714
  zigzagPeakCount: 5,
12129
12715
  labelPositionRatio: .09,
12130
- labelGapRatio: .1
12716
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING
12131
12717
  };
12132
12718
  const FIX_MISSION_TASK_METADATA = {
12133
12719
  id: "fix-mission-task",
@@ -12147,6 +12733,7 @@ const FIX_MISSION_TASK_METADATA = {
12147
12733
  },
12148
12734
  drawRule: "Line9",
12149
12735
  capturesLabelSize: true,
12736
+ labelGeometryUsesResolvedMetrics: true,
12150
12737
  params: [
12151
12738
  {
12152
12739
  key: "arrowheadLengthRatio",
@@ -12208,20 +12795,11 @@ const FIX_MISSION_TASK_METADATA = {
12208
12795
  max: 1,
12209
12796
  step: .01
12210
12797
  },
12211
- {
12212
- key: "labelGapRatio",
12213
- presentationTier: "advanced",
12214
- label: "Label gap",
12215
- description: "Size of the shaft gap holding the F label",
12216
- type: "number",
12217
- min: 0,
12218
- max: 1,
12219
- step: .01
12220
- }
12798
+ LABEL_PADDING_PARAM
12221
12799
  ]
12222
12800
  };
12223
12801
  /** Generates the Fix mission task symbol (341100). */
12224
- function createFixMissionTaskSymbol(coordinates, options = {}) {
12802
+ function createFixMissionTaskSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
12225
12803
  const c1 = coordinates[0];
12226
12804
  const c2 = coordinates[1];
12227
12805
  const tip = project(c1[0], c1[1]);
@@ -12254,12 +12832,12 @@ function createFixMissionTaskSymbol(coordinates, options = {}) {
12254
12832
  const exitRatio = Math.min(1, zigzagEnd + toothStep / 2);
12255
12833
  const zigzagEntry = vecAdd(tail, vecScale(along, axisLength * entryRatio));
12256
12834
  const zigzagExit = vecAdd(tail, vecScale(along, axisLength * exitRatio));
12257
- const labelPosition = clamp01(options.labelPositionRatio ?? DEFAULT_FIX_MISSION_TASK_OPTIONS.labelPositionRatio);
12258
- const labelGap = axisLength * clamp01(options.labelGapRatio ?? DEFAULT_FIX_MISSION_TASK_OPTIONS.labelGapRatio);
12259
- const labelDistance = axisLength * labelPosition;
12835
+ const labelDistance = axisLength * clamp01(options.labelPositionRatio ?? DEFAULT_FIX_MISSION_TASK_OPTIONS.labelPositionRatio);
12260
12836
  const labelCenter = vecAdd(tail, vecScale(along, labelDistance));
12261
- const gapStartDistance = Math.max(0, labelDistance - labelGap / 2);
12262
- const gapEndDistance = Math.min(axisLength, labelDistance + labelGap / 2);
12837
+ const labelRotation = labelRotationAlong(along);
12838
+ const gapHalf = labelKnockoutHalfExtent("F", along, labelRotation, options, context, DEFAULT_FIX_MISSION_TASK_OPTIONS.labelPadding);
12839
+ const gapStartDistance = Math.max(0, labelDistance - gapHalf);
12840
+ const gapEndDistance = Math.min(axisLength, labelDistance + gapHalf);
12263
12841
  const gapStart = vecAdd(tail, vecScale(along, gapStartDistance));
12264
12842
  const gapEnd = vecAdd(tail, vecScale(along, gapEndDistance));
12265
12843
  return {
@@ -12281,7 +12859,7 @@ function createFixMissionTaskSymbol(coordinates, options = {}) {
12281
12859
  createArrowHead$1(tip, along, arrowheadLength, arrowheadWidth)
12282
12860
  ]
12283
12861
  }
12284
- }, createLabelFeature(labelCenter, "F", labelRotationAlong(along), options)]
12862
+ }, createLabelFeature(labelCenter, "F", labelRotation, options)]
12285
12863
  };
12286
12864
  }
12287
12865
  const FIX_MISSION_TASK = defineControlMeasure({
@@ -12326,6 +12904,7 @@ const FLOT_METADATA = {
12326
12904
  },
12327
12905
  drawRule: "Line1",
12328
12906
  capturesLabelSize: true,
12907
+ labelGeometryUsesResolvedMetrics: ["N"],
12329
12908
  params: [
12330
12909
  ...SMOOTH_LINE_PARAMS,
12331
12910
  {
@@ -12348,7 +12927,7 @@ const FLOT_METADATA = {
12348
12927
  step: 1,
12349
12928
  unit: "m"
12350
12929
  },
12351
- LINE_LABEL_PADDING_PARAM,
12930
+ LABEL_PADDING_PARAM,
12352
12931
  {
12353
12932
  key: "arcSegments",
12354
12933
  presentationTier: "advanced",
@@ -12670,7 +13249,7 @@ function phaseLineNameParams(placeholder) {
12670
13249
  type: "boolean",
12671
13250
  visibleWhen: (opts) => Boolean(String(opts.phaseLineName ?? "").trim())
12672
13251
  },
12673
- LINE_LABEL_PADDING_PARAM
13252
+ LABEL_PADDING_PARAM
12674
13253
  ];
12675
13254
  }
12676
13255
  //#endregion
@@ -12855,6 +13434,7 @@ const AIRHEAD_LINE_METADATA = {
12855
13434
  },
12856
13435
  drawRule: "Area1",
12857
13436
  capturesLabelSize: true,
13437
+ labelGeometryUsesResolvedMetrics: ["N"],
12858
13438
  params: LABELED_AREA_PARAMS,
12859
13439
  textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
12860
13440
  };
@@ -13508,6 +14088,7 @@ const FORTIFIED_LINE_METADATA = {
13508
14088
  },
13509
14089
  drawRule: "Line1",
13510
14090
  capturesLabelSize: true,
14091
+ labelGeometryUsesResolvedMetrics: ["N"],
13511
14092
  params: FORTIFIED_PARAMS,
13512
14093
  textAmplifiers: [HOSTILE_LINE_AMPLIFIER]
13513
14094
  };
@@ -13668,6 +14249,7 @@ const FORTIFIED_AREA_METADATA = {
13668
14249
  },
13669
14250
  drawRule: "Area1",
13670
14251
  capturesLabelSize: true,
14252
+ labelGeometryUsesResolvedMetrics: ["N"],
13671
14253
  params: [...FORTIFIED_AREA_PARAMS, AREA_LABEL_PADDING_PARAM],
13672
14254
  textAmplifiers: AREA_TEXT_AMPLIFIERS
13673
14255
  };
@@ -13782,195 +14364,55 @@ const FRONTAL_ATTACK = defineControlMeasure({
13782
14364
  [-.4, -.35],
13783
14365
  [-1, -.8],
13784
14366
  [.45, .55]
13785
- ],
13786
- textAmplifiers: {
13787
- T: "FOX",
13788
- W: "080400Z",
13789
- W1: "120300Z"
13790
- },
13791
- options: { labelSize: 80 }
13792
- }
13793
- });
13794
- //#endregion
13795
- //#region src/generators/cm34-mission-tasks/forward-passage-of-lines.ts
13796
- const DEFAULT_FORWARD_PASSAGE_OF_LINES_OPTIONS = DEFAULT_DELAY_OPTIONS;
13797
- const FORWARD_PASSAGE_OF_LINES_METADATA = {
13798
- id: "forward-passage-of-lines",
13799
- name: "Forward Passage of Lines",
13800
- description: "Passes one force through another force's positions toward the enemy.",
13801
- entity: "Mission Tasks",
13802
- entityType: "Forward Passage of Lines",
13803
- value: "344100",
13804
- minCoordinates: 3,
13805
- maxCoordinates: 3,
13806
- geometry: "line",
13807
- geometryTypes: ["MultiLineString", "Point"],
13808
- paints: {
13809
- stroke: true,
13810
- fill: "none",
13811
- text: true
13812
- },
13813
- drawRule: "Line24",
13814
- capturesLabelSize: true,
13815
- params: DELAY_PARAMS
13816
- };
13817
- function createForwardPassageOfLinesSymbol(coordinates, options = {}) {
13818
- return createDelayVariantSymbol(coordinates, {
13819
- glyph: "P(F)",
13820
- part: "forward-passage-of-lines"
13821
- }, options);
13822
- }
13823
- const FORWARD_PASSAGE_OF_LINES = defineControlMeasure({
13824
- metadata: FORWARD_PASSAGE_OF_LINES_METADATA,
13825
- generator: createForwardPassageOfLinesSymbol,
13826
- defaultOptions: DEFAULT_FORWARD_PASSAGE_OF_LINES_OPTIONS,
13827
- rule: line24DrawRule,
13828
- previewSample: {
13829
- controlPoints: [
13830
- [1, -.4],
13831
- [-1, -.4],
13832
- [0, .6]
13833
- ],
13834
- options: { labelGapRatio: .42 }
13835
- }
13836
- });
13837
- //#endregion
13838
- //#region src/generators/cm34-mission-tasks/isolate.ts
13839
- /**
13840
- * Default options for the ISOLATE symbol.
13841
- */
13842
- const DEFAULT_ISOLATE_OPTIONS = {
13843
- openingAngle: 30,
13844
- barbCount: 9,
13845
- barbLengthRatio: .18
13846
- };
13847
- const ISOLATE_METADATA = {
13848
- id: "isolate",
13849
- name: "Isolate",
13850
- description: "Seals an enemy off from support, denies freedom of movement, and prevents contact with other enemy forces while offensive action continues against it.",
13851
- entity: "Mission Tasks",
13852
- entityType: "Isolate",
13853
- value: "341500",
13854
- minCoordinates: 2,
13855
- maxCoordinates: 2,
13856
- geometry: "line",
13857
- geometryTypes: ["MultiLineString"],
13858
- paints: {
13859
- stroke: true,
13860
- fill: "none",
13861
- text: false
13862
- },
13863
- drawRule: "Area15",
13864
- params: [
13865
- {
13866
- key: "openingAngle",
13867
- presentationTier: "advanced",
13868
- label: "Opening",
13869
- description: "Angular width of the gap on the friendly side, in degrees",
13870
- type: "number",
13871
- min: 0,
13872
- max: 180,
13873
- step: 1
13874
- },
13875
- {
13876
- key: "barbCount",
13877
- presentationTier: "advanced",
13878
- label: "Barbs",
13879
- description: "Number of inward-pointing barbs spaced along the closed arc",
13880
- type: "number",
13881
- min: 1,
13882
- max: 24,
13883
- step: 1
13884
- },
13885
- {
13886
- key: "barbLengthRatio",
13887
- presentationTier: "advanced",
13888
- label: "Barb length",
13889
- description: "Length of each inward barb, as a ratio of the symbol radius",
13890
- type: "number",
13891
- min: .01,
13892
- max: 1,
13893
- step: .01
13894
- }
13895
- ]
13896
- };
13897
- /**
13898
- * Generates a GeoJSON FeatureCollection for an ISOLATE mission task symbol (341500).
13899
- *
13900
- * A near-complete circle centered on PT. 1 whose radius reaches PT. 2. The arc
13901
- * starts (bare) at PT. 2's bearing and sweeps clockwise around the closed
13902
- * portion to a single arrow tip at the far end; the wedge-shaped gap (default
13903
- * 30°) between that arrow tip and PT. 2 is the opening — the "friendly side".
13904
- * Inward-pointing arrowhead barbs are spaced along the closed arc, set in from
13905
- * both ends so they never touch PT. 2 or the arrow tip, conveying the
13906
- * sealing-off of the enclosed force. Unlike the bracket mission tasks, isolate
13907
- * carries no glyph label.
13908
- *
13909
- * Input Points:
13910
- * - PT. 1: Center point
13911
- * - PT. 2: Start point — fixes the radius and the bearing of the opening
13912
- *
13913
- * @param coordinates - [PT. 1, PT. 2] as Position arrays
13914
- * @param options - Configuration options
13915
- * @returns GeoJSON FeatureCollection containing a single MultiLineString feature
13916
- */
13917
- function createIsolateSymbol(coordinates, options = {}) {
13918
- const openingAngle = options.openingAngle ?? DEFAULT_ISOLATE_OPTIONS.openingAngle;
13919
- const barbCount = options.barbCount ?? DEFAULT_ISOLATE_OPTIONS.barbCount;
13920
- const barbLengthRatio = options.barbLengthRatio ?? DEFAULT_ISOLATE_OPTIONS.barbLengthRatio;
13921
- const center = project(coordinates[0][0], coordinates[0][1]);
13922
- const vRadius = vecSub(project(coordinates[1][0], coordinates[1][1]), center);
13923
- const radius = vecMag(vRadius);
13924
- if (radius < 1e-6) return {
13925
- type: "FeatureCollection",
13926
- features: []
13927
- };
13928
- const startAngle = Math.atan2(vRadius[1], vRadius[0]);
13929
- const openingRad = Math.min(Math.max(openingAngle, 0), 359) * Math.PI / 180;
13930
- const arcSpan = 2 * Math.PI - openingRad;
13931
- const endAngle = startAngle - arcSpan;
13932
- const pointOnCircle = (angle, r = radius) => vecAdd(center, [r * Math.cos(angle), r * Math.sin(angle)]);
13933
- const segments = Math.max(8, Math.round(64 * arcSpan / (2 * Math.PI)));
13934
- const arc = [];
13935
- for (let i = 0; i <= segments; i++) {
13936
- const p = pointOnCircle(startAngle - arcSpan * i / segments);
13937
- arc.push(unproject(p[0], p[1]));
13938
- }
13939
- const barbLength = radius * barbLengthRatio;
13940
- const lines = [arc];
13941
- const tipDir = [Math.sin(endAngle), -Math.cos(endAngle)];
13942
- lines.push(createArrowHead$1(pointOnCircle(endAngle), tipDir, barbLength, barbLength));
13943
- const halfWidth = Math.asin(Math.min(1, barbLength / 2 / radius));
13944
- const tipRadius = Math.max(0, radius - barbLength);
13945
- const steps = Math.max(1, barbCount);
13946
- for (let k = 1; k <= steps; k++) {
13947
- const angle = startAngle - arcSpan * k / (steps + 1);
13948
- const left = pointOnCircle(angle - halfWidth);
13949
- const right = pointOnCircle(angle + halfWidth);
13950
- const tip = pointOnCircle(angle, tipRadius);
13951
- lines.push([
13952
- unproject(left[0], left[1]),
13953
- unproject(tip[0], tip[1]),
13954
- unproject(right[0], right[1])
13955
- ]);
14367
+ ],
14368
+ textAmplifiers: {
14369
+ T: "FOX",
14370
+ W: "080400Z",
14371
+ W1: "120300Z"
14372
+ },
14373
+ options: { labelSize: 80 }
13956
14374
  }
13957
- return {
13958
- type: "FeatureCollection",
13959
- features: [{
13960
- type: "Feature",
13961
- properties: { part: "isolate" },
13962
- geometry: {
13963
- type: "MultiLineString",
13964
- coordinates: lines
13965
- }
13966
- }]
13967
- };
14375
+ });
14376
+ //#endregion
14377
+ //#region src/generators/cm34-mission-tasks/forward-passage-of-lines.ts
14378
+ const DEFAULT_FORWARD_PASSAGE_OF_LINES_OPTIONS = DEFAULT_DELAY_OPTIONS;
14379
+ const FORWARD_PASSAGE_OF_LINES_METADATA = {
14380
+ id: "forward-passage-of-lines",
14381
+ name: "Forward Passage of Lines",
14382
+ description: "Passes one force through another force's positions toward the enemy.",
14383
+ entity: "Mission Tasks",
14384
+ entityType: "Forward Passage of Lines",
14385
+ value: "344100",
14386
+ minCoordinates: 3,
14387
+ maxCoordinates: 3,
14388
+ geometry: "line",
14389
+ geometryTypes: ["MultiLineString", "Point"],
14390
+ paints: {
14391
+ stroke: true,
14392
+ fill: "none",
14393
+ text: true
14394
+ },
14395
+ drawRule: "Line24",
14396
+ capturesLabelSize: true,
14397
+ labelGeometryUsesResolvedMetrics: true,
14398
+ params: DELAY_PARAMS
14399
+ };
14400
+ function createForwardPassageOfLinesSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
14401
+ return createDelayVariantSymbol(coordinates, {
14402
+ glyph: "P(F)",
14403
+ part: "forward-passage-of-lines"
14404
+ }, options, context);
13968
14405
  }
13969
- const ISOLATE = defineControlMeasure({
13970
- metadata: ISOLATE_METADATA,
13971
- generator: createIsolateSymbol,
13972
- defaultOptions: DEFAULT_ISOLATE_OPTIONS,
13973
- rule: isolateDrawRule
14406
+ const FORWARD_PASSAGE_OF_LINES = defineControlMeasure({
14407
+ metadata: FORWARD_PASSAGE_OF_LINES_METADATA,
14408
+ generator: createForwardPassageOfLinesSymbol,
14409
+ defaultOptions: DEFAULT_FORWARD_PASSAGE_OF_LINES_OPTIONS,
14410
+ rule: line24DrawRule,
14411
+ previewSample: { controlPoints: [
14412
+ [1, -.4],
14413
+ [-1, -.4],
14414
+ [0, .6]
14415
+ ] }
13974
14416
  });
13975
14417
  //#endregion
13976
14418
  //#region src/generators/cm34-mission-tasks/movement-to-contact.ts
@@ -14194,6 +14636,7 @@ const OCCUPY_METADATA = {
14194
14636
  },
14195
14637
  drawRule: "Area16",
14196
14638
  capturesLabelSize: true,
14639
+ labelGeometryUsesResolvedMetrics: true,
14197
14640
  params: [
14198
14641
  {
14199
14642
  key: "openingAngle",
@@ -14238,10 +14681,10 @@ const OCCUPY_METADATA = {
14238
14681
  }
14239
14682
  ]
14240
14683
  };
14241
- const TAU$1 = Math.PI * 2;
14242
- const MIN_LABEL_GAP_METERS$1 = 1;
14684
+ const TAU$2 = Math.PI * 2;
14685
+ const MIN_LABEL_GAP_METERS$2 = 1;
14243
14686
  /** Generates the Occupy mission task symbol (341700). */
14244
- function createOccupySymbol(coordinates, options = {}) {
14687
+ function createOccupySymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
14245
14688
  const center = project(coordinates[0][0], coordinates[0][1]);
14246
14689
  const radiusVector = vecSub(project(coordinates[1][0], coordinates[1][1]), center);
14247
14690
  const radius = vecMag(radiusVector);
@@ -14255,11 +14698,11 @@ function createOccupySymbol(coordinates, options = {}) {
14255
14698
  };
14256
14699
  const bulgeVector = [radiusVector[1], -radiusVector[0]];
14257
14700
  const halfTurnResolution = normalizeArcResolution(resolved.resolution) / 2;
14258
- const boundarySpan = TAU$1 - Math.min(180, Math.max(5, resolved.openingAngle)) * Math.PI / 180;
14701
+ const boundarySpan = TAU$2 - Math.min(180, Math.max(5, resolved.openingAngle)) * Math.PI / 180;
14259
14702
  const labelSweep = boundarySpan / 2;
14260
- const labelHeight = resolveLabelOffsetMeters(options, 1, radius * .12);
14703
+ const labelHeight = resolveTextMetrics("O", options, context, "regular", radius * .12).height;
14261
14704
  const labelPadding = Math.max(0, resolved.labelPadding);
14262
- const labelHalfGap = Math.min(boundarySpan / 4, Math.max(labelHeight * (LABEL_HALF_GLYPH_RATIO + labelPadding), MIN_LABEL_GAP_METERS$1) / radius);
14705
+ const labelHalfGap = Math.min(boundarySpan / 4, Math.max(labelHeight * (LABEL_HALF_GLYPH_RATIO + labelPadding), MIN_LABEL_GAP_METERS$2) / radius);
14263
14706
  const sampleArc = (from, to) => sampleProjectedArc(center, radiusVector, bulgeVector, from, to, halfTurnResolution);
14264
14707
  const lines = [sampleArc(0, labelSweep - labelHalfGap), sampleArc(labelSweep + labelHalfGap, boundarySpan)];
14265
14708
  const terminal = arcPointAt(center, radiusVector, bulgeVector, boundarySpan);
@@ -14298,7 +14741,7 @@ const OCCUPY = defineControlMeasure({
14298
14741
  const DEFAULT_PENETRATE_OPTIONS = {
14299
14742
  arrowheadLengthRatio: .15,
14300
14743
  arrowheadWidthRatio: .2,
14301
- labelGapRatio: .2
14744
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING
14302
14745
  };
14303
14746
  const PENETRATE_METADATA = {
14304
14747
  id: "penetrate",
@@ -14318,6 +14761,7 @@ const PENETRATE_METADATA = {
14318
14761
  },
14319
14762
  drawRule: "Area17",
14320
14763
  capturesLabelSize: true,
14764
+ labelGeometryUsesResolvedMetrics: true,
14321
14765
  params: [
14322
14766
  {
14323
14767
  key: "arrowheadLengthRatio",
@@ -14339,20 +14783,11 @@ const PENETRATE_METADATA = {
14339
14783
  max: 1,
14340
14784
  step: .01
14341
14785
  },
14342
- {
14343
- key: "labelGapRatio",
14344
- presentationTier: "advanced",
14345
- label: "Label gap",
14346
- description: "Size of the shaft gap holding the 'P' label",
14347
- type: "number",
14348
- min: 0,
14349
- max: 1,
14350
- step: .01
14351
- }
14786
+ LABEL_PADDING_PARAM
14352
14787
  ]
14353
14788
  };
14354
14789
  /** Generates the PENETRATE mission task symbol (341800). */
14355
- function createPenetrateSymbol(coordinates, options = {}) {
14790
+ function createPenetrateSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
14356
14791
  const frame = createProjectedBaselineFrame(project(coordinates[0][0], coordinates[0][1]), project(coordinates[1][0], coordinates[1][1]), {
14357
14792
  origin: "midpoint",
14358
14793
  normal: "right"
@@ -14372,29 +14807,29 @@ function createPenetrateSymbol(coordinates, options = {}) {
14372
14807
  const tail = vecAdd(frame.origin, vecScale(frame.normal, signedTailDistance));
14373
14808
  const arrowDirection = vecScale(vecSub(tip, tail), 1 / shaftLength);
14374
14809
  const labelPoint = vecScale(vecAdd(tail, tip), .5);
14375
- const gapHalf = shaftLength * Math.min(1, Math.max(0, options.labelGapRatio ?? DEFAULT_PENETRATE_OPTIONS.labelGapRatio)) / 2;
14810
+ const shaftRotation = Math.atan2(arrowDirection[1], arrowDirection[0]);
14811
+ const renderedLabelRotation = Math.PI - shaftRotation;
14812
+ const labelRotation = normalizeRadians(Math.PI - keepTextLeftToRight(renderedLabelRotation));
14813
+ const gapHalf = Math.min(shaftLength / 2, labelKnockoutHalfExtent("P", arrowDirection, labelRotation, options, context, DEFAULT_PENETRATE_OPTIONS.labelPadding));
14376
14814
  const gapStart = vecAdd(labelPoint, vecScale(arrowDirection, -gapHalf));
14377
14815
  const gapEnd = vecAdd(labelPoint, vecScale(arrowDirection, gapHalf));
14378
14816
  const headLength = frame.length * Math.max(0, options.arrowheadLengthRatio ?? DEFAULT_PENETRATE_OPTIONS.arrowheadLengthRatio);
14379
14817
  const headWidth = frame.length * Math.max(0, options.arrowheadWidthRatio ?? DEFAULT_PENETRATE_OPTIONS.arrowheadWidthRatio);
14380
- const lineFeature = {
14381
- type: "Feature",
14382
- properties: { part: "penetrate" },
14383
- geometry: {
14384
- type: "MultiLineString",
14385
- coordinates: [
14386
- [coordinates[0], coordinates[1]],
14387
- [unproject(tail[0], tail[1]), unproject(gapStart[0], gapStart[1])],
14388
- [unproject(gapEnd[0], gapEnd[1]), unproject(tip[0], tip[1])],
14389
- createArrowHead$1(tip, arrowDirection, headLength, headWidth)
14390
- ]
14391
- }
14392
- };
14393
- const shaftRotation = Math.atan2(arrowDirection[1], arrowDirection[0]);
14394
- const renderedLabelRotation = Math.PI - shaftRotation;
14395
14818
  return {
14396
14819
  type: "FeatureCollection",
14397
- features: [lineFeature, createLabelFeature(labelPoint, "P", normalizeRadians(Math.PI - keepTextLeftToRight(renderedLabelRotation)), {
14820
+ features: [{
14821
+ type: "Feature",
14822
+ properties: { part: "penetrate" },
14823
+ geometry: {
14824
+ type: "MultiLineString",
14825
+ coordinates: [
14826
+ [coordinates[0], coordinates[1]],
14827
+ [unproject(tail[0], tail[1]), unproject(gapStart[0], gapStart[1])],
14828
+ [unproject(gapEnd[0], gapEnd[1]), unproject(tip[0], tip[1])],
14829
+ createArrowHead$1(tip, arrowDirection, headLength, headWidth)
14830
+ ]
14831
+ }
14832
+ }, createLabelFeature(labelPoint, "P", labelRotation, {
14398
14833
  labelSize: options.labelSize,
14399
14834
  labelSizePixels: options.labelSizePixels
14400
14835
  })]
@@ -14418,7 +14853,7 @@ const DEFAULT_PURSUIT_OPTIONS = {
14418
14853
  arrowheadWidthRatio: .12,
14419
14854
  terminalBarLengthRatio: 1.5,
14420
14855
  labelPosition: .6,
14421
- labelGapRatio: .18,
14856
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING,
14422
14857
  resolution: 64
14423
14858
  };
14424
14859
  const PURSUIT_METADATA = {
@@ -14439,6 +14874,7 @@ const PURSUIT_METADATA = {
14439
14874
  },
14440
14875
  drawRule: "Line33",
14441
14876
  capturesLabelSize: true,
14877
+ labelGeometryUsesResolvedMetrics: true,
14442
14878
  params: [
14443
14879
  {
14444
14880
  key: "arrowheadLengthRatio",
@@ -14480,21 +14916,12 @@ const PURSUIT_METADATA = {
14480
14916
  max: 1,
14481
14917
  step: .01
14482
14918
  },
14483
- {
14484
- key: "labelGapRatio",
14485
- presentationTier: "advanced",
14486
- label: "Label gap",
14487
- description: "Size of the straight-line cutout holding the P label",
14488
- type: "number",
14489
- min: 0,
14490
- max: 1,
14491
- step: .01
14492
- },
14919
+ LABEL_PADDING_PARAM,
14493
14920
  ...ARC_RESOLUTION_PARAMS
14494
14921
  ]
14495
14922
  };
14496
14923
  /** Generates the Pursuit mission task symbol (344000). */
14497
- function createPursuit(coordinates, options = {}) {
14924
+ function createPursuit(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
14498
14925
  const [c1, c2, c3] = coordinates;
14499
14926
  const p1 = project(c1[0], c1[1]);
14500
14927
  const p2 = project(c2[0], c2[1]);
@@ -14515,8 +14942,10 @@ function createPursuit(coordinates, options = {}) {
14515
14942
  features: []
14516
14943
  };
14517
14944
  const p3 = vecAdd(p2, vecScale(diameterAxis, signedDiameter));
14518
- const labelPoint = vecAdd(p1, vecScale(straightDirection, straightLength * clamp01(options.labelPosition ?? DEFAULT_PURSUIT_OPTIONS.labelPosition)));
14519
- const gapHalf = straightLength * clamp01(options.labelGapRatio ?? DEFAULT_PURSUIT_OPTIONS.labelGapRatio) / 2;
14945
+ const labelPosition = clamp01(options.labelPosition ?? DEFAULT_PURSUIT_OPTIONS.labelPosition);
14946
+ const labelPoint = vecAdd(p1, vecScale(straightDirection, straightLength * labelPosition));
14947
+ const labelRotation = labelRotationAlong(straightDirection);
14948
+ const gapHalf = Math.min(straightLength * Math.min(labelPosition, 1 - labelPosition), labelKnockoutHalfExtent("P", straightDirection, labelRotation, options, context, DEFAULT_PURSUIT_OPTIONS.labelPadding));
14520
14949
  const gapStart = vecAdd(labelPoint, vecScale(straightDirection, -gapHalf));
14521
14950
  const gapEnd = vecAdd(labelPoint, vecScale(straightDirection, gapHalf));
14522
14951
  const center = midpoint(p2, p3);
@@ -14543,7 +14972,7 @@ function createPursuit(coordinates, options = {}) {
14543
14972
  terminalBar
14544
14973
  ]
14545
14974
  }
14546
- }, createLabelFeature(labelPoint, "P", labelRotationAlong(straightDirection), options)]
14975
+ }, createLabelFeature(labelPoint, "P", labelRotation, options)]
14547
14976
  };
14548
14977
  }
14549
14978
  const PURSUIT = defineControlMeasure({
@@ -14581,27 +15010,80 @@ const REARWARD_PASSAGE_OF_LINES_METADATA = {
14581
15010
  },
14582
15011
  drawRule: "Line24",
14583
15012
  capturesLabelSize: true,
15013
+ labelGeometryUsesResolvedMetrics: true,
14584
15014
  params: DELAY_PARAMS
14585
15015
  };
14586
- function createRearwardPassageOfLinesSymbol(coordinates, options = {}) {
15016
+ function createRearwardPassageOfLinesSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
14587
15017
  return createDelayVariantSymbol(coordinates, {
14588
15018
  glyph: "P(R)",
14589
15019
  part: "rearward-passage-of-lines"
14590
- }, options);
15020
+ }, options, context);
14591
15021
  }
14592
15022
  const REARWARD_PASSAGE_OF_LINES = defineControlMeasure({
14593
15023
  metadata: REARWARD_PASSAGE_OF_LINES_METADATA,
14594
15024
  generator: createRearwardPassageOfLinesSymbol,
14595
15025
  defaultOptions: DEFAULT_REARWARD_PASSAGE_OF_LINES_OPTIONS,
14596
15026
  rule: line24DrawRule,
14597
- previewSample: {
14598
- controlPoints: [
14599
- [-1, -.4],
14600
- [1, -.4],
14601
- [0, .6]
14602
- ],
14603
- options: { labelGapRatio: .42 }
14604
- }
15027
+ previewSample: { controlPoints: [
15028
+ [-1, -.4],
15029
+ [1, -.4],
15030
+ [0, .6]
15031
+ ] }
15032
+ });
15033
+ //#endregion
15034
+ //#region src/generators/cm34-mission-tasks/relief-in-place.ts
15035
+ const DEFAULT_RELIEF_IN_PLACE_OPTIONS = {
15036
+ arrowheadLengthRatio: DEFAULT_DELAY_OPTIONS.arrowheadLengthRatio,
15037
+ arrowheadWidthRatio: DEFAULT_DELAY_OPTIONS.arrowheadWidthRatio,
15038
+ arcSegments: DEFAULT_DELAY_OPTIONS.arcSegments
15039
+ };
15040
+ const RELIEF_IN_PLACE_METADATA = {
15041
+ id: "relief-in-place",
15042
+ name: "Relief in Place",
15043
+ description: "Replaces all or part of a unit in an area with an incoming unit while maintaining the original unit's responsibilities.",
15044
+ entity: "Mission Tasks",
15045
+ entityType: "Relief in Place",
15046
+ value: "341900",
15047
+ minCoordinates: 4,
15048
+ maxCoordinates: 4,
15049
+ geometry: "line",
15050
+ geometryTypes: ["MultiLineString", "Point"],
15051
+ paints: {
15052
+ stroke: true,
15053
+ fill: "none",
15054
+ text: true
15055
+ },
15056
+ drawRule: "Area18",
15057
+ capturesLabelSize: true,
15058
+ params: DELAY_PARAMS.filter((param) => param.key !== "labelPadding")
15059
+ };
15060
+ /** Generates the RELIEF IN PLACE mission task symbol (341900). */
15061
+ function createReliefInPlaceSymbol(coordinates, options = {}) {
15062
+ return createHairpinSymbol(coordinates, {
15063
+ part: "relief-in-place",
15064
+ buildLabel: ({ points, upperDirection, upperArm }) => ({
15065
+ upperArm,
15066
+ label: createLabelFeature(averagePoints(points), "RIP", labelRotationAlong(upperDirection), {
15067
+ labelSize: options.labelSize,
15068
+ labelSizePixels: options.labelSizePixels
15069
+ })
15070
+ })
15071
+ }, options);
15072
+ }
15073
+ function averagePoints(points) {
15074
+ return vecScale(points.reduce((sum, point) => vecAdd(sum, point), [0, 0]), 1 / points.length);
15075
+ }
15076
+ const RELIEF_IN_PLACE = defineControlMeasure({
15077
+ metadata: RELIEF_IN_PLACE_METADATA,
15078
+ generator: createReliefInPlaceSymbol,
15079
+ defaultOptions: DEFAULT_RELIEF_IN_PLACE_OPTIONS,
15080
+ rule: reliefInPlaceDrawRule,
15081
+ previewSample: { controlPoints: [
15082
+ [-1, .45],
15083
+ [.45, .45],
15084
+ [.45, -.45],
15085
+ [-1, -.45]
15086
+ ] }
14605
15087
  });
14606
15088
  //#endregion
14607
15089
  //#region src/generators/cm34-mission-tasks/retire.ts
@@ -14624,14 +15106,15 @@ const RETIRE_METADATA = {
14624
15106
  },
14625
15107
  drawRule: "Line24",
14626
15108
  capturesLabelSize: true,
15109
+ labelGeometryUsesResolvedMetrics: true,
14627
15110
  params: DELAY_PARAMS
14628
15111
  };
14629
15112
  /** Generates the RETIRE/RETIREMENT mission task symbol (342000). */
14630
- function createRetireSymbol(coordinates, options = {}) {
15113
+ function createRetireSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
14631
15114
  return createDelayVariantSymbol(coordinates, {
14632
15115
  glyph: "R",
14633
15116
  part: "retire"
14634
- }, options);
15117
+ }, options, context);
14635
15118
  }
14636
15119
  const RETIRE = defineControlMeasure({
14637
15120
  metadata: RETIRE_METADATA,
@@ -14685,11 +15168,137 @@ const SCREEN = defineControlMeasure({
14685
15168
  }
14686
15169
  });
14687
15170
  //#endregion
15171
+ //#region src/generators/cm34-mission-tasks/secure.ts
15172
+ const DEFAULT_SECURE_OPTIONS = {
15173
+ openingAngle: 30,
15174
+ resolution: 64,
15175
+ arrowheadLengthRatio: .2,
15176
+ arrowheadWidthRatio: .4,
15177
+ labelPadding: .2
15178
+ };
15179
+ const SECURE_METADATA = {
15180
+ id: "secure",
15181
+ name: "Secure",
15182
+ description: "Prevents a unit, facility, or geographical location from being damaged or destroyed by enemy action.",
15183
+ entity: "Mission Tasks",
15184
+ entityType: "Secure",
15185
+ value: "342100",
15186
+ minCoordinates: 2,
15187
+ maxCoordinates: 2,
15188
+ geometry: "area",
15189
+ geometryTypes: ["MultiLineString", "Point"],
15190
+ paints: {
15191
+ stroke: true,
15192
+ fill: "none",
15193
+ text: true
15194
+ },
15195
+ drawRule: "Area19",
15196
+ capturesLabelSize: true,
15197
+ labelGeometryUsesResolvedMetrics: true,
15198
+ params: [
15199
+ {
15200
+ key: "openingAngle",
15201
+ presentationTier: "advanced",
15202
+ label: "Opening",
15203
+ description: "Angular width of the opening on the friendly side, in degrees",
15204
+ type: "number",
15205
+ min: 5,
15206
+ max: 180,
15207
+ step: 1
15208
+ },
15209
+ ...ARC_RESOLUTION_PARAMS,
15210
+ {
15211
+ key: "arrowheadLengthRatio",
15212
+ presentationTier: "advanced",
15213
+ label: "Arrowhead length",
15214
+ description: "Arrowhead length as a ratio of the symbol radius",
15215
+ type: "number",
15216
+ min: 0,
15217
+ max: 1,
15218
+ step: .01
15219
+ },
15220
+ {
15221
+ key: "arrowheadWidthRatio",
15222
+ presentationTier: "advanced",
15223
+ label: "Arrowhead width",
15224
+ description: "Arrowhead width as a ratio of the symbol radius",
15225
+ type: "number",
15226
+ min: 0,
15227
+ max: 1,
15228
+ step: .01
15229
+ },
15230
+ {
15231
+ key: "labelPadding",
15232
+ presentationTier: "advanced",
15233
+ label: "S padding",
15234
+ description: "Extra boundary clearance on each side of S, as a ratio of its height",
15235
+ type: "number",
15236
+ min: 0,
15237
+ max: 2,
15238
+ step: .05
15239
+ }
15240
+ ]
15241
+ };
15242
+ const TAU$1 = Math.PI * 2;
15243
+ const MIN_LABEL_GAP_METERS$1 = 1;
15244
+ /** Generates the Secure mission task symbol (342100). */
15245
+ function createSecureSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
15246
+ const center = project(coordinates[0][0], coordinates[0][1]);
15247
+ const radiusVector = vecSub(project(coordinates[1][0], coordinates[1][1]), center);
15248
+ const radius = vecMag(radiusVector);
15249
+ if (radius < 1e-6) return {
15250
+ type: "FeatureCollection",
15251
+ features: []
15252
+ };
15253
+ const resolved = {
15254
+ ...DEFAULT_SECURE_OPTIONS,
15255
+ ...options
15256
+ };
15257
+ const bulgeVector = [radiusVector[1], -radiusVector[0]];
15258
+ const halfTurnResolution = normalizeArcResolution(resolved.resolution) / 2;
15259
+ const boundarySpan = TAU$1 - Math.min(180, Math.max(5, resolved.openingAngle)) * Math.PI / 180;
15260
+ const labelSweep = boundarySpan / 2;
15261
+ const labelHeight = resolveTextMetrics("S", options, context, "regular", radius * .12).height;
15262
+ const labelPadding = Math.max(0, resolved.labelPadding);
15263
+ const labelHalfGap = Math.min(boundarySpan / 4, Math.max(labelHeight * (LABEL_HALF_GLYPH_RATIO + labelPadding), MIN_LABEL_GAP_METERS$1) / radius);
15264
+ const sampleArc = (from, to) => sampleProjectedArc(center, radiusVector, bulgeVector, from, to, halfTurnResolution);
15265
+ const terminal = arcPointAt(center, radiusVector, bulgeVector, boundarySpan);
15266
+ const radial = vecScale(vecSub(terminal, center), 1 / radius);
15267
+ const tangent = [radial[1], -radial[0]];
15268
+ const lines = [
15269
+ sampleArc(0, labelSweep - labelHalfGap),
15270
+ sampleArc(labelSweep + labelHalfGap, boundarySpan),
15271
+ createArrowHead$1(terminal, tangent, radius * Math.max(0, resolved.arrowheadLengthRatio), radius * Math.max(0, resolved.arrowheadWidthRatio))
15272
+ ];
15273
+ const labelPoint = arcPointAt(center, radiusVector, bulgeVector, labelSweep);
15274
+ return {
15275
+ type: "FeatureCollection",
15276
+ features: [{
15277
+ type: "Feature",
15278
+ properties: { part: "secure" },
15279
+ geometry: {
15280
+ type: "MultiLineString",
15281
+ coordinates: lines
15282
+ }
15283
+ }, createLabelFeature(labelPoint, "S", Math.PI, options)]
15284
+ };
15285
+ }
15286
+ const SECURE = defineControlMeasure({
15287
+ metadata: SECURE_METADATA,
15288
+ generator: createSecureSymbol,
15289
+ defaultOptions: DEFAULT_SECURE_OPTIONS,
15290
+ rule: secureDrawRule,
15291
+ previewSample: {
15292
+ controlPoints: [[0, 0], [-.8, .05]],
15293
+ options: { labelSize: 90 }
15294
+ }
15295
+ });
15296
+ //#endregion
14688
15297
  //#region src/generators/cm34-mission-tasks/seize.ts
14689
15298
  const DEFAULT_SEIZE_OPTIONS = {
14690
15299
  arrowheadLengthRatio: .1,
14691
15300
  arrowheadWidthRatio: .12,
14692
- labelGapRatio: .13,
15301
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING,
14693
15302
  curveSegments: 48,
14694
15303
  circleSegments: 64
14695
15304
  };
@@ -14714,16 +15323,7 @@ const SEIZE_PARAMS = [
14714
15323
  max: .5,
14715
15324
  step: .01
14716
15325
  },
14717
- {
14718
- key: "labelGapRatio",
14719
- presentationTier: "advanced",
14720
- label: "Label gap",
14721
- description: "Size of the curve gap holding the S label",
14722
- type: "number",
14723
- min: 0,
14724
- max: .5,
14725
- step: .01
14726
- },
15326
+ LABEL_PADDING_PARAM,
14727
15327
  {
14728
15328
  key: "curveSegments",
14729
15329
  presentationTier: "advanced",
@@ -14763,6 +15363,7 @@ const SEIZE_METADATA = {
14763
15363
  },
14764
15364
  drawRule: "Line27",
14765
15365
  capturesLabelSize: true,
15366
+ labelGeometryUsesResolvedMetrics: true,
14766
15367
  params: SEIZE_PARAMS
14767
15368
  };
14768
15369
  function positiveAngle(angle) {
@@ -14794,7 +15395,7 @@ function threePointArc(start, middle, end) {
14794
15395
  };
14795
15396
  }
14796
15397
  /** Shared geometry for mission tasks that use the Seize graphic. */
14797
- function createSeizeLikeSymbol(coordinates, options = {}, label = "S", part = "seize") {
15398
+ function createSeizeLikeSymbol(coordinates, options = {}, label = "S", part = "seize", context = {}) {
14798
15399
  const [c1, c2, c3, c4] = coordinates;
14799
15400
  const center = project(c1[0], c1[1]);
14800
15401
  const start = project(c2[0], c2[1]);
@@ -14815,8 +15416,14 @@ function createSeizeLikeSymbol(coordinates, options = {}, label = "S", part = "s
14815
15416
  let arrowDirection;
14816
15417
  let labelPoint;
14817
15418
  let tangentAtLabel;
15419
+ let labelRotation;
14818
15420
  if (arc) {
14819
- const gapHalf = clamp01(options.labelGapRatio ?? DEFAULT_SEIZE_OPTIONS.labelGapRatio) / 2;
15421
+ const labelAngle = arc.startAngle + arc.sweepAngle / 2;
15422
+ const sweepSign = Math.sign(arc.sweepAngle);
15423
+ labelPoint = vecAdd(arc.center, [arc.radius * Math.cos(labelAngle), arc.radius * Math.sin(labelAngle)]);
15424
+ tangentAtLabel = [-Math.sin(labelAngle) * sweepSign, Math.cos(labelAngle) * sweepSign];
15425
+ labelRotation = labelRotationAlong(tangentAtLabel);
15426
+ const gapHalf = arcKnockoutHalfFraction(labelKnockoutHalfExtent(label, tangentAtLabel, labelRotation, options, context, DEFAULT_SEIZE_OPTIONS.labelPadding), arc.radius, arc.sweepAngle);
14820
15427
  const gapStart = .5 - gapHalf;
14821
15428
  const gapEnd = .5 + gapHalf;
14822
15429
  const sampleArc = (from, to) => {
@@ -14832,22 +15439,19 @@ function createSeizeLikeSymbol(coordinates, options = {}, label = "S", part = "s
14832
15439
  firstArcPart = sampleArc(0, gapStart);
14833
15440
  secondArcPart = sampleArc(gapEnd, 1);
14834
15441
  const endAngle = arc.startAngle + arc.sweepAngle;
14835
- const sweepSign = Math.sign(arc.sweepAngle);
14836
15442
  arrowDirection = [-Math.sin(endAngle) * sweepSign, Math.cos(endAngle) * sweepSign];
14837
- const labelAngle = arc.startAngle + arc.sweepAngle / 2;
14838
- labelPoint = vecAdd(arc.center, [arc.radius * Math.cos(labelAngle), arc.radius * Math.sin(labelAngle)]);
14839
- tangentAtLabel = [-Math.sin(labelAngle) * sweepSign, Math.cos(labelAngle) * sweepSign];
14840
15443
  } else {
14841
15444
  labelPoint = vecScale(vecAdd(start, tip), .5);
14842
- const gapHalf = span * clamp01(options.labelGapRatio ?? DEFAULT_SEIZE_OPTIONS.labelGapRatio) / 2;
14843
15445
  const direction = vecNorm(vecSub(tip, start));
15446
+ tangentAtLabel = direction;
15447
+ labelRotation = labelRotationAlong(tangentAtLabel);
15448
+ const gapHalf = Math.min(span / 2, labelKnockoutHalfExtent(label, direction, labelRotation, options, context, DEFAULT_SEIZE_OPTIONS.labelPadding));
14844
15449
  const before = vecAdd(labelPoint, vecScale(direction, -gapHalf));
14845
15450
  const after = vecAdd(labelPoint, vecScale(direction, gapHalf));
14846
15451
  firstArcPart = [toPosition(start), toPosition(before)];
14847
15452
  secondArcPart = [toPosition(after), toPosition(tip)];
14848
15453
  const terminal = vecSub(tip, curvePoint);
14849
15454
  arrowDirection = vecMag(terminal) >= 1e-6 ? vecNorm(terminal) : vecNorm(vecSub(tip, start));
14850
- tangentAtLabel = direction;
14851
15455
  }
14852
15456
  const startAngle = Math.atan2(radiusVector[1], radiusVector[0]);
14853
15457
  const circle = [];
@@ -14870,12 +15474,12 @@ function createSeizeLikeSymbol(coordinates, options = {}, label = "S", part = "s
14870
15474
  type: "MultiLineString",
14871
15475
  coordinates: lines
14872
15476
  }
14873
- }, createLabelFeature(labelPoint, label, labelRotationAlong(tangentAtLabel), options)]
15477
+ }, createLabelFeature(labelPoint, label, labelRotation, options)]
14874
15478
  };
14875
15479
  }
14876
15480
  /** Generates the Seize mission task symbol (342300). */
14877
- function createSeizeSymbol(coordinates, options = {}) {
14878
- return createSeizeLikeSymbol(coordinates, options);
15481
+ function createSeizeSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
15482
+ return createSeizeLikeSymbol(coordinates, options, "S", "seize", context);
14879
15483
  }
14880
15484
  const SEIZE = defineControlMeasure({
14881
15485
  metadata: SEIZE_METADATA,
@@ -14913,11 +15517,12 @@ const EVACUATE_METADATA = {
14913
15517
  },
14914
15518
  drawRule: "Line27",
14915
15519
  capturesLabelSize: true,
15520
+ labelGeometryUsesResolvedMetrics: true,
14916
15521
  params: SEIZE_PARAMS
14917
15522
  };
14918
15523
  /** Generates the Evacuate mission task symbol (344500). */
14919
- function createEvacuateSymbol(coordinates, options = {}) {
14920
- return createSeizeLikeSymbol(coordinates, options, "E", "evacuate");
15524
+ function createEvacuateSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
15525
+ return createSeizeLikeSymbol(coordinates, options, "E", "evacuate", context);
14921
15526
  }
14922
15527
  const EVACUATE = defineControlMeasure({
14923
15528
  metadata: EVACUATE_METADATA,
@@ -14955,13 +15560,14 @@ const WITHDRAW_METADATA = {
14955
15560
  },
14956
15561
  drawRule: "Line24",
14957
15562
  capturesLabelSize: true,
15563
+ labelGeometryUsesResolvedMetrics: true,
14958
15564
  params: DELAY_PARAMS
14959
15565
  };
14960
- function createWithdrawSymbol(coordinates, options = {}) {
15566
+ function createWithdrawSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
14961
15567
  return createDelayVariantSymbol(coordinates, {
14962
15568
  glyph: "W",
14963
15569
  part: "withdraw"
14964
- }, options);
15570
+ }, options, context);
14965
15571
  }
14966
15572
  const WITHDRAW = defineControlMeasure({
14967
15573
  metadata: WITHDRAW_METADATA,
@@ -14990,27 +15596,25 @@ const WITHDRAW_UNDER_PRESSURE_METADATA = {
14990
15596
  },
14991
15597
  drawRule: "Line24",
14992
15598
  capturesLabelSize: true,
15599
+ labelGeometryUsesResolvedMetrics: true,
14993
15600
  params: DELAY_PARAMS
14994
15601
  };
14995
- function createWithdrawUnderPressureSymbol(coordinates, options = {}) {
15602
+ function createWithdrawUnderPressureSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
14996
15603
  return createDelayVariantSymbol(coordinates, {
14997
15604
  glyph: "WP",
14998
15605
  part: "withdraw-under-pressure"
14999
- }, options);
15606
+ }, options, context);
15000
15607
  }
15001
15608
  const WITHDRAW_UNDER_PRESSURE = defineControlMeasure({
15002
15609
  metadata: WITHDRAW_UNDER_PRESSURE_METADATA,
15003
15610
  generator: createWithdrawUnderPressureSymbol,
15004
15611
  defaultOptions: DEFAULT_WITHDRAW_UNDER_PRESSURE_OPTIONS,
15005
15612
  rule: line24DrawRule,
15006
- previewSample: {
15007
- controlPoints: [
15008
- [-1, -.4],
15009
- [1, -.4],
15010
- [0, .6]
15011
- ],
15012
- options: { labelGapRatio: .3 }
15013
- }
15613
+ previewSample: { controlPoints: [
15614
+ [-1, -.4],
15615
+ [1, -.4],
15616
+ [0, .6]
15617
+ ] }
15014
15618
  });
15015
15619
  //#endregion
15016
15620
  //#region src/generators/cm27-protection-areas/turn.ts
@@ -15076,10 +15680,10 @@ const TURN_METADATA = {
15076
15680
  * arc and placing its tip at PT1. PT3 selects which side of the PT2 -> PT1
15077
15681
  * chord contains the arc.
15078
15682
  *
15079
- * `labelGapRatio` opens a gap centered on the arc midpoint, sized as a ratio
15080
- * of the arc sweep, for measures that carry a doctrinal glyph there.
15683
+ * `labelGapHalfLength` optionally resolves a meter-denominated half-gap from the arc midpoint
15684
+ * anchor for measures that carry a doctrinal glyph there.
15081
15685
  */
15082
- function createTurnLikeSymbol(coordinates, options = {}, labelGapRatio = 0) {
15686
+ function createTurnLikeSymbol(coordinates, options = {}, labelGapHalfLength) {
15083
15687
  const tip = project(coordinates[0][0], coordinates[0][1]);
15084
15688
  const rear = project(coordinates[1][0], coordinates[1][1]);
15085
15689
  const side = quarterArcSide(rear, tip, coordinates[2] ? project(coordinates[2][0], coordinates[2][1]) : void 0);
@@ -15104,14 +15708,14 @@ function createTurnLikeSymbol(coordinates, options = {}, labelGapRatio = 0) {
15104
15708
  }
15105
15709
  return points;
15106
15710
  };
15107
- const gapHalf = clamp01(labelGapRatio) / 2;
15108
- const arcParts = gapHalf > 0 ? [sampleArc(0, .5 - gapHalf), sampleArc(.5 + gapHalf, 1)] : [sampleArc(0, 1)];
15109
15711
  const sweepSign = Math.sign(arc.sweepAngle);
15110
15712
  const midAngle = arc.startAngle + arc.sweepAngle / 2;
15111
15713
  const labelAnchor = {
15112
15714
  point: arc.midpoint,
15113
15715
  tangent: [-Math.sin(midAngle) * sweepSign, Math.cos(midAngle) * sweepSign]
15114
15716
  };
15717
+ const gapHalf = arcKnockoutHalfFraction(labelGapHalfLength?.(labelAnchor) ?? 0, arc.radius, arc.sweepAngle);
15718
+ const arcParts = gapHalf > 0 ? [sampleArc(0, .5 - gapHalf), sampleArc(.5 + gapHalf, 1)] : [sampleArc(0, 1)];
15115
15719
  const headWidth = totalLength * (options.arrowheadWidthRatio ?? DEFAULT_TURN_OPTIONS.arrowheadWidthRatio);
15116
15720
  const arrowHead = createArrowHeadRing(tip, vecNorm(vecSub(tip, headBase)), vecMag(vecSub(tip, headBase)), headWidth);
15117
15721
  return {
@@ -15165,7 +15769,7 @@ const DEFAULT_TURN_MISSION_TASK_OPTIONS = {
15165
15769
  ...DEFAULT_TURN_OPTIONS,
15166
15770
  arrowheadLengthRatio: .14,
15167
15771
  arrowheadWidthRatio: .14,
15168
- labelGapRatio: .18
15772
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING
15169
15773
  };
15170
15774
  const TURN_MISSION_TASK_METADATA = {
15171
15775
  id: "turn-mission-task",
@@ -15188,24 +15792,19 @@ const TURN_MISSION_TASK_METADATA = {
15188
15792
  },
15189
15793
  drawRule: "Line10",
15190
15794
  capturesLabelSize: true,
15191
- params: [...TURN_PARAMS, {
15192
- key: "labelGapRatio",
15193
- presentationTier: "advanced",
15194
- label: "Label gap",
15195
- description: "Size of the arc gap holding the doctrinal 'T' label",
15196
- type: "number",
15197
- min: 0,
15198
- max: 1,
15199
- step: .01
15200
- }]
15795
+ labelGeometryUsesResolvedMetrics: true,
15796
+ params: [...TURN_PARAMS, LABEL_PADDING_PARAM]
15201
15797
  };
15202
15798
  /** Generates the TURN mission task symbol (344700). */
15203
- function createTurnMissionTaskSymbol(coordinates, options = {}) {
15799
+ function createTurnMissionTaskSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
15204
15800
  const { features, labelAnchor } = createTurnLikeSymbol(coordinates, {
15205
15801
  arrowheadLengthRatio: options.arrowheadLengthRatio ?? DEFAULT_TURN_MISSION_TASK_OPTIONS.arrowheadLengthRatio,
15206
15802
  arrowheadWidthRatio: options.arrowheadWidthRatio ?? DEFAULT_TURN_MISSION_TASK_OPTIONS.arrowheadWidthRatio,
15207
15803
  arcSegments: options.arcSegments ?? DEFAULT_TURN_MISSION_TASK_OPTIONS.arcSegments
15208
- }, options.labelGapRatio ?? DEFAULT_TURN_MISSION_TASK_OPTIONS.labelGapRatio);
15804
+ }, (anchor) => {
15805
+ const rotation = labelRotationAlong(anchor.tangent);
15806
+ return labelKnockoutHalfExtent("T", anchor.tangent, rotation, options, context, DEFAULT_TURN_MISSION_TASK_OPTIONS.labelPadding);
15807
+ });
15209
15808
  if (!labelAnchor) return {
15210
15809
  type: "FeatureCollection",
15211
15810
  features: []
@@ -15251,6 +15850,7 @@ const JOINT_TACTICAL_ACTION_AREA_METADATA = {
15251
15850
  },
15252
15851
  drawRule: "Area1",
15253
15852
  capturesLabelSize: true,
15853
+ labelGeometryUsesResolvedMetrics: ["N"],
15254
15854
  params: PREFIXED_LABELED_AREA_PARAMS,
15255
15855
  textAmplifiers: AREA_TEXT_AMPLIFIERS
15256
15856
  };
@@ -15311,6 +15911,7 @@ const LANDING_ZONE_METADATA = {
15311
15911
  },
15312
15912
  drawRule: "Area1",
15313
15913
  capturesLabelSize: true,
15914
+ labelGeometryUsesResolvedMetrics: ["N"],
15314
15915
  params: PREFIXED_LABELED_AREA_PARAMS,
15315
15916
  textAmplifiers: AREA_TEXT_AMPLIFIERS
15316
15917
  };
@@ -15366,6 +15967,7 @@ const PENETRATION_BOX_METADATA = {
15366
15967
  },
15367
15968
  drawRule: "Area1",
15368
15969
  capturesLabelSize: true,
15970
+ labelGeometryUsesResolvedMetrics: ["N"],
15369
15971
  params: LABELED_AREA_PARAMS,
15370
15972
  textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
15371
15973
  };
@@ -15417,6 +16019,7 @@ const LIMITED_ACCESS_AREA_METADATA = {
15417
16019
  },
15418
16020
  drawRule: "Area1",
15419
16021
  capturesLabelSize: true,
16022
+ labelGeometryUsesResolvedMetrics: ["N"],
15420
16023
  params: [...AREA_SMOOTH_PARAMS, AREA_LABEL_PADDING_PARAM],
15421
16024
  textAmplifiers: [AREA_HOSTILE_AMPLIFIER]
15422
16025
  };
@@ -15512,6 +16115,7 @@ const NO_FIRE_AREA_IRREGULAR_METADATA = {
15512
16115
  },
15513
16116
  drawRule: "Area1",
15514
16117
  capturesLabelSize: true,
16118
+ labelGeometryUsesResolvedMetrics: ["N"],
15515
16119
  params: [...AREA_SMOOTH_PARAMS, AREA_LABEL_PADDING_PARAM],
15516
16120
  textAmplifiers: [...NO_FIRE_AREA_TEXT_AMPLIFIERS, AREA_HOSTILE_AMPLIFIER]
15517
16121
  };
@@ -15757,6 +16361,7 @@ const MINEFIELD_METADATA = {
15757
16361
  text: true
15758
16362
  },
15759
16363
  drawRule: "Point2",
16364
+ labelGeometryUsesResolvedMetrics: ["N"],
15760
16365
  params: [
15761
16366
  MINE_TYPE_PARAM,
15762
16367
  {
@@ -16059,6 +16664,7 @@ const COMMON_METADATA = {
16059
16664
  },
16060
16665
  drawRule: "Area1",
16061
16666
  capturesLabelSize: true,
16667
+ labelGeometryUsesResolvedMetrics: ["N"],
16062
16668
  params: [
16063
16669
  MINE_TYPE_PARAM,
16064
16670
  ...AREA_SMOOTH_PARAMS,
@@ -16081,7 +16687,8 @@ const MINED_AREA_METADATA = {
16081
16687
  name: "Mined Area",
16082
16688
  description: "An area dangerous because of the presence or suspected presence of mines.",
16083
16689
  entityType: "Mined Area",
16084
- value: "270800"
16690
+ value: "270800",
16691
+ labelGeometryUsesResolvedMetrics: true
16085
16692
  };
16086
16693
  function axisIntersections(ring, axis, target) {
16087
16694
  const other = axis === 0 ? 1 : 0;
@@ -16628,6 +17235,7 @@ const PICKUP_ZONE_METADATA = {
16628
17235
  },
16629
17236
  drawRule: "Area1",
16630
17237
  capturesLabelSize: true,
17238
+ labelGeometryUsesResolvedMetrics: ["N"],
16631
17239
  params: PREFIXED_LABELED_AREA_PARAMS,
16632
17240
  textAmplifiers: AREA_TEXT_AMPLIFIERS
16633
17241
  };
@@ -16683,6 +17291,7 @@ const ASSAULT_POSITION_METADATA = {
16683
17291
  },
16684
17292
  drawRule: "Area1",
16685
17293
  capturesLabelSize: true,
17294
+ labelGeometryUsesResolvedMetrics: ["N"],
16686
17295
  params: PREFIXED_LABELED_AREA_PARAMS,
16687
17296
  textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
16688
17297
  };
@@ -16738,6 +17347,7 @@ const ATTACK_POSITION_METADATA = {
16738
17347
  },
16739
17348
  drawRule: "Area1",
16740
17349
  capturesLabelSize: true,
17350
+ labelGeometryUsesResolvedMetrics: ["N"],
16741
17351
  params: PREFIXED_LABELED_AREA_PARAMS,
16742
17352
  textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
16743
17353
  };
@@ -16793,6 +17403,7 @@ const OBJECTIVE_AREA_METADATA = {
16793
17403
  },
16794
17404
  drawRule: "Area1",
16795
17405
  capturesLabelSize: true,
17406
+ labelGeometryUsesResolvedMetrics: ["N"],
16796
17407
  params: PREFIXED_LABELED_AREA_PARAMS,
16797
17408
  textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
16798
17409
  };
@@ -17142,6 +17753,7 @@ const STRONG_POINT_METADATA = {
17142
17753
  },
17143
17754
  drawRule: "Area1",
17144
17755
  capturesLabelSize: true,
17756
+ labelGeometryUsesResolvedMetrics: ["N"],
17145
17757
  params: ECHELON_AREA_PARAMS,
17146
17758
  textAmplifiers: AREA_UNIT_DESIGNATION_HOSTILE_AMPLIFIERS
17147
17759
  };
@@ -17463,6 +18075,7 @@ const SUBMARINE_ACTION_AREA_METADATA = {
17463
18075
  },
17464
18076
  drawRule: "Area1",
17465
18077
  capturesLabelSize: true,
18078
+ labelGeometryUsesResolvedMetrics: ["N"],
17466
18079
  params: PREFIXED_LABELED_AREA_PARAMS,
17467
18080
  textAmplifiers: AREA_TEXT_AMPLIFIERS_NO_ADDITIONAL_INFO
17468
18081
  };
@@ -17523,6 +18136,7 @@ const SUBMARINE_GENERATED_ACTION_AREA_METADATA = {
17523
18136
  },
17524
18137
  drawRule: "Area1",
17525
18138
  capturesLabelSize: true,
18139
+ labelGeometryUsesResolvedMetrics: ["N"],
17526
18140
  params: PREFIXED_LABELED_AREA_PARAMS,
17527
18141
  textAmplifiers: AREA_TEXT_AMPLIFIERS_NO_ADDITIONAL_INFO
17528
18142
  };
@@ -17845,13 +18459,17 @@ const DEFINITIONS = {
17845
18459
  bypass: BYPASS,
17846
18460
  canalize: CANALIZE,
17847
18461
  clear: CLEAR,
18462
+ "cordon-and-knock": CORDON_AND_KNOCK,
18463
+ "cordon-and-search": CORDON_AND_SEARCH,
17848
18464
  counterattack: COUNTERATTACK,
17849
18465
  "counterattack-by-fire": COUNTERATTACK_BY_FIRE,
17850
18466
  cover: COVER,
17851
18467
  delay: DELAY,
18468
+ demonstrate: DEMONSTRATE,
17852
18469
  disengage: DISENGAGE,
17853
18470
  "disrupt-mission-task": DISRUPT_MISSION_TASK,
17854
18471
  envelopment: ENVELOPMENT,
18472
+ exploitation: EXPLOITATION,
17855
18473
  "forward-passage-of-lines": FORWARD_PASSAGE_OF_LINES,
17856
18474
  infiltration: INFILTRATION,
17857
18475
  guard: GUARD,
@@ -17861,8 +18479,10 @@ const DEFINITIONS = {
17861
18479
  penetrate: PENETRATE,
17862
18480
  pursuit: PURSUIT,
17863
18481
  "rearward-passage-of-lines": REARWARD_PASSAGE_OF_LINES,
18482
+ "relief-in-place": RELIEF_IN_PLACE,
17864
18483
  retire: RETIRE,
17865
18484
  screen: SCREEN,
18485
+ secure: SECURE,
17866
18486
  seize: SEIZE,
17867
18487
  evacuate: EVACUATE,
17868
18488
  withdraw: WITHDRAW,
@@ -18292,4 +18912,4 @@ function assertNever(value) {
18292
18912
  throw new Error(`Unhandled control measure kind: ${String(value)}`);
18293
18913
  }
18294
18914
  //#endregion
18295
- export { DEFAULT_FIX_MISSION_TASK_OPTIONS as $, DEFAULT_LABEL_SIZE_CLAMP_CSS_PIXELS as $t, DEFAULT_SCREEN_OPTIONS as A, area27DrawRule as An, DEFAULT_LIGHT_LINE_OPTIONS as At, PROBABLE_LINE_OF_DEPLOYMENT_DASH as B, project as Bn, DEFAULT_ASSEMBLY_AREA_OPTIONS as Bt, DEFAULT_LANDING_ZONE_OPTIONS as C, point12DrawRule as Cn, DEFAULT_CLASSIC_ARROW_OPTIONS as Ct, DEFAULT_WITHDRAW_OPTIONS as D, blockDrawRule as Dn, DEFAULT_BLOCK_MISSION_TASK_OPTIONS as Dt, DEFAULT_WITHDRAW_UNDER_PRESSURE_OPTIONS as E, disruptDrawRule as En, DEFAULT_BREACH_OPTIONS as Et, DEFAULT_MOVEMENT_TO_CONTACT_OPTIONS as F, snapToMidpointPerpendicular as Fn, DEFAULT_BATTLE_POSITION_OPTIONS as Ft, DEFAULT_HANDOVER_LINE_OPTIONS as G, roundToFixed as Gn, DEFAULT_DIRECTION_OF_SUPPORTING_ATTACK_OPTIONS as Gt, DEFAULT_LINE_OF_DEPARTURE_OPTIONS as H, unproject as Hn, DEFAULT_MOBILE_DEFENSE_OPTIONS as Ht, DEFAULT_FRONTAL_ATTACK_OPTIONS as I, createBaselineFrame as In, DEFAULT_ATTACK_HELICOPTER_OPTIONS as It, DEFAULT_HOLDING_LINE_OPTIONS as J, TEXT_AMPLIFIER_FIELDS as Jt, DEFAULT_FORWARD_EDGE_OF_BATTLE_AREA_OPTIONS as K, DEFAULT_DIRECTION_OF_MAIN_ATTACK_OPTIONS as Kt, DEFAULT_FORTIFIED_AREA_OPTIONS as L, calculateMetrics as Ln, DEFAULT_ATTACK_BY_FIRE_OPTIONS as Lt, DEFAULT_REARWARD_PASSAGE_OF_LINES_OPTIONS as M, createMidpointPerpendicularDrawRule as Mn, DEFAULT_BLOCK_ARROW_OPTIONS as Mt, DEFAULT_PENETRATE_OPTIONS as N, getMidpointPerpendicularSignedDistance as Nn, DEFAULT_BLOCK_OPTIONS as Nt, DEFAULT_EVACUATE_OPTIONS as O, centerRadiusDrawRule as On, DEFAULT_GENERIC_C2_LINE_OPTIONS as Ot, DEFAULT_OCCUPY_OPTIONS as P, pointOnMidpointPerpendicularAxis as Pn, DEFAULT_CONTAIN_OPTIONS as Pt, DEFAULT_FLOT_OPTIONS as Q, DEFAULT_LABEL_HEIGHT_CSS_PIXELS as Qt, DEFAULT_FORTIFIED_LINE_OPTIONS as R, computeInitialWidthPoint as Rn, DEFAULT_ANTITANK_WALL_OPTIONS as Rt, DEFAULT_MAIN_ATTACK_OPTIONS as S, staticPointDrawRule as Sn, DEFAULT_GENERIC_CIRCLE_OPTIONS as St, DEFAULT_TURN_OPTIONS as T, occupyDrawRule as Tn, DEFAULT_BYPASS_OPTIONS as Tt, DEFAULT_LIMIT_OF_ADVANCE_OPTIONS as U, EPSILON as Un, DEFAULT_ENGAGEMENT_AREA_OPTIONS as Ut, DEFAULT_LINE_OF_DEPARTURE_CONTACT_OPTIONS as V, sphericalBearing as Vn, DEFAULT_AREA_OF_OPERATIONS_OPTIONS as Vt, DEFAULT_BATTLE_HANDOVER_LINE_OPTIONS as W, getMetersPerPixel as Wn, DEFAULT_AREA_DEFENSE_OPTIONS as Wt, DEFAULT_BRIDGEHEAD_LINE_OPTIONS as X, normalizeTextAmplifiers as Xt, DEFAULT_INFILTRATION_LANE_OPTIONS as Y, canonicalTextAmplifierKey as Yt, DEFAULT_PHASE_LINE_OPTIONS as Z, resolveAmplifierPlacement as Zt, DEFAULT_PICKUP_ZONE_OPTIONS as _, line1DrawRule as _n, DEFAULT_GENERIC_TEXT_OPTIONS as _t, DEFINITIONS as a, DEFAULT_SYMBOL_COLOR as an, DEFAULT_DISRUPT_MISSION_TASK_OPTIONS as at, DEFAULT_OBSTACLE_BYPASS_DIFFICULT_OPTIONS as b, dynamicPointDrawRule as bn, DEFAULT_GENERIC_POLYGON_OPTIONS as bt, getDefaultOptions as c, rectangleDrawRule as cn, DEFAULT_COVER_OPTIONS as ct, DEFAULT_SUPPORT_BY_FIRE_OPTIONS as d, polyline1DrawRule as dn, DEFAULT_COUNTERATTACK_OPTIONS as dt, DEFAULT_LINE_CAP as en, DEFAULT_FIX_OPTIONS as et, DEFAULT_RETAIN_OPTIONS as f, line27DrawRule as fn, DEFAULT_SUPPORTING_ATTACK_OPTIONS as ft, DEFAULT_PRINCIPAL_DIRECTION_OF_FIRE_OPTIONS as g, turnDrawRule as gn, RADAR_SEARCH_DOCTRINE_STROKE_COLOR as gt, SECONDARY_DIRECTION_OF_FIRE_DASH as h, line23DrawRule as hn, RADAR_SEARCH_DOCTRINE_FILL_COLOR as ht, CONTROL_MEASURE_METADATA as i, DEFAULT_STROKE_WIDTH_CSS_PIXELS as in, DEFAULT_GUARD_OPTIONS as it, DEFAULT_RETIRE_OPTIONS as j, computeDefaultMidpointPerpendicularPoint as jn, DEFAULT_BOUNDARY_OPTIONS as jt, DEFAULT_SEIZE_OPTIONS as k, containDrawRule as kn, DEFAULT_ENGINEER_WORK_LINE_OPTIONS as kt, listControlMeasureMetadata as l, axis1DrawRule as ln, DEFAULT_TACTICAL_ARROW_OPTIONS as lt, DEFAULT_SECONDARY_DIRECTION_OF_FIRE_OPTIONS as m, line24DrawRule as mn, DEFAULT_RADAR_SEARCH_DOCTRINE_OPTIONS as mt, resolveStyleHints as n, DEFAULT_PORTRAYAL as nn, DEFAULT_ENCIRCLEMENT_OPTIONS as nt, getControlMeasureMetadata as o, DEFAULT_AMBUSH_OPTIONS as on, DEFAULT_DISENGAGE_OPTIONS as ot, DEFAULT_STRONG_POINT_OPTIONS as p, line26DrawRule as pn, DEFAULT_CLEAR_OPTIONS as pt, DEFAULT_RELEASE_LINE_OPTIONS as q, DEFAULT_DIRECTION_OF_ATTACK_AVIATION_OPTIONS as qt, CONTROL_MEASURE_IDS as r, DEFAULT_STROKE_DASH_CSS_PIXELS as rn, DEFAULT_DISRUPT_OPTIONS as rt, getControlMeasureMetadataByValue as s, DEFAULT_AIRBORNE_ATTACK_OPTIONS as sn, DEFAULT_DELAY_OPTIONS as st, renderControlMeasure as t, DEFAULT_LINE_JOIN as tn, DEFAULT_FINAL_PROTECTIVE_FIRE_OPTIONS as tt, DEFAULT_TURNING_MOVEMENT_OPTIONS as u, supportByFireDrawRule as un, DEFAULT_COUNTERATTACK_BY_FIRE_OPTIONS as ut, DEFAULT_OBSTACLE_BYPASS_IMPOSSIBLE_OPTIONS as v, ambushDrawRule as vn, DEFAULT_GENERIC_SECTOR_OPTIONS as vt, DEFAULT_JOINT_TACTICAL_ACTION_AREA_OPTIONS as w, penetrateDrawRule as wn, DEFAULT_CANALIZE_OPTIONS as wt, DEFAULT_MINEFIELD_OPTIONS as x, sectorDrawRule as xn, DEFAULT_GENERIC_LINE_OPTIONS as xt, DEFAULT_OBSTACLE_BYPASS_EASY_OPTIONS as y, attackByFireDrawRule as yn, DEFAULT_GENERIC_RECTANGLE_OPTIONS as yt, DEFAULT_PROBABLE_LINE_OF_DEPLOYMENT_OPTIONS as z, haversineDistance as zn, DEFAULT_ANTITANK_DITCH_OPTIONS as zt };
18915
+ export { DEFAULT_PHASE_LINE_OPTIONS as $, DEFAULT_DIRECTION_OF_ATTACK_AVIATION_OPTIONS as $t, DEFAULT_SECURE_OPTIONS as A, secureDrawRule as An, DEFAULT_CANALIZE_OPTIONS as At, DEFAULT_FORTIFIED_LINE_OPTIONS as B, getMidpointPerpendicularSignedDistance as Bn, DEFAULT_CONTAIN_OPTIONS as Bt, DEFAULT_LANDING_ZONE_OPTIONS as C, line1DrawRule as Cn, DEFAULT_GENERIC_TEXT_OPTIONS as Ct, DEFAULT_WITHDRAW_OPTIONS as D, sectorDrawRule as Dn, DEFAULT_GENERIC_LINE_OPTIONS as Dt, DEFAULT_WITHDRAW_UNDER_PRESSURE_OPTIONS as E, dynamicPointDrawRule as En, DEFAULT_GENERIC_POLYGON_OPTIONS as Et, DEFAULT_PENETRATE_OPTIONS as F, centerRadiusDrawRule as Fn, DEFAULT_ENGINEER_WORK_LINE_OPTIONS as Ft, DEFAULT_LIMIT_OF_ADVANCE_OPTIONS as G, computeInitialWidthPoint as Gn, DEFAULT_ANTITANK_DITCH_OPTIONS as Gt, PROBABLE_LINE_OF_DEPLOYMENT_DASH as H, snapToMidpointPerpendicular as Hn, DEFAULT_ATTACK_HELICOPTER_OPTIONS as Ht, DEFAULT_OCCUPY_OPTIONS as I, containDrawRule as In, DEFAULT_LIGHT_LINE_OPTIONS as It, DEFAULT_FORWARD_EDGE_OF_BATTLE_AREA_OPTIONS as J, sphericalBearing as Jn, DEFAULT_MOBILE_DEFENSE_OPTIONS as Jt, DEFAULT_BATTLE_HANDOVER_LINE_OPTIONS as K, haversineDistance as Kn, DEFAULT_ASSEMBLY_AREA_OPTIONS as Kt, DEFAULT_MOVEMENT_TO_CONTACT_OPTIONS as L, area27DrawRule as Ln, DEFAULT_BOUNDARY_OPTIONS as Lt, DEFAULT_RETIRE_OPTIONS as M, occupyDrawRule as Mn, DEFAULT_BREACH_OPTIONS as Mt, DEFAULT_RELIEF_IN_PLACE_OPTIONS as N, disruptDrawRule as Nn, DEFAULT_BLOCK_MISSION_TASK_OPTIONS as Nt, DEFAULT_EVACUATE_OPTIONS as O, staticPointDrawRule as On, DEFAULT_GENERIC_CIRCLE_OPTIONS as Ot, DEFAULT_REARWARD_PASSAGE_OF_LINES_OPTIONS as P, blockDrawRule as Pn, DEFAULT_GENERIC_C2_LINE_OPTIONS as Pt, DEFAULT_BRIDGEHEAD_LINE_OPTIONS as Q, roundToFixed as Qn, DEFAULT_DIRECTION_OF_MAIN_ATTACK_OPTIONS as Qt, DEFAULT_FRONTAL_ATTACK_OPTIONS as R, computeDefaultMidpointPerpendicularPoint as Rn, DEFAULT_BLOCK_ARROW_OPTIONS as Rt, DEFAULT_MAIN_ATTACK_OPTIONS as S, turnDrawRule as Sn, RADAR_SEARCH_DOCTRINE_STROKE_COLOR as St, DEFAULT_TURN_OPTIONS as T, attackByFireDrawRule as Tn, DEFAULT_GENERIC_RECTANGLE_OPTIONS as Tt, DEFAULT_LINE_OF_DEPARTURE_CONTACT_OPTIONS as U, createBaselineFrame as Un, DEFAULT_ATTACK_BY_FIRE_OPTIONS as Ut, DEFAULT_PROBABLE_LINE_OF_DEPLOYMENT_OPTIONS as V, pointOnMidpointPerpendicularAxis as Vn, DEFAULT_BATTLE_POSITION_OPTIONS as Vt, DEFAULT_LINE_OF_DEPARTURE_OPTIONS as W, calculateMetrics as Wn, DEFAULT_ANTITANK_WALL_OPTIONS as Wt, DEFAULT_HOLDING_LINE_OPTIONS as X, EPSILON as Xn, DEFAULT_AREA_DEFENSE_OPTIONS as Xt, DEFAULT_RELEASE_LINE_OPTIONS as Y, unproject as Yn, DEFAULT_ENGAGEMENT_AREA_OPTIONS as Yt, DEFAULT_INFILTRATION_LANE_OPTIONS as Z, getMetersPerPixel as Zn, DEFAULT_DIRECTION_OF_SUPPORTING_ATTACK_OPTIONS as Zt, DEFAULT_PICKUP_ZONE_OPTIONS as _, polyline1DrawRule as _n, DEFAULT_CORDON_AND_KNOCK_OPTIONS as _t, DEFINITIONS as a, DEFAULT_LABEL_SIZE_CLAMP_CSS_PIXELS as an, DEFAULT_DISRUPT_OPTIONS as at, DEFAULT_OBSTACLE_BYPASS_DIFFICULT_OPTIONS as b, line24DrawRule as bn, DEFAULT_RADAR_SEARCH_DOCTRINE_OPTIONS as bt, getDefaultOptions as c, DEFAULT_PORTRAYAL as cn, DEFAULT_DISENGAGE_OPTIONS as ct, DEFAULT_SUPPORT_BY_FIRE_OPTIONS as d, DEFAULT_SYMBOL_COLOR as dn, DEFAULT_COVER_OPTIONS as dt, TEXT_AMPLIFIER_FIELDS as en, DEFAULT_FLOT_OPTIONS as et, DEFAULT_RETAIN_OPTIONS as f, DEFAULT_AMBUSH_OPTIONS as fn, DEFAULT_TACTICAL_ARROW_OPTIONS as ft, DEFAULT_PRINCIPAL_DIRECTION_OF_FIRE_OPTIONS as g, supportByFireDrawRule as gn, DEFAULT_CORDON_AND_SEARCH_OPTIONS as gt, SECONDARY_DIRECTION_OF_FIRE_DASH as h, axis1DrawRule as hn, DEFAULT_SUPPORTING_ATTACK_OPTIONS as ht, CONTROL_MEASURE_METADATA as i, DEFAULT_LABEL_HEIGHT_CSS_PIXELS as in, DEFAULT_ENCIRCLEMENT_OPTIONS as it, DEFAULT_SCREEN_OPTIONS as j, penetrateDrawRule as jn, DEFAULT_BYPASS_OPTIONS as jt, DEFAULT_SEIZE_OPTIONS as k, point12DrawRule as kn, DEFAULT_CLASSIC_ARROW_OPTIONS as kt, listControlMeasureMetadata as l, DEFAULT_STROKE_DASH_CSS_PIXELS as ln, DEFAULT_DEMONSTRATE_OPTIONS as lt, DEFAULT_SECONDARY_DIRECTION_OF_FIRE_OPTIONS as m, rectangleDrawRule as mn, DEFAULT_COUNTERATTACK_OPTIONS as mt, resolveStyleHints as n, normalizeTextAmplifiers as nn, DEFAULT_FIX_OPTIONS as nt, getControlMeasureMetadata as o, DEFAULT_LINE_CAP as on, DEFAULT_GUARD_OPTIONS as ot, DEFAULT_STRONG_POINT_OPTIONS as p, DEFAULT_AIRBORNE_ATTACK_OPTIONS as pn, DEFAULT_COUNTERATTACK_BY_FIRE_OPTIONS as pt, DEFAULT_HANDOVER_LINE_OPTIONS as q, project as qn, DEFAULT_AREA_OF_OPERATIONS_OPTIONS as qt, CONTROL_MEASURE_IDS as r, resolveAmplifierPlacement as rn, DEFAULT_FINAL_PROTECTIVE_FIRE_OPTIONS as rt, getControlMeasureMetadataByValue as s, DEFAULT_LINE_JOIN as sn, DEFAULT_DISRUPT_MISSION_TASK_OPTIONS as st, renderControlMeasure as t, canonicalTextAmplifierKey as tn, DEFAULT_FIX_MISSION_TASK_OPTIONS as tt, DEFAULT_TURNING_MOVEMENT_OPTIONS as u, DEFAULT_STROKE_WIDTH_CSS_PIXELS as un, DEFAULT_DELAY_OPTIONS as ut, DEFAULT_OBSTACLE_BYPASS_IMPOSSIBLE_OPTIONS as v, line27DrawRule as vn, DEFAULT_ISOLATE_OPTIONS as vt, DEFAULT_JOINT_TACTICAL_ACTION_AREA_OPTIONS as w, ambushDrawRule as wn, DEFAULT_GENERIC_SECTOR_OPTIONS as wt, DEFAULT_MINEFIELD_OPTIONS as x, line23DrawRule as xn, RADAR_SEARCH_DOCTRINE_FILL_COLOR as xt, DEFAULT_OBSTACLE_BYPASS_EASY_OPTIONS as y, line26DrawRule as yn, DEFAULT_CLEAR_OPTIONS as yt, DEFAULT_FORTIFIED_AREA_OPTIONS as z, createMidpointPerpendicularDrawRule as zn, DEFAULT_BLOCK_OPTIONS as zt };