@orbat-mapper/control-measures 0.2.0 → 0.3.1

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.
@@ -558,7 +558,7 @@ function samePosition(a, b) {
558
558
  }
559
559
  //#endregion
560
560
  //#region src/draw-rules/area1.ts
561
- function derive$10(points) {
561
+ function derive$11(points) {
562
562
  return clonePositions(points);
563
563
  }
564
564
  /**
@@ -578,9 +578,9 @@ const area1DrawRule = {
578
578
  id: "area1",
579
579
  minimumUserPoints: 3,
580
580
  closedRing: true,
581
- derive: derive$10,
581
+ derive: derive$11,
582
582
  transform(event) {
583
- return derive$10(event.next);
583
+ return derive$11(event.next);
584
584
  }
585
585
  };
586
586
  //#endregion
@@ -606,14 +606,14 @@ const disruptDrawRule = createMidpointPerpendicularDrawRule({
606
606
  * that fixes both the radius and the bearing of the symbol's opening. Both
607
607
  * points are user-clicked, so the canonical array is just the (clamped) input.
608
608
  */
609
- function derive$9(points) {
609
+ function derive$10(points) {
610
610
  return clonePositions(points.slice(0, 2));
611
611
  }
612
612
  const centerRadiusDrawRule = {
613
613
  id: "area15:center-radius",
614
614
  minimumUserPoints: 2,
615
615
  canonicalPointCount: 2,
616
- derive: derive$9,
616
+ derive: derive$10,
617
617
  transform(event) {
618
618
  const { previous, next, activePointIndex } = event;
619
619
  if (activePointIndex === 0 && previous.length >= 2 && next.length >= 2) {
@@ -622,12 +622,25 @@ const centerRadiusDrawRule = {
622
622
  const origin = previous[1];
623
623
  return [clonePosition(next[0]), [origin[0] + dx, origin[1] + dy]];
624
624
  }
625
- return derive$9(next);
625
+ return derive$10(next);
626
626
  }
627
627
  };
628
628
  /** Backward-compatible doctrinal name for Area15's shared center-radius rule. */
629
629
  const isolateDrawRule = centerRadiusDrawRule;
630
630
  //#endregion
631
+ //#region src/draw-rules/area17.ts
632
+ /**
633
+ * Area17 anchor draw rule for Penetrate.
634
+ *
635
+ * PT.1/PT.2 are the free barrier endpoints. PT.3 is the arrow tail,
636
+ * constrained to the perpendicular axis through the barrier midpoint.
637
+ */
638
+ const penetrateDrawRule = createMidpointPerpendicularDrawRule({
639
+ id: "area17:penetrate",
640
+ minimumUserPoints: 3,
641
+ minimumPreviewPoints: 2
642
+ });
643
+ //#endregion
631
644
  //#region src/draw-rules/point12.ts
632
645
  /**
633
646
  * Point12 anchor draw rule for the obstacle-bypass family.
@@ -645,16 +658,16 @@ const point12DrawRule = createMidpointPerpendicularDrawRule({ id: "point12:obsta
645
658
  * Point1 — a single anchor point (e.g. Text's center). One user click commits
646
659
  * the measure; dragging the point simply translates it.
647
660
  */
648
- function derive$8(points) {
661
+ function derive$9(points) {
649
662
  return clonePositions(points.slice(0, 1));
650
663
  }
651
664
  const pointDrawRule = {
652
665
  id: "point1:anchor",
653
666
  minimumUserPoints: 1,
654
667
  canonicalPointCount: 1,
655
- derive: derive$8,
668
+ derive: derive$9,
656
669
  transform(event) {
657
- return derive$8(event.next);
670
+ return derive$9(event.next);
658
671
  }
659
672
  };
660
673
  //#endregion
@@ -685,20 +698,20 @@ const ambushDrawRule = createMidpointPerpendicularDrawRule({
685
698
  });
686
699
  //#endregion
687
700
  //#region src/draw-rules/line1.ts
688
- function derive$7(points) {
701
+ function derive$8(points) {
689
702
  return clonePositions(points);
690
703
  }
691
704
  const line1DrawRule = {
692
705
  id: "line1",
693
706
  minimumUserPoints: 2,
694
- derive: derive$7,
707
+ derive: derive$8,
695
708
  transform(event) {
696
- return derive$7(event.next);
709
+ return derive$8(event.next);
697
710
  }
698
711
  };
699
712
  //#endregion
700
713
  //#region src/draw-rules/line3.ts
701
- function derive$6(points) {
714
+ function derive$7(points) {
702
715
  return clonePositions(points);
703
716
  }
704
717
  /**
@@ -713,23 +726,23 @@ const line3DrawRule = {
713
726
  id: "line3",
714
727
  minimumUserPoints: 3,
715
728
  canonicalPointCount: 3,
716
- derive: derive$6,
729
+ derive: derive$7,
717
730
  transform(event) {
718
- return derive$6(event.next);
731
+ return derive$7(event.next);
719
732
  }
720
733
  };
721
734
  //#endregion
722
735
  //#region src/draw-rules/line9.ts
723
- function derive$5(points) {
736
+ function derive$6(points) {
724
737
  return clonePositions(points);
725
738
  }
726
739
  const line9DrawRule = {
727
740
  id: "line9",
728
741
  minimumUserPoints: 2,
729
742
  canonicalPointCount: 2,
730
- derive: derive$5,
743
+ derive: derive$6,
731
744
  transform(event) {
732
- return derive$5(event.next);
745
+ return derive$6(event.next);
733
746
  }
734
747
  };
735
748
  //#endregion
@@ -836,7 +849,7 @@ const line24DrawRule = createMidpointPerpendicularDrawRule({
836
849
  });
837
850
  //#endregion
838
851
  //#region src/draw-rules/line26.ts
839
- function derive$4(points) {
852
+ function derive$5(points) {
840
853
  if (points.length === 2) {
841
854
  const [p1, p2] = points;
842
855
  const dx = p2[0] - p1[0];
@@ -875,9 +888,89 @@ const line26DrawRule = {
875
888
  minimumUserPoints: 4,
876
889
  minimumPreviewPoints: 2,
877
890
  canonicalPointCount: 4,
891
+ derive: derive$5,
892
+ transform(event) {
893
+ return derive$5(event.next);
894
+ }
895
+ };
896
+ //#endregion
897
+ //#region src/draw-rules/line27.ts
898
+ function deriveArcPoint(p1, p2, p4) {
899
+ const center = project(p1[0], p1[1]);
900
+ const start = project(p2[0], p2[1]);
901
+ const tip = project(p4[0], p4[1]);
902
+ const arc = createQuarterArc(start, tip, quarterArcSide(start, tip, center) === 1 ? -1 : 1);
903
+ if (!arc) return [(p2[0] + p4[0]) / 2, (p2[1] + p4[1]) / 2];
904
+ return unproject(arc.midpoint[0], arc.midpoint[1]);
905
+ }
906
+ function derive$4(points) {
907
+ if (points.length < 2) return clonePositions(points);
908
+ const [p1, p2] = points;
909
+ if (points.length === 2) {
910
+ const dx = p2[0] - p1[0];
911
+ const dy = p2[1] - p1[1];
912
+ const p4 = [p2[0] + dx * 2.5, p2[1] + dy * 2.5];
913
+ return clonePositions([
914
+ p1,
915
+ p2,
916
+ deriveArcPoint(p1, p2, p4),
917
+ p4
918
+ ]);
919
+ }
920
+ if (points.length === 3) {
921
+ const p4 = points[2];
922
+ return clonePositions([
923
+ p1,
924
+ p2,
925
+ deriveArcPoint(p1, p2, p4),
926
+ p4
927
+ ]);
928
+ }
929
+ return clonePositions(points.slice(0, 4));
930
+ }
931
+ /**
932
+ * Line27 anchor draw rule for the Seize mission task.
933
+ *
934
+ * PT. 1 is the circle center, PT. 2 fixes its radius and the curved arrow's
935
+ * start, derived PT. 3 lies on and selects the circular arc, and the user's
936
+ * third click becomes PT. 4 (the arrow tip). PT. 3 remains directly editable
937
+ * after commit. Dragging PT. 1 carries PT. 2 with it so the assigned-unit
938
+ * circle keeps its radius.
939
+ */
940
+ const line27DrawRule = {
941
+ id: "line27:seize",
942
+ minimumUserPoints: 3,
943
+ minimumPreviewPoints: 2,
944
+ canonicalPointCount: 4,
878
945
  derive: derive$4,
879
946
  transform(event) {
880
- return derive$4(event.next);
947
+ const { previous, next, activePointIndex } = event;
948
+ if (activePointIndex === 0 && previous.length >= 4 && next.length >= 4) {
949
+ const dx = next[0][0] - previous[0][0];
950
+ const dy = next[0][1] - previous[0][1];
951
+ const oldRadiusPoint = previous[1];
952
+ const p1 = clonePosition(next[0]);
953
+ const p2 = [oldRadiusPoint[0] + dx, oldRadiusPoint[1] + dy];
954
+ const p4 = clonePosition(next[3]);
955
+ return [
956
+ p1,
957
+ p2,
958
+ deriveArcPoint(p1, p2, p4),
959
+ p4
960
+ ];
961
+ }
962
+ if ((activePointIndex === 1 || activePointIndex === 3) && next.length >= 4) {
963
+ const p1 = clonePosition(next[0]);
964
+ const p2 = clonePosition(next[1]);
965
+ const p4 = clonePosition(next[3]);
966
+ return [
967
+ p1,
968
+ p2,
969
+ deriveArcPoint(p1, p2, p4),
970
+ p4
971
+ ];
972
+ }
973
+ return clonePositions(next.slice(0, 4));
881
974
  }
882
975
  };
883
976
  //#endregion
@@ -965,6 +1058,48 @@ const axis1DrawRule = {
965
1058
  return packWithWidth(next, projected);
966
1059
  }
967
1060
  };
1061
+ /**
1062
+ * Recesses the CATK body from P1. The original P1 remains the tip of the fire
1063
+ * arrow, while P2 and the remaining Axis1 points continue to shape the body.
1064
+ */
1065
+ function counterattackByFireBodyCoordinates(points) {
1066
+ const fireTipCoordinate = points[0];
1067
+ const neckCoordinate = points[1];
1068
+ if (!fireTipCoordinate || !neckCoordinate) return null;
1069
+ const fireTip = project(fireTipCoordinate[0], fireTipCoordinate[1]);
1070
+ const terminal = vecSub(fireTip, project(neckCoordinate[0], neckCoordinate[1]));
1071
+ if (vecMag(terminal) < 1e-6) return null;
1072
+ const bodyTip = vecAdd(fireTip, vecScale(terminal, -.3));
1073
+ return [unproject(bodyTip[0], bodyTip[1]), ...points.slice(1)];
1074
+ }
1075
+ function restoreFireTip(points, fireTip) {
1076
+ if (points.length > 0 && fireTip) points[0] = clonePosition(fireTip);
1077
+ return points;
1078
+ }
1079
+ /**
1080
+ * Axis1 interaction for Counterattack by Fire, with its width frame anchored
1081
+ * to the recessed CATK body tip rather than P1, which belongs to the separate
1082
+ * fire arrow.
1083
+ */
1084
+ const counterattackByFireDrawRule = {
1085
+ ...axis1DrawRule,
1086
+ id: "axis1:counterattack-by-fire",
1087
+ derive(points) {
1088
+ const bodyPoints = counterattackByFireBodyCoordinates(points);
1089
+ if (!bodyPoints) return axis1DrawRule.derive(points);
1090
+ return restoreFireTip(axis1DrawRule.derive(bodyPoints), points[0]);
1091
+ },
1092
+ transform(event) {
1093
+ const previousBody = counterattackByFireBodyCoordinates(event.previous);
1094
+ const nextBody = counterattackByFireBodyCoordinates(event.next);
1095
+ if (!previousBody || !nextBody) return axis1DrawRule.transform(event);
1096
+ return restoreFireTip(axis1DrawRule.transform({
1097
+ ...event,
1098
+ previous: previousBody,
1099
+ next: nextBody
1100
+ }), event.next[0]);
1101
+ }
1102
+ };
968
1103
  //#endregion
969
1104
  //#region src/draw-rules/area21.ts
970
1105
  function derive$1(points) {
@@ -4779,28 +4914,29 @@ function generateAntitankDitchTriangles(projectedPoints, toothHeight, toothWidth
4779
4914
  return triangles;
4780
4915
  }
4781
4916
  /**
4782
- * Projects `positions` and, when `options.smooth` is set, curves the result
4783
- * through the control points (centripetal Catmull-Rom) before the teeth are
4784
- * traced — matching Fortified Line's base-path smoothing. Returns the input
4785
- * unchanged (no project/unproject round trip) when smoothing is off.
4917
+ * When `options.smooth` is set, curves the projected control points
4918
+ * (centripetal Catmull-Rom) before the teeth are traced — matching Fortified
4919
+ * Line's base-path smoothing.
4786
4920
  */
4787
- function smoothedDitchBasePath(positions, options) {
4788
- const projectedPoints = positions.map((position) => project(position[0], position[1]));
4921
+ function ditchPaths(projectedPoints, options) {
4789
4922
  const smoothedPath = smoothLineVerts(projectedPoints, options, 16);
4790
4923
  const toothHeight = calculateAntitankDitchHeight(options);
4791
4924
  const toothWidthRatio = Math.max(EPSILON, options.toothWidthRatio ?? DEFAULT_TOOTH_WIDTH_RATIO$1);
4792
- return options.smooth && projectedPoints.length >= 3 ? evenlySpacePath(smoothedPath, toothWidthRatio * toothHeight) : smoothedPath;
4925
+ return {
4926
+ smoothedPath,
4927
+ basePath: options.smooth && projectedPoints.length >= 3 ? evenlySpacePath(smoothedPath, toothWidthRatio * toothHeight) : smoothedPath
4928
+ };
4793
4929
  }
4794
4930
  function buildAntitankDitchTriangles(basePath, options) {
4795
4931
  return generateAntitankDitchTriangles(basePath, calculateAntitankDitchHeight(options), options.toothWidthRatio ?? DEFAULT_TOOTH_WIDTH_RATIO$1);
4796
4932
  }
4797
4933
  function createAntitankDitchUnderConstruction(positions, options = {}, textAmplifiers = {}) {
4798
- const basePath = smoothedDitchBasePath(positions, options);
4934
+ const { smoothedPath, basePath } = ditchPaths(positions.map((position) => project(position[0], position[1])), options);
4799
4935
  const triangles = buildAntitankDitchTriangles(basePath, options);
4800
4936
  const baseLine = options.smooth ? basePath.map((point) => unproject(point[0], point[1])) : positions.map((position) => [position[0], position[1]]);
4801
4937
  const triangleLines = triangles.map((triangle) => triangle.map((point) => unproject(point[0], point[1])));
4802
4938
  const labels = [];
4803
- pushHostileEndLabels(labels, basePath, textAmplifiers.N, options);
4939
+ pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options);
4804
4940
  return {
4805
4941
  type: "FeatureCollection",
4806
4942
  features: [{
@@ -4814,12 +4950,12 @@ function createAntitankDitchUnderConstruction(positions, options = {}, textAmpli
4814
4950
  };
4815
4951
  }
4816
4952
  function createAntitankDitchCompleted(positions, options = {}, textAmplifiers = {}) {
4817
- const basePath = smoothedDitchBasePath(positions, options);
4953
+ const { smoothedPath, basePath } = ditchPaths(positions.map((position) => project(position[0], position[1])), options);
4818
4954
  const polygons = buildAntitankDitchTriangles(basePath, options).map((triangle) => {
4819
4955
  return [[...triangle, triangle[0]].map((point) => unproject(point[0], point[1]))];
4820
4956
  });
4821
4957
  const labels = [];
4822
- pushHostileEndLabels(labels, basePath, textAmplifiers.N, options);
4958
+ pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options);
4823
4959
  return {
4824
4960
  type: "FeatureCollection",
4825
4961
  features: [{
@@ -4915,10 +5051,9 @@ function createAntitankWall(positions, options = {}, textAmplifiers = {}) {
4915
5051
  const toothSpacingRatio = options.toothSpacingRatio ?? DEFAULT_TOOTH_SPACING_RATIO;
4916
5052
  const projectedPoints = positions.map((position) => project(position[0], position[1]));
4917
5053
  const smoothedPath = smoothLineVerts(projectedPoints, options, 16);
4918
- const basePath = options.smooth && projectedPoints.length >= 3 ? evenlySpacePath(smoothedPath, Math.max(EPSILON, toothWidthRatio) * toothHeight) : smoothedPath;
4919
- const wallLine = generateAntitankWallPoints(basePath, toothHeight, toothWidthRatio, toothSpacingRatio).map((point) => unproject(point[0], point[1]));
5054
+ const wallLine = generateAntitankWallPoints(options.smooth && projectedPoints.length >= 3 ? evenlySpacePath(smoothedPath, Math.max(EPSILON, toothWidthRatio) * toothHeight) : smoothedPath, toothHeight, toothWidthRatio, toothSpacingRatio).map((point) => unproject(point[0], point[1]));
4920
5055
  const labels = [];
4921
- pushHostileEndLabels(labels, basePath, textAmplifiers.N, options);
5056
+ pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options);
4922
5057
  return {
4923
5058
  type: "FeatureCollection",
4924
5059
  features: [{
@@ -6448,6 +6583,10 @@ const GENERIC_C2_LINE = defineControlMeasure({
6448
6583
  });
6449
6584
  //#endregion
6450
6585
  //#region src/generators/cm34-mission-tasks/shared.ts
6586
+ /** Unprojects a point in Web Mercator meters back to a GeoJSON position. */
6587
+ function toPosition(point) {
6588
+ return unproject(point[0], point[1]);
6589
+ }
6451
6590
  /**
6452
6591
  * Builds an open chevron arrowhead (left → tip → right) as unprojected
6453
6592
  * coordinates. The tip sits at `tip`, pointing along `dir`, with the base set
@@ -7966,17 +8105,28 @@ function createSupportingAttack(coordinates, options = {}) {
7966
8105
  properties: {},
7967
8106
  geometry: {
7968
8107
  type: "LineString",
7969
- coordinates: [
7970
- ...geometry.shaftLeft,
7971
- geometry.headRing[0],
7972
- geometry.headRing[1],
7973
- geometry.headRing[2],
7974
- ...[...geometry.shaftRight].reverse()
7975
- ].map((p) => unproject(p[0], p[1]))
8108
+ coordinates: supportingAttackOutline(geometry)
7976
8109
  }
7977
8110
  }]
7978
8111
  };
7979
8112
  }
8113
+ /**
8114
+ * Traces the single-LineString supporting-attack outline from already-computed
8115
+ * attack geometry, as unprojected coordinates. Exposed so measures that build
8116
+ * on the supporting-attack body (Counterattack by Fire) can reuse the trace
8117
+ * without running {@link processAttackGeometry} a second time.
8118
+ *
8119
+ * Path: [ShaftLeft...] -> OuterLeft -> Tip -> OuterRight -> [ShaftRight...(reversed)]
8120
+ */
8121
+ function supportingAttackOutline(geometry) {
8122
+ return [
8123
+ ...geometry.shaftLeft,
8124
+ geometry.headRing[0],
8125
+ geometry.headRing[1],
8126
+ geometry.headRing[2],
8127
+ ...[...geometry.shaftRight].reverse()
8128
+ ].map((p) => unproject(p[0], p[1]));
8129
+ }
7980
8130
  const SUPPORTING_ATTACK = defineControlMeasure({
7981
8131
  metadata: SUPPORTING_ATTACK_METADATA,
7982
8132
  generator: createSupportingAttack,
@@ -8082,6 +8232,185 @@ const COUNTERATTACK = defineControlMeasure({
8082
8232
  }
8083
8233
  });
8084
8234
  //#endregion
8235
+ //#region src/internal/arrowhead.ts
8236
+ /**
8237
+ * Builds a closed, filled arrowhead triangle ring in unprojected coordinates.
8238
+ *
8239
+ * `tip` is the point of the arrow, `dir` the unit direction the arrow points,
8240
+ * `length` the distance from tip back to the base, and `width` the base width.
8241
+ * The returned ring is `[left, tip, right, left]`.
8242
+ */
8243
+ function createArrowHeadRing(tip, dir, length, width) {
8244
+ const perp = [-dir[1], dir[0]];
8245
+ const base = vecSub(tip, vecScale(dir, length));
8246
+ const left = vecAdd(base, vecScale(perp, width / 2));
8247
+ const right = vecSub(base, vecScale(perp, width / 2));
8248
+ return [
8249
+ unproject(left[0], left[1]),
8250
+ unproject(tip[0], tip[1]),
8251
+ unproject(right[0], right[1]),
8252
+ unproject(left[0], left[1])
8253
+ ];
8254
+ }
8255
+ //#endregion
8256
+ //#region src/generators/cm34-mission-tasks/counterattack-by-fire.ts
8257
+ const DEFAULT_COUNTERATTACK_BY_FIRE_OPTIONS = { ...DEFAULT_COUNTERATTACK_OPTIONS };
8258
+ /** Length of the solid fire arrowhead relative to the recessed P1 extension. */
8259
+ const COUNTERATTACK_BY_FIRE_HEAD_RATIO = .18;
8260
+ const COUNTERATTACK_BY_FIRE_METADATA = {
8261
+ id: "counterattack-by-fire",
8262
+ name: "Counterattack by Fire",
8263
+ description: "An attack against an enemy attacking force using fires",
8264
+ entity: "Mission Tasks",
8265
+ entityType: "Counterattack by Fire",
8266
+ value: "340700",
8267
+ minCoordinates: 3,
8268
+ geometry: "line",
8269
+ geometryTypes: [
8270
+ "LineString",
8271
+ "MultiLineString",
8272
+ "Polygon",
8273
+ "Point"
8274
+ ],
8275
+ paints: {
8276
+ stroke: true,
8277
+ fill: "fixed",
8278
+ text: true
8279
+ },
8280
+ drawRule: "Axis1",
8281
+ capturesLabelSize: true,
8282
+ params: [...ATTACK_SHAFT_PARAMS, {
8283
+ key: "labelPosition",
8284
+ label: "Label position",
8285
+ description: "Position of the CATK label along the shaft, from tail (0) to head (1)",
8286
+ type: "number",
8287
+ min: 0,
8288
+ max: 1,
8289
+ step: .01
8290
+ }],
8291
+ textAmplifiers: [{
8292
+ key: "T",
8293
+ label: "Unique designation",
8294
+ description: "Field T — the counterattack's unique designation.",
8295
+ placeholder: "1",
8296
+ maxLength: 21
8297
+ }]
8298
+ };
8299
+ /** Generates the Counterattack by Fire mission task (340700). */
8300
+ function createCounterattackByFire(coordinates, options = {}, textAmplifiers = {}) {
8301
+ const bodyCoordinates = counterattackByFireBodyCoordinates(coordinates);
8302
+ if (!bodyCoordinates) return {
8303
+ type: "FeatureCollection",
8304
+ features: []
8305
+ };
8306
+ const { geometry } = processAttackGeometry(bodyCoordinates, options);
8307
+ const fireTipCoordinate = coordinates[0];
8308
+ if (!geometry || !fireTipCoordinate) return {
8309
+ type: "FeatureCollection",
8310
+ features: []
8311
+ };
8312
+ const fireTip = project(fireTipCoordinate[0], fireTipCoordinate[1]);
8313
+ const bodyTip = geometry.ptTip;
8314
+ const fireExtension = vecSub(fireTip, bodyTip);
8315
+ const extensionLength = vecMag(fireExtension);
8316
+ if (extensionLength < 1e-6) return {
8317
+ type: "FeatureCollection",
8318
+ features: []
8319
+ };
8320
+ const along = vecNorm(fireExtension);
8321
+ const perpendicular = [-along[1], along[0]];
8322
+ const headHalfWidth = vecMag(vecSub(geometry.headRing[0], geometry.ptBase));
8323
+ const headLength = vecMag(vecSub(geometry.ptTip, geometry.ptBase));
8324
+ const bracketCenter = vecAdd(bodyTip, vecScale(fireExtension, .5));
8325
+ const bracketUpper = vecAdd(bracketCenter, vecScale(perpendicular, headHalfWidth));
8326
+ const bracketLower = vecAdd(bracketCenter, vecScale(perpendicular, -headHalfWidth));
8327
+ const flare = headHalfWidth * .55;
8328
+ const bracketOuterUpper = vecAdd(vecAdd(bracketUpper, vecScale(along, -headLength)), vecScale(perpendicular, flare));
8329
+ const bracketOuterLower = vecAdd(vecAdd(bracketLower, vecScale(along, -headLength)), vecScale(perpendicular, -flare));
8330
+ const fireHeadLength = extensionLength * COUNTERATTACK_BY_FIRE_HEAD_RATIO;
8331
+ const fireHeadBase = vecAdd(fireTip, vecScale(along, -fireHeadLength));
8332
+ const features = [
8333
+ {
8334
+ type: "Feature",
8335
+ properties: {
8336
+ part: "counterattack-body",
8337
+ style: { strokeDash: [...COUNTERATTACK_DASH] }
8338
+ },
8339
+ geometry: {
8340
+ type: "LineString",
8341
+ coordinates: supportingAttackOutline(geometry)
8342
+ }
8343
+ },
8344
+ {
8345
+ type: "Feature",
8346
+ properties: {
8347
+ part: "fire-attachment",
8348
+ style: { strokeDash: [...COUNTERATTACK_DASH] }
8349
+ },
8350
+ geometry: {
8351
+ type: "MultiLineString",
8352
+ coordinates: [[
8353
+ toPosition(bracketOuterUpper),
8354
+ toPosition(bracketUpper),
8355
+ toPosition(bracketLower),
8356
+ toPosition(bracketOuterLower)
8357
+ ], [toPosition(bracketCenter), toPosition(fireHeadBase)]]
8358
+ }
8359
+ },
8360
+ {
8361
+ type: "Feature",
8362
+ properties: {
8363
+ part: "fire-arrowhead",
8364
+ fill: true,
8365
+ style: SOLID_ACCENT_FILL
8366
+ },
8367
+ geometry: {
8368
+ type: "Polygon",
8369
+ coordinates: [createArrowHeadRing(fireTip, along, fireHeadLength, fireHeadLength * 1.15)]
8370
+ }
8371
+ }
8372
+ ];
8373
+ const { segments, totalLength } = polylineSegments(geometry.shaftCenterline);
8374
+ const frame = pointAlongPolyline(segments, totalLength, options.labelPosition ?? DEFAULT_COUNTERATTACK_BY_FIRE_OPTIONS.labelPosition);
8375
+ if (frame) {
8376
+ const designation = textAmplifiers.T?.trim();
8377
+ features.push({
8378
+ type: "Feature",
8379
+ properties: {
8380
+ part: "label",
8381
+ labelPlacement: false,
8382
+ amplifierField: "T",
8383
+ text: designation ? `CATK ${designation}` : "CATK",
8384
+ rotation: labelRotationAlong(frame.along),
8385
+ ...labelSizeProps(options)
8386
+ },
8387
+ geometry: {
8388
+ type: "Point",
8389
+ coordinates: toPosition(frame.point)
8390
+ }
8391
+ });
8392
+ }
8393
+ return {
8394
+ type: "FeatureCollection",
8395
+ features
8396
+ };
8397
+ }
8398
+ const COUNTERATTACK_BY_FIRE = defineControlMeasure({
8399
+ metadata: COUNTERATTACK_BY_FIRE_METADATA,
8400
+ generator: createCounterattackByFire,
8401
+ defaultOptions: DEFAULT_COUNTERATTACK_BY_FIRE_OPTIONS,
8402
+ rule: counterattackByFireDrawRule,
8403
+ previewSample: {
8404
+ controlPoints: [
8405
+ [1.35, 0],
8406
+ [-.2, -.1],
8407
+ [-1, -.75],
8408
+ [.45, .7]
8409
+ ],
8410
+ options: { labelSize: 120 }
8411
+ }
8412
+ });
8413
+ //#endregion
8085
8414
  //#region src/internal/field-of-fire.ts
8086
8415
  /**
8087
8416
  * Builds the open (stroked) arrowhead at the outward tip of one arm. The two
@@ -8492,6 +8821,44 @@ const DEFAULT_DELAY_OPTIONS = {
8492
8821
  labelGapRatio: .18,
8493
8822
  arcSegments: 32
8494
8823
  };
8824
+ const DELAY_PARAMS = [
8825
+ {
8826
+ key: "arrowheadLengthRatio",
8827
+ label: "Arrowhead length",
8828
+ description: "Arrowhead length as a ratio of the straight-line length",
8829
+ type: "number",
8830
+ min: .01,
8831
+ max: 1,
8832
+ step: .01
8833
+ },
8834
+ {
8835
+ key: "arrowheadWidthRatio",
8836
+ label: "Arrowhead width",
8837
+ description: "Arrowhead base width as a ratio of the straight-line length",
8838
+ type: "number",
8839
+ min: .01,
8840
+ max: 1,
8841
+ step: .01
8842
+ },
8843
+ {
8844
+ key: "labelGapRatio",
8845
+ label: "Label gap",
8846
+ description: "Size of the shaft gap holding the task label, as a ratio of the shaft length",
8847
+ type: "number",
8848
+ min: 0,
8849
+ max: 1,
8850
+ step: .01
8851
+ },
8852
+ {
8853
+ key: "arcSegments",
8854
+ label: "Arc segments",
8855
+ description: "Number of straight segments used to approximate the semicircular arc",
8856
+ type: "number",
8857
+ min: 4,
8858
+ max: 96,
8859
+ step: 1
8860
+ }
8861
+ ];
8495
8862
  const DELAY_METADATA = {
8496
8863
  id: "delay",
8497
8864
  name: "Delay",
@@ -8510,44 +8877,7 @@ const DELAY_METADATA = {
8510
8877
  },
8511
8878
  drawRule: "Line24",
8512
8879
  capturesLabelSize: true,
8513
- params: [
8514
- {
8515
- key: "arrowheadLengthRatio",
8516
- label: "Arrowhead length",
8517
- description: "Arrowhead length as a ratio of the straight-line length",
8518
- type: "number",
8519
- min: .01,
8520
- max: 1,
8521
- step: .01
8522
- },
8523
- {
8524
- key: "arrowheadWidthRatio",
8525
- label: "Arrowhead width",
8526
- description: "Arrowhead base width as a ratio of the straight-line length",
8527
- type: "number",
8528
- min: .01,
8529
- max: 1,
8530
- step: .01
8531
- },
8532
- {
8533
- key: "labelGapRatio",
8534
- label: "Label gap",
8535
- description: "Size of the shaft gap holding the 'D' label, as a ratio of the shaft length",
8536
- type: "number",
8537
- min: 0,
8538
- max: 1,
8539
- step: .01
8540
- },
8541
- {
8542
- key: "arcSegments",
8543
- label: "Arc segments",
8544
- description: "Number of straight segments used to approximate the semicircular arc",
8545
- type: "number",
8546
- min: 4,
8547
- max: 96,
8548
- step: 1
8549
- }
8550
- ]
8880
+ params: DELAY_PARAMS
8551
8881
  };
8552
8882
  function createDelaySymbol(coordinates, options = {}) {
8553
8883
  const [c1, c2, c3] = coordinates;
@@ -8608,6 +8938,26 @@ function createSemicircleArc(start, end, bulgeDir, requestedSegments) {
8608
8938
  }
8609
8939
  return points;
8610
8940
  }
8941
+ /**
8942
+ * Line24 mission tasks that share Delay's geometry and differ only in the
8943
+ * glyph shown in the shaft gap and the `part` tag on the line feature.
8944
+ */
8945
+ function createDelayVariantSymbol(coordinates, config, options = {}) {
8946
+ return {
8947
+ type: "FeatureCollection",
8948
+ features: createDelaySymbol(coordinates, options).features.map((feature) => ({
8949
+ ...feature,
8950
+ properties: feature.geometry.type === "Point" ? {
8951
+ ...feature.properties,
8952
+ part: "label",
8953
+ text: config.glyph
8954
+ } : {
8955
+ ...feature.properties,
8956
+ part: config.part
8957
+ }
8958
+ }))
8959
+ };
8960
+ }
8611
8961
  const DELAY = defineControlMeasure({
8612
8962
  metadata: DELAY_METADATA,
8613
8963
  generator: createDelaySymbol,
@@ -8615,6 +8965,204 @@ const DELAY = defineControlMeasure({
8615
8965
  rule: line24DrawRule
8616
8966
  });
8617
8967
  //#endregion
8968
+ //#region src/generators/cm34-mission-tasks/disengage.ts
8969
+ const DEFAULT_DISENGAGE_OPTIONS = DEFAULT_DELAY_OPTIONS;
8970
+ const DISENGAGE_METADATA = {
8971
+ id: "disengage",
8972
+ name: "Disengage",
8973
+ description: "Breaks contact with the enemy to enable another mission or avoid decisive engagement.",
8974
+ entity: "Mission Tasks",
8975
+ entityType: "Disengage",
8976
+ value: "344400",
8977
+ minCoordinates: 3,
8978
+ maxCoordinates: 3,
8979
+ geometry: "line",
8980
+ geometryTypes: ["MultiLineString", "Point"],
8981
+ paints: {
8982
+ stroke: true,
8983
+ fill: "none",
8984
+ text: true
8985
+ },
8986
+ drawRule: "Line24",
8987
+ capturesLabelSize: true,
8988
+ params: DELAY_PARAMS
8989
+ };
8990
+ function createDisengageSymbol(coordinates, options = {}) {
8991
+ return createDelayVariantSymbol(coordinates, {
8992
+ glyph: "DIS",
8993
+ part: "disengage"
8994
+ }, options);
8995
+ }
8996
+ const DISENGAGE = defineControlMeasure({
8997
+ metadata: DISENGAGE_METADATA,
8998
+ generator: createDisengageSymbol,
8999
+ defaultOptions: DEFAULT_DISENGAGE_OPTIONS,
9000
+ rule: line24DrawRule,
9001
+ previewSample: {
9002
+ controlPoints: [
9003
+ [-1, -.4],
9004
+ [1, -.4],
9005
+ [0, .6]
9006
+ ],
9007
+ options: { labelGapRatio: .36 }
9008
+ }
9009
+ });
9010
+ //#endregion
9011
+ //#region src/generators/cm34-mission-tasks/disrupt.ts
9012
+ const DEFAULT_DISRUPT_MISSION_TASK_OPTIONS = {
9013
+ middleArrowLengthRatio: .65,
9014
+ bottomArrowLengthRatio: .45,
9015
+ shaftLengthRatio: .25,
9016
+ arrowheadLengthRatio: .15,
9017
+ arrowheadWidthRatio: .2,
9018
+ labelGapRatio: .2
9019
+ };
9020
+ const DISRUPT_MISSION_TASK_METADATA = {
9021
+ id: "disrupt-mission-task",
9022
+ name: "Disrupt",
9023
+ description: "Upsets an enemy's formation or tempo and causes the enemy force to attack prematurely or in a piecemeal fashion.",
9024
+ entity: "Mission Tasks",
9025
+ entityType: "Disrupt",
9026
+ value: "341000",
9027
+ minCoordinates: 2,
9028
+ maxCoordinates: 3,
9029
+ geometry: "line",
9030
+ geometryTypes: ["MultiLineString", "Point"],
9031
+ paints: {
9032
+ stroke: true,
9033
+ fill: "none",
9034
+ text: true
9035
+ },
9036
+ drawRule: "Area12",
9037
+ capturesLabelSize: true,
9038
+ params: [
9039
+ {
9040
+ key: "middleArrowLengthRatio",
9041
+ label: "Middle arrow length",
9042
+ description: "Middle arrow length as a ratio of the PT.3-controlled top arrow",
9043
+ type: "number",
9044
+ min: .01,
9045
+ max: 1,
9046
+ step: .01
9047
+ },
9048
+ {
9049
+ key: "bottomArrowLengthRatio",
9050
+ label: "Bottom arrow length",
9051
+ description: "Bottom arrow length as a ratio of the PT.3-controlled top arrow",
9052
+ type: "number",
9053
+ min: .01,
9054
+ max: 1,
9055
+ step: .01
9056
+ },
9057
+ {
9058
+ key: "shaftLengthRatio",
9059
+ label: "Rear shaft length",
9060
+ description: "Middle shaft extension behind the spine, as a ratio of the top arrow",
9061
+ type: "number",
9062
+ min: 0,
9063
+ max: 1,
9064
+ step: .01
9065
+ },
9066
+ {
9067
+ key: "arrowheadLengthRatio",
9068
+ label: "Arrowhead length",
9069
+ description: "Arrowhead length as a ratio of the spine length",
9070
+ type: "number",
9071
+ min: .01,
9072
+ max: 1,
9073
+ step: .01
9074
+ },
9075
+ {
9076
+ key: "arrowheadWidthRatio",
9077
+ label: "Arrowhead width",
9078
+ description: "Arrowhead base width as a ratio of the spine length",
9079
+ type: "number",
9080
+ min: .01,
9081
+ max: 1,
9082
+ step: .01
9083
+ },
9084
+ {
9085
+ key: "labelGapRatio",
9086
+ label: "Label gap",
9087
+ description: "Size of the middle-shaft gap holding the 'D' label",
9088
+ type: "number",
9089
+ min: 0,
9090
+ max: 1,
9091
+ step: .01
9092
+ }
9093
+ ]
9094
+ };
9095
+ /** Generates the DISRUPT mission task symbol (341000). */
9096
+ function createDisruptMissionTaskSymbol(coordinates, options = {}) {
9097
+ const p1 = project(coordinates[0][0], coordinates[0][1]);
9098
+ const p2 = project(coordinates[1][0], coordinates[1][1]);
9099
+ const frame = createProjectedBaselineFrame(p1, p2, {
9100
+ origin: "p1",
9101
+ normal: "left"
9102
+ });
9103
+ if (!frame) return {
9104
+ type: "FeatureCollection",
9105
+ features: []
9106
+ };
9107
+ const c3 = coordinates[2];
9108
+ const signedTopLength = c3 ? frame.signedNormalDistance(c3) : frame.length * 1.5;
9109
+ const topLength = Math.abs(signedTopLength);
9110
+ if (topLength < 1e-6) return {
9111
+ type: "FeatureCollection",
9112
+ features: []
9113
+ };
9114
+ const arrowDirection = vecScale(frame.normal, signedTopLength < 0 ? -1 : 1);
9115
+ const middle = vecScale(vecAdd(p1, p2), .5);
9116
+ /** Scales `base` by the named ratio option, falling back to its default and clamping negatives. */
9117
+ const scaled = (base, key) => base * Math.max(0, options[key] ?? DEFAULT_DISRUPT_MISSION_TASK_OPTIONS[key]);
9118
+ const middleLength = scaled(topLength, "middleArrowLengthRatio");
9119
+ const bottomLength = scaled(topLength, "bottomArrowLengthRatio");
9120
+ const rearLength = scaled(topLength, "shaftLengthRatio");
9121
+ const headLength = scaled(frame.length, "arrowheadLengthRatio");
9122
+ const headWidth = scaled(frame.length, "arrowheadWidthRatio");
9123
+ const topTip = vecAdd(p1, vecScale(arrowDirection, topLength));
9124
+ const middleTip = vecAdd(middle, vecScale(arrowDirection, middleLength));
9125
+ const bottomTip = vecAdd(p2, vecScale(arrowDirection, bottomLength));
9126
+ const rearEnd = vecAdd(middle, vecScale(arrowDirection, -rearLength));
9127
+ const labelPoint = vecScale(vecAdd(middle, middleTip), .5);
9128
+ const labelGapRatio = clamp01(options.labelGapRatio ?? DEFAULT_DISRUPT_MISSION_TASK_OPTIONS.labelGapRatio);
9129
+ const gapHalf = Math.min(topLength * labelGapRatio / 2, middleLength / 2);
9130
+ const gapStart = vecAdd(labelPoint, vecScale(arrowDirection, -gapHalf));
9131
+ const gapEnd = vecAdd(labelPoint, vecScale(arrowDirection, gapHalf));
9132
+ const p1LonLat = unproject(p1[0], p1[1]);
9133
+ const p2LonLat = unproject(p2[0], p2[1]);
9134
+ return {
9135
+ type: "FeatureCollection",
9136
+ features: [{
9137
+ type: "Feature",
9138
+ properties: { part: "disrupt" },
9139
+ geometry: {
9140
+ type: "MultiLineString",
9141
+ coordinates: [
9142
+ [p1LonLat, p2LonLat],
9143
+ [[...p1LonLat], unproject(topTip[0], topTip[1])],
9144
+ createArrowHead$1(topTip, arrowDirection, headLength, headWidth),
9145
+ [unproject(rearEnd[0], rearEnd[1]), unproject(gapStart[0], gapStart[1])],
9146
+ [unproject(gapEnd[0], gapEnd[1]), unproject(middleTip[0], middleTip[1])],
9147
+ createArrowHead$1(middleTip, arrowDirection, headLength, headWidth),
9148
+ [[...p2LonLat], unproject(bottomTip[0], bottomTip[1])],
9149
+ createArrowHead$1(bottomTip, arrowDirection, headLength, headWidth)
9150
+ ]
9151
+ }
9152
+ }, createLabelFeature(labelPoint, "D", labelRotationAlong(arrowDirection), {
9153
+ labelSize: options.labelSize,
9154
+ labelSizePixels: options.labelSizePixels
9155
+ })]
9156
+ };
9157
+ }
9158
+ const DISRUPT_MISSION_TASK = defineControlMeasure({
9159
+ metadata: DISRUPT_MISSION_TASK_METADATA,
9160
+ generator: createDisruptMissionTaskSymbol,
9161
+ defaultOptions: DEFAULT_DISRUPT_MISSION_TASK_OPTIONS,
9162
+ rule: disruptDrawRule,
9163
+ previewSample: { controlPoints: [[0, .4], [0, -.4]] }
9164
+ });
9165
+ //#endregion
8618
9166
  //#region src/generators/cm34-mission-tasks/guard.ts
8619
9167
  const DEFAULT_GUARD_OPTIONS = DEFAULT_SECURITY_TASK_OPTIONS;
8620
9168
  const GUARD_METADATA = {
@@ -8660,27 +9208,6 @@ const GUARD = defineControlMeasure({
8660
9208
  }
8661
9209
  });
8662
9210
  //#endregion
8663
- //#region src/internal/arrowhead.ts
8664
- /**
8665
- * Builds a closed, filled arrowhead triangle ring in unprojected coordinates.
8666
- *
8667
- * `tip` is the point of the arrow, `dir` the unit direction the arrow points,
8668
- * `length` the distance from tip back to the base, and `width` the base width.
8669
- * The returned ring is `[left, tip, right, left]`.
8670
- */
8671
- function createArrowHeadRing(tip, dir, length, width) {
8672
- const perp = [-dir[1], dir[0]];
8673
- const base = vecSub(tip, vecScale(dir, length));
8674
- const left = vecAdd(base, vecScale(perp, width / 2));
8675
- const right = vecSub(base, vecScale(perp, width / 2));
8676
- return [
8677
- unproject(left[0], left[1]),
8678
- unproject(tip[0], tip[1]),
8679
- unproject(right[0], right[1]),
8680
- unproject(left[0], left[1])
8681
- ];
8682
- }
8683
- //#endregion
8684
9211
  //#region src/generators/cm27-protection-areas/disrupt.ts
8685
9212
  const DEFAULT_DISRUPT_OPTIONS = {
8686
9213
  middleArrowLengthRatio: .65,
@@ -9467,6 +9994,175 @@ const FIX = defineControlMeasure({
9467
9994
  previewSample: { controlPoints: [[1, 0], [-1, 0]] }
9468
9995
  });
9469
9996
  //#endregion
9997
+ //#region src/generators/cm34-mission-tasks/fix.ts
9998
+ const DEFAULT_FIX_MISSION_TASK_OPTIONS = {
9999
+ arrowheadLengthRatio: .11,
10000
+ arrowheadWidthRatio: .18,
10001
+ zigzagStartRatio: .18,
10002
+ zigzagEndRatio: .8,
10003
+ zigzagPeakCount: 5,
10004
+ labelPositionRatio: .09,
10005
+ labelGapRatio: .1
10006
+ };
10007
+ const FIX_MISSION_TASK_METADATA = {
10008
+ id: "fix-mission-task",
10009
+ name: "Fix",
10010
+ description: "Prevents an enemy force from moving from a specific location for a specified period of time.",
10011
+ entity: "Mission Tasks",
10012
+ entityType: "Fix",
10013
+ value: "341100",
10014
+ minCoordinates: 2,
10015
+ maxCoordinates: 2,
10016
+ geometry: "line",
10017
+ geometryTypes: ["MultiLineString", "Point"],
10018
+ paints: {
10019
+ stroke: true,
10020
+ fill: "none",
10021
+ text: true
10022
+ },
10023
+ drawRule: "Line9",
10024
+ capturesLabelSize: true,
10025
+ params: [
10026
+ {
10027
+ key: "arrowheadLengthRatio",
10028
+ label: "Arrowhead length",
10029
+ description: "Arrowhead length as a ratio of the axis length",
10030
+ type: "number",
10031
+ min: .01,
10032
+ max: .5,
10033
+ step: .01
10034
+ },
10035
+ {
10036
+ key: "arrowheadWidthRatio",
10037
+ label: "Arrowhead width",
10038
+ description: "Arrowhead base width as a ratio of the axis length",
10039
+ type: "number",
10040
+ min: .01,
10041
+ max: .5,
10042
+ step: .01
10043
+ },
10044
+ {
10045
+ key: "zigzagStartRatio",
10046
+ label: "Zigzag start",
10047
+ description: "Position where the zigzag begins, measured from PT2",
10048
+ type: "number",
10049
+ min: 0,
10050
+ max: 1,
10051
+ step: .01
10052
+ },
10053
+ {
10054
+ key: "zigzagEndRatio",
10055
+ label: "Zigzag end",
10056
+ description: "Position where the zigzag ends, measured from PT2",
10057
+ type: "number",
10058
+ min: 0,
10059
+ max: 1,
10060
+ step: .01
10061
+ },
10062
+ {
10063
+ key: "zigzagPeakCount",
10064
+ label: "Zigzag peaks",
10065
+ description: "Number of upward peaks in the zigzag body",
10066
+ type: "number",
10067
+ min: 1,
10068
+ max: 12,
10069
+ step: 1
10070
+ },
10071
+ {
10072
+ key: "labelPositionRatio",
10073
+ label: "Label position",
10074
+ description: "Position of the F label along the axis, measured from PT2",
10075
+ type: "number",
10076
+ min: 0,
10077
+ max: 1,
10078
+ step: .01
10079
+ },
10080
+ {
10081
+ key: "labelGapRatio",
10082
+ label: "Label gap",
10083
+ description: "Size of the shaft gap holding the F label",
10084
+ type: "number",
10085
+ min: 0,
10086
+ max: 1,
10087
+ step: .01
10088
+ }
10089
+ ]
10090
+ };
10091
+ /** Generates the Fix mission task symbol (341100). */
10092
+ function createFixMissionTaskSymbol(coordinates, options = {}) {
10093
+ const c1 = coordinates[0];
10094
+ const c2 = coordinates[1];
10095
+ const tip = project(c1[0], c1[1]);
10096
+ const tail = project(c2[0], c2[1]);
10097
+ const axis = vecSub(tip, tail);
10098
+ const axisLength = vecMag(axis);
10099
+ if (axisLength < 1e-6) return {
10100
+ type: "FeatureCollection",
10101
+ features: []
10102
+ };
10103
+ const along = vecNorm(axis);
10104
+ const perpendicular = [-along[1], along[0]];
10105
+ const arrowheadLength = axisLength * Math.max(0, options.arrowheadLengthRatio ?? DEFAULT_FIX_MISSION_TASK_OPTIONS.arrowheadLengthRatio);
10106
+ const arrowheadWidth = axisLength * Math.max(0, options.arrowheadWidthRatio ?? DEFAULT_FIX_MISSION_TASK_OPTIONS.arrowheadWidthRatio);
10107
+ const requestedStart = clamp01(options.zigzagStartRatio ?? DEFAULT_FIX_MISSION_TASK_OPTIONS.zigzagStartRatio);
10108
+ const requestedEnd = clamp01(options.zigzagEndRatio ?? DEFAULT_FIX_MISSION_TASK_OPTIONS.zigzagEndRatio);
10109
+ const zigzagStart = Math.min(requestedStart, requestedEnd);
10110
+ const zigzagEnd = Math.max(requestedStart, requestedEnd);
10111
+ const extremumCount = Math.round(clamp(options.zigzagPeakCount ?? DEFAULT_FIX_MISSION_TASK_OPTIONS.zigzagPeakCount, 1, 12)) * 2 + 1;
10112
+ const amplitude = arrowheadWidth / 2;
10113
+ const zigzag = [];
10114
+ for (let index = 0; index < extremumCount; index += 1) {
10115
+ const progress = extremumCount === 1 ? 0 : index / (extremumCount - 1);
10116
+ const distance = axisLength * (zigzagStart + (zigzagEnd - zigzagStart) * progress);
10117
+ const side = index % 2 === 0 ? -1 : 1;
10118
+ zigzag.push(vecAdd(vecAdd(tail, vecScale(along, distance)), vecScale(perpendicular, amplitude * side)));
10119
+ }
10120
+ const toothStep = extremumCount > 1 ? (zigzagEnd - zigzagStart) / (extremumCount - 1) : 0;
10121
+ const entryRatio = Math.max(0, zigzagStart - toothStep / 2);
10122
+ const exitRatio = Math.min(1, zigzagEnd + toothStep / 2);
10123
+ const zigzagEntry = vecAdd(tail, vecScale(along, axisLength * entryRatio));
10124
+ const zigzagExit = vecAdd(tail, vecScale(along, axisLength * exitRatio));
10125
+ const labelPosition = clamp01(options.labelPositionRatio ?? DEFAULT_FIX_MISSION_TASK_OPTIONS.labelPositionRatio);
10126
+ const labelGap = axisLength * clamp01(options.labelGapRatio ?? DEFAULT_FIX_MISSION_TASK_OPTIONS.labelGapRatio);
10127
+ const labelDistance = axisLength * labelPosition;
10128
+ const labelCenter = vecAdd(tail, vecScale(along, labelDistance));
10129
+ const gapStartDistance = Math.max(0, labelDistance - labelGap / 2);
10130
+ const gapEndDistance = Math.min(axisLength, labelDistance + labelGap / 2);
10131
+ const gapStart = vecAdd(tail, vecScale(along, gapStartDistance));
10132
+ const gapEnd = vecAdd(tail, vecScale(along, gapEndDistance));
10133
+ return {
10134
+ type: "FeatureCollection",
10135
+ features: [{
10136
+ type: "Feature",
10137
+ properties: { part: "fix-mission-task" },
10138
+ geometry: {
10139
+ type: "MultiLineString",
10140
+ coordinates: [
10141
+ [toPosition(tail), toPosition(gapStart)],
10142
+ [
10143
+ toPosition(gapEnd),
10144
+ toPosition(zigzagEntry),
10145
+ ...zigzag.map(toPosition),
10146
+ toPosition(zigzagExit),
10147
+ toPosition(tip)
10148
+ ],
10149
+ createArrowHead$1(tip, along, arrowheadLength, arrowheadWidth)
10150
+ ]
10151
+ }
10152
+ }, createLabelFeature(labelCenter, "F", labelRotationAlong(along), options)]
10153
+ };
10154
+ }
10155
+ const FIX_MISSION_TASK = defineControlMeasure({
10156
+ metadata: FIX_MISSION_TASK_METADATA,
10157
+ generator: createFixMissionTaskSymbol,
10158
+ defaultOptions: DEFAULT_FIX_MISSION_TASK_OPTIONS,
10159
+ rule: line9DrawRule,
10160
+ previewSample: {
10161
+ controlPoints: [[1, 0], [-1, 0]],
10162
+ options: { labelSize: 110 }
10163
+ }
10164
+ });
10165
+ //#endregion
9470
10166
  //#region src/generators/cm14-maneuver-lines/flot.ts
9471
10167
  const DEFAULT_RADIUS = 50;
9472
10168
  const DEFAULT_RADIUS_PIXELS = 10;
@@ -9653,7 +10349,7 @@ function createFLOT(positions, options = {}, textAmplifiers = {}) {
9653
10349
  }
9654
10350
  };
9655
10351
  const labels = [];
9656
- pushHostileEndLabels(labels, allPoints, textAmplifiers.N, options);
10352
+ pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options);
9657
10353
  return {
9658
10354
  type: "FeatureCollection",
9659
10355
  features: [feature, ...labels]
@@ -10332,17 +11028,17 @@ function createFortifiedLine(positions, options = {}, textAmplifiers = {}) {
10332
11028
  const { smooth = DEFAULT_FORTIFIED_LINE_OPTIONS.smooth, smoothResolution = DEFAULT_FORTIFIED_LINE_OPTIONS.smoothResolution } = options;
10333
11029
  const safeSize = calculateEffectiveSize(options);
10334
11030
  const projectedPoints = positions.map((p) => project(p[0], p[1]));
10335
- const allPoints = generateFortifiedPoints(smooth ? evenlySpacePath(catmullRom(projectedPoints, normalizeSmoothResolution$2(smoothResolution, DEFAULT_SMOOTH_RESOLUTION$1)), 2 * safeSize) : projectedPoints, safeSize);
11031
+ const smoothedPath = smooth ? catmullRom(projectedPoints, normalizeSmoothResolution$2(smoothResolution, DEFAULT_SMOOTH_RESOLUTION$1)) : projectedPoints;
10336
11032
  const feature = {
10337
11033
  type: "Feature",
10338
11034
  properties: {},
10339
11035
  geometry: {
10340
11036
  type: "LineString",
10341
- coordinates: allPoints.map((p) => unproject(p[0], p[1]))
11037
+ coordinates: generateFortifiedPoints(smooth ? evenlySpacePath(smoothedPath, 2 * safeSize) : smoothedPath, safeSize).map((p) => unproject(p[0], p[1]))
10342
11038
  }
10343
11039
  };
10344
11040
  const labels = [];
10345
- pushHostileEndLabels(labels, allPoints, textAmplifiers.N, options);
11041
+ pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options);
10346
11042
  return {
10347
11043
  type: "FeatureCollection",
10348
11044
  features: [feature, ...labels]
@@ -10784,6 +11480,200 @@ const ISOLATE = defineControlMeasure({
10784
11480
  rule: isolateDrawRule
10785
11481
  });
10786
11482
  //#endregion
11483
+ //#region src/generators/cm34-mission-tasks/penetrate.ts
11484
+ const DEFAULT_PENETRATE_OPTIONS = {
11485
+ arrowheadLengthRatio: .15,
11486
+ arrowheadWidthRatio: .2,
11487
+ labelGapRatio: .2
11488
+ };
11489
+ const PENETRATE_METADATA = {
11490
+ id: "penetrate",
11491
+ name: "Penetrate",
11492
+ description: "Breaks through an enemy defense and disrupts the continuity of the defensive system.",
11493
+ entity: "Mission Tasks",
11494
+ entityType: "Penetrate",
11495
+ value: "341800",
11496
+ minCoordinates: 3,
11497
+ maxCoordinates: 3,
11498
+ geometry: "line",
11499
+ geometryTypes: ["MultiLineString", "Point"],
11500
+ paints: {
11501
+ stroke: true,
11502
+ fill: "none",
11503
+ text: true
11504
+ },
11505
+ drawRule: "Area17",
11506
+ capturesLabelSize: true,
11507
+ params: [
11508
+ {
11509
+ key: "arrowheadLengthRatio",
11510
+ label: "Arrowhead length",
11511
+ description: "Arrowhead length as a ratio of the barrier length",
11512
+ type: "number",
11513
+ min: .01,
11514
+ max: 1,
11515
+ step: .01
11516
+ },
11517
+ {
11518
+ key: "arrowheadWidthRatio",
11519
+ label: "Arrowhead width",
11520
+ description: "Arrowhead base width as a ratio of the barrier length",
11521
+ type: "number",
11522
+ min: .01,
11523
+ max: 1,
11524
+ step: .01
11525
+ },
11526
+ {
11527
+ key: "labelGapRatio",
11528
+ label: "Label gap",
11529
+ description: "Size of the shaft gap holding the 'P' label",
11530
+ type: "number",
11531
+ min: 0,
11532
+ max: 1,
11533
+ step: .01
11534
+ }
11535
+ ]
11536
+ };
11537
+ /** Generates the PENETRATE mission task symbol (341800). */
11538
+ function createPenetrateSymbol(coordinates, options = {}) {
11539
+ const frame = createProjectedBaselineFrame(project(coordinates[0][0], coordinates[0][1]), project(coordinates[1][0], coordinates[1][1]), {
11540
+ origin: "midpoint",
11541
+ normal: "right"
11542
+ });
11543
+ if (!frame) return {
11544
+ type: "FeatureCollection",
11545
+ features: []
11546
+ };
11547
+ const c3 = coordinates[2];
11548
+ const signedTailDistance = c3 ? frame.signedNormalDistance(c3) : frame.length * 1.5;
11549
+ const shaftLength = Math.abs(signedTailDistance);
11550
+ if (shaftLength < 1e-6) return {
11551
+ type: "FeatureCollection",
11552
+ features: []
11553
+ };
11554
+ const tip = frame.origin;
11555
+ const tail = vecAdd(frame.origin, vecScale(frame.normal, signedTailDistance));
11556
+ const arrowDirection = vecScale(vecSub(tip, tail), 1 / shaftLength);
11557
+ const labelPoint = vecScale(vecAdd(tail, tip), .5);
11558
+ const gapHalf = shaftLength * Math.min(1, Math.max(0, options.labelGapRatio ?? DEFAULT_PENETRATE_OPTIONS.labelGapRatio)) / 2;
11559
+ const gapStart = vecAdd(labelPoint, vecScale(arrowDirection, -gapHalf));
11560
+ const gapEnd = vecAdd(labelPoint, vecScale(arrowDirection, gapHalf));
11561
+ const headLength = frame.length * Math.max(0, options.arrowheadLengthRatio ?? DEFAULT_PENETRATE_OPTIONS.arrowheadLengthRatio);
11562
+ const headWidth = frame.length * Math.max(0, options.arrowheadWidthRatio ?? DEFAULT_PENETRATE_OPTIONS.arrowheadWidthRatio);
11563
+ const lineFeature = {
11564
+ type: "Feature",
11565
+ properties: { part: "penetrate" },
11566
+ geometry: {
11567
+ type: "MultiLineString",
11568
+ coordinates: [
11569
+ [coordinates[0], coordinates[1]],
11570
+ [unproject(tail[0], tail[1]), unproject(gapStart[0], gapStart[1])],
11571
+ [unproject(gapEnd[0], gapEnd[1]), unproject(tip[0], tip[1])],
11572
+ createArrowHead$1(tip, arrowDirection, headLength, headWidth)
11573
+ ]
11574
+ }
11575
+ };
11576
+ const shaftRotation = Math.atan2(arrowDirection[1], arrowDirection[0]);
11577
+ const renderedLabelRotation = Math.PI - shaftRotation;
11578
+ return {
11579
+ type: "FeatureCollection",
11580
+ features: [lineFeature, createLabelFeature(labelPoint, "P", normalizeRadians(Math.PI - keepTextLeftToRight(renderedLabelRotation)), {
11581
+ labelSize: options.labelSize,
11582
+ labelSizePixels: options.labelSizePixels
11583
+ })]
11584
+ };
11585
+ }
11586
+ const PENETRATE = defineControlMeasure({
11587
+ metadata: PENETRATE_METADATA,
11588
+ generator: createPenetrateSymbol,
11589
+ defaultOptions: DEFAULT_PENETRATE_OPTIONS,
11590
+ rule: penetrateDrawRule,
11591
+ previewSample: { controlPoints: [
11592
+ [0, .4],
11593
+ [0, -.4],
11594
+ [-1.2, 0]
11595
+ ] }
11596
+ });
11597
+ //#endregion
11598
+ //#region src/generators/cm34-mission-tasks/rearward-passage-of-lines.ts
11599
+ const DEFAULT_REARWARD_PASSAGE_OF_LINES_OPTIONS = DEFAULT_DELAY_OPTIONS;
11600
+ const REARWARD_PASSAGE_OF_LINES_METADATA = {
11601
+ id: "rearward-passage-of-lines",
11602
+ name: "Rearward Passage of Lines",
11603
+ description: "Passes one force through another force's positions toward the rear.",
11604
+ entity: "Mission Tasks",
11605
+ entityType: "Rearward Passage of Lines",
11606
+ value: "344200",
11607
+ minCoordinates: 3,
11608
+ maxCoordinates: 3,
11609
+ geometry: "line",
11610
+ geometryTypes: ["MultiLineString", "Point"],
11611
+ paints: {
11612
+ stroke: true,
11613
+ fill: "none",
11614
+ text: true
11615
+ },
11616
+ drawRule: "Line24",
11617
+ capturesLabelSize: true,
11618
+ params: DELAY_PARAMS
11619
+ };
11620
+ function createRearwardPassageOfLinesSymbol(coordinates, options = {}) {
11621
+ return createDelayVariantSymbol(coordinates, {
11622
+ glyph: "P(R)",
11623
+ part: "rearward-passage-of-lines"
11624
+ }, options);
11625
+ }
11626
+ const REARWARD_PASSAGE_OF_LINES = defineControlMeasure({
11627
+ metadata: REARWARD_PASSAGE_OF_LINES_METADATA,
11628
+ generator: createRearwardPassageOfLinesSymbol,
11629
+ defaultOptions: DEFAULT_REARWARD_PASSAGE_OF_LINES_OPTIONS,
11630
+ rule: line24DrawRule,
11631
+ previewSample: {
11632
+ controlPoints: [
11633
+ [-1, -.4],
11634
+ [1, -.4],
11635
+ [0, .6]
11636
+ ],
11637
+ options: { labelGapRatio: .42 }
11638
+ }
11639
+ });
11640
+ //#endregion
11641
+ //#region src/generators/cm34-mission-tasks/retire.ts
11642
+ const DEFAULT_RETIRE_OPTIONS = DEFAULT_DELAY_OPTIONS;
11643
+ const RETIRE_METADATA = {
11644
+ id: "retire",
11645
+ name: "Retire/Retirement",
11646
+ description: "Moves a force out of contact away from the enemy.",
11647
+ entity: "Mission Tasks",
11648
+ entityType: "Retire/Retirement",
11649
+ value: "342000",
11650
+ minCoordinates: 3,
11651
+ maxCoordinates: 3,
11652
+ geometry: "line",
11653
+ geometryTypes: ["MultiLineString", "Point"],
11654
+ paints: {
11655
+ stroke: true,
11656
+ fill: "none",
11657
+ text: true
11658
+ },
11659
+ drawRule: "Line24",
11660
+ capturesLabelSize: true,
11661
+ params: DELAY_PARAMS
11662
+ };
11663
+ /** Generates the RETIRE/RETIREMENT mission task symbol (342000). */
11664
+ function createRetireSymbol(coordinates, options = {}) {
11665
+ return createDelayVariantSymbol(coordinates, {
11666
+ glyph: "R",
11667
+ part: "retire"
11668
+ }, options);
11669
+ }
11670
+ const RETIRE = defineControlMeasure({
11671
+ metadata: RETIRE_METADATA,
11672
+ generator: createRetireSymbol,
11673
+ defaultOptions: DEFAULT_RETIRE_OPTIONS,
11674
+ rule: line24DrawRule
11675
+ });
11676
+ //#endregion
10787
11677
  //#region src/generators/cm34-mission-tasks/screen.ts
10788
11678
  const DEFAULT_SCREEN_OPTIONS = DEFAULT_SECURITY_TASK_OPTIONS;
10789
11679
  const SCREEN_METADATA = {
@@ -10829,6 +11719,281 @@ const SCREEN = defineControlMeasure({
10829
11719
  }
10830
11720
  });
10831
11721
  //#endregion
11722
+ //#region src/generators/cm34-mission-tasks/seize.ts
11723
+ const DEFAULT_SEIZE_OPTIONS = {
11724
+ arrowheadLengthRatio: .1,
11725
+ arrowheadWidthRatio: .12,
11726
+ labelGapRatio: .13,
11727
+ curveSegments: 48,
11728
+ circleSegments: 64
11729
+ };
11730
+ const SEIZE_METADATA = {
11731
+ id: "seize",
11732
+ name: "Seize",
11733
+ description: "Takes possession of a designated area using overwhelming force and establishes control of it.",
11734
+ entity: "Mission Tasks",
11735
+ entityType: "Seize",
11736
+ value: "342300",
11737
+ minCoordinates: 4,
11738
+ maxCoordinates: 4,
11739
+ geometry: "line",
11740
+ geometryTypes: ["MultiLineString", "Point"],
11741
+ paints: {
11742
+ stroke: true,
11743
+ fill: "none",
11744
+ text: true
11745
+ },
11746
+ drawRule: "Line27",
11747
+ capturesLabelSize: true,
11748
+ params: [
11749
+ {
11750
+ key: "arrowheadLengthRatio",
11751
+ label: "Arrowhead length",
11752
+ description: "Arrowhead length as a ratio of the PT. 2-to-PT. 4 span",
11753
+ type: "number",
11754
+ min: .01,
11755
+ max: .5,
11756
+ step: .01
11757
+ },
11758
+ {
11759
+ key: "arrowheadWidthRatio",
11760
+ label: "Arrowhead width",
11761
+ description: "Arrowhead base width as a ratio of the PT. 2-to-PT. 4 span",
11762
+ type: "number",
11763
+ min: .01,
11764
+ max: .5,
11765
+ step: .01
11766
+ },
11767
+ {
11768
+ key: "labelGapRatio",
11769
+ label: "Label gap",
11770
+ description: "Size of the curve gap holding the S label",
11771
+ type: "number",
11772
+ min: 0,
11773
+ max: .5,
11774
+ step: .01
11775
+ },
11776
+ {
11777
+ key: "curveSegments",
11778
+ label: "Curve segments",
11779
+ description: "Number of straight segments used to approximate the circular arc",
11780
+ type: "number",
11781
+ min: 4,
11782
+ max: 128,
11783
+ step: 1
11784
+ },
11785
+ {
11786
+ key: "circleSegments",
11787
+ label: "Circle segments",
11788
+ description: "Number of straight segments used to approximate the assigned-unit circle",
11789
+ type: "number",
11790
+ min: 12,
11791
+ max: 128,
11792
+ step: 1
11793
+ }
11794
+ ]
11795
+ };
11796
+ function positiveAngle(angle) {
11797
+ const normalized = angle % (Math.PI * 2);
11798
+ return normalized < 0 ? normalized + Math.PI * 2 : normalized;
11799
+ }
11800
+ /** Returns the unique circular arc from start to end that passes through middle. */
11801
+ function threePointArc(start, middle, end) {
11802
+ const [ax, ay] = start;
11803
+ const [bx, by] = middle;
11804
+ const [cx, cy] = end;
11805
+ const determinant = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
11806
+ const scale = Math.max(vecMag(vecSub(middle, start)), vecMag(vecSub(end, start)), 1);
11807
+ if (Math.abs(determinant) < 1e-6 * scale * scale) return null;
11808
+ const a2 = ax * ax + ay * ay;
11809
+ const b2 = bx * bx + by * by;
11810
+ const c2 = cx * cx + cy * cy;
11811
+ const center = [(a2 * (by - cy) + b2 * (cy - ay) + c2 * (ay - by)) / determinant, (a2 * (cx - bx) + b2 * (ax - cx) + c2 * (bx - ax)) / determinant];
11812
+ const radius = vecMag(vecSub(start, center));
11813
+ if (radius < 1e-6) return null;
11814
+ const startAngle = Math.atan2(ay - center[1], ax - center[0]);
11815
+ const middleAngle = Math.atan2(by - center[1], bx - center[0]);
11816
+ const counterclockwiseSweep = positiveAngle(Math.atan2(cy - center[1], cx - center[0]) - startAngle);
11817
+ return {
11818
+ center,
11819
+ radius,
11820
+ startAngle,
11821
+ sweepAngle: positiveAngle(middleAngle - startAngle) <= counterclockwiseSweep ? counterclockwiseSweep : counterclockwiseSweep - Math.PI * 2
11822
+ };
11823
+ }
11824
+ /** Generates the Seize mission task symbol (342300). */
11825
+ function createSeizeSymbol(coordinates, options = {}) {
11826
+ const [c1, c2, c3, c4] = coordinates;
11827
+ const center = project(c1[0], c1[1]);
11828
+ const start = project(c2[0], c2[1]);
11829
+ const curvePoint = project(c3[0], c3[1]);
11830
+ const tip = project(c4[0], c4[1]);
11831
+ const radiusVector = vecSub(start, center);
11832
+ const radius = vecMag(radiusVector);
11833
+ const span = vecMag(vecSub(tip, start));
11834
+ if (radius < 1e-6 || span < 1e-6) return {
11835
+ type: "FeatureCollection",
11836
+ features: []
11837
+ };
11838
+ const curveSegments = Math.round(Math.min(128, Math.max(4, options.curveSegments ?? DEFAULT_SEIZE_OPTIONS.curveSegments)));
11839
+ const circleSegments = Math.round(Math.min(128, Math.max(12, options.circleSegments ?? DEFAULT_SEIZE_OPTIONS.circleSegments)));
11840
+ const arc = threePointArc(start, curvePoint, tip);
11841
+ let firstArcPart;
11842
+ let secondArcPart;
11843
+ let arrowDirection;
11844
+ let labelPoint;
11845
+ let tangentAtLabel;
11846
+ if (arc) {
11847
+ const gapHalf = clamp01(options.labelGapRatio ?? DEFAULT_SEIZE_OPTIONS.labelGapRatio) / 2;
11848
+ const gapStart = .5 - gapHalf;
11849
+ const gapEnd = .5 + gapHalf;
11850
+ const sampleArc = (from, to) => {
11851
+ const count = Math.max(1, Math.ceil(curveSegments * (to - from)));
11852
+ const points = [];
11853
+ for (let index = 0; index <= count; index += 1) {
11854
+ const progress = from + (to - from) * index / count;
11855
+ const angle = arc.startAngle + arc.sweepAngle * progress;
11856
+ points.push(toPosition(vecAdd(arc.center, [arc.radius * Math.cos(angle), arc.radius * Math.sin(angle)])));
11857
+ }
11858
+ return points;
11859
+ };
11860
+ firstArcPart = sampleArc(0, gapStart);
11861
+ secondArcPart = sampleArc(gapEnd, 1);
11862
+ const endAngle = arc.startAngle + arc.sweepAngle;
11863
+ const sweepSign = Math.sign(arc.sweepAngle);
11864
+ arrowDirection = [-Math.sin(endAngle) * sweepSign, Math.cos(endAngle) * sweepSign];
11865
+ const labelAngle = arc.startAngle + arc.sweepAngle / 2;
11866
+ labelPoint = vecAdd(arc.center, [arc.radius * Math.cos(labelAngle), arc.radius * Math.sin(labelAngle)]);
11867
+ tangentAtLabel = [-Math.sin(labelAngle) * sweepSign, Math.cos(labelAngle) * sweepSign];
11868
+ } else {
11869
+ labelPoint = vecScale(vecAdd(start, tip), .5);
11870
+ const gapHalf = span * clamp01(options.labelGapRatio ?? DEFAULT_SEIZE_OPTIONS.labelGapRatio) / 2;
11871
+ const direction = vecNorm(vecSub(tip, start));
11872
+ const before = vecAdd(labelPoint, vecScale(direction, -gapHalf));
11873
+ const after = vecAdd(labelPoint, vecScale(direction, gapHalf));
11874
+ firstArcPart = [toPosition(start), toPosition(before)];
11875
+ secondArcPart = [toPosition(after), toPosition(tip)];
11876
+ const terminal = vecSub(tip, curvePoint);
11877
+ arrowDirection = vecMag(terminal) >= 1e-6 ? vecNorm(terminal) : vecNorm(vecSub(tip, start));
11878
+ tangentAtLabel = direction;
11879
+ }
11880
+ const startAngle = Math.atan2(radiusVector[1], radiusVector[0]);
11881
+ const circle = [];
11882
+ for (let index = 0; index <= circleSegments; index += 1) {
11883
+ const angle = startAngle + index / circleSegments * Math.PI * 2;
11884
+ circle.push(toPosition(vecAdd(center, [radius * Math.cos(angle), radius * Math.sin(angle)])));
11885
+ }
11886
+ return {
11887
+ type: "FeatureCollection",
11888
+ features: [{
11889
+ type: "Feature",
11890
+ properties: { part: "seize" },
11891
+ geometry: {
11892
+ type: "MultiLineString",
11893
+ coordinates: [
11894
+ circle,
11895
+ firstArcPart,
11896
+ secondArcPart,
11897
+ createArrowHead$1(tip, arrowDirection, span * Math.max(0, options.arrowheadLengthRatio ?? DEFAULT_SEIZE_OPTIONS.arrowheadLengthRatio), span * Math.max(0, options.arrowheadWidthRatio ?? DEFAULT_SEIZE_OPTIONS.arrowheadWidthRatio))
11898
+ ]
11899
+ }
11900
+ }, createLabelFeature(labelPoint, "S", labelRotationAlong(tangentAtLabel), options)]
11901
+ };
11902
+ }
11903
+ const SEIZE = defineControlMeasure({
11904
+ metadata: SEIZE_METADATA,
11905
+ generator: createSeizeSymbol,
11906
+ defaultOptions: DEFAULT_SEIZE_OPTIONS,
11907
+ rule: line27DrawRule,
11908
+ previewSample: {
11909
+ controlPoints: [
11910
+ [-.8, .35],
11911
+ [-.45, .35],
11912
+ [.15, .25],
11913
+ [.85, -.55]
11914
+ ],
11915
+ options: { labelSize: 100 }
11916
+ }
11917
+ });
11918
+ //#endregion
11919
+ //#region src/generators/cm34-mission-tasks/withdraw.ts
11920
+ const DEFAULT_WITHDRAW_OPTIONS = DEFAULT_DELAY_OPTIONS;
11921
+ const WITHDRAW_METADATA = {
11922
+ id: "withdraw",
11923
+ name: "Withdraw",
11924
+ description: "Disengages a force from the enemy and moves it away from enemy contact.",
11925
+ entity: "Mission Tasks",
11926
+ entityType: "Withdraw",
11927
+ value: "342400",
11928
+ minCoordinates: 3,
11929
+ maxCoordinates: 3,
11930
+ geometry: "line",
11931
+ geometryTypes: ["MultiLineString", "Point"],
11932
+ paints: {
11933
+ stroke: true,
11934
+ fill: "none",
11935
+ text: true
11936
+ },
11937
+ drawRule: "Line24",
11938
+ capturesLabelSize: true,
11939
+ params: DELAY_PARAMS
11940
+ };
11941
+ function createWithdrawSymbol(coordinates, options = {}) {
11942
+ return createDelayVariantSymbol(coordinates, {
11943
+ glyph: "W",
11944
+ part: "withdraw"
11945
+ }, options);
11946
+ }
11947
+ const WITHDRAW = defineControlMeasure({
11948
+ metadata: WITHDRAW_METADATA,
11949
+ generator: createWithdrawSymbol,
11950
+ defaultOptions: DEFAULT_WITHDRAW_OPTIONS,
11951
+ rule: line24DrawRule
11952
+ });
11953
+ //#endregion
11954
+ //#region src/generators/cm34-mission-tasks/withdraw-under-pressure.ts
11955
+ const DEFAULT_WITHDRAW_UNDER_PRESSURE_OPTIONS = DEFAULT_DELAY_OPTIONS;
11956
+ const WITHDRAW_UNDER_PRESSURE_METADATA = {
11957
+ id: "withdraw-under-pressure",
11958
+ name: "Withdraw Under Pressure",
11959
+ description: "Disengages a force from the enemy and moves it away while under enemy pressure.",
11960
+ entity: "Mission Tasks",
11961
+ entityType: "Withdraw Under Pressure",
11962
+ value: "342500",
11963
+ minCoordinates: 3,
11964
+ maxCoordinates: 3,
11965
+ geometry: "line",
11966
+ geometryTypes: ["MultiLineString", "Point"],
11967
+ paints: {
11968
+ stroke: true,
11969
+ fill: "none",
11970
+ text: true
11971
+ },
11972
+ drawRule: "Line24",
11973
+ capturesLabelSize: true,
11974
+ params: DELAY_PARAMS
11975
+ };
11976
+ function createWithdrawUnderPressureSymbol(coordinates, options = {}) {
11977
+ return createDelayVariantSymbol(coordinates, {
11978
+ glyph: "WP",
11979
+ part: "withdraw-under-pressure"
11980
+ }, options);
11981
+ }
11982
+ const WITHDRAW_UNDER_PRESSURE = defineControlMeasure({
11983
+ metadata: WITHDRAW_UNDER_PRESSURE_METADATA,
11984
+ generator: createWithdrawUnderPressureSymbol,
11985
+ defaultOptions: DEFAULT_WITHDRAW_UNDER_PRESSURE_OPTIONS,
11986
+ rule: line24DrawRule,
11987
+ previewSample: {
11988
+ controlPoints: [
11989
+ [-1, -.4],
11990
+ [1, -.4],
11991
+ [0, .6]
11992
+ ],
11993
+ options: { labelGapRatio: .3 }
11994
+ }
11995
+ });
11996
+ //#endregion
10832
11997
  //#region src/generators/cm15-maneuver-areas/jointTacticalActionArea.ts
10833
11998
  /** Default options for the Joint tactical action area control measure. */
10834
11999
  const DEFAULT_JOINT_TACTICAL_ACTION_AREA_OPTIONS = {
@@ -12699,11 +13864,20 @@ const DEFINITIONS = {
12699
13864
  canalize: CANALIZE,
12700
13865
  clear: CLEAR,
12701
13866
  counterattack: COUNTERATTACK,
13867
+ "counterattack-by-fire": COUNTERATTACK_BY_FIRE,
12702
13868
  cover: COVER,
12703
13869
  delay: DELAY,
13870
+ disengage: DISENGAGE,
13871
+ "disrupt-mission-task": DISRUPT_MISSION_TASK,
12704
13872
  guard: GUARD,
12705
13873
  isolate: ISOLATE,
13874
+ penetrate: PENETRATE,
13875
+ "rearward-passage-of-lines": REARWARD_PASSAGE_OF_LINES,
13876
+ retire: RETIRE,
12706
13877
  screen: SCREEN,
13878
+ seize: SEIZE,
13879
+ withdraw: WITHDRAW,
13880
+ "withdraw-under-pressure": WITHDRAW_UNDER_PRESSURE,
12707
13881
  "antitank-ditch-under-construction": ANTITANK_DITCH_UNDER_CONSTRUCTION,
12708
13882
  "antitank-ditch-completed": ANTITANK_DITCH_COMPLETED,
12709
13883
  "antitank-wall": ANTITANK_WALL,
@@ -12733,6 +13907,7 @@ const DEFINITIONS = {
12733
13907
  block: BLOCK,
12734
13908
  disrupt: DISRUPT,
12735
13909
  fix: FIX,
13910
+ "fix-mission-task": FIX_MISSION_TASK,
12736
13911
  turn: TURN,
12737
13912
  "obstacle-bypass-easy": OBSTACLE_BYPASS_EASY,
12738
13913
  "obstacle-bypass-difficult": OBSTACLE_BYPASS_DIFFICULT,
@@ -13100,4 +14275,4 @@ function assertNever(value) {
13100
14275
  throw new Error(`Unhandled control measure kind: ${String(value)}`);
13101
14276
  }
13102
14277
  //#endregion
13103
- export { DEFAULT_CANALIZE_OPTIONS as $, roundToFixed as $t, DEFAULT_FORWARD_EDGE_OF_BATTLE_AREA_OPTIONS as A, line24DrawRule as At, DEFAULT_GUARD_OPTIONS as B, computeDefaultMidpointPerpendicularPoint as Bt, DEFAULT_JOINT_TACTICAL_ACTION_AREA_OPTIONS as C, resolveAmplifierPlacement as Ct, DEFAULT_FORTIFIED_LINE_OPTIONS as D, axis1DrawRule as Dt, DEFAULT_FORTIFIED_AREA_OPTIONS as E, rectangleDrawRule as Et, DEFAULT_FLOT_OPTIONS as F, attackByFireDrawRule as Ft, DEFAULT_SUPPORTING_ATTACK_OPTIONS as G, createBaselineFrame as Gt, DEFAULT_COVER_OPTIONS as H, getMidpointPerpendicularSignedDistance as Ht, DEFAULT_FIX_OPTIONS as I, point12DrawRule as It, DEFAULT_GENERIC_RECTANGLE_OPTIONS as J, haversineDistance as Jt, DEFAULT_CLEAR_OPTIONS as K, calculateMetrics as Kt, DEFAULT_FINAL_PROTECTIVE_FIRE_OPTIONS as L, centerRadiusDrawRule as Lt, DEFAULT_HOLDING_LINE_OPTIONS as M, turnDrawRule as Mt, DEFAULT_BRIDGEHEAD_LINE_OPTIONS as N, line1DrawRule as Nt, DEFAULT_BATTLE_HANDOVER_LINE_OPTIONS as O, supportByFireDrawRule as Ot, DEFAULT_PHASE_LINE_OPTIONS as P, ambushDrawRule as Pt, DEFAULT_CLASSIC_ARROW_OPTIONS as Q, getMetersPerPixel as Qt, DEFAULT_ENCIRCLEMENT_OPTIONS as R, disruptDrawRule as Rt, DEFAULT_LANDING_ZONE_OPTIONS as S, normalizeTextAmplifiers as St, DEFAULT_FRONTAL_ATTACK_OPTIONS as T, DEFAULT_AIRBORNE_ATTACK_OPTIONS as Tt, DEFAULT_TACTICAL_ARROW_OPTIONS as U, pointOnMidpointPerpendicularAxis as Ut, DEFAULT_DELAY_OPTIONS as V, createMidpointPerpendicularDrawRule as Vt, DEFAULT_COUNTERATTACK_OPTIONS as W, snapToMidpointPerpendicular as Wt, DEFAULT_GENERIC_LINE_OPTIONS as X, unproject as Xt, DEFAULT_GENERIC_POLYGON_OPTIONS as Y, project as Yt, DEFAULT_GENERIC_CIRCLE_OPTIONS as Z, EPSILON as Zt, DEFAULT_PICKUP_ZONE_OPTIONS as _, DEFAULT_DIRECTION_OF_SUPPORTING_ATTACK_OPTIONS as _t, DEFINITIONS as a, DEFAULT_LIGHT_LINE_OPTIONS as at, DEFAULT_OBSTACLE_BYPASS_DIFFICULT_OPTIONS as b, TEXT_AMPLIFIER_FIELDS as bt, getDefaultOptions as c, DEFAULT_BLOCK_OPTIONS as ct, DEFAULT_TURN_OPTIONS as d, DEFAULT_ATTACK_BY_FIRE_OPTIONS as dt, DEFAULT_BYPASS_OPTIONS as et, DEFAULT_SUPPORT_BY_FIRE_OPTIONS as f, DEFAULT_ANTITANK_WALL_OPTIONS as ft, DEFAULT_PRINCIPAL_DIRECTION_OF_FIRE_OPTIONS as g, DEFAULT_AREA_DEFENSE_OPTIONS as gt, SECONDARY_DIRECTION_OF_FIRE_DASH as h, DEFAULT_AREA_OF_OPERATIONS_OPTIONS as ht, CONTROL_MEASURE_METADATA as i, DEFAULT_ENGINEER_WORK_LINE_OPTIONS as it, DEFAULT_RELEASE_LINE_OPTIONS as j, line23DrawRule as jt, DEFAULT_HANDOVER_LINE_OPTIONS as k, line26DrawRule as kt, listControlMeasureMetadata as l, DEFAULT_BATTLE_POSITION_OPTIONS as lt, DEFAULT_SECONDARY_DIRECTION_OF_FIRE_OPTIONS as m, DEFAULT_ASSEMBLY_AREA_OPTIONS as mt, resolveStyleHints as n, DEFAULT_BLOCK_MISSION_TASK_OPTIONS as nt, getControlMeasureMetadata as o, DEFAULT_BOUNDARY_OPTIONS as ot, DEFAULT_STRONG_POINT_OPTIONS as p, DEFAULT_ANTITANK_DITCH_OPTIONS as pt, DEFAULT_GENERIC_TEXT_OPTIONS as q, computeInitialWidthPoint as qt, CONTROL_MEASURE_IDS as r, DEFAULT_GENERIC_C2_LINE_OPTIONS as rt, getControlMeasureMetadataByValue as s, DEFAULT_BLOCK_ARROW_OPTIONS as st, renderControlMeasure as t, DEFAULT_BREACH_OPTIONS as tt, DEFAULT_TURNING_MOVEMENT_OPTIONS as u, DEFAULT_ATTACK_HELICOPTER_OPTIONS as ut, DEFAULT_OBSTACLE_BYPASS_IMPOSSIBLE_OPTIONS as v, DEFAULT_DIRECTION_OF_MAIN_ATTACK_OPTIONS as vt, DEFAULT_SCREEN_OPTIONS as w, DEFAULT_AMBUSH_OPTIONS as wt, DEFAULT_MAIN_ATTACK_OPTIONS as x, canonicalTextAmplifierKey as xt, DEFAULT_OBSTACLE_BYPASS_EASY_OPTIONS as y, DEFAULT_DIRECTION_OF_ATTACK_AVIATION_OPTIONS as yt, DEFAULT_DISRUPT_OPTIONS as z, blockDrawRule as zt };
14278
+ export { DEFAULT_COUNTERATTACK_BY_FIRE_OPTIONS as $, createMidpointPerpendicularDrawRule as $t, DEFAULT_PENETRATE_OPTIONS as A, TEXT_AMPLIFIER_FIELDS as At, DEFAULT_PHASE_LINE_OPTIONS as B, line26DrawRule as Bt, DEFAULT_JOINT_TACTICAL_ACTION_AREA_OPTIONS as C, DEFAULT_ANTITANK_DITCH_OPTIONS as Ct, DEFAULT_SCREEN_OPTIONS as D, DEFAULT_DIRECTION_OF_SUPPORTING_ATTACK_OPTIONS as Dt, DEFAULT_SEIZE_OPTIONS as E, DEFAULT_AREA_DEFENSE_OPTIONS as Et, DEFAULT_HANDOVER_LINE_OPTIONS as F, DEFAULT_AIRBORNE_ATTACK_OPTIONS as Ft, DEFAULT_ENCIRCLEMENT_OPTIONS as G, ambushDrawRule as Gt, DEFAULT_FIX_MISSION_TASK_OPTIONS as H, line23DrawRule as Ht, DEFAULT_FORWARD_EDGE_OF_BATTLE_AREA_OPTIONS as I, rectangleDrawRule as It, DEFAULT_DISRUPT_MISSION_TASK_OPTIONS as J, penetrateDrawRule as Jt, DEFAULT_DISRUPT_OPTIONS as K, attackByFireDrawRule as Kt, DEFAULT_RELEASE_LINE_OPTIONS as L, axis1DrawRule as Lt, DEFAULT_FORTIFIED_AREA_OPTIONS as M, normalizeTextAmplifiers as Mt, DEFAULT_FORTIFIED_LINE_OPTIONS as N, resolveAmplifierPlacement as Nt, DEFAULT_RETIRE_OPTIONS as O, DEFAULT_DIRECTION_OF_MAIN_ATTACK_OPTIONS as Ot, DEFAULT_BATTLE_HANDOVER_LINE_OPTIONS as P, DEFAULT_AMBUSH_OPTIONS as Pt, DEFAULT_TACTICAL_ARROW_OPTIONS as Q, computeDefaultMidpointPerpendicularPoint as Qt, DEFAULT_HOLDING_LINE_OPTIONS as R, supportByFireDrawRule as Rt, DEFAULT_LANDING_ZONE_OPTIONS as S, DEFAULT_ANTITANK_WALL_OPTIONS as St, DEFAULT_WITHDRAW_OPTIONS as T, DEFAULT_AREA_OF_OPERATIONS_OPTIONS as Tt, DEFAULT_FIX_OPTIONS as U, turnDrawRule as Ut, DEFAULT_FLOT_OPTIONS as V, line24DrawRule as Vt, DEFAULT_FINAL_PROTECTIVE_FIRE_OPTIONS as W, line1DrawRule as Wt, DEFAULT_DELAY_OPTIONS as X, disruptDrawRule as Xt, DEFAULT_DISENGAGE_OPTIONS as Y, centerRadiusDrawRule as Yt, DEFAULT_COVER_OPTIONS as Z, blockDrawRule as Zt, DEFAULT_PICKUP_ZONE_OPTIONS as _, DEFAULT_BLOCK_ARROW_OPTIONS as _t, DEFINITIONS as a, computeInitialWidthPoint as an, DEFAULT_GENERIC_POLYGON_OPTIONS as at, DEFAULT_OBSTACLE_BYPASS_DIFFICULT_OPTIONS as b, DEFAULT_ATTACK_HELICOPTER_OPTIONS as bt, getDefaultOptions as c, unproject as cn, DEFAULT_CLASSIC_ARROW_OPTIONS as ct, DEFAULT_TURN_OPTIONS as d, roundToFixed as dn, DEFAULT_BREACH_OPTIONS as dt, getMidpointPerpendicularSignedDistance as en, DEFAULT_COUNTERATTACK_OPTIONS as et, DEFAULT_SUPPORT_BY_FIRE_OPTIONS as f, DEFAULT_BLOCK_MISSION_TASK_OPTIONS as ft, DEFAULT_PRINCIPAL_DIRECTION_OF_FIRE_OPTIONS as g, DEFAULT_BOUNDARY_OPTIONS as gt, SECONDARY_DIRECTION_OF_FIRE_DASH as h, DEFAULT_LIGHT_LINE_OPTIONS as ht, CONTROL_MEASURE_METADATA as i, calculateMetrics as in, DEFAULT_GENERIC_RECTANGLE_OPTIONS as it, DEFAULT_FRONTAL_ATTACK_OPTIONS as j, canonicalTextAmplifierKey as jt, DEFAULT_REARWARD_PASSAGE_OF_LINES_OPTIONS as k, DEFAULT_DIRECTION_OF_ATTACK_AVIATION_OPTIONS as kt, listControlMeasureMetadata as l, EPSILON as ln, DEFAULT_CANALIZE_OPTIONS as lt, DEFAULT_SECONDARY_DIRECTION_OF_FIRE_OPTIONS as m, DEFAULT_ENGINEER_WORK_LINE_OPTIONS as mt, resolveStyleHints as n, snapToMidpointPerpendicular as nn, DEFAULT_CLEAR_OPTIONS as nt, getControlMeasureMetadata as o, haversineDistance as on, DEFAULT_GENERIC_LINE_OPTIONS as ot, DEFAULT_STRONG_POINT_OPTIONS as p, DEFAULT_GENERIC_C2_LINE_OPTIONS as pt, DEFAULT_GUARD_OPTIONS as q, point12DrawRule as qt, CONTROL_MEASURE_IDS as r, createBaselineFrame as rn, DEFAULT_GENERIC_TEXT_OPTIONS as rt, getControlMeasureMetadataByValue as s, project as sn, DEFAULT_GENERIC_CIRCLE_OPTIONS as st, renderControlMeasure as t, pointOnMidpointPerpendicularAxis as tn, DEFAULT_SUPPORTING_ATTACK_OPTIONS as tt, DEFAULT_TURNING_MOVEMENT_OPTIONS as u, getMetersPerPixel as un, DEFAULT_BYPASS_OPTIONS as ut, DEFAULT_OBSTACLE_BYPASS_IMPOSSIBLE_OPTIONS as v, DEFAULT_BLOCK_OPTIONS as vt, DEFAULT_WITHDRAW_UNDER_PRESSURE_OPTIONS as w, DEFAULT_ASSEMBLY_AREA_OPTIONS as wt, DEFAULT_MAIN_ATTACK_OPTIONS as x, DEFAULT_ATTACK_BY_FIRE_OPTIONS as xt, DEFAULT_OBSTACLE_BYPASS_EASY_OPTIONS as y, DEFAULT_BATTLE_POSITION_OPTIONS as yt, DEFAULT_BRIDGEHEAD_LINE_OPTIONS as z, line27DrawRule as zt };