@orbat-mapper/control-measures 0.17.0 → 0.19.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);
@@ -1646,6 +1762,16 @@ const rectangleDrawRule = {
1646
1762
  }
1647
1763
  };
1648
1764
  //#endregion
1765
+ //#region src/draw-rules/line25.ts
1766
+ /** Line25: PT. 1 is the arrow tip; PT. 2 is the rear of the symbol. */
1767
+ const line25DrawRule = {
1768
+ id: "line25",
1769
+ minimumUserPoints: 2,
1770
+ canonicalPointCount: 2,
1771
+ derive: (points) => clonePositions(points.slice(0, 2)),
1772
+ transform: (event) => clonePositions(event.next.slice(0, 2))
1773
+ };
1774
+ //#endregion
1649
1775
  //#region src/internal/echelon.ts
1650
1776
  /**
1651
1777
  * APP-6 echelon (Field B) glyph vocabulary, shared by the control measures that
@@ -3015,6 +3141,70 @@ function labelBoxIntersectsSegment(anchor, a, b, textWidth, textHeight, rotation
3015
3141
  }
3016
3142
  return true;
3017
3143
  }
3144
+ /**
3145
+ * Splits the polyline `points` into the pieces that fall outside a
3146
+ * `2*halfWidth × 2*halfHeight` label box centered at `anchor` with stored
3147
+ * `rotation` — the clipping counterpart to {@link labelBoxIntersectsSegment},
3148
+ * for ornament geometry (barbs, ticks) that must break around a glyph rather
3149
+ * than merely decide whether it collides.
3150
+ *
3151
+ * Each segment is clipped in the box's own frame with a two-slab parametric
3152
+ * test; pieces that rejoin across a shared vertex are merged so a polyline
3153
+ * crossing the box mid-span stays one line string per surviving run instead of
3154
+ * fragmenting at every interior vertex.
3155
+ */
3156
+ function clipPolylineOutsideLabelBox(points, anchor, halfWidth, halfHeight, rotation) {
3157
+ const [widthAxis, heightAxis] = labelBoxAxes(rotation);
3158
+ const out = [];
3159
+ const append = (piece) => {
3160
+ if (piece.length < 2 || vecMag(vecSub(piece[1], piece[0])) < 1e-6) return;
3161
+ const previous = out.at(-1);
3162
+ if (previous && vecMag(vecSub(previous.at(-1), piece[0])) < 1e-6) previous.push(...piece.slice(1));
3163
+ else out.push(piece);
3164
+ };
3165
+ for (let i = 1; i < points.length; i++) {
3166
+ const a = points[i - 1];
3167
+ const b = points[i];
3168
+ const delta = vecSub(b, a);
3169
+ const relative = vecSub(a, anchor);
3170
+ const origin = [vecDot(relative, widthAxis), vecDot(relative, heightAxis)];
3171
+ const direction = [vecDot(delta, widthAxis), vecDot(delta, heightAxis)];
3172
+ let enter = 0;
3173
+ let exit = 1;
3174
+ let intersects = true;
3175
+ for (const [coordinate, rate, halfExtent] of [[
3176
+ origin[0],
3177
+ direction[0],
3178
+ halfWidth
3179
+ ], [
3180
+ origin[1],
3181
+ direction[1],
3182
+ halfHeight
3183
+ ]]) {
3184
+ if (Math.abs(rate) < 1e-6) {
3185
+ if (Math.abs(coordinate) > halfExtent) intersects = false;
3186
+ continue;
3187
+ }
3188
+ const first = (-halfExtent - coordinate) / rate;
3189
+ const second = (halfExtent - coordinate) / rate;
3190
+ enter = Math.max(enter, Math.min(first, second));
3191
+ exit = Math.min(exit, Math.max(first, second));
3192
+ if (enter > exit) intersects = false;
3193
+ }
3194
+ if (!intersects || exit < 0 || enter > 1) {
3195
+ append([a, b]);
3196
+ continue;
3197
+ }
3198
+ const clippedEnter = Math.max(0, enter);
3199
+ const clippedExit = Math.min(1, exit);
3200
+ const pointAt = (t) => t <= 0 ? a : t >= 1 ? b : vecAdd(a, vecScale(delta, t));
3201
+ if (clippedEnter > 0) append([a, pointAt(clippedEnter)]);
3202
+ if (clippedExit < 1) append([pointAt(clippedExit), b]);
3203
+ }
3204
+ return out;
3205
+ }
3206
+ /** Default extra clearance for an intrinsic label that interrupts a stroke. */
3207
+ const DEFAULT_LABEL_KNOCKOUT_PADDING = .3;
3018
3208
  /** Field N descriptor shared by Line1 graphics that opt into hostile endpoint markers. */
3019
3209
  const HOSTILE_LINE_AMPLIFIER = {
3020
3210
  key: "N",
@@ -3023,12 +3213,12 @@ const HOSTILE_LINE_AMPLIFIER = {
3023
3213
  placeholder: "ENY",
3024
3214
  maxLength: 3
3025
3215
  };
3026
- /** Label-padding control shared by newly label-bearing Line1 graphics. */
3027
- const LINE_LABEL_PADDING_PARAM = {
3216
+ /** Label-padding control shared by label-bearing graphics. */
3217
+ const LABEL_PADDING_PARAM = {
3028
3218
  key: "labelPadding",
3029
3219
  presentationTier: "advanced",
3030
3220
  label: "Label padding",
3031
- description: "Extra clearance between the line and its labels, as a ratio of the label height",
3221
+ description: "Extra clearance around labels, as a ratio of the label height",
3032
3222
  type: "number",
3033
3223
  min: 0,
3034
3224
  max: 2,
@@ -3136,8 +3326,8 @@ function estimatedTextWidth(text, textHeightMeters) {
3136
3326
  return text.length * LABEL_CHAR_ASPECT_RATIO * textHeightMeters;
3137
3327
  }
3138
3328
  /** 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);
3329
+ function resolveTextMetrics(text, options, context, style = "regular", fallbackHeightMeters = 50) {
3330
+ const legacyHeight = resolveLabelOffsetMeters(options, 1, fallbackHeightMeters);
3141
3331
  const constructionScale = context?.constructionMetersPerCssPixel;
3142
3332
  if (!(constructionScale !== void 0 && constructionScale > 0)) return {
3143
3333
  width: estimatedTextWidth(text, legacyHeight),
@@ -3168,6 +3358,28 @@ function resolveTextMetrics(text, options, context, style = "regular") {
3168
3358
  height: measured.heightCssPixels * constructionScale
3169
3359
  };
3170
3360
  }
3361
+ /**
3362
+ * Half-length of the knockout required for a label along `direction`.
3363
+ *
3364
+ * The text box itself is always cleared; `labelPadding` adds symmetric clearance expressed in
3365
+ * resolved label heights. Keeping this calculation here gives every generator the same public
3366
+ * label-clearance interface while hiding font measurement and rotated-box projection details.
3367
+ */
3368
+ function labelKnockoutHalfExtent(text, direction, rotation, options, context, defaultPadding = 0) {
3369
+ const metrics = resolveTextMetrics(text, options, context);
3370
+ const padding = Math.max(0, options.labelPadding ?? defaultPadding) * metrics.height;
3371
+ return labelHalfExtent(direction, metrics.width, metrics.height, rotation) + padding;
3372
+ }
3373
+ /**
3374
+ * {@link labelKnockoutHalfExtent} expressed as a fraction of an arc's angular
3375
+ * sweep, for generators that carve their knockout in arc-progress space rather
3376
+ * than in meters. Clamped to `0.5` so the gap can never exceed the arc.
3377
+ */
3378
+ function arcKnockoutHalfFraction(halfExtentMeters, radius, sweepAngle) {
3379
+ const arcLength = radius * Math.abs(sweepAngle);
3380
+ if (!(arcLength > 1e-6)) return .5;
3381
+ return Math.min(.5, Math.max(0, halfExtentMeters) / arcLength);
3382
+ }
3171
3383
  /** The end-label text for a phase-line naming, or `""` when unnamed. */
3172
3384
  function phaseLineLabelText(name, includePrefix) {
3173
3385
  if (!name) return "";
@@ -3632,6 +3844,7 @@ const DIRECTION_OF_ATTACK_AVIATION_METADATA = {
3632
3844
  },
3633
3845
  drawRule: "Line1",
3634
3846
  capturesLabelSize: true,
3847
+ labelGeometryUsesResolvedMetrics: ["T"],
3635
3848
  params: [
3636
3849
  ...SMOOTH_LINE_PARAMS,
3637
3850
  {
@@ -3870,6 +4083,7 @@ const DIRECTION_OF_MAIN_ATTACK_METADATA = {
3870
4083
  },
3871
4084
  drawRule: "Line1",
3872
4085
  capturesLabelSize: true,
4086
+ labelGeometryUsesResolvedMetrics: ["T"],
3873
4087
  params: [
3874
4088
  ...SMOOTH_LINE_PARAMS,
3875
4089
  ...DIRECTION_OF_ATTACK_PARAMS,
@@ -3938,6 +4152,7 @@ const DIRECTION_OF_SUPPORTING_ATTACK_METADATA = {
3938
4152
  },
3939
4153
  drawRule: "Line1",
3940
4154
  capturesLabelSize: true,
4155
+ labelGeometryUsesResolvedMetrics: ["T"],
3941
4156
  params: [...SMOOTH_LINE_PARAMS, ...DIRECTION_OF_ATTACK_PARAMS],
3942
4157
  textAmplifiers: DIRECTION_OF_ATTACK_TEXT_AMPLIFIERS
3943
4158
  };
@@ -4568,6 +4783,7 @@ const AREA_METADATA = {
4568
4783
  },
4569
4784
  drawRule: "Area1",
4570
4785
  capturesLabelSize: true,
4786
+ labelGeometryUsesResolvedMetrics: ["N"],
4571
4787
  params: LABELED_AREA_PARAMS,
4572
4788
  textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
4573
4789
  };
@@ -4831,6 +5047,7 @@ const ENGAGEMENT_AREA_METADATA = {
4831
5047
  },
4832
5048
  drawRule: "Area1",
4833
5049
  capturesLabelSize: true,
5050
+ labelGeometryUsesResolvedMetrics: ["N"],
4834
5051
  params: PREFIXED_LABELED_AREA_PARAMS,
4835
5052
  textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
4836
5053
  };
@@ -4897,6 +5114,26 @@ function sampleProjectedArc(center, startRadius, bulgeRadius, start, end, resolu
4897
5114
  return unproject(point[0], point[1]);
4898
5115
  });
4899
5116
  }
5117
+ /**
5118
+ * Samples the 180-degree arc standing on the diameter `start`→`end`, bulging
5119
+ * toward `bulgeDir`. Shared by the mission tasks whose shaft turns back on
5120
+ * itself (Delay, Relief in Place, Demonstrate); `requestedSegments` is the
5121
+ * segment count for the half-turn.
5122
+ */
5123
+ function createSemicircleArc(start, end, bulgeDir, requestedSegments) {
5124
+ const diameterVector = vecSub(end, start);
5125
+ const diameter = vecMag(diameterVector);
5126
+ if (diameter < 1e-6) return [unproject(start[0], start[1])];
5127
+ const radius = diameter / 2;
5128
+ const center = vecAdd(start, vecScale(diameterVector, .5));
5129
+ const startRadius = vecScale(diameterVector, -.5);
5130
+ const bulgeRadius = vecScale(bulgeDir, radius);
5131
+ const segments = Math.max(4, Math.round(requestedSegments));
5132
+ return Array.from({ length: segments + 1 }, (_, index) => {
5133
+ const point = arcPointAt(center, startRadius, bulgeRadius, index / segments * Math.PI);
5134
+ return unproject(point[0], point[1]);
5135
+ });
5136
+ }
4900
5137
  //#endregion
4901
5138
  //#region src/generators/cm15-maneuver-areas/mobile-defense.ts
4902
5139
  const DEFAULT_MOBILE_DEFENSE_OPTIONS = { labelPadding: 0 };
@@ -4922,6 +5159,7 @@ const MOBILE_DEFENSE_METADATA = {
4922
5159
  },
4923
5160
  drawRule: "Area27",
4924
5161
  capturesLabelSize: true,
5162
+ labelGeometryUsesResolvedMetrics: true,
4925
5163
  params: [{
4926
5164
  key: "labelPadding",
4927
5165
  presentationTier: "advanced",
@@ -5098,6 +5336,7 @@ const AREA_OF_OPERATIONS_METADATA = {
5098
5336
  },
5099
5337
  drawRule: "Area1",
5100
5338
  capturesLabelSize: true,
5339
+ labelGeometryUsesResolvedMetrics: ["N"],
5101
5340
  params: PREFIXED_LABELED_AREA_PARAMS,
5102
5341
  textAmplifiers: AREA_TEXT_AMPLIFIERS
5103
5342
  };
@@ -5160,6 +5399,7 @@ const NAMED_AREA_OF_INTEREST_METADATA = {
5160
5399
  },
5161
5400
  drawRule: "Area1",
5162
5401
  capturesLabelSize: true,
5402
+ labelGeometryUsesResolvedMetrics: ["N"],
5163
5403
  params: PREFIXED_LABELED_AREA_PARAMS,
5164
5404
  textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
5165
5405
  };
@@ -5219,6 +5459,7 @@ const TARGET_AREA_OF_INTEREST_METADATA = {
5219
5459
  },
5220
5460
  drawRule: "Area1",
5221
5461
  capturesLabelSize: true,
5462
+ labelGeometryUsesResolvedMetrics: ["N"],
5222
5463
  params: PREFIXED_LABELED_AREA_PARAMS,
5223
5464
  textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
5224
5465
  };
@@ -5287,6 +5528,7 @@ const AIRFIELD_ZONE_METADATA = {
5287
5528
  },
5288
5529
  drawRule: "Area1",
5289
5530
  capturesLabelSize: true,
5531
+ labelGeometryUsesResolvedMetrics: ["N"],
5290
5532
  params: [
5291
5533
  ...AREA_SMOOTH_PARAMS,
5292
5534
  {
@@ -5471,6 +5713,7 @@ const BASE_CAMP_METADATA = {
5471
5713
  },
5472
5714
  drawRule: "Area1",
5473
5715
  capturesLabelSize: true,
5716
+ labelGeometryUsesResolvedMetrics: ["N"],
5474
5717
  params: PREFIXED_ECHELON_AREA_PARAMS,
5475
5718
  textAmplifiers: AREA_TEXT_AMPLIFIERS
5476
5719
  };
@@ -5529,6 +5772,7 @@ const GUERRILLA_BASE_METADATA = {
5529
5772
  },
5530
5773
  drawRule: "Area1",
5531
5774
  capturesLabelSize: true,
5775
+ labelGeometryUsesResolvedMetrics: ["N"],
5532
5776
  params: PREFIXED_ECHELON_AREA_PARAMS,
5533
5777
  textAmplifiers: AREA_TEXT_AMPLIFIERS
5534
5778
  };
@@ -5592,6 +5836,7 @@ const GENERIC_C2_AREA_METADATA = {
5592
5836
  },
5593
5837
  drawRule: "Area1",
5594
5838
  capturesLabelSize: true,
5839
+ labelGeometryUsesResolvedMetrics: ["N"],
5595
5840
  params: LABELED_AREA_PARAMS,
5596
5841
  textAmplifiers: AREA_TEXT_AMPLIFIERS
5597
5842
  };
@@ -5647,6 +5892,7 @@ const ASSEMBLY_AREA_METADATA = {
5647
5892
  },
5648
5893
  drawRule: "Area1",
5649
5894
  capturesLabelSize: true,
5895
+ labelGeometryUsesResolvedMetrics: ["N"],
5650
5896
  params: PREFIXED_LABELED_AREA_PARAMS,
5651
5897
  textAmplifiers: AREA_TEXT_AMPLIFIERS
5652
5898
  };
@@ -5698,7 +5944,7 @@ const FORTIFIED_PARAMS = [
5698
5944
  step: 1,
5699
5945
  unit: "m"
5700
5946
  },
5701
- LINE_LABEL_PADDING_PARAM
5947
+ LABEL_PADDING_PARAM
5702
5948
  ];
5703
5949
  const ANTITANK_DITCH_PARAMS = [
5704
5950
  ...SMOOTH_LINE_PARAMS,
@@ -5733,7 +5979,7 @@ const ANTITANK_DITCH_PARAMS = [
5733
5979
  step: .05,
5734
5980
  unit: "x"
5735
5981
  },
5736
- LINE_LABEL_PADDING_PARAM
5982
+ LABEL_PADDING_PARAM
5737
5983
  ];
5738
5984
  const ANTITANK_WALL_PARAMS = [...ANTITANK_DITCH_PARAMS, {
5739
5985
  key: "toothSpacingRatio",
@@ -5776,6 +6022,7 @@ const ANTITANK_DITCH_UNDER_CONSTRUCTION_METADATA = {
5776
6022
  },
5777
6023
  drawRule: "Line1",
5778
6024
  capturesLabelSize: true,
6025
+ labelGeometryUsesResolvedMetrics: ["N"],
5779
6026
  params: ANTITANK_DITCH_PARAMS,
5780
6027
  textAmplifiers: [HOSTILE_LINE_AMPLIFIER]
5781
6028
  };
@@ -5797,6 +6044,7 @@ const ANTITANK_DITCH_COMPLETED_METADATA = {
5797
6044
  },
5798
6045
  drawRule: "Line1",
5799
6046
  capturesLabelSize: true,
6047
+ labelGeometryUsesResolvedMetrics: ["N"],
5800
6048
  params: ANTITANK_DITCH_PARAMS,
5801
6049
  textAmplifiers: [HOSTILE_LINE_AMPLIFIER]
5802
6050
  };
@@ -5929,6 +6177,7 @@ const ANTITANK_WALL_METADATA = {
5929
6177
  },
5930
6178
  drawRule: "Line1",
5931
6179
  capturesLabelSize: true,
6180
+ labelGeometryUsesResolvedMetrics: ["N"],
5932
6181
  params: ANTITANK_WALL_PARAMS,
5933
6182
  textAmplifiers: [HOSTILE_LINE_AMPLIFIER]
5934
6183
  };
@@ -6395,6 +6644,7 @@ const BATTLE_POSITION_METADATA = {
6395
6644
  },
6396
6645
  drawRule: "Area1",
6397
6646
  capturesLabelSize: true,
6647
+ labelGeometryUsesResolvedMetrics: ["N"],
6398
6648
  params: ECHELON_AREA_PARAMS,
6399
6649
  textAmplifiers: AREA_UNIT_DESIGNATION_HOSTILE_AMPLIFIERS
6400
6650
  };
@@ -6538,6 +6788,7 @@ const BATTLE_POSITION_PREPARED_METADATA = {
6538
6788
  },
6539
6789
  drawRule: "Area1",
6540
6790
  capturesLabelSize: true,
6791
+ labelGeometryUsesResolvedMetrics: ["N"],
6541
6792
  params: PREFIXED_ECHELON_AREA_PARAMS,
6542
6793
  textAmplifiers: AREA_UNIT_DESIGNATION_HOSTILE_AMPLIFIERS
6543
6794
  };
@@ -6606,6 +6857,7 @@ const CONTAIN_METADATA = {
6606
6857
  },
6607
6858
  drawRule: "Area5",
6608
6859
  capturesLabelSize: true,
6860
+ labelGeometryUsesResolvedMetrics: true,
6609
6861
  textAmplifiers: [AREA_HOSTILE_AMPLIFIER],
6610
6862
  params: [{
6611
6863
  key: "labelPadding",
@@ -7501,6 +7753,7 @@ const BOUNDARY_METADATA = {
7501
7753
  text: true
7502
7754
  },
7503
7755
  drawRule: "Line1",
7756
+ labelGeometryUsesResolvedMetrics: ["N"],
7504
7757
  params: [
7505
7758
  {
7506
7759
  key: "smooth",
@@ -7919,6 +8172,7 @@ const GENERIC_C2_LINE_METADATA = {
7919
8172
  },
7920
8173
  drawRule: "Line1",
7921
8174
  capturesLabelSize: true,
8175
+ labelGeometryUsesResolvedMetrics: ["N"],
7922
8176
  params: [
7923
8177
  ...SMOOTH_LINE_PARAMS,
7924
8178
  ...echelonLineParams("line"),
@@ -7940,7 +8194,7 @@ const GENERIC_C2_LINE_METADATA = {
7940
8194
  type: "boolean",
7941
8195
  visibleWhen: (opts) => Boolean(String(opts.phaseLineName ?? "").trim())
7942
8196
  },
7943
- LINE_LABEL_PADDING_PARAM
8197
+ LABEL_PADDING_PARAM
7944
8198
  ],
7945
8199
  textAmplifiers: [
7946
8200
  {
@@ -8167,7 +8421,7 @@ function createLabelFeature(point, text, rotation, labelSize = {}) {
8167
8421
  * @param config - Per-measure end caps, label glyph, and sizing
8168
8422
  * @returns GeoJSON FeatureCollection containing MultiLineString and Point features
8169
8423
  */
8170
- function createBracketSymbol(coordinates, config) {
8424
+ function createBracketSymbol(coordinates, config, context) {
8171
8425
  const [c1, c2, c3] = coordinates;
8172
8426
  const p1 = project(c1[0], c1[1]);
8173
8427
  const p2 = project(c2[0], c2[1]);
@@ -8187,7 +8441,10 @@ function createBracketSymbol(coordinates, config) {
8187
8441
  const rearMidpoint = vecAdd(frontMidpoint, vecScale(perpDir, perpDistance));
8188
8442
  const rearTop = vecSub(rearMidpoint, halfFront);
8189
8443
  const rearBottom = vecAdd(rearMidpoint, halfFront);
8190
- const gapHalf = frontLength * config.labelGapRatio / 2;
8444
+ const rearLineRotation = Math.atan2(dirFront[1], dirFront[0]);
8445
+ const renderedLabelRotation = Math.PI - (rearLineRotation - Math.PI / 2);
8446
+ const labelRotation = normalizeRadians(Math.PI - keepTextLeftToRight(renderedLabelRotation));
8447
+ const gapHalf = Math.min(frontLength / 2, labelKnockoutHalfExtent(config.labelText, dirFront, labelRotation, config.labelOptions ?? {}, context));
8191
8448
  const gapPt1 = vecAdd(rearMidpoint, vecScale(dirFront, -gapHalf));
8192
8449
  const gapPt2 = vecAdd(rearMidpoint, vecScale(dirFront, gapHalf));
8193
8450
  const [topCap, bottomCap] = config.buildEndCaps({
@@ -8198,39 +8455,35 @@ function createBracketSymbol(coordinates, config) {
8198
8455
  frontLength,
8199
8456
  rearTop
8200
8457
  });
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
8458
  return {
8226
8459
  type: "FeatureCollection",
8227
- features: [mainFeature, createLabelFeature(rearMidpoint, config.labelText, labelRotation, config.labelSize)]
8460
+ features: [{
8461
+ type: "Feature",
8462
+ properties: { part: config.part },
8463
+ geometry: {
8464
+ type: "MultiLineString",
8465
+ coordinates: [
8466
+ topCap,
8467
+ [
8468
+ unproject(p1[0], p1[1]),
8469
+ unproject(rearTop[0], rearTop[1]),
8470
+ unproject(gapPt1[0], gapPt1[1])
8471
+ ],
8472
+ [
8473
+ unproject(gapPt2[0], gapPt2[1]),
8474
+ unproject(rearBottom[0], rearBottom[1]),
8475
+ unproject(p2[0], p2[1])
8476
+ ],
8477
+ bottomCap
8478
+ ]
8479
+ }
8480
+ }, createLabelFeature(rearMidpoint, config.labelText, labelRotation, config.labelOptions)]
8228
8481
  };
8229
8482
  }
8230
8483
  //#endregion
8231
8484
  //#region src/generators/cm34-mission-tasks/block.ts
8232
8485
  const DEFAULT_STEM_RATIO = 1.5;
8233
- const DEFAULT_BLOCK_MISSION_TASK_OPTIONS = { labelGapRatio: .2 };
8486
+ const DEFAULT_BLOCK_MISSION_TASK_OPTIONS = { labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING };
8234
8487
  const BLOCK_MISSION_TASK_METADATA = {
8235
8488
  id: "block-mission-task",
8236
8489
  name: "Block",
@@ -8248,16 +8501,8 @@ const BLOCK_MISSION_TASK_METADATA = {
8248
8501
  },
8249
8502
  drawRule: "Area11",
8250
8503
  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
- }]
8504
+ labelGeometryUsesResolvedMetrics: true,
8505
+ params: [LABEL_PADDING_PARAM]
8261
8506
  };
8262
8507
  /**
8263
8508
  * Generates a GeoJSON FeatureCollection for a BLOCK mission task symbol (340100).
@@ -8269,8 +8514,7 @@ const BLOCK_MISSION_TASK_METADATA = {
8269
8514
  * @param options - Configuration options
8270
8515
  * @returns GeoJSON FeatureCollection containing MultiLineString and Point features
8271
8516
  */
8272
- function createBlockMissionTaskSymbol(coordinates, options = {}) {
8273
- const labelGapRatio = options.labelGapRatio ?? DEFAULT_BLOCK_MISSION_TASK_OPTIONS.labelGapRatio;
8517
+ function createBlockMissionTaskSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
8274
8518
  const p1 = project(coordinates[0][0], coordinates[0][1]);
8275
8519
  const p2 = project(coordinates[1][0], coordinates[1][1]);
8276
8520
  const frame = createProjectedBaselineFrame(p1, p2, {
@@ -8287,12 +8531,12 @@ function createBlockMissionTaskSymbol(coordinates, options = {}) {
8287
8531
  const pStemEnd = vecAdd(mid, vecScale(frame.normal, stemDistance));
8288
8532
  const labelPoint = vecAdd(mid, vecScale(frame.normal, stemDistance / 2));
8289
8533
  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
8534
  const shaftRotation = Math.atan2(frame.normal[1], frame.normal[0]);
8294
8535
  const renderedLabelRotation = Math.PI - shaftRotation;
8295
8536
  const labelRotation = normalizeRadians(Math.PI - keepTextLeftToRight(renderedLabelRotation));
8537
+ const gapHalf = Math.min(labelKnockoutHalfExtent("B", stemDirection, labelRotation, options, context, DEFAULT_BLOCK_MISSION_TASK_OPTIONS.labelPadding), Math.abs(stemDistance) / 2);
8538
+ const gapStart = vecAdd(labelPoint, vecScale(stemDirection, -gapHalf));
8539
+ const gapEnd = vecAdd(labelPoint, vecScale(stemDirection, gapHalf));
8296
8540
  return {
8297
8541
  type: "FeatureCollection",
8298
8542
  features: [{
@@ -8327,7 +8571,7 @@ const BLOCK_MISSION_TASK = defineControlMeasure({
8327
8571
  const DEFAULT_BREACH_OPTIONS = {
8328
8572
  tickLengthRatio: .15,
8329
8573
  tickAngle: 45,
8330
- labelGapRatio: .2
8574
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING
8331
8575
  };
8332
8576
  const BREACH_METADATA = {
8333
8577
  id: "breach",
@@ -8347,6 +8591,7 @@ const BREACH_METADATA = {
8347
8591
  },
8348
8592
  drawRule: "Point12",
8349
8593
  capturesLabelSize: true,
8594
+ labelGeometryUsesResolvedMetrics: true,
8350
8595
  params: [
8351
8596
  {
8352
8597
  key: "tickLengthRatio",
@@ -8368,16 +8613,7 @@ const BREACH_METADATA = {
8368
8613
  max: 90,
8369
8614
  step: 1
8370
8615
  },
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
- }
8616
+ LABEL_PADDING_PARAM
8381
8617
  ]
8382
8618
  };
8383
8619
  /**
@@ -8391,16 +8627,15 @@ const BREACH_METADATA = {
8391
8627
  * @param options - Configuration options
8392
8628
  * @returns GeoJSON FeatureCollection containing MultiLineString and Point features
8393
8629
  */
8394
- function createBreachSymbol(coordinates, options = {}) {
8630
+ function createBreachSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
8395
8631
  const tickLengthRatio = options.tickLengthRatio ?? DEFAULT_BREACH_OPTIONS.tickLengthRatio;
8396
8632
  const tickAngle = options.tickAngle ?? DEFAULT_BREACH_OPTIONS.tickAngle;
8397
8633
  return createBracketSymbol(coordinates, {
8398
8634
  part: "breach",
8399
8635
  labelText: "B",
8400
- labelGapRatio: options.labelGapRatio ?? DEFAULT_BREACH_OPTIONS.labelGapRatio,
8401
- labelSize: {
8402
- labelSize: options.labelSize,
8403
- labelSizePixels: options.labelSizePixels
8636
+ labelOptions: {
8637
+ ...options,
8638
+ labelPadding: options.labelPadding ?? DEFAULT_BREACH_OPTIONS.labelPadding
8404
8639
  },
8405
8640
  buildEndCaps: ({ p1, p2, dirFront, perpDir, frontLength }) => {
8406
8641
  const halfTick = frontLength * tickLengthRatio / 2;
@@ -8416,7 +8651,7 @@ function createBreachSymbol(coordinates, options = {}) {
8416
8651
  const tick2End = vecAdd(p2, vecScale(tick2Dir, halfTick));
8417
8652
  return [[unproject(tick1Start[0], tick1Start[1]), unproject(tick1End[0], tick1End[1])], [unproject(tick2Start[0], tick2Start[1]), unproject(tick2End[0], tick2End[1])]];
8418
8653
  }
8419
- });
8654
+ }, context);
8420
8655
  }
8421
8656
  const BREACH = defineControlMeasure({
8422
8657
  metadata: BREACH_METADATA,
@@ -8431,7 +8666,7 @@ const BREACH = defineControlMeasure({
8431
8666
  */
8432
8667
  const DEFAULT_BYPASS_OPTIONS = {
8433
8668
  arrowheadLengthRatio: .12,
8434
- labelGapRatio: .2
8669
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING
8435
8670
  };
8436
8671
  const BYPASS_METADATA = {
8437
8672
  id: "bypass",
@@ -8451,6 +8686,7 @@ const BYPASS_METADATA = {
8451
8686
  },
8452
8687
  drawRule: "Point12",
8453
8688
  capturesLabelSize: true,
8689
+ labelGeometryUsesResolvedMetrics: true,
8454
8690
  params: [{
8455
8691
  key: "arrowheadLengthRatio",
8456
8692
  presentationTier: "advanced",
@@ -8460,16 +8696,7 @@ const BYPASS_METADATA = {
8460
8696
  min: .01,
8461
8697
  max: 1,
8462
8698
  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
- }]
8699
+ }, LABEL_PADDING_PARAM]
8473
8700
  };
8474
8701
  /**
8475
8702
  * Generates a GeoJSON FeatureCollection for a BYPASS mission task symbol (340300).
@@ -8482,22 +8709,21 @@ const BYPASS_METADATA = {
8482
8709
  * @param options - Configuration options
8483
8710
  * @returns GeoJSON FeatureCollection containing MultiLineString and Point features
8484
8711
  */
8485
- function createBypassSymbol(coordinates, options = {}) {
8712
+ function createBypassSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
8486
8713
  const arrowheadLengthRatio = options.arrowheadLengthRatio ?? DEFAULT_BYPASS_OPTIONS.arrowheadLengthRatio;
8487
8714
  return createBracketSymbol(coordinates, {
8488
8715
  part: "bypass",
8489
8716
  labelText: "B",
8490
- labelGapRatio: options.labelGapRatio ?? DEFAULT_BYPASS_OPTIONS.labelGapRatio,
8491
- labelSize: {
8492
- labelSize: options.labelSize,
8493
- labelSizePixels: options.labelSizePixels
8717
+ labelOptions: {
8718
+ ...options,
8719
+ labelPadding: options.labelPadding ?? DEFAULT_BYPASS_OPTIONS.labelPadding
8494
8720
  },
8495
8721
  buildEndCaps: ({ p1, p2, frontLength, rearTop }) => {
8496
8722
  const arrowheadLength = frontLength * arrowheadLengthRatio;
8497
8723
  const arrowDir = vecNorm(vecSub(p1, rearTop));
8498
8724
  return [createArrowHead$1(p1, arrowDir, arrowheadLength, arrowheadLength), createArrowHead$1(p2, arrowDir, arrowheadLength, arrowheadLength)];
8499
8725
  }
8500
- });
8726
+ }, context);
8501
8727
  }
8502
8728
  const BYPASS = defineControlMeasure({
8503
8729
  metadata: BYPASS_METADATA,
@@ -8513,7 +8739,7 @@ const BYPASS = defineControlMeasure({
8513
8739
  const DEFAULT_CANALIZE_OPTIONS = {
8514
8740
  tickLengthRatio: .15,
8515
8741
  tickAngle: 45,
8516
- labelGapRatio: .2
8742
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING
8517
8743
  };
8518
8744
  const CANALIZE_METADATA = {
8519
8745
  id: "canalize",
@@ -8533,6 +8759,7 @@ const CANALIZE_METADATA = {
8533
8759
  },
8534
8760
  drawRule: "Point12",
8535
8761
  capturesLabelSize: true,
8762
+ labelGeometryUsesResolvedMetrics: true,
8536
8763
  params: [
8537
8764
  {
8538
8765
  key: "tickLengthRatio",
@@ -8554,16 +8781,7 @@ const CANALIZE_METADATA = {
8554
8781
  max: 90,
8555
8782
  step: 1
8556
8783
  },
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
- }
8784
+ LABEL_PADDING_PARAM
8567
8785
  ]
8568
8786
  };
8569
8787
  /**
@@ -8577,16 +8795,15 @@ const CANALIZE_METADATA = {
8577
8795
  * @param options - Configuration options
8578
8796
  * @returns GeoJSON FeatureCollection containing MultiLineString and Point features
8579
8797
  */
8580
- function createCanalizeSymbol(coordinates, options = {}) {
8798
+ function createCanalizeSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
8581
8799
  const tickLengthRatio = options.tickLengthRatio ?? DEFAULT_CANALIZE_OPTIONS.tickLengthRatio;
8582
8800
  const tickAngle = options.tickAngle ?? DEFAULT_CANALIZE_OPTIONS.tickAngle;
8583
8801
  return createBracketSymbol(coordinates, {
8584
8802
  part: "canalize",
8585
8803
  labelText: "C",
8586
- labelGapRatio: options.labelGapRatio ?? DEFAULT_CANALIZE_OPTIONS.labelGapRatio,
8587
- labelSize: {
8588
- labelSize: options.labelSize,
8589
- labelSizePixels: options.labelSizePixels
8804
+ labelOptions: {
8805
+ ...options,
8806
+ labelPadding: options.labelPadding ?? DEFAULT_CANALIZE_OPTIONS.labelPadding
8590
8807
  },
8591
8808
  buildEndCaps: ({ p1, p2, dirFront, perpDir, frontLength }) => {
8592
8809
  const halfTick = frontLength * tickLengthRatio / 2;
@@ -8601,7 +8818,7 @@ function createCanalizeSymbol(coordinates, options = {}) {
8601
8818
  const tick2End = vecAdd(p2, vecScale(tick2Dir, halfTick));
8602
8819
  return [[unproject(tick1Start[0], tick1Start[1]), unproject(tick1End[0], tick1End[1])], [unproject(tick2Start[0], tick2Start[1]), unproject(tick2End[0], tick2End[1])]];
8603
8820
  }
8604
- });
8821
+ }, context);
8605
8822
  }
8606
8823
  const CANALIZE = defineControlMeasure({
8607
8824
  metadata: CANALIZE_METADATA,
@@ -9670,12 +9887,12 @@ const DEFAULT_CLEAR_OPTIONS = {
9670
9887
  arrowInsetRatio: .15,
9671
9888
  arrowheadLengthRatio: .15,
9672
9889
  arrowheadWidthRatio: .2,
9673
- labelGapRatio: .2
9890
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING
9674
9891
  };
9675
9892
  const CLEAR_METADATA = {
9676
9893
  id: "clear",
9677
9894
  name: "Clear",
9678
- description: "Eliminates enemy forces within an assigned area by destroying, capturing, or forcing their withdrawal so they cannot offer organized resistance.",
9895
+ description: "Eliminates all enemy forces within an assigned area by destroying, capturing, or forcing their withdrawal so they cannot offer organized resistance.",
9679
9896
  entity: "Mission Tasks",
9680
9897
  entityType: "Clear",
9681
9898
  value: "340500",
@@ -9690,6 +9907,7 @@ const CLEAR_METADATA = {
9690
9907
  },
9691
9908
  drawRule: "Line23",
9692
9909
  capturesLabelSize: true,
9910
+ labelGeometryUsesResolvedMetrics: true,
9693
9911
  params: [
9694
9912
  {
9695
9913
  key: "arrowInsetRatio",
@@ -9721,16 +9939,7 @@ const CLEAR_METADATA = {
9721
9939
  max: 1,
9722
9940
  step: .01
9723
9941
  },
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
- }
9942
+ LABEL_PADDING_PARAM
9734
9943
  ]
9735
9944
  };
9736
9945
  /**
@@ -9754,11 +9963,10 @@ const CLEAR_METADATA = {
9754
9963
  * @param options - Configuration options
9755
9964
  * @returns GeoJSON FeatureCollection containing MultiLineString and Point features
9756
9965
  */
9757
- function createClearSymbol(coordinates, options = {}) {
9966
+ function createClearSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
9758
9967
  const arrowInsetRatio = Math.min(.49, Math.max(0, options.arrowInsetRatio ?? DEFAULT_CLEAR_OPTIONS.arrowInsetRatio));
9759
9968
  const arrowheadLengthRatio = options.arrowheadLengthRatio ?? DEFAULT_CLEAR_OPTIONS.arrowheadLengthRatio;
9760
9969
  const arrowheadWidthRatio = options.arrowheadWidthRatio ?? DEFAULT_CLEAR_OPTIONS.arrowheadWidthRatio;
9761
- const labelGapRatio = options.labelGapRatio ?? DEFAULT_CLEAR_OPTIONS.labelGapRatio;
9762
9970
  const [c1, c2, c3] = coordinates;
9763
9971
  const p1 = project(c1[0], c1[1]);
9764
9972
  const p2 = project(c2[0], c2[1]);
@@ -9777,6 +9985,9 @@ function createClearSymbol(coordinates, options = {}) {
9777
9985
  const arrowDir = vecScale(perpToRear, -1);
9778
9986
  const arrowheadLength = height * arrowheadLengthRatio;
9779
9987
  const arrowheadWidth = height * arrowheadWidthRatio;
9988
+ const lineRotation = Math.atan2(dirLine[1], dirLine[0]);
9989
+ const renderedLabelRotation = Math.PI - (lineRotation - Math.PI / 2);
9990
+ const labelRotation = normalizeRadians(Math.PI - keepTextLeftToRight(renderedLabelRotation));
9780
9991
  const tipAt = (t) => vecAdd(p1, vecScale(dirLine, t * height));
9781
9992
  const lines = [[unproject(p1[0], p1[1]), unproject(p2[0], p2[1])]];
9782
9993
  const fullTs = [
@@ -9790,7 +10001,7 @@ function createClearSymbol(coordinates, options = {}) {
9790
10001
  const rearEnd = vecAdd(tip, vecScale(perpToRear, shaftLength));
9791
10002
  if (i === labelIndex && shaftLength > 1e-6) {
9792
10003
  const labelCenter = vecAdd(tip, vecScale(perpToRear, shaftLength * .5));
9793
- const gapHalf = shaftLength * labelGapRatio / 2;
10004
+ const gapHalf = Math.min(shaftLength / 2, labelKnockoutHalfExtent("C", perpToRear, labelRotation, options, context, DEFAULT_CLEAR_OPTIONS.labelPadding));
9794
10005
  const gapRear = vecAdd(labelCenter, vecScale(perpToRear, gapHalf));
9795
10006
  const gapTip = vecAdd(labelCenter, vecScale(perpToRear, -gapHalf));
9796
10007
  lines.push([unproject(rearEnd[0], rearEnd[1]), unproject(gapRear[0], gapRear[1])]);
@@ -9806,9 +10017,6 @@ function createClearSymbol(coordinates, options = {}) {
9806
10017
  coordinates: lines
9807
10018
  }
9808
10019
  };
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
10020
  const labelTip = tipAt(fullTs[labelIndex]);
9813
10021
  return {
9814
10022
  type: "FeatureCollection",
@@ -9825,57 +10033,307 @@ const CLEAR = defineControlMeasure({
9825
10033
  rule: line23DrawRule
9826
10034
  });
9827
10035
  //#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
10036
+ //#region src/generators/cm34-mission-tasks/isolate.ts
10037
+ /**
10038
+ * Default options for the ISOLATE symbol.
10039
+ */
10040
+ const DEFAULT_ISOLATE_OPTIONS = {
10041
+ openingAngle: 30,
10042
+ barbCount: 9,
10043
+ barbLengthRatio: .18,
10044
+ labelPadding: .2
9841
10045
  };
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");
10046
+ const ISOLATE_VARIANT_PARAMS = [
10047
+ {
10048
+ key: "openingAngle",
10049
+ presentationTier: "advanced",
10050
+ label: "Opening",
10051
+ description: "Angular width of the gap on the friendly side, in degrees",
10052
+ type: "number",
10053
+ min: 0,
10054
+ max: 180,
10055
+ step: 1
10056
+ },
10057
+ {
10058
+ key: "barbCount",
10059
+ presentationTier: "advanced",
10060
+ label: "Barbs",
10061
+ description: "Number of inward-pointing barbs spaced along the closed arc",
10062
+ type: "number",
10063
+ min: 1,
10064
+ max: 24,
10065
+ step: 1
10066
+ },
10067
+ {
10068
+ key: "barbLengthRatio",
10069
+ presentationTier: "advanced",
10070
+ label: "Barb length",
10071
+ description: "Length of each inward barb, as a ratio of the symbol radius",
10072
+ type: "number",
10073
+ min: .01,
10074
+ max: 1,
10075
+ step: .01
9860
10076
  }
9861
- return labels;
10077
+ ];
10078
+ function isolateVariantParams(labelText) {
10079
+ return [...ISOLATE_VARIANT_PARAMS, {
10080
+ ...LABEL_PADDING_PARAM,
10081
+ label: `${labelText} padding`,
10082
+ description: `Extra boundary clearance on each side of ${labelText}, as a ratio of its height`
10083
+ }];
9862
10084
  }
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,
10085
+ const ISOLATE_METADATA = {
10086
+ id: "isolate",
10087
+ name: "Isolate",
10088
+ description: "Seals an enemy off physically and psychologically from sources of support, denies freedom of movement, and prevents contact with other enemy forces.",
10089
+ entity: "Mission Tasks",
10090
+ entityType: "Isolate",
10091
+ value: "341500",
10092
+ minCoordinates: 2,
10093
+ maxCoordinates: 2,
10094
+ geometry: "line",
10095
+ geometryTypes: ["MultiLineString", "Point"],
10096
+ paints: {
10097
+ stroke: true,
10098
+ fill: "none",
10099
+ text: true
10100
+ },
10101
+ drawRule: "Area15",
10102
+ capturesLabelSize: true,
10103
+ labelGeometryUsesResolvedMetrics: true,
10104
+ params: isolateVariantParams("I")
10105
+ };
10106
+ const LABEL_ROTATION = Math.PI;
10107
+ /**
10108
+ * Generates a GeoJSON FeatureCollection for an ISOLATE mission task symbol (341500).
10109
+ *
10110
+ * A near-complete circle centered on PT. 1 whose radius reaches PT. 2. The arc
10111
+ * starts (bare) at PT. 2's bearing and sweeps clockwise around the closed
10112
+ * portion to a single arrow tip at the far end; the wedge-shaped gap (default
10113
+ * 30°) between that arrow tip and PT. 2 is the opening — the "friendly side".
10114
+ * Inward-pointing arrowhead barbs are spaced along the closed arc, set in from
10115
+ * both ends so they never touch PT. 2 or the arrow tip, conveying the
10116
+ * sealing-off of the enclosed force. A doctrinal "I" interrupts the closed arc
10117
+ * opposite the friendly-side opening.
10118
+ *
10119
+ * Input Points:
10120
+ * - PT. 1: Center point
10121
+ * - PT. 2: Start point — fixes the radius and the bearing of the opening
10122
+ *
10123
+ * @param coordinates - [PT. 1, PT. 2] as Position arrays
10124
+ * @param options - Configuration options
10125
+ * @returns GeoJSON FeatureCollection containing MultiLineString and Point features
10126
+ */
10127
+ function createIsolateSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
10128
+ return createIsolateVariantSymbol(coordinates, options, {
10129
+ part: "isolate",
10130
+ labelText: "I"
10131
+ }, context);
10132
+ }
10133
+ /** Shared geometry for Isolate and its doctrinal cordon variants. */
10134
+ function createIsolateVariantSymbol(coordinates, options, config, context = {}) {
10135
+ const openingAngle = options.openingAngle ?? DEFAULT_ISOLATE_OPTIONS.openingAngle;
10136
+ const barbCount = options.barbCount ?? DEFAULT_ISOLATE_OPTIONS.barbCount;
10137
+ const barbLengthRatio = options.barbLengthRatio ?? DEFAULT_ISOLATE_OPTIONS.barbLengthRatio;
10138
+ const center = project(coordinates[0][0], coordinates[0][1]);
10139
+ const vRadius = vecSub(project(coordinates[1][0], coordinates[1][1]), center);
10140
+ const radius = vecMag(vRadius);
10141
+ if (radius < 1e-6) return {
10142
+ type: "FeatureCollection",
10143
+ features: []
10144
+ };
10145
+ const startAngle = Math.atan2(vRadius[1], vRadius[0]);
10146
+ const openingRad = Math.min(Math.max(openingAngle, 0), 359) * Math.PI / 180;
10147
+ const arcSpan = 2 * Math.PI - openingRad;
10148
+ const endAngle = startAngle - arcSpan;
10149
+ const pointOnCircle = (angle, r = radius) => vecAdd(center, [r * Math.cos(angle), r * Math.sin(angle)]);
10150
+ const labelSweep = arcSpan / 2;
10151
+ const labelMetrics = resolveTextMetrics(config.labelText, options, context, "regular", radius * .12);
10152
+ const labelPadding = Math.max(0, options.labelPadding ?? DEFAULT_ISOLATE_OPTIONS.labelPadding);
10153
+ const labelHalfWidth = Math.max(labelMetrics.width / 2, labelMetrics.height * LABEL_HALF_GLYPH_RATIO);
10154
+ const labelHalfGap = Math.min(arcSpan / 4, Math.max(labelHalfWidth + labelMetrics.height * labelPadding, 1) / radius);
10155
+ const labelPoint = pointOnCircle(startAngle - labelSweep);
10156
+ const labelBoxHalfWidth = labelMetrics.width / 2 + labelMetrics.height * labelPadding;
10157
+ const labelBoxHalfHeight = labelMetrics.height / 2 + labelMetrics.height * labelPadding;
10158
+ const sampleArc = (from, to) => {
10159
+ const span = to - from;
10160
+ const segments = Math.max(2, Math.round(64 * span / (2 * Math.PI)));
10161
+ const arc = [];
10162
+ for (let i = 0; i <= segments; i++) {
10163
+ const p = pointOnCircle(startAngle - from - span * i / segments);
10164
+ arc.push(unproject(p[0], p[1]));
10165
+ }
10166
+ return arc;
10167
+ };
10168
+ const barbLength = radius * barbLengthRatio;
10169
+ const lines = [sampleArc(0, labelSweep - labelHalfGap), sampleArc(labelSweep + labelHalfGap, arcSpan)];
10170
+ const tipDir = [Math.sin(endAngle), -Math.cos(endAngle)];
10171
+ lines.push(createArrowHead$1(pointOnCircle(endAngle), tipDir, barbLength, barbLength));
10172
+ const halfWidth = Math.asin(Math.min(1, barbLength / 2 / radius));
10173
+ const tipRadius = Math.max(0, radius - barbLength);
10174
+ const steps = Math.max(1, barbCount);
10175
+ for (let k = 1; k <= steps; k++) {
10176
+ const angle = startAngle - arcSpan * k / (steps + 1);
10177
+ const left = pointOnCircle(angle - halfWidth);
10178
+ const right = pointOnCircle(angle + halfWidth);
10179
+ const tip = pointOnCircle(angle, tipRadius);
10180
+ const barbLines = config.knockoutBarbs ? clipPolylineOutsideLabelBox([
10181
+ left,
10182
+ tip,
10183
+ right
10184
+ ], labelPoint, labelBoxHalfWidth, labelBoxHalfHeight, LABEL_ROTATION) : [[
10185
+ left,
10186
+ tip,
10187
+ right
10188
+ ]];
10189
+ for (const barb of barbLines) lines.push(barb.map((point) => unproject(point[0], point[1])));
10190
+ }
10191
+ return {
10192
+ type: "FeatureCollection",
10193
+ features: [{
10194
+ type: "Feature",
10195
+ properties: { part: config.part },
10196
+ geometry: {
10197
+ type: "MultiLineString",
10198
+ coordinates: lines
10199
+ }
10200
+ }, createLabelFeature(labelPoint, config.labelText, LABEL_ROTATION, options)]
10201
+ };
10202
+ }
10203
+ const ISOLATE = defineControlMeasure({
10204
+ metadata: ISOLATE_METADATA,
10205
+ generator: createIsolateSymbol,
10206
+ defaultOptions: DEFAULT_ISOLATE_OPTIONS,
10207
+ rule: isolateDrawRule
10208
+ });
10209
+ //#endregion
10210
+ //#region src/generators/cm34-mission-tasks/cordon-and-knock.ts
10211
+ const DEFAULT_CORDON_AND_KNOCK_OPTIONS = DEFAULT_ISOLATE_OPTIONS;
10212
+ const CORDON_AND_KNOCK_METADATA = {
10213
+ id: "cordon-and-knock",
10214
+ name: "Cordon and Knock",
10215
+ description: "Isolates a target area and requests or demands access before searching it.",
10216
+ entity: "Mission Tasks",
10217
+ entityType: "Cordon and Knock",
10218
+ value: "342600",
10219
+ minCoordinates: 2,
10220
+ maxCoordinates: 2,
10221
+ geometry: "line",
10222
+ geometryTypes: ["MultiLineString", "Point"],
10223
+ paints: {
10224
+ stroke: true,
10225
+ fill: "none",
10226
+ text: true
10227
+ },
10228
+ drawRule: "Area15",
10229
+ capturesLabelSize: true,
10230
+ labelGeometryUsesResolvedMetrics: true,
10231
+ params: isolateVariantParams("C/K")
10232
+ };
10233
+ /** Generates the Cordon and Knock mission-task symbol (342600). */
10234
+ function createCordonAndKnockSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
10235
+ return createIsolateVariantSymbol(coordinates, options, {
10236
+ part: "cordon-and-knock",
10237
+ labelText: "C/K",
10238
+ knockoutBarbs: true
10239
+ }, context);
10240
+ }
10241
+ const CORDON_AND_KNOCK = defineControlMeasure({
10242
+ metadata: CORDON_AND_KNOCK_METADATA,
10243
+ generator: createCordonAndKnockSymbol,
10244
+ defaultOptions: DEFAULT_CORDON_AND_KNOCK_OPTIONS,
10245
+ rule: isolateDrawRule
10246
+ });
10247
+ //#endregion
10248
+ //#region src/generators/cm34-mission-tasks/cordon-and-search.ts
10249
+ const DEFAULT_CORDON_AND_SEARCH_OPTIONS = DEFAULT_ISOLATE_OPTIONS;
10250
+ const CORDON_AND_SEARCH_METADATA = {
10251
+ id: "cordon-and-search",
10252
+ name: "Cordon and Search",
10253
+ description: "Isolates and searches a target area to capture or destroy enemy forces and contraband.",
10254
+ entity: "Mission Tasks",
10255
+ entityType: "Cordon and Search",
10256
+ value: "342700",
10257
+ minCoordinates: 2,
10258
+ maxCoordinates: 2,
10259
+ geometry: "line",
10260
+ geometryTypes: ["MultiLineString", "Point"],
10261
+ paints: {
10262
+ stroke: true,
10263
+ fill: "none",
10264
+ text: true
10265
+ },
10266
+ drawRule: "Area15",
10267
+ capturesLabelSize: true,
10268
+ labelGeometryUsesResolvedMetrics: true,
10269
+ params: isolateVariantParams("C/S")
10270
+ };
10271
+ /** Generates the Cordon and Search mission-task symbol (342700). */
10272
+ function createCordonAndSearchSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
10273
+ return createIsolateVariantSymbol(coordinates, options, {
10274
+ part: "cordon-and-search",
10275
+ labelText: "C/S",
10276
+ knockoutBarbs: true
10277
+ }, context);
10278
+ }
10279
+ const CORDON_AND_SEARCH = defineControlMeasure({
10280
+ metadata: CORDON_AND_SEARCH_METADATA,
10281
+ generator: createCordonAndSearchSymbol,
10282
+ defaultOptions: DEFAULT_CORDON_AND_SEARCH_OPTIONS,
10283
+ rule: isolateDrawRule
10284
+ });
10285
+ //#endregion
10286
+ //#region src/generators/cm15-maneuver-areas/maneuver-arrow-task-shared.ts
10287
+ const DEFAULT_MANEUVER_ARROW_TASK_AMPLIFIER_OPTIONS = {
10288
+ labelPosition: .25,
10289
+ dtgPosition: .62,
10290
+ labelPadding: 0
10291
+ };
10292
+ const DEFAULT_MANEUVER_ARROW_TASK_OPTIONS = {
10293
+ shaftWidthRatio: DEFAULT_SHAFT_WIDTH_RATIO$1,
10294
+ rearWidthRatio: DEFAULT_REAR_WIDTH_RATIO,
10295
+ smooth: false,
10296
+ smoothResolution: 5,
10297
+ crossbarLengthRatio: 1.1,
10298
+ ...DEFAULT_MANEUVER_ARROW_TASK_AMPLIFIER_OPTIONS
10299
+ };
10300
+ function createManeuverArrowTaskAmplifierLabels(geometry, options, textAmplifiers) {
10301
+ const resolved = {
10302
+ ...DEFAULT_MANEUVER_ARROW_TASK_AMPLIFIER_OPTIONS,
10303
+ ...options
10304
+ };
10305
+ const centerline = geometry.shaftCenterline;
10306
+ const { segments, totalLength } = polylineSegments(centerline);
10307
+ if (segments.length === 0) return [];
10308
+ const labels = [];
10309
+ const sizeProps = labelSizeProps(resolved);
10310
+ const labelFrame = pointAlongPolyline(segments, totalLength, clamp01(resolved.labelPosition));
10311
+ if (labelFrame && textAmplifiers.T) pushLabel(labels, labelFrame.point, textAmplifiers.T, labelRotationAlong(labelFrame.along), sizeProps, void 0, "T");
10312
+ const dtgFrame = pointAlongPolyline(segments, totalLength, clamp01(resolved.dtgPosition));
10313
+ if (dtgFrame) {
10314
+ const rowOffset = resolveLabelOffsetMeters(resolved, .55 + Math.max(0, resolved.labelPadding));
10315
+ const rotation = labelRotationAlong(dtgFrame.along);
10316
+ if (textAmplifiers.W) pushLabel(labels, vecAdd(dtgFrame.point, vecScale(dtgFrame.perp, rowOffset)), textAmplifiers.W, rotation, sizeProps, void 0, "W");
10317
+ if (textAmplifiers.W1) pushLabel(labels, vecSub(dtgFrame.point, vecScale(dtgFrame.perp, rowOffset)), textAmplifiers.W1, rotation, sizeProps, void 0, "W1");
10318
+ }
10319
+ return labels;
10320
+ }
10321
+ function createManeuverArrowTask(coordinates, options, textAmplifiers, config) {
10322
+ const merged = {
10323
+ ...DEFAULT_MANEUVER_ARROW_TASK_OPTIONS,
10324
+ ...options
10325
+ };
10326
+ const resolved = {
10327
+ ...merged,
10328
+ rearWidthRatio: resolveRearWidthRatio(options, merged.shaftWidthRatio)
10329
+ };
10330
+ const { geometry } = processAttackGeometry(coordinates, resolved);
10331
+ if (!geometry) return {
10332
+ type: "FeatureCollection",
10333
+ features: []
10334
+ };
10335
+ const outline = [
10336
+ ...geometry.shaftLeft,
9879
10337
  geometry.headRing[0],
9880
10338
  geometry.headRing[1],
9881
10339
  geometry.headRing[2],
@@ -10110,7 +10568,7 @@ const COUNTERATTACK_DASH = [6, 4];
10110
10568
  const COUNTERATTACK_METADATA = {
10111
10569
  id: "counterattack",
10112
10570
  name: "Counterattack",
10113
- description: "A variation of attack by a defending force against an attacking enemy force",
10571
+ description: "Strikes an attacking enemy force with defending forces to deny its objectives and regain the initiative.",
10114
10572
  entity: "Mission Tasks",
10115
10573
  entityType: "Counterattack",
10116
10574
  value: "340600",
@@ -10211,7 +10669,7 @@ const COUNTERATTACK_BY_FIRE_HEAD_RATIO = .18;
10211
10669
  const COUNTERATTACK_BY_FIRE_METADATA = {
10212
10670
  id: "counterattack-by-fire",
10213
10671
  name: "Counterattack by Fire",
10214
- description: "An attack against an enemy attacking force using fires",
10672
+ description: "Uses primarily direct and indirect fires against an attacking enemy force to defeat or destroy it, restore a position, or block a penetration.",
10215
10673
  entity: "Mission Tasks",
10216
10674
  entityType: "Counterattack by Fire",
10217
10675
  value: "340700",
@@ -10587,7 +11045,7 @@ const DEFAULT_COVER_OPTIONS = DEFAULT_SECURITY_TASK_OPTIONS;
10587
11045
  const COVER_METADATA = {
10588
11046
  id: "cover",
10589
11047
  name: "Cover",
10590
- description: "Protects the main body by fighting to gain time while observing and reporting information and preventing enemy ground observation of and direct fire against the main body; unlike a screen or guard, a covering force is tactically self-contained and operates independently of the main body.",
11048
+ description: "Protects the main body by fighting to gain time and preventing enemy ground observation and direct fire. Operates independently of the main body.",
10591
11049
  entity: "Mission Tasks",
10592
11050
  entityType: "Security",
10593
11051
  entitySubtype: "Cover",
@@ -10631,7 +11089,7 @@ const COVER = defineControlMeasure({
10631
11089
  const DEFAULT_DELAY_OPTIONS = {
10632
11090
  arrowheadLengthRatio: .12,
10633
11091
  arrowheadWidthRatio: .12,
10634
- labelGapRatio: .18,
11092
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING,
10635
11093
  arcSegments: 32
10636
11094
  };
10637
11095
  const DELAY_PARAMS = [
@@ -10655,16 +11113,7 @@ const DELAY_PARAMS = [
10655
11113
  max: 1,
10656
11114
  step: .01
10657
11115
  },
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
- },
11116
+ LABEL_PADDING_PARAM,
10668
11117
  {
10669
11118
  key: "arcSegments",
10670
11119
  presentationTier: "advanced",
@@ -10694,9 +11143,16 @@ const DELAY_METADATA = {
10694
11143
  },
10695
11144
  drawRule: "Line24",
10696
11145
  capturesLabelSize: true,
11146
+ labelGeometryUsesResolvedMetrics: true,
10697
11147
  params: DELAY_PARAMS
10698
11148
  };
10699
- function createDelaySymbol(coordinates, options = {}) {
11149
+ function createDelaySymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
11150
+ return createDelayTaskSymbol(coordinates, {
11151
+ glyph: "D",
11152
+ part: "delay"
11153
+ }, options, context);
11154
+ }
11155
+ function createDelayTaskSymbol(coordinates, config, options, context) {
10700
11156
  const [c1, c2, c3] = coordinates;
10701
11157
  const p1 = project(c1[0], c1[1]);
10702
11158
  const p2 = project(c2[0], c2[1]);
@@ -10712,8 +11168,9 @@ function createDelaySymbol(coordinates, options = {}) {
10712
11168
  const signedDiameter = vecDot(vecSub(p3, p2), diameterAxis);
10713
11169
  const diameterEnd = vecAdd(p2, vecScale(diameterAxis, signedDiameter));
10714
11170
  const diameter = Math.abs(signedDiameter);
10715
- const gapHalf = lineLength * Math.min(1, Math.max(0, options.labelGapRatio ?? DEFAULT_DELAY_OPTIONS.labelGapRatio)) / 2;
10716
11171
  const labelPoint = vecAdd(p1, vecScale(dirLine, lineLength / 2));
11172
+ const labelRotation = labelRotationAlong(dirLine);
11173
+ const gapHalf = Math.min(lineLength / 2, labelKnockoutHalfExtent(config.glyph, dirLine, labelRotation, options, context, DEFAULT_DELAY_OPTIONS.labelPadding));
10717
11174
  const gapStart = vecAdd(labelPoint, vecScale(dirLine, -gapHalf));
10718
11175
  const gapEnd = vecAdd(labelPoint, vecScale(dirLine, gapHalf));
10719
11176
  const lines = [
@@ -10726,52 +11183,23 @@ function createDelaySymbol(coordinates, options = {}) {
10726
11183
  type: "FeatureCollection",
10727
11184
  features: [{
10728
11185
  type: "Feature",
10729
- properties: { part: "delay" },
11186
+ properties: { part: config.part },
10730
11187
  geometry: {
10731
11188
  type: "MultiLineString",
10732
11189
  coordinates: lines
10733
11190
  }
10734
- }, createLabelFeature(labelPoint, "D", labelRotationAlong(dirLine), {
11191
+ }, createLabelFeature(labelPoint, config.glyph, labelRotation, {
10735
11192
  labelSize: options.labelSize,
10736
11193
  labelSizePixels: options.labelSizePixels
10737
11194
  })]
10738
11195
  };
10739
11196
  }
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
11197
  /**
10757
11198
  * Line24 mission tasks that share Delay's geometry and differ only in the
10758
11199
  * glyph shown in the shaft gap and the `part` tag on the line feature.
10759
11200
  */
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
- };
11201
+ function createDelayVariantSymbol(coordinates, config, options = {}, context = {}) {
11202
+ return createDelayTaskSymbol(coordinates, config, options, context);
10775
11203
  }
10776
11204
  const DELAY = defineControlMeasure({
10777
11205
  metadata: DELAY_METADATA,
@@ -10780,6 +11208,122 @@ const DELAY = defineControlMeasure({
10780
11208
  rule: line24DrawRule
10781
11209
  });
10782
11210
  //#endregion
11211
+ //#region src/generators/cm34-mission-tasks/hairpin.ts
11212
+ /**
11213
+ * Shared scaffold for the Area18 "hairpin" mission tasks (Relief in Place,
11214
+ * Demonstrate). Owns the doctrinal geometry both measures share: two parallel
11215
+ * arms with an arrow tip at each open end (PT. 1 and PT. 3), joined by a
11216
+ * semicircular turn across PT. 2 → PT. 3.
11217
+ *
11218
+ * Both tips use the same option ratios, each scaled by the length of its own
11219
+ * arm (PT. 1/PT. 2 and PT. 3/PT. 4). Only glyph placement differs between the
11220
+ * measures, which each supply via {@link HairpinSymbolConfig.buildLabel}.
11221
+ */
11222
+ function createHairpinSymbol(coordinates, config, options = {}) {
11223
+ const [c1, c2, c3, c4] = coordinates;
11224
+ const p1 = project(c1[0], c1[1]);
11225
+ const p2 = project(c2[0], c2[1]);
11226
+ const p3 = project(c3[0], c3[1]);
11227
+ const p4 = project(c4[0], c4[1]);
11228
+ const upperShaft = vecSub(p2, p1);
11229
+ const upperLength = vecMag(upperShaft);
11230
+ const lowerShaft = vecSub(p3, p4);
11231
+ const lowerLength = vecMag(lowerShaft);
11232
+ if (upperLength < 1e-6 || lowerLength < 1e-6) return {
11233
+ type: "FeatureCollection",
11234
+ features: []
11235
+ };
11236
+ const upperDirection = vecNorm(upperShaft);
11237
+ const lowerDirection = vecNorm(lowerShaft);
11238
+ const arrowheadLengthRatio = Math.max(0, options.arrowheadLengthRatio ?? DEFAULT_DELAY_OPTIONS.arrowheadLengthRatio);
11239
+ const arrowheadWidthRatio = Math.max(0, options.arrowheadWidthRatio ?? DEFAULT_DELAY_OPTIONS.arrowheadWidthRatio);
11240
+ const { label, upperArm } = config.buildLabel({
11241
+ points: [
11242
+ p1,
11243
+ p2,
11244
+ p3,
11245
+ p4
11246
+ ],
11247
+ upperDirection,
11248
+ upperLength,
11249
+ upperArm: [[unproject(p1[0], p1[1]), unproject(p2[0], p2[1])]]
11250
+ });
11251
+ return {
11252
+ type: "FeatureCollection",
11253
+ features: [{
11254
+ type: "Feature",
11255
+ properties: { part: config.part },
11256
+ geometry: {
11257
+ type: "MultiLineString",
11258
+ coordinates: [
11259
+ ...upperArm,
11260
+ createSemicircleArc(p2, p3, upperDirection, options.arcSegments ?? DEFAULT_DELAY_OPTIONS.arcSegments),
11261
+ [unproject(p4[0], p4[1]), unproject(p3[0], p3[1])],
11262
+ createArrowHead$1(p1, vecScale(upperDirection, -1), upperLength * arrowheadLengthRatio, upperLength * arrowheadWidthRatio),
11263
+ createArrowHead$1(p3, lowerDirection, lowerLength * arrowheadLengthRatio, lowerLength * arrowheadWidthRatio)
11264
+ ]
11265
+ }
11266
+ }, label]
11267
+ };
11268
+ }
11269
+ //#endregion
11270
+ //#region src/generators/cm34-mission-tasks/demonstrate.ts
11271
+ const DEFAULT_DEMONSTRATE_OPTIONS = DEFAULT_DELAY_OPTIONS;
11272
+ const DEMONSTRATE_METADATA = {
11273
+ id: "demonstrate",
11274
+ name: "Demonstrate/Demonstration",
11275
+ description: "Deceives the enemy with a show of force in an area where no decision is sought, without intending direct or indirect contact.",
11276
+ entity: "Mission Tasks",
11277
+ entityType: "Demonstrate/Demonstration",
11278
+ value: "343300",
11279
+ minCoordinates: 4,
11280
+ maxCoordinates: 4,
11281
+ geometry: "line",
11282
+ geometryTypes: ["MultiLineString", "Point"],
11283
+ paints: {
11284
+ stroke: true,
11285
+ fill: "none",
11286
+ text: true
11287
+ },
11288
+ drawRule: "Area18",
11289
+ capturesLabelSize: true,
11290
+ labelGeometryUsesResolvedMetrics: true,
11291
+ params: DELAY_PARAMS
11292
+ };
11293
+ /** Generates the DEMONSTRATE/DEMONSTRATION mission task symbol (343300). */
11294
+ function createDemonstrateSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
11295
+ return createHairpinSymbol(coordinates, {
11296
+ part: "demonstrate",
11297
+ buildLabel: ({ points, upperDirection, upperLength }) => {
11298
+ const [p1, p2] = points;
11299
+ const labelPoint = vecAdd(p1, vecScale(upperDirection, upperLength / 2));
11300
+ const rotation = labelRotationAlong(upperDirection);
11301
+ const gapHalf = Math.min(upperLength / 2, labelKnockoutHalfExtent("DEM", upperDirection, rotation, options, context, DEFAULT_DEMONSTRATE_OPTIONS.labelPadding));
11302
+ const gapStart = vecAdd(labelPoint, vecScale(upperDirection, -gapHalf));
11303
+ const gapEnd = vecAdd(labelPoint, vecScale(upperDirection, gapHalf));
11304
+ return {
11305
+ upperArm: [[unproject(p1[0], p1[1]), unproject(gapStart[0], gapStart[1])], [unproject(gapEnd[0], gapEnd[1]), unproject(p2[0], p2[1])]],
11306
+ label: createLabelFeature(labelPoint, "DEM", rotation, {
11307
+ labelSize: options.labelSize,
11308
+ labelSizePixels: options.labelSizePixels
11309
+ })
11310
+ };
11311
+ }
11312
+ }, options);
11313
+ }
11314
+ const DEMONSTRATE = defineControlMeasure({
11315
+ metadata: DEMONSTRATE_METADATA,
11316
+ generator: createDemonstrateSymbol,
11317
+ defaultOptions: DEFAULT_DEMONSTRATE_OPTIONS,
11318
+ rule: demonstrateDrawRule,
11319
+ previewSample: { controlPoints: [
11320
+ [-1, -.45],
11321
+ [.45, -.45],
11322
+ [.45, .45],
11323
+ [-1, .45]
11324
+ ] }
11325
+ });
11326
+ //#endregion
10783
11327
  //#region src/generators/cm34-mission-tasks/disengage.ts
10784
11328
  const DEFAULT_DISENGAGE_OPTIONS = DEFAULT_DELAY_OPTIONS;
10785
11329
  const DISENGAGE_METADATA = {
@@ -10800,27 +11344,25 @@ const DISENGAGE_METADATA = {
10800
11344
  },
10801
11345
  drawRule: "Line24",
10802
11346
  capturesLabelSize: true,
11347
+ labelGeometryUsesResolvedMetrics: true,
10803
11348
  params: DELAY_PARAMS
10804
11349
  };
10805
- function createDisengageSymbol(coordinates, options = {}) {
11350
+ function createDisengageSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
10806
11351
  return createDelayVariantSymbol(coordinates, {
10807
11352
  glyph: "DIS",
10808
11353
  part: "disengage"
10809
- }, options);
11354
+ }, options, context);
10810
11355
  }
10811
11356
  const DISENGAGE = defineControlMeasure({
10812
11357
  metadata: DISENGAGE_METADATA,
10813
11358
  generator: createDisengageSymbol,
10814
11359
  defaultOptions: DEFAULT_DISENGAGE_OPTIONS,
10815
11360
  rule: line24DrawRule,
10816
- previewSample: {
10817
- controlPoints: [
10818
- [-1, -.4],
10819
- [1, -.4],
10820
- [0, .6]
10821
- ],
10822
- options: { labelGapRatio: .36 }
10823
- }
11361
+ previewSample: { controlPoints: [
11362
+ [-1, -.4],
11363
+ [1, -.4],
11364
+ [0, .6]
11365
+ ] }
10824
11366
  });
10825
11367
  //#endregion
10826
11368
  //#region src/generators/cm34-mission-tasks/disrupt.ts
@@ -10830,7 +11372,7 @@ const DEFAULT_DISRUPT_MISSION_TASK_OPTIONS = {
10830
11372
  shaftLengthRatio: .25,
10831
11373
  arrowheadLengthRatio: .15,
10832
11374
  arrowheadWidthRatio: .2,
10833
- labelGapRatio: .2
11375
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING
10834
11376
  };
10835
11377
  const DISRUPT_MISSION_TASK_METADATA = {
10836
11378
  id: "disrupt-mission-task",
@@ -10850,6 +11392,7 @@ const DISRUPT_MISSION_TASK_METADATA = {
10850
11392
  },
10851
11393
  drawRule: "Area12",
10852
11394
  capturesLabelSize: true,
11395
+ labelGeometryUsesResolvedMetrics: true,
10853
11396
  params: [
10854
11397
  {
10855
11398
  key: "middleArrowLengthRatio",
@@ -10901,20 +11444,11 @@ const DISRUPT_MISSION_TASK_METADATA = {
10901
11444
  max: 1,
10902
11445
  step: .01
10903
11446
  },
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
- }
11447
+ LABEL_PADDING_PARAM
10914
11448
  ]
10915
11449
  };
10916
11450
  /** Generates the DISRUPT mission task symbol (341000). */
10917
- function createDisruptMissionTaskSymbol(coordinates, options = {}) {
11451
+ function createDisruptMissionTaskSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
10918
11452
  const p1 = project(coordinates[0][0], coordinates[0][1]);
10919
11453
  const p2 = project(coordinates[1][0], coordinates[1][1]);
10920
11454
  const frame = createProjectedBaselineFrame(p1, p2, {
@@ -10946,8 +11480,8 @@ function createDisruptMissionTaskSymbol(coordinates, options = {}) {
10946
11480
  const bottomTip = vecAdd(p2, vecScale(arrowDirection, bottomLength));
10947
11481
  const rearEnd = vecAdd(middle, vecScale(arrowDirection, -rearLength));
10948
11482
  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);
11483
+ const labelRotation = labelRotationAlong(arrowDirection);
11484
+ const gapHalf = Math.min(labelKnockoutHalfExtent("D", arrowDirection, labelRotation, options, context, DEFAULT_DISRUPT_MISSION_TASK_OPTIONS.labelPadding), middleLength / 2);
10951
11485
  const gapStart = vecAdd(labelPoint, vecScale(arrowDirection, -gapHalf));
10952
11486
  const gapEnd = vecAdd(labelPoint, vecScale(arrowDirection, gapHalf));
10953
11487
  const p1LonLat = unproject(p1[0], p1[1]);
@@ -10970,7 +11504,7 @@ function createDisruptMissionTaskSymbol(coordinates, options = {}) {
10970
11504
  createArrowHead$1(bottomTip, arrowDirection, headLength, headWidth)
10971
11505
  ]
10972
11506
  }
10973
- }, createLabelFeature(labelPoint, "D", labelRotationAlong(arrowDirection), {
11507
+ }, createLabelFeature(labelPoint, "D", labelRotation, {
10974
11508
  labelSize: options.labelSize,
10975
11509
  labelSizePixels: options.labelSizePixels
10976
11510
  })]
@@ -10989,13 +11523,13 @@ const DEFAULT_ENVELOPMENT_OPTIONS = {
10989
11523
  arrowheadLengthRatio: .1,
10990
11524
  arrowheadWidthRatio: .12,
10991
11525
  labelPosition: .6,
10992
- labelGapRatio: .18,
11526
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING,
10993
11527
  resolution: 64
10994
11528
  };
10995
11529
  const ENVELOPMENT_METADATA = {
10996
11530
  id: "envelopment",
10997
11531
  name: "Envelopment",
10998
- description: "A form of maneuver in which an attacking force avoids an enemy’s principal defense by attacking along an assailable flank.",
11532
+ description: "Avoids the enemy’s principal defenses by attacking along an assailable flank to strike the flank or rear.",
10999
11533
  entity: "Mission Tasks",
11000
11534
  entityType: "Envelopment",
11001
11535
  value: "343500",
@@ -11010,6 +11544,7 @@ const ENVELOPMENT_METADATA = {
11010
11544
  },
11011
11545
  drawRule: "Line31",
11012
11546
  capturesLabelSize: true,
11547
+ labelGeometryUsesResolvedMetrics: true,
11013
11548
  params: [
11014
11549
  {
11015
11550
  key: "arrowheadLengthRatio",
@@ -11041,76 +11576,351 @@ const ENVELOPMENT_METADATA = {
11041
11576
  max: 1,
11042
11577
  step: .01
11043
11578
  },
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
- },
11579
+ LABEL_PADDING_PARAM,
11054
11580
  ...ARC_RESOLUTION_PARAMS
11055
11581
  ]
11056
11582
  };
11057
- /** Generates the Envelopment mission task symbol (343500). */
11058
- function createEnvelopment(coordinates, options = {}) {
11059
- const [c1, c2, c3, c4] = coordinates;
11060
- const p1 = project(c1[0], c1[1]);
11061
- const p2 = project(c2[0], c2[1]);
11062
- const diameterAnchor = project(c3[0], c3[1]);
11063
- const p4 = project(c4[0], c4[1]);
11064
- const straight = vecSub(p2, p1);
11065
- const straightLength = vecMag(straight);
11066
- const diameter = vecMag(vecSub(diameterAnchor, p2));
11067
- if (straightLength < 1e-6 || diameter < 1e-6) return {
11583
+ /** Generates the Envelopment mission task symbol (343500). */
11584
+ function createEnvelopment(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
11585
+ const [c1, c2, c3, c4] = coordinates;
11586
+ const p1 = project(c1[0], c1[1]);
11587
+ const p2 = project(c2[0], c2[1]);
11588
+ const diameterAnchor = project(c3[0], c3[1]);
11589
+ const p4 = project(c4[0], c4[1]);
11590
+ const straight = vecSub(p2, p1);
11591
+ const straightLength = vecMag(straight);
11592
+ const diameter = vecMag(vecSub(diameterAnchor, p2));
11593
+ if (straightLength < 1e-6 || diameter < 1e-6) return {
11594
+ type: "FeatureCollection",
11595
+ features: []
11596
+ };
11597
+ const straightDirection = vecNorm(straight);
11598
+ const p3 = vecAdd(p2, vecScale(straightDirection, diameter));
11599
+ const labelPosition = clamp01(options.labelPosition ?? DEFAULT_ENVELOPMENT_OPTIONS.labelPosition);
11600
+ const labelPoint = vecAdd(p1, vecScale(straight, labelPosition));
11601
+ const labelRotation = labelRotationAlong(straightDirection);
11602
+ const gapHalf = Math.min(straightLength * Math.min(labelPosition, 1 - labelPosition), labelKnockoutHalfExtent("E", straightDirection, labelRotation, options, context, DEFAULT_ENVELOPMENT_OPTIONS.labelPadding));
11603
+ const gapStart = vecAdd(labelPoint, vecScale(straightDirection, -gapHalf));
11604
+ const gapEnd = vecAdd(labelPoint, vecScale(straightDirection, gapHalf));
11605
+ const center = midpoint(p2, p3);
11606
+ const startRadius = vecSub(p2, center);
11607
+ let bulgeRadius = [-startRadius[1], startRadius[0]];
11608
+ if (vecDot(vecSub(p4, center), bulgeRadius) < 0) bulgeRadius = vecScale(bulgeRadius, -1);
11609
+ const arc = sampleProjectedArc(center, startRadius, bulgeRadius, 0, Math.PI, normalizeArcResolution(options.resolution) / 2);
11610
+ const arrowhead = createArrowHead$1(p3, vecNorm(vecScale(bulgeRadius, -1)), diameter * Math.max(0, options.arrowheadLengthRatio ?? DEFAULT_ENVELOPMENT_OPTIONS.arrowheadLengthRatio), diameter * Math.max(0, options.arrowheadWidthRatio ?? DEFAULT_ENVELOPMENT_OPTIONS.arrowheadWidthRatio));
11611
+ return {
11612
+ type: "FeatureCollection",
11613
+ features: [{
11614
+ type: "Feature",
11615
+ properties: { part: "envelopment" },
11616
+ geometry: {
11617
+ type: "MultiLineString",
11618
+ coordinates: [
11619
+ [c1, unproject(gapStart[0], gapStart[1])],
11620
+ [unproject(gapEnd[0], gapEnd[1]), c2],
11621
+ arc,
11622
+ arrowhead
11623
+ ]
11624
+ }
11625
+ }, createLabelFeature(labelPoint, "E", labelRotation, options)]
11626
+ };
11627
+ }
11628
+ const ENVELOPMENT = defineControlMeasure({
11629
+ metadata: ENVELOPMENT_METADATA,
11630
+ generator: createEnvelopment,
11631
+ defaultOptions: DEFAULT_ENVELOPMENT_OPTIONS,
11632
+ rule: line31DrawRule,
11633
+ previewSample: {
11634
+ controlPoints: [
11635
+ [-.9, -.25],
11636
+ [-.15, -.25],
11637
+ [.85, -.25],
11638
+ [.35, .45]
11639
+ ],
11640
+ options: { labelSize: 100 }
11641
+ }
11642
+ });
11643
+ //#endregion
11644
+ //#region src/generators/cm34-mission-tasks/exploitation.ts
11645
+ const EXPLOITATION_FIN_DASH = [6, 4];
11646
+ const DEFAULT_EXPLOITATION_OPTIONS = {};
11647
+ const EXPLOITATION_METADATA = {
11648
+ id: "exploitation",
11649
+ name: "Exploit/Exploitation",
11650
+ description: "Follows a successful attack to disorganize the enemy in depth and capitalize on its reduced ability to react.",
11651
+ entity: "Mission Tasks",
11652
+ entityType: "Exploit/Exploitation",
11653
+ value: "343100",
11654
+ minCoordinates: 3,
11655
+ maxCoordinates: 3,
11656
+ geometry: "line",
11657
+ geometryTypes: ["MultiLineString"],
11658
+ paints: {
11659
+ stroke: true,
11660
+ fill: "none",
11661
+ text: false
11662
+ },
11663
+ drawRule: "Line30"
11664
+ };
11665
+ /** Generates the Exploit/Exploitation mission task symbol (343100). */
11666
+ function createExploitation(coordinates, _options = {}) {
11667
+ const [c1, c2, c3] = coordinates;
11668
+ const p1 = project(c1[0], c1[1]);
11669
+ const p2 = project(c2[0], c2[1]);
11670
+ const p3 = project(c3[0], c3[1]);
11671
+ const axis = vecSub(p1, p2);
11672
+ const axisLength = vecMag(axis);
11673
+ const finLength = vecMag(vecSub(p3, p2));
11674
+ if (axisLength < 1e-6 || finLength < 1e-6) return {
11675
+ type: "FeatureCollection",
11676
+ features: []
11677
+ };
11678
+ const direction = vecNorm(axis);
11679
+ const chevron = (tip) => createArrowHead$1(tip, direction, finLength * Math.SQRT1_2, finLength * Math.SQRT2);
11680
+ const [finLeft, , finRight] = chevron(p2);
11681
+ return {
11682
+ type: "FeatureCollection",
11683
+ features: [{
11684
+ type: "Feature",
11685
+ properties: { part: "exploitation" },
11686
+ geometry: {
11687
+ type: "MultiLineString",
11688
+ coordinates: [[c2, c1], chevron(p1)]
11689
+ }
11690
+ }, {
11691
+ type: "Feature",
11692
+ properties: {
11693
+ part: "fins",
11694
+ style: { strokeDash: [...EXPLOITATION_FIN_DASH] }
11695
+ },
11696
+ geometry: {
11697
+ type: "MultiLineString",
11698
+ coordinates: [[c2, finLeft], [c2, finRight]]
11699
+ }
11700
+ }]
11701
+ };
11702
+ }
11703
+ const EXPLOITATION = defineControlMeasure({
11704
+ metadata: EXPLOITATION_METADATA,
11705
+ generator: createExploitation,
11706
+ defaultOptions: DEFAULT_EXPLOITATION_OPTIONS,
11707
+ rule: line30DrawRule,
11708
+ previewSample: { controlPoints: [
11709
+ [.8, 0],
11710
+ [-.6, 0],
11711
+ [-.9, .3]
11712
+ ] }
11713
+ });
11714
+ //#endregion
11715
+ //#region src/generators/cm34-mission-tasks/follow-shared.ts
11716
+ const DEFAULT_FOLLOW_OPTIONS = { arrowheadSize: 1 };
11717
+ const FOLLOW_ARROWHEAD_SIZE_PARAM = {
11718
+ key: "arrowheadSize",
11719
+ presentationTier: "advanced",
11720
+ label: "Arrowhead size",
11721
+ description: "Arrowhead scale relative to its default size; the tip stays at point 1.",
11722
+ type: "number",
11723
+ min: .1,
11724
+ max: 3,
11725
+ step: .1
11726
+ };
11727
+ const FOLLOW_TEXT_AMPLIFIERS = [{
11728
+ key: "T",
11729
+ label: "Unique designation",
11730
+ description: "Field T — optional unique designation inside the rear shape.",
11731
+ maxLength: 21
11732
+ }];
11733
+ /** Both anchors are independent, so the preview reads left-to-right through PT. 1. */
11734
+ const FOLLOW_PREVIEW_SAMPLE = { controlPoints: [[1, 0], [-1, 0]] };
11735
+ const FOLLOW_AND_ASSUME_VARIANT = {
11736
+ body: [
11737
+ [0, .09],
11738
+ [.24, .09],
11739
+ [.32, 0],
11740
+ [.24, -.09],
11741
+ [0, -.09],
11742
+ [0, .09]
11743
+ ],
11744
+ shaftEnd: .95,
11745
+ shaftDash: [6, 4],
11746
+ arrowhead: {
11747
+ filled: false,
11748
+ points: [
11749
+ [1, 0],
11750
+ [.78, .22],
11751
+ [.78, .18],
11752
+ [.96, 0],
11753
+ [.78, -.18],
11754
+ [.78, -.22],
11755
+ [1, 0]
11756
+ ]
11757
+ },
11758
+ labelX: .14
11759
+ };
11760
+ const FOLLOW_AND_SUPPORT_VARIANT = {
11761
+ body: [
11762
+ [0, .08],
11763
+ [.26, .08],
11764
+ [.32, 0],
11765
+ [.26, -.08],
11766
+ [0, -.08],
11767
+ [.08, 0],
11768
+ [0, .08]
11769
+ ],
11770
+ shaftEnd: .81,
11771
+ arrowhead: {
11772
+ filled: true,
11773
+ points: [
11774
+ [1, 0],
11775
+ [.81, .075],
11776
+ [.81, -.075],
11777
+ [1, 0]
11778
+ ]
11779
+ },
11780
+ labelX: .17
11781
+ };
11782
+ function createFollowSymbol(coordinates, config, options, textAmplifiers) {
11783
+ const tip = project(coordinates[0][0], coordinates[0][1]);
11784
+ const rear = project(coordinates[1][0], coordinates[1][1]);
11785
+ const dx = tip[0] - rear[0];
11786
+ const dy = tip[1] - rear[1];
11787
+ if (Math.hypot(dx, dy) < 1e-6) return {
11068
11788
  type: "FeatureCollection",
11069
11789
  features: []
11070
11790
  };
11071
- const straightDirection = vecNorm(straight);
11072
- 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;
11075
- const gapStart = vecAdd(labelPoint, vecScale(straightDirection, -gapHalf));
11076
- const gapEnd = vecAdd(labelPoint, vecScale(straightDirection, gapHalf));
11077
- const center = midpoint(p2, p3);
11078
- const startRadius = vecSub(p2, center);
11079
- let bulgeRadius = [-startRadius[1], startRadius[0]];
11080
- if (vecDot(vecSub(p4, center), bulgeRadius) < 0) bulgeRadius = vecScale(bulgeRadius, -1);
11081
- const arc = sampleProjectedArc(center, startRadius, bulgeRadius, 0, Math.PI, normalizeArcResolution(options.resolution) / 2);
11082
- const arrowhead = createArrowHead$1(p3, vecNorm(vecScale(bulgeRadius, -1)), diameter * Math.max(0, options.arrowheadLengthRatio ?? DEFAULT_ENVELOPMENT_OPTIONS.arrowheadLengthRatio), diameter * Math.max(0, options.arrowheadWidthRatio ?? DEFAULT_ENVELOPMENT_OPTIONS.arrowheadWidthRatio));
11083
- return {
11084
- type: "FeatureCollection",
11085
- features: [{
11791
+ const at = (x, y) => unproject(rear[0] + x * dx - y * dy, rear[1] + x * dy + y * dx);
11792
+ const requestedSize = options.arrowheadSize ?? DEFAULT_FOLLOW_OPTIONS.arrowheadSize;
11793
+ const size = Number.isFinite(requestedSize) ? clamp(requestedSize, FOLLOW_ARROWHEAD_SIZE_PARAM.min, FOLLOW_ARROWHEAD_SIZE_PARAM.max) : DEFAULT_FOLLOW_OPTIONS.arrowheadSize;
11794
+ const headAt = (x, y) => at(1 + (x - 1) * size, y * size);
11795
+ const head = config.arrowhead.points.map(([x, y]) => headAt(x, y));
11796
+ const features = [
11797
+ {
11086
11798
  type: "Feature",
11087
- properties: { part: "envelopment" },
11799
+ properties: { part: "body" },
11088
11800
  geometry: {
11089
- type: "MultiLineString",
11090
- coordinates: [
11091
- [c1, unproject(gapStart[0], gapStart[1])],
11092
- [unproject(gapEnd[0], gapEnd[1]), c2],
11093
- arc,
11094
- arrowhead
11095
- ]
11801
+ type: "LineString",
11802
+ coordinates: config.body.map(([x, y]) => at(x, y))
11803
+ }
11804
+ },
11805
+ {
11806
+ type: "Feature",
11807
+ properties: {
11808
+ part: "shaft",
11809
+ ...config.shaftDash ? { style: { strokeDash: [...config.shaftDash] } } : {}
11810
+ },
11811
+ geometry: {
11812
+ type: "LineString",
11813
+ coordinates: [at(.32, 0), headAt(config.shaftEnd, 0)]
11814
+ }
11815
+ },
11816
+ config.arrowhead.filled ? {
11817
+ type: "Feature",
11818
+ properties: {
11819
+ part: "arrowhead",
11820
+ fill: true,
11821
+ style: { fillPattern: "solid" }
11822
+ },
11823
+ geometry: {
11824
+ type: "Polygon",
11825
+ coordinates: [head]
11826
+ }
11827
+ } : {
11828
+ type: "Feature",
11829
+ properties: { part: "arrowhead" },
11830
+ geometry: {
11831
+ type: "LineString",
11832
+ coordinates: head
11096
11833
  }
11097
- }, createLabelFeature(labelPoint, "E", labelRotationAlong(straightDirection), options)]
11834
+ }
11835
+ ];
11836
+ const designation = textAmplifiers.T?.trim();
11837
+ if (designation) features.push({
11838
+ type: "Feature",
11839
+ properties: {
11840
+ part: "label",
11841
+ labelPlacement: false,
11842
+ amplifierField: "T",
11843
+ text: designation,
11844
+ rotation: labelRotationAlong([dx, dy]),
11845
+ ...labelSizeProps(options)
11846
+ },
11847
+ geometry: {
11848
+ type: "Point",
11849
+ coordinates: at(config.labelX, 0)
11850
+ }
11851
+ });
11852
+ return {
11853
+ type: "FeatureCollection",
11854
+ features
11098
11855
  };
11099
11856
  }
11100
- const ENVELOPMENT = defineControlMeasure({
11101
- metadata: ENVELOPMENT_METADATA,
11102
- generator: createEnvelopment,
11103
- defaultOptions: DEFAULT_ENVELOPMENT_OPTIONS,
11104
- rule: line31DrawRule,
11105
- previewSample: {
11106
- controlPoints: [
11107
- [-.9, -.25],
11108
- [-.15, -.25],
11109
- [.85, -.25],
11110
- [.35, .45]
11857
+ //#endregion
11858
+ //#region src/generators/cm34-mission-tasks/follow-and-assume.ts
11859
+ function createFollowAndAssume(coordinates, options = {}, textAmplifiers = {}) {
11860
+ return createFollowSymbol(coordinates, FOLLOW_AND_ASSUME_VARIANT, options, textAmplifiers);
11861
+ }
11862
+ const FOLLOW_AND_ASSUME = defineControlMeasure({
11863
+ metadata: {
11864
+ id: "follow-and-assume",
11865
+ name: "Follow and Assume",
11866
+ description: "Follows and supports a lead force during an offensive operation and continues the mission if the lead force cannot continue.",
11867
+ entity: "Mission Tasks",
11868
+ entityType: "Follow and Assume",
11869
+ value: "341200",
11870
+ minCoordinates: 2,
11871
+ maxCoordinates: 2,
11872
+ geometry: "line",
11873
+ geometryTypes: ["LineString", "Point"],
11874
+ paints: {
11875
+ stroke: true,
11876
+ fill: "none",
11877
+ text: true
11878
+ },
11879
+ drawRule: "Line25",
11880
+ capturesLabelSize: true,
11881
+ params: [FOLLOW_ARROWHEAD_SIZE_PARAM],
11882
+ textAmplifiers: FOLLOW_TEXT_AMPLIFIERS
11883
+ },
11884
+ generator: createFollowAndAssume,
11885
+ defaultOptions: DEFAULT_FOLLOW_OPTIONS,
11886
+ rule: line25DrawRule,
11887
+ previewSample: FOLLOW_PREVIEW_SAMPLE
11888
+ });
11889
+ //#endregion
11890
+ //#region src/generators/cm34-mission-tasks/follow-and-support.ts
11891
+ function createFollowAndSupport(coordinates, options = {}, textAmplifiers = {}) {
11892
+ return createFollowSymbol(coordinates, FOLLOW_AND_SUPPORT_VARIANT, options, textAmplifiers);
11893
+ }
11894
+ const FOLLOW_AND_SUPPORT = defineControlMeasure({
11895
+ metadata: {
11896
+ id: "follow-and-support",
11897
+ name: "Follow and Support",
11898
+ description: "Follows and supports a lead force during an offensive operation, taking on tasks that would otherwise slow the lead force’s advance.",
11899
+ entity: "Mission Tasks",
11900
+ entityType: "Follow and Support",
11901
+ value: "341300",
11902
+ minCoordinates: 2,
11903
+ maxCoordinates: 2,
11904
+ geometry: "line",
11905
+ geometryTypes: [
11906
+ "LineString",
11907
+ "Polygon",
11908
+ "Point"
11111
11909
  ],
11112
- options: { labelSize: 100 }
11113
- }
11910
+ paints: {
11911
+ stroke: true,
11912
+ fill: "fixed",
11913
+ text: true
11914
+ },
11915
+ drawRule: "Line25",
11916
+ capturesLabelSize: true,
11917
+ params: [FOLLOW_ARROWHEAD_SIZE_PARAM],
11918
+ textAmplifiers: FOLLOW_TEXT_AMPLIFIERS
11919
+ },
11920
+ generator: createFollowAndSupport,
11921
+ defaultOptions: DEFAULT_FOLLOW_OPTIONS,
11922
+ rule: line25DrawRule,
11923
+ previewSample: FOLLOW_PREVIEW_SAMPLE
11114
11924
  });
11115
11925
  //#endregion
11116
11926
  //#region src/generators/cm34-mission-tasks/infiltration.ts
@@ -11118,7 +11928,7 @@ const DEFAULT_INFILTRATION_OPTIONS = {
11118
11928
  arrowheadLengthRatio: .1,
11119
11929
  arrowheadWidthRatio: .1,
11120
11930
  labelPosition: .6,
11121
- labelGapRatio: .24,
11931
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING,
11122
11932
  resolution: 64
11123
11933
  };
11124
11934
  const INFILTRATION_METADATA = {
@@ -11139,6 +11949,7 @@ const INFILTRATION_METADATA = {
11139
11949
  },
11140
11950
  drawRule: "Line32",
11141
11951
  capturesLabelSize: true,
11952
+ labelGeometryUsesResolvedMetrics: true,
11142
11953
  params: [
11143
11954
  {
11144
11955
  key: "arrowheadLengthRatio",
@@ -11170,16 +11981,7 @@ const INFILTRATION_METADATA = {
11170
11981
  max: 1,
11171
11982
  step: .01
11172
11983
  },
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
- },
11984
+ LABEL_PADDING_PARAM,
11183
11985
  ...ARC_RESOLUTION_PARAMS
11184
11986
  ]
11185
11987
  };
@@ -11205,7 +12007,7 @@ function tangentDirection(p1, p2, p3) {
11205
12007
  return direction;
11206
12008
  }
11207
12009
  /** Generates the Infiltration mission task symbol (343800). */
11208
- function createInfiltration(coordinates, options = {}) {
12010
+ function createInfiltration(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
11209
12011
  const [c1, c2, c3] = coordinates;
11210
12012
  const p1 = project(c1[0], c1[1]);
11211
12013
  const p2 = project(c2[0], c2[1]);
@@ -11238,8 +12040,10 @@ function createInfiltration(coordinates, options = {}) {
11238
12040
  const arcResolution = normalizeArcResolution(options.resolution) / 2;
11239
12041
  const firstArc = sampleProjectedArc(firstCenter, sideRadius, alongRadius, 0, Math.PI / 2, arcResolution);
11240
12042
  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;
12043
+ const labelPosition = clamp01(options.labelPosition ?? DEFAULT_INFILTRATION_OPTIONS.labelPosition);
12044
+ const labelPoint = vecAdd(p1, vecScale(straight, labelPosition));
12045
+ const labelRotation = labelRotationAlong(along);
12046
+ const gapHalf = Math.min(straightLength * Math.min(labelPosition, 1 - labelPosition), labelKnockoutHalfExtent("IN", along, labelRotation, options, context, DEFAULT_INFILTRATION_OPTIONS.labelPadding));
11243
12047
  const gapStart = vecAdd(labelPoint, vecScale(along, -gapHalf));
11244
12048
  const gapEnd = vecAdd(labelPoint, vecScale(along, gapHalf));
11245
12049
  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 +12063,7 @@ function createInfiltration(coordinates, options = {}) {
11259
12063
  arrowhead
11260
12064
  ]
11261
12065
  }
11262
- }, createLabelFeature(labelPoint, "IN", labelRotationAlong(along), options)]
12066
+ }, createLabelFeature(labelPoint, "IN", labelRotation, options)]
11263
12067
  };
11264
12068
  }
11265
12069
  const INFILTRATION = defineControlMeasure({
@@ -11282,7 +12086,7 @@ const DEFAULT_GUARD_OPTIONS = DEFAULT_SECURITY_TASK_OPTIONS;
11282
12086
  const GUARD_METADATA = {
11283
12087
  id: "guard",
11284
12088
  name: "Guard",
11285
- description: "Protects the main body by fighting to gain time while observing and reporting information and preventing enemy ground observation of and direct fire against the main body; a guard force operates within supporting range of the main body and cannot operate independently of it.",
12089
+ description: "Protects the main body by fighting to gain time and preventing enemy ground observation and direct fire. Operates within range of the main body’s supporting fires.",
11286
12090
  entity: "Mission Tasks",
11287
12091
  entityType: "Security",
11288
12092
  entitySubtype: "Guard",
@@ -11546,6 +12350,7 @@ const DROP_ZONE_METADATA = {
11546
12350
  },
11547
12351
  drawRule: "Area1",
11548
12352
  capturesLabelSize: true,
12353
+ labelGeometryUsesResolvedMetrics: ["N"],
11549
12354
  params: PREFIXED_LABELED_AREA_PARAMS,
11550
12355
  textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
11551
12356
  };
@@ -11610,6 +12415,7 @@ const ENCIRCLEMENT_METADATA = {
11610
12415
  },
11611
12416
  drawRule: "Area1",
11612
12417
  capturesLabelSize: true,
12418
+ labelGeometryUsesResolvedMetrics: ["N"],
11613
12419
  params: ENCIRCLEMENT_PARAMS,
11614
12420
  textAmplifiers: [{
11615
12421
  key: "N",
@@ -11763,6 +12569,7 @@ const EXTRACTION_ZONE_METADATA = {
11763
12569
  },
11764
12570
  drawRule: "Area1",
11765
12571
  capturesLabelSize: true,
12572
+ labelGeometryUsesResolvedMetrics: ["N"],
11766
12573
  params: PREFIXED_LABELED_AREA_PARAMS,
11767
12574
  textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
11768
12575
  };
@@ -12127,7 +12934,7 @@ const DEFAULT_FIX_MISSION_TASK_OPTIONS = {
12127
12934
  zigzagEndRatio: .8,
12128
12935
  zigzagPeakCount: 5,
12129
12936
  labelPositionRatio: .09,
12130
- labelGapRatio: .1
12937
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING
12131
12938
  };
12132
12939
  const FIX_MISSION_TASK_METADATA = {
12133
12940
  id: "fix-mission-task",
@@ -12147,6 +12954,7 @@ const FIX_MISSION_TASK_METADATA = {
12147
12954
  },
12148
12955
  drawRule: "Line9",
12149
12956
  capturesLabelSize: true,
12957
+ labelGeometryUsesResolvedMetrics: true,
12150
12958
  params: [
12151
12959
  {
12152
12960
  key: "arrowheadLengthRatio",
@@ -12208,20 +13016,11 @@ const FIX_MISSION_TASK_METADATA = {
12208
13016
  max: 1,
12209
13017
  step: .01
12210
13018
  },
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
- }
13019
+ LABEL_PADDING_PARAM
12221
13020
  ]
12222
13021
  };
12223
13022
  /** Generates the Fix mission task symbol (341100). */
12224
- function createFixMissionTaskSymbol(coordinates, options = {}) {
13023
+ function createFixMissionTaskSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
12225
13024
  const c1 = coordinates[0];
12226
13025
  const c2 = coordinates[1];
12227
13026
  const tip = project(c1[0], c1[1]);
@@ -12254,12 +13053,12 @@ function createFixMissionTaskSymbol(coordinates, options = {}) {
12254
13053
  const exitRatio = Math.min(1, zigzagEnd + toothStep / 2);
12255
13054
  const zigzagEntry = vecAdd(tail, vecScale(along, axisLength * entryRatio));
12256
13055
  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;
13056
+ const labelDistance = axisLength * clamp01(options.labelPositionRatio ?? DEFAULT_FIX_MISSION_TASK_OPTIONS.labelPositionRatio);
12260
13057
  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);
13058
+ const labelRotation = labelRotationAlong(along);
13059
+ const gapHalf = labelKnockoutHalfExtent("F", along, labelRotation, options, context, DEFAULT_FIX_MISSION_TASK_OPTIONS.labelPadding);
13060
+ const gapStartDistance = Math.max(0, labelDistance - gapHalf);
13061
+ const gapEndDistance = Math.min(axisLength, labelDistance + gapHalf);
12263
13062
  const gapStart = vecAdd(tail, vecScale(along, gapStartDistance));
12264
13063
  const gapEnd = vecAdd(tail, vecScale(along, gapEndDistance));
12265
13064
  return {
@@ -12281,7 +13080,7 @@ function createFixMissionTaskSymbol(coordinates, options = {}) {
12281
13080
  createArrowHead$1(tip, along, arrowheadLength, arrowheadWidth)
12282
13081
  ]
12283
13082
  }
12284
- }, createLabelFeature(labelCenter, "F", labelRotationAlong(along), options)]
13083
+ }, createLabelFeature(labelCenter, "F", labelRotation, options)]
12285
13084
  };
12286
13085
  }
12287
13086
  const FIX_MISSION_TASK = defineControlMeasure({
@@ -12326,6 +13125,7 @@ const FLOT_METADATA = {
12326
13125
  },
12327
13126
  drawRule: "Line1",
12328
13127
  capturesLabelSize: true,
13128
+ labelGeometryUsesResolvedMetrics: ["N"],
12329
13129
  params: [
12330
13130
  ...SMOOTH_LINE_PARAMS,
12331
13131
  {
@@ -12348,7 +13148,7 @@ const FLOT_METADATA = {
12348
13148
  step: 1,
12349
13149
  unit: "m"
12350
13150
  },
12351
- LINE_LABEL_PADDING_PARAM,
13151
+ LABEL_PADDING_PARAM,
12352
13152
  {
12353
13153
  key: "arcSegments",
12354
13154
  presentationTier: "advanced",
@@ -12670,7 +13470,7 @@ function phaseLineNameParams(placeholder) {
12670
13470
  type: "boolean",
12671
13471
  visibleWhen: (opts) => Boolean(String(opts.phaseLineName ?? "").trim())
12672
13472
  },
12673
- LINE_LABEL_PADDING_PARAM
13473
+ LABEL_PADDING_PARAM
12674
13474
  ];
12675
13475
  }
12676
13476
  //#endregion
@@ -12855,6 +13655,7 @@ const AIRHEAD_LINE_METADATA = {
12855
13655
  },
12856
13656
  drawRule: "Area1",
12857
13657
  capturesLabelSize: true,
13658
+ labelGeometryUsesResolvedMetrics: ["N"],
12858
13659
  params: LABELED_AREA_PARAMS,
12859
13660
  textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
12860
13661
  };
@@ -13508,6 +14309,7 @@ const FORTIFIED_LINE_METADATA = {
13508
14309
  },
13509
14310
  drawRule: "Line1",
13510
14311
  capturesLabelSize: true,
14312
+ labelGeometryUsesResolvedMetrics: ["N"],
13511
14313
  params: FORTIFIED_PARAMS,
13512
14314
  textAmplifiers: [HOSTILE_LINE_AMPLIFIER]
13513
14315
  };
@@ -13668,6 +14470,7 @@ const FORTIFIED_AREA_METADATA = {
13668
14470
  },
13669
14471
  drawRule: "Area1",
13670
14472
  capturesLabelSize: true,
14473
+ labelGeometryUsesResolvedMetrics: ["N"],
13671
14474
  params: [...FORTIFIED_AREA_PARAMS, AREA_LABEL_PADDING_PARAM],
13672
14475
  textAmplifiers: AREA_TEXT_AMPLIFIERS
13673
14476
  };
@@ -13782,195 +14585,55 @@ const FRONTAL_ATTACK = defineControlMeasure({
13782
14585
  [-.4, -.35],
13783
14586
  [-1, -.8],
13784
14587
  [.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
- ]);
14588
+ ],
14589
+ textAmplifiers: {
14590
+ T: "FOX",
14591
+ W: "080400Z",
14592
+ W1: "120300Z"
14593
+ },
14594
+ options: { labelSize: 80 }
13956
14595
  }
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
- };
14596
+ });
14597
+ //#endregion
14598
+ //#region src/generators/cm34-mission-tasks/forward-passage-of-lines.ts
14599
+ const DEFAULT_FORWARD_PASSAGE_OF_LINES_OPTIONS = DEFAULT_DELAY_OPTIONS;
14600
+ const FORWARD_PASSAGE_OF_LINES_METADATA = {
14601
+ id: "forward-passage-of-lines",
14602
+ name: "Forward Passage of Lines",
14603
+ description: "Passes one force through another force's positions toward the enemy.",
14604
+ entity: "Mission Tasks",
14605
+ entityType: "Forward Passage of Lines",
14606
+ value: "344100",
14607
+ minCoordinates: 3,
14608
+ maxCoordinates: 3,
14609
+ geometry: "line",
14610
+ geometryTypes: ["MultiLineString", "Point"],
14611
+ paints: {
14612
+ stroke: true,
14613
+ fill: "none",
14614
+ text: true
14615
+ },
14616
+ drawRule: "Line24",
14617
+ capturesLabelSize: true,
14618
+ labelGeometryUsesResolvedMetrics: true,
14619
+ params: DELAY_PARAMS
14620
+ };
14621
+ function createForwardPassageOfLinesSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
14622
+ return createDelayVariantSymbol(coordinates, {
14623
+ glyph: "P(F)",
14624
+ part: "forward-passage-of-lines"
14625
+ }, options, context);
13968
14626
  }
13969
- const ISOLATE = defineControlMeasure({
13970
- metadata: ISOLATE_METADATA,
13971
- generator: createIsolateSymbol,
13972
- defaultOptions: DEFAULT_ISOLATE_OPTIONS,
13973
- rule: isolateDrawRule
14627
+ const FORWARD_PASSAGE_OF_LINES = defineControlMeasure({
14628
+ metadata: FORWARD_PASSAGE_OF_LINES_METADATA,
14629
+ generator: createForwardPassageOfLinesSymbol,
14630
+ defaultOptions: DEFAULT_FORWARD_PASSAGE_OF_LINES_OPTIONS,
14631
+ rule: line24DrawRule,
14632
+ previewSample: { controlPoints: [
14633
+ [1, -.4],
14634
+ [-1, -.4],
14635
+ [0, .6]
14636
+ ] }
13974
14637
  });
13975
14638
  //#endregion
13976
14639
  //#region src/generators/cm34-mission-tasks/movement-to-contact.ts
@@ -14179,7 +14842,7 @@ const DEFAULT_OCCUPY_OPTIONS = {
14179
14842
  const OCCUPY_METADATA = {
14180
14843
  id: "occupy",
14181
14844
  name: "Occupy",
14182
- description: "Moves a friendly force into an area so it can control that area.",
14845
+ description: "Moves into and controls an area without enemy opposition.",
14183
14846
  entity: "Mission Tasks",
14184
14847
  entityType: "Occupy",
14185
14848
  value: "341700",
@@ -14194,6 +14857,7 @@ const OCCUPY_METADATA = {
14194
14857
  },
14195
14858
  drawRule: "Area16",
14196
14859
  capturesLabelSize: true,
14860
+ labelGeometryUsesResolvedMetrics: true,
14197
14861
  params: [
14198
14862
  {
14199
14863
  key: "openingAngle",
@@ -14238,10 +14902,10 @@ const OCCUPY_METADATA = {
14238
14902
  }
14239
14903
  ]
14240
14904
  };
14241
- const TAU$1 = Math.PI * 2;
14242
- const MIN_LABEL_GAP_METERS$1 = 1;
14905
+ const TAU$2 = Math.PI * 2;
14906
+ const MIN_LABEL_GAP_METERS$2 = 1;
14243
14907
  /** Generates the Occupy mission task symbol (341700). */
14244
- function createOccupySymbol(coordinates, options = {}) {
14908
+ function createOccupySymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
14245
14909
  const center = project(coordinates[0][0], coordinates[0][1]);
14246
14910
  const radiusVector = vecSub(project(coordinates[1][0], coordinates[1][1]), center);
14247
14911
  const radius = vecMag(radiusVector);
@@ -14255,11 +14919,11 @@ function createOccupySymbol(coordinates, options = {}) {
14255
14919
  };
14256
14920
  const bulgeVector = [radiusVector[1], -radiusVector[0]];
14257
14921
  const halfTurnResolution = normalizeArcResolution(resolved.resolution) / 2;
14258
- const boundarySpan = TAU$1 - Math.min(180, Math.max(5, resolved.openingAngle)) * Math.PI / 180;
14922
+ const boundarySpan = TAU$2 - Math.min(180, Math.max(5, resolved.openingAngle)) * Math.PI / 180;
14259
14923
  const labelSweep = boundarySpan / 2;
14260
- const labelHeight = resolveLabelOffsetMeters(options, 1, radius * .12);
14924
+ const labelHeight = resolveTextMetrics("O", options, context, "regular", radius * .12).height;
14261
14925
  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);
14926
+ const labelHalfGap = Math.min(boundarySpan / 4, Math.max(labelHeight * (LABEL_HALF_GLYPH_RATIO + labelPadding), MIN_LABEL_GAP_METERS$2) / radius);
14263
14927
  const sampleArc = (from, to) => sampleProjectedArc(center, radiusVector, bulgeVector, from, to, halfTurnResolution);
14264
14928
  const lines = [sampleArc(0, labelSweep - labelHalfGap), sampleArc(labelSweep + labelHalfGap, boundarySpan)];
14265
14929
  const terminal = arcPointAt(center, radiusVector, bulgeVector, boundarySpan);
@@ -14298,12 +14962,12 @@ const OCCUPY = defineControlMeasure({
14298
14962
  const DEFAULT_PENETRATE_OPTIONS = {
14299
14963
  arrowheadLengthRatio: .15,
14300
14964
  arrowheadWidthRatio: .2,
14301
- labelGapRatio: .2
14965
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING
14302
14966
  };
14303
14967
  const PENETRATE_METADATA = {
14304
14968
  id: "penetrate",
14305
14969
  name: "Penetrate",
14306
- description: "Breaks through an enemy defense and disrupts the continuity of the defensive system.",
14970
+ description: "Attacks on a narrow front to break the continuity of an enemy defense and enable the isolation and defeat of enemy forces in detail.",
14307
14971
  entity: "Mission Tasks",
14308
14972
  entityType: "Penetrate",
14309
14973
  value: "341800",
@@ -14318,6 +14982,7 @@ const PENETRATE_METADATA = {
14318
14982
  },
14319
14983
  drawRule: "Area17",
14320
14984
  capturesLabelSize: true,
14985
+ labelGeometryUsesResolvedMetrics: true,
14321
14986
  params: [
14322
14987
  {
14323
14988
  key: "arrowheadLengthRatio",
@@ -14339,20 +15004,11 @@ const PENETRATE_METADATA = {
14339
15004
  max: 1,
14340
15005
  step: .01
14341
15006
  },
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
- }
15007
+ LABEL_PADDING_PARAM
14352
15008
  ]
14353
15009
  };
14354
15010
  /** Generates the PENETRATE mission task symbol (341800). */
14355
- function createPenetrateSymbol(coordinates, options = {}) {
15011
+ function createPenetrateSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
14356
15012
  const frame = createProjectedBaselineFrame(project(coordinates[0][0], coordinates[0][1]), project(coordinates[1][0], coordinates[1][1]), {
14357
15013
  origin: "midpoint",
14358
15014
  normal: "right"
@@ -14372,29 +15028,29 @@ function createPenetrateSymbol(coordinates, options = {}) {
14372
15028
  const tail = vecAdd(frame.origin, vecScale(frame.normal, signedTailDistance));
14373
15029
  const arrowDirection = vecScale(vecSub(tip, tail), 1 / shaftLength);
14374
15030
  const labelPoint = vecScale(vecAdd(tail, tip), .5);
14375
- const gapHalf = shaftLength * Math.min(1, Math.max(0, options.labelGapRatio ?? DEFAULT_PENETRATE_OPTIONS.labelGapRatio)) / 2;
15031
+ const shaftRotation = Math.atan2(arrowDirection[1], arrowDirection[0]);
15032
+ const renderedLabelRotation = Math.PI - shaftRotation;
15033
+ const labelRotation = normalizeRadians(Math.PI - keepTextLeftToRight(renderedLabelRotation));
15034
+ const gapHalf = Math.min(shaftLength / 2, labelKnockoutHalfExtent("P", arrowDirection, labelRotation, options, context, DEFAULT_PENETRATE_OPTIONS.labelPadding));
14376
15035
  const gapStart = vecAdd(labelPoint, vecScale(arrowDirection, -gapHalf));
14377
15036
  const gapEnd = vecAdd(labelPoint, vecScale(arrowDirection, gapHalf));
14378
15037
  const headLength = frame.length * Math.max(0, options.arrowheadLengthRatio ?? DEFAULT_PENETRATE_OPTIONS.arrowheadLengthRatio);
14379
15038
  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
15039
  return {
14396
15040
  type: "FeatureCollection",
14397
- features: [lineFeature, createLabelFeature(labelPoint, "P", normalizeRadians(Math.PI - keepTextLeftToRight(renderedLabelRotation)), {
15041
+ features: [{
15042
+ type: "Feature",
15043
+ properties: { part: "penetrate" },
15044
+ geometry: {
15045
+ type: "MultiLineString",
15046
+ coordinates: [
15047
+ [coordinates[0], coordinates[1]],
15048
+ [unproject(tail[0], tail[1]), unproject(gapStart[0], gapStart[1])],
15049
+ [unproject(gapEnd[0], gapEnd[1]), unproject(tip[0], tip[1])],
15050
+ createArrowHead$1(tip, arrowDirection, headLength, headWidth)
15051
+ ]
15052
+ }
15053
+ }, createLabelFeature(labelPoint, "P", labelRotation, {
14398
15054
  labelSize: options.labelSize,
14399
15055
  labelSizePixels: options.labelSizePixels
14400
15056
  })]
@@ -14418,13 +15074,13 @@ const DEFAULT_PURSUIT_OPTIONS = {
14418
15074
  arrowheadWidthRatio: .12,
14419
15075
  terminalBarLengthRatio: 1.5,
14420
15076
  labelPosition: .6,
14421
- labelGapRatio: .18,
15077
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING,
14422
15078
  resolution: 64
14423
15079
  };
14424
15080
  const PURSUIT_METADATA = {
14425
15081
  id: "pursuit",
14426
15082
  name: "Pursuit",
14427
- description: "A mission task to catch or cut off a hostile force attempting to escape, with the aim of destroying it.",
15083
+ description: "Catches or cuts off a disorganized enemy force attempting to escape, with the aim of destroying it.",
14428
15084
  entity: "Mission Tasks",
14429
15085
  entityType: "Pursuit",
14430
15086
  value: "344000",
@@ -14439,6 +15095,7 @@ const PURSUIT_METADATA = {
14439
15095
  },
14440
15096
  drawRule: "Line33",
14441
15097
  capturesLabelSize: true,
15098
+ labelGeometryUsesResolvedMetrics: true,
14442
15099
  params: [
14443
15100
  {
14444
15101
  key: "arrowheadLengthRatio",
@@ -14480,21 +15137,12 @@ const PURSUIT_METADATA = {
14480
15137
  max: 1,
14481
15138
  step: .01
14482
15139
  },
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
- },
15140
+ LABEL_PADDING_PARAM,
14493
15141
  ...ARC_RESOLUTION_PARAMS
14494
15142
  ]
14495
15143
  };
14496
15144
  /** Generates the Pursuit mission task symbol (344000). */
14497
- function createPursuit(coordinates, options = {}) {
15145
+ function createPursuit(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
14498
15146
  const [c1, c2, c3] = coordinates;
14499
15147
  const p1 = project(c1[0], c1[1]);
14500
15148
  const p2 = project(c2[0], c2[1]);
@@ -14515,8 +15163,10 @@ function createPursuit(coordinates, options = {}) {
14515
15163
  features: []
14516
15164
  };
14517
15165
  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;
15166
+ const labelPosition = clamp01(options.labelPosition ?? DEFAULT_PURSUIT_OPTIONS.labelPosition);
15167
+ const labelPoint = vecAdd(p1, vecScale(straightDirection, straightLength * labelPosition));
15168
+ const labelRotation = labelRotationAlong(straightDirection);
15169
+ const gapHalf = Math.min(straightLength * Math.min(labelPosition, 1 - labelPosition), labelKnockoutHalfExtent("P", straightDirection, labelRotation, options, context, DEFAULT_PURSUIT_OPTIONS.labelPadding));
14520
15170
  const gapStart = vecAdd(labelPoint, vecScale(straightDirection, -gapHalf));
14521
15171
  const gapEnd = vecAdd(labelPoint, vecScale(straightDirection, gapHalf));
14522
15172
  const center = midpoint(p2, p3);
@@ -14543,7 +15193,7 @@ function createPursuit(coordinates, options = {}) {
14543
15193
  terminalBar
14544
15194
  ]
14545
15195
  }
14546
- }, createLabelFeature(labelPoint, "P", labelRotationAlong(straightDirection), options)]
15196
+ }, createLabelFeature(labelPoint, "P", labelRotation, options)]
14547
15197
  };
14548
15198
  }
14549
15199
  const PURSUIT = defineControlMeasure({
@@ -14566,7 +15216,7 @@ const DEFAULT_REARWARD_PASSAGE_OF_LINES_OPTIONS = DEFAULT_DELAY_OPTIONS;
14566
15216
  const REARWARD_PASSAGE_OF_LINES_METADATA = {
14567
15217
  id: "rearward-passage-of-lines",
14568
15218
  name: "Rearward Passage of Lines",
14569
- description: "Passes one force through another force's positions toward the rear.",
15219
+ description: "Passes one force through another force’s positions while moving away from the enemy.",
14570
15220
  entity: "Mission Tasks",
14571
15221
  entityType: "Rearward Passage of Lines",
14572
15222
  value: "344200",
@@ -14581,27 +15231,80 @@ const REARWARD_PASSAGE_OF_LINES_METADATA = {
14581
15231
  },
14582
15232
  drawRule: "Line24",
14583
15233
  capturesLabelSize: true,
15234
+ labelGeometryUsesResolvedMetrics: true,
14584
15235
  params: DELAY_PARAMS
14585
15236
  };
14586
- function createRearwardPassageOfLinesSymbol(coordinates, options = {}) {
15237
+ function createRearwardPassageOfLinesSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
14587
15238
  return createDelayVariantSymbol(coordinates, {
14588
15239
  glyph: "P(R)",
14589
15240
  part: "rearward-passage-of-lines"
14590
- }, options);
15241
+ }, options, context);
14591
15242
  }
14592
15243
  const REARWARD_PASSAGE_OF_LINES = defineControlMeasure({
14593
15244
  metadata: REARWARD_PASSAGE_OF_LINES_METADATA,
14594
15245
  generator: createRearwardPassageOfLinesSymbol,
14595
15246
  defaultOptions: DEFAULT_REARWARD_PASSAGE_OF_LINES_OPTIONS,
14596
15247
  rule: line24DrawRule,
14597
- previewSample: {
14598
- controlPoints: [
14599
- [-1, -.4],
14600
- [1, -.4],
14601
- [0, .6]
14602
- ],
14603
- options: { labelGapRatio: .42 }
14604
- }
15248
+ previewSample: { controlPoints: [
15249
+ [-1, -.4],
15250
+ [1, -.4],
15251
+ [0, .6]
15252
+ ] }
15253
+ });
15254
+ //#endregion
15255
+ //#region src/generators/cm34-mission-tasks/relief-in-place.ts
15256
+ const DEFAULT_RELIEF_IN_PLACE_OPTIONS = {
15257
+ arrowheadLengthRatio: DEFAULT_DELAY_OPTIONS.arrowheadLengthRatio,
15258
+ arrowheadWidthRatio: DEFAULT_DELAY_OPTIONS.arrowheadWidthRatio,
15259
+ arcSegments: DEFAULT_DELAY_OPTIONS.arcSegments
15260
+ };
15261
+ const RELIEF_IN_PLACE_METADATA = {
15262
+ id: "relief-in-place",
15263
+ name: "Relief in Place",
15264
+ description: "Replaces all or part of a unit in an area and transfers responsibility for its mission and assigned area to the incoming unit.",
15265
+ entity: "Mission Tasks",
15266
+ entityType: "Relief in Place",
15267
+ value: "341900",
15268
+ minCoordinates: 4,
15269
+ maxCoordinates: 4,
15270
+ geometry: "line",
15271
+ geometryTypes: ["MultiLineString", "Point"],
15272
+ paints: {
15273
+ stroke: true,
15274
+ fill: "none",
15275
+ text: true
15276
+ },
15277
+ drawRule: "Area18",
15278
+ capturesLabelSize: true,
15279
+ params: DELAY_PARAMS.filter((param) => param.key !== "labelPadding")
15280
+ };
15281
+ /** Generates the RELIEF IN PLACE mission task symbol (341900). */
15282
+ function createReliefInPlaceSymbol(coordinates, options = {}) {
15283
+ return createHairpinSymbol(coordinates, {
15284
+ part: "relief-in-place",
15285
+ buildLabel: ({ points, upperDirection, upperArm }) => ({
15286
+ upperArm,
15287
+ label: createLabelFeature(averagePoints(points), "RIP", labelRotationAlong(upperDirection), {
15288
+ labelSize: options.labelSize,
15289
+ labelSizePixels: options.labelSizePixels
15290
+ })
15291
+ })
15292
+ }, options);
15293
+ }
15294
+ function averagePoints(points) {
15295
+ return vecScale(points.reduce((sum, point) => vecAdd(sum, point), [0, 0]), 1 / points.length);
15296
+ }
15297
+ const RELIEF_IN_PLACE = defineControlMeasure({
15298
+ metadata: RELIEF_IN_PLACE_METADATA,
15299
+ generator: createReliefInPlaceSymbol,
15300
+ defaultOptions: DEFAULT_RELIEF_IN_PLACE_OPTIONS,
15301
+ rule: reliefInPlaceDrawRule,
15302
+ previewSample: { controlPoints: [
15303
+ [-1, .45],
15304
+ [.45, .45],
15305
+ [.45, -.45],
15306
+ [-1, -.45]
15307
+ ] }
14605
15308
  });
14606
15309
  //#endregion
14607
15310
  //#region src/generators/cm34-mission-tasks/retire.ts
@@ -14624,14 +15327,15 @@ const RETIRE_METADATA = {
14624
15327
  },
14625
15328
  drawRule: "Line24",
14626
15329
  capturesLabelSize: true,
15330
+ labelGeometryUsesResolvedMetrics: true,
14627
15331
  params: DELAY_PARAMS
14628
15332
  };
14629
15333
  /** Generates the RETIRE/RETIREMENT mission task symbol (342000). */
14630
- function createRetireSymbol(coordinates, options = {}) {
15334
+ function createRetireSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
14631
15335
  return createDelayVariantSymbol(coordinates, {
14632
15336
  glyph: "R",
14633
15337
  part: "retire"
14634
- }, options);
15338
+ }, options, context);
14635
15339
  }
14636
15340
  const RETIRE = defineControlMeasure({
14637
15341
  metadata: RETIRE_METADATA,
@@ -14685,11 +15389,137 @@ const SCREEN = defineControlMeasure({
14685
15389
  }
14686
15390
  });
14687
15391
  //#endregion
15392
+ //#region src/generators/cm34-mission-tasks/secure.ts
15393
+ const DEFAULT_SECURE_OPTIONS = {
15394
+ openingAngle: 30,
15395
+ resolution: 64,
15396
+ arrowheadLengthRatio: .2,
15397
+ arrowheadWidthRatio: .4,
15398
+ labelPadding: .2
15399
+ };
15400
+ const SECURE_METADATA = {
15401
+ id: "secure",
15402
+ name: "Secure",
15403
+ description: "Prevents a unit, facility, or geographical location from being damaged or destroyed by enemy action.",
15404
+ entity: "Mission Tasks",
15405
+ entityType: "Secure",
15406
+ value: "342100",
15407
+ minCoordinates: 2,
15408
+ maxCoordinates: 2,
15409
+ geometry: "area",
15410
+ geometryTypes: ["MultiLineString", "Point"],
15411
+ paints: {
15412
+ stroke: true,
15413
+ fill: "none",
15414
+ text: true
15415
+ },
15416
+ drawRule: "Area19",
15417
+ capturesLabelSize: true,
15418
+ labelGeometryUsesResolvedMetrics: true,
15419
+ params: [
15420
+ {
15421
+ key: "openingAngle",
15422
+ presentationTier: "advanced",
15423
+ label: "Opening",
15424
+ description: "Angular width of the opening on the friendly side, in degrees",
15425
+ type: "number",
15426
+ min: 5,
15427
+ max: 180,
15428
+ step: 1
15429
+ },
15430
+ ...ARC_RESOLUTION_PARAMS,
15431
+ {
15432
+ key: "arrowheadLengthRatio",
15433
+ presentationTier: "advanced",
15434
+ label: "Arrowhead length",
15435
+ description: "Arrowhead length as a ratio of the symbol radius",
15436
+ type: "number",
15437
+ min: 0,
15438
+ max: 1,
15439
+ step: .01
15440
+ },
15441
+ {
15442
+ key: "arrowheadWidthRatio",
15443
+ presentationTier: "advanced",
15444
+ label: "Arrowhead width",
15445
+ description: "Arrowhead width as a ratio of the symbol radius",
15446
+ type: "number",
15447
+ min: 0,
15448
+ max: 1,
15449
+ step: .01
15450
+ },
15451
+ {
15452
+ key: "labelPadding",
15453
+ presentationTier: "advanced",
15454
+ label: "S padding",
15455
+ description: "Extra boundary clearance on each side of S, as a ratio of its height",
15456
+ type: "number",
15457
+ min: 0,
15458
+ max: 2,
15459
+ step: .05
15460
+ }
15461
+ ]
15462
+ };
15463
+ const TAU$1 = Math.PI * 2;
15464
+ const MIN_LABEL_GAP_METERS$1 = 1;
15465
+ /** Generates the Secure mission task symbol (342100). */
15466
+ function createSecureSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
15467
+ const center = project(coordinates[0][0], coordinates[0][1]);
15468
+ const radiusVector = vecSub(project(coordinates[1][0], coordinates[1][1]), center);
15469
+ const radius = vecMag(radiusVector);
15470
+ if (radius < 1e-6) return {
15471
+ type: "FeatureCollection",
15472
+ features: []
15473
+ };
15474
+ const resolved = {
15475
+ ...DEFAULT_SECURE_OPTIONS,
15476
+ ...options
15477
+ };
15478
+ const bulgeVector = [radiusVector[1], -radiusVector[0]];
15479
+ const halfTurnResolution = normalizeArcResolution(resolved.resolution) / 2;
15480
+ const boundarySpan = TAU$1 - Math.min(180, Math.max(5, resolved.openingAngle)) * Math.PI / 180;
15481
+ const labelSweep = boundarySpan / 2;
15482
+ const labelHeight = resolveTextMetrics("S", options, context, "regular", radius * .12).height;
15483
+ const labelPadding = Math.max(0, resolved.labelPadding);
15484
+ const labelHalfGap = Math.min(boundarySpan / 4, Math.max(labelHeight * (LABEL_HALF_GLYPH_RATIO + labelPadding), MIN_LABEL_GAP_METERS$1) / radius);
15485
+ const sampleArc = (from, to) => sampleProjectedArc(center, radiusVector, bulgeVector, from, to, halfTurnResolution);
15486
+ const terminal = arcPointAt(center, radiusVector, bulgeVector, boundarySpan);
15487
+ const radial = vecScale(vecSub(terminal, center), 1 / radius);
15488
+ const tangent = [radial[1], -radial[0]];
15489
+ const lines = [
15490
+ sampleArc(0, labelSweep - labelHalfGap),
15491
+ sampleArc(labelSweep + labelHalfGap, boundarySpan),
15492
+ createArrowHead$1(terminal, tangent, radius * Math.max(0, resolved.arrowheadLengthRatio), radius * Math.max(0, resolved.arrowheadWidthRatio))
15493
+ ];
15494
+ const labelPoint = arcPointAt(center, radiusVector, bulgeVector, labelSweep);
15495
+ return {
15496
+ type: "FeatureCollection",
15497
+ features: [{
15498
+ type: "Feature",
15499
+ properties: { part: "secure" },
15500
+ geometry: {
15501
+ type: "MultiLineString",
15502
+ coordinates: lines
15503
+ }
15504
+ }, createLabelFeature(labelPoint, "S", Math.PI, options)]
15505
+ };
15506
+ }
15507
+ const SECURE = defineControlMeasure({
15508
+ metadata: SECURE_METADATA,
15509
+ generator: createSecureSymbol,
15510
+ defaultOptions: DEFAULT_SECURE_OPTIONS,
15511
+ rule: secureDrawRule,
15512
+ previewSample: {
15513
+ controlPoints: [[0, 0], [-.8, .05]],
15514
+ options: { labelSize: 90 }
15515
+ }
15516
+ });
15517
+ //#endregion
14688
15518
  //#region src/generators/cm34-mission-tasks/seize.ts
14689
15519
  const DEFAULT_SEIZE_OPTIONS = {
14690
15520
  arrowheadLengthRatio: .1,
14691
15521
  arrowheadWidthRatio: .12,
14692
- labelGapRatio: .13,
15522
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING,
14693
15523
  curveSegments: 48,
14694
15524
  circleSegments: 64
14695
15525
  };
@@ -14714,16 +15544,7 @@ const SEIZE_PARAMS = [
14714
15544
  max: .5,
14715
15545
  step: .01
14716
15546
  },
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
- },
15547
+ LABEL_PADDING_PARAM,
14727
15548
  {
14728
15549
  key: "curveSegments",
14729
15550
  presentationTier: "advanced",
@@ -14748,7 +15569,7 @@ const SEIZE_PARAMS = [
14748
15569
  const SEIZE_METADATA = {
14749
15570
  id: "seize",
14750
15571
  name: "Seize",
14751
- description: "Takes possession of a designated area using overwhelming force and establishes control of it.",
15572
+ description: "Takes possession of a designated area using overwhelming force, overcoming enemy opposition and preventing enemy direct fire on the objective.",
14752
15573
  entity: "Mission Tasks",
14753
15574
  entityType: "Seize",
14754
15575
  value: "342300",
@@ -14763,6 +15584,7 @@ const SEIZE_METADATA = {
14763
15584
  },
14764
15585
  drawRule: "Line27",
14765
15586
  capturesLabelSize: true,
15587
+ labelGeometryUsesResolvedMetrics: true,
14766
15588
  params: SEIZE_PARAMS
14767
15589
  };
14768
15590
  function positiveAngle(angle) {
@@ -14794,7 +15616,7 @@ function threePointArc(start, middle, end) {
14794
15616
  };
14795
15617
  }
14796
15618
  /** Shared geometry for mission tasks that use the Seize graphic. */
14797
- function createSeizeLikeSymbol(coordinates, options = {}, label = "S", part = "seize") {
15619
+ function createSeizeLikeSymbol(coordinates, options = {}, label = "S", part = "seize", context = {}) {
14798
15620
  const [c1, c2, c3, c4] = coordinates;
14799
15621
  const center = project(c1[0], c1[1]);
14800
15622
  const start = project(c2[0], c2[1]);
@@ -14815,8 +15637,14 @@ function createSeizeLikeSymbol(coordinates, options = {}, label = "S", part = "s
14815
15637
  let arrowDirection;
14816
15638
  let labelPoint;
14817
15639
  let tangentAtLabel;
15640
+ let labelRotation;
14818
15641
  if (arc) {
14819
- const gapHalf = clamp01(options.labelGapRatio ?? DEFAULT_SEIZE_OPTIONS.labelGapRatio) / 2;
15642
+ const labelAngle = arc.startAngle + arc.sweepAngle / 2;
15643
+ const sweepSign = Math.sign(arc.sweepAngle);
15644
+ labelPoint = vecAdd(arc.center, [arc.radius * Math.cos(labelAngle), arc.radius * Math.sin(labelAngle)]);
15645
+ tangentAtLabel = [-Math.sin(labelAngle) * sweepSign, Math.cos(labelAngle) * sweepSign];
15646
+ labelRotation = labelRotationAlong(tangentAtLabel);
15647
+ const gapHalf = arcKnockoutHalfFraction(labelKnockoutHalfExtent(label, tangentAtLabel, labelRotation, options, context, DEFAULT_SEIZE_OPTIONS.labelPadding), arc.radius, arc.sweepAngle);
14820
15648
  const gapStart = .5 - gapHalf;
14821
15649
  const gapEnd = .5 + gapHalf;
14822
15650
  const sampleArc = (from, to) => {
@@ -14832,22 +15660,19 @@ function createSeizeLikeSymbol(coordinates, options = {}, label = "S", part = "s
14832
15660
  firstArcPart = sampleArc(0, gapStart);
14833
15661
  secondArcPart = sampleArc(gapEnd, 1);
14834
15662
  const endAngle = arc.startAngle + arc.sweepAngle;
14835
- const sweepSign = Math.sign(arc.sweepAngle);
14836
15663
  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
15664
  } else {
14841
15665
  labelPoint = vecScale(vecAdd(start, tip), .5);
14842
- const gapHalf = span * clamp01(options.labelGapRatio ?? DEFAULT_SEIZE_OPTIONS.labelGapRatio) / 2;
14843
15666
  const direction = vecNorm(vecSub(tip, start));
15667
+ tangentAtLabel = direction;
15668
+ labelRotation = labelRotationAlong(tangentAtLabel);
15669
+ const gapHalf = Math.min(span / 2, labelKnockoutHalfExtent(label, direction, labelRotation, options, context, DEFAULT_SEIZE_OPTIONS.labelPadding));
14844
15670
  const before = vecAdd(labelPoint, vecScale(direction, -gapHalf));
14845
15671
  const after = vecAdd(labelPoint, vecScale(direction, gapHalf));
14846
15672
  firstArcPart = [toPosition(start), toPosition(before)];
14847
15673
  secondArcPart = [toPosition(after), toPosition(tip)];
14848
15674
  const terminal = vecSub(tip, curvePoint);
14849
15675
  arrowDirection = vecMag(terminal) >= 1e-6 ? vecNorm(terminal) : vecNorm(vecSub(tip, start));
14850
- tangentAtLabel = direction;
14851
15676
  }
14852
15677
  const startAngle = Math.atan2(radiusVector[1], radiusVector[0]);
14853
15678
  const circle = [];
@@ -14870,12 +15695,12 @@ function createSeizeLikeSymbol(coordinates, options = {}, label = "S", part = "s
14870
15695
  type: "MultiLineString",
14871
15696
  coordinates: lines
14872
15697
  }
14873
- }, createLabelFeature(labelPoint, label, labelRotationAlong(tangentAtLabel), options)]
15698
+ }, createLabelFeature(labelPoint, label, labelRotation, options)]
14874
15699
  };
14875
15700
  }
14876
15701
  /** Generates the Seize mission task symbol (342300). */
14877
- function createSeizeSymbol(coordinates, options = {}) {
14878
- return createSeizeLikeSymbol(coordinates, options);
15702
+ function createSeizeSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
15703
+ return createSeizeLikeSymbol(coordinates, options, "S", "seize", context);
14879
15704
  }
14880
15705
  const SEIZE = defineControlMeasure({
14881
15706
  metadata: SEIZE_METADATA,
@@ -14913,11 +15738,12 @@ const EVACUATE_METADATA = {
14913
15738
  },
14914
15739
  drawRule: "Line27",
14915
15740
  capturesLabelSize: true,
15741
+ labelGeometryUsesResolvedMetrics: true,
14916
15742
  params: SEIZE_PARAMS
14917
15743
  };
14918
15744
  /** Generates the Evacuate mission task symbol (344500). */
14919
- function createEvacuateSymbol(coordinates, options = {}) {
14920
- return createSeizeLikeSymbol(coordinates, options, "E", "evacuate");
15745
+ function createEvacuateSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
15746
+ return createSeizeLikeSymbol(coordinates, options, "E", "evacuate", context);
14921
15747
  }
14922
15748
  const EVACUATE = defineControlMeasure({
14923
15749
  metadata: EVACUATE_METADATA,
@@ -14955,13 +15781,14 @@ const WITHDRAW_METADATA = {
14955
15781
  },
14956
15782
  drawRule: "Line24",
14957
15783
  capturesLabelSize: true,
15784
+ labelGeometryUsesResolvedMetrics: true,
14958
15785
  params: DELAY_PARAMS
14959
15786
  };
14960
- function createWithdrawSymbol(coordinates, options = {}) {
15787
+ function createWithdrawSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
14961
15788
  return createDelayVariantSymbol(coordinates, {
14962
15789
  glyph: "W",
14963
15790
  part: "withdraw"
14964
- }, options);
15791
+ }, options, context);
14965
15792
  }
14966
15793
  const WITHDRAW = defineControlMeasure({
14967
15794
  metadata: WITHDRAW_METADATA,
@@ -14990,27 +15817,25 @@ const WITHDRAW_UNDER_PRESSURE_METADATA = {
14990
15817
  },
14991
15818
  drawRule: "Line24",
14992
15819
  capturesLabelSize: true,
15820
+ labelGeometryUsesResolvedMetrics: true,
14993
15821
  params: DELAY_PARAMS
14994
15822
  };
14995
- function createWithdrawUnderPressureSymbol(coordinates, options = {}) {
15823
+ function createWithdrawUnderPressureSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
14996
15824
  return createDelayVariantSymbol(coordinates, {
14997
15825
  glyph: "WP",
14998
15826
  part: "withdraw-under-pressure"
14999
- }, options);
15827
+ }, options, context);
15000
15828
  }
15001
15829
  const WITHDRAW_UNDER_PRESSURE = defineControlMeasure({
15002
15830
  metadata: WITHDRAW_UNDER_PRESSURE_METADATA,
15003
15831
  generator: createWithdrawUnderPressureSymbol,
15004
15832
  defaultOptions: DEFAULT_WITHDRAW_UNDER_PRESSURE_OPTIONS,
15005
15833
  rule: line24DrawRule,
15006
- previewSample: {
15007
- controlPoints: [
15008
- [-1, -.4],
15009
- [1, -.4],
15010
- [0, .6]
15011
- ],
15012
- options: { labelGapRatio: .3 }
15013
- }
15834
+ previewSample: { controlPoints: [
15835
+ [-1, -.4],
15836
+ [1, -.4],
15837
+ [0, .6]
15838
+ ] }
15014
15839
  });
15015
15840
  //#endregion
15016
15841
  //#region src/generators/cm27-protection-areas/turn.ts
@@ -15076,10 +15901,10 @@ const TURN_METADATA = {
15076
15901
  * arc and placing its tip at PT1. PT3 selects which side of the PT2 -> PT1
15077
15902
  * chord contains the arc.
15078
15903
  *
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.
15904
+ * `labelGapHalfLength` optionally resolves a meter-denominated half-gap from the arc midpoint
15905
+ * anchor for measures that carry a doctrinal glyph there.
15081
15906
  */
15082
- function createTurnLikeSymbol(coordinates, options = {}, labelGapRatio = 0) {
15907
+ function createTurnLikeSymbol(coordinates, options = {}, labelGapHalfLength) {
15083
15908
  const tip = project(coordinates[0][0], coordinates[0][1]);
15084
15909
  const rear = project(coordinates[1][0], coordinates[1][1]);
15085
15910
  const side = quarterArcSide(rear, tip, coordinates[2] ? project(coordinates[2][0], coordinates[2][1]) : void 0);
@@ -15104,14 +15929,14 @@ function createTurnLikeSymbol(coordinates, options = {}, labelGapRatio = 0) {
15104
15929
  }
15105
15930
  return points;
15106
15931
  };
15107
- const gapHalf = clamp01(labelGapRatio) / 2;
15108
- const arcParts = gapHalf > 0 ? [sampleArc(0, .5 - gapHalf), sampleArc(.5 + gapHalf, 1)] : [sampleArc(0, 1)];
15109
15932
  const sweepSign = Math.sign(arc.sweepAngle);
15110
15933
  const midAngle = arc.startAngle + arc.sweepAngle / 2;
15111
15934
  const labelAnchor = {
15112
15935
  point: arc.midpoint,
15113
15936
  tangent: [-Math.sin(midAngle) * sweepSign, Math.cos(midAngle) * sweepSign]
15114
15937
  };
15938
+ const gapHalf = arcKnockoutHalfFraction(labelGapHalfLength?.(labelAnchor) ?? 0, arc.radius, arc.sweepAngle);
15939
+ const arcParts = gapHalf > 0 ? [sampleArc(0, .5 - gapHalf), sampleArc(.5 + gapHalf, 1)] : [sampleArc(0, 1)];
15115
15940
  const headWidth = totalLength * (options.arrowheadWidthRatio ?? DEFAULT_TURN_OPTIONS.arrowheadWidthRatio);
15116
15941
  const arrowHead = createArrowHeadRing(tip, vecNorm(vecSub(tip, headBase)), vecMag(vecSub(tip, headBase)), headWidth);
15117
15942
  return {
@@ -15165,12 +15990,12 @@ const DEFAULT_TURN_MISSION_TASK_OPTIONS = {
15165
15990
  ...DEFAULT_TURN_OPTIONS,
15166
15991
  arrowheadLengthRatio: .14,
15167
15992
  arrowheadWidthRatio: .14,
15168
- labelGapRatio: .18
15993
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING
15169
15994
  };
15170
15995
  const TURN_MISSION_TASK_METADATA = {
15171
15996
  id: "turn-mission-task",
15172
15997
  name: "Turn",
15173
- description: "Causes an enemy force to move from one avenue of approach to another or into an engagement area.",
15998
+ description: "Forces an enemy force from one avenue of approach or movement corridor to another.",
15174
15999
  entity: "Mission Tasks",
15175
16000
  entityType: "Turn",
15176
16001
  value: "344700",
@@ -15188,24 +16013,19 @@ const TURN_MISSION_TASK_METADATA = {
15188
16013
  },
15189
16014
  drawRule: "Line10",
15190
16015
  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
- }]
16016
+ labelGeometryUsesResolvedMetrics: true,
16017
+ params: [...TURN_PARAMS, LABEL_PADDING_PARAM]
15201
16018
  };
15202
16019
  /** Generates the TURN mission task symbol (344700). */
15203
- function createTurnMissionTaskSymbol(coordinates, options = {}) {
16020
+ function createTurnMissionTaskSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
15204
16021
  const { features, labelAnchor } = createTurnLikeSymbol(coordinates, {
15205
16022
  arrowheadLengthRatio: options.arrowheadLengthRatio ?? DEFAULT_TURN_MISSION_TASK_OPTIONS.arrowheadLengthRatio,
15206
16023
  arrowheadWidthRatio: options.arrowheadWidthRatio ?? DEFAULT_TURN_MISSION_TASK_OPTIONS.arrowheadWidthRatio,
15207
16024
  arcSegments: options.arcSegments ?? DEFAULT_TURN_MISSION_TASK_OPTIONS.arcSegments
15208
- }, options.labelGapRatio ?? DEFAULT_TURN_MISSION_TASK_OPTIONS.labelGapRatio);
16025
+ }, (anchor) => {
16026
+ const rotation = labelRotationAlong(anchor.tangent);
16027
+ return labelKnockoutHalfExtent("T", anchor.tangent, rotation, options, context, DEFAULT_TURN_MISSION_TASK_OPTIONS.labelPadding);
16028
+ });
15209
16029
  if (!labelAnchor) return {
15210
16030
  type: "FeatureCollection",
15211
16031
  features: []
@@ -15251,6 +16071,7 @@ const JOINT_TACTICAL_ACTION_AREA_METADATA = {
15251
16071
  },
15252
16072
  drawRule: "Area1",
15253
16073
  capturesLabelSize: true,
16074
+ labelGeometryUsesResolvedMetrics: ["N"],
15254
16075
  params: PREFIXED_LABELED_AREA_PARAMS,
15255
16076
  textAmplifiers: AREA_TEXT_AMPLIFIERS
15256
16077
  };
@@ -15311,6 +16132,7 @@ const LANDING_ZONE_METADATA = {
15311
16132
  },
15312
16133
  drawRule: "Area1",
15313
16134
  capturesLabelSize: true,
16135
+ labelGeometryUsesResolvedMetrics: ["N"],
15314
16136
  params: PREFIXED_LABELED_AREA_PARAMS,
15315
16137
  textAmplifiers: AREA_TEXT_AMPLIFIERS
15316
16138
  };
@@ -15366,6 +16188,7 @@ const PENETRATION_BOX_METADATA = {
15366
16188
  },
15367
16189
  drawRule: "Area1",
15368
16190
  capturesLabelSize: true,
16191
+ labelGeometryUsesResolvedMetrics: ["N"],
15369
16192
  params: LABELED_AREA_PARAMS,
15370
16193
  textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
15371
16194
  };
@@ -15417,6 +16240,7 @@ const LIMITED_ACCESS_AREA_METADATA = {
15417
16240
  },
15418
16241
  drawRule: "Area1",
15419
16242
  capturesLabelSize: true,
16243
+ labelGeometryUsesResolvedMetrics: ["N"],
15420
16244
  params: [...AREA_SMOOTH_PARAMS, AREA_LABEL_PADDING_PARAM],
15421
16245
  textAmplifiers: [AREA_HOSTILE_AMPLIFIER]
15422
16246
  };
@@ -15512,6 +16336,7 @@ const NO_FIRE_AREA_IRREGULAR_METADATA = {
15512
16336
  },
15513
16337
  drawRule: "Area1",
15514
16338
  capturesLabelSize: true,
16339
+ labelGeometryUsesResolvedMetrics: ["N"],
15515
16340
  params: [...AREA_SMOOTH_PARAMS, AREA_LABEL_PADDING_PARAM],
15516
16341
  textAmplifiers: [...NO_FIRE_AREA_TEXT_AMPLIFIERS, AREA_HOSTILE_AMPLIFIER]
15517
16342
  };
@@ -15757,6 +16582,7 @@ const MINEFIELD_METADATA = {
15757
16582
  text: true
15758
16583
  },
15759
16584
  drawRule: "Point2",
16585
+ labelGeometryUsesResolvedMetrics: ["N"],
15760
16586
  params: [
15761
16587
  MINE_TYPE_PARAM,
15762
16588
  {
@@ -16059,6 +16885,7 @@ const COMMON_METADATA = {
16059
16885
  },
16060
16886
  drawRule: "Area1",
16061
16887
  capturesLabelSize: true,
16888
+ labelGeometryUsesResolvedMetrics: ["N"],
16062
16889
  params: [
16063
16890
  MINE_TYPE_PARAM,
16064
16891
  ...AREA_SMOOTH_PARAMS,
@@ -16081,7 +16908,8 @@ const MINED_AREA_METADATA = {
16081
16908
  name: "Mined Area",
16082
16909
  description: "An area dangerous because of the presence or suspected presence of mines.",
16083
16910
  entityType: "Mined Area",
16084
- value: "270800"
16911
+ value: "270800",
16912
+ labelGeometryUsesResolvedMetrics: true
16085
16913
  };
16086
16914
  function axisIntersections(ring, axis, target) {
16087
16915
  const other = axis === 0 ? 1 : 0;
@@ -16628,6 +17456,7 @@ const PICKUP_ZONE_METADATA = {
16628
17456
  },
16629
17457
  drawRule: "Area1",
16630
17458
  capturesLabelSize: true,
17459
+ labelGeometryUsesResolvedMetrics: ["N"],
16631
17460
  params: PREFIXED_LABELED_AREA_PARAMS,
16632
17461
  textAmplifiers: AREA_TEXT_AMPLIFIERS
16633
17462
  };
@@ -16683,6 +17512,7 @@ const ASSAULT_POSITION_METADATA = {
16683
17512
  },
16684
17513
  drawRule: "Area1",
16685
17514
  capturesLabelSize: true,
17515
+ labelGeometryUsesResolvedMetrics: ["N"],
16686
17516
  params: PREFIXED_LABELED_AREA_PARAMS,
16687
17517
  textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
16688
17518
  };
@@ -16738,6 +17568,7 @@ const ATTACK_POSITION_METADATA = {
16738
17568
  },
16739
17569
  drawRule: "Area1",
16740
17570
  capturesLabelSize: true,
17571
+ labelGeometryUsesResolvedMetrics: ["N"],
16741
17572
  params: PREFIXED_LABELED_AREA_PARAMS,
16742
17573
  textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
16743
17574
  };
@@ -16793,6 +17624,7 @@ const OBJECTIVE_AREA_METADATA = {
16793
17624
  },
16794
17625
  drawRule: "Area1",
16795
17626
  capturesLabelSize: true,
17627
+ labelGeometryUsesResolvedMetrics: ["N"],
16796
17628
  params: PREFIXED_LABELED_AREA_PARAMS,
16797
17629
  textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
16798
17630
  };
@@ -17142,6 +17974,7 @@ const STRONG_POINT_METADATA = {
17142
17974
  },
17143
17975
  drawRule: "Area1",
17144
17976
  capturesLabelSize: true,
17977
+ labelGeometryUsesResolvedMetrics: ["N"],
17145
17978
  params: ECHELON_AREA_PARAMS,
17146
17979
  textAmplifiers: AREA_UNIT_DESIGNATION_HOSTILE_AMPLIFIERS
17147
17980
  };
@@ -17463,6 +18296,7 @@ const SUBMARINE_ACTION_AREA_METADATA = {
17463
18296
  },
17464
18297
  drawRule: "Area1",
17465
18298
  capturesLabelSize: true,
18299
+ labelGeometryUsesResolvedMetrics: ["N"],
17466
18300
  params: PREFIXED_LABELED_AREA_PARAMS,
17467
18301
  textAmplifiers: AREA_TEXT_AMPLIFIERS_NO_ADDITIONAL_INFO
17468
18302
  };
@@ -17523,6 +18357,7 @@ const SUBMARINE_GENERATED_ACTION_AREA_METADATA = {
17523
18357
  },
17524
18358
  drawRule: "Area1",
17525
18359
  capturesLabelSize: true,
18360
+ labelGeometryUsesResolvedMetrics: ["N"],
17526
18361
  params: PREFIXED_LABELED_AREA_PARAMS,
17527
18362
  textAmplifiers: AREA_TEXT_AMPLIFIERS_NO_ADDITIONAL_INFO
17528
18363
  };
@@ -17845,13 +18680,19 @@ const DEFINITIONS = {
17845
18680
  bypass: BYPASS,
17846
18681
  canalize: CANALIZE,
17847
18682
  clear: CLEAR,
18683
+ "cordon-and-knock": CORDON_AND_KNOCK,
18684
+ "cordon-and-search": CORDON_AND_SEARCH,
17848
18685
  counterattack: COUNTERATTACK,
17849
18686
  "counterattack-by-fire": COUNTERATTACK_BY_FIRE,
17850
18687
  cover: COVER,
17851
18688
  delay: DELAY,
18689
+ demonstrate: DEMONSTRATE,
17852
18690
  disengage: DISENGAGE,
17853
18691
  "disrupt-mission-task": DISRUPT_MISSION_TASK,
17854
18692
  envelopment: ENVELOPMENT,
18693
+ exploitation: EXPLOITATION,
18694
+ "follow-and-assume": FOLLOW_AND_ASSUME,
18695
+ "follow-and-support": FOLLOW_AND_SUPPORT,
17855
18696
  "forward-passage-of-lines": FORWARD_PASSAGE_OF_LINES,
17856
18697
  infiltration: INFILTRATION,
17857
18698
  guard: GUARD,
@@ -17861,8 +18702,10 @@ const DEFINITIONS = {
17861
18702
  penetrate: PENETRATE,
17862
18703
  pursuit: PURSUIT,
17863
18704
  "rearward-passage-of-lines": REARWARD_PASSAGE_OF_LINES,
18705
+ "relief-in-place": RELIEF_IN_PLACE,
17864
18706
  retire: RETIRE,
17865
18707
  screen: SCREEN,
18708
+ secure: SECURE,
17866
18709
  seize: SEIZE,
17867
18710
  evacuate: EVACUATE,
17868
18711
  withdraw: WITHDRAW,
@@ -18292,4 +19135,4 @@ function assertNever(value) {
18292
19135
  throw new Error(`Unhandled control measure kind: ${String(value)}`);
18293
19136
  }
18294
19137
  //#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 };
19138
+ export { DEFAULT_PHASE_LINE_OPTIONS as $, getMetersPerPixel as $n, DEFAULT_DIRECTION_OF_SUPPORTING_ATTACK_OPTIONS as $t, DEFAULT_SECURE_OPTIONS as A, staticPointDrawRule as An, DEFAULT_GENERIC_CIRCLE_OPTIONS as At, DEFAULT_FORTIFIED_LINE_OPTIONS as B, computeDefaultMidpointPerpendicularPoint as Bn, DEFAULT_BLOCK_ARROW_OPTIONS as Bt, DEFAULT_LANDING_ZONE_OPTIONS as C, line23DrawRule as Cn, RADAR_SEARCH_DOCTRINE_FILL_COLOR as Ct, DEFAULT_WITHDRAW_OPTIONS as D, attackByFireDrawRule as Dn, DEFAULT_GENERIC_RECTANGLE_OPTIONS as Dt, DEFAULT_WITHDRAW_UNDER_PRESSURE_OPTIONS as E, ambushDrawRule as En, DEFAULT_GENERIC_SECTOR_OPTIONS as Et, DEFAULT_PENETRATE_OPTIONS as F, disruptDrawRule as Fn, DEFAULT_BLOCK_MISSION_TASK_OPTIONS as Ft, DEFAULT_LIMIT_OF_ADVANCE_OPTIONS as G, createBaselineFrame as Gn, DEFAULT_ATTACK_BY_FIRE_OPTIONS as Gt, PROBABLE_LINE_OF_DEPLOYMENT_DASH as H, getMidpointPerpendicularSignedDistance as Hn, DEFAULT_CONTAIN_OPTIONS as Ht, DEFAULT_OCCUPY_OPTIONS as I, blockDrawRule as In, DEFAULT_GENERIC_C2_LINE_OPTIONS as It, DEFAULT_FORWARD_EDGE_OF_BATTLE_AREA_OPTIONS as J, haversineDistance as Jn, DEFAULT_ASSEMBLY_AREA_OPTIONS as Jt, DEFAULT_BATTLE_HANDOVER_LINE_OPTIONS as K, calculateMetrics as Kn, DEFAULT_ANTITANK_WALL_OPTIONS as Kt, DEFAULT_MOVEMENT_TO_CONTACT_OPTIONS as L, centerRadiusDrawRule as Ln, DEFAULT_ENGINEER_WORK_LINE_OPTIONS as Lt, DEFAULT_RETIRE_OPTIONS as M, secureDrawRule as Mn, DEFAULT_CANALIZE_OPTIONS as Mt, DEFAULT_RELIEF_IN_PLACE_OPTIONS as N, penetrateDrawRule as Nn, DEFAULT_BYPASS_OPTIONS as Nt, DEFAULT_EVACUATE_OPTIONS as O, dynamicPointDrawRule as On, DEFAULT_GENERIC_POLYGON_OPTIONS as Ot, DEFAULT_REARWARD_PASSAGE_OF_LINES_OPTIONS as P, occupyDrawRule as Pn, DEFAULT_BREACH_OPTIONS as Pt, DEFAULT_BRIDGEHEAD_LINE_OPTIONS as Q, EPSILON as Qn, DEFAULT_AREA_DEFENSE_OPTIONS as Qt, DEFAULT_FRONTAL_ATTACK_OPTIONS as R, containDrawRule as Rn, DEFAULT_LIGHT_LINE_OPTIONS as Rt, DEFAULT_MAIN_ATTACK_OPTIONS as S, line24DrawRule as Sn, DEFAULT_RADAR_SEARCH_DOCTRINE_OPTIONS as St, DEFAULT_TURN_OPTIONS as T, line1DrawRule as Tn, DEFAULT_GENERIC_TEXT_OPTIONS as Tt, DEFAULT_LINE_OF_DEPARTURE_CONTACT_OPTIONS as U, pointOnMidpointPerpendicularAxis as Un, DEFAULT_BATTLE_POSITION_OPTIONS as Ut, DEFAULT_PROBABLE_LINE_OF_DEPLOYMENT_OPTIONS as V, createMidpointPerpendicularDrawRule as Vn, DEFAULT_BLOCK_OPTIONS as Vt, DEFAULT_LINE_OF_DEPARTURE_OPTIONS as W, snapToMidpointPerpendicular as Wn, DEFAULT_ATTACK_HELICOPTER_OPTIONS as Wt, DEFAULT_HOLDING_LINE_OPTIONS as X, sphericalBearing as Xn, DEFAULT_MOBILE_DEFENSE_OPTIONS as Xt, DEFAULT_RELEASE_LINE_OPTIONS as Y, project as Yn, DEFAULT_AREA_OF_OPERATIONS_OPTIONS as Yt, DEFAULT_INFILTRATION_LANE_OPTIONS as Z, unproject as Zn, DEFAULT_ENGAGEMENT_AREA_OPTIONS as Zt, DEFAULT_PICKUP_ZONE_OPTIONS as _, axis1DrawRule as _n, DEFAULT_SUPPORTING_ATTACK_OPTIONS as _t, DEFINITIONS as a, resolveAmplifierPlacement as an, DEFAULT_DISRUPT_OPTIONS as at, DEFAULT_OBSTACLE_BYPASS_DIFFICULT_OPTIONS as b, line27DrawRule as bn, DEFAULT_ISOLATE_OPTIONS as bt, getDefaultOptions as c, DEFAULT_LINE_CAP as cn, createFollowAndAssume as ct, DEFAULT_SUPPORT_BY_FIRE_OPTIONS as d, DEFAULT_STROKE_DASH_CSS_PIXELS as dn, DEFAULT_DEMONSTRATE_OPTIONS as dt, DEFAULT_DIRECTION_OF_MAIN_ATTACK_OPTIONS as en, roundToFixed as er, DEFAULT_FLOT_OPTIONS as et, DEFAULT_RETAIN_OPTIONS as f, DEFAULT_STROKE_WIDTH_CSS_PIXELS as fn, DEFAULT_DELAY_OPTIONS as ft, DEFAULT_PRINCIPAL_DIRECTION_OF_FIRE_OPTIONS as g, rectangleDrawRule as gn, DEFAULT_COUNTERATTACK_OPTIONS as gt, SECONDARY_DIRECTION_OF_FIRE_DASH as h, DEFAULT_AIRBORNE_ATTACK_OPTIONS as hn, DEFAULT_COUNTERATTACK_BY_FIRE_OPTIONS as ht, CONTROL_MEASURE_METADATA as i, normalizeTextAmplifiers as in, DEFAULT_ENCIRCLEMENT_OPTIONS as it, DEFAULT_SCREEN_OPTIONS as j, point12DrawRule as jn, DEFAULT_CLASSIC_ARROW_OPTIONS as jt, DEFAULT_SEIZE_OPTIONS as k, sectorDrawRule as kn, DEFAULT_GENERIC_LINE_OPTIONS as kt, listControlMeasureMetadata as l, DEFAULT_LINE_JOIN as ln, DEFAULT_DISRUPT_MISSION_TASK_OPTIONS as lt, DEFAULT_SECONDARY_DIRECTION_OF_FIRE_OPTIONS as m, DEFAULT_AMBUSH_OPTIONS as mn, DEFAULT_TACTICAL_ARROW_OPTIONS as mt, resolveStyleHints as n, TEXT_AMPLIFIER_FIELDS as nn, DEFAULT_FIX_OPTIONS as nt, getControlMeasureMetadata as o, DEFAULT_LABEL_HEIGHT_CSS_PIXELS as on, DEFAULT_GUARD_OPTIONS as ot, DEFAULT_STRONG_POINT_OPTIONS as p, DEFAULT_SYMBOL_COLOR as pn, DEFAULT_COVER_OPTIONS as pt, DEFAULT_HANDOVER_LINE_OPTIONS as q, computeInitialWidthPoint as qn, DEFAULT_ANTITANK_DITCH_OPTIONS as qt, CONTROL_MEASURE_IDS as r, canonicalTextAmplifierKey as rn, DEFAULT_FINAL_PROTECTIVE_FIRE_OPTIONS as rt, getControlMeasureMetadataByValue as s, DEFAULT_LABEL_SIZE_CLAMP_CSS_PIXELS as sn, createFollowAndSupport as st, renderControlMeasure as t, DEFAULT_DIRECTION_OF_ATTACK_AVIATION_OPTIONS as tn, DEFAULT_FIX_MISSION_TASK_OPTIONS as tt, DEFAULT_TURNING_MOVEMENT_OPTIONS as u, DEFAULT_PORTRAYAL as un, DEFAULT_DISENGAGE_OPTIONS as ut, DEFAULT_OBSTACLE_BYPASS_IMPOSSIBLE_OPTIONS as v, supportByFireDrawRule as vn, DEFAULT_CORDON_AND_SEARCH_OPTIONS as vt, DEFAULT_JOINT_TACTICAL_ACTION_AREA_OPTIONS as w, turnDrawRule as wn, RADAR_SEARCH_DOCTRINE_STROKE_COLOR as wt, DEFAULT_MINEFIELD_OPTIONS as x, line26DrawRule as xn, DEFAULT_CLEAR_OPTIONS as xt, DEFAULT_OBSTACLE_BYPASS_EASY_OPTIONS as y, polyline1DrawRule as yn, DEFAULT_CORDON_AND_KNOCK_OPTIONS as yt, DEFAULT_FORTIFIED_AREA_OPTIONS as z, area27DrawRule as zn, DEFAULT_BOUNDARY_OPTIONS as zt };