@orbat-mapper/control-measures 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -673,7 +673,7 @@ function frameFor(p1, p2) {
673
673
  }
674
674
  //#endregion
675
675
  //#region src/draw-rules/area1.ts
676
- function derive$14(points) {
676
+ function derive$15(points) {
677
677
  return clonePositions(points);
678
678
  }
679
679
  /**
@@ -693,9 +693,9 @@ const area1DrawRule = {
693
693
  id: "area1",
694
694
  minimumUserPoints: 3,
695
695
  closedRing: true,
696
- derive: derive$14,
696
+ derive: derive$15,
697
697
  transform(event) {
698
- return derive$14(event.next);
698
+ return derive$15(event.next);
699
699
  }
700
700
  };
701
701
  //#endregion
@@ -735,14 +735,14 @@ const containDrawRule = createMidpointPerpendicularDrawRule({
735
735
  * that fixes both the radius and the bearing of the symbol's opening. Both
736
736
  * points are user-clicked, so the canonical array is just the (clamped) input.
737
737
  */
738
- function derive$13(points) {
738
+ function derive$14(points) {
739
739
  return clonePositions(points.slice(0, 2));
740
740
  }
741
741
  const centerRadiusDrawRule = {
742
742
  id: "area15:center-radius",
743
743
  minimumUserPoints: 2,
744
744
  canonicalPointCount: 2,
745
- derive: derive$13,
745
+ derive: derive$14,
746
746
  transform(event) {
747
747
  const { previous, next, activePointIndex } = event;
748
748
  if (activePointIndex === 0 && previous.length >= 2 && next.length >= 2) {
@@ -751,7 +751,7 @@ const centerRadiusDrawRule = {
751
751
  const origin = previous[1];
752
752
  return [clonePosition(next[0]), [origin[0] + dx, origin[1] + dy]];
753
753
  }
754
- return derive$13(next);
754
+ return derive$14(next);
755
755
  }
756
756
  };
757
757
  /** Backward-compatible doctrinal name for Area15's shared center-radius rule. */
@@ -785,6 +785,18 @@ const disruptDrawRule = createMidpointPerpendicularDrawRule({
785
785
  normal: "left"
786
786
  });
787
787
  //#endregion
788
+ //#region src/draw-rules/area16.ts
789
+ /**
790
+ * Area16 — center + start/radius point, mechanically identical to Area15. It
791
+ * re-labels the shared behaviour rather than aliasing it because a definition's
792
+ * `rule.id` must carry its `metadata.drawRule` as a prefix (see the registry
793
+ * invariant in `__tests__/metadata-registry.spec.ts`, ADR-0013).
794
+ */
795
+ const occupyDrawRule = {
796
+ ...centerRadiusDrawRule,
797
+ id: "area16:center-radius"
798
+ };
799
+ //#endregion
788
800
  //#region src/draw-rules/area17.ts
789
801
  /**
790
802
  * Area17 anchor draw rule for Penetrate.
@@ -798,6 +810,72 @@ const penetrateDrawRule = createMidpointPerpendicularDrawRule({
798
810
  minimumPreviewPoints: 2
799
811
  });
800
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
801
879
  //#region src/draw-rules/point12.ts
802
880
  /**
803
881
  * Point12 anchor draw rule for the obstacle-bypass family.
@@ -815,16 +893,16 @@ const point12DrawRule = createMidpointPerpendicularDrawRule({ id: "point12:obsta
815
893
  * Point1 — a single anchor point (e.g. Text's center). One user click commits
816
894
  * the measure; dragging the point simply translates it.
817
895
  */
818
- function derive$12(points) {
896
+ function derive$13(points) {
819
897
  return clonePositions(points.slice(0, 1));
820
898
  }
821
899
  const pointDrawRule = {
822
900
  id: "point1:anchor",
823
901
  minimumUserPoints: 1,
824
902
  canonicalPointCount: 1,
825
- derive: derive$12,
903
+ derive: derive$13,
826
904
  transform(event) {
827
- return derive$12(event.next);
905
+ return derive$13(event.next);
828
906
  }
829
907
  };
830
908
  //#endregion
@@ -851,7 +929,7 @@ const SECTOR_ANGLE_SNAP_RADIANS = Math.PI / 36;
851
929
  * independently control a radius and a true-north azimuth. Dragging the anchor
852
930
  * translates both edge points so the sector retains its size and orientation.
853
931
  */
854
- function derive$11(points) {
932
+ function derive$12(points) {
855
933
  return clonePositions(points.slice(0, 3));
856
934
  }
857
935
  function guidePoints$2(points) {
@@ -879,7 +957,7 @@ const sectorDrawRule = {
879
957
  showGuide: true,
880
958
  guidePoints: guidePoints$2,
881
959
  constrainAngles,
882
- derive: derive$11,
960
+ derive: derive$12,
883
961
  transform(event) {
884
962
  const { previous, next, activePointIndex } = event;
885
963
  if (activePointIndex === 0 && previous.length >= 3 && next.length >= 3) {
@@ -891,7 +969,7 @@ const sectorDrawRule = {
891
969
  [previous[2][0] + dx, previous[2][1] + dy]
892
970
  ];
893
971
  }
894
- return derive$11(next);
972
+ return derive$12(next);
895
973
  }
896
974
  };
897
975
  //#endregion
@@ -945,6 +1023,56 @@ const ambushDrawRule = createMidpointPerpendicularDrawRule({
945
1023
  defaultDistanceRatio: -.5
946
1024
  });
947
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
948
1076
  //#region src/draw-rules/line1.ts
949
1077
  function derive$10(points) {
950
1078
  return clonePositions(points);
@@ -1362,6 +1490,23 @@ const line32DrawRule = {
1362
1490
  }
1363
1491
  };
1364
1492
  //#endregion
1493
+ //#region src/draw-rules/line33.ts
1494
+ /**
1495
+ * Line33 anchor draw rule for the Pursuit mission task.
1496
+ *
1497
+ * PT. 1 and PT. 2 define the straight portion. PT. 3 is constrained to the
1498
+ * perpendicular axis through PT. 2, where its signed distance determines the
1499
+ * semicircular arc's diameter and which side of the straight line it occupies.
1500
+ */
1501
+ const line33DrawRule = createMidpointPerpendicularDrawRule({
1502
+ id: "line33:pursuit",
1503
+ origin: "p2",
1504
+ defaultDistanceRatio: .6,
1505
+ minimumUserPoints: 3,
1506
+ minimumPreviewPoints: 2,
1507
+ finishOnLastPointRepeat: true
1508
+ });
1509
+ //#endregion
1365
1510
  //#region src/draw-rules/polyline1.ts
1366
1511
  /**
1367
1512
  * Polyline1 anchors for the Infiltration Lane.
@@ -2986,6 +3131,70 @@ function labelBoxIntersectsSegment(anchor, a, b, textWidth, textHeight, rotation
2986
3131
  }
2987
3132
  return true;
2988
3133
  }
3134
+ /**
3135
+ * Splits the polyline `points` into the pieces that fall outside a
3136
+ * `2*halfWidth × 2*halfHeight` label box centered at `anchor` with stored
3137
+ * `rotation` — the clipping counterpart to {@link labelBoxIntersectsSegment},
3138
+ * for ornament geometry (barbs, ticks) that must break around a glyph rather
3139
+ * than merely decide whether it collides.
3140
+ *
3141
+ * Each segment is clipped in the box's own frame with a two-slab parametric
3142
+ * test; pieces that rejoin across a shared vertex are merged so a polyline
3143
+ * crossing the box mid-span stays one line string per surviving run instead of
3144
+ * fragmenting at every interior vertex.
3145
+ */
3146
+ function clipPolylineOutsideLabelBox(points, anchor, halfWidth, halfHeight, rotation) {
3147
+ const [widthAxis, heightAxis] = labelBoxAxes(rotation);
3148
+ const out = [];
3149
+ const append = (piece) => {
3150
+ if (piece.length < 2 || vecMag(vecSub(piece[1], piece[0])) < 1e-6) return;
3151
+ const previous = out.at(-1);
3152
+ if (previous && vecMag(vecSub(previous.at(-1), piece[0])) < 1e-6) previous.push(...piece.slice(1));
3153
+ else out.push(piece);
3154
+ };
3155
+ for (let i = 1; i < points.length; i++) {
3156
+ const a = points[i - 1];
3157
+ const b = points[i];
3158
+ const delta = vecSub(b, a);
3159
+ const relative = vecSub(a, anchor);
3160
+ const origin = [vecDot(relative, widthAxis), vecDot(relative, heightAxis)];
3161
+ const direction = [vecDot(delta, widthAxis), vecDot(delta, heightAxis)];
3162
+ let enter = 0;
3163
+ let exit = 1;
3164
+ let intersects = true;
3165
+ for (const [coordinate, rate, halfExtent] of [[
3166
+ origin[0],
3167
+ direction[0],
3168
+ halfWidth
3169
+ ], [
3170
+ origin[1],
3171
+ direction[1],
3172
+ halfHeight
3173
+ ]]) {
3174
+ if (Math.abs(rate) < 1e-6) {
3175
+ if (Math.abs(coordinate) > halfExtent) intersects = false;
3176
+ continue;
3177
+ }
3178
+ const first = (-halfExtent - coordinate) / rate;
3179
+ const second = (halfExtent - coordinate) / rate;
3180
+ enter = Math.max(enter, Math.min(first, second));
3181
+ exit = Math.min(exit, Math.max(first, second));
3182
+ if (enter > exit) intersects = false;
3183
+ }
3184
+ if (!intersects || exit < 0 || enter > 1) {
3185
+ append([a, b]);
3186
+ continue;
3187
+ }
3188
+ const clippedEnter = Math.max(0, enter);
3189
+ const clippedExit = Math.min(1, exit);
3190
+ const pointAt = (t) => t <= 0 ? a : t >= 1 ? b : vecAdd(a, vecScale(delta, t));
3191
+ if (clippedEnter > 0) append([a, pointAt(clippedEnter)]);
3192
+ if (clippedExit < 1) append([pointAt(clippedExit), b]);
3193
+ }
3194
+ return out;
3195
+ }
3196
+ /** Default extra clearance for an intrinsic label that interrupts a stroke. */
3197
+ const DEFAULT_LABEL_KNOCKOUT_PADDING = .3;
2989
3198
  /** Field N descriptor shared by Line1 graphics that opt into hostile endpoint markers. */
2990
3199
  const HOSTILE_LINE_AMPLIFIER = {
2991
3200
  key: "N",
@@ -2994,12 +3203,12 @@ const HOSTILE_LINE_AMPLIFIER = {
2994
3203
  placeholder: "ENY",
2995
3204
  maxLength: 3
2996
3205
  };
2997
- /** Label-padding control shared by newly label-bearing Line1 graphics. */
2998
- const LINE_LABEL_PADDING_PARAM = {
3206
+ /** Label-padding control shared by label-bearing graphics. */
3207
+ const LABEL_PADDING_PARAM = {
2999
3208
  key: "labelPadding",
3000
3209
  presentationTier: "advanced",
3001
3210
  label: "Label padding",
3002
- description: "Extra clearance between the line and its labels, as a ratio of the label height",
3211
+ description: "Extra clearance around labels, as a ratio of the label height",
3003
3212
  type: "number",
3004
3213
  min: 0,
3005
3214
  max: 2,
@@ -3107,8 +3316,8 @@ function estimatedTextWidth(text, textHeightMeters) {
3107
3316
  return text.length * LABEL_CHAR_ASPECT_RATIO * textHeightMeters;
3108
3317
  }
3109
3318
  /** Resolve the label's final display size, then measure and convert it to construction metres. */
3110
- function resolveTextMetrics(text, options, context, style = "regular") {
3111
- const legacyHeight = resolveLabelOffsetMeters(options, 1);
3319
+ function resolveTextMetrics(text, options, context, style = "regular", fallbackHeightMeters = 50) {
3320
+ const legacyHeight = resolveLabelOffsetMeters(options, 1, fallbackHeightMeters);
3112
3321
  const constructionScale = context?.constructionMetersPerCssPixel;
3113
3322
  if (!(constructionScale !== void 0 && constructionScale > 0)) return {
3114
3323
  width: estimatedTextWidth(text, legacyHeight),
@@ -3139,6 +3348,28 @@ function resolveTextMetrics(text, options, context, style = "regular") {
3139
3348
  height: measured.heightCssPixels * constructionScale
3140
3349
  };
3141
3350
  }
3351
+ /**
3352
+ * Half-length of the knockout required for a label along `direction`.
3353
+ *
3354
+ * The text box itself is always cleared; `labelPadding` adds symmetric clearance expressed in
3355
+ * resolved label heights. Keeping this calculation here gives every generator the same public
3356
+ * label-clearance interface while hiding font measurement and rotated-box projection details.
3357
+ */
3358
+ function labelKnockoutHalfExtent(text, direction, rotation, options, context, defaultPadding = 0) {
3359
+ const metrics = resolveTextMetrics(text, options, context);
3360
+ const padding = Math.max(0, options.labelPadding ?? defaultPadding) * metrics.height;
3361
+ return labelHalfExtent(direction, metrics.width, metrics.height, rotation) + padding;
3362
+ }
3363
+ /**
3364
+ * {@link labelKnockoutHalfExtent} expressed as a fraction of an arc's angular
3365
+ * sweep, for generators that carve their knockout in arc-progress space rather
3366
+ * than in meters. Clamped to `0.5` so the gap can never exceed the arc.
3367
+ */
3368
+ function arcKnockoutHalfFraction(halfExtentMeters, radius, sweepAngle) {
3369
+ const arcLength = radius * Math.abs(sweepAngle);
3370
+ if (!(arcLength > 1e-6)) return .5;
3371
+ return Math.min(.5, Math.max(0, halfExtentMeters) / arcLength);
3372
+ }
3142
3373
  /** The end-label text for a phase-line naming, or `""` when unnamed. */
3143
3374
  function phaseLineLabelText(name, includePrefix) {
3144
3375
  if (!name) return "";
@@ -3603,6 +3834,7 @@ const DIRECTION_OF_ATTACK_AVIATION_METADATA = {
3603
3834
  },
3604
3835
  drawRule: "Line1",
3605
3836
  capturesLabelSize: true,
3837
+ labelGeometryUsesResolvedMetrics: ["T"],
3606
3838
  params: [
3607
3839
  ...SMOOTH_LINE_PARAMS,
3608
3840
  {
@@ -3841,6 +4073,7 @@ const DIRECTION_OF_MAIN_ATTACK_METADATA = {
3841
4073
  },
3842
4074
  drawRule: "Line1",
3843
4075
  capturesLabelSize: true,
4076
+ labelGeometryUsesResolvedMetrics: ["T"],
3844
4077
  params: [
3845
4078
  ...SMOOTH_LINE_PARAMS,
3846
4079
  ...DIRECTION_OF_ATTACK_PARAMS,
@@ -3909,6 +4142,7 @@ const DIRECTION_OF_SUPPORTING_ATTACK_METADATA = {
3909
4142
  },
3910
4143
  drawRule: "Line1",
3911
4144
  capturesLabelSize: true,
4145
+ labelGeometryUsesResolvedMetrics: ["T"],
3912
4146
  params: [...SMOOTH_LINE_PARAMS, ...DIRECTION_OF_ATTACK_PARAMS],
3913
4147
  textAmplifiers: DIRECTION_OF_ATTACK_TEXT_AMPLIFIERS
3914
4148
  };
@@ -4539,6 +4773,7 @@ const AREA_METADATA = {
4539
4773
  },
4540
4774
  drawRule: "Area1",
4541
4775
  capturesLabelSize: true,
4776
+ labelGeometryUsesResolvedMetrics: ["N"],
4542
4777
  params: LABELED_AREA_PARAMS,
4543
4778
  textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
4544
4779
  };
@@ -4569,7 +4804,7 @@ const AREA = defineControlMeasure({
4569
4804
  //#endregion
4570
4805
  //#region src/generators/cm15-maneuver-areas/area-defense.ts
4571
4806
  const DEFAULT_AREA_DEFENSE_OPTIONS = {
4572
- arrowOpeningAngle: 30,
4807
+ openingAngle: 30,
4573
4808
  labelOpeningAngle: 20,
4574
4809
  barbCount: 6,
4575
4810
  barbLengthRatio: .16,
@@ -4599,7 +4834,7 @@ const AREA_DEFENSE_METADATA = {
4599
4834
  capturesLabelSize: true,
4600
4835
  params: [
4601
4836
  {
4602
- key: "arrowOpeningAngle",
4837
+ key: "openingAngle",
4603
4838
  presentationTier: "advanced",
4604
4839
  label: "Arrow opening",
4605
4840
  description: "Angular opening between the two arrow tips, in degrees",
@@ -4680,7 +4915,7 @@ function createAreaDefense(coordinates, options = {}) {
4680
4915
  features: []
4681
4916
  };
4682
4917
  const upperTipAngle = Math.atan2(radiusVector[1], radiusVector[0]);
4683
- const arrowHalf = Math.min(120, Math.max(5, resolved.arrowOpeningAngle)) * Math.PI / 360;
4918
+ const arrowHalf = Math.min(120, Math.max(5, resolved.openingAngle)) * Math.PI / 360;
4684
4919
  const labelHalf = Math.min(90, Math.max(5, resolved.labelOpeningAngle)) * Math.PI / 360;
4685
4920
  const labelCenter = upperTipAngle + arrowHalf - Math.PI;
4686
4921
  const upperStart = labelCenter + labelHalf;
@@ -4802,6 +5037,7 @@ const ENGAGEMENT_AREA_METADATA = {
4802
5037
  },
4803
5038
  drawRule: "Area1",
4804
5039
  capturesLabelSize: true,
5040
+ labelGeometryUsesResolvedMetrics: ["N"],
4805
5041
  params: PREFIXED_LABELED_AREA_PARAMS,
4806
5042
  textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
4807
5043
  };
@@ -4868,6 +5104,26 @@ function sampleProjectedArc(center, startRadius, bulgeRadius, start, end, resolu
4868
5104
  return unproject(point[0], point[1]);
4869
5105
  });
4870
5106
  }
5107
+ /**
5108
+ * Samples the 180-degree arc standing on the diameter `start`→`end`, bulging
5109
+ * toward `bulgeDir`. Shared by the mission tasks whose shaft turns back on
5110
+ * itself (Delay, Relief in Place, Demonstrate); `requestedSegments` is the
5111
+ * segment count for the half-turn.
5112
+ */
5113
+ function createSemicircleArc(start, end, bulgeDir, requestedSegments) {
5114
+ const diameterVector = vecSub(end, start);
5115
+ const diameter = vecMag(diameterVector);
5116
+ if (diameter < 1e-6) return [unproject(start[0], start[1])];
5117
+ const radius = diameter / 2;
5118
+ const center = vecAdd(start, vecScale(diameterVector, .5));
5119
+ const startRadius = vecScale(diameterVector, -.5);
5120
+ const bulgeRadius = vecScale(bulgeDir, radius);
5121
+ const segments = Math.max(4, Math.round(requestedSegments));
5122
+ return Array.from({ length: segments + 1 }, (_, index) => {
5123
+ const point = arcPointAt(center, startRadius, bulgeRadius, index / segments * Math.PI);
5124
+ return unproject(point[0], point[1]);
5125
+ });
5126
+ }
4871
5127
  //#endregion
4872
5128
  //#region src/generators/cm15-maneuver-areas/mobile-defense.ts
4873
5129
  const DEFAULT_MOBILE_DEFENSE_OPTIONS = { labelPadding: 0 };
@@ -4893,6 +5149,7 @@ const MOBILE_DEFENSE_METADATA = {
4893
5149
  },
4894
5150
  drawRule: "Area27",
4895
5151
  capturesLabelSize: true,
5152
+ labelGeometryUsesResolvedMetrics: true,
4896
5153
  params: [{
4897
5154
  key: "labelPadding",
4898
5155
  presentationTier: "advanced",
@@ -5069,6 +5326,7 @@ const AREA_OF_OPERATIONS_METADATA = {
5069
5326
  },
5070
5327
  drawRule: "Area1",
5071
5328
  capturesLabelSize: true,
5329
+ labelGeometryUsesResolvedMetrics: ["N"],
5072
5330
  params: PREFIXED_LABELED_AREA_PARAMS,
5073
5331
  textAmplifiers: AREA_TEXT_AMPLIFIERS
5074
5332
  };
@@ -5131,6 +5389,7 @@ const NAMED_AREA_OF_INTEREST_METADATA = {
5131
5389
  },
5132
5390
  drawRule: "Area1",
5133
5391
  capturesLabelSize: true,
5392
+ labelGeometryUsesResolvedMetrics: ["N"],
5134
5393
  params: PREFIXED_LABELED_AREA_PARAMS,
5135
5394
  textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
5136
5395
  };
@@ -5190,6 +5449,7 @@ const TARGET_AREA_OF_INTEREST_METADATA = {
5190
5449
  },
5191
5450
  drawRule: "Area1",
5192
5451
  capturesLabelSize: true,
5452
+ labelGeometryUsesResolvedMetrics: ["N"],
5193
5453
  params: PREFIXED_LABELED_AREA_PARAMS,
5194
5454
  textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
5195
5455
  };
@@ -5258,6 +5518,7 @@ const AIRFIELD_ZONE_METADATA = {
5258
5518
  },
5259
5519
  drawRule: "Area1",
5260
5520
  capturesLabelSize: true,
5521
+ labelGeometryUsesResolvedMetrics: ["N"],
5261
5522
  params: [
5262
5523
  ...AREA_SMOOTH_PARAMS,
5263
5524
  {
@@ -5442,6 +5703,7 @@ const BASE_CAMP_METADATA = {
5442
5703
  },
5443
5704
  drawRule: "Area1",
5444
5705
  capturesLabelSize: true,
5706
+ labelGeometryUsesResolvedMetrics: ["N"],
5445
5707
  params: PREFIXED_ECHELON_AREA_PARAMS,
5446
5708
  textAmplifiers: AREA_TEXT_AMPLIFIERS
5447
5709
  };
@@ -5500,6 +5762,7 @@ const GUERRILLA_BASE_METADATA = {
5500
5762
  },
5501
5763
  drawRule: "Area1",
5502
5764
  capturesLabelSize: true,
5765
+ labelGeometryUsesResolvedMetrics: ["N"],
5503
5766
  params: PREFIXED_ECHELON_AREA_PARAMS,
5504
5767
  textAmplifiers: AREA_TEXT_AMPLIFIERS
5505
5768
  };
@@ -5563,6 +5826,7 @@ const GENERIC_C2_AREA_METADATA = {
5563
5826
  },
5564
5827
  drawRule: "Area1",
5565
5828
  capturesLabelSize: true,
5829
+ labelGeometryUsesResolvedMetrics: ["N"],
5566
5830
  params: LABELED_AREA_PARAMS,
5567
5831
  textAmplifiers: AREA_TEXT_AMPLIFIERS
5568
5832
  };
@@ -5618,6 +5882,7 @@ const ASSEMBLY_AREA_METADATA = {
5618
5882
  },
5619
5883
  drawRule: "Area1",
5620
5884
  capturesLabelSize: true,
5885
+ labelGeometryUsesResolvedMetrics: ["N"],
5621
5886
  params: PREFIXED_LABELED_AREA_PARAMS,
5622
5887
  textAmplifiers: AREA_TEXT_AMPLIFIERS
5623
5888
  };
@@ -5669,7 +5934,7 @@ const FORTIFIED_PARAMS = [
5669
5934
  step: 1,
5670
5935
  unit: "m"
5671
5936
  },
5672
- LINE_LABEL_PADDING_PARAM
5937
+ LABEL_PADDING_PARAM
5673
5938
  ];
5674
5939
  const ANTITANK_DITCH_PARAMS = [
5675
5940
  ...SMOOTH_LINE_PARAMS,
@@ -5704,7 +5969,7 @@ const ANTITANK_DITCH_PARAMS = [
5704
5969
  step: .05,
5705
5970
  unit: "x"
5706
5971
  },
5707
- LINE_LABEL_PADDING_PARAM
5972
+ LABEL_PADDING_PARAM
5708
5973
  ];
5709
5974
  const ANTITANK_WALL_PARAMS = [...ANTITANK_DITCH_PARAMS, {
5710
5975
  key: "toothSpacingRatio",
@@ -5747,6 +6012,7 @@ const ANTITANK_DITCH_UNDER_CONSTRUCTION_METADATA = {
5747
6012
  },
5748
6013
  drawRule: "Line1",
5749
6014
  capturesLabelSize: true,
6015
+ labelGeometryUsesResolvedMetrics: ["N"],
5750
6016
  params: ANTITANK_DITCH_PARAMS,
5751
6017
  textAmplifiers: [HOSTILE_LINE_AMPLIFIER]
5752
6018
  };
@@ -5768,6 +6034,7 @@ const ANTITANK_DITCH_COMPLETED_METADATA = {
5768
6034
  },
5769
6035
  drawRule: "Line1",
5770
6036
  capturesLabelSize: true,
6037
+ labelGeometryUsesResolvedMetrics: ["N"],
5771
6038
  params: ANTITANK_DITCH_PARAMS,
5772
6039
  textAmplifiers: [HOSTILE_LINE_AMPLIFIER]
5773
6040
  };
@@ -5900,6 +6167,7 @@ const ANTITANK_WALL_METADATA = {
5900
6167
  },
5901
6168
  drawRule: "Line1",
5902
6169
  capturesLabelSize: true,
6170
+ labelGeometryUsesResolvedMetrics: ["N"],
5903
6171
  params: ANTITANK_WALL_PARAMS,
5904
6172
  textAmplifiers: [HOSTILE_LINE_AMPLIFIER]
5905
6173
  };
@@ -6116,7 +6384,7 @@ const DEFAULT_ATTACK_HELICOPTER_OPTIONS = {
6116
6384
  symbolHeightRatio: .45,
6117
6385
  triangleSizeRatio: .25,
6118
6386
  bottomBarWidthRatio: 1.5,
6119
- bowtieWidthRatio: .8
6387
+ bowTieWidthRatio: .8
6120
6388
  };
6121
6389
  const ATTACK_HELICOPTER_METADATA = {
6122
6390
  id: "attack-helicopter",
@@ -6168,7 +6436,7 @@ const ATTACK_HELICOPTER_METADATA = {
6168
6436
  step: .1
6169
6437
  },
6170
6438
  {
6171
- key: "bowtieWidthRatio",
6439
+ key: "bowTieWidthRatio",
6172
6440
  presentationTier: "advanced",
6173
6441
  label: "Bowtie width",
6174
6442
  description: "Size of the central bowtie relative to the reference triangle.",
@@ -6237,7 +6505,7 @@ function createAttackHelicopter(coordinates, options = {}) {
6237
6505
  const symbolHeightRatio = options.symbolHeightRatio ?? DEFAULT_ATTACK_HELICOPTER_OPTIONS.symbolHeightRatio;
6238
6506
  const triangleSizeRatio = options.triangleSizeRatio ?? DEFAULT_ATTACK_HELICOPTER_OPTIONS.triangleSizeRatio;
6239
6507
  const bottomBarWidthRatio = options.bottomBarWidthRatio ?? DEFAULT_ATTACK_HELICOPTER_OPTIONS.bottomBarWidthRatio;
6240
- const bowtieWidthRatio = options.bowtieWidthRatio ?? DEFAULT_ATTACK_HELICOPTER_OPTIONS.bowtieWidthRatio;
6508
+ const bowTieWidthRatio = options.bowTieWidthRatio ?? DEFAULT_ATTACK_HELICOPTER_OPTIONS.bowTieWidthRatio;
6241
6509
  const baseUnit = headWidth * .45;
6242
6510
  const shaftLength = headWidth * symbolHeightRatio;
6243
6511
  const triHeight = baseUnit * triangleSizeRatio;
@@ -6283,7 +6551,7 @@ function createAttackHelicopter(coordinates, options = {}) {
6283
6551
  coordinates: [unproject(pBarLeft[0], pBarLeft[1]), unproject(pBarRight[0], pBarRight[1])]
6284
6552
  }
6285
6553
  };
6286
- const bowTieSize = refTriHeight * bowtieWidthRatio;
6554
+ const bowTieSize = refTriHeight * bowTieWidthRatio;
6287
6555
  const bowTieHalfWidth = bowTieSize * .6;
6288
6556
  const pBowTieRightBaseCenter = vecAdd(center, vecScale(u, bowTieSize));
6289
6557
  const pBowTieRightTop = vecAdd(pBowTieRightBaseCenter, vecScale(n, bowTieHalfWidth));
@@ -6366,6 +6634,7 @@ const BATTLE_POSITION_METADATA = {
6366
6634
  },
6367
6635
  drawRule: "Area1",
6368
6636
  capturesLabelSize: true,
6637
+ labelGeometryUsesResolvedMetrics: ["N"],
6369
6638
  params: ECHELON_AREA_PARAMS,
6370
6639
  textAmplifiers: AREA_UNIT_DESIGNATION_HOSTILE_AMPLIFIERS
6371
6640
  };
@@ -6509,6 +6778,7 @@ const BATTLE_POSITION_PREPARED_METADATA = {
6509
6778
  },
6510
6779
  drawRule: "Area1",
6511
6780
  capturesLabelSize: true,
6781
+ labelGeometryUsesResolvedMetrics: ["N"],
6512
6782
  params: PREFIXED_ECHELON_AREA_PARAMS,
6513
6783
  textAmplifiers: AREA_UNIT_DESIGNATION_HOSTILE_AMPLIFIERS
6514
6784
  };
@@ -6577,6 +6847,7 @@ const CONTAIN_METADATA = {
6577
6847
  },
6578
6848
  drawRule: "Area5",
6579
6849
  capturesLabelSize: true,
6850
+ labelGeometryUsesResolvedMetrics: true,
6580
6851
  textAmplifiers: [AREA_HOSTILE_AMPLIFIER],
6581
6852
  params: [{
6582
6853
  key: "labelPadding",
@@ -7472,6 +7743,7 @@ const BOUNDARY_METADATA = {
7472
7743
  text: true
7473
7744
  },
7474
7745
  drawRule: "Line1",
7746
+ labelGeometryUsesResolvedMetrics: ["N"],
7475
7747
  params: [
7476
7748
  {
7477
7749
  key: "smooth",
@@ -7890,6 +8162,7 @@ const GENERIC_C2_LINE_METADATA = {
7890
8162
  },
7891
8163
  drawRule: "Line1",
7892
8164
  capturesLabelSize: true,
8165
+ labelGeometryUsesResolvedMetrics: ["N"],
7893
8166
  params: [
7894
8167
  ...SMOOTH_LINE_PARAMS,
7895
8168
  ...echelonLineParams("line"),
@@ -7911,7 +8184,7 @@ const GENERIC_C2_LINE_METADATA = {
7911
8184
  type: "boolean",
7912
8185
  visibleWhen: (opts) => Boolean(String(opts.phaseLineName ?? "").trim())
7913
8186
  },
7914
- LINE_LABEL_PADDING_PARAM
8187
+ LABEL_PADDING_PARAM
7915
8188
  ],
7916
8189
  textAmplifiers: [
7917
8190
  {
@@ -8138,7 +8411,7 @@ function createLabelFeature(point, text, rotation, labelSize = {}) {
8138
8411
  * @param config - Per-measure end caps, label glyph, and sizing
8139
8412
  * @returns GeoJSON FeatureCollection containing MultiLineString and Point features
8140
8413
  */
8141
- function createBracketSymbol(coordinates, config) {
8414
+ function createBracketSymbol(coordinates, config, context) {
8142
8415
  const [c1, c2, c3] = coordinates;
8143
8416
  const p1 = project(c1[0], c1[1]);
8144
8417
  const p2 = project(c2[0], c2[1]);
@@ -8158,7 +8431,10 @@ function createBracketSymbol(coordinates, config) {
8158
8431
  const rearMidpoint = vecAdd(frontMidpoint, vecScale(perpDir, perpDistance));
8159
8432
  const rearTop = vecSub(rearMidpoint, halfFront);
8160
8433
  const rearBottom = vecAdd(rearMidpoint, halfFront);
8161
- const gapHalf = frontLength * config.labelGapRatio / 2;
8434
+ const rearLineRotation = Math.atan2(dirFront[1], dirFront[0]);
8435
+ const renderedLabelRotation = Math.PI - (rearLineRotation - Math.PI / 2);
8436
+ const labelRotation = normalizeRadians(Math.PI - keepTextLeftToRight(renderedLabelRotation));
8437
+ const gapHalf = Math.min(frontLength / 2, labelKnockoutHalfExtent(config.labelText, dirFront, labelRotation, config.labelOptions ?? {}, context));
8162
8438
  const gapPt1 = vecAdd(rearMidpoint, vecScale(dirFront, -gapHalf));
8163
8439
  const gapPt2 = vecAdd(rearMidpoint, vecScale(dirFront, gapHalf));
8164
8440
  const [topCap, bottomCap] = config.buildEndCaps({
@@ -8169,39 +8445,35 @@ function createBracketSymbol(coordinates, config) {
8169
8445
  frontLength,
8170
8446
  rearTop
8171
8447
  });
8172
- const mainFeature = {
8173
- type: "Feature",
8174
- properties: { part: config.part },
8175
- geometry: {
8176
- type: "MultiLineString",
8177
- coordinates: [
8178
- topCap,
8179
- [
8180
- unproject(p1[0], p1[1]),
8181
- unproject(rearTop[0], rearTop[1]),
8182
- unproject(gapPt1[0], gapPt1[1])
8183
- ],
8184
- [
8185
- unproject(gapPt2[0], gapPt2[1]),
8186
- unproject(rearBottom[0], rearBottom[1]),
8187
- unproject(p2[0], p2[1])
8188
- ],
8189
- bottomCap
8190
- ]
8191
- }
8192
- };
8193
- const rearLineRotation = Math.atan2(dirFront[1], dirFront[0]);
8194
- const renderedLabelRotation = Math.PI - (rearLineRotation - Math.PI / 2);
8195
- const labelRotation = normalizeRadians(Math.PI - keepTextLeftToRight(renderedLabelRotation));
8196
8448
  return {
8197
8449
  type: "FeatureCollection",
8198
- features: [mainFeature, createLabelFeature(rearMidpoint, config.labelText, labelRotation, config.labelSize)]
8450
+ features: [{
8451
+ type: "Feature",
8452
+ properties: { part: config.part },
8453
+ geometry: {
8454
+ type: "MultiLineString",
8455
+ coordinates: [
8456
+ topCap,
8457
+ [
8458
+ unproject(p1[0], p1[1]),
8459
+ unproject(rearTop[0], rearTop[1]),
8460
+ unproject(gapPt1[0], gapPt1[1])
8461
+ ],
8462
+ [
8463
+ unproject(gapPt2[0], gapPt2[1]),
8464
+ unproject(rearBottom[0], rearBottom[1]),
8465
+ unproject(p2[0], p2[1])
8466
+ ],
8467
+ bottomCap
8468
+ ]
8469
+ }
8470
+ }, createLabelFeature(rearMidpoint, config.labelText, labelRotation, config.labelOptions)]
8199
8471
  };
8200
8472
  }
8201
8473
  //#endregion
8202
8474
  //#region src/generators/cm34-mission-tasks/block.ts
8203
8475
  const DEFAULT_STEM_RATIO = 1.5;
8204
- const DEFAULT_BLOCK_MISSION_TASK_OPTIONS = { labelGapRatio: .2 };
8476
+ const DEFAULT_BLOCK_MISSION_TASK_OPTIONS = { labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING };
8205
8477
  const BLOCK_MISSION_TASK_METADATA = {
8206
8478
  id: "block-mission-task",
8207
8479
  name: "Block",
@@ -8219,16 +8491,8 @@ const BLOCK_MISSION_TASK_METADATA = {
8219
8491
  },
8220
8492
  drawRule: "Area11",
8221
8493
  capturesLabelSize: true,
8222
- params: [{
8223
- key: "labelGapRatio",
8224
- presentationTier: "advanced",
8225
- label: "Label gap",
8226
- description: "Size of the shaft gap holding the 'B' label, as a ratio of the front length",
8227
- type: "number",
8228
- min: 0,
8229
- max: 1,
8230
- step: .01
8231
- }]
8494
+ labelGeometryUsesResolvedMetrics: true,
8495
+ params: [LABEL_PADDING_PARAM]
8232
8496
  };
8233
8497
  /**
8234
8498
  * Generates a GeoJSON FeatureCollection for a BLOCK mission task symbol (340100).
@@ -8240,8 +8504,7 @@ const BLOCK_MISSION_TASK_METADATA = {
8240
8504
  * @param options - Configuration options
8241
8505
  * @returns GeoJSON FeatureCollection containing MultiLineString and Point features
8242
8506
  */
8243
- function createBlockMissionTaskSymbol(coordinates, options = {}) {
8244
- const labelGapRatio = options.labelGapRatio ?? DEFAULT_BLOCK_MISSION_TASK_OPTIONS.labelGapRatio;
8507
+ function createBlockMissionTaskSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
8245
8508
  const p1 = project(coordinates[0][0], coordinates[0][1]);
8246
8509
  const p2 = project(coordinates[1][0], coordinates[1][1]);
8247
8510
  const frame = createProjectedBaselineFrame(p1, p2, {
@@ -8258,12 +8521,12 @@ function createBlockMissionTaskSymbol(coordinates, options = {}) {
8258
8521
  const pStemEnd = vecAdd(mid, vecScale(frame.normal, stemDistance));
8259
8522
  const labelPoint = vecAdd(mid, vecScale(frame.normal, stemDistance / 2));
8260
8523
  const stemDirection = vecScale(frame.normal, stemDistance < 0 ? -1 : 1);
8261
- const gapHalf = Math.min(frame.length * labelGapRatio / 2, Math.abs(stemDistance) / 2);
8262
- const gapStart = vecAdd(labelPoint, vecScale(stemDirection, -gapHalf));
8263
- const gapEnd = vecAdd(labelPoint, vecScale(stemDirection, gapHalf));
8264
8524
  const shaftRotation = Math.atan2(frame.normal[1], frame.normal[0]);
8265
8525
  const renderedLabelRotation = Math.PI - shaftRotation;
8266
8526
  const labelRotation = normalizeRadians(Math.PI - keepTextLeftToRight(renderedLabelRotation));
8527
+ const gapHalf = Math.min(labelKnockoutHalfExtent("B", stemDirection, labelRotation, options, context, DEFAULT_BLOCK_MISSION_TASK_OPTIONS.labelPadding), Math.abs(stemDistance) / 2);
8528
+ const gapStart = vecAdd(labelPoint, vecScale(stemDirection, -gapHalf));
8529
+ const gapEnd = vecAdd(labelPoint, vecScale(stemDirection, gapHalf));
8267
8530
  return {
8268
8531
  type: "FeatureCollection",
8269
8532
  features: [{
@@ -8298,7 +8561,7 @@ const BLOCK_MISSION_TASK = defineControlMeasure({
8298
8561
  const DEFAULT_BREACH_OPTIONS = {
8299
8562
  tickLengthRatio: .15,
8300
8563
  tickAngle: 45,
8301
- labelGapRatio: .2
8564
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING
8302
8565
  };
8303
8566
  const BREACH_METADATA = {
8304
8567
  id: "breach",
@@ -8318,6 +8581,7 @@ const BREACH_METADATA = {
8318
8581
  },
8319
8582
  drawRule: "Point12",
8320
8583
  capturesLabelSize: true,
8584
+ labelGeometryUsesResolvedMetrics: true,
8321
8585
  params: [
8322
8586
  {
8323
8587
  key: "tickLengthRatio",
@@ -8339,16 +8603,7 @@ const BREACH_METADATA = {
8339
8603
  max: 90,
8340
8604
  step: 1
8341
8605
  },
8342
- {
8343
- key: "labelGapRatio",
8344
- presentationTier: "advanced",
8345
- label: "Label gap",
8346
- description: "Size of the rear-line gap holding the 'B' label, as a ratio of the front length",
8347
- type: "number",
8348
- min: 0,
8349
- max: 1,
8350
- step: .01
8351
- }
8606
+ LABEL_PADDING_PARAM
8352
8607
  ]
8353
8608
  };
8354
8609
  /**
@@ -8362,16 +8617,15 @@ const BREACH_METADATA = {
8362
8617
  * @param options - Configuration options
8363
8618
  * @returns GeoJSON FeatureCollection containing MultiLineString and Point features
8364
8619
  */
8365
- function createBreachSymbol(coordinates, options = {}) {
8620
+ function createBreachSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
8366
8621
  const tickLengthRatio = options.tickLengthRatio ?? DEFAULT_BREACH_OPTIONS.tickLengthRatio;
8367
8622
  const tickAngle = options.tickAngle ?? DEFAULT_BREACH_OPTIONS.tickAngle;
8368
8623
  return createBracketSymbol(coordinates, {
8369
8624
  part: "breach",
8370
8625
  labelText: "B",
8371
- labelGapRatio: options.labelGapRatio ?? DEFAULT_BREACH_OPTIONS.labelGapRatio,
8372
- labelSize: {
8373
- labelSize: options.labelSize,
8374
- labelSizePixels: options.labelSizePixels
8626
+ labelOptions: {
8627
+ ...options,
8628
+ labelPadding: options.labelPadding ?? DEFAULT_BREACH_OPTIONS.labelPadding
8375
8629
  },
8376
8630
  buildEndCaps: ({ p1, p2, dirFront, perpDir, frontLength }) => {
8377
8631
  const halfTick = frontLength * tickLengthRatio / 2;
@@ -8387,7 +8641,7 @@ function createBreachSymbol(coordinates, options = {}) {
8387
8641
  const tick2End = vecAdd(p2, vecScale(tick2Dir, halfTick));
8388
8642
  return [[unproject(tick1Start[0], tick1Start[1]), unproject(tick1End[0], tick1End[1])], [unproject(tick2Start[0], tick2Start[1]), unproject(tick2End[0], tick2End[1])]];
8389
8643
  }
8390
- });
8644
+ }, context);
8391
8645
  }
8392
8646
  const BREACH = defineControlMeasure({
8393
8647
  metadata: BREACH_METADATA,
@@ -8402,7 +8656,7 @@ const BREACH = defineControlMeasure({
8402
8656
  */
8403
8657
  const DEFAULT_BYPASS_OPTIONS = {
8404
8658
  arrowheadLengthRatio: .12,
8405
- labelGapRatio: .2
8659
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING
8406
8660
  };
8407
8661
  const BYPASS_METADATA = {
8408
8662
  id: "bypass",
@@ -8422,6 +8676,7 @@ const BYPASS_METADATA = {
8422
8676
  },
8423
8677
  drawRule: "Point12",
8424
8678
  capturesLabelSize: true,
8679
+ labelGeometryUsesResolvedMetrics: true,
8425
8680
  params: [{
8426
8681
  key: "arrowheadLengthRatio",
8427
8682
  presentationTier: "advanced",
@@ -8431,16 +8686,7 @@ const BYPASS_METADATA = {
8431
8686
  min: .01,
8432
8687
  max: 1,
8433
8688
  step: .01
8434
- }, {
8435
- key: "labelGapRatio",
8436
- presentationTier: "advanced",
8437
- label: "Label gap",
8438
- description: "Size of the rear-line gap holding the 'B' label, as a ratio of the front length",
8439
- type: "number",
8440
- min: 0,
8441
- max: 1,
8442
- step: .01
8443
- }]
8689
+ }, LABEL_PADDING_PARAM]
8444
8690
  };
8445
8691
  /**
8446
8692
  * Generates a GeoJSON FeatureCollection for a BYPASS mission task symbol (340300).
@@ -8453,22 +8699,21 @@ const BYPASS_METADATA = {
8453
8699
  * @param options - Configuration options
8454
8700
  * @returns GeoJSON FeatureCollection containing MultiLineString and Point features
8455
8701
  */
8456
- function createBypassSymbol(coordinates, options = {}) {
8702
+ function createBypassSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
8457
8703
  const arrowheadLengthRatio = options.arrowheadLengthRatio ?? DEFAULT_BYPASS_OPTIONS.arrowheadLengthRatio;
8458
8704
  return createBracketSymbol(coordinates, {
8459
8705
  part: "bypass",
8460
8706
  labelText: "B",
8461
- labelGapRatio: options.labelGapRatio ?? DEFAULT_BYPASS_OPTIONS.labelGapRatio,
8462
- labelSize: {
8463
- labelSize: options.labelSize,
8464
- labelSizePixels: options.labelSizePixels
8707
+ labelOptions: {
8708
+ ...options,
8709
+ labelPadding: options.labelPadding ?? DEFAULT_BYPASS_OPTIONS.labelPadding
8465
8710
  },
8466
8711
  buildEndCaps: ({ p1, p2, frontLength, rearTop }) => {
8467
8712
  const arrowheadLength = frontLength * arrowheadLengthRatio;
8468
8713
  const arrowDir = vecNorm(vecSub(p1, rearTop));
8469
8714
  return [createArrowHead$1(p1, arrowDir, arrowheadLength, arrowheadLength), createArrowHead$1(p2, arrowDir, arrowheadLength, arrowheadLength)];
8470
8715
  }
8471
- });
8716
+ }, context);
8472
8717
  }
8473
8718
  const BYPASS = defineControlMeasure({
8474
8719
  metadata: BYPASS_METADATA,
@@ -8484,7 +8729,7 @@ const BYPASS = defineControlMeasure({
8484
8729
  const DEFAULT_CANALIZE_OPTIONS = {
8485
8730
  tickLengthRatio: .15,
8486
8731
  tickAngle: 45,
8487
- labelGapRatio: .2
8732
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING
8488
8733
  };
8489
8734
  const CANALIZE_METADATA = {
8490
8735
  id: "canalize",
@@ -8504,6 +8749,7 @@ const CANALIZE_METADATA = {
8504
8749
  },
8505
8750
  drawRule: "Point12",
8506
8751
  capturesLabelSize: true,
8752
+ labelGeometryUsesResolvedMetrics: true,
8507
8753
  params: [
8508
8754
  {
8509
8755
  key: "tickLengthRatio",
@@ -8525,16 +8771,7 @@ const CANALIZE_METADATA = {
8525
8771
  max: 90,
8526
8772
  step: 1
8527
8773
  },
8528
- {
8529
- key: "labelGapRatio",
8530
- presentationTier: "advanced",
8531
- label: "Label gap",
8532
- description: "Size of the rear-line gap holding the 'C' label, as a ratio of the front length",
8533
- type: "number",
8534
- min: 0,
8535
- max: 1,
8536
- step: .01
8537
- }
8774
+ LABEL_PADDING_PARAM
8538
8775
  ]
8539
8776
  };
8540
8777
  /**
@@ -8548,16 +8785,15 @@ const CANALIZE_METADATA = {
8548
8785
  * @param options - Configuration options
8549
8786
  * @returns GeoJSON FeatureCollection containing MultiLineString and Point features
8550
8787
  */
8551
- function createCanalizeSymbol(coordinates, options = {}) {
8788
+ function createCanalizeSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
8552
8789
  const tickLengthRatio = options.tickLengthRatio ?? DEFAULT_CANALIZE_OPTIONS.tickLengthRatio;
8553
8790
  const tickAngle = options.tickAngle ?? DEFAULT_CANALIZE_OPTIONS.tickAngle;
8554
8791
  return createBracketSymbol(coordinates, {
8555
8792
  part: "canalize",
8556
8793
  labelText: "C",
8557
- labelGapRatio: options.labelGapRatio ?? DEFAULT_CANALIZE_OPTIONS.labelGapRatio,
8558
- labelSize: {
8559
- labelSize: options.labelSize,
8560
- labelSizePixels: options.labelSizePixels
8794
+ labelOptions: {
8795
+ ...options,
8796
+ labelPadding: options.labelPadding ?? DEFAULT_CANALIZE_OPTIONS.labelPadding
8561
8797
  },
8562
8798
  buildEndCaps: ({ p1, p2, dirFront, perpDir, frontLength }) => {
8563
8799
  const halfTick = frontLength * tickLengthRatio / 2;
@@ -8572,7 +8808,7 @@ function createCanalizeSymbol(coordinates, options = {}) {
8572
8808
  const tick2End = vecAdd(p2, vecScale(tick2Dir, halfTick));
8573
8809
  return [[unproject(tick1Start[0], tick1Start[1]), unproject(tick1End[0], tick1End[1])], [unproject(tick2Start[0], tick2Start[1]), unproject(tick2End[0], tick2End[1])]];
8574
8810
  }
8575
- });
8811
+ }, context);
8576
8812
  }
8577
8813
  const CANALIZE = defineControlMeasure({
8578
8814
  metadata: CANALIZE_METADATA,
@@ -9641,7 +9877,7 @@ const DEFAULT_CLEAR_OPTIONS = {
9641
9877
  arrowInsetRatio: .15,
9642
9878
  arrowheadLengthRatio: .15,
9643
9879
  arrowheadWidthRatio: .2,
9644
- labelGapRatio: .2
9880
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING
9645
9881
  };
9646
9882
  const CLEAR_METADATA = {
9647
9883
  id: "clear",
@@ -9661,6 +9897,7 @@ const CLEAR_METADATA = {
9661
9897
  },
9662
9898
  drawRule: "Line23",
9663
9899
  capturesLabelSize: true,
9900
+ labelGeometryUsesResolvedMetrics: true,
9664
9901
  params: [
9665
9902
  {
9666
9903
  key: "arrowInsetRatio",
@@ -9692,16 +9929,7 @@ const CLEAR_METADATA = {
9692
9929
  max: 1,
9693
9930
  step: .01
9694
9931
  },
9695
- {
9696
- key: "labelGapRatio",
9697
- presentationTier: "advanced",
9698
- label: "Label gap",
9699
- description: "Size of the shaft gap holding the 'C' label, as a ratio of the shaft length",
9700
- type: "number",
9701
- min: 0,
9702
- max: 1,
9703
- step: .01
9704
- }
9932
+ LABEL_PADDING_PARAM
9705
9933
  ]
9706
9934
  };
9707
9935
  /**
@@ -9725,11 +9953,10 @@ const CLEAR_METADATA = {
9725
9953
  * @param options - Configuration options
9726
9954
  * @returns GeoJSON FeatureCollection containing MultiLineString and Point features
9727
9955
  */
9728
- function createClearSymbol(coordinates, options = {}) {
9956
+ function createClearSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
9729
9957
  const arrowInsetRatio = Math.min(.49, Math.max(0, options.arrowInsetRatio ?? DEFAULT_CLEAR_OPTIONS.arrowInsetRatio));
9730
9958
  const arrowheadLengthRatio = options.arrowheadLengthRatio ?? DEFAULT_CLEAR_OPTIONS.arrowheadLengthRatio;
9731
9959
  const arrowheadWidthRatio = options.arrowheadWidthRatio ?? DEFAULT_CLEAR_OPTIONS.arrowheadWidthRatio;
9732
- const labelGapRatio = options.labelGapRatio ?? DEFAULT_CLEAR_OPTIONS.labelGapRatio;
9733
9960
  const [c1, c2, c3] = coordinates;
9734
9961
  const p1 = project(c1[0], c1[1]);
9735
9962
  const p2 = project(c2[0], c2[1]);
@@ -9748,6 +9975,9 @@ function createClearSymbol(coordinates, options = {}) {
9748
9975
  const arrowDir = vecScale(perpToRear, -1);
9749
9976
  const arrowheadLength = height * arrowheadLengthRatio;
9750
9977
  const arrowheadWidth = height * arrowheadWidthRatio;
9978
+ const lineRotation = Math.atan2(dirLine[1], dirLine[0]);
9979
+ const renderedLabelRotation = Math.PI - (lineRotation - Math.PI / 2);
9980
+ const labelRotation = normalizeRadians(Math.PI - keepTextLeftToRight(renderedLabelRotation));
9751
9981
  const tipAt = (t) => vecAdd(p1, vecScale(dirLine, t * height));
9752
9982
  const lines = [[unproject(p1[0], p1[1]), unproject(p2[0], p2[1])]];
9753
9983
  const fullTs = [
@@ -9761,7 +9991,7 @@ function createClearSymbol(coordinates, options = {}) {
9761
9991
  const rearEnd = vecAdd(tip, vecScale(perpToRear, shaftLength));
9762
9992
  if (i === labelIndex && shaftLength > 1e-6) {
9763
9993
  const labelCenter = vecAdd(tip, vecScale(perpToRear, shaftLength * .5));
9764
- const gapHalf = shaftLength * labelGapRatio / 2;
9994
+ const gapHalf = Math.min(shaftLength / 2, labelKnockoutHalfExtent("C", perpToRear, labelRotation, options, context, DEFAULT_CLEAR_OPTIONS.labelPadding));
9765
9995
  const gapRear = vecAdd(labelCenter, vecScale(perpToRear, gapHalf));
9766
9996
  const gapTip = vecAdd(labelCenter, vecScale(perpToRear, -gapHalf));
9767
9997
  lines.push([unproject(rearEnd[0], rearEnd[1]), unproject(gapRear[0], gapRear[1])]);
@@ -9777,9 +10007,6 @@ function createClearSymbol(coordinates, options = {}) {
9777
10007
  coordinates: lines
9778
10008
  }
9779
10009
  };
9780
- const lineRotation = Math.atan2(dirLine[1], dirLine[0]);
9781
- const renderedLabelRotation = Math.PI - (lineRotation - Math.PI / 2);
9782
- const labelRotation = normalizeRadians(Math.PI - keepTextLeftToRight(renderedLabelRotation));
9783
10010
  const labelTip = tipAt(fullTs[labelIndex]);
9784
10011
  return {
9785
10012
  type: "FeatureCollection",
@@ -9796,44 +10023,294 @@ const CLEAR = defineControlMeasure({
9796
10023
  rule: line23DrawRule
9797
10024
  });
9798
10025
  //#endregion
9799
- //#region src/generators/cm15-maneuver-areas/maneuver-arrow-task-shared.ts
9800
- const DEFAULT_MANEUVER_ARROW_TASK_AMPLIFIER_OPTIONS = {
9801
- labelPosition: .25,
9802
- dtgPosition: .62,
9803
- labelPadding: 0
9804
- };
9805
- const DEFAULT_MANEUVER_ARROW_TASK_OPTIONS = {
9806
- shaftWidthRatio: DEFAULT_SHAFT_WIDTH_RATIO$1,
9807
- rearWidthRatio: DEFAULT_REAR_WIDTH_RATIO,
9808
- smooth: false,
9809
- smoothResolution: 5,
9810
- crossbarLengthRatio: 1.1,
9811
- ...DEFAULT_MANEUVER_ARROW_TASK_AMPLIFIER_OPTIONS
10026
+ //#region src/generators/cm34-mission-tasks/isolate.ts
10027
+ /**
10028
+ * Default options for the ISOLATE symbol.
10029
+ */
10030
+ const DEFAULT_ISOLATE_OPTIONS = {
10031
+ openingAngle: 30,
10032
+ barbCount: 9,
10033
+ barbLengthRatio: .18,
10034
+ labelPadding: .2
9812
10035
  };
9813
- function createManeuverArrowTaskAmplifierLabels(geometry, options, textAmplifiers) {
9814
- const resolved = {
9815
- ...DEFAULT_MANEUVER_ARROW_TASK_AMPLIFIER_OPTIONS,
9816
- ...options
9817
- };
9818
- const centerline = geometry.shaftCenterline;
9819
- const { segments, totalLength } = polylineSegments(centerline);
9820
- if (segments.length === 0) return [];
9821
- const labels = [];
9822
- const sizeProps = labelSizeProps(resolved);
9823
- const labelFrame = pointAlongPolyline(segments, totalLength, clamp01(resolved.labelPosition));
9824
- if (labelFrame && textAmplifiers.T) pushLabel(labels, labelFrame.point, textAmplifiers.T, labelRotationAlong(labelFrame.along), sizeProps, void 0, "T");
9825
- const dtgFrame = pointAlongPolyline(segments, totalLength, clamp01(resolved.dtgPosition));
9826
- if (dtgFrame) {
9827
- const rowOffset = resolveLabelOffsetMeters(resolved, .55 + Math.max(0, resolved.labelPadding));
9828
- const rotation = labelRotationAlong(dtgFrame.along);
9829
- if (textAmplifiers.W) pushLabel(labels, vecAdd(dtgFrame.point, vecScale(dtgFrame.perp, rowOffset)), textAmplifiers.W, rotation, sizeProps, void 0, "W");
9830
- if (textAmplifiers.W1) pushLabel(labels, vecSub(dtgFrame.point, vecScale(dtgFrame.perp, rowOffset)), textAmplifiers.W1, rotation, sizeProps, void 0, "W1");
10036
+ const ISOLATE_VARIANT_PARAMS = [
10037
+ {
10038
+ key: "openingAngle",
10039
+ presentationTier: "advanced",
10040
+ label: "Opening",
10041
+ description: "Angular width of the gap on the friendly side, in degrees",
10042
+ type: "number",
10043
+ min: 0,
10044
+ max: 180,
10045
+ step: 1
10046
+ },
10047
+ {
10048
+ key: "barbCount",
10049
+ presentationTier: "advanced",
10050
+ label: "Barbs",
10051
+ description: "Number of inward-pointing barbs spaced along the closed arc",
10052
+ type: "number",
10053
+ min: 1,
10054
+ max: 24,
10055
+ step: 1
10056
+ },
10057
+ {
10058
+ key: "barbLengthRatio",
10059
+ presentationTier: "advanced",
10060
+ label: "Barb length",
10061
+ description: "Length of each inward barb, as a ratio of the symbol radius",
10062
+ type: "number",
10063
+ min: .01,
10064
+ max: 1,
10065
+ step: .01
9831
10066
  }
9832
- return labels;
10067
+ ];
10068
+ function isolateVariantParams(labelText) {
10069
+ return [...ISOLATE_VARIANT_PARAMS, {
10070
+ ...LABEL_PADDING_PARAM,
10071
+ label: `${labelText} padding`,
10072
+ description: `Extra boundary clearance on each side of ${labelText}, as a ratio of its height`
10073
+ }];
9833
10074
  }
9834
- function createManeuverArrowTask(coordinates, options, textAmplifiers, config) {
9835
- const merged = {
9836
- ...DEFAULT_MANEUVER_ARROW_TASK_OPTIONS,
10075
+ const ISOLATE_METADATA = {
10076
+ id: "isolate",
10077
+ name: "Isolate",
10078
+ description: "Seals an enemy off from support, denies freedom of movement, and prevents contact with other enemy forces while offensive action continues against it.",
10079
+ entity: "Mission Tasks",
10080
+ entityType: "Isolate",
10081
+ value: "341500",
10082
+ minCoordinates: 2,
10083
+ maxCoordinates: 2,
10084
+ geometry: "line",
10085
+ geometryTypes: ["MultiLineString", "Point"],
10086
+ paints: {
10087
+ stroke: true,
10088
+ fill: "none",
10089
+ text: true
10090
+ },
10091
+ drawRule: "Area15",
10092
+ capturesLabelSize: true,
10093
+ labelGeometryUsesResolvedMetrics: true,
10094
+ params: isolateVariantParams("I")
10095
+ };
10096
+ const LABEL_ROTATION = Math.PI;
10097
+ /**
10098
+ * Generates a GeoJSON FeatureCollection for an ISOLATE mission task symbol (341500).
10099
+ *
10100
+ * A near-complete circle centered on PT. 1 whose radius reaches PT. 2. The arc
10101
+ * starts (bare) at PT. 2's bearing and sweeps clockwise around the closed
10102
+ * portion to a single arrow tip at the far end; the wedge-shaped gap (default
10103
+ * 30°) between that arrow tip and PT. 2 is the opening — the "friendly side".
10104
+ * Inward-pointing arrowhead barbs are spaced along the closed arc, set in from
10105
+ * both ends so they never touch PT. 2 or the arrow tip, conveying the
10106
+ * sealing-off of the enclosed force. A doctrinal "I" interrupts the closed arc
10107
+ * opposite the friendly-side opening.
10108
+ *
10109
+ * Input Points:
10110
+ * - PT. 1: Center point
10111
+ * - PT. 2: Start point — fixes the radius and the bearing of the opening
10112
+ *
10113
+ * @param coordinates - [PT. 1, PT. 2] as Position arrays
10114
+ * @param options - Configuration options
10115
+ * @returns GeoJSON FeatureCollection containing MultiLineString and Point features
10116
+ */
10117
+ function createIsolateSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
10118
+ return createIsolateVariantSymbol(coordinates, options, {
10119
+ part: "isolate",
10120
+ labelText: "I"
10121
+ }, context);
10122
+ }
10123
+ /** Shared geometry for Isolate and its doctrinal cordon variants. */
10124
+ function createIsolateVariantSymbol(coordinates, options, config, context = {}) {
10125
+ const openingAngle = options.openingAngle ?? DEFAULT_ISOLATE_OPTIONS.openingAngle;
10126
+ const barbCount = options.barbCount ?? DEFAULT_ISOLATE_OPTIONS.barbCount;
10127
+ const barbLengthRatio = options.barbLengthRatio ?? DEFAULT_ISOLATE_OPTIONS.barbLengthRatio;
10128
+ const center = project(coordinates[0][0], coordinates[0][1]);
10129
+ const vRadius = vecSub(project(coordinates[1][0], coordinates[1][1]), center);
10130
+ const radius = vecMag(vRadius);
10131
+ if (radius < 1e-6) return {
10132
+ type: "FeatureCollection",
10133
+ features: []
10134
+ };
10135
+ const startAngle = Math.atan2(vRadius[1], vRadius[0]);
10136
+ const openingRad = Math.min(Math.max(openingAngle, 0), 359) * Math.PI / 180;
10137
+ const arcSpan = 2 * Math.PI - openingRad;
10138
+ const endAngle = startAngle - arcSpan;
10139
+ const pointOnCircle = (angle, r = radius) => vecAdd(center, [r * Math.cos(angle), r * Math.sin(angle)]);
10140
+ const labelSweep = arcSpan / 2;
10141
+ const labelMetrics = resolveTextMetrics(config.labelText, options, context, "regular", radius * .12);
10142
+ const labelPadding = Math.max(0, options.labelPadding ?? DEFAULT_ISOLATE_OPTIONS.labelPadding);
10143
+ const labelHalfWidth = Math.max(labelMetrics.width / 2, labelMetrics.height * LABEL_HALF_GLYPH_RATIO);
10144
+ const labelHalfGap = Math.min(arcSpan / 4, Math.max(labelHalfWidth + labelMetrics.height * labelPadding, 1) / radius);
10145
+ const labelPoint = pointOnCircle(startAngle - labelSweep);
10146
+ const labelBoxHalfWidth = labelMetrics.width / 2 + labelMetrics.height * labelPadding;
10147
+ const labelBoxHalfHeight = labelMetrics.height / 2 + labelMetrics.height * labelPadding;
10148
+ const sampleArc = (from, to) => {
10149
+ const span = to - from;
10150
+ const segments = Math.max(2, Math.round(64 * span / (2 * Math.PI)));
10151
+ const arc = [];
10152
+ for (let i = 0; i <= segments; i++) {
10153
+ const p = pointOnCircle(startAngle - from - span * i / segments);
10154
+ arc.push(unproject(p[0], p[1]));
10155
+ }
10156
+ return arc;
10157
+ };
10158
+ const barbLength = radius * barbLengthRatio;
10159
+ const lines = [sampleArc(0, labelSweep - labelHalfGap), sampleArc(labelSweep + labelHalfGap, arcSpan)];
10160
+ const tipDir = [Math.sin(endAngle), -Math.cos(endAngle)];
10161
+ lines.push(createArrowHead$1(pointOnCircle(endAngle), tipDir, barbLength, barbLength));
10162
+ const halfWidth = Math.asin(Math.min(1, barbLength / 2 / radius));
10163
+ const tipRadius = Math.max(0, radius - barbLength);
10164
+ const steps = Math.max(1, barbCount);
10165
+ for (let k = 1; k <= steps; k++) {
10166
+ const angle = startAngle - arcSpan * k / (steps + 1);
10167
+ const left = pointOnCircle(angle - halfWidth);
10168
+ const right = pointOnCircle(angle + halfWidth);
10169
+ const tip = pointOnCircle(angle, tipRadius);
10170
+ const barbLines = config.knockoutBarbs ? clipPolylineOutsideLabelBox([
10171
+ left,
10172
+ tip,
10173
+ right
10174
+ ], labelPoint, labelBoxHalfWidth, labelBoxHalfHeight, LABEL_ROTATION) : [[
10175
+ left,
10176
+ tip,
10177
+ right
10178
+ ]];
10179
+ for (const barb of barbLines) lines.push(barb.map((point) => unproject(point[0], point[1])));
10180
+ }
10181
+ return {
10182
+ type: "FeatureCollection",
10183
+ features: [{
10184
+ type: "Feature",
10185
+ properties: { part: config.part },
10186
+ geometry: {
10187
+ type: "MultiLineString",
10188
+ coordinates: lines
10189
+ }
10190
+ }, createLabelFeature(labelPoint, config.labelText, LABEL_ROTATION, options)]
10191
+ };
10192
+ }
10193
+ const ISOLATE = defineControlMeasure({
10194
+ metadata: ISOLATE_METADATA,
10195
+ generator: createIsolateSymbol,
10196
+ defaultOptions: DEFAULT_ISOLATE_OPTIONS,
10197
+ rule: isolateDrawRule
10198
+ });
10199
+ //#endregion
10200
+ //#region src/generators/cm34-mission-tasks/cordon-and-knock.ts
10201
+ const DEFAULT_CORDON_AND_KNOCK_OPTIONS = DEFAULT_ISOLATE_OPTIONS;
10202
+ const CORDON_AND_KNOCK_METADATA = {
10203
+ id: "cordon-and-knock",
10204
+ name: "Cordon and Knock",
10205
+ description: "Isolates an area while forces call on its occupants to cooperate before conducting further action.",
10206
+ entity: "Mission Tasks",
10207
+ entityType: "Cordon and Knock",
10208
+ value: "342600",
10209
+ minCoordinates: 2,
10210
+ maxCoordinates: 2,
10211
+ geometry: "line",
10212
+ geometryTypes: ["MultiLineString", "Point"],
10213
+ paints: {
10214
+ stroke: true,
10215
+ fill: "none",
10216
+ text: true
10217
+ },
10218
+ drawRule: "Area15",
10219
+ capturesLabelSize: true,
10220
+ labelGeometryUsesResolvedMetrics: true,
10221
+ params: isolateVariantParams("C/K")
10222
+ };
10223
+ /** Generates the Cordon and Knock mission-task symbol (342600). */
10224
+ function createCordonAndKnockSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
10225
+ return createIsolateVariantSymbol(coordinates, options, {
10226
+ part: "cordon-and-knock",
10227
+ labelText: "C/K",
10228
+ knockoutBarbs: true
10229
+ }, context);
10230
+ }
10231
+ const CORDON_AND_KNOCK = defineControlMeasure({
10232
+ metadata: CORDON_AND_KNOCK_METADATA,
10233
+ generator: createCordonAndKnockSymbol,
10234
+ defaultOptions: DEFAULT_CORDON_AND_KNOCK_OPTIONS,
10235
+ rule: isolateDrawRule
10236
+ });
10237
+ //#endregion
10238
+ //#region src/generators/cm34-mission-tasks/cordon-and-search.ts
10239
+ const DEFAULT_CORDON_AND_SEARCH_OPTIONS = DEFAULT_ISOLATE_OPTIONS;
10240
+ const CORDON_AND_SEARCH_METADATA = {
10241
+ id: "cordon-and-search",
10242
+ name: "Cordon and Search",
10243
+ description: "Isolates an area so forces can systematically search it and its occupants.",
10244
+ entity: "Mission Tasks",
10245
+ entityType: "Cordon and Search",
10246
+ value: "342700",
10247
+ minCoordinates: 2,
10248
+ maxCoordinates: 2,
10249
+ geometry: "line",
10250
+ geometryTypes: ["MultiLineString", "Point"],
10251
+ paints: {
10252
+ stroke: true,
10253
+ fill: "none",
10254
+ text: true
10255
+ },
10256
+ drawRule: "Area15",
10257
+ capturesLabelSize: true,
10258
+ labelGeometryUsesResolvedMetrics: true,
10259
+ params: isolateVariantParams("C/S")
10260
+ };
10261
+ /** Generates the Cordon and Search mission-task symbol (342700). */
10262
+ function createCordonAndSearchSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
10263
+ return createIsolateVariantSymbol(coordinates, options, {
10264
+ part: "cordon-and-search",
10265
+ labelText: "C/S",
10266
+ knockoutBarbs: true
10267
+ }, context);
10268
+ }
10269
+ const CORDON_AND_SEARCH = defineControlMeasure({
10270
+ metadata: CORDON_AND_SEARCH_METADATA,
10271
+ generator: createCordonAndSearchSymbol,
10272
+ defaultOptions: DEFAULT_CORDON_AND_SEARCH_OPTIONS,
10273
+ rule: isolateDrawRule
10274
+ });
10275
+ //#endregion
10276
+ //#region src/generators/cm15-maneuver-areas/maneuver-arrow-task-shared.ts
10277
+ const DEFAULT_MANEUVER_ARROW_TASK_AMPLIFIER_OPTIONS = {
10278
+ labelPosition: .25,
10279
+ dtgPosition: .62,
10280
+ labelPadding: 0
10281
+ };
10282
+ const DEFAULT_MANEUVER_ARROW_TASK_OPTIONS = {
10283
+ shaftWidthRatio: DEFAULT_SHAFT_WIDTH_RATIO$1,
10284
+ rearWidthRatio: DEFAULT_REAR_WIDTH_RATIO,
10285
+ smooth: false,
10286
+ smoothResolution: 5,
10287
+ crossbarLengthRatio: 1.1,
10288
+ ...DEFAULT_MANEUVER_ARROW_TASK_AMPLIFIER_OPTIONS
10289
+ };
10290
+ function createManeuverArrowTaskAmplifierLabels(geometry, options, textAmplifiers) {
10291
+ const resolved = {
10292
+ ...DEFAULT_MANEUVER_ARROW_TASK_AMPLIFIER_OPTIONS,
10293
+ ...options
10294
+ };
10295
+ const centerline = geometry.shaftCenterline;
10296
+ const { segments, totalLength } = polylineSegments(centerline);
10297
+ if (segments.length === 0) return [];
10298
+ const labels = [];
10299
+ const sizeProps = labelSizeProps(resolved);
10300
+ const labelFrame = pointAlongPolyline(segments, totalLength, clamp01(resolved.labelPosition));
10301
+ if (labelFrame && textAmplifiers.T) pushLabel(labels, labelFrame.point, textAmplifiers.T, labelRotationAlong(labelFrame.along), sizeProps, void 0, "T");
10302
+ const dtgFrame = pointAlongPolyline(segments, totalLength, clamp01(resolved.dtgPosition));
10303
+ if (dtgFrame) {
10304
+ const rowOffset = resolveLabelOffsetMeters(resolved, .55 + Math.max(0, resolved.labelPadding));
10305
+ const rotation = labelRotationAlong(dtgFrame.along);
10306
+ if (textAmplifiers.W) pushLabel(labels, vecAdd(dtgFrame.point, vecScale(dtgFrame.perp, rowOffset)), textAmplifiers.W, rotation, sizeProps, void 0, "W");
10307
+ if (textAmplifiers.W1) pushLabel(labels, vecSub(dtgFrame.point, vecScale(dtgFrame.perp, rowOffset)), textAmplifiers.W1, rotation, sizeProps, void 0, "W1");
10308
+ }
10309
+ return labels;
10310
+ }
10311
+ function createManeuverArrowTask(coordinates, options, textAmplifiers, config) {
10312
+ const merged = {
10313
+ ...DEFAULT_MANEUVER_ARROW_TASK_OPTIONS,
9837
10314
  ...options
9838
10315
  };
9839
10316
  const resolved = {
@@ -10602,7 +11079,7 @@ const COVER = defineControlMeasure({
10602
11079
  const DEFAULT_DELAY_OPTIONS = {
10603
11080
  arrowheadLengthRatio: .12,
10604
11081
  arrowheadWidthRatio: .12,
10605
- labelGapRatio: .18,
11082
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING,
10606
11083
  arcSegments: 32
10607
11084
  };
10608
11085
  const DELAY_PARAMS = [
@@ -10626,16 +11103,7 @@ const DELAY_PARAMS = [
10626
11103
  max: 1,
10627
11104
  step: .01
10628
11105
  },
10629
- {
10630
- key: "labelGapRatio",
10631
- presentationTier: "advanced",
10632
- label: "Label gap",
10633
- description: "Size of the shaft gap holding the task label, as a ratio of the shaft length",
10634
- type: "number",
10635
- min: 0,
10636
- max: 1,
10637
- step: .01
10638
- },
11106
+ LABEL_PADDING_PARAM,
10639
11107
  {
10640
11108
  key: "arcSegments",
10641
11109
  presentationTier: "advanced",
@@ -10665,9 +11133,16 @@ const DELAY_METADATA = {
10665
11133
  },
10666
11134
  drawRule: "Line24",
10667
11135
  capturesLabelSize: true,
11136
+ labelGeometryUsesResolvedMetrics: true,
10668
11137
  params: DELAY_PARAMS
10669
11138
  };
10670
- function createDelaySymbol(coordinates, options = {}) {
11139
+ function createDelaySymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
11140
+ return createDelayTaskSymbol(coordinates, {
11141
+ glyph: "D",
11142
+ part: "delay"
11143
+ }, options, context);
11144
+ }
11145
+ function createDelayTaskSymbol(coordinates, config, options, context) {
10671
11146
  const [c1, c2, c3] = coordinates;
10672
11147
  const p1 = project(c1[0], c1[1]);
10673
11148
  const p2 = project(c2[0], c2[1]);
@@ -10683,8 +11158,9 @@ function createDelaySymbol(coordinates, options = {}) {
10683
11158
  const signedDiameter = vecDot(vecSub(p3, p2), diameterAxis);
10684
11159
  const diameterEnd = vecAdd(p2, vecScale(diameterAxis, signedDiameter));
10685
11160
  const diameter = Math.abs(signedDiameter);
10686
- const gapHalf = lineLength * Math.min(1, Math.max(0, options.labelGapRatio ?? DEFAULT_DELAY_OPTIONS.labelGapRatio)) / 2;
10687
11161
  const labelPoint = vecAdd(p1, vecScale(dirLine, lineLength / 2));
11162
+ const labelRotation = labelRotationAlong(dirLine);
11163
+ const gapHalf = Math.min(lineLength / 2, labelKnockoutHalfExtent(config.glyph, dirLine, labelRotation, options, context, DEFAULT_DELAY_OPTIONS.labelPadding));
10688
11164
  const gapStart = vecAdd(labelPoint, vecScale(dirLine, -gapHalf));
10689
11165
  const gapEnd = vecAdd(labelPoint, vecScale(dirLine, gapHalf));
10690
11166
  const lines = [
@@ -10693,58 +11169,27 @@ function createDelaySymbol(coordinates, options = {}) {
10693
11169
  createArrowHead$1(p1, vecScale(dirLine, -1), lineLength * (options.arrowheadLengthRatio ?? DEFAULT_DELAY_OPTIONS.arrowheadLengthRatio), lineLength * (options.arrowheadWidthRatio ?? DEFAULT_DELAY_OPTIONS.arrowheadWidthRatio))
10694
11170
  ];
10695
11171
  if (diameter >= 1e-6) lines.push(createSemicircleArc(p2, diameterEnd, dirLine, options.arcSegments ?? DEFAULT_DELAY_OPTIONS.arcSegments));
10696
- const lineFeature = {
10697
- type: "Feature",
10698
- properties: { part: "delay" },
10699
- geometry: {
10700
- type: "MultiLineString",
10701
- coordinates: lines
10702
- }
10703
- };
10704
- const lineRotation = Math.atan2(dirLine[1], dirLine[0]);
10705
11172
  return {
10706
11173
  type: "FeatureCollection",
10707
- features: [lineFeature, createLabelFeature(labelPoint, "D", normalizeRadians(Math.PI + lineRotation), {
11174
+ features: [{
11175
+ type: "Feature",
11176
+ properties: { part: config.part },
11177
+ geometry: {
11178
+ type: "MultiLineString",
11179
+ coordinates: lines
11180
+ }
11181
+ }, createLabelFeature(labelPoint, config.glyph, labelRotation, {
10708
11182
  labelSize: options.labelSize,
10709
11183
  labelSizePixels: options.labelSizePixels
10710
11184
  })]
10711
11185
  };
10712
11186
  }
10713
- function createSemicircleArc(start, end, bulgeDir, requestedSegments) {
10714
- const diameterVector = vecSub(end, start);
10715
- const diameter = vecMag(diameterVector);
10716
- if (diameter < 1e-6) return [unproject(start[0], start[1])];
10717
- const diameterDir = vecNorm(diameterVector);
10718
- const center = vecAdd(start, vecScale(diameterVector, .5));
10719
- const radius = diameter / 2;
10720
- const segments = Math.max(4, Math.round(requestedSegments));
10721
- const points = [];
10722
- for (let i = 0; i <= segments; i += 1) {
10723
- const theta = i / segments * Math.PI;
10724
- const point = vecAdd(center, vecAdd(vecScale(diameterDir, -Math.cos(theta) * radius), vecScale(bulgeDir, Math.sin(theta) * radius)));
10725
- points.push(unproject(point[0], point[1]));
10726
- }
10727
- return points;
10728
- }
10729
11187
  /**
10730
11188
  * Line24 mission tasks that share Delay's geometry and differ only in the
10731
11189
  * glyph shown in the shaft gap and the `part` tag on the line feature.
10732
11190
  */
10733
- function createDelayVariantSymbol(coordinates, config, options = {}) {
10734
- return {
10735
- type: "FeatureCollection",
10736
- features: createDelaySymbol(coordinates, options).features.map((feature) => ({
10737
- ...feature,
10738
- properties: feature.geometry.type === "Point" ? {
10739
- ...feature.properties,
10740
- part: "label",
10741
- text: config.glyph
10742
- } : {
10743
- ...feature.properties,
10744
- part: config.part
10745
- }
10746
- }))
10747
- };
11191
+ function createDelayVariantSymbol(coordinates, config, options = {}, context = {}) {
11192
+ return createDelayTaskSymbol(coordinates, config, options, context);
10748
11193
  }
10749
11194
  const DELAY = defineControlMeasure({
10750
11195
  metadata: DELAY_METADATA,
@@ -10753,6 +11198,122 @@ const DELAY = defineControlMeasure({
10753
11198
  rule: line24DrawRule
10754
11199
  });
10755
11200
  //#endregion
11201
+ //#region src/generators/cm34-mission-tasks/hairpin.ts
11202
+ /**
11203
+ * Shared scaffold for the Area18 "hairpin" mission tasks (Relief in Place,
11204
+ * Demonstrate). Owns the doctrinal geometry both measures share: two parallel
11205
+ * arms with an arrow tip at each open end (PT. 1 and PT. 3), joined by a
11206
+ * semicircular turn across PT. 2 → PT. 3.
11207
+ *
11208
+ * Both tips use the same option ratios, each scaled by the length of its own
11209
+ * arm (PT. 1/PT. 2 and PT. 3/PT. 4). Only glyph placement differs between the
11210
+ * measures, which each supply via {@link HairpinSymbolConfig.buildLabel}.
11211
+ */
11212
+ function createHairpinSymbol(coordinates, config, options = {}) {
11213
+ const [c1, c2, c3, c4] = coordinates;
11214
+ const p1 = project(c1[0], c1[1]);
11215
+ const p2 = project(c2[0], c2[1]);
11216
+ const p3 = project(c3[0], c3[1]);
11217
+ const p4 = project(c4[0], c4[1]);
11218
+ const upperShaft = vecSub(p2, p1);
11219
+ const upperLength = vecMag(upperShaft);
11220
+ const lowerShaft = vecSub(p3, p4);
11221
+ const lowerLength = vecMag(lowerShaft);
11222
+ if (upperLength < 1e-6 || lowerLength < 1e-6) return {
11223
+ type: "FeatureCollection",
11224
+ features: []
11225
+ };
11226
+ const upperDirection = vecNorm(upperShaft);
11227
+ const lowerDirection = vecNorm(lowerShaft);
11228
+ const arrowheadLengthRatio = Math.max(0, options.arrowheadLengthRatio ?? DEFAULT_DELAY_OPTIONS.arrowheadLengthRatio);
11229
+ const arrowheadWidthRatio = Math.max(0, options.arrowheadWidthRatio ?? DEFAULT_DELAY_OPTIONS.arrowheadWidthRatio);
11230
+ const { label, upperArm } = config.buildLabel({
11231
+ points: [
11232
+ p1,
11233
+ p2,
11234
+ p3,
11235
+ p4
11236
+ ],
11237
+ upperDirection,
11238
+ upperLength,
11239
+ upperArm: [[unproject(p1[0], p1[1]), unproject(p2[0], p2[1])]]
11240
+ });
11241
+ return {
11242
+ type: "FeatureCollection",
11243
+ features: [{
11244
+ type: "Feature",
11245
+ properties: { part: config.part },
11246
+ geometry: {
11247
+ type: "MultiLineString",
11248
+ coordinates: [
11249
+ ...upperArm,
11250
+ createSemicircleArc(p2, p3, upperDirection, options.arcSegments ?? DEFAULT_DELAY_OPTIONS.arcSegments),
11251
+ [unproject(p4[0], p4[1]), unproject(p3[0], p3[1])],
11252
+ createArrowHead$1(p1, vecScale(upperDirection, -1), upperLength * arrowheadLengthRatio, upperLength * arrowheadWidthRatio),
11253
+ createArrowHead$1(p3, lowerDirection, lowerLength * arrowheadLengthRatio, lowerLength * arrowheadWidthRatio)
11254
+ ]
11255
+ }
11256
+ }, label]
11257
+ };
11258
+ }
11259
+ //#endregion
11260
+ //#region src/generators/cm34-mission-tasks/demonstrate.ts
11261
+ const DEFAULT_DEMONSTRATE_OPTIONS = DEFAULT_DELAY_OPTIONS;
11262
+ const DEMONSTRATE_METADATA = {
11263
+ id: "demonstrate",
11264
+ name: "Demonstrate/Demonstration",
11265
+ description: "Uses a show of force to deceive the enemy without seeking decisive engagement.",
11266
+ entity: "Mission Tasks",
11267
+ entityType: "Demonstrate/Demonstration",
11268
+ value: "343300",
11269
+ minCoordinates: 4,
11270
+ maxCoordinates: 4,
11271
+ geometry: "line",
11272
+ geometryTypes: ["MultiLineString", "Point"],
11273
+ paints: {
11274
+ stroke: true,
11275
+ fill: "none",
11276
+ text: true
11277
+ },
11278
+ drawRule: "Area18",
11279
+ capturesLabelSize: true,
11280
+ labelGeometryUsesResolvedMetrics: true,
11281
+ params: DELAY_PARAMS
11282
+ };
11283
+ /** Generates the DEMONSTRATE/DEMONSTRATION mission task symbol (343300). */
11284
+ function createDemonstrateSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
11285
+ return createHairpinSymbol(coordinates, {
11286
+ part: "demonstrate",
11287
+ buildLabel: ({ points, upperDirection, upperLength }) => {
11288
+ const [p1, p2] = points;
11289
+ const labelPoint = vecAdd(p1, vecScale(upperDirection, upperLength / 2));
11290
+ const rotation = labelRotationAlong(upperDirection);
11291
+ const gapHalf = Math.min(upperLength / 2, labelKnockoutHalfExtent("DEM", upperDirection, rotation, options, context, DEFAULT_DEMONSTRATE_OPTIONS.labelPadding));
11292
+ const gapStart = vecAdd(labelPoint, vecScale(upperDirection, -gapHalf));
11293
+ const gapEnd = vecAdd(labelPoint, vecScale(upperDirection, gapHalf));
11294
+ return {
11295
+ upperArm: [[unproject(p1[0], p1[1]), unproject(gapStart[0], gapStart[1])], [unproject(gapEnd[0], gapEnd[1]), unproject(p2[0], p2[1])]],
11296
+ label: createLabelFeature(labelPoint, "DEM", rotation, {
11297
+ labelSize: options.labelSize,
11298
+ labelSizePixels: options.labelSizePixels
11299
+ })
11300
+ };
11301
+ }
11302
+ }, options);
11303
+ }
11304
+ const DEMONSTRATE = defineControlMeasure({
11305
+ metadata: DEMONSTRATE_METADATA,
11306
+ generator: createDemonstrateSymbol,
11307
+ defaultOptions: DEFAULT_DEMONSTRATE_OPTIONS,
11308
+ rule: demonstrateDrawRule,
11309
+ previewSample: { controlPoints: [
11310
+ [-1, -.45],
11311
+ [.45, -.45],
11312
+ [.45, .45],
11313
+ [-1, .45]
11314
+ ] }
11315
+ });
11316
+ //#endregion
10756
11317
  //#region src/generators/cm34-mission-tasks/disengage.ts
10757
11318
  const DEFAULT_DISENGAGE_OPTIONS = DEFAULT_DELAY_OPTIONS;
10758
11319
  const DISENGAGE_METADATA = {
@@ -10773,27 +11334,25 @@ const DISENGAGE_METADATA = {
10773
11334
  },
10774
11335
  drawRule: "Line24",
10775
11336
  capturesLabelSize: true,
11337
+ labelGeometryUsesResolvedMetrics: true,
10776
11338
  params: DELAY_PARAMS
10777
11339
  };
10778
- function createDisengageSymbol(coordinates, options = {}) {
11340
+ function createDisengageSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
10779
11341
  return createDelayVariantSymbol(coordinates, {
10780
11342
  glyph: "DIS",
10781
11343
  part: "disengage"
10782
- }, options);
11344
+ }, options, context);
10783
11345
  }
10784
11346
  const DISENGAGE = defineControlMeasure({
10785
11347
  metadata: DISENGAGE_METADATA,
10786
11348
  generator: createDisengageSymbol,
10787
11349
  defaultOptions: DEFAULT_DISENGAGE_OPTIONS,
10788
11350
  rule: line24DrawRule,
10789
- previewSample: {
10790
- controlPoints: [
10791
- [-1, -.4],
10792
- [1, -.4],
10793
- [0, .6]
10794
- ],
10795
- options: { labelGapRatio: .36 }
10796
- }
11351
+ previewSample: { controlPoints: [
11352
+ [-1, -.4],
11353
+ [1, -.4],
11354
+ [0, .6]
11355
+ ] }
10797
11356
  });
10798
11357
  //#endregion
10799
11358
  //#region src/generators/cm34-mission-tasks/disrupt.ts
@@ -10803,7 +11362,7 @@ const DEFAULT_DISRUPT_MISSION_TASK_OPTIONS = {
10803
11362
  shaftLengthRatio: .25,
10804
11363
  arrowheadLengthRatio: .15,
10805
11364
  arrowheadWidthRatio: .2,
10806
- labelGapRatio: .2
11365
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING
10807
11366
  };
10808
11367
  const DISRUPT_MISSION_TASK_METADATA = {
10809
11368
  id: "disrupt-mission-task",
@@ -10823,6 +11382,7 @@ const DISRUPT_MISSION_TASK_METADATA = {
10823
11382
  },
10824
11383
  drawRule: "Area12",
10825
11384
  capturesLabelSize: true,
11385
+ labelGeometryUsesResolvedMetrics: true,
10826
11386
  params: [
10827
11387
  {
10828
11388
  key: "middleArrowLengthRatio",
@@ -10874,20 +11434,11 @@ const DISRUPT_MISSION_TASK_METADATA = {
10874
11434
  max: 1,
10875
11435
  step: .01
10876
11436
  },
10877
- {
10878
- key: "labelGapRatio",
10879
- presentationTier: "advanced",
10880
- label: "Label gap",
10881
- description: "Size of the middle-shaft gap holding the 'D' label",
10882
- type: "number",
10883
- min: 0,
10884
- max: 1,
10885
- step: .01
10886
- }
11437
+ LABEL_PADDING_PARAM
10887
11438
  ]
10888
11439
  };
10889
11440
  /** Generates the DISRUPT mission task symbol (341000). */
10890
- function createDisruptMissionTaskSymbol(coordinates, options = {}) {
11441
+ function createDisruptMissionTaskSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
10891
11442
  const p1 = project(coordinates[0][0], coordinates[0][1]);
10892
11443
  const p2 = project(coordinates[1][0], coordinates[1][1]);
10893
11444
  const frame = createProjectedBaselineFrame(p1, p2, {
@@ -10919,8 +11470,8 @@ function createDisruptMissionTaskSymbol(coordinates, options = {}) {
10919
11470
  const bottomTip = vecAdd(p2, vecScale(arrowDirection, bottomLength));
10920
11471
  const rearEnd = vecAdd(middle, vecScale(arrowDirection, -rearLength));
10921
11472
  const labelPoint = vecScale(vecAdd(middle, middleTip), .5);
10922
- const labelGapRatio = clamp01(options.labelGapRatio ?? DEFAULT_DISRUPT_MISSION_TASK_OPTIONS.labelGapRatio);
10923
- const gapHalf = Math.min(topLength * labelGapRatio / 2, middleLength / 2);
11473
+ const labelRotation = labelRotationAlong(arrowDirection);
11474
+ const gapHalf = Math.min(labelKnockoutHalfExtent("D", arrowDirection, labelRotation, options, context, DEFAULT_DISRUPT_MISSION_TASK_OPTIONS.labelPadding), middleLength / 2);
10924
11475
  const gapStart = vecAdd(labelPoint, vecScale(arrowDirection, -gapHalf));
10925
11476
  const gapEnd = vecAdd(labelPoint, vecScale(arrowDirection, gapHalf));
10926
11477
  const p1LonLat = unproject(p1[0], p1[1]);
@@ -10943,7 +11494,7 @@ function createDisruptMissionTaskSymbol(coordinates, options = {}) {
10943
11494
  createArrowHead$1(bottomTip, arrowDirection, headLength, headWidth)
10944
11495
  ]
10945
11496
  }
10946
- }, createLabelFeature(labelPoint, "D", labelRotationAlong(arrowDirection), {
11497
+ }, createLabelFeature(labelPoint, "D", labelRotation, {
10947
11498
  labelSize: options.labelSize,
10948
11499
  labelSizePixels: options.labelSizePixels
10949
11500
  })]
@@ -10962,7 +11513,7 @@ const DEFAULT_ENVELOPMENT_OPTIONS = {
10962
11513
  arrowheadLengthRatio: .1,
10963
11514
  arrowheadWidthRatio: .12,
10964
11515
  labelPosition: .6,
10965
- labelGapRatio: .18,
11516
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING,
10966
11517
  resolution: 64
10967
11518
  };
10968
11519
  const ENVELOPMENT_METADATA = {
@@ -10983,6 +11534,7 @@ const ENVELOPMENT_METADATA = {
10983
11534
  },
10984
11535
  drawRule: "Line31",
10985
11536
  capturesLabelSize: true,
11537
+ labelGeometryUsesResolvedMetrics: true,
10986
11538
  params: [
10987
11539
  {
10988
11540
  key: "arrowheadLengthRatio",
@@ -11014,21 +11566,12 @@ const ENVELOPMENT_METADATA = {
11014
11566
  max: 1,
11015
11567
  step: .01
11016
11568
  },
11017
- {
11018
- key: "labelGapRatio",
11019
- presentationTier: "advanced",
11020
- label: "Label gap",
11021
- description: "Size of the straight-line cutout holding the E label",
11022
- type: "number",
11023
- min: 0,
11024
- max: 1,
11025
- step: .01
11026
- },
11569
+ LABEL_PADDING_PARAM,
11027
11570
  ...ARC_RESOLUTION_PARAMS
11028
11571
  ]
11029
11572
  };
11030
11573
  /** Generates the Envelopment mission task symbol (343500). */
11031
- function createEnvelopment(coordinates, options = {}) {
11574
+ function createEnvelopment(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
11032
11575
  const [c1, c2, c3, c4] = coordinates;
11033
11576
  const p1 = project(c1[0], c1[1]);
11034
11577
  const p2 = project(c2[0], c2[1]);
@@ -11043,8 +11586,10 @@ function createEnvelopment(coordinates, options = {}) {
11043
11586
  };
11044
11587
  const straightDirection = vecNorm(straight);
11045
11588
  const p3 = vecAdd(p2, vecScale(straightDirection, diameter));
11046
- const labelPoint = vecAdd(p1, vecScale(straight, clamp01(options.labelPosition ?? DEFAULT_ENVELOPMENT_OPTIONS.labelPosition)));
11047
- const gapHalf = straightLength * clamp01(options.labelGapRatio ?? DEFAULT_ENVELOPMENT_OPTIONS.labelGapRatio) / 2;
11589
+ const labelPosition = clamp01(options.labelPosition ?? DEFAULT_ENVELOPMENT_OPTIONS.labelPosition);
11590
+ const labelPoint = vecAdd(p1, vecScale(straight, labelPosition));
11591
+ const labelRotation = labelRotationAlong(straightDirection);
11592
+ const gapHalf = Math.min(straightLength * Math.min(labelPosition, 1 - labelPosition), labelKnockoutHalfExtent("E", straightDirection, labelRotation, options, context, DEFAULT_ENVELOPMENT_OPTIONS.labelPadding));
11048
11593
  const gapStart = vecAdd(labelPoint, vecScale(straightDirection, -gapHalf));
11049
11594
  const gapEnd = vecAdd(labelPoint, vecScale(straightDirection, gapHalf));
11050
11595
  const center = midpoint(p2, p3);
@@ -11067,7 +11612,7 @@ function createEnvelopment(coordinates, options = {}) {
11067
11612
  arrowhead
11068
11613
  ]
11069
11614
  }
11070
- }, createLabelFeature(labelPoint, "E", labelRotationAlong(straightDirection), options)]
11615
+ }, createLabelFeature(labelPoint, "E", labelRotation, options)]
11071
11616
  };
11072
11617
  }
11073
11618
  const ENVELOPMENT = defineControlMeasure({
@@ -11086,12 +11631,83 @@ const ENVELOPMENT = defineControlMeasure({
11086
11631
  }
11087
11632
  });
11088
11633
  //#endregion
11634
+ //#region src/generators/cm34-mission-tasks/exploitation.ts
11635
+ const EXPLOITATION_FIN_DASH = [6, 4];
11636
+ const DEFAULT_EXPLOITATION_OPTIONS = {};
11637
+ const EXPLOITATION_METADATA = {
11638
+ id: "exploitation",
11639
+ name: "Exploit/Exploitation",
11640
+ description: "The arrow points in the direction of the action; its tip may indicate where the action is to conclude, and its base marks the tasked unit’s current or projected location.",
11641
+ entity: "Mission Tasks",
11642
+ entityType: "Exploit/Exploitation",
11643
+ value: "343100",
11644
+ minCoordinates: 3,
11645
+ maxCoordinates: 3,
11646
+ geometry: "line",
11647
+ geometryTypes: ["MultiLineString"],
11648
+ paints: {
11649
+ stroke: true,
11650
+ fill: "none",
11651
+ text: false
11652
+ },
11653
+ drawRule: "Line30"
11654
+ };
11655
+ /** Generates the Exploit/Exploitation mission task symbol (343100). */
11656
+ function createExploitation(coordinates, _options = {}) {
11657
+ const [c1, c2, c3] = coordinates;
11658
+ const p1 = project(c1[0], c1[1]);
11659
+ const p2 = project(c2[0], c2[1]);
11660
+ const p3 = project(c3[0], c3[1]);
11661
+ const axis = vecSub(p1, p2);
11662
+ const axisLength = vecMag(axis);
11663
+ const finLength = vecMag(vecSub(p3, p2));
11664
+ if (axisLength < 1e-6 || finLength < 1e-6) return {
11665
+ type: "FeatureCollection",
11666
+ features: []
11667
+ };
11668
+ const direction = vecNorm(axis);
11669
+ const chevron = (tip) => createArrowHead$1(tip, direction, finLength * Math.SQRT1_2, finLength * Math.SQRT2);
11670
+ const [finLeft, , finRight] = chevron(p2);
11671
+ return {
11672
+ type: "FeatureCollection",
11673
+ features: [{
11674
+ type: "Feature",
11675
+ properties: { part: "exploitation" },
11676
+ geometry: {
11677
+ type: "MultiLineString",
11678
+ coordinates: [[c2, c1], chevron(p1)]
11679
+ }
11680
+ }, {
11681
+ type: "Feature",
11682
+ properties: {
11683
+ part: "fins",
11684
+ style: { strokeDash: [...EXPLOITATION_FIN_DASH] }
11685
+ },
11686
+ geometry: {
11687
+ type: "MultiLineString",
11688
+ coordinates: [[c2, finLeft], [c2, finRight]]
11689
+ }
11690
+ }]
11691
+ };
11692
+ }
11693
+ const EXPLOITATION = defineControlMeasure({
11694
+ metadata: EXPLOITATION_METADATA,
11695
+ generator: createExploitation,
11696
+ defaultOptions: DEFAULT_EXPLOITATION_OPTIONS,
11697
+ rule: line30DrawRule,
11698
+ previewSample: { controlPoints: [
11699
+ [.8, 0],
11700
+ [-.6, 0],
11701
+ [-.9, .3]
11702
+ ] }
11703
+ });
11704
+ //#endregion
11089
11705
  //#region src/generators/cm34-mission-tasks/infiltration.ts
11090
11706
  const DEFAULT_INFILTRATION_OPTIONS = {
11091
11707
  arrowheadLengthRatio: .1,
11092
11708
  arrowheadWidthRatio: .1,
11093
11709
  labelPosition: .6,
11094
- labelGapRatio: .24,
11710
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING,
11095
11711
  resolution: 64
11096
11712
  };
11097
11713
  const INFILTRATION_METADATA = {
@@ -11112,6 +11728,7 @@ const INFILTRATION_METADATA = {
11112
11728
  },
11113
11729
  drawRule: "Line32",
11114
11730
  capturesLabelSize: true,
11731
+ labelGeometryUsesResolvedMetrics: true,
11115
11732
  params: [
11116
11733
  {
11117
11734
  key: "arrowheadLengthRatio",
@@ -11143,16 +11760,7 @@ const INFILTRATION_METADATA = {
11143
11760
  max: 1,
11144
11761
  step: .01
11145
11762
  },
11146
- {
11147
- key: "labelGapRatio",
11148
- presentationTier: "advanced",
11149
- label: "Label gap",
11150
- description: "Size of the initial-line cutout holding the IN label",
11151
- type: "number",
11152
- min: 0,
11153
- max: 1,
11154
- step: .01
11155
- },
11763
+ LABEL_PADDING_PARAM,
11156
11764
  ...ARC_RESOLUTION_PARAMS
11157
11765
  ]
11158
11766
  };
@@ -11178,7 +11786,7 @@ function tangentDirection(p1, p2, p3) {
11178
11786
  return direction;
11179
11787
  }
11180
11788
  /** Generates the Infiltration mission task symbol (343800). */
11181
- function createInfiltration(coordinates, options = {}) {
11789
+ function createInfiltration(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
11182
11790
  const [c1, c2, c3] = coordinates;
11183
11791
  const p1 = project(c1[0], c1[1]);
11184
11792
  const p2 = project(c2[0], c2[1]);
@@ -11211,8 +11819,10 @@ function createInfiltration(coordinates, options = {}) {
11211
11819
  const arcResolution = normalizeArcResolution(options.resolution) / 2;
11212
11820
  const firstArc = sampleProjectedArc(firstCenter, sideRadius, alongRadius, 0, Math.PI / 2, arcResolution);
11213
11821
  const secondArc = sampleProjectedArc(secondCenter, vecScale(alongRadius, -1), vecScale(sideRadius, -1), 0, Math.PI / 2, arcResolution);
11214
- const labelPoint = vecAdd(p1, vecScale(straight, clamp01(options.labelPosition ?? DEFAULT_INFILTRATION_OPTIONS.labelPosition)));
11215
- const gapHalf = straightLength * clamp01(options.labelGapRatio ?? DEFAULT_INFILTRATION_OPTIONS.labelGapRatio) / 2;
11822
+ const labelPosition = clamp01(options.labelPosition ?? DEFAULT_INFILTRATION_OPTIONS.labelPosition);
11823
+ const labelPoint = vecAdd(p1, vecScale(straight, labelPosition));
11824
+ const labelRotation = labelRotationAlong(along);
11825
+ const gapHalf = Math.min(straightLength * Math.min(labelPosition, 1 - labelPosition), labelKnockoutHalfExtent("IN", along, labelRotation, options, context, DEFAULT_INFILTRATION_OPTIONS.labelPadding));
11216
11826
  const gapStart = vecAdd(labelPoint, vecScale(along, -gapHalf));
11217
11827
  const gapEnd = vecAdd(labelPoint, vecScale(along, gapHalf));
11218
11828
  const arrowhead = createArrowHead$1(p3, along, spanLength * Math.max(0, options.arrowheadLengthRatio ?? DEFAULT_INFILTRATION_OPTIONS.arrowheadLengthRatio), spanLength * Math.max(0, options.arrowheadWidthRatio ?? DEFAULT_INFILTRATION_OPTIONS.arrowheadWidthRatio));
@@ -11232,7 +11842,7 @@ function createInfiltration(coordinates, options = {}) {
11232
11842
  arrowhead
11233
11843
  ]
11234
11844
  }
11235
- }, createLabelFeature(labelPoint, "IN", labelRotationAlong(along), options)]
11845
+ }, createLabelFeature(labelPoint, "IN", labelRotation, options)]
11236
11846
  };
11237
11847
  }
11238
11848
  const INFILTRATION = defineControlMeasure({
@@ -11519,6 +12129,7 @@ const DROP_ZONE_METADATA = {
11519
12129
  },
11520
12130
  drawRule: "Area1",
11521
12131
  capturesLabelSize: true,
12132
+ labelGeometryUsesResolvedMetrics: ["N"],
11522
12133
  params: PREFIXED_LABELED_AREA_PARAMS,
11523
12134
  textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
11524
12135
  };
@@ -11583,6 +12194,7 @@ const ENCIRCLEMENT_METADATA = {
11583
12194
  },
11584
12195
  drawRule: "Area1",
11585
12196
  capturesLabelSize: true,
12197
+ labelGeometryUsesResolvedMetrics: ["N"],
11586
12198
  params: ENCIRCLEMENT_PARAMS,
11587
12199
  textAmplifiers: [{
11588
12200
  key: "N",
@@ -11736,6 +12348,7 @@ const EXTRACTION_ZONE_METADATA = {
11736
12348
  },
11737
12349
  drawRule: "Area1",
11738
12350
  capturesLabelSize: true,
12351
+ labelGeometryUsesResolvedMetrics: ["N"],
11739
12352
  params: PREFIXED_LABELED_AREA_PARAMS,
11740
12353
  textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
11741
12354
  };
@@ -12100,7 +12713,7 @@ const DEFAULT_FIX_MISSION_TASK_OPTIONS = {
12100
12713
  zigzagEndRatio: .8,
12101
12714
  zigzagPeakCount: 5,
12102
12715
  labelPositionRatio: .09,
12103
- labelGapRatio: .1
12716
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING
12104
12717
  };
12105
12718
  const FIX_MISSION_TASK_METADATA = {
12106
12719
  id: "fix-mission-task",
@@ -12120,6 +12733,7 @@ const FIX_MISSION_TASK_METADATA = {
12120
12733
  },
12121
12734
  drawRule: "Line9",
12122
12735
  capturesLabelSize: true,
12736
+ labelGeometryUsesResolvedMetrics: true,
12123
12737
  params: [
12124
12738
  {
12125
12739
  key: "arrowheadLengthRatio",
@@ -12181,20 +12795,11 @@ const FIX_MISSION_TASK_METADATA = {
12181
12795
  max: 1,
12182
12796
  step: .01
12183
12797
  },
12184
- {
12185
- key: "labelGapRatio",
12186
- presentationTier: "advanced",
12187
- label: "Label gap",
12188
- description: "Size of the shaft gap holding the F label",
12189
- type: "number",
12190
- min: 0,
12191
- max: 1,
12192
- step: .01
12193
- }
12798
+ LABEL_PADDING_PARAM
12194
12799
  ]
12195
12800
  };
12196
12801
  /** Generates the Fix mission task symbol (341100). */
12197
- function createFixMissionTaskSymbol(coordinates, options = {}) {
12802
+ function createFixMissionTaskSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
12198
12803
  const c1 = coordinates[0];
12199
12804
  const c2 = coordinates[1];
12200
12805
  const tip = project(c1[0], c1[1]);
@@ -12227,12 +12832,12 @@ function createFixMissionTaskSymbol(coordinates, options = {}) {
12227
12832
  const exitRatio = Math.min(1, zigzagEnd + toothStep / 2);
12228
12833
  const zigzagEntry = vecAdd(tail, vecScale(along, axisLength * entryRatio));
12229
12834
  const zigzagExit = vecAdd(tail, vecScale(along, axisLength * exitRatio));
12230
- const labelPosition = clamp01(options.labelPositionRatio ?? DEFAULT_FIX_MISSION_TASK_OPTIONS.labelPositionRatio);
12231
- const labelGap = axisLength * clamp01(options.labelGapRatio ?? DEFAULT_FIX_MISSION_TASK_OPTIONS.labelGapRatio);
12232
- const labelDistance = axisLength * labelPosition;
12835
+ const labelDistance = axisLength * clamp01(options.labelPositionRatio ?? DEFAULT_FIX_MISSION_TASK_OPTIONS.labelPositionRatio);
12233
12836
  const labelCenter = vecAdd(tail, vecScale(along, labelDistance));
12234
- const gapStartDistance = Math.max(0, labelDistance - labelGap / 2);
12235
- const gapEndDistance = Math.min(axisLength, labelDistance + labelGap / 2);
12837
+ const labelRotation = labelRotationAlong(along);
12838
+ const gapHalf = labelKnockoutHalfExtent("F", along, labelRotation, options, context, DEFAULT_FIX_MISSION_TASK_OPTIONS.labelPadding);
12839
+ const gapStartDistance = Math.max(0, labelDistance - gapHalf);
12840
+ const gapEndDistance = Math.min(axisLength, labelDistance + gapHalf);
12236
12841
  const gapStart = vecAdd(tail, vecScale(along, gapStartDistance));
12237
12842
  const gapEnd = vecAdd(tail, vecScale(along, gapEndDistance));
12238
12843
  return {
@@ -12254,7 +12859,7 @@ function createFixMissionTaskSymbol(coordinates, options = {}) {
12254
12859
  createArrowHead$1(tip, along, arrowheadLength, arrowheadWidth)
12255
12860
  ]
12256
12861
  }
12257
- }, createLabelFeature(labelCenter, "F", labelRotationAlong(along), options)]
12862
+ }, createLabelFeature(labelCenter, "F", labelRotation, options)]
12258
12863
  };
12259
12864
  }
12260
12865
  const FIX_MISSION_TASK = defineControlMeasure({
@@ -12299,6 +12904,7 @@ const FLOT_METADATA = {
12299
12904
  },
12300
12905
  drawRule: "Line1",
12301
12906
  capturesLabelSize: true,
12907
+ labelGeometryUsesResolvedMetrics: ["N"],
12302
12908
  params: [
12303
12909
  ...SMOOTH_LINE_PARAMS,
12304
12910
  {
@@ -12321,7 +12927,7 @@ const FLOT_METADATA = {
12321
12927
  step: 1,
12322
12928
  unit: "m"
12323
12929
  },
12324
- LINE_LABEL_PADDING_PARAM,
12930
+ LABEL_PADDING_PARAM,
12325
12931
  {
12326
12932
  key: "arcSegments",
12327
12933
  presentationTier: "advanced",
@@ -12643,7 +13249,7 @@ function phaseLineNameParams(placeholder) {
12643
13249
  type: "boolean",
12644
13250
  visibleWhen: (opts) => Boolean(String(opts.phaseLineName ?? "").trim())
12645
13251
  },
12646
- LINE_LABEL_PADDING_PARAM
13252
+ LABEL_PADDING_PARAM
12647
13253
  ];
12648
13254
  }
12649
13255
  //#endregion
@@ -12828,6 +13434,7 @@ const AIRHEAD_LINE_METADATA = {
12828
13434
  },
12829
13435
  drawRule: "Area1",
12830
13436
  capturesLabelSize: true,
13437
+ labelGeometryUsesResolvedMetrics: ["N"],
12831
13438
  params: LABELED_AREA_PARAMS,
12832
13439
  textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
12833
13440
  };
@@ -13481,6 +14088,7 @@ const FORTIFIED_LINE_METADATA = {
13481
14088
  },
13482
14089
  drawRule: "Line1",
13483
14090
  capturesLabelSize: true,
14091
+ labelGeometryUsesResolvedMetrics: ["N"],
13484
14092
  params: FORTIFIED_PARAMS,
13485
14093
  textAmplifiers: [HOSTILE_LINE_AMPLIFIER]
13486
14094
  };
@@ -13641,6 +14249,7 @@ const FORTIFIED_AREA_METADATA = {
13641
14249
  },
13642
14250
  drawRule: "Area1",
13643
14251
  capturesLabelSize: true,
14252
+ labelGeometryUsesResolvedMetrics: ["N"],
13644
14253
  params: [...FORTIFIED_AREA_PARAMS, AREA_LABEL_PADDING_PARAM],
13645
14254
  textAmplifiers: AREA_TEXT_AMPLIFIERS
13646
14255
  };
@@ -13765,142 +14374,45 @@ const FRONTAL_ATTACK = defineControlMeasure({
13765
14374
  }
13766
14375
  });
13767
14376
  //#endregion
13768
- //#region src/generators/cm34-mission-tasks/isolate.ts
13769
- /**
13770
- * Default options for the ISOLATE symbol.
13771
- */
13772
- const DEFAULT_ISOLATE_OPTIONS = {
13773
- openingAngle: 30,
13774
- barbCount: 9,
13775
- barbLengthRatio: .18
13776
- };
13777
- const ISOLATE_METADATA = {
13778
- id: "isolate",
13779
- name: "Isolate",
13780
- description: "Seals an enemy off from support, denies freedom of movement, and prevents contact with other enemy forces while offensive action continues against it.",
14377
+ //#region src/generators/cm34-mission-tasks/forward-passage-of-lines.ts
14378
+ const DEFAULT_FORWARD_PASSAGE_OF_LINES_OPTIONS = DEFAULT_DELAY_OPTIONS;
14379
+ const FORWARD_PASSAGE_OF_LINES_METADATA = {
14380
+ id: "forward-passage-of-lines",
14381
+ name: "Forward Passage of Lines",
14382
+ description: "Passes one force through another force's positions toward the enemy.",
13781
14383
  entity: "Mission Tasks",
13782
- entityType: "Isolate",
13783
- value: "341500",
13784
- minCoordinates: 2,
13785
- maxCoordinates: 2,
14384
+ entityType: "Forward Passage of Lines",
14385
+ value: "344100",
14386
+ minCoordinates: 3,
14387
+ maxCoordinates: 3,
13786
14388
  geometry: "line",
13787
- geometryTypes: ["MultiLineString"],
14389
+ geometryTypes: ["MultiLineString", "Point"],
13788
14390
  paints: {
13789
14391
  stroke: true,
13790
14392
  fill: "none",
13791
- text: false
14393
+ text: true
13792
14394
  },
13793
- drawRule: "Area15",
13794
- params: [
13795
- {
13796
- key: "openingAngle",
13797
- presentationTier: "advanced",
13798
- label: "Opening",
13799
- description: "Angular width of the gap on the friendly side, in degrees",
13800
- type: "number",
13801
- min: 0,
13802
- max: 180,
13803
- step: 1
13804
- },
13805
- {
13806
- key: "barbCount",
13807
- presentationTier: "advanced",
13808
- label: "Barbs",
13809
- description: "Number of inward-pointing barbs spaced along the closed arc",
13810
- type: "number",
13811
- min: 1,
13812
- max: 24,
13813
- step: 1
13814
- },
13815
- {
13816
- key: "barbLengthRatio",
13817
- presentationTier: "advanced",
13818
- label: "Barb length",
13819
- description: "Length of each inward barb, as a ratio of the symbol radius",
13820
- type: "number",
13821
- min: .01,
13822
- max: 1,
13823
- step: .01
13824
- }
13825
- ]
14395
+ drawRule: "Line24",
14396
+ capturesLabelSize: true,
14397
+ labelGeometryUsesResolvedMetrics: true,
14398
+ params: DELAY_PARAMS
13826
14399
  };
13827
- /**
13828
- * Generates a GeoJSON FeatureCollection for an ISOLATE mission task symbol (341500).
13829
- *
13830
- * A near-complete circle centered on PT. 1 whose radius reaches PT. 2. The arc
13831
- * starts (bare) at PT. 2's bearing and sweeps clockwise around the closed
13832
- * portion to a single arrow tip at the far end; the wedge-shaped gap (default
13833
- * 30°) between that arrow tip and PT. 2 is the opening — the "friendly side".
13834
- * Inward-pointing arrowhead barbs are spaced along the closed arc, set in from
13835
- * both ends so they never touch PT. 2 or the arrow tip, conveying the
13836
- * sealing-off of the enclosed force. Unlike the bracket mission tasks, isolate
13837
- * carries no glyph label.
13838
- *
13839
- * Input Points:
13840
- * - PT. 1: Center point
13841
- * - PT. 2: Start point — fixes the radius and the bearing of the opening
13842
- *
13843
- * @param coordinates - [PT. 1, PT. 2] as Position arrays
13844
- * @param options - Configuration options
13845
- * @returns GeoJSON FeatureCollection containing a single MultiLineString feature
13846
- */
13847
- function createIsolateSymbol(coordinates, options = {}) {
13848
- const openingAngle = options.openingAngle ?? DEFAULT_ISOLATE_OPTIONS.openingAngle;
13849
- const barbCount = options.barbCount ?? DEFAULT_ISOLATE_OPTIONS.barbCount;
13850
- const barbLengthRatio = options.barbLengthRatio ?? DEFAULT_ISOLATE_OPTIONS.barbLengthRatio;
13851
- const center = project(coordinates[0][0], coordinates[0][1]);
13852
- const vRadius = vecSub(project(coordinates[1][0], coordinates[1][1]), center);
13853
- const radius = vecMag(vRadius);
13854
- if (radius < 1e-6) return {
13855
- type: "FeatureCollection",
13856
- features: []
13857
- };
13858
- const startAngle = Math.atan2(vRadius[1], vRadius[0]);
13859
- const openingRad = Math.min(Math.max(openingAngle, 0), 359) * Math.PI / 180;
13860
- const arcSpan = 2 * Math.PI - openingRad;
13861
- const endAngle = startAngle - arcSpan;
13862
- const pointOnCircle = (angle, r = radius) => vecAdd(center, [r * Math.cos(angle), r * Math.sin(angle)]);
13863
- const segments = Math.max(8, Math.round(64 * arcSpan / (2 * Math.PI)));
13864
- const arc = [];
13865
- for (let i = 0; i <= segments; i++) {
13866
- const p = pointOnCircle(startAngle - arcSpan * i / segments);
13867
- arc.push(unproject(p[0], p[1]));
13868
- }
13869
- const barbLength = radius * barbLengthRatio;
13870
- const lines = [arc];
13871
- const tipDir = [Math.sin(endAngle), -Math.cos(endAngle)];
13872
- lines.push(createArrowHead$1(pointOnCircle(endAngle), tipDir, barbLength, barbLength));
13873
- const halfWidth = Math.asin(Math.min(1, barbLength / 2 / radius));
13874
- const tipRadius = Math.max(0, radius - barbLength);
13875
- const steps = Math.max(1, barbCount);
13876
- for (let k = 1; k <= steps; k++) {
13877
- const angle = startAngle - arcSpan * k / (steps + 1);
13878
- const left = pointOnCircle(angle - halfWidth);
13879
- const right = pointOnCircle(angle + halfWidth);
13880
- const tip = pointOnCircle(angle, tipRadius);
13881
- lines.push([
13882
- unproject(left[0], left[1]),
13883
- unproject(tip[0], tip[1]),
13884
- unproject(right[0], right[1])
13885
- ]);
13886
- }
13887
- return {
13888
- type: "FeatureCollection",
13889
- features: [{
13890
- type: "Feature",
13891
- properties: { part: "isolate" },
13892
- geometry: {
13893
- type: "MultiLineString",
13894
- coordinates: lines
13895
- }
13896
- }]
13897
- };
13898
- }
13899
- const ISOLATE = defineControlMeasure({
13900
- metadata: ISOLATE_METADATA,
13901
- generator: createIsolateSymbol,
13902
- defaultOptions: DEFAULT_ISOLATE_OPTIONS,
13903
- rule: isolateDrawRule
14400
+ function createForwardPassageOfLinesSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
14401
+ return createDelayVariantSymbol(coordinates, {
14402
+ glyph: "P(F)",
14403
+ part: "forward-passage-of-lines"
14404
+ }, options, context);
14405
+ }
14406
+ const FORWARD_PASSAGE_OF_LINES = defineControlMeasure({
14407
+ metadata: FORWARD_PASSAGE_OF_LINES_METADATA,
14408
+ generator: createForwardPassageOfLinesSymbol,
14409
+ defaultOptions: DEFAULT_FORWARD_PASSAGE_OF_LINES_OPTIONS,
14410
+ rule: line24DrawRule,
14411
+ previewSample: { controlPoints: [
14412
+ [1, -.4],
14413
+ [-1, -.4],
14414
+ [0, .6]
14415
+ ] }
13904
14416
  });
13905
14417
  //#endregion
13906
14418
  //#region src/generators/cm34-mission-tasks/movement-to-contact.ts
@@ -14098,77 +14610,196 @@ const MOVEMENT_TO_CONTACT = defineControlMeasure({
14098
14610
  }
14099
14611
  });
14100
14612
  //#endregion
14101
- //#region src/generators/cm34-mission-tasks/penetrate.ts
14102
- const DEFAULT_PENETRATE_OPTIONS = {
14103
- arrowheadLengthRatio: .15,
14104
- arrowheadWidthRatio: .2,
14105
- labelGapRatio: .2
14106
- };
14107
- const PENETRATE_METADATA = {
14108
- id: "penetrate",
14109
- name: "Penetrate",
14110
- description: "Breaks through an enemy defense and disrupts the continuity of the defensive system.",
14613
+ //#region src/generators/cm34-mission-tasks/occupy.ts
14614
+ const DEFAULT_OCCUPY_OPTIONS = {
14615
+ openingAngle: 30,
14616
+ resolution: 64,
14617
+ xSizeRatio: .2,
14618
+ xAspectRatio: 1.5,
14619
+ labelPadding: .2
14620
+ };
14621
+ const OCCUPY_METADATA = {
14622
+ id: "occupy",
14623
+ name: "Occupy",
14624
+ description: "Moves a friendly force into an area so it can control that area.",
14111
14625
  entity: "Mission Tasks",
14112
- entityType: "Penetrate",
14113
- value: "341800",
14114
- minCoordinates: 3,
14115
- maxCoordinates: 3,
14116
- geometry: "line",
14626
+ entityType: "Occupy",
14627
+ value: "341700",
14628
+ minCoordinates: 2,
14629
+ maxCoordinates: 2,
14630
+ geometry: "area",
14117
14631
  geometryTypes: ["MultiLineString", "Point"],
14118
14632
  paints: {
14119
14633
  stroke: true,
14120
14634
  fill: "none",
14121
14635
  text: true
14122
14636
  },
14123
- drawRule: "Area17",
14637
+ drawRule: "Area16",
14124
14638
  capturesLabelSize: true,
14639
+ labelGeometryUsesResolvedMetrics: true,
14125
14640
  params: [
14126
14641
  {
14127
- key: "arrowheadLengthRatio",
14642
+ key: "openingAngle",
14128
14643
  presentationTier: "advanced",
14129
- label: "Arrowhead length",
14130
- description: "Arrowhead length as a ratio of the barrier length",
14644
+ label: "Opening",
14645
+ description: "Angular width of the opening on the friendly side, in degrees",
14131
14646
  type: "number",
14132
- min: .01,
14133
- max: 1,
14134
- step: .01
14647
+ min: 5,
14648
+ max: 180,
14649
+ step: 1
14135
14650
  },
14651
+ ...ARC_RESOLUTION_PARAMS,
14136
14652
  {
14137
- key: "arrowheadWidthRatio",
14653
+ key: "xSizeRatio",
14138
14654
  presentationTier: "advanced",
14139
- label: "Arrowhead width",
14140
- description: "Arrowhead base width as a ratio of the barrier length",
14655
+ label: "X size",
14656
+ description: "Overall size of the terminal X as a ratio of the symbol radius",
14141
14657
  type: "number",
14142
- min: .01,
14143
- max: 1,
14658
+ min: .02,
14659
+ max: .75,
14144
14660
  step: .01
14145
14661
  },
14146
14662
  {
14147
- key: "labelGapRatio",
14663
+ key: "xAspectRatio",
14664
+ presentationTier: "advanced",
14665
+ label: "X aspect ratio",
14666
+ description: "Width-to-height ratio of the terminal X",
14667
+ type: "number",
14668
+ min: .25,
14669
+ max: 4,
14670
+ step: .05
14671
+ },
14672
+ {
14673
+ key: "labelPadding",
14148
14674
  presentationTier: "advanced",
14149
- label: "Label gap",
14150
- description: "Size of the shaft gap holding the 'P' label",
14675
+ label: "O padding",
14676
+ description: "Extra boundary clearance on each side of O, as a ratio of its height",
14151
14677
  type: "number",
14152
14678
  min: 0,
14153
- max: 1,
14154
- step: .01
14679
+ max: 2,
14680
+ step: .05
14155
14681
  }
14156
14682
  ]
14157
14683
  };
14158
- /** Generates the PENETRATE mission task symbol (341800). */
14159
- function createPenetrateSymbol(coordinates, options = {}) {
14160
- const frame = createProjectedBaselineFrame(project(coordinates[0][0], coordinates[0][1]), project(coordinates[1][0], coordinates[1][1]), {
14161
- origin: "midpoint",
14162
- normal: "right"
14163
- });
14164
- if (!frame) return {
14165
- type: "FeatureCollection",
14166
- features: []
14167
- };
14168
- const c3 = coordinates[2];
14169
- const signedTailDistance = c3 ? frame.signedNormalDistance(c3) : frame.length * 1.5;
14170
- const shaftLength = Math.abs(signedTailDistance);
14171
- if (shaftLength < 1e-6) return {
14684
+ const TAU$2 = Math.PI * 2;
14685
+ const MIN_LABEL_GAP_METERS$2 = 1;
14686
+ /** Generates the Occupy mission task symbol (341700). */
14687
+ function createOccupySymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
14688
+ const center = project(coordinates[0][0], coordinates[0][1]);
14689
+ const radiusVector = vecSub(project(coordinates[1][0], coordinates[1][1]), center);
14690
+ const radius = vecMag(radiusVector);
14691
+ if (radius < 1e-6) return {
14692
+ type: "FeatureCollection",
14693
+ features: []
14694
+ };
14695
+ const resolved = {
14696
+ ...DEFAULT_OCCUPY_OPTIONS,
14697
+ ...options
14698
+ };
14699
+ const bulgeVector = [radiusVector[1], -radiusVector[0]];
14700
+ const halfTurnResolution = normalizeArcResolution(resolved.resolution) / 2;
14701
+ const boundarySpan = TAU$2 - Math.min(180, Math.max(5, resolved.openingAngle)) * Math.PI / 180;
14702
+ const labelSweep = boundarySpan / 2;
14703
+ const labelHeight = resolveTextMetrics("O", options, context, "regular", radius * .12).height;
14704
+ const labelPadding = Math.max(0, resolved.labelPadding);
14705
+ const labelHalfGap = Math.min(boundarySpan / 4, Math.max(labelHeight * (LABEL_HALF_GLYPH_RATIO + labelPadding), MIN_LABEL_GAP_METERS$2) / radius);
14706
+ const sampleArc = (from, to) => sampleProjectedArc(center, radiusVector, bulgeVector, from, to, halfTurnResolution);
14707
+ const lines = [sampleArc(0, labelSweep - labelHalfGap), sampleArc(labelSweep + labelHalfGap, boundarySpan)];
14708
+ const terminal = arcPointAt(center, radiusVector, bulgeVector, boundarySpan);
14709
+ const radial = vecScale(vecSub(terminal, center), 1 / radius);
14710
+ const tangent = [radial[1], -radial[0]];
14711
+ const halfHeight = radius * Math.max(0, resolved.xSizeRatio) / 2;
14712
+ const radialHalfWidth = vecScale(radial, halfHeight * Math.max(.25, resolved.xAspectRatio));
14713
+ const tangentHalfHeight = vecScale(tangent, halfHeight);
14714
+ const stroke = (diagonal) => [vecAdd(terminal, diagonal), vecSub(terminal, diagonal)].map((point) => unproject(point[0], point[1]));
14715
+ lines.push(stroke(vecAdd(radialHalfWidth, tangentHalfHeight)), stroke(vecSub(radialHalfWidth, tangentHalfHeight)));
14716
+ const labelPoint = arcPointAt(center, radiusVector, bulgeVector, labelSweep);
14717
+ return {
14718
+ type: "FeatureCollection",
14719
+ features: [{
14720
+ type: "Feature",
14721
+ properties: { part: "occupy" },
14722
+ geometry: {
14723
+ type: "MultiLineString",
14724
+ coordinates: lines
14725
+ }
14726
+ }, createLabelFeature(labelPoint, "O", Math.PI, options)]
14727
+ };
14728
+ }
14729
+ const OCCUPY = defineControlMeasure({
14730
+ metadata: OCCUPY_METADATA,
14731
+ generator: createOccupySymbol,
14732
+ defaultOptions: DEFAULT_OCCUPY_OPTIONS,
14733
+ rule: occupyDrawRule,
14734
+ previewSample: {
14735
+ controlPoints: [[0, 0], [-.8, .2]],
14736
+ options: { labelSize: 90 }
14737
+ }
14738
+ });
14739
+ //#endregion
14740
+ //#region src/generators/cm34-mission-tasks/penetrate.ts
14741
+ const DEFAULT_PENETRATE_OPTIONS = {
14742
+ arrowheadLengthRatio: .15,
14743
+ arrowheadWidthRatio: .2,
14744
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING
14745
+ };
14746
+ const PENETRATE_METADATA = {
14747
+ id: "penetrate",
14748
+ name: "Penetrate",
14749
+ description: "Breaks through an enemy defense and disrupts the continuity of the defensive system.",
14750
+ entity: "Mission Tasks",
14751
+ entityType: "Penetrate",
14752
+ value: "341800",
14753
+ minCoordinates: 3,
14754
+ maxCoordinates: 3,
14755
+ geometry: "line",
14756
+ geometryTypes: ["MultiLineString", "Point"],
14757
+ paints: {
14758
+ stroke: true,
14759
+ fill: "none",
14760
+ text: true
14761
+ },
14762
+ drawRule: "Area17",
14763
+ capturesLabelSize: true,
14764
+ labelGeometryUsesResolvedMetrics: true,
14765
+ params: [
14766
+ {
14767
+ key: "arrowheadLengthRatio",
14768
+ presentationTier: "advanced",
14769
+ label: "Arrowhead length",
14770
+ description: "Arrowhead length as a ratio of the barrier length",
14771
+ type: "number",
14772
+ min: .01,
14773
+ max: 1,
14774
+ step: .01
14775
+ },
14776
+ {
14777
+ key: "arrowheadWidthRatio",
14778
+ presentationTier: "advanced",
14779
+ label: "Arrowhead width",
14780
+ description: "Arrowhead base width as a ratio of the barrier length",
14781
+ type: "number",
14782
+ min: .01,
14783
+ max: 1,
14784
+ step: .01
14785
+ },
14786
+ LABEL_PADDING_PARAM
14787
+ ]
14788
+ };
14789
+ /** Generates the PENETRATE mission task symbol (341800). */
14790
+ function createPenetrateSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
14791
+ const frame = createProjectedBaselineFrame(project(coordinates[0][0], coordinates[0][1]), project(coordinates[1][0], coordinates[1][1]), {
14792
+ origin: "midpoint",
14793
+ normal: "right"
14794
+ });
14795
+ if (!frame) return {
14796
+ type: "FeatureCollection",
14797
+ features: []
14798
+ };
14799
+ const c3 = coordinates[2];
14800
+ const signedTailDistance = c3 ? frame.signedNormalDistance(c3) : frame.length * 1.5;
14801
+ const shaftLength = Math.abs(signedTailDistance);
14802
+ if (shaftLength < 1e-6) return {
14172
14803
  type: "FeatureCollection",
14173
14804
  features: []
14174
14805
  };
@@ -14176,29 +14807,29 @@ function createPenetrateSymbol(coordinates, options = {}) {
14176
14807
  const tail = vecAdd(frame.origin, vecScale(frame.normal, signedTailDistance));
14177
14808
  const arrowDirection = vecScale(vecSub(tip, tail), 1 / shaftLength);
14178
14809
  const labelPoint = vecScale(vecAdd(tail, tip), .5);
14179
- const gapHalf = shaftLength * Math.min(1, Math.max(0, options.labelGapRatio ?? DEFAULT_PENETRATE_OPTIONS.labelGapRatio)) / 2;
14810
+ const shaftRotation = Math.atan2(arrowDirection[1], arrowDirection[0]);
14811
+ const renderedLabelRotation = Math.PI - shaftRotation;
14812
+ const labelRotation = normalizeRadians(Math.PI - keepTextLeftToRight(renderedLabelRotation));
14813
+ const gapHalf = Math.min(shaftLength / 2, labelKnockoutHalfExtent("P", arrowDirection, labelRotation, options, context, DEFAULT_PENETRATE_OPTIONS.labelPadding));
14180
14814
  const gapStart = vecAdd(labelPoint, vecScale(arrowDirection, -gapHalf));
14181
14815
  const gapEnd = vecAdd(labelPoint, vecScale(arrowDirection, gapHalf));
14182
14816
  const headLength = frame.length * Math.max(0, options.arrowheadLengthRatio ?? DEFAULT_PENETRATE_OPTIONS.arrowheadLengthRatio);
14183
14817
  const headWidth = frame.length * Math.max(0, options.arrowheadWidthRatio ?? DEFAULT_PENETRATE_OPTIONS.arrowheadWidthRatio);
14184
- const lineFeature = {
14185
- type: "Feature",
14186
- properties: { part: "penetrate" },
14187
- geometry: {
14188
- type: "MultiLineString",
14189
- coordinates: [
14190
- [coordinates[0], coordinates[1]],
14191
- [unproject(tail[0], tail[1]), unproject(gapStart[0], gapStart[1])],
14192
- [unproject(gapEnd[0], gapEnd[1]), unproject(tip[0], tip[1])],
14193
- createArrowHead$1(tip, arrowDirection, headLength, headWidth)
14194
- ]
14195
- }
14196
- };
14197
- const shaftRotation = Math.atan2(arrowDirection[1], arrowDirection[0]);
14198
- const renderedLabelRotation = Math.PI - shaftRotation;
14199
14818
  return {
14200
14819
  type: "FeatureCollection",
14201
- features: [lineFeature, createLabelFeature(labelPoint, "P", normalizeRadians(Math.PI - keepTextLeftToRight(renderedLabelRotation)), {
14820
+ features: [{
14821
+ type: "Feature",
14822
+ properties: { part: "penetrate" },
14823
+ geometry: {
14824
+ type: "MultiLineString",
14825
+ coordinates: [
14826
+ [coordinates[0], coordinates[1]],
14827
+ [unproject(tail[0], tail[1]), unproject(gapStart[0], gapStart[1])],
14828
+ [unproject(gapEnd[0], gapEnd[1]), unproject(tip[0], tip[1])],
14829
+ createArrowHead$1(tip, arrowDirection, headLength, headWidth)
14830
+ ]
14831
+ }
14832
+ }, createLabelFeature(labelPoint, "P", labelRotation, {
14202
14833
  labelSize: options.labelSize,
14203
14834
  labelSizePixels: options.labelSizePixels
14204
14835
  })]
@@ -14216,6 +14847,149 @@ const PENETRATE = defineControlMeasure({
14216
14847
  ] }
14217
14848
  });
14218
14849
  //#endregion
14850
+ //#region src/generators/cm34-mission-tasks/pursuit.ts
14851
+ const DEFAULT_PURSUIT_OPTIONS = {
14852
+ arrowheadLengthRatio: .1,
14853
+ arrowheadWidthRatio: .12,
14854
+ terminalBarLengthRatio: 1.5,
14855
+ labelPosition: .6,
14856
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING,
14857
+ resolution: 64
14858
+ };
14859
+ const PURSUIT_METADATA = {
14860
+ id: "pursuit",
14861
+ name: "Pursuit",
14862
+ description: "A mission task to catch or cut off a hostile force attempting to escape, with the aim of destroying it.",
14863
+ entity: "Mission Tasks",
14864
+ entityType: "Pursuit",
14865
+ value: "344000",
14866
+ minCoordinates: 3,
14867
+ maxCoordinates: 3,
14868
+ geometry: "line",
14869
+ geometryTypes: ["MultiLineString", "Point"],
14870
+ paints: {
14871
+ stroke: true,
14872
+ fill: "none",
14873
+ text: true
14874
+ },
14875
+ drawRule: "Line33",
14876
+ capturesLabelSize: true,
14877
+ labelGeometryUsesResolvedMetrics: true,
14878
+ params: [
14879
+ {
14880
+ key: "arrowheadLengthRatio",
14881
+ presentationTier: "advanced",
14882
+ label: "Arrowhead length",
14883
+ description: "Arrowhead length as a ratio of the arc diameter",
14884
+ type: "number",
14885
+ min: .01,
14886
+ max: .5,
14887
+ step: .01
14888
+ },
14889
+ {
14890
+ key: "arrowheadWidthRatio",
14891
+ presentationTier: "advanced",
14892
+ label: "Arrowhead width",
14893
+ description: "Arrowhead width as a ratio of the arc diameter",
14894
+ type: "number",
14895
+ min: .01,
14896
+ max: .5,
14897
+ step: .01
14898
+ },
14899
+ {
14900
+ key: "terminalBarLengthRatio",
14901
+ presentationTier: "advanced",
14902
+ label: "Terminal bar length",
14903
+ description: "Terminal bar length as a multiple of the arrowhead width",
14904
+ type: "number",
14905
+ min: .1,
14906
+ max: 10,
14907
+ step: .1
14908
+ },
14909
+ {
14910
+ key: "labelPosition",
14911
+ presentationTier: "advanced",
14912
+ label: "Label position",
14913
+ description: "Position of the P label along the straight portion",
14914
+ type: "number",
14915
+ min: 0,
14916
+ max: 1,
14917
+ step: .01
14918
+ },
14919
+ LABEL_PADDING_PARAM,
14920
+ ...ARC_RESOLUTION_PARAMS
14921
+ ]
14922
+ };
14923
+ /** Generates the Pursuit mission task symbol (344000). */
14924
+ function createPursuit(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
14925
+ const [c1, c2, c3] = coordinates;
14926
+ const p1 = project(c1[0], c1[1]);
14927
+ const p2 = project(c2[0], c2[1]);
14928
+ const rawP3 = project(c3[0], c3[1]);
14929
+ const frame = createProjectedBaselineFrame(p1, p2, {
14930
+ origin: "p2",
14931
+ normal: "right"
14932
+ });
14933
+ if (!frame) return {
14934
+ type: "FeatureCollection",
14935
+ features: []
14936
+ };
14937
+ const { direction: straightDirection, normal: diameterAxis, length: straightLength } = frame;
14938
+ const signedDiameter = vecDot(vecSub(rawP3, frame.origin), diameterAxis);
14939
+ const diameter = Math.abs(signedDiameter);
14940
+ if (diameter < 1e-6) return {
14941
+ type: "FeatureCollection",
14942
+ features: []
14943
+ };
14944
+ const p3 = vecAdd(p2, vecScale(diameterAxis, signedDiameter));
14945
+ const labelPosition = clamp01(options.labelPosition ?? DEFAULT_PURSUIT_OPTIONS.labelPosition);
14946
+ const labelPoint = vecAdd(p1, vecScale(straightDirection, straightLength * labelPosition));
14947
+ const labelRotation = labelRotationAlong(straightDirection);
14948
+ const gapHalf = Math.min(straightLength * Math.min(labelPosition, 1 - labelPosition), labelKnockoutHalfExtent("P", straightDirection, labelRotation, options, context, DEFAULT_PURSUIT_OPTIONS.labelPadding));
14949
+ const gapStart = vecAdd(labelPoint, vecScale(straightDirection, -gapHalf));
14950
+ const gapEnd = vecAdd(labelPoint, vecScale(straightDirection, gapHalf));
14951
+ const center = midpoint(p2, p3);
14952
+ const arc = sampleProjectedArc(center, vecSub(p2, center), vecScale(straightDirection, diameter / 2), 0, Math.PI, normalizeArcResolution(options.resolution) / 2);
14953
+ const arrowheadLength = diameter * Math.max(0, options.arrowheadLengthRatio ?? DEFAULT_PURSUIT_OPTIONS.arrowheadLengthRatio);
14954
+ const arrowheadWidth = diameter * Math.max(0, options.arrowheadWidthRatio ?? DEFAULT_PURSUIT_OPTIONS.arrowheadWidthRatio);
14955
+ const arrowhead = createArrowHead$1(p3, vecScale(straightDirection, -1), arrowheadLength, arrowheadWidth);
14956
+ const terminalBarHalf = arrowheadWidth * Math.max(0, options.terminalBarLengthRatio ?? DEFAULT_PURSUIT_OPTIONS.terminalBarLengthRatio) / 2;
14957
+ const terminalStart = vecAdd(p3, vecScale(diameterAxis, terminalBarHalf));
14958
+ const terminalEnd = vecSub(p3, vecScale(diameterAxis, terminalBarHalf));
14959
+ const terminalBar = [unproject(terminalStart[0], terminalStart[1]), unproject(terminalEnd[0], terminalEnd[1])];
14960
+ return {
14961
+ type: "FeatureCollection",
14962
+ features: [{
14963
+ type: "Feature",
14964
+ properties: { part: "pursuit" },
14965
+ geometry: {
14966
+ type: "MultiLineString",
14967
+ coordinates: [
14968
+ [c1, unproject(gapStart[0], gapStart[1])],
14969
+ [unproject(gapEnd[0], gapEnd[1]), c2],
14970
+ arc,
14971
+ arrowhead,
14972
+ terminalBar
14973
+ ]
14974
+ }
14975
+ }, createLabelFeature(labelPoint, "P", labelRotation, options)]
14976
+ };
14977
+ }
14978
+ const PURSUIT = defineControlMeasure({
14979
+ metadata: PURSUIT_METADATA,
14980
+ generator: createPursuit,
14981
+ defaultOptions: DEFAULT_PURSUIT_OPTIONS,
14982
+ rule: line33DrawRule,
14983
+ previewSample: {
14984
+ controlPoints: [
14985
+ [-1, .4],
14986
+ [1, .4],
14987
+ [0, -.6]
14988
+ ],
14989
+ options: { labelSize: 100 }
14990
+ }
14991
+ });
14992
+ //#endregion
14219
14993
  //#region src/generators/cm34-mission-tasks/rearward-passage-of-lines.ts
14220
14994
  const DEFAULT_REARWARD_PASSAGE_OF_LINES_OPTIONS = DEFAULT_DELAY_OPTIONS;
14221
14995
  const REARWARD_PASSAGE_OF_LINES_METADATA = {
@@ -14236,27 +15010,80 @@ const REARWARD_PASSAGE_OF_LINES_METADATA = {
14236
15010
  },
14237
15011
  drawRule: "Line24",
14238
15012
  capturesLabelSize: true,
15013
+ labelGeometryUsesResolvedMetrics: true,
14239
15014
  params: DELAY_PARAMS
14240
15015
  };
14241
- function createRearwardPassageOfLinesSymbol(coordinates, options = {}) {
15016
+ function createRearwardPassageOfLinesSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
14242
15017
  return createDelayVariantSymbol(coordinates, {
14243
15018
  glyph: "P(R)",
14244
15019
  part: "rearward-passage-of-lines"
14245
- }, options);
15020
+ }, options, context);
14246
15021
  }
14247
15022
  const REARWARD_PASSAGE_OF_LINES = defineControlMeasure({
14248
15023
  metadata: REARWARD_PASSAGE_OF_LINES_METADATA,
14249
15024
  generator: createRearwardPassageOfLinesSymbol,
14250
15025
  defaultOptions: DEFAULT_REARWARD_PASSAGE_OF_LINES_OPTIONS,
14251
15026
  rule: line24DrawRule,
14252
- previewSample: {
14253
- controlPoints: [
14254
- [-1, -.4],
14255
- [1, -.4],
14256
- [0, .6]
14257
- ],
14258
- options: { labelGapRatio: .42 }
14259
- }
15027
+ previewSample: { controlPoints: [
15028
+ [-1, -.4],
15029
+ [1, -.4],
15030
+ [0, .6]
15031
+ ] }
15032
+ });
15033
+ //#endregion
15034
+ //#region src/generators/cm34-mission-tasks/relief-in-place.ts
15035
+ const DEFAULT_RELIEF_IN_PLACE_OPTIONS = {
15036
+ arrowheadLengthRatio: DEFAULT_DELAY_OPTIONS.arrowheadLengthRatio,
15037
+ arrowheadWidthRatio: DEFAULT_DELAY_OPTIONS.arrowheadWidthRatio,
15038
+ arcSegments: DEFAULT_DELAY_OPTIONS.arcSegments
15039
+ };
15040
+ const RELIEF_IN_PLACE_METADATA = {
15041
+ id: "relief-in-place",
15042
+ name: "Relief in Place",
15043
+ description: "Replaces all or part of a unit in an area with an incoming unit while maintaining the original unit's responsibilities.",
15044
+ entity: "Mission Tasks",
15045
+ entityType: "Relief in Place",
15046
+ value: "341900",
15047
+ minCoordinates: 4,
15048
+ maxCoordinates: 4,
15049
+ geometry: "line",
15050
+ geometryTypes: ["MultiLineString", "Point"],
15051
+ paints: {
15052
+ stroke: true,
15053
+ fill: "none",
15054
+ text: true
15055
+ },
15056
+ drawRule: "Area18",
15057
+ capturesLabelSize: true,
15058
+ params: DELAY_PARAMS.filter((param) => param.key !== "labelPadding")
15059
+ };
15060
+ /** Generates the RELIEF IN PLACE mission task symbol (341900). */
15061
+ function createReliefInPlaceSymbol(coordinates, options = {}) {
15062
+ return createHairpinSymbol(coordinates, {
15063
+ part: "relief-in-place",
15064
+ buildLabel: ({ points, upperDirection, upperArm }) => ({
15065
+ upperArm,
15066
+ label: createLabelFeature(averagePoints(points), "RIP", labelRotationAlong(upperDirection), {
15067
+ labelSize: options.labelSize,
15068
+ labelSizePixels: options.labelSizePixels
15069
+ })
15070
+ })
15071
+ }, options);
15072
+ }
15073
+ function averagePoints(points) {
15074
+ return vecScale(points.reduce((sum, point) => vecAdd(sum, point), [0, 0]), 1 / points.length);
15075
+ }
15076
+ const RELIEF_IN_PLACE = defineControlMeasure({
15077
+ metadata: RELIEF_IN_PLACE_METADATA,
15078
+ generator: createReliefInPlaceSymbol,
15079
+ defaultOptions: DEFAULT_RELIEF_IN_PLACE_OPTIONS,
15080
+ rule: reliefInPlaceDrawRule,
15081
+ previewSample: { controlPoints: [
15082
+ [-1, .45],
15083
+ [.45, .45],
15084
+ [.45, -.45],
15085
+ [-1, -.45]
15086
+ ] }
14260
15087
  });
14261
15088
  //#endregion
14262
15089
  //#region src/generators/cm34-mission-tasks/retire.ts
@@ -14279,14 +15106,15 @@ const RETIRE_METADATA = {
14279
15106
  },
14280
15107
  drawRule: "Line24",
14281
15108
  capturesLabelSize: true,
15109
+ labelGeometryUsesResolvedMetrics: true,
14282
15110
  params: DELAY_PARAMS
14283
15111
  };
14284
15112
  /** Generates the RETIRE/RETIREMENT mission task symbol (342000). */
14285
- function createRetireSymbol(coordinates, options = {}) {
15113
+ function createRetireSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
14286
15114
  return createDelayVariantSymbol(coordinates, {
14287
15115
  glyph: "R",
14288
15116
  part: "retire"
14289
- }, options);
15117
+ }, options, context);
14290
15118
  }
14291
15119
  const RETIRE = defineControlMeasure({
14292
15120
  metadata: RETIRE_METADATA,
@@ -14340,85 +15168,204 @@ const SCREEN = defineControlMeasure({
14340
15168
  }
14341
15169
  });
14342
15170
  //#endregion
14343
- //#region src/generators/cm34-mission-tasks/seize.ts
14344
- const DEFAULT_SEIZE_OPTIONS = {
14345
- arrowheadLengthRatio: .1,
14346
- arrowheadWidthRatio: .12,
14347
- labelGapRatio: .13,
14348
- curveSegments: 48,
14349
- circleSegments: 64
15171
+ //#region src/generators/cm34-mission-tasks/secure.ts
15172
+ const DEFAULT_SECURE_OPTIONS = {
15173
+ openingAngle: 30,
15174
+ resolution: 64,
15175
+ arrowheadLengthRatio: .2,
15176
+ arrowheadWidthRatio: .4,
15177
+ labelPadding: .2
14350
15178
  };
14351
- const SEIZE_METADATA = {
14352
- id: "seize",
14353
- name: "Seize",
14354
- description: "Takes possession of a designated area using overwhelming force and establishes control of it.",
15179
+ const SECURE_METADATA = {
15180
+ id: "secure",
15181
+ name: "Secure",
15182
+ description: "Prevents a unit, facility, or geographical location from being damaged or destroyed by enemy action.",
14355
15183
  entity: "Mission Tasks",
14356
- entityType: "Seize",
14357
- value: "342300",
14358
- minCoordinates: 4,
14359
- maxCoordinates: 4,
14360
- geometry: "line",
15184
+ entityType: "Secure",
15185
+ value: "342100",
15186
+ minCoordinates: 2,
15187
+ maxCoordinates: 2,
15188
+ geometry: "area",
14361
15189
  geometryTypes: ["MultiLineString", "Point"],
14362
15190
  paints: {
14363
15191
  stroke: true,
14364
15192
  fill: "none",
14365
15193
  text: true
14366
15194
  },
14367
- drawRule: "Line27",
15195
+ drawRule: "Area19",
14368
15196
  capturesLabelSize: true,
15197
+ labelGeometryUsesResolvedMetrics: true,
14369
15198
  params: [
14370
15199
  {
14371
- key: "arrowheadLengthRatio",
15200
+ key: "openingAngle",
14372
15201
  presentationTier: "advanced",
14373
- label: "Arrowhead length",
14374
- description: "Arrowhead length as a ratio of the PT. 2-to-PT. 4 span",
15202
+ label: "Opening",
15203
+ description: "Angular width of the opening on the friendly side, in degrees",
14375
15204
  type: "number",
14376
- min: .01,
14377
- max: .5,
14378
- step: .01
15205
+ min: 5,
15206
+ max: 180,
15207
+ step: 1
14379
15208
  },
15209
+ ...ARC_RESOLUTION_PARAMS,
14380
15210
  {
14381
- key: "arrowheadWidthRatio",
15211
+ key: "arrowheadLengthRatio",
14382
15212
  presentationTier: "advanced",
14383
- label: "Arrowhead width",
14384
- description: "Arrowhead base width as a ratio of the PT. 2-to-PT. 4 span",
15213
+ label: "Arrowhead length",
15214
+ description: "Arrowhead length as a ratio of the symbol radius",
14385
15215
  type: "number",
14386
- min: .01,
14387
- max: .5,
15216
+ min: 0,
15217
+ max: 1,
14388
15218
  step: .01
14389
15219
  },
14390
15220
  {
14391
- key: "labelGapRatio",
15221
+ key: "arrowheadWidthRatio",
14392
15222
  presentationTier: "advanced",
14393
- label: "Label gap",
14394
- description: "Size of the curve gap holding the S label",
15223
+ label: "Arrowhead width",
15224
+ description: "Arrowhead width as a ratio of the symbol radius",
14395
15225
  type: "number",
14396
15226
  min: 0,
14397
- max: .5,
15227
+ max: 1,
14398
15228
  step: .01
14399
15229
  },
14400
15230
  {
14401
- key: "curveSegments",
14402
- presentationTier: "advanced",
14403
- label: "Curve segments",
14404
- description: "Number of straight segments used to approximate the circular arc",
14405
- type: "number",
14406
- min: 4,
14407
- max: 128,
14408
- step: 1
14409
- },
14410
- {
14411
- key: "circleSegments",
15231
+ key: "labelPadding",
14412
15232
  presentationTier: "advanced",
14413
- label: "Circle segments",
14414
- description: "Number of straight segments used to approximate the assigned-unit circle",
15233
+ label: "S padding",
15234
+ description: "Extra boundary clearance on each side of S, as a ratio of its height",
14415
15235
  type: "number",
14416
- min: 12,
14417
- max: 128,
14418
- step: 1
15236
+ min: 0,
15237
+ max: 2,
15238
+ step: .05
14419
15239
  }
14420
15240
  ]
14421
15241
  };
15242
+ const TAU$1 = Math.PI * 2;
15243
+ const MIN_LABEL_GAP_METERS$1 = 1;
15244
+ /** Generates the Secure mission task symbol (342100). */
15245
+ function createSecureSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
15246
+ const center = project(coordinates[0][0], coordinates[0][1]);
15247
+ const radiusVector = vecSub(project(coordinates[1][0], coordinates[1][1]), center);
15248
+ const radius = vecMag(radiusVector);
15249
+ if (radius < 1e-6) return {
15250
+ type: "FeatureCollection",
15251
+ features: []
15252
+ };
15253
+ const resolved = {
15254
+ ...DEFAULT_SECURE_OPTIONS,
15255
+ ...options
15256
+ };
15257
+ const bulgeVector = [radiusVector[1], -radiusVector[0]];
15258
+ const halfTurnResolution = normalizeArcResolution(resolved.resolution) / 2;
15259
+ const boundarySpan = TAU$1 - Math.min(180, Math.max(5, resolved.openingAngle)) * Math.PI / 180;
15260
+ const labelSweep = boundarySpan / 2;
15261
+ const labelHeight = resolveTextMetrics("S", options, context, "regular", radius * .12).height;
15262
+ const labelPadding = Math.max(0, resolved.labelPadding);
15263
+ const labelHalfGap = Math.min(boundarySpan / 4, Math.max(labelHeight * (LABEL_HALF_GLYPH_RATIO + labelPadding), MIN_LABEL_GAP_METERS$1) / radius);
15264
+ const sampleArc = (from, to) => sampleProjectedArc(center, radiusVector, bulgeVector, from, to, halfTurnResolution);
15265
+ const terminal = arcPointAt(center, radiusVector, bulgeVector, boundarySpan);
15266
+ const radial = vecScale(vecSub(terminal, center), 1 / radius);
15267
+ const tangent = [radial[1], -radial[0]];
15268
+ const lines = [
15269
+ sampleArc(0, labelSweep - labelHalfGap),
15270
+ sampleArc(labelSweep + labelHalfGap, boundarySpan),
15271
+ createArrowHead$1(terminal, tangent, radius * Math.max(0, resolved.arrowheadLengthRatio), radius * Math.max(0, resolved.arrowheadWidthRatio))
15272
+ ];
15273
+ const labelPoint = arcPointAt(center, radiusVector, bulgeVector, labelSweep);
15274
+ return {
15275
+ type: "FeatureCollection",
15276
+ features: [{
15277
+ type: "Feature",
15278
+ properties: { part: "secure" },
15279
+ geometry: {
15280
+ type: "MultiLineString",
15281
+ coordinates: lines
15282
+ }
15283
+ }, createLabelFeature(labelPoint, "S", Math.PI, options)]
15284
+ };
15285
+ }
15286
+ const SECURE = defineControlMeasure({
15287
+ metadata: SECURE_METADATA,
15288
+ generator: createSecureSymbol,
15289
+ defaultOptions: DEFAULT_SECURE_OPTIONS,
15290
+ rule: secureDrawRule,
15291
+ previewSample: {
15292
+ controlPoints: [[0, 0], [-.8, .05]],
15293
+ options: { labelSize: 90 }
15294
+ }
15295
+ });
15296
+ //#endregion
15297
+ //#region src/generators/cm34-mission-tasks/seize.ts
15298
+ const DEFAULT_SEIZE_OPTIONS = {
15299
+ arrowheadLengthRatio: .1,
15300
+ arrowheadWidthRatio: .12,
15301
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING,
15302
+ curveSegments: 48,
15303
+ circleSegments: 64
15304
+ };
15305
+ const SEIZE_PARAMS = [
15306
+ {
15307
+ key: "arrowheadLengthRatio",
15308
+ presentationTier: "advanced",
15309
+ label: "Arrowhead length",
15310
+ description: "Arrowhead length as a ratio of the PT. 2-to-PT. 4 span",
15311
+ type: "number",
15312
+ min: .01,
15313
+ max: .5,
15314
+ step: .01
15315
+ },
15316
+ {
15317
+ key: "arrowheadWidthRatio",
15318
+ presentationTier: "advanced",
15319
+ label: "Arrowhead width",
15320
+ description: "Arrowhead base width as a ratio of the PT. 2-to-PT. 4 span",
15321
+ type: "number",
15322
+ min: .01,
15323
+ max: .5,
15324
+ step: .01
15325
+ },
15326
+ LABEL_PADDING_PARAM,
15327
+ {
15328
+ key: "curveSegments",
15329
+ presentationTier: "advanced",
15330
+ label: "Curve segments",
15331
+ description: "Number of straight segments used to approximate the circular arc",
15332
+ type: "number",
15333
+ min: 4,
15334
+ max: 128,
15335
+ step: 1
15336
+ },
15337
+ {
15338
+ key: "circleSegments",
15339
+ presentationTier: "advanced",
15340
+ label: "Circle segments",
15341
+ description: "Number of straight segments used to approximate the assigned-unit circle",
15342
+ type: "number",
15343
+ min: 12,
15344
+ max: 128,
15345
+ step: 1
15346
+ }
15347
+ ];
15348
+ const SEIZE_METADATA = {
15349
+ id: "seize",
15350
+ name: "Seize",
15351
+ description: "Takes possession of a designated area using overwhelming force and establishes control of it.",
15352
+ entity: "Mission Tasks",
15353
+ entityType: "Seize",
15354
+ value: "342300",
15355
+ minCoordinates: 4,
15356
+ maxCoordinates: 4,
15357
+ geometry: "line",
15358
+ geometryTypes: ["MultiLineString", "Point"],
15359
+ paints: {
15360
+ stroke: true,
15361
+ fill: "none",
15362
+ text: true
15363
+ },
15364
+ drawRule: "Line27",
15365
+ capturesLabelSize: true,
15366
+ labelGeometryUsesResolvedMetrics: true,
15367
+ params: SEIZE_PARAMS
15368
+ };
14422
15369
  function positiveAngle(angle) {
14423
15370
  const normalized = angle % (Math.PI * 2);
14424
15371
  return normalized < 0 ? normalized + Math.PI * 2 : normalized;
@@ -14447,8 +15394,8 @@ function threePointArc(start, middle, end) {
14447
15394
  sweepAngle: positiveAngle(middleAngle - startAngle) <= counterclockwiseSweep ? counterclockwiseSweep : counterclockwiseSweep - Math.PI * 2
14448
15395
  };
14449
15396
  }
14450
- /** Generates the Seize mission task symbol (342300). */
14451
- function createSeizeSymbol(coordinates, options = {}) {
15397
+ /** Shared geometry for mission tasks that use the Seize graphic. */
15398
+ function createSeizeLikeSymbol(coordinates, options = {}, label = "S", part = "seize", context = {}) {
14452
15399
  const [c1, c2, c3, c4] = coordinates;
14453
15400
  const center = project(c1[0], c1[1]);
14454
15401
  const start = project(c2[0], c2[1]);
@@ -14469,8 +15416,14 @@ function createSeizeSymbol(coordinates, options = {}) {
14469
15416
  let arrowDirection;
14470
15417
  let labelPoint;
14471
15418
  let tangentAtLabel;
15419
+ let labelRotation;
14472
15420
  if (arc) {
14473
- const gapHalf = clamp01(options.labelGapRatio ?? DEFAULT_SEIZE_OPTIONS.labelGapRatio) / 2;
15421
+ const labelAngle = arc.startAngle + arc.sweepAngle / 2;
15422
+ const sweepSign = Math.sign(arc.sweepAngle);
15423
+ labelPoint = vecAdd(arc.center, [arc.radius * Math.cos(labelAngle), arc.radius * Math.sin(labelAngle)]);
15424
+ tangentAtLabel = [-Math.sin(labelAngle) * sweepSign, Math.cos(labelAngle) * sweepSign];
15425
+ labelRotation = labelRotationAlong(tangentAtLabel);
15426
+ const gapHalf = arcKnockoutHalfFraction(labelKnockoutHalfExtent(label, tangentAtLabel, labelRotation, options, context, DEFAULT_SEIZE_OPTIONS.labelPadding), arc.radius, arc.sweepAngle);
14474
15427
  const gapStart = .5 - gapHalf;
14475
15428
  const gapEnd = .5 + gapHalf;
14476
15429
  const sampleArc = (from, to) => {
@@ -14486,22 +15439,19 @@ function createSeizeSymbol(coordinates, options = {}) {
14486
15439
  firstArcPart = sampleArc(0, gapStart);
14487
15440
  secondArcPart = sampleArc(gapEnd, 1);
14488
15441
  const endAngle = arc.startAngle + arc.sweepAngle;
14489
- const sweepSign = Math.sign(arc.sweepAngle);
14490
15442
  arrowDirection = [-Math.sin(endAngle) * sweepSign, Math.cos(endAngle) * sweepSign];
14491
- const labelAngle = arc.startAngle + arc.sweepAngle / 2;
14492
- labelPoint = vecAdd(arc.center, [arc.radius * Math.cos(labelAngle), arc.radius * Math.sin(labelAngle)]);
14493
- tangentAtLabel = [-Math.sin(labelAngle) * sweepSign, Math.cos(labelAngle) * sweepSign];
14494
15443
  } else {
14495
15444
  labelPoint = vecScale(vecAdd(start, tip), .5);
14496
- const gapHalf = span * clamp01(options.labelGapRatio ?? DEFAULT_SEIZE_OPTIONS.labelGapRatio) / 2;
14497
15445
  const direction = vecNorm(vecSub(tip, start));
15446
+ tangentAtLabel = direction;
15447
+ labelRotation = labelRotationAlong(tangentAtLabel);
15448
+ const gapHalf = Math.min(span / 2, labelKnockoutHalfExtent(label, direction, labelRotation, options, context, DEFAULT_SEIZE_OPTIONS.labelPadding));
14498
15449
  const before = vecAdd(labelPoint, vecScale(direction, -gapHalf));
14499
15450
  const after = vecAdd(labelPoint, vecScale(direction, gapHalf));
14500
15451
  firstArcPart = [toPosition(start), toPosition(before)];
14501
15452
  secondArcPart = [toPosition(after), toPosition(tip)];
14502
15453
  const terminal = vecSub(tip, curvePoint);
14503
15454
  arrowDirection = vecMag(terminal) >= 1e-6 ? vecNorm(terminal) : vecNorm(vecSub(tip, start));
14504
- tangentAtLabel = direction;
14505
15455
  }
14506
15456
  const startAngle = Math.atan2(radiusVector[1], radiusVector[0]);
14507
15457
  const circle = [];
@@ -14509,115 +15459,367 @@ function createSeizeSymbol(coordinates, options = {}) {
14509
15459
  const angle = startAngle + index / circleSegments * Math.PI * 2;
14510
15460
  circle.push(toPosition(vecAdd(center, [radius * Math.cos(angle), radius * Math.sin(angle)])));
14511
15461
  }
15462
+ const lines = [
15463
+ circle,
15464
+ firstArcPart,
15465
+ secondArcPart,
15466
+ createArrowHead$1(tip, arrowDirection, span * Math.max(0, options.arrowheadLengthRatio ?? DEFAULT_SEIZE_OPTIONS.arrowheadLengthRatio), span * Math.max(0, options.arrowheadWidthRatio ?? DEFAULT_SEIZE_OPTIONS.arrowheadWidthRatio))
15467
+ ];
15468
+ return {
15469
+ type: "FeatureCollection",
15470
+ features: [{
15471
+ type: "Feature",
15472
+ properties: { part },
15473
+ geometry: {
15474
+ type: "MultiLineString",
15475
+ coordinates: lines
15476
+ }
15477
+ }, createLabelFeature(labelPoint, label, labelRotation, options)]
15478
+ };
15479
+ }
15480
+ /** Generates the Seize mission task symbol (342300). */
15481
+ function createSeizeSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
15482
+ return createSeizeLikeSymbol(coordinates, options, "S", "seize", context);
15483
+ }
15484
+ const SEIZE = defineControlMeasure({
15485
+ metadata: SEIZE_METADATA,
15486
+ generator: createSeizeSymbol,
15487
+ defaultOptions: DEFAULT_SEIZE_OPTIONS,
15488
+ rule: line27DrawRule,
15489
+ previewSample: {
15490
+ controlPoints: [
15491
+ [-.8, .35],
15492
+ [-.45, .35],
15493
+ [.15, .25],
15494
+ [.85, -.55]
15495
+ ],
15496
+ options: { labelSize: 100 }
15497
+ }
15498
+ });
15499
+ //#endregion
15500
+ //#region src/generators/cm34-mission-tasks/evacuate.ts
15501
+ const DEFAULT_EVACUATE_OPTIONS = DEFAULT_SEIZE_OPTIONS;
15502
+ const EVACUATE_METADATA = {
15503
+ id: "evacuate",
15504
+ name: "Evacuate",
15505
+ description: "Removes personnel or materiel from a threatened area.",
15506
+ entity: "Mission Tasks",
15507
+ entityType: "Evacuation",
15508
+ value: "344500",
15509
+ minCoordinates: 4,
15510
+ maxCoordinates: 4,
15511
+ geometry: "line",
15512
+ geometryTypes: ["MultiLineString", "Point"],
15513
+ paints: {
15514
+ stroke: true,
15515
+ fill: "none",
15516
+ text: true
15517
+ },
15518
+ drawRule: "Line27",
15519
+ capturesLabelSize: true,
15520
+ labelGeometryUsesResolvedMetrics: true,
15521
+ params: SEIZE_PARAMS
15522
+ };
15523
+ /** Generates the Evacuate mission task symbol (344500). */
15524
+ function createEvacuateSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
15525
+ return createSeizeLikeSymbol(coordinates, options, "E", "evacuate", context);
15526
+ }
15527
+ const EVACUATE = defineControlMeasure({
15528
+ metadata: EVACUATE_METADATA,
15529
+ generator: createEvacuateSymbol,
15530
+ defaultOptions: DEFAULT_EVACUATE_OPTIONS,
15531
+ rule: line27DrawRule,
15532
+ previewSample: {
15533
+ controlPoints: [
15534
+ [-.8, .35],
15535
+ [-.45, .35],
15536
+ [.15, .25],
15537
+ [.85, -.55]
15538
+ ],
15539
+ options: { labelSize: 100 }
15540
+ }
15541
+ });
15542
+ //#endregion
15543
+ //#region src/generators/cm34-mission-tasks/withdraw.ts
15544
+ const DEFAULT_WITHDRAW_OPTIONS = DEFAULT_DELAY_OPTIONS;
15545
+ const WITHDRAW_METADATA = {
15546
+ id: "withdraw",
15547
+ name: "Withdraw",
15548
+ description: "Disengages a force from the enemy and moves it away from enemy contact.",
15549
+ entity: "Mission Tasks",
15550
+ entityType: "Withdraw",
15551
+ value: "342400",
15552
+ minCoordinates: 3,
15553
+ maxCoordinates: 3,
15554
+ geometry: "line",
15555
+ geometryTypes: ["MultiLineString", "Point"],
15556
+ paints: {
15557
+ stroke: true,
15558
+ fill: "none",
15559
+ text: true
15560
+ },
15561
+ drawRule: "Line24",
15562
+ capturesLabelSize: true,
15563
+ labelGeometryUsesResolvedMetrics: true,
15564
+ params: DELAY_PARAMS
15565
+ };
15566
+ function createWithdrawSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
15567
+ return createDelayVariantSymbol(coordinates, {
15568
+ glyph: "W",
15569
+ part: "withdraw"
15570
+ }, options, context);
15571
+ }
15572
+ const WITHDRAW = defineControlMeasure({
15573
+ metadata: WITHDRAW_METADATA,
15574
+ generator: createWithdrawSymbol,
15575
+ defaultOptions: DEFAULT_WITHDRAW_OPTIONS,
15576
+ rule: line24DrawRule
15577
+ });
15578
+ //#endregion
15579
+ //#region src/generators/cm34-mission-tasks/withdraw-under-pressure.ts
15580
+ const DEFAULT_WITHDRAW_UNDER_PRESSURE_OPTIONS = DEFAULT_DELAY_OPTIONS;
15581
+ const WITHDRAW_UNDER_PRESSURE_METADATA = {
15582
+ id: "withdraw-under-pressure",
15583
+ name: "Withdraw Under Pressure",
15584
+ description: "Disengages a force from the enemy and moves it away while under enemy pressure.",
15585
+ entity: "Mission Tasks",
15586
+ entityType: "Withdraw Under Pressure",
15587
+ value: "342500",
15588
+ minCoordinates: 3,
15589
+ maxCoordinates: 3,
15590
+ geometry: "line",
15591
+ geometryTypes: ["MultiLineString", "Point"],
15592
+ paints: {
15593
+ stroke: true,
15594
+ fill: "none",
15595
+ text: true
15596
+ },
15597
+ drawRule: "Line24",
15598
+ capturesLabelSize: true,
15599
+ labelGeometryUsesResolvedMetrics: true,
15600
+ params: DELAY_PARAMS
15601
+ };
15602
+ function createWithdrawUnderPressureSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
15603
+ return createDelayVariantSymbol(coordinates, {
15604
+ glyph: "WP",
15605
+ part: "withdraw-under-pressure"
15606
+ }, options, context);
15607
+ }
15608
+ const WITHDRAW_UNDER_PRESSURE = defineControlMeasure({
15609
+ metadata: WITHDRAW_UNDER_PRESSURE_METADATA,
15610
+ generator: createWithdrawUnderPressureSymbol,
15611
+ defaultOptions: DEFAULT_WITHDRAW_UNDER_PRESSURE_OPTIONS,
15612
+ rule: line24DrawRule,
15613
+ previewSample: { controlPoints: [
15614
+ [-1, -.4],
15615
+ [1, -.4],
15616
+ [0, .6]
15617
+ ] }
15618
+ });
15619
+ //#endregion
15620
+ //#region src/generators/cm27-protection-areas/turn.ts
15621
+ const DEFAULT_TURN_OPTIONS = {
15622
+ arrowheadLengthRatio: .16,
15623
+ arrowheadWidthRatio: .16,
15624
+ arcSegments: 64
15625
+ };
15626
+ const TURN_PARAMS = [
15627
+ {
15628
+ key: "arrowheadLengthRatio",
15629
+ presentationTier: "advanced",
15630
+ label: "Arrowhead length",
15631
+ description: "Arrowhead length as a ratio of the rear-to-tip distance",
15632
+ type: "number",
15633
+ min: .01,
15634
+ max: .5,
15635
+ step: .01
15636
+ },
15637
+ {
15638
+ key: "arrowheadWidthRatio",
15639
+ presentationTier: "advanced",
15640
+ label: "Arrowhead width",
15641
+ description: "Arrowhead base width as a ratio of the rear-to-tip distance",
15642
+ type: "number",
15643
+ min: .01,
15644
+ max: .5,
15645
+ step: .01
15646
+ },
15647
+ {
15648
+ key: "arcSegments",
15649
+ presentationTier: "advanced",
15650
+ label: "Arc segments",
15651
+ description: "Number of line segments approximating the quarter-circle shaft.",
15652
+ type: "number",
15653
+ min: 4,
15654
+ max: 128,
15655
+ step: 1
15656
+ }
15657
+ ];
15658
+ const TURN_METADATA = {
15659
+ id: "turn",
15660
+ name: "Turn",
15661
+ description: "Diverts an enemy formation from one avenue of approach to another or into an engagement area. The arrow indicates the desired direction of turn.",
15662
+ entity: "Protection Areas",
15663
+ entityType: "Obstacle Effects",
15664
+ entitySubtype: "Turn",
15665
+ value: "270504",
15666
+ minCoordinates: 2,
15667
+ geometry: "line",
15668
+ geometryTypes: ["MultiLineString", "MultiPolygon"],
15669
+ paints: {
15670
+ stroke: true,
15671
+ fill: "fixed",
15672
+ text: false
15673
+ },
15674
+ drawRule: "Line10",
15675
+ params: TURN_PARAMS
15676
+ };
15677
+ /**
15678
+ * Shared geometry for the Turn graphic: a quarter-circle shaft from PT2
15679
+ * (rear) to the arrowhead base, with the arrowhead continuing tangent to the
15680
+ * arc and placing its tip at PT1. PT3 selects which side of the PT2 -> PT1
15681
+ * chord contains the arc.
15682
+ *
15683
+ * `labelGapHalfLength` optionally resolves a meter-denominated half-gap from the arc midpoint
15684
+ * anchor for measures that carry a doctrinal glyph there.
15685
+ */
15686
+ function createTurnLikeSymbol(coordinates, options = {}, labelGapHalfLength) {
15687
+ const tip = project(coordinates[0][0], coordinates[0][1]);
15688
+ const rear = project(coordinates[1][0], coordinates[1][1]);
15689
+ const side = quarterArcSide(rear, tip, coordinates[2] ? project(coordinates[2][0], coordinates[2][1]) : void 0);
15690
+ const rearToTip = vecSub(tip, rear);
15691
+ const totalLength = vecMag(rearToTip);
15692
+ if (totalLength < 1e-6) return { features: [] };
15693
+ const requestedHeadLength = totalLength * (options.arrowheadLengthRatio ?? DEFAULT_TURN_OPTIONS.arrowheadLengthRatio);
15694
+ const tangentComponent = Math.min(Math.max(requestedHeadLength, EPSILON), totalLength * .95) / Math.SQRT2;
15695
+ const arcChordLength = Math.sqrt(totalLength * totalLength - tangentComponent * tangentComponent) - tangentComponent;
15696
+ const chordOffset = Math.atan2(tangentComponent, arcChordLength + tangentComponent);
15697
+ const headBase = vecAdd(rear, vecScale(rotate(vecNorm(rearToTip), side * chordOffset), arcChordLength));
15698
+ const arc = createQuarterArc(rear, headBase, side);
15699
+ if (!arc) return { features: [] };
15700
+ const segmentCount = Math.round(Math.min(128, Math.max(4, options.arcSegments ?? DEFAULT_TURN_OPTIONS.arcSegments)));
15701
+ const sampleArc = (from, to) => {
15702
+ const count = Math.max(1, Math.ceil(segmentCount * (to - from)));
15703
+ const points = [];
15704
+ for (let i = 0; i <= count; i++) {
15705
+ const progress = from + (to - from) * i / count;
15706
+ const angle = arc.startAngle + arc.sweepAngle * progress;
15707
+ points.push(unproject(arc.center[0] + arc.radius * Math.cos(angle), arc.center[1] + arc.radius * Math.sin(angle)));
15708
+ }
15709
+ return points;
15710
+ };
15711
+ const sweepSign = Math.sign(arc.sweepAngle);
15712
+ const midAngle = arc.startAngle + arc.sweepAngle / 2;
15713
+ const labelAnchor = {
15714
+ point: arc.midpoint,
15715
+ tangent: [-Math.sin(midAngle) * sweepSign, Math.cos(midAngle) * sweepSign]
15716
+ };
15717
+ const gapHalf = arcKnockoutHalfFraction(labelGapHalfLength?.(labelAnchor) ?? 0, arc.radius, arc.sweepAngle);
15718
+ const arcParts = gapHalf > 0 ? [sampleArc(0, .5 - gapHalf), sampleArc(.5 + gapHalf, 1)] : [sampleArc(0, 1)];
15719
+ const headWidth = totalLength * (options.arrowheadWidthRatio ?? DEFAULT_TURN_OPTIONS.arrowheadWidthRatio);
15720
+ const arrowHead = createArrowHeadRing(tip, vecNorm(vecSub(tip, headBase)), vecMag(vecSub(tip, headBase)), headWidth);
14512
15721
  return {
14513
- type: "FeatureCollection",
14514
15722
  features: [{
14515
15723
  type: "Feature",
14516
- properties: { part: "seize" },
15724
+ properties: { part: "arc" },
14517
15725
  geometry: {
14518
15726
  type: "MultiLineString",
14519
- coordinates: [
14520
- circle,
14521
- firstArcPart,
14522
- secondArcPart,
14523
- createArrowHead$1(tip, arrowDirection, span * Math.max(0, options.arrowheadLengthRatio ?? DEFAULT_SEIZE_OPTIONS.arrowheadLengthRatio), span * Math.max(0, options.arrowheadWidthRatio ?? DEFAULT_SEIZE_OPTIONS.arrowheadWidthRatio))
14524
- ]
15727
+ coordinates: arcParts
15728
+ }
15729
+ }, {
15730
+ type: "Feature",
15731
+ properties: {
15732
+ part: "arrowhead",
15733
+ fill: true,
15734
+ style: SOLID_ACCENT_FILL
15735
+ },
15736
+ geometry: {
15737
+ type: "MultiPolygon",
15738
+ coordinates: [[[...arrowHead]]]
14525
15739
  }
14526
- }, createLabelFeature(labelPoint, "S", labelRotationAlong(tangentAtLabel), options)]
15740
+ }],
15741
+ labelAnchor
14527
15742
  };
14528
15743
  }
14529
- const SEIZE = defineControlMeasure({
14530
- metadata: SEIZE_METADATA,
14531
- generator: createSeizeSymbol,
14532
- defaultOptions: DEFAULT_SEIZE_OPTIONS,
14533
- rule: line27DrawRule,
14534
- previewSample: {
14535
- controlPoints: [
14536
- [-.8, .35],
14537
- [-.45, .35],
14538
- [.15, .25],
14539
- [.85, -.55]
14540
- ],
14541
- options: { labelSize: 100 }
14542
- }
14543
- });
14544
- //#endregion
14545
- //#region src/generators/cm34-mission-tasks/withdraw.ts
14546
- const DEFAULT_WITHDRAW_OPTIONS = DEFAULT_DELAY_OPTIONS;
14547
- const WITHDRAW_METADATA = {
14548
- id: "withdraw",
14549
- name: "Withdraw",
14550
- description: "Disengages a force from the enemy and moves it away from enemy contact.",
14551
- entity: "Mission Tasks",
14552
- entityType: "Withdraw",
14553
- value: "342400",
14554
- minCoordinates: 3,
14555
- maxCoordinates: 3,
14556
- geometry: "line",
14557
- geometryTypes: ["MultiLineString", "Point"],
14558
- paints: {
14559
- stroke: true,
14560
- fill: "none",
14561
- text: true
14562
- },
14563
- drawRule: "Line24",
14564
- capturesLabelSize: true,
14565
- params: DELAY_PARAMS
14566
- };
14567
- function createWithdrawSymbol(coordinates, options = {}) {
14568
- return createDelayVariantSymbol(coordinates, {
14569
- glyph: "W",
14570
- part: "withdraw"
14571
- }, options);
15744
+ /**
15745
+ * Generates a quarter-circle shaft from PT2 (rear) to the arrowhead base.
15746
+ * The arrowhead continues tangent to the arc and places its tip at PT1.
15747
+ * PT3 selects which side of the PT2 -> PT1 chord contains the arc.
15748
+ */
15749
+ function createTurn(coordinates, options = {}) {
15750
+ return {
15751
+ type: "FeatureCollection",
15752
+ features: createTurnLikeSymbol(coordinates, options).features
15753
+ };
14572
15754
  }
14573
- const WITHDRAW = defineControlMeasure({
14574
- metadata: WITHDRAW_METADATA,
14575
- generator: createWithdrawSymbol,
14576
- defaultOptions: DEFAULT_WITHDRAW_OPTIONS,
14577
- rule: line24DrawRule
15755
+ function rotate(vector, angle) {
15756
+ const cos = Math.cos(angle);
15757
+ const sin = Math.sin(angle);
15758
+ return [vector[0] * cos - vector[1] * sin, vector[0] * sin + vector[1] * cos];
15759
+ }
15760
+ const TURN = defineControlMeasure({
15761
+ metadata: TURN_METADATA,
15762
+ generator: createTurn,
15763
+ defaultOptions: DEFAULT_TURN_OPTIONS,
15764
+ rule: turnDrawRule
14578
15765
  });
14579
15766
  //#endregion
14580
- //#region src/generators/cm34-mission-tasks/withdraw-under-pressure.ts
14581
- const DEFAULT_WITHDRAW_UNDER_PRESSURE_OPTIONS = DEFAULT_DELAY_OPTIONS;
14582
- const WITHDRAW_UNDER_PRESSURE_METADATA = {
14583
- id: "withdraw-under-pressure",
14584
- name: "Withdraw Under Pressure",
14585
- description: "Disengages a force from the enemy and moves it away while under enemy pressure.",
15767
+ //#region src/generators/cm34-mission-tasks/turn.ts
15768
+ const DEFAULT_TURN_MISSION_TASK_OPTIONS = {
15769
+ ...DEFAULT_TURN_OPTIONS,
15770
+ arrowheadLengthRatio: .14,
15771
+ arrowheadWidthRatio: .14,
15772
+ labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING
15773
+ };
15774
+ const TURN_MISSION_TASK_METADATA = {
15775
+ id: "turn-mission-task",
15776
+ name: "Turn",
15777
+ description: "Causes an enemy force to move from one avenue of approach to another or into an engagement area.",
14586
15778
  entity: "Mission Tasks",
14587
- entityType: "Withdraw Under Pressure",
14588
- value: "342500",
14589
- minCoordinates: 3,
14590
- maxCoordinates: 3,
15779
+ entityType: "Turn",
15780
+ value: "344700",
15781
+ minCoordinates: 2,
14591
15782
  geometry: "line",
14592
- geometryTypes: ["MultiLineString", "Point"],
15783
+ geometryTypes: [
15784
+ "MultiLineString",
15785
+ "MultiPolygon",
15786
+ "Point"
15787
+ ],
14593
15788
  paints: {
14594
15789
  stroke: true,
14595
- fill: "none",
15790
+ fill: "fixed",
14596
15791
  text: true
14597
15792
  },
14598
- drawRule: "Line24",
15793
+ drawRule: "Line10",
14599
15794
  capturesLabelSize: true,
14600
- params: DELAY_PARAMS
14601
- };
14602
- function createWithdrawUnderPressureSymbol(coordinates, options = {}) {
14603
- return createDelayVariantSymbol(coordinates, {
14604
- glyph: "WP",
14605
- part: "withdraw-under-pressure"
14606
- }, options);
15795
+ labelGeometryUsesResolvedMetrics: true,
15796
+ params: [...TURN_PARAMS, LABEL_PADDING_PARAM]
15797
+ };
15798
+ /** Generates the TURN mission task symbol (344700). */
15799
+ function createTurnMissionTaskSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
15800
+ const { features, labelAnchor } = createTurnLikeSymbol(coordinates, {
15801
+ arrowheadLengthRatio: options.arrowheadLengthRatio ?? DEFAULT_TURN_MISSION_TASK_OPTIONS.arrowheadLengthRatio,
15802
+ arrowheadWidthRatio: options.arrowheadWidthRatio ?? DEFAULT_TURN_MISSION_TASK_OPTIONS.arrowheadWidthRatio,
15803
+ arcSegments: options.arcSegments ?? DEFAULT_TURN_MISSION_TASK_OPTIONS.arcSegments
15804
+ }, (anchor) => {
15805
+ const rotation = labelRotationAlong(anchor.tangent);
15806
+ return labelKnockoutHalfExtent("T", anchor.tangent, rotation, options, context, DEFAULT_TURN_MISSION_TASK_OPTIONS.labelPadding);
15807
+ });
15808
+ if (!labelAnchor) return {
15809
+ type: "FeatureCollection",
15810
+ features: []
15811
+ };
15812
+ const label = createLabelFeature(labelAnchor.point, "T", labelRotationAlong(labelAnchor.tangent), options);
15813
+ return {
15814
+ type: "FeatureCollection",
15815
+ features: [...features, label]
15816
+ };
14607
15817
  }
14608
- const WITHDRAW_UNDER_PRESSURE = defineControlMeasure({
14609
- metadata: WITHDRAW_UNDER_PRESSURE_METADATA,
14610
- generator: createWithdrawUnderPressureSymbol,
14611
- defaultOptions: DEFAULT_WITHDRAW_UNDER_PRESSURE_OPTIONS,
14612
- rule: line24DrawRule,
14613
- previewSample: {
14614
- controlPoints: [
14615
- [-1, -.4],
14616
- [1, -.4],
14617
- [0, .6]
14618
- ],
14619
- options: { labelGapRatio: .3 }
14620
- }
15818
+ const TURN_MISSION_TASK = defineControlMeasure({
15819
+ metadata: TURN_MISSION_TASK_METADATA,
15820
+ generator: createTurnMissionTaskSymbol,
15821
+ defaultOptions: DEFAULT_TURN_MISSION_TASK_OPTIONS,
15822
+ rule: turnDrawRule
14621
15823
  });
14622
15824
  //#endregion
14623
15825
  //#region src/generators/cm15-maneuver-areas/jointTacticalActionArea.ts
@@ -14648,6 +15850,7 @@ const JOINT_TACTICAL_ACTION_AREA_METADATA = {
14648
15850
  },
14649
15851
  drawRule: "Area1",
14650
15852
  capturesLabelSize: true,
15853
+ labelGeometryUsesResolvedMetrics: ["N"],
14651
15854
  params: PREFIXED_LABELED_AREA_PARAMS,
14652
15855
  textAmplifiers: AREA_TEXT_AMPLIFIERS
14653
15856
  };
@@ -14708,6 +15911,7 @@ const LANDING_ZONE_METADATA = {
14708
15911
  },
14709
15912
  drawRule: "Area1",
14710
15913
  capturesLabelSize: true,
15914
+ labelGeometryUsesResolvedMetrics: ["N"],
14711
15915
  params: PREFIXED_LABELED_AREA_PARAMS,
14712
15916
  textAmplifiers: AREA_TEXT_AMPLIFIERS
14713
15917
  };
@@ -14763,6 +15967,7 @@ const PENETRATION_BOX_METADATA = {
14763
15967
  },
14764
15968
  drawRule: "Area1",
14765
15969
  capturesLabelSize: true,
15970
+ labelGeometryUsesResolvedMetrics: ["N"],
14766
15971
  params: LABELED_AREA_PARAMS,
14767
15972
  textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
14768
15973
  };
@@ -14814,6 +16019,7 @@ const LIMITED_ACCESS_AREA_METADATA = {
14814
16019
  },
14815
16020
  drawRule: "Area1",
14816
16021
  capturesLabelSize: true,
16022
+ labelGeometryUsesResolvedMetrics: ["N"],
14817
16023
  params: [...AREA_SMOOTH_PARAMS, AREA_LABEL_PADDING_PARAM],
14818
16024
  textAmplifiers: [AREA_HOSTILE_AMPLIFIER]
14819
16025
  };
@@ -14909,6 +16115,7 @@ const NO_FIRE_AREA_IRREGULAR_METADATA = {
14909
16115
  },
14910
16116
  drawRule: "Area1",
14911
16117
  capturesLabelSize: true,
16118
+ labelGeometryUsesResolvedMetrics: ["N"],
14912
16119
  params: [...AREA_SMOOTH_PARAMS, AREA_LABEL_PADDING_PARAM],
14913
16120
  textAmplifiers: [...NO_FIRE_AREA_TEXT_AMPLIFIERS, AREA_HOSTILE_AMPLIFIER]
14914
16121
  };
@@ -15154,6 +16361,7 @@ const MINEFIELD_METADATA = {
15154
16361
  text: true
15155
16362
  },
15156
16363
  drawRule: "Point2",
16364
+ labelGeometryUsesResolvedMetrics: ["N"],
15157
16365
  params: [
15158
16366
  MINE_TYPE_PARAM,
15159
16367
  {
@@ -15456,6 +16664,7 @@ const COMMON_METADATA = {
15456
16664
  },
15457
16665
  drawRule: "Area1",
15458
16666
  capturesLabelSize: true,
16667
+ labelGeometryUsesResolvedMetrics: ["N"],
15459
16668
  params: [
15460
16669
  MINE_TYPE_PARAM,
15461
16670
  ...AREA_SMOOTH_PARAMS,
@@ -15478,7 +16687,8 @@ const MINED_AREA_METADATA = {
15478
16687
  name: "Mined Area",
15479
16688
  description: "An area dangerous because of the presence or suspected presence of mines.",
15480
16689
  entityType: "Mined Area",
15481
- value: "270800"
16690
+ value: "270800",
16691
+ labelGeometryUsesResolvedMetrics: true
15482
16692
  };
15483
16693
  function axisIntersections(ring, axis, target) {
15484
16694
  const other = axis === 0 ? 1 : 0;
@@ -16025,6 +17235,7 @@ const PICKUP_ZONE_METADATA = {
16025
17235
  },
16026
17236
  drawRule: "Area1",
16027
17237
  capturesLabelSize: true,
17238
+ labelGeometryUsesResolvedMetrics: ["N"],
16028
17239
  params: PREFIXED_LABELED_AREA_PARAMS,
16029
17240
  textAmplifiers: AREA_TEXT_AMPLIFIERS
16030
17241
  };
@@ -16080,6 +17291,7 @@ const ASSAULT_POSITION_METADATA = {
16080
17291
  },
16081
17292
  drawRule: "Area1",
16082
17293
  capturesLabelSize: true,
17294
+ labelGeometryUsesResolvedMetrics: ["N"],
16083
17295
  params: PREFIXED_LABELED_AREA_PARAMS,
16084
17296
  textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
16085
17297
  };
@@ -16135,6 +17347,7 @@ const ATTACK_POSITION_METADATA = {
16135
17347
  },
16136
17348
  drawRule: "Area1",
16137
17349
  capturesLabelSize: true,
17350
+ labelGeometryUsesResolvedMetrics: ["N"],
16138
17351
  params: PREFIXED_LABELED_AREA_PARAMS,
16139
17352
  textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
16140
17353
  };
@@ -16190,6 +17403,7 @@ const OBJECTIVE_AREA_METADATA = {
16190
17403
  },
16191
17404
  drawRule: "Area1",
16192
17405
  capturesLabelSize: true,
17406
+ labelGeometryUsesResolvedMetrics: ["N"],
16193
17407
  params: PREFIXED_LABELED_AREA_PARAMS,
16194
17408
  textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
16195
17409
  };
@@ -16539,6 +17753,7 @@ const STRONG_POINT_METADATA = {
16539
17753
  },
16540
17754
  drawRule: "Area1",
16541
17755
  capturesLabelSize: true,
17756
+ labelGeometryUsesResolvedMetrics: ["N"],
16542
17757
  params: ECHELON_AREA_PARAMS,
16543
17758
  textAmplifiers: AREA_UNIT_DESIGNATION_HOSTILE_AMPLIFIERS
16544
17759
  };
@@ -16860,6 +18075,7 @@ const SUBMARINE_ACTION_AREA_METADATA = {
16860
18075
  },
16861
18076
  drawRule: "Area1",
16862
18077
  capturesLabelSize: true,
18078
+ labelGeometryUsesResolvedMetrics: ["N"],
16863
18079
  params: PREFIXED_LABELED_AREA_PARAMS,
16864
18080
  textAmplifiers: AREA_TEXT_AMPLIFIERS_NO_ADDITIONAL_INFO
16865
18081
  };
@@ -16920,6 +18136,7 @@ const SUBMARINE_GENERATED_ACTION_AREA_METADATA = {
16920
18136
  },
16921
18137
  drawRule: "Area1",
16922
18138
  capturesLabelSize: true,
18139
+ labelGeometryUsesResolvedMetrics: ["N"],
16923
18140
  params: PREFIXED_LABELED_AREA_PARAMS,
16924
18141
  textAmplifiers: AREA_TEXT_AMPLIFIERS_NO_ADDITIONAL_INFO
16925
18142
  };
@@ -17103,130 +18320,6 @@ const SUPPORT_BY_FIRE = defineControlMeasure({
17103
18320
  rule: supportByFireDrawRule
17104
18321
  });
17105
18322
  //#endregion
17106
- //#region src/generators/cm27-protection-areas/turn.ts
17107
- const DEFAULT_TURN_OPTIONS = {
17108
- arrowheadLengthRatio: .16,
17109
- arrowheadWidthRatio: .16,
17110
- arcSegments: 64
17111
- };
17112
- const TURN_METADATA = {
17113
- id: "turn",
17114
- name: "Turn",
17115
- description: "Diverts an enemy formation from one avenue of approach to another or into an engagement area. The arrow indicates the desired direction of turn.",
17116
- entity: "Protection Areas",
17117
- entityType: "Obstacle Effects",
17118
- entitySubtype: "Turn",
17119
- value: "270504",
17120
- minCoordinates: 2,
17121
- geometry: "line",
17122
- geometryTypes: ["MultiLineString", "MultiPolygon"],
17123
- paints: {
17124
- stroke: true,
17125
- fill: "fixed",
17126
- text: false
17127
- },
17128
- drawRule: "Line10",
17129
- params: [
17130
- {
17131
- key: "arrowheadLengthRatio",
17132
- presentationTier: "advanced",
17133
- label: "Arrowhead length",
17134
- description: "Arrowhead length as a ratio of the rear-to-tip distance",
17135
- type: "number",
17136
- min: .01,
17137
- max: .5,
17138
- step: .01
17139
- },
17140
- {
17141
- key: "arrowheadWidthRatio",
17142
- presentationTier: "advanced",
17143
- label: "Arrowhead width",
17144
- description: "Arrowhead base width as a ratio of the rear-to-tip distance",
17145
- type: "number",
17146
- min: .01,
17147
- max: .5,
17148
- step: .01
17149
- },
17150
- {
17151
- key: "arcSegments",
17152
- presentationTier: "advanced",
17153
- label: "Arc segments",
17154
- description: "Number of line segments approximating the quarter-circle shaft.",
17155
- type: "number",
17156
- min: 4,
17157
- max: 128,
17158
- step: 1
17159
- }
17160
- ]
17161
- };
17162
- /**
17163
- * Generates a quarter-circle shaft from PT2 (rear) to the arrowhead base.
17164
- * The arrowhead continues tangent to the arc and places its tip at PT1.
17165
- * PT3 selects which side of the PT2 -> PT1 chord contains the arc.
17166
- */
17167
- function createTurn(coordinates, options = {}) {
17168
- const tip = project(coordinates[0][0], coordinates[0][1]);
17169
- const rear = project(coordinates[1][0], coordinates[1][1]);
17170
- const side = quarterArcSide(rear, tip, coordinates[2] ? project(coordinates[2][0], coordinates[2][1]) : void 0);
17171
- const rearToTip = vecSub(tip, rear);
17172
- const totalLength = vecMag(rearToTip);
17173
- if (totalLength < 1e-6) return {
17174
- type: "FeatureCollection",
17175
- features: []
17176
- };
17177
- const requestedHeadLength = totalLength * (options.arrowheadLengthRatio ?? DEFAULT_TURN_OPTIONS.arrowheadLengthRatio);
17178
- const tangentComponent = Math.min(Math.max(requestedHeadLength, EPSILON), totalLength * .95) / Math.SQRT2;
17179
- const arcChordLength = Math.sqrt(totalLength * totalLength - tangentComponent * tangentComponent) - tangentComponent;
17180
- const chordOffset = Math.atan2(tangentComponent, arcChordLength + tangentComponent);
17181
- const headBase = vecAdd(rear, vecScale(rotate(vecNorm(rearToTip), side * chordOffset), arcChordLength));
17182
- const arc = createQuarterArc(rear, headBase, side);
17183
- if (!arc) return {
17184
- type: "FeatureCollection",
17185
- features: []
17186
- };
17187
- const segmentCount = Math.round(Math.min(128, Math.max(4, options.arcSegments ?? DEFAULT_TURN_OPTIONS.arcSegments)));
17188
- const arcCoordinates = [];
17189
- for (let i = 0; i <= segmentCount; i++) {
17190
- const angle = arc.startAngle + arc.sweepAngle * i / segmentCount;
17191
- arcCoordinates.push(unproject(arc.center[0] + arc.radius * Math.cos(angle), arc.center[1] + arc.radius * Math.sin(angle)));
17192
- }
17193
- const headWidth = totalLength * (options.arrowheadWidthRatio ?? DEFAULT_TURN_OPTIONS.arrowheadWidthRatio);
17194
- const arrowHead = createArrowHeadRing(tip, vecNorm(vecSub(tip, headBase)), vecMag(vecSub(tip, headBase)), headWidth);
17195
- return {
17196
- type: "FeatureCollection",
17197
- features: [{
17198
- type: "Feature",
17199
- properties: { part: "arc" },
17200
- geometry: {
17201
- type: "MultiLineString",
17202
- coordinates: [arcCoordinates]
17203
- }
17204
- }, {
17205
- type: "Feature",
17206
- properties: {
17207
- part: "arrowhead",
17208
- fill: true,
17209
- style: SOLID_ACCENT_FILL
17210
- },
17211
- geometry: {
17212
- type: "MultiPolygon",
17213
- coordinates: [[[...arrowHead]]]
17214
- }
17215
- }]
17216
- };
17217
- }
17218
- function rotate(vector, angle) {
17219
- const cos = Math.cos(angle);
17220
- const sin = Math.sin(angle);
17221
- return [vector[0] * cos - vector[1] * sin, vector[0] * sin + vector[1] * cos];
17222
- }
17223
- const TURN = defineControlMeasure({
17224
- metadata: TURN_METADATA,
17225
- generator: createTurn,
17226
- defaultOptions: DEFAULT_TURN_OPTIONS,
17227
- rule: turnDrawRule
17228
- });
17229
- //#endregion
17230
18323
  //#region src/generators/cm15-maneuver-areas/turning-movement.ts
17231
18324
  const DEFAULT_CROSSBAR_POSITION = 0;
17232
18325
  const DEFAULT_TURNING_MOVEMENT_OPTIONS = {
@@ -17366,24 +18459,35 @@ const DEFINITIONS = {
17366
18459
  bypass: BYPASS,
17367
18460
  canalize: CANALIZE,
17368
18461
  clear: CLEAR,
18462
+ "cordon-and-knock": CORDON_AND_KNOCK,
18463
+ "cordon-and-search": CORDON_AND_SEARCH,
17369
18464
  counterattack: COUNTERATTACK,
17370
18465
  "counterattack-by-fire": COUNTERATTACK_BY_FIRE,
17371
18466
  cover: COVER,
17372
18467
  delay: DELAY,
18468
+ demonstrate: DEMONSTRATE,
17373
18469
  disengage: DISENGAGE,
17374
18470
  "disrupt-mission-task": DISRUPT_MISSION_TASK,
17375
18471
  envelopment: ENVELOPMENT,
18472
+ exploitation: EXPLOITATION,
18473
+ "forward-passage-of-lines": FORWARD_PASSAGE_OF_LINES,
17376
18474
  infiltration: INFILTRATION,
17377
18475
  guard: GUARD,
17378
18476
  isolate: ISOLATE,
17379
18477
  "movement-to-contact": MOVEMENT_TO_CONTACT,
18478
+ occupy: OCCUPY,
17380
18479
  penetrate: PENETRATE,
18480
+ pursuit: PURSUIT,
17381
18481
  "rearward-passage-of-lines": REARWARD_PASSAGE_OF_LINES,
18482
+ "relief-in-place": RELIEF_IN_PLACE,
17382
18483
  retire: RETIRE,
17383
18484
  screen: SCREEN,
18485
+ secure: SECURE,
17384
18486
  seize: SEIZE,
18487
+ evacuate: EVACUATE,
17385
18488
  withdraw: WITHDRAW,
17386
18489
  "withdraw-under-pressure": WITHDRAW_UNDER_PRESSURE,
18490
+ "turn-mission-task": TURN_MISSION_TASK,
17387
18491
  "antitank-ditch-under-construction": ANTITANK_DITCH_UNDER_CONSTRUCTION,
17388
18492
  "antitank-ditch-completed": ANTITANK_DITCH_COMPLETED,
17389
18493
  "antitank-wall": ANTITANK_WALL,
@@ -17808,4 +18912,4 @@ function assertNever(value) {
17808
18912
  throw new Error(`Unhandled control measure kind: ${String(value)}`);
17809
18913
  }
17810
18914
  //#endregion
17811
- export { DEFAULT_FINAL_PROTECTIVE_FIRE_OPTIONS as $, DEFAULT_LINE_JOIN as $t, DEFAULT_RETIRE_OPTIONS as A, getMidpointPerpendicularSignedDistance as An, DEFAULT_BLOCK_ARROW_OPTIONS as At, DEFAULT_LINE_OF_DEPARTURE_OPTIONS as B, EPSILON as Bn, DEFAULT_MOBILE_DEFENSE_OPTIONS as Bt, DEFAULT_MAIN_ATTACK_OPTIONS as C, disruptDrawRule as Cn, DEFAULT_BYPASS_OPTIONS as Ct, DEFAULT_WITHDRAW_OPTIONS as D, area27DrawRule as Dn, DEFAULT_ENGINEER_WORK_LINE_OPTIONS as Dt, DEFAULT_WITHDRAW_UNDER_PRESSURE_OPTIONS as E, containDrawRule as En, DEFAULT_GENERIC_C2_LINE_OPTIONS as Et, DEFAULT_FORTIFIED_AREA_OPTIONS as F, computeInitialWidthPoint as Fn, DEFAULT_ATTACK_BY_FIRE_OPTIONS as Ft, DEFAULT_RELEASE_LINE_OPTIONS as G, DEFAULT_DIRECTION_OF_ATTACK_AVIATION_OPTIONS as Gt, DEFAULT_BATTLE_HANDOVER_LINE_OPTIONS as H, roundToFixed as Hn, DEFAULT_AREA_DEFENSE_OPTIONS as Ht, DEFAULT_FORTIFIED_LINE_OPTIONS as I, haversineDistance as In, DEFAULT_ANTITANK_WALL_OPTIONS as It, DEFAULT_BRIDGEHEAD_LINE_OPTIONS as J, normalizeTextAmplifiers as Jt, DEFAULT_HOLDING_LINE_OPTIONS as K, TEXT_AMPLIFIER_FIELDS as Kt, DEFAULT_PROBABLE_LINE_OF_DEPLOYMENT_OPTIONS as L, project as Ln, DEFAULT_ANTITANK_DITCH_OPTIONS as Lt, DEFAULT_PENETRATE_OPTIONS as M, snapToMidpointPerpendicular as Mn, DEFAULT_CONTAIN_OPTIONS as Mt, DEFAULT_MOVEMENT_TO_CONTACT_OPTIONS as N, createBaselineFrame as Nn, DEFAULT_BATTLE_POSITION_OPTIONS as Nt, DEFAULT_SEIZE_OPTIONS as O, computeDefaultMidpointPerpendicularPoint as On, DEFAULT_LIGHT_LINE_OPTIONS as Ot, DEFAULT_FRONTAL_ATTACK_OPTIONS as P, calculateMetrics as Pn, DEFAULT_ATTACK_HELICOPTER_OPTIONS as Pt, DEFAULT_FIX_OPTIONS as Q, DEFAULT_LINE_CAP as Qt, PROBABLE_LINE_OF_DEPLOYMENT_DASH as R, sphericalBearing as Rn, DEFAULT_ASSEMBLY_AREA_OPTIONS as Rt, DEFAULT_MINEFIELD_OPTIONS as S, penetrateDrawRule as Sn, DEFAULT_CANALIZE_OPTIONS as St, DEFAULT_JOINT_TACTICAL_ACTION_AREA_OPTIONS as T, centerRadiusDrawRule as Tn, DEFAULT_BLOCK_MISSION_TASK_OPTIONS as Tt, DEFAULT_HANDOVER_LINE_OPTIONS as U, DEFAULT_DIRECTION_OF_SUPPORTING_ATTACK_OPTIONS as Ut, DEFAULT_LIMIT_OF_ADVANCE_OPTIONS as V, getMetersPerPixel as Vn, DEFAULT_ENGAGEMENT_AREA_OPTIONS as Vt, DEFAULT_FORWARD_EDGE_OF_BATTLE_AREA_OPTIONS as W, DEFAULT_DIRECTION_OF_MAIN_ATTACK_OPTIONS as Wt, DEFAULT_FLOT_OPTIONS as X, DEFAULT_LABEL_HEIGHT_CSS_PIXELS as Xt, DEFAULT_PHASE_LINE_OPTIONS as Y, resolveAmplifierPlacement as Yt, DEFAULT_FIX_MISSION_TASK_OPTIONS as Z, DEFAULT_LABEL_SIZE_CLAMP_CSS_PIXELS as Zt, DEFAULT_PRINCIPAL_DIRECTION_OF_FIRE_OPTIONS as _, attackByFireDrawRule as _n, DEFAULT_GENERIC_RECTANGLE_OPTIONS as _t, DEFINITIONS as a, DEFAULT_AIRBORNE_ATTACK_OPTIONS as an, DEFAULT_DELAY_OPTIONS as at, DEFAULT_OBSTACLE_BYPASS_EASY_OPTIONS as b, staticPointDrawRule as bn, DEFAULT_GENERIC_CIRCLE_OPTIONS as bt, getDefaultOptions as c, supportByFireDrawRule as cn, DEFAULT_COUNTERATTACK_BY_FIRE_OPTIONS as ct, DEFAULT_TURN_OPTIONS as d, line26DrawRule as dn, DEFAULT_CLEAR_OPTIONS as dt, DEFAULT_PORTRAYAL as en, DEFAULT_ENCIRCLEMENT_OPTIONS as et, DEFAULT_SUPPORT_BY_FIRE_OPTIONS as f, line24DrawRule as fn, DEFAULT_RADAR_SEARCH_DOCTRINE_OPTIONS as ft, SECONDARY_DIRECTION_OF_FIRE_DASH as g, ambushDrawRule as gn, DEFAULT_GENERIC_SECTOR_OPTIONS as gt, DEFAULT_SECONDARY_DIRECTION_OF_FIRE_OPTIONS as h, line1DrawRule as hn, DEFAULT_GENERIC_TEXT_OPTIONS as ht, CONTROL_MEASURE_METADATA as i, DEFAULT_AMBUSH_OPTIONS as in, DEFAULT_DISENGAGE_OPTIONS as it, DEFAULT_REARWARD_PASSAGE_OF_LINES_OPTIONS as j, pointOnMidpointPerpendicularAxis as jn, DEFAULT_BLOCK_OPTIONS as jt, DEFAULT_SCREEN_OPTIONS as k, createMidpointPerpendicularDrawRule as kn, DEFAULT_BOUNDARY_OPTIONS as kt, listControlMeasureMetadata as l, polyline1DrawRule as ln, DEFAULT_COUNTERATTACK_OPTIONS as lt, DEFAULT_STRONG_POINT_OPTIONS as m, turnDrawRule as mn, RADAR_SEARCH_DOCTRINE_STROKE_COLOR as mt, resolveStyleHints as n, DEFAULT_STROKE_WIDTH_CSS_PIXELS as nn, DEFAULT_GUARD_OPTIONS as nt, getControlMeasureMetadata as o, rectangleDrawRule as on, DEFAULT_COVER_OPTIONS as ot, DEFAULT_RETAIN_OPTIONS as p, line23DrawRule as pn, RADAR_SEARCH_DOCTRINE_FILL_COLOR as pt, DEFAULT_INFILTRATION_LANE_OPTIONS as q, canonicalTextAmplifierKey as qt, CONTROL_MEASURE_IDS as r, DEFAULT_SYMBOL_COLOR as rn, DEFAULT_DISRUPT_MISSION_TASK_OPTIONS as rt, getControlMeasureMetadataByValue as s, axis1DrawRule as sn, DEFAULT_TACTICAL_ARROW_OPTIONS as st, renderControlMeasure as t, DEFAULT_STROKE_DASH_CSS_PIXELS as tn, DEFAULT_DISRUPT_OPTIONS as tt, DEFAULT_TURNING_MOVEMENT_OPTIONS as u, line27DrawRule as un, DEFAULT_SUPPORTING_ATTACK_OPTIONS as ut, DEFAULT_PICKUP_ZONE_OPTIONS as v, dynamicPointDrawRule as vn, DEFAULT_GENERIC_POLYGON_OPTIONS as vt, DEFAULT_LANDING_ZONE_OPTIONS as w, blockDrawRule as wn, DEFAULT_BREACH_OPTIONS as wt, DEFAULT_OBSTACLE_BYPASS_DIFFICULT_OPTIONS as x, point12DrawRule as xn, DEFAULT_CLASSIC_ARROW_OPTIONS as xt, DEFAULT_OBSTACLE_BYPASS_IMPOSSIBLE_OPTIONS as y, sectorDrawRule as yn, DEFAULT_GENERIC_LINE_OPTIONS as yt, DEFAULT_LINE_OF_DEPARTURE_CONTACT_OPTIONS as z, unproject as zn, DEFAULT_AREA_OF_OPERATIONS_OPTIONS as zt };
18915
+ export { DEFAULT_PHASE_LINE_OPTIONS as $, DEFAULT_DIRECTION_OF_ATTACK_AVIATION_OPTIONS as $t, DEFAULT_SECURE_OPTIONS as A, secureDrawRule as An, DEFAULT_CANALIZE_OPTIONS as At, DEFAULT_FORTIFIED_LINE_OPTIONS as B, getMidpointPerpendicularSignedDistance as Bn, DEFAULT_CONTAIN_OPTIONS as Bt, DEFAULT_LANDING_ZONE_OPTIONS as C, line1DrawRule as Cn, DEFAULT_GENERIC_TEXT_OPTIONS as Ct, DEFAULT_WITHDRAW_OPTIONS as D, sectorDrawRule as Dn, DEFAULT_GENERIC_LINE_OPTIONS as Dt, DEFAULT_WITHDRAW_UNDER_PRESSURE_OPTIONS as E, dynamicPointDrawRule as En, DEFAULT_GENERIC_POLYGON_OPTIONS as Et, DEFAULT_PENETRATE_OPTIONS as F, centerRadiusDrawRule as Fn, DEFAULT_ENGINEER_WORK_LINE_OPTIONS as Ft, DEFAULT_LIMIT_OF_ADVANCE_OPTIONS as G, computeInitialWidthPoint as Gn, DEFAULT_ANTITANK_DITCH_OPTIONS as Gt, PROBABLE_LINE_OF_DEPLOYMENT_DASH as H, snapToMidpointPerpendicular as Hn, DEFAULT_ATTACK_HELICOPTER_OPTIONS as Ht, DEFAULT_OCCUPY_OPTIONS as I, containDrawRule as In, DEFAULT_LIGHT_LINE_OPTIONS as It, DEFAULT_FORWARD_EDGE_OF_BATTLE_AREA_OPTIONS as J, sphericalBearing as Jn, DEFAULT_MOBILE_DEFENSE_OPTIONS as Jt, DEFAULT_BATTLE_HANDOVER_LINE_OPTIONS as K, haversineDistance as Kn, DEFAULT_ASSEMBLY_AREA_OPTIONS as Kt, DEFAULT_MOVEMENT_TO_CONTACT_OPTIONS as L, area27DrawRule as Ln, DEFAULT_BOUNDARY_OPTIONS as Lt, DEFAULT_RETIRE_OPTIONS as M, occupyDrawRule as Mn, DEFAULT_BREACH_OPTIONS as Mt, DEFAULT_RELIEF_IN_PLACE_OPTIONS as N, disruptDrawRule as Nn, DEFAULT_BLOCK_MISSION_TASK_OPTIONS as Nt, DEFAULT_EVACUATE_OPTIONS as O, staticPointDrawRule as On, DEFAULT_GENERIC_CIRCLE_OPTIONS as Ot, DEFAULT_REARWARD_PASSAGE_OF_LINES_OPTIONS as P, blockDrawRule as Pn, DEFAULT_GENERIC_C2_LINE_OPTIONS as Pt, DEFAULT_BRIDGEHEAD_LINE_OPTIONS as Q, roundToFixed as Qn, DEFAULT_DIRECTION_OF_MAIN_ATTACK_OPTIONS as Qt, DEFAULT_FRONTAL_ATTACK_OPTIONS as R, computeDefaultMidpointPerpendicularPoint as Rn, DEFAULT_BLOCK_ARROW_OPTIONS as Rt, DEFAULT_MAIN_ATTACK_OPTIONS as S, turnDrawRule as Sn, RADAR_SEARCH_DOCTRINE_STROKE_COLOR as St, DEFAULT_TURN_OPTIONS as T, attackByFireDrawRule as Tn, DEFAULT_GENERIC_RECTANGLE_OPTIONS as Tt, DEFAULT_LINE_OF_DEPARTURE_CONTACT_OPTIONS as U, createBaselineFrame as Un, DEFAULT_ATTACK_BY_FIRE_OPTIONS as Ut, DEFAULT_PROBABLE_LINE_OF_DEPLOYMENT_OPTIONS as V, pointOnMidpointPerpendicularAxis as Vn, DEFAULT_BATTLE_POSITION_OPTIONS as Vt, DEFAULT_LINE_OF_DEPARTURE_OPTIONS as W, calculateMetrics as Wn, DEFAULT_ANTITANK_WALL_OPTIONS as Wt, DEFAULT_HOLDING_LINE_OPTIONS as X, EPSILON as Xn, DEFAULT_AREA_DEFENSE_OPTIONS as Xt, DEFAULT_RELEASE_LINE_OPTIONS as Y, unproject as Yn, DEFAULT_ENGAGEMENT_AREA_OPTIONS as Yt, DEFAULT_INFILTRATION_LANE_OPTIONS as Z, getMetersPerPixel as Zn, DEFAULT_DIRECTION_OF_SUPPORTING_ATTACK_OPTIONS as Zt, DEFAULT_PICKUP_ZONE_OPTIONS as _, polyline1DrawRule as _n, DEFAULT_CORDON_AND_KNOCK_OPTIONS as _t, DEFINITIONS as a, DEFAULT_LABEL_SIZE_CLAMP_CSS_PIXELS as an, DEFAULT_DISRUPT_OPTIONS as at, DEFAULT_OBSTACLE_BYPASS_DIFFICULT_OPTIONS as b, line24DrawRule as bn, DEFAULT_RADAR_SEARCH_DOCTRINE_OPTIONS as bt, getDefaultOptions as c, DEFAULT_PORTRAYAL as cn, DEFAULT_DISENGAGE_OPTIONS as ct, DEFAULT_SUPPORT_BY_FIRE_OPTIONS as d, DEFAULT_SYMBOL_COLOR as dn, DEFAULT_COVER_OPTIONS as dt, TEXT_AMPLIFIER_FIELDS as en, DEFAULT_FLOT_OPTIONS as et, DEFAULT_RETAIN_OPTIONS as f, DEFAULT_AMBUSH_OPTIONS as fn, DEFAULT_TACTICAL_ARROW_OPTIONS as ft, DEFAULT_PRINCIPAL_DIRECTION_OF_FIRE_OPTIONS as g, supportByFireDrawRule as gn, DEFAULT_CORDON_AND_SEARCH_OPTIONS as gt, SECONDARY_DIRECTION_OF_FIRE_DASH as h, axis1DrawRule as hn, DEFAULT_SUPPORTING_ATTACK_OPTIONS as ht, CONTROL_MEASURE_METADATA as i, DEFAULT_LABEL_HEIGHT_CSS_PIXELS as in, DEFAULT_ENCIRCLEMENT_OPTIONS as it, DEFAULT_SCREEN_OPTIONS as j, penetrateDrawRule as jn, DEFAULT_BYPASS_OPTIONS as jt, DEFAULT_SEIZE_OPTIONS as k, point12DrawRule as kn, DEFAULT_CLASSIC_ARROW_OPTIONS as kt, listControlMeasureMetadata as l, DEFAULT_STROKE_DASH_CSS_PIXELS as ln, DEFAULT_DEMONSTRATE_OPTIONS as lt, DEFAULT_SECONDARY_DIRECTION_OF_FIRE_OPTIONS as m, rectangleDrawRule as mn, DEFAULT_COUNTERATTACK_OPTIONS as mt, resolveStyleHints as n, normalizeTextAmplifiers as nn, DEFAULT_FIX_OPTIONS as nt, getControlMeasureMetadata as o, DEFAULT_LINE_CAP as on, DEFAULT_GUARD_OPTIONS as ot, DEFAULT_STRONG_POINT_OPTIONS as p, DEFAULT_AIRBORNE_ATTACK_OPTIONS as pn, DEFAULT_COUNTERATTACK_BY_FIRE_OPTIONS as pt, DEFAULT_HANDOVER_LINE_OPTIONS as q, project as qn, DEFAULT_AREA_OF_OPERATIONS_OPTIONS as qt, CONTROL_MEASURE_IDS as r, resolveAmplifierPlacement as rn, DEFAULT_FINAL_PROTECTIVE_FIRE_OPTIONS as rt, getControlMeasureMetadataByValue as s, DEFAULT_LINE_JOIN as sn, DEFAULT_DISRUPT_MISSION_TASK_OPTIONS as st, renderControlMeasure as t, canonicalTextAmplifierKey as tn, DEFAULT_FIX_MISSION_TASK_OPTIONS as tt, DEFAULT_TURNING_MOVEMENT_OPTIONS as u, DEFAULT_STROKE_WIDTH_CSS_PIXELS as un, DEFAULT_DELAY_OPTIONS as ut, DEFAULT_OBSTACLE_BYPASS_IMPOSSIBLE_OPTIONS as v, line27DrawRule as vn, DEFAULT_ISOLATE_OPTIONS as vt, DEFAULT_JOINT_TACTICAL_ACTION_AREA_OPTIONS as w, ambushDrawRule as wn, DEFAULT_GENERIC_SECTOR_OPTIONS as wt, DEFAULT_MINEFIELD_OPTIONS as x, line23DrawRule as xn, RADAR_SEARCH_DOCTRINE_FILL_COLOR as xt, DEFAULT_OBSTACLE_BYPASS_EASY_OPTIONS as y, line26DrawRule as yn, DEFAULT_CLEAR_OPTIONS as yt, DEFAULT_FORTIFIED_AREA_OPTIONS as z, createMidpointPerpendicularDrawRule as zn, DEFAULT_BLOCK_OPTIONS as zt };