@orbat-mapper/control-measures 0.7.0 → 0.9.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.
@@ -586,20 +586,34 @@ const area1DrawRule = {
586
586
  }
587
587
  };
588
588
  //#endregion
589
- //#region src/draw-rules/area11.ts
590
- const blockDrawRule = createMidpointPerpendicularDrawRule({ id: "area11:block" });
591
- //#endregion
592
- //#region src/draw-rules/area12.ts
589
+ //#region src/draw-rules/area27.ts
593
590
  /**
594
- * Area12 anchor draw rule for Disrupt.
591
+ * Area27 anchor draw rule for Mobile Defense.
595
592
  *
596
- * P1/P2 are the free spine endpoints. P3 is the longest-arrow tip, constrained
597
- * to the perpendicular axis through P1 (not the baseline midpoint).
593
+ * PT. 1 is the arrow tip and PT. 2 is the end of the straight-line portion.
594
+ * PT. 3 fixes the diameter and side of the 180-degree arc, constrained to the
595
+ * perpendicular axis through PT. 2 so the arc stays perpendicular to the
596
+ * action line when any of the three handles is edited.
598
597
  */
599
- const disruptDrawRule = createMidpointPerpendicularDrawRule({
600
- id: "area12:disrupt",
601
- origin: "p1",
602
- normal: "left"
598
+ const area27DrawRule = createMidpointPerpendicularDrawRule({
599
+ id: "area27:mobile-defense",
600
+ minimumUserPoints: 3,
601
+ minimumPreviewPoints: 2,
602
+ origin: "p2",
603
+ normal: "left",
604
+ defaultDistanceRatio: .6
605
+ });
606
+ //#endregion
607
+ //#region src/draw-rules/area5.ts
608
+ /**
609
+ * Area5 — PT. 1/PT. 2 define the open diameter and PT. 3 defines the arrow end.
610
+ * The arrow stays on the perpendicular bisector of the diameter.
611
+ */
612
+ const containDrawRule = createMidpointPerpendicularDrawRule({
613
+ id: "area5:contain",
614
+ minimumUserPoints: 3,
615
+ defaultDistanceRatio: -1.5,
616
+ minimumPreviewPoints: 2
603
617
  });
604
618
  //#endregion
605
619
  //#region src/draw-rules/area15.ts
@@ -630,6 +644,34 @@ const centerRadiusDrawRule = {
630
644
  /** Backward-compatible doctrinal name for Area15's shared center-radius rule. */
631
645
  const isolateDrawRule = centerRadiusDrawRule;
632
646
  //#endregion
647
+ //#region src/draw-rules/area6.ts
648
+ /**
649
+ * Area6 — center + start point, mechanically identical to Area15. It re-labels
650
+ * the shared behaviour rather than aliasing it because a definition's `rule.id`
651
+ * must carry its `metadata.drawRule` as a prefix (see the registry invariant in
652
+ * `__tests__/metadata-registry.spec.ts`, ADR-0013).
653
+ */
654
+ const retainDrawRule = {
655
+ ...centerRadiusDrawRule,
656
+ id: "area6:center-radius"
657
+ };
658
+ //#endregion
659
+ //#region src/draw-rules/area11.ts
660
+ const blockDrawRule = createMidpointPerpendicularDrawRule({ id: "area11:block" });
661
+ //#endregion
662
+ //#region src/draw-rules/area12.ts
663
+ /**
664
+ * Area12 anchor draw rule for Disrupt.
665
+ *
666
+ * P1/P2 are the free spine endpoints. P3 is the longest-arrow tip, constrained
667
+ * to the perpendicular axis through P1 (not the baseline midpoint).
668
+ */
669
+ const disruptDrawRule = createMidpointPerpendicularDrawRule({
670
+ id: "area12:disrupt",
671
+ origin: "p1",
672
+ normal: "left"
673
+ });
674
+ //#endregion
633
675
  //#region src/draw-rules/area17.ts
634
676
  /**
635
677
  * Area17 anchor draw rule for Penetrate.
@@ -2674,6 +2716,13 @@ function pushEndLabels(out, verts, textForEnd, options = {}) {
2674
2716
  * measurement is available at generator time.
2675
2717
  */
2676
2718
  const LABEL_CHAR_ASPECT_RATIO = .6;
2719
+ /**
2720
+ * Half a single glyph's width, as a ratio of its height — the base boundary
2721
+ * gap a measure reserves for one intrinsic capital letter (Retain's R,
2722
+ * Contain's C). Wider than half `LABEL_CHAR_ASPECT_RATIO` because these
2723
+ * glyphs are stamped alone rather than averaged over a word.
2724
+ */
2725
+ const LABEL_HALF_GLYPH_RATIO = .45;
2677
2726
  /** Estimated rendered width of `text` given its resolved label height in meters. */
2678
2727
  function estimatedTextWidth(text, textHeightMeters) {
2679
2728
  return text.length * LABEL_CHAR_ASPECT_RATIO * textHeightMeters;
@@ -2792,7 +2841,7 @@ function pushEndLabelsAbove(out, verts, textForEnd, options = {}) {
2792
2841
  * as a complete FeatureCollection.
2793
2842
  */
2794
2843
  function fixedLabelLineFeatures(positions, options, textForEnd, hostileText, context) {
2795
- const { part, placement = "beyond", phaseLineName, includePrefix = true, smooth, smoothResolution, ...sizeOptions } = options;
2844
+ const { part, placement = "beyond", phaseLineName, includePrefix = true, lineStyle, smooth, smoothResolution, ...sizeOptions } = options;
2796
2845
  const verts = smoothLineVerts(positions.map((p) => project(p[0], p[1])), {
2797
2846
  smooth,
2798
2847
  smoothResolution
@@ -2800,7 +2849,10 @@ function fixedLabelLineFeatures(positions, options, textForEnd, hostileText, con
2800
2849
  const lineCoordinates = smooth ? verts.map((v) => unproject(v[0], v[1])) : positions;
2801
2850
  const features = [{
2802
2851
  type: "Feature",
2803
- properties: { part },
2852
+ properties: lineStyle === void 0 ? { part } : {
2853
+ part,
2854
+ style: lineStyle
2855
+ },
2804
2856
  geometry: {
2805
2857
  type: "LineString",
2806
2858
  coordinates: lineCoordinates
@@ -4205,13 +4257,13 @@ const AREA_DEFENSE_METADATA = {
4205
4257
  }
4206
4258
  ]
4207
4259
  };
4208
- function pointOnCircle(center, radius, angle) {
4260
+ function pointOnCircle$1(center, radius, angle) {
4209
4261
  return vecAdd(center, [radius * Math.cos(angle), radius * Math.sin(angle)]);
4210
4262
  }
4211
- function sampleArc$1(center, radius, start, end) {
4263
+ function sampleArc$2(center, radius, start, end) {
4212
4264
  const span = end - start;
4213
4265
  const segments = Math.max(8, Math.round(48 * Math.abs(span) / Math.PI));
4214
- return Array.from({ length: segments + 1 }, (_, index) => pointOnCircle(center, radius, start + span * index / segments));
4266
+ return Array.from({ length: segments + 1 }, (_, index) => pointOnCircle$1(center, radius, start + span * index / segments));
4215
4267
  }
4216
4268
  function openArrowhead(tip, direction, length) {
4217
4269
  const base = vecSub(tip, [direction[0] * length, direction[1] * length]);
@@ -4242,8 +4294,8 @@ function createAreaDefense(coordinates, options = {}) {
4242
4294
  const upperEnd = upperTipAngle;
4243
4295
  const lowerStart = labelCenter - labelHalf;
4244
4296
  const lowerEnd = upperTipAngle + arrowHalf * 2 - Math.PI * 2;
4245
- const upperArc = sampleArc$1(center, radius, upperStart, upperEnd);
4246
- const lowerArc = sampleArc$1(center, radius, lowerStart, lowerEnd);
4297
+ const upperArc = sampleArc$2(center, radius, upperStart, upperEnd);
4298
+ const lowerArc = sampleArc$2(center, radius, lowerStart, lowerEnd);
4247
4299
  const arrowLength = radius * Math.max(0, resolved.arrowheadLengthRatio);
4248
4300
  const upperDirection = [-Math.sin(upperEnd), Math.cos(upperEnd)];
4249
4301
  const lowerDirection = [Math.sin(lowerEnd), -Math.cos(lowerEnd)];
@@ -4268,17 +4320,17 @@ function createAreaDefense(coordinates, options = {}) {
4268
4320
  const barbPolygons = [];
4269
4321
  for (const arc of barbArcs) for (let ordinal = 1; ordinal <= arc.count; ordinal++) {
4270
4322
  const angle = arc.start + (arc.end - arc.start) * ordinal / (arc.count + 1);
4271
- const left = pointOnCircle(center, radius, angle - barbHalfAngle);
4272
- const right = pointOnCircle(center, radius, angle + barbHalfAngle);
4323
+ const left = pointOnCircle$1(center, radius, angle - barbHalfAngle);
4324
+ const right = pointOnCircle$1(center, radius, angle + barbHalfAngle);
4273
4325
  const ring = [
4274
4326
  left,
4275
- pointOnCircle(center, radius + barbLength, angle),
4327
+ pointOnCircle$1(center, radius + barbLength, angle),
4276
4328
  right,
4277
4329
  left
4278
4330
  ].map((point) => unproject(point[0], point[1]));
4279
4331
  barbPolygons.push([ring]);
4280
4332
  }
4281
- const labelPoint = pointOnCircle(center, radius, labelCenter);
4333
+ const labelPoint = pointOnCircle$1(center, radius, labelCenter);
4282
4334
  return {
4283
4335
  type: "FeatureCollection",
4284
4336
  features: [
@@ -4330,6 +4382,273 @@ const AREA_DEFENSE = defineControlMeasure({
4330
4382
  }
4331
4383
  });
4332
4384
  //#endregion
4385
+ //#region src/generators/cm15-maneuver-areas/engagementArea.ts
4386
+ /** Default options for the Engagement area control measure. */
4387
+ const DEFAULT_ENGAGEMENT_AREA_OPTIONS = {
4388
+ smooth: false,
4389
+ smoothResolution: 16
4390
+ };
4391
+ const ENGAGEMENT_AREA_METADATA = {
4392
+ id: "engagement-area",
4393
+ name: "Engagement area",
4394
+ description: "An area where the commander intends to contain and destroy an enemy force with the massed effects of all available weapons and supporting systems.",
4395
+ entity: "Maneuver Areas",
4396
+ entityType: "Engagement Area",
4397
+ value: "151300",
4398
+ minCoordinates: 3,
4399
+ geometry: "area",
4400
+ geometryTypes: [
4401
+ "Polygon",
4402
+ "MultiLineString",
4403
+ "Point"
4404
+ ],
4405
+ paints: {
4406
+ stroke: true,
4407
+ fill: "none",
4408
+ text: true
4409
+ },
4410
+ drawRule: "Area1",
4411
+ capturesLabelSize: true,
4412
+ params: LABELED_AREA_PARAMS,
4413
+ textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
4414
+ };
4415
+ /**
4416
+ * Creates an Engagement area control measure: a closed area through
4417
+ * `positions` labeled at its center with the doctrinal `EA` designation plus
4418
+ * an optional Field T unique designation and Field N ENY marker.
4419
+ */
4420
+ function createEngagementArea(positions, options = {}, textAmplifiers = {}, context = {}) {
4421
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "EA"), context.amplifierPlacements, context);
4422
+ }
4423
+ const ENGAGEMENT_AREA = defineControlMeasure({
4424
+ metadata: ENGAGEMENT_AREA_METADATA,
4425
+ generator: createEngagementArea,
4426
+ defaultOptions: DEFAULT_ENGAGEMENT_AREA_OPTIONS,
4427
+ rule: area1DrawRule,
4428
+ previewSample: {
4429
+ controlPoints: [
4430
+ [-1, -.5],
4431
+ [1, -.5],
4432
+ [1, .5],
4433
+ [-1, .5]
4434
+ ],
4435
+ textAmplifiers: { T: "ALPHA" },
4436
+ options: { labelSize: 96 }
4437
+ }
4438
+ });
4439
+ //#endregion
4440
+ //#region src/internal/arrowhead.ts
4441
+ /**
4442
+ * Builds a closed, filled arrowhead triangle ring in unprojected coordinates.
4443
+ *
4444
+ * `tip` is the point of the arrow, `dir` the unit direction the arrow points,
4445
+ * `length` the distance from tip back to the base, and `width` the base width.
4446
+ * The returned ring is `[left, tip, right, left]`.
4447
+ */
4448
+ function createArrowHeadRing(tip, dir, length, width) {
4449
+ const perp = [-dir[1], dir[0]];
4450
+ const base = vecSub(tip, vecScale(dir, length));
4451
+ const left = vecAdd(base, vecScale(perp, width / 2));
4452
+ const right = vecSub(base, vecScale(perp, width / 2));
4453
+ return [
4454
+ unproject(left[0], left[1]),
4455
+ unproject(tip[0], tip[1]),
4456
+ unproject(right[0], right[1]),
4457
+ unproject(left[0], left[1])
4458
+ ];
4459
+ }
4460
+ //#endregion
4461
+ //#region src/internal/projected-arc.ts
4462
+ /** The arc point at `angle`, in projected meters. */
4463
+ function arcPointAt(center, startRadius, bulgeRadius, angle) {
4464
+ return vecAdd(center, vecAdd(vecScale(startRadius, Math.cos(angle)), vecScale(bulgeRadius, Math.sin(angle))));
4465
+ }
4466
+ /**
4467
+ * Samples the arc from `start` to `end` radians as unprojected positions.
4468
+ * `resolution` is the segment count a full half-turn would use, so the density
4469
+ * of a partial arc stays proportional to how much of the circle it covers.
4470
+ */
4471
+ function sampleProjectedArc(center, startRadius, bulgeRadius, start, end, resolution) {
4472
+ const segments = Math.max(2, Math.ceil(resolution * Math.abs(end - start) / Math.PI));
4473
+ return Array.from({ length: segments + 1 }, (_, index) => {
4474
+ const point = arcPointAt(center, startRadius, bulgeRadius, start + (end - start) * index / segments);
4475
+ return unproject(point[0], point[1]);
4476
+ });
4477
+ }
4478
+ //#endregion
4479
+ //#region src/generators/cm15-maneuver-areas/mobile-defense.ts
4480
+ const DEFAULT_MOBILE_DEFENSE_OPTIONS = { labelPadding: 0 };
4481
+ const MOBILE_DEFENSE_METADATA = {
4482
+ id: "mobile-defense",
4483
+ name: "Mobile Defense",
4484
+ description: "A defensive maneuver that uses terrain, fires, and maneuver to defeat an attacking enemy force.",
4485
+ entity: "Maneuver Areas",
4486
+ entityType: "Mobile Defense",
4487
+ value: "152800",
4488
+ minCoordinates: 3,
4489
+ maxCoordinates: 3,
4490
+ geometry: "area",
4491
+ geometryTypes: [
4492
+ "MultiLineString",
4493
+ "MultiPolygon",
4494
+ "Point"
4495
+ ],
4496
+ paints: {
4497
+ stroke: true,
4498
+ fill: "fixed",
4499
+ text: true
4500
+ },
4501
+ drawRule: "Area27",
4502
+ capturesLabelSize: true,
4503
+ params: [{
4504
+ key: "labelPadding",
4505
+ presentationTier: "advanced",
4506
+ label: "Label padding",
4507
+ description: "Extra clearance around MD, as a ratio of label height",
4508
+ type: "number",
4509
+ min: 0,
4510
+ max: 2,
4511
+ step: .05
4512
+ }]
4513
+ };
4514
+ const ARC_RESOLUTION = 48;
4515
+ const STRAIGHT_ARROW_LENGTH_RATIO = .12;
4516
+ const STRAIGHT_ARROW_WIDTH_RATIO = .12;
4517
+ const TOP_STRAIGHT_BARB_POSITION_RATIO = .25;
4518
+ const FIRST_BARB_POSITION_RATIO = .29;
4519
+ const FIRST_BARB_CLEARANCE_RATIO = .01;
4520
+ const BARB_WIDTH_RATIO = .45;
4521
+ const BARB_LENGTH_RATIO = .42;
4522
+ const ARC_BARB_BASE_SEGMENTS = 8;
4523
+ function createBarb(center, along, outward, width, length) {
4524
+ const left = vecAdd(center, vecScale(along, width / 2));
4525
+ const right = vecSub(center, vecScale(along, width / 2));
4526
+ const tip = vecAdd(center, vecScale(outward, length));
4527
+ return [
4528
+ toPosition(left),
4529
+ toPosition(tip),
4530
+ toPosition(right),
4531
+ toPosition(left)
4532
+ ];
4533
+ }
4534
+ function createArcBarb(center, startRadius, bulgeRadius, angle, halfWidthAngle, length) {
4535
+ const point = arcPointAt(center, startRadius, bulgeRadius, angle);
4536
+ const outward = vecNorm(vecSub(point, center));
4537
+ const baseStart = arcPointAt(center, startRadius, bulgeRadius, angle - halfWidthAngle);
4538
+ const baseEnd = arcPointAt(center, startRadius, bulgeRadius, angle + halfWidthAngle);
4539
+ const sagitta = vecMag(startRadius) * (1 - Math.cos(halfWidthAngle));
4540
+ const tip = vecAdd(point, vecScale(outward, Math.max(0, length - sagitta)));
4541
+ const curvedBase = Array.from({ length: 9 }, (_, index) => toPosition(arcPointAt(center, startRadius, bulgeRadius, angle - halfWidthAngle + 2 * halfWidthAngle * index / ARC_BARB_BASE_SEGMENTS)));
4542
+ return [
4543
+ toPosition(baseStart),
4544
+ toPosition(tip),
4545
+ toPosition(baseEnd),
4546
+ ...curvedBase.slice(1, -1).reverse(),
4547
+ toPosition(baseStart)
4548
+ ];
4549
+ }
4550
+ function createMobileDefense(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
4551
+ const p1 = project(coordinates[0][0], coordinates[0][1]);
4552
+ const p2 = project(coordinates[1][0], coordinates[1][1]);
4553
+ const p3 = project(coordinates[2][0], coordinates[2][1]);
4554
+ const lineVector = vecSub(p2, p1);
4555
+ const diameterVector = vecSub(p3, p2);
4556
+ const lineLength = vecMag(lineVector);
4557
+ const diameter = vecMag(diameterVector);
4558
+ if (lineLength < 1e-6 || diameter < 1e-6) return {
4559
+ type: "FeatureCollection",
4560
+ features: []
4561
+ };
4562
+ const lineDirection = vecNorm(lineVector);
4563
+ const lineNormal = [-lineDirection[1], lineDirection[0]];
4564
+ const center = vecAdd(p2, vecScale(diameterVector, .5));
4565
+ const startRadius = vecScale(diameterVector, .5);
4566
+ const bulgeRadius = [startRadius[1], -startRadius[0]];
4567
+ const arrowLength = lineLength * STRAIGHT_ARROW_LENGTH_RATIO;
4568
+ const arrowWidth = lineLength * STRAIGHT_ARROW_WIDTH_RATIO;
4569
+ const topStart = vecAdd(vecAdd(p1, diameterVector), vecScale(lineDirection, arrowLength));
4570
+ const labelPoint = arcPointAt(center, startRadius, bulgeRadius, Math.PI / 2);
4571
+ const metrics = resolveTextMetrics("MD", options, context);
4572
+ const padding = Math.max(0, options.labelPadding ?? DEFAULT_MOBILE_DEFENSE_OPTIONS.labelPadding);
4573
+ const halfGap = Math.min(Math.PI * diameter / 4 - EPSILON, metrics.height / 2 + metrics.height * padding);
4574
+ const gapAngle = Math.max(0, halfGap / (diameter / 2));
4575
+ const upperArc = sampleProjectedArc(center, startRadius, bulgeRadius, 0, Math.PI / 2 - gapAngle, ARC_RESOLUTION);
4576
+ const lowerArc = sampleProjectedArc(center, startRadius, bulgeRadius, Math.PI / 2 + gapAngle, Math.PI, ARC_RESOLUTION);
4577
+ const boundary = [
4578
+ [toPosition(topStart), toPosition(p3)],
4579
+ [toPosition(p1), toPosition(p2)],
4580
+ upperArc,
4581
+ lowerArc,
4582
+ createArrowHeadRing(p1, vecScale(lineDirection, -1), arrowLength, arrowWidth).slice(0, -1)
4583
+ ];
4584
+ const firstBarbClearance = lineLength * FIRST_BARB_CLEARANCE_RATIO;
4585
+ const availableStraightLength = lineLength - arrowLength - firstBarbClearance;
4586
+ const barbScale = Math.min(diameter, availableStraightLength / BARB_WIDTH_RATIO);
4587
+ const barbWidth = barbScale * BARB_WIDTH_RATIO;
4588
+ const barbLength = barbScale * BARB_LENGTH_RATIO;
4589
+ const firstBarbDistance = Math.min(lineLength - barbWidth / 2, Math.max(lineLength * FIRST_BARB_POSITION_RATIO, arrowLength + barbWidth / 2 + firstBarbClearance));
4590
+ const arcBarbHalfWidthAngle = Math.asin(Math.min(1, barbWidth / diameter));
4591
+ const barbs = [
4592
+ [createBarb(vecAdd(topStart, vecScale(vecSub(p3, topStart), TOP_STRAIGHT_BARB_POSITION_RATIO)), lineDirection, lineNormal, barbWidth, barbLength)],
4593
+ [createBarb(vecAdd(p1, vecScale(lineDirection, firstBarbDistance)), lineDirection, vecScale(lineNormal, -1), barbWidth, barbLength)],
4594
+ [createArcBarb(center, startRadius, bulgeRadius, arcBarbHalfWidthAngle, arcBarbHalfWidthAngle, barbLength)],
4595
+ [createArcBarb(center, startRadius, bulgeRadius, Math.PI - arcBarbHalfWidthAngle, arcBarbHalfWidthAngle, barbLength)]
4596
+ ];
4597
+ return {
4598
+ type: "FeatureCollection",
4599
+ features: [
4600
+ {
4601
+ type: "Feature",
4602
+ properties: { part: "mobile-defense" },
4603
+ geometry: {
4604
+ type: "MultiLineString",
4605
+ coordinates: boundary
4606
+ }
4607
+ },
4608
+ {
4609
+ type: "Feature",
4610
+ properties: {
4611
+ part: "barbs",
4612
+ fill: true,
4613
+ style: SOLID_ACCENT_FILL
4614
+ },
4615
+ geometry: {
4616
+ type: "MultiPolygon",
4617
+ coordinates: barbs
4618
+ }
4619
+ },
4620
+ {
4621
+ type: "Feature",
4622
+ properties: {
4623
+ part: "label",
4624
+ labelPlacement: false,
4625
+ text: "MD",
4626
+ rotation: labelRotationAlong(lineDirection),
4627
+ ...labelSizeProps(options)
4628
+ },
4629
+ geometry: {
4630
+ type: "Point",
4631
+ coordinates: toPosition(labelPoint)
4632
+ }
4633
+ }
4634
+ ]
4635
+ };
4636
+ }
4637
+ const MOBILE_DEFENSE = defineControlMeasure({
4638
+ metadata: MOBILE_DEFENSE_METADATA,
4639
+ generator: createMobileDefense,
4640
+ defaultOptions: DEFAULT_MOBILE_DEFENSE_OPTIONS,
4641
+ rule: area27DrawRule,
4642
+ previewSample: {
4643
+ controlPoints: [
4644
+ [-1, -.25],
4645
+ [.65, -.25],
4646
+ [.65, .35]
4647
+ ],
4648
+ options: { labelSize: 90 }
4649
+ }
4650
+ });
4651
+ //#endregion
4333
4652
  //#region src/generators/cm12-command-control-areas/area-of-operations.ts
4334
4653
  /** Default options for the Area of operations control measure. */
4335
4654
  const DEFAULT_AREA_OF_OPERATIONS_OPTIONS = {
@@ -5653,33 +5972,36 @@ const BATTLE_POSITION_METADATA = {
5653
5972
  textAmplifiers: AREA_UNIT_DESIGNATION_HOSTILE_AMPLIFIERS
5654
5973
  };
5655
5974
  /**
5656
- * Creates a Battle Position control measure: a closed area through `positions`
5657
- * carrying a single echelon (Field B) glyph straddling its boundary — anchored
5658
- * to the bottom edge by default, slidable via `echelonPosition`.
5975
+ * Shared body of Battle Position and its dashed sibling, Battle Position
5976
+ * Prepared (P) but not Occupied: the closed (optionally smoothed) ring, the
5977
+ * Field B echelon glyph straddling the boundary with its masking gap, the
5978
+ * centered Field T designation and Field N "ENY" markers, and the gapped
5979
+ * boundary that merges every mask (ADR-0023). The two measures differ only in
5980
+ * the `namePrefix` put in front of Field T and the intrinsic `boundaryStyle`
5981
+ * the boundary feature carries, so both are parameters here — mirroring
5982
+ * `echelon-labeled-area.ts`'s role for Base Camp / Guerrilla Base.
5659
5983
  *
5660
- * @param positions - Area anchor points (≥3); the input contract is enforced at
5661
- * the render seam (ADR-0014).
5662
- * @param options - {@link BattlePositionOptions}.
5663
- * @param textAmplifiers - Field T (designation, centered in the area) plus an
5664
- * optional Field N "ENY" hostile marker at the west/east edges (no H/W rows —
5665
- * this measure's template pairs the designation with the Field B glyph, not
5666
- * the plain L-15 boundary template).
5984
+ * Strong Point deliberately keeps its own copy: its perimeter tics are new
5985
+ * geometry, not a parameter swap.
5667
5986
  */
5668
- function createBattlePosition(positions, options = {}, textAmplifiers = {}, context = {}) {
5987
+ function battlePositionAreaFeatures(positions, options, textAmplifiers, context, variant = {}) {
5669
5988
  const { echelon = DEFAULT_BATTLE_POSITION_OPTIONS.echelon, echelonSize = DEFAULT_BATTLE_POSITION_OPTIONS.echelonSize, echelonSizePixels, echelonPadding = DEFAULT_BATTLE_POSITION_OPTIONS.echelonPadding, echelonPosition = DEFAULT_BATTLE_POSITION_OPTIONS.echelonPosition, echelonAnchor, metersPerPixel, smooth = DEFAULT_BATTLE_POSITION_OPTIONS.smooth, smoothResolution = DEFAULT_BATTLE_POSITION_OPTIONS.smoothResolution } = options;
5670
5989
  const h = resolveEchelonHeight(echelonSize, echelonSizePixels, metersPerPixel);
5671
5990
  const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$6);
5672
5991
  const { strokes, fills, gaps } = placeEchelon(buildAreaPerimeter(verts), h, echelon, echelonPadding, echelonPosition, echelonAnchor);
5673
5992
  const labelFeatures = [];
5674
5993
  const enyGaps = pushAreaLabels(labelFeatures, verts, {
5675
- name: textAmplifiers.T,
5994
+ name: variant.namePrefix ? composeAreaName(variant.namePrefix, textAmplifiers.T) : textAmplifiers.T,
5676
5995
  hostile: textAmplifiers.N
5677
5996
  }, options, context.amplifierPlacements, void 0, context);
5678
5997
  const boundaryCoords = buildGappedLine(verts, [...gaps, ...enyGaps]);
5679
5998
  const features = [];
5680
5999
  if (boundaryCoords.length > 0) features.push({
5681
6000
  type: "Feature",
5682
- properties: { part: "boundary" },
6001
+ properties: variant.boundaryStyle ? {
6002
+ part: "boundary",
6003
+ style: variant.boundaryStyle
6004
+ } : { part: "boundary" },
5683
6005
  geometry: {
5684
6006
  type: "MultiLineString",
5685
6007
  coordinates: boundaryCoords
@@ -5712,6 +6034,22 @@ function createBattlePosition(positions, options = {}, textAmplifiers = {}, cont
5712
6034
  };
5713
6035
  }
5714
6036
  /**
6037
+ * Creates a Battle Position control measure: a closed area through `positions`
6038
+ * carrying a single echelon (Field B) glyph straddling its boundary — anchored
6039
+ * to the bottom edge by default, slidable via `echelonPosition`.
6040
+ *
6041
+ * @param positions - Area anchor points (≥3); the input contract is enforced at
6042
+ * the render seam (ADR-0014).
6043
+ * @param options - {@link BattlePositionOptions}.
6044
+ * @param textAmplifiers - Field T (designation, centered in the area) plus an
6045
+ * optional Field N "ENY" hostile marker at the west/east edges (no H/W rows —
6046
+ * this measure's template pairs the designation with the Field B glyph, not
6047
+ * the plain L-15 boundary template).
6048
+ */
6049
+ function createBattlePosition(positions, options = {}, textAmplifiers = {}, context = {}) {
6050
+ return battlePositionAreaFeatures(positions, options, textAmplifiers, context);
6051
+ }
6052
+ /**
5715
6053
  * `freezeOrientation` hook (ADR-0023) shared by Battle Position and Strong
5716
6054
  * Point: pins the echelon anchor to its current bottom-edge fraction so a
5717
6055
  * subsequent rigid rotation of `controlPoints` (e.g. the transform box's
@@ -5749,32 +6087,276 @@ const BATTLE_POSITION = defineControlMeasure({
5749
6087
  }
5750
6088
  });
5751
6089
  //#endregion
5752
- //#region src/generators/cm27-protection-areas/block.ts
5753
- const DEFAULT_STEM_RATIO$1 = 1.5;
5754
- const BLOCK_METADATA = {
5755
- id: "block",
5756
- name: "Block",
5757
- description: "Integrates fires and obstacles to stop an attacker along an avenue of approach or prevent it from passing through an engagement area. Establishes a limit of enemy advance.",
5758
- entity: "Protection Areas",
5759
- entityType: "Obstacle Effects",
5760
- entitySubtype: "Block",
5761
- value: "270501",
5762
- minCoordinates: 2,
5763
- geometry: "line",
5764
- geometryTypes: ["LineString"],
6090
+ //#region src/generators/cm15-maneuver-areas/battlePositionPrepared.ts
6091
+ /** Intrinsic dash pattern of the prepared-but-not-occupied boundary. */
6092
+ const BATTLE_POSITION_PREPARED_DASH = [6, 4];
6093
+ /** Default options for the Battle Position Prepared control measure. */
6094
+ const DEFAULT_BATTLE_POSITION_PREPARED_OPTIONS = { ...DEFAULT_BATTLE_POSITION_OPTIONS };
6095
+ const BATTLE_POSITION_PREPARED_METADATA = {
6096
+ id: "battle-position-prepared",
6097
+ name: "Battle Position Prepared (P) but not Occupied",
6098
+ description: "A battle position that has been prepared for occupation but is not yet occupied, drawn with a dashed boundary and a (P)-prefixed designation.",
6099
+ entity: "Maneuver Areas",
6100
+ entityType: "Battle Position",
6101
+ entitySubtype: "Battle Position Prepared (P) but not Occupied",
6102
+ value: "151202",
6103
+ minCoordinates: 3,
6104
+ geometry: "area",
6105
+ geometryTypes: [
6106
+ "MultiLineString",
6107
+ "MultiPolygon",
6108
+ "Point"
6109
+ ],
5765
6110
  paints: {
5766
6111
  stroke: true,
5767
- fill: "none",
5768
- text: false
6112
+ fill: "fixed",
6113
+ text: true
5769
6114
  },
5770
- drawRule: "Area11"
6115
+ drawRule: "Area1",
6116
+ capturesLabelSize: true,
6117
+ params: ECHELON_AREA_PARAMS,
6118
+ textAmplifiers: AREA_UNIT_DESIGNATION_HOSTILE_AMPLIFIERS
5771
6119
  };
5772
- const DEFAULT_BLOCK_OPTIONS = {};
5773
6120
  /**
5774
- * Generates a GeoJSON FeatureCollection for a "Block" tactical symbol (T-shape).
6121
+ * Creates a Battle Position Prepared (P) but not Occupied control measure: a
6122
+ * Battle Position whose boundary carries an intrinsic dash and whose centered
6123
+ * designation is prefixed with `(P)`. Delegates to
6124
+ * {@link battlePositionAreaFeatures}, the shared Battle Position body.
5775
6125
  *
5776
- * @param coordinates - An array of GeoJSON Positions (Lon/Lat).
5777
- * Expects exactly 3 points:
6126
+ * @param positions - Area anchor points (≥3); the input contract is enforced at
6127
+ * the render seam (ADR-0014).
6128
+ * @param options - {@link BattlePositionPreparedOptions}.
6129
+ * @param textAmplifiers - Field T (designation, centered in the area and
6130
+ * prefixed with `(P)`) plus an optional Field N "ENY" hostile marker at the
6131
+ * west/east edges.
6132
+ */
6133
+ function createBattlePositionPrepared(positions, options = {}, textAmplifiers = {}, context = {}) {
6134
+ return battlePositionAreaFeatures(positions, options, textAmplifiers, context, {
6135
+ namePrefix: "(P)",
6136
+ boundaryStyle: { strokeDash: [...BATTLE_POSITION_PREPARED_DASH] }
6137
+ });
6138
+ }
6139
+ const BATTLE_POSITION_PREPARED = defineControlMeasure({
6140
+ metadata: BATTLE_POSITION_PREPARED_METADATA,
6141
+ generator: createBattlePositionPrepared,
6142
+ defaultOptions: DEFAULT_BATTLE_POSITION_PREPARED_OPTIONS,
6143
+ rule: area1DrawRule,
6144
+ freezeOrientation: freezeEchelonAnchor,
6145
+ previewSample: {
6146
+ controlPoints: [
6147
+ [-1, -.5],
6148
+ [1, -.5],
6149
+ [1, .5],
6150
+ [-1, .5]
6151
+ ],
6152
+ textAmplifiers: { T: "MARS" },
6153
+ options: {
6154
+ echelonSize: 200,
6155
+ smooth: true,
6156
+ labelSize: 96
6157
+ }
6158
+ }
6159
+ });
6160
+ //#endregion
6161
+ //#region src/generators/cm15-maneuver-areas/contain.ts
6162
+ const DEFAULT_CONTAIN_OPTIONS = {
6163
+ labelPadding: .2,
6164
+ resolution: 32
6165
+ };
6166
+ const CONTAIN_METADATA = {
6167
+ id: "contain",
6168
+ name: "Contain",
6169
+ description: "Stops, holds, or surrounds enemy forces and prevents them from withdrawing or using them elsewhere.",
6170
+ entity: "Maneuver Areas",
6171
+ entityType: "Battle Position",
6172
+ entitySubtype: "Contain",
6173
+ value: "151204",
6174
+ minCoordinates: 3,
6175
+ maxCoordinates: 3,
6176
+ geometry: "area",
6177
+ geometryTypes: ["MultiLineString", "Point"],
6178
+ paints: {
6179
+ stroke: true,
6180
+ fill: "none",
6181
+ text: true
6182
+ },
6183
+ drawRule: "Area5",
6184
+ capturesLabelSize: true,
6185
+ textAmplifiers: [AREA_HOSTILE_AMPLIFIER],
6186
+ params: [{
6187
+ key: "labelPadding",
6188
+ presentationTier: "advanced",
6189
+ label: "Label padding",
6190
+ description: "Extra clearance around C and ENY, as a ratio of label height",
6191
+ type: "number",
6192
+ min: 0,
6193
+ max: 2,
6194
+ step: .05
6195
+ }, {
6196
+ key: "resolution",
6197
+ presentationTier: "advanced",
6198
+ label: "Resolution",
6199
+ description: "Number of segments used to approximate the semicircle",
6200
+ type: "number",
6201
+ min: 8,
6202
+ max: 128,
6203
+ step: 1
6204
+ }]
6205
+ };
6206
+ /** Where the ENY label sits along the arrow, as a ratio of its full length. */
6207
+ const HOSTILE_LABEL_POSITION_RATIO = .55;
6208
+ /** Creates the Contain semicircle, inward tics, center-pointing arrow, and C label. */
6209
+ function createContain(coordinates, options = {}, textAmplifiers = {}, context = {}) {
6210
+ const p1 = project(coordinates[0][0], coordinates[0][1]);
6211
+ const p2 = project(coordinates[1][0], coordinates[1][1]);
6212
+ const p3 = project(coordinates[2][0], coordinates[2][1]);
6213
+ const diameterLength = vecMag(vecSub(p2, p1));
6214
+ if (diameterLength < 1e-6) return {
6215
+ type: "FeatureCollection",
6216
+ features: []
6217
+ };
6218
+ const center = [(p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2];
6219
+ const arrowVector = vecSub(p3, center);
6220
+ const arrowLength = vecMag(arrowVector);
6221
+ if (arrowLength < 1e-6) return {
6222
+ type: "FeatureCollection",
6223
+ features: []
6224
+ };
6225
+ const radius = diameterLength / 2;
6226
+ const openingDirection = vecNorm(arrowVector);
6227
+ const labelRotation = labelRotationAlong(openingDirection);
6228
+ const startRadius = vecSub(p1, center);
6229
+ const bulgeRadius = vecScale(openingDirection, -radius);
6230
+ const h = Math.min(radius * .45, Math.max(EPSILON, resolveLabelOffsetMeters(options, 1, radius * .12)));
6231
+ const labelPadding = Math.max(0, options.labelPadding ?? DEFAULT_CONTAIN_OPTIONS.labelPadding);
6232
+ const resolution = Math.round(Math.min(128, Math.max(8, options.resolution ?? DEFAULT_CONTAIN_OPTIONS.resolution)));
6233
+ const hostileText = textAmplifiers.N ?? "ENY";
6234
+ const labelHalfAngle = Math.min(Math.PI / 3, h * (LABEL_HALF_GLYPH_RATIO + labelPadding) / radius);
6235
+ const labelAngle = Math.PI / 2;
6236
+ const boundary = [sampleProjectedArc(center, startRadius, bulgeRadius, 0, labelAngle - labelHalfAngle, resolution), sampleProjectedArc(center, startRadius, bulgeRadius, labelAngle + labelHalfAngle, Math.PI, resolution)];
6237
+ const tics = [];
6238
+ const spacingAngle = h / radius;
6239
+ for (let angle = spacingAngle / 2; angle < Math.PI; angle += spacingAngle) {
6240
+ if (Math.abs(angle - labelAngle) <= labelHalfAngle) continue;
6241
+ const outer = arcPointAt(center, startRadius, bulgeRadius, angle);
6242
+ const inner = vecAdd(outer, vecScale(vecNorm(vecSub(center, outer)), h));
6243
+ tics.push([unproject(outer[0], outer[1]), unproject(inner[0], inner[1])]);
6244
+ }
6245
+ const arrowhead = createArrowHeadRing(center, vecScale(openingDirection, -1), h, h).slice(0, 3);
6246
+ const arrow = [];
6247
+ const hostilePoint = vecAdd(center, vecScale(arrowVector, HOSTILE_LABEL_POSITION_RATIO));
6248
+ if (hostileText) {
6249
+ const metrics = resolveTextMetrics(hostileText, options, context);
6250
+ const maximumHalfGap = arrowLength * Math.min(HOSTILE_LABEL_POSITION_RATIO, 1 - HOSTILE_LABEL_POSITION_RATIO);
6251
+ const halfGap = Math.min(maximumHalfGap, metrics.width / 2 + labelPadding * metrics.height);
6252
+ const farGapEdge = vecAdd(hostilePoint, vecScale(openingDirection, halfGap));
6253
+ const nearGapEdge = vecAdd(hostilePoint, vecScale(openingDirection, -halfGap));
6254
+ arrow.push([unproject(p3[0], p3[1]), unproject(farGapEdge[0], farGapEdge[1])], [unproject(nearGapEdge[0], nearGapEdge[1]), unproject(center[0], center[1])]);
6255
+ } else arrow.push([unproject(p3[0], p3[1]), unproject(center[0], center[1])]);
6256
+ arrow.push(arrowhead);
6257
+ const labelPoint = arcPointAt(center, startRadius, bulgeRadius, labelAngle);
6258
+ const sizeProps = labelSizeProps(options);
6259
+ const features = [
6260
+ {
6261
+ type: "Feature",
6262
+ properties: { part: "boundary" },
6263
+ geometry: {
6264
+ type: "MultiLineString",
6265
+ coordinates: boundary
6266
+ }
6267
+ },
6268
+ {
6269
+ type: "Feature",
6270
+ properties: { part: "tics" },
6271
+ geometry: {
6272
+ type: "MultiLineString",
6273
+ coordinates: tics
6274
+ }
6275
+ },
6276
+ {
6277
+ type: "Feature",
6278
+ properties: { part: "arrow" },
6279
+ geometry: {
6280
+ type: "MultiLineString",
6281
+ coordinates: arrow
6282
+ }
6283
+ },
6284
+ {
6285
+ type: "Feature",
6286
+ properties: {
6287
+ part: "label",
6288
+ labelPlacement: false,
6289
+ text: "C",
6290
+ rotation: labelRotation,
6291
+ ...sizeProps
6292
+ },
6293
+ geometry: {
6294
+ type: "Point",
6295
+ coordinates: unproject(labelPoint[0], labelPoint[1])
6296
+ }
6297
+ }
6298
+ ];
6299
+ if (hostileText) features.push({
6300
+ type: "Feature",
6301
+ properties: {
6302
+ part: "label",
6303
+ labelPlacement: false,
6304
+ amplifierField: "N",
6305
+ text: hostileText,
6306
+ rotation: labelRotation,
6307
+ ...sizeProps
6308
+ },
6309
+ geometry: {
6310
+ type: "Point",
6311
+ coordinates: unproject(hostilePoint[0], hostilePoint[1])
6312
+ }
6313
+ });
6314
+ return {
6315
+ type: "FeatureCollection",
6316
+ features
6317
+ };
6318
+ }
6319
+ const CONTAIN = defineControlMeasure({
6320
+ metadata: CONTAIN_METADATA,
6321
+ generator: createContain,
6322
+ defaultOptions: DEFAULT_CONTAIN_OPTIONS,
6323
+ rule: containDrawRule,
6324
+ previewSample: {
6325
+ controlPoints: [
6326
+ [0, .7],
6327
+ [0, -.7],
6328
+ [1.5, 0]
6329
+ ],
6330
+ options: { labelSize: 90 }
6331
+ }
6332
+ });
6333
+ //#endregion
6334
+ //#region src/generators/cm27-protection-areas/block.ts
6335
+ const DEFAULT_STEM_RATIO$1 = 1.5;
6336
+ const BLOCK_METADATA = {
6337
+ id: "block",
6338
+ name: "Block",
6339
+ description: "Integrates fires and obstacles to stop an attacker along an avenue of approach or prevent it from passing through an engagement area. Establishes a limit of enemy advance.",
6340
+ entity: "Protection Areas",
6341
+ entityType: "Obstacle Effects",
6342
+ entitySubtype: "Block",
6343
+ value: "270501",
6344
+ minCoordinates: 2,
6345
+ geometry: "line",
6346
+ geometryTypes: ["LineString"],
6347
+ paints: {
6348
+ stroke: true,
6349
+ fill: "none",
6350
+ text: false
6351
+ },
6352
+ drawRule: "Area11"
6353
+ };
6354
+ const DEFAULT_BLOCK_OPTIONS = {};
6355
+ /**
6356
+ * Generates a GeoJSON FeatureCollection for a "Block" tactical symbol (T-shape).
6357
+ *
6358
+ * @param coordinates - An array of GeoJSON Positions (Lon/Lat).
6359
+ * Expects exactly 3 points:
5778
6360
  * - PT1 & PT2: Endpoints of the vertical line.
5779
6361
  * - PT3: Defines the endpoint of the horizontal line (stem).
5780
6362
  * @param options - Configuration options.
@@ -7965,7 +8547,7 @@ function clockwiseSweepDegrees(leftAzimuth, rightAzimuth, collapseToleranceDegre
7965
8547
  return sweep === 0 && rawSweep !== 0 ? 360 : sweep;
7966
8548
  }
7967
8549
  /** Samples an arc of `sweepDegrees` at `radius`, starting from `leftAzimuth`. */
7968
- function sampleArc(anchor, radius, leftAzimuth, sweepDegrees, resolution) {
8550
+ function sampleArc$1(anchor, radius, leftAzimuth, sweepDegrees, resolution) {
7969
8551
  const segmentCount = Math.max(1, Math.ceil(sweepDegrees / 360 * resolution));
7970
8552
  return Array.from({ length: segmentCount + 1 }, (_, index) => {
7971
8553
  return destinationPoint(anchor, radius, (leftAzimuth + index / segmentCount * sweepDegrees) * Math.PI / 180);
@@ -7973,8 +8555,8 @@ function sampleArc(anchor, radius, leftAzimuth, sweepDegrees, resolution) {
7973
8555
  }
7974
8556
  /** Closed ring tracing the outer arc, back along the inner arc, and closed. */
7975
8557
  function annularSectorRing(anchor, innerRadius, outerRadius, leftAzimuth, sweepDegrees, resolution) {
7976
- const outerArc = sampleArc(anchor, outerRadius, leftAzimuth, sweepDegrees, resolution);
7977
- const innerArc = sampleArc(anchor, innerRadius, leftAzimuth, sweepDegrees, resolution).reverse();
8558
+ const outerArc = sampleArc$1(anchor, outerRadius, leftAzimuth, sweepDegrees, resolution);
8559
+ const innerArc = sampleArc$1(anchor, innerRadius, leftAzimuth, sweepDegrees, resolution).reverse();
7978
8560
  return [
7979
8561
  ...outerArc,
7980
8562
  ...innerArc,
@@ -8680,27 +9262,6 @@ const COUNTERATTACK = defineControlMeasure({
8680
9262
  }
8681
9263
  });
8682
9264
  //#endregion
8683
- //#region src/internal/arrowhead.ts
8684
- /**
8685
- * Builds a closed, filled arrowhead triangle ring in unprojected coordinates.
8686
- *
8687
- * `tip` is the point of the arrow, `dir` the unit direction the arrow points,
8688
- * `length` the distance from tip back to the base, and `width` the base width.
8689
- * The returned ring is `[left, tip, right, left]`.
8690
- */
8691
- function createArrowHeadRing(tip, dir, length, width) {
8692
- const perp = [-dir[1], dir[0]];
8693
- const base = vecSub(tip, vecScale(dir, length));
8694
- const left = vecAdd(base, vecScale(perp, width / 2));
8695
- const right = vecSub(base, vecScale(perp, width / 2));
8696
- return [
8697
- unproject(left[0], left[1]),
8698
- unproject(tip[0], tip[1]),
8699
- unproject(right[0], right[1]),
8700
- unproject(left[0], left[1])
8701
- ];
8702
- }
8703
- //#endregion
8704
9265
  //#region src/generators/cm34-mission-tasks/counterattack-by-fire.ts
8705
9266
  const DEFAULT_COUNTERATTACK_BY_FIRE_OPTIONS = { ...DEFAULT_COUNTERATTACK_OPTIONS };
8706
9267
  /** Length of the solid fire arrowhead relative to the recessed P1 extension. */
@@ -11005,6 +11566,155 @@ const BRIDGEHEAD_LINE = defineControlMeasure({
11005
11566
  }
11006
11567
  });
11007
11568
  //#endregion
11569
+ //#region src/generators/cm14-maneuver-lines/params.ts
11570
+ /** Phase-line naming controls shared by the fixed-label Line1 maneuver lines. */
11571
+ function phaseLineNameParams(placeholder) {
11572
+ return [
11573
+ {
11574
+ key: "phaseLineName",
11575
+ presentationTier: "primary",
11576
+ semanticRole: "doctrinal",
11577
+ label: "Phase line name",
11578
+ description: "Name the line as a phase line — rendered \"PL <name>\" beyond each end of the line.",
11579
+ type: "text",
11580
+ placeholder,
11581
+ maxLength: 21,
11582
+ field: "T"
11583
+ },
11584
+ {
11585
+ key: "includePrefix",
11586
+ label: "Show PL prefix",
11587
+ description: "Prefix the phase line name with \"PL\" (or show the bare name).",
11588
+ type: "boolean",
11589
+ visibleWhen: (opts) => Boolean(String(opts.phaseLineName ?? "").trim())
11590
+ },
11591
+ LINE_LABEL_PADDING_PARAM
11592
+ ];
11593
+ }
11594
+ //#endregion
11595
+ //#region src/generators/cm14-maneuver-lines/final-coordination-line.ts
11596
+ /** Default options for the Final coordination line control measure. */
11597
+ const DEFAULT_FINAL_COORDINATION_LINE_OPTIONS = {
11598
+ includePrefix: true,
11599
+ smooth: false,
11600
+ smoothResolution: 12
11601
+ };
11602
+ const FINAL_COORDINATION_LINE_METADATA = {
11603
+ id: "final-coordination-line",
11604
+ name: "Final coordination line",
11605
+ description: "A line close to the enemy position used to coordinate the lifting or shifting of supporting fires with the final deployment of maneuver elements.",
11606
+ entity: "Maneuver Lines",
11607
+ entityType: "Final Coordination Line",
11608
+ value: "140700",
11609
+ minCoordinates: 2,
11610
+ geometry: "line",
11611
+ geometryTypes: ["LineString", "Point"],
11612
+ paints: {
11613
+ stroke: true,
11614
+ fill: "none",
11615
+ text: true
11616
+ },
11617
+ drawRule: "Line1",
11618
+ capturesLabelSize: true,
11619
+ params: [...SMOOTH_LINE_PARAMS, ...phaseLineNameParams("OPAL")],
11620
+ textAmplifiers: [HOSTILE_LINE_AMPLIFIER]
11621
+ };
11622
+ /** Creates a Final coordination line labeled "FCL" above both ends. */
11623
+ function createFinalCoordinationLine(positions, options = {}, textAmplifiers = {}, context = {}) {
11624
+ return fixedLabelLineFeatures(positions, {
11625
+ part: "final-coordination-line",
11626
+ placement: "above",
11627
+ ...options
11628
+ }, "FCL", textAmplifiers.N, context);
11629
+ }
11630
+ const FINAL_COORDINATION_LINE = defineControlMeasure({
11631
+ metadata: FINAL_COORDINATION_LINE_METADATA,
11632
+ generator: createFinalCoordinationLine,
11633
+ defaultOptions: DEFAULT_FINAL_COORDINATION_LINE_OPTIONS,
11634
+ rule: line1DrawRule,
11635
+ previewSample: {
11636
+ controlPoints: [[-1, 0], [1, 0]],
11637
+ options: { labelSize: 96 }
11638
+ }
11639
+ });
11640
+ //#endregion
11641
+ //#region src/generators/cm14-maneuver-lines/airhead-line.ts
11642
+ /** Clearance between the boundary's southernmost point and the caption, as a
11643
+ * ratio of the caption's height. */
11644
+ const CAPTION_CLEARANCE_RATIO = .9;
11645
+ /** Default options for the Airhead line control measure. */
11646
+ const DEFAULT_AIRHEAD_LINE_OPTIONS = {
11647
+ smooth: false,
11648
+ smoothResolution: 16
11649
+ };
11650
+ const AIRHEAD_LINE_METADATA = {
11651
+ id: "airhead-line",
11652
+ name: "Airhead line",
11653
+ description: "A line denoting the limits of the objective area for an airborne assault.",
11654
+ entity: "Maneuver Lines",
11655
+ entityType: "Airhead Line",
11656
+ value: "141300",
11657
+ minCoordinates: 3,
11658
+ geometry: "area",
11659
+ geometryTypes: [
11660
+ "Polygon",
11661
+ "MultiLineString",
11662
+ "Point"
11663
+ ],
11664
+ paints: {
11665
+ stroke: true,
11666
+ fill: "none",
11667
+ text: true
11668
+ },
11669
+ drawRule: "Area1",
11670
+ capturesLabelSize: true,
11671
+ params: LABELED_AREA_PARAMS,
11672
+ textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
11673
+ };
11674
+ /** Creates an Airhead line: a closed boundary captioned "AIRHEAD LINE" below. */
11675
+ function createAirheadLine(positions, options = {}, textAmplifiers = {}, context = {}) {
11676
+ const { smooth = false, smoothResolution = 16, ...labelOptions } = options;
11677
+ const verts = buildClosedRing(positions, smooth, smoothResolution, 16);
11678
+ const labelFeatures = [];
11679
+ const gaps = pushAreaLabels(labelFeatures, verts, composeAreaLabelTexts(textAmplifiers, ""), labelOptions, context.amplifierPlacements, void 0, context);
11680
+ const [cx] = ringCentroid(verts);
11681
+ const minY = Math.min(...verts.map((vertex) => vertex[1]));
11682
+ const clearance = resolveLabelOffsetMeters(labelOptions, CAPTION_CLEARANCE_RATIO + Math.max(0, labelOptions.labelPadding ?? 0));
11683
+ labelFeatures.push({
11684
+ type: "Feature",
11685
+ properties: {
11686
+ part: "label",
11687
+ labelPlacementKey: "label:airhead-line",
11688
+ text: "AIRHEAD LINE",
11689
+ rotation: HORIZONTAL_LABEL_ROTATION,
11690
+ ...labelSizeProps(labelOptions)
11691
+ },
11692
+ geometry: {
11693
+ type: "Point",
11694
+ coordinates: unproject(cx, minY - clearance)
11695
+ }
11696
+ });
11697
+ return {
11698
+ type: "FeatureCollection",
11699
+ features: [boundaryFeature(verts, gaps), ...labelFeatures]
11700
+ };
11701
+ }
11702
+ const AIRHEAD_LINE = defineControlMeasure({
11703
+ metadata: AIRHEAD_LINE_METADATA,
11704
+ generator: createAirheadLine,
11705
+ defaultOptions: DEFAULT_AIRHEAD_LINE_OPTIONS,
11706
+ rule: area1DrawRule,
11707
+ previewSample: {
11708
+ controlPoints: [
11709
+ [-1, -.4],
11710
+ [1, -.4],
11711
+ [1, .6],
11712
+ [-1, .6]
11713
+ ],
11714
+ options: { labelSize: 96 }
11715
+ }
11716
+ });
11717
+ //#endregion
11008
11718
  //#region src/generators/cm14-maneuver-lines/holding-line.ts
11009
11719
  /** Default options for the Holding line control measure. */
11010
11720
  const DEFAULT_HOLDING_LINE_OPTIONS = {
@@ -11394,6 +12104,193 @@ const BATTLE_HANDOVER_LINE = defineControlMeasure({
11394
12104
  }
11395
12105
  });
11396
12106
  //#endregion
12107
+ //#region src/generators/cm14-maneuver-lines/limit-of-advance.ts
12108
+ /** Default options for the Limit of advance control measure. */
12109
+ const DEFAULT_LIMIT_OF_ADVANCE_OPTIONS = {
12110
+ includePrefix: true,
12111
+ smooth: false,
12112
+ smoothResolution: 12
12113
+ };
12114
+ const LIMIT_OF_ADVANCE_METADATA = {
12115
+ id: "limit-of-advance",
12116
+ name: "Limit of advance",
12117
+ description: "An easily recognized terrain feature beyond which attacking elements will not advance.",
12118
+ entity: "Maneuver Lines",
12119
+ entityType: "Limit of Advance",
12120
+ value: "140900",
12121
+ minCoordinates: 2,
12122
+ geometry: "line",
12123
+ geometryTypes: ["LineString", "Point"],
12124
+ paints: {
12125
+ stroke: true,
12126
+ fill: "none",
12127
+ text: true
12128
+ },
12129
+ drawRule: "Line1",
12130
+ capturesLabelSize: true,
12131
+ params: [...SMOOTH_LINE_PARAMS, ...phaseLineNameParams("RUBY")],
12132
+ textAmplifiers: [HOSTILE_LINE_AMPLIFIER]
12133
+ };
12134
+ /** Creates a Limit of advance line labeled "LOA" above both ends. */
12135
+ function createLimitOfAdvance(positions, options = {}, textAmplifiers = {}, context = {}) {
12136
+ return fixedLabelLineFeatures(positions, {
12137
+ part: "limit-of-advance",
12138
+ placement: "above",
12139
+ ...options
12140
+ }, "LOA", textAmplifiers.N, context);
12141
+ }
12142
+ const LIMIT_OF_ADVANCE = defineControlMeasure({
12143
+ metadata: LIMIT_OF_ADVANCE_METADATA,
12144
+ generator: createLimitOfAdvance,
12145
+ defaultOptions: DEFAULT_LIMIT_OF_ADVANCE_OPTIONS,
12146
+ rule: line1DrawRule,
12147
+ previewSample: {
12148
+ controlPoints: [[-1, 0], [1, 0]],
12149
+ options: { labelSize: 96 }
12150
+ }
12151
+ });
12152
+ //#endregion
12153
+ //#region src/generators/cm14-maneuver-lines/line-of-departure.ts
12154
+ /** Default options for the Line of departure control measure. */
12155
+ const DEFAULT_LINE_OF_DEPARTURE_OPTIONS = {
12156
+ includePrefix: true,
12157
+ smooth: false,
12158
+ smoothResolution: 12
12159
+ };
12160
+ const LINE_OF_DEPARTURE_METADATA = {
12161
+ id: "line-of-departure",
12162
+ name: "Line of departure",
12163
+ description: "A line designated to coordinate the departure of attacking elements.",
12164
+ entity: "Maneuver Lines",
12165
+ entityType: "Line of Departure",
12166
+ value: "141000",
12167
+ minCoordinates: 2,
12168
+ geometry: "line",
12169
+ geometryTypes: ["LineString", "Point"],
12170
+ paints: {
12171
+ stroke: true,
12172
+ fill: "none",
12173
+ text: true
12174
+ },
12175
+ drawRule: "Line1",
12176
+ capturesLabelSize: true,
12177
+ params: [...SMOOTH_LINE_PARAMS, ...phaseLineNameParams("JADE")],
12178
+ textAmplifiers: [HOSTILE_LINE_AMPLIFIER]
12179
+ };
12180
+ /** Creates a Line of departure labeled "LD" above both ends. */
12181
+ function createLineOfDeparture(positions, options = {}, textAmplifiers = {}, context = {}) {
12182
+ return fixedLabelLineFeatures(positions, {
12183
+ part: "line-of-departure",
12184
+ placement: "above",
12185
+ ...options
12186
+ }, "LD", textAmplifiers.N, context);
12187
+ }
12188
+ const LINE_OF_DEPARTURE = defineControlMeasure({
12189
+ metadata: LINE_OF_DEPARTURE_METADATA,
12190
+ generator: createLineOfDeparture,
12191
+ defaultOptions: DEFAULT_LINE_OF_DEPARTURE_OPTIONS,
12192
+ rule: line1DrawRule,
12193
+ previewSample: {
12194
+ controlPoints: [[-1, 0], [1, 0]],
12195
+ options: { labelSize: 96 }
12196
+ }
12197
+ });
12198
+ //#endregion
12199
+ //#region src/generators/cm14-maneuver-lines/line-of-departure-contact.ts
12200
+ /** Default options for the Line of departure / line of contact control measure. */
12201
+ const DEFAULT_LINE_OF_DEPARTURE_CONTACT_OPTIONS = {
12202
+ includePrefix: true,
12203
+ smooth: false,
12204
+ smoothResolution: 12
12205
+ };
12206
+ const LINE_OF_DEPARTURE_CONTACT_METADATA = {
12207
+ id: "line-of-departure-contact",
12208
+ name: "Line of departure / line of contact",
12209
+ description: "The designation of forward friendly positions as the line of departure when opposing forces are in contact.",
12210
+ entity: "Maneuver Lines",
12211
+ entityType: "Line of Departure / Line of Contact",
12212
+ value: "141100",
12213
+ minCoordinates: 2,
12214
+ geometry: "line",
12215
+ geometryTypes: ["LineString", "Point"],
12216
+ paints: {
12217
+ stroke: true,
12218
+ fill: "none",
12219
+ text: true
12220
+ },
12221
+ drawRule: "Line1",
12222
+ capturesLabelSize: true,
12223
+ params: [...SMOOTH_LINE_PARAMS, ...phaseLineNameParams("AMBER")],
12224
+ textAmplifiers: [HOSTILE_LINE_AMPLIFIER]
12225
+ };
12226
+ /** Creates a Line of departure / line of contact labeled "LD/LC" above both ends. */
12227
+ function createLineOfDepartureContact(positions, options = {}, textAmplifiers = {}, context = {}) {
12228
+ return fixedLabelLineFeatures(positions, {
12229
+ part: "line-of-departure-contact",
12230
+ placement: "above",
12231
+ ...options
12232
+ }, "LD/LC", textAmplifiers.N, context);
12233
+ }
12234
+ const LINE_OF_DEPARTURE_CONTACT = defineControlMeasure({
12235
+ metadata: LINE_OF_DEPARTURE_CONTACT_METADATA,
12236
+ generator: createLineOfDepartureContact,
12237
+ defaultOptions: DEFAULT_LINE_OF_DEPARTURE_CONTACT_OPTIONS,
12238
+ rule: line1DrawRule,
12239
+ previewSample: {
12240
+ controlPoints: [[-1, 0], [1, 0]],
12241
+ options: { labelSize: 96 }
12242
+ }
12243
+ });
12244
+ //#endregion
12245
+ //#region src/generators/cm14-maneuver-lines/probable-line-of-deployment.ts
12246
+ /** Default options for the Probable line of deployment control measure. */
12247
+ const DEFAULT_PROBABLE_LINE_OF_DEPLOYMENT_OPTIONS = {
12248
+ includePrefix: true,
12249
+ smooth: false,
12250
+ smoothResolution: 12
12251
+ };
12252
+ /** Intrinsic doctrinal dash pattern for Probable Line of Deployment. */
12253
+ const PROBABLE_LINE_OF_DEPLOYMENT_DASH = [6, 4];
12254
+ const PROBABLE_LINE_OF_DEPLOYMENT_METADATA = {
12255
+ id: "probable-line-of-deployment",
12256
+ name: "Probable line of deployment",
12257
+ description: "A line selected on the ground, usually the last covered and concealed position prior to the objective and forward of the line of departure, where attacking units deploy prior to beginning an assault.",
12258
+ entity: "Maneuver Lines",
12259
+ entityType: "Probable Line of Deployment",
12260
+ value: "141200",
12261
+ minCoordinates: 2,
12262
+ geometry: "line",
12263
+ geometryTypes: ["LineString", "Point"],
12264
+ paints: {
12265
+ stroke: true,
12266
+ fill: "none",
12267
+ text: true
12268
+ },
12269
+ drawRule: "Line1",
12270
+ capturesLabelSize: true,
12271
+ params: [...SMOOTH_LINE_PARAMS, ...phaseLineNameParams("ONYX")],
12272
+ textAmplifiers: [HOSTILE_LINE_AMPLIFIER]
12273
+ };
12274
+ /** Creates a Probable line of deployment labeled "PLD" above both ends, with a dashed stroke. */
12275
+ function createProbableLineOfDeployment(positions, options = {}, textAmplifiers = {}, context = {}) {
12276
+ return fixedLabelLineFeatures(positions, {
12277
+ part: "probable-line-of-deployment",
12278
+ placement: "above",
12279
+ lineStyle: { strokeDash: [...PROBABLE_LINE_OF_DEPLOYMENT_DASH] },
12280
+ ...options
12281
+ }, "PLD", textAmplifiers.N, context);
12282
+ }
12283
+ const PROBABLE_LINE_OF_DEPLOYMENT = defineControlMeasure({
12284
+ metadata: PROBABLE_LINE_OF_DEPLOYMENT_METADATA,
12285
+ generator: createProbableLineOfDeployment,
12286
+ defaultOptions: DEFAULT_PROBABLE_LINE_OF_DEPLOYMENT_OPTIONS,
12287
+ rule: line1DrawRule,
12288
+ previewSample: {
12289
+ controlPoints: [[-1, 0], [1, 0]],
12290
+ options: { labelSize: 96 }
12291
+ }
12292
+ });
12293
+ //#endregion
11397
12294
  //#region src/generators/cm29-protection-lines/fortifiedLine.ts
11398
12295
  const DEFAULT_TOOTH_SIZE = 50;
11399
12296
  const DEFAULT_TOOTH_SIZE_PIXELS = 10;
@@ -12634,6 +13531,57 @@ const LANDING_ZONE = defineControlMeasure({
12634
13531
  }
12635
13532
  });
12636
13533
  //#endregion
13534
+ //#region src/generators/cm15-maneuver-areas/penetration-box.ts
13535
+ /** Default options for the Penetration box control measure. */
13536
+ const DEFAULT_PENETRATION_BOX_OPTIONS = {
13537
+ smooth: false,
13538
+ smoothResolution: 16
13539
+ };
13540
+ const PENETRATION_BOX_METADATA = {
13541
+ id: "penetration-box",
13542
+ name: "Penetration box",
13543
+ description: "An area in which a force ruptures the enemy's defense on a narrow front to break the continuity of the defensive system.",
13544
+ entity: "Maneuver Areas",
13545
+ entityType: "Penetration Box",
13546
+ value: "151900",
13547
+ minCoordinates: 3,
13548
+ geometry: "area",
13549
+ geometryTypes: [
13550
+ "Polygon",
13551
+ "MultiLineString",
13552
+ "Point"
13553
+ ],
13554
+ paints: {
13555
+ stroke: true,
13556
+ fill: "none",
13557
+ text: true
13558
+ },
13559
+ drawRule: "Area1",
13560
+ capturesLabelSize: true,
13561
+ params: LABELED_AREA_PARAMS,
13562
+ textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
13563
+ };
13564
+ /** Creates a Penetration box: a bare closed area through `positions`. */
13565
+ function createPenetrationBox(positions, options = {}, textAmplifiers = {}, context = {}) {
13566
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, ""), context.amplifierPlacements, context);
13567
+ }
13568
+ const PENETRATION_BOX = defineControlMeasure({
13569
+ metadata: PENETRATION_BOX_METADATA,
13570
+ generator: createPenetrationBox,
13571
+ defaultOptions: DEFAULT_PENETRATION_BOX_OPTIONS,
13572
+ rule: area1DrawRule,
13573
+ previewSample: {
13574
+ controlPoints: [
13575
+ [-1, -.5],
13576
+ [1, -.5],
13577
+ [1, .5],
13578
+ [-1, .5]
13579
+ ],
13580
+ textAmplifiers: {},
13581
+ options: { labelSize: 96 }
13582
+ }
13583
+ });
13584
+ //#endregion
12637
13585
  //#region src/generators/cm15-maneuver-areas/limitedAccessArea.ts
12638
13586
  /** Default options for the Limited Access Area control measure. */
12639
13587
  const DEFAULT_LIMITED_ACCESS_AREA_OPTIONS = {
@@ -14379,6 +15327,180 @@ const STRONG_POINT = defineControlMeasure({
14379
15327
  }
14380
15328
  });
14381
15329
  //#endregion
15330
+ //#region src/generators/cm15-maneuver-areas/retain.ts
15331
+ const DEFAULT_RETAIN_OPTIONS = {
15332
+ openingAngle: 30,
15333
+ arrowheadLengthRatio: .1,
15334
+ arrowheadWidthRatio: .12,
15335
+ labelPadding: 0
15336
+ };
15337
+ const RETAIN_METADATA = {
15338
+ id: "retain",
15339
+ name: "Retain",
15340
+ description: "Ensures that a terrain feature controlled by a friendly force remains secure.",
15341
+ entity: "Maneuver Areas",
15342
+ entityType: "Battle Position",
15343
+ entitySubtype: "Retain",
15344
+ value: "151205",
15345
+ minCoordinates: 2,
15346
+ maxCoordinates: 2,
15347
+ geometry: "area",
15348
+ geometryTypes: ["MultiLineString", "Point"],
15349
+ paints: {
15350
+ stroke: true,
15351
+ fill: "none",
15352
+ text: true
15353
+ },
15354
+ drawRule: "Area6",
15355
+ capturesLabelSize: true,
15356
+ params: [
15357
+ {
15358
+ key: "openingAngle",
15359
+ presentationTier: "advanced",
15360
+ label: "Opening",
15361
+ description: "Angular opening on the friendly side, in degrees",
15362
+ type: "number",
15363
+ min: 5,
15364
+ max: 120,
15365
+ step: 1
15366
+ },
15367
+ {
15368
+ key: "arrowheadLengthRatio",
15369
+ presentationTier: "advanced",
15370
+ label: "Arrowhead length",
15371
+ description: "Open-arrowhead length as a ratio of the symbol radius",
15372
+ type: "number",
15373
+ min: .02,
15374
+ max: .5,
15375
+ step: .01
15376
+ },
15377
+ {
15378
+ key: "arrowheadWidthRatio",
15379
+ presentationTier: "advanced",
15380
+ label: "Arrowhead width",
15381
+ description: "Open-arrowhead base width as a ratio of the symbol radius",
15382
+ type: "number",
15383
+ min: .02,
15384
+ max: .5,
15385
+ step: .01
15386
+ },
15387
+ {
15388
+ key: "labelPadding",
15389
+ presentationTier: "advanced",
15390
+ label: "R padding",
15391
+ description: "Extra boundary clearance on each side of R, as a ratio of its height",
15392
+ type: "number",
15393
+ min: 0,
15394
+ max: 2,
15395
+ step: .05
15396
+ }
15397
+ ]
15398
+ };
15399
+ const TAU = Math.PI * 2;
15400
+ /** Smallest visible boundary gap, in projected meters, for tiny label heights. */
15401
+ const MIN_LABEL_GAP_METERS = 1;
15402
+ function pointOnCircle(center, radius, angle) {
15403
+ return vecAdd(center, [radius * Math.cos(angle), radius * Math.sin(angle)]);
15404
+ }
15405
+ function sampleArc(center, radius, start, end) {
15406
+ const segments = Math.max(8, Math.ceil(48 * Math.abs(end - start) / Math.PI));
15407
+ return Array.from({ length: segments + 1 }, (_, index) => {
15408
+ const point = pointOnCircle(center, radius, start + (end - start) * index / segments);
15409
+ return unproject(point[0], point[1]);
15410
+ });
15411
+ }
15412
+ /** Creates the Retain boundary, outward tics, and intrinsic R label. */
15413
+ function createRetain(coordinates, options = {}) {
15414
+ const center = project(coordinates[0][0], coordinates[0][1]);
15415
+ const radiusVector = vecSub(project(coordinates[1][0], coordinates[1][1]), center);
15416
+ const radius = vecMag(radiusVector);
15417
+ if (radius < 1e-6) return {
15418
+ type: "FeatureCollection",
15419
+ features: []
15420
+ };
15421
+ const resolved = {
15422
+ ...DEFAULT_RETAIN_OPTIONS,
15423
+ ...options
15424
+ };
15425
+ const startAngle = Math.atan2(radiusVector[1], radiusVector[0]);
15426
+ const opening = Math.min(120, Math.max(5, resolved.openingAngle)) * Math.PI / 180;
15427
+ const h = resolveLabelOffsetMeters(options, 1, radius * .1);
15428
+ const labelPadding = Math.max(0, resolved.labelPadding);
15429
+ const boundarySpan = TAU - opening;
15430
+ const labelDistance = Math.PI - opening / 2;
15431
+ const labelCenter = startAngle - labelDistance;
15432
+ const labelHalfGap = Math.min(Math.PI / 2, Math.max(h * (LABEL_HALF_GLYPH_RATIO + labelPadding), MIN_LABEL_GAP_METERS) / radius);
15433
+ const labelStart = startAngle - labelDistance + labelHalfGap;
15434
+ const labelEnd = startAngle - labelDistance - labelHalfGap;
15435
+ const boundaryEnd = startAngle - boundarySpan;
15436
+ const arrowTip = pointOnCircle(center, radius, boundaryEnd);
15437
+ const arrowheadLength = radius * Math.max(0, resolved.arrowheadLengthRatio);
15438
+ const arrowheadWidth = radius * Math.max(0, resolved.arrowheadWidthRatio);
15439
+ const boundary = [
15440
+ sampleArc(center, radius, startAngle, labelStart),
15441
+ sampleArc(center, radius, labelEnd, boundaryEnd),
15442
+ createArrowHeadRing(arrowTip, [Math.sin(boundaryEnd), -Math.cos(boundaryEnd)], arrowheadLength, arrowheadWidth).slice(0, 3)
15443
+ ];
15444
+ const tics = [];
15445
+ const spacingAngle = Math.max(h / radius, Math.PI / 180);
15446
+ const arrowClearanceAngle = (arrowheadLength + arrowheadWidth / 2 + h / 2) / radius;
15447
+ for (let distance = spacingAngle / 2; distance < boundarySpan - arrowClearanceAngle; distance += spacingAngle) {
15448
+ if (distance > labelDistance - labelHalfGap && distance < labelDistance + labelHalfGap) continue;
15449
+ const angle = startAngle - distance;
15450
+ const cos = Math.cos(angle);
15451
+ const sin = Math.sin(angle);
15452
+ const innerX = center[0] + radius * cos;
15453
+ const innerY = center[1] + radius * sin;
15454
+ tics.push([unproject(innerX, innerY), unproject(innerX + h * cos, innerY + h * sin)]);
15455
+ }
15456
+ const labelPoint = pointOnCircle(center, radius, labelCenter);
15457
+ return {
15458
+ type: "FeatureCollection",
15459
+ features: [
15460
+ {
15461
+ type: "Feature",
15462
+ properties: { part: "boundary" },
15463
+ geometry: {
15464
+ type: "MultiLineString",
15465
+ coordinates: boundary
15466
+ }
15467
+ },
15468
+ {
15469
+ type: "Feature",
15470
+ properties: { part: "tics" },
15471
+ geometry: {
15472
+ type: "MultiLineString",
15473
+ coordinates: tics
15474
+ }
15475
+ },
15476
+ {
15477
+ type: "Feature",
15478
+ properties: {
15479
+ part: "label",
15480
+ labelPlacement: false,
15481
+ text: "R",
15482
+ rotation: Math.PI,
15483
+ ...labelSizeProps(options)
15484
+ },
15485
+ geometry: {
15486
+ type: "Point",
15487
+ coordinates: unproject(labelPoint[0], labelPoint[1])
15488
+ }
15489
+ }
15490
+ ]
15491
+ };
15492
+ }
15493
+ const RETAIN = defineControlMeasure({
15494
+ metadata: RETAIN_METADATA,
15495
+ generator: createRetain,
15496
+ defaultOptions: DEFAULT_RETAIN_OPTIONS,
15497
+ rule: retainDrawRule,
15498
+ previewSample: {
15499
+ controlPoints: [[0, 0], [-.8, .2]],
15500
+ options: { labelSize: 90 }
15501
+ }
15502
+ });
15503
+ //#endregion
14382
15504
  //#region src/generators/cm15-maneuver-areas/submarineActionArea.ts
14383
15505
  /** Default options for the Submarine action area control measure. */
14384
15506
  const DEFAULT_SUBMARINE_ACTION_AREA_OPTIONS = {
@@ -14837,7 +15959,10 @@ const DEFINITIONS = {
14837
15959
  "engineer-work-line": ENGINEER_WORK_LINE,
14838
15960
  "generic-c2-line": GENERIC_C2_LINE,
14839
15961
  "battle-position": BATTLE_POSITION,
15962
+ "battle-position-prepared": BATTLE_POSITION_PREPARED,
15963
+ contain: CONTAIN,
14840
15964
  "strong-point": STRONG_POINT,
15965
+ retain: RETAIN,
14841
15966
  ambush: AMBUSH,
14842
15967
  "direction-of-attack-aviation": DIRECTION_OF_ATTACK_AVIATION,
14843
15968
  "direction-of-main-attack": DIRECTION_OF_MAIN_ATTACK,
@@ -14850,6 +15975,8 @@ const DEFINITIONS = {
14850
15975
  encirclement: ENCIRCLEMENT,
14851
15976
  area: AREA,
14852
15977
  "area-defense": AREA_DEFENSE,
15978
+ "engagement-area": ENGAGEMENT_AREA,
15979
+ "mobile-defense": MOBILE_DEFENSE,
14853
15980
  "assembly-area": ASSEMBLY_AREA,
14854
15981
  "joint-tactical-action-area": JOINT_TACTICAL_ACTION_AREA,
14855
15982
  "submarine-action-area": SUBMARINE_ACTION_AREA,
@@ -14858,6 +15985,7 @@ const DEFINITIONS = {
14858
15985
  "extraction-zone": EXTRACTION_ZONE,
14859
15986
  "landing-zone": LANDING_ZONE,
14860
15987
  "limited-access-area": LIMITED_ACCESS_AREA,
15988
+ "penetration-box": PENETRATION_BOX,
14861
15989
  "no-fire-area-irregular": NO_FIRE_AREA_IRREGULAR,
14862
15990
  "minefield-dynamic": DYNAMIC_MINEFIELD,
14863
15991
  "mined-area": MINED_AREA,
@@ -14891,10 +16019,16 @@ const DEFINITIONS = {
14891
16019
  "phase-line": PHASE_LINE,
14892
16020
  "forward-edge-of-battle-area": FORWARD_EDGE_OF_BATTLE_AREA,
14893
16021
  "bridgehead-line": BRIDGEHEAD_LINE,
16022
+ "final-coordination-line": FINAL_COORDINATION_LINE,
16023
+ "airhead-line": AIRHEAD_LINE,
14894
16024
  "holding-line": HOLDING_LINE,
14895
16025
  "release-line": RELEASE_LINE,
14896
16026
  "handover-line": HANDOVER_LINE,
14897
16027
  "battle-handover-line": BATTLE_HANDOVER_LINE,
16028
+ "limit-of-advance": LIMIT_OF_ADVANCE,
16029
+ "line-of-departure": LINE_OF_DEPARTURE,
16030
+ "line-of-departure-contact": LINE_OF_DEPARTURE_CONTACT,
16031
+ "probable-line-of-deployment": PROBABLE_LINE_OF_DEPLOYMENT,
14898
16032
  "block-mission-task": BLOCK_MISSION_TASK,
14899
16033
  breach: BREACH,
14900
16034
  bypass: BYPASS,
@@ -15337,4 +16471,4 @@ function assertNever(value) {
15337
16471
  throw new Error(`Unhandled control measure kind: ${String(value)}`);
15338
16472
  }
15339
16473
  //#endregion
15340
- export { DEFAULT_TACTICAL_ARROW_OPTIONS as $, line26DrawRule as $t, DEFAULT_REARWARD_PASSAGE_OF_LINES_OPTIONS as A, DEFAULT_AREA_OF_OPERATIONS_OPTIONS as At, DEFAULT_BRIDGEHEAD_LINE_OPTIONS as B, DEFAULT_LABEL_SIZE_CLAMP_CSS_PIXELS as Bt, DEFAULT_LANDING_ZONE_OPTIONS as C, project as Cn, DEFAULT_BLOCK_OPTIONS as Ct, DEFAULT_SEIZE_OPTIONS as D, getMetersPerPixel as Dn, DEFAULT_ANTITANK_WALL_OPTIONS as Dt, DEFAULT_WITHDRAW_OPTIONS as E, EPSILON as En, DEFAULT_ATTACK_BY_FIRE_OPTIONS as Et, DEFAULT_BATTLE_HANDOVER_LINE_OPTIONS as F, TEXT_AMPLIFIER_FIELDS as Ft, DEFAULT_FINAL_PROTECTIVE_FIRE_OPTIONS as G, DEFAULT_STROKE_WIDTH_CSS_PIXELS as Gt, DEFAULT_FLOT_OPTIONS as H, DEFAULT_LINE_JOIN as Ht, DEFAULT_HANDOVER_LINE_OPTIONS as I, canonicalTextAmplifierKey as It, DEFAULT_GUARD_OPTIONS as J, DEFAULT_AIRBORNE_ATTACK_OPTIONS as Jt, DEFAULT_ENCIRCLEMENT_OPTIONS as K, DEFAULT_SYMBOL_COLOR as Kt, DEFAULT_FORWARD_EDGE_OF_BATTLE_AREA_OPTIONS as L, normalizeTextAmplifiers as Lt, DEFAULT_FRONTAL_ATTACK_OPTIONS as M, DEFAULT_DIRECTION_OF_SUPPORTING_ATTACK_OPTIONS as Mt, DEFAULT_FORTIFIED_AREA_OPTIONS as N, DEFAULT_DIRECTION_OF_MAIN_ATTACK_OPTIONS as Nt, DEFAULT_SCREEN_OPTIONS as O, roundToFixed as On, DEFAULT_ANTITANK_DITCH_OPTIONS as Ot, DEFAULT_FORTIFIED_LINE_OPTIONS as P, DEFAULT_DIRECTION_OF_ATTACK_AVIATION_OPTIONS as Pt, DEFAULT_COVER_OPTIONS as Q, line27DrawRule as Qt, DEFAULT_RELEASE_LINE_OPTIONS as R, resolveAmplifierPlacement as Rt, DEFAULT_MAIN_ATTACK_OPTIONS as S, haversineDistance as Sn, DEFAULT_BLOCK_ARROW_OPTIONS as St, DEFAULT_WITHDRAW_UNDER_PRESSURE_OPTIONS as T, unproject as Tn, DEFAULT_ATTACK_HELICOPTER_OPTIONS as Tt, DEFAULT_FIX_MISSION_TASK_OPTIONS as U, DEFAULT_PORTRAYAL as Ut, DEFAULT_PHASE_LINE_OPTIONS as V, DEFAULT_LINE_CAP as Vt, DEFAULT_FIX_OPTIONS as W, DEFAULT_STROKE_DASH_CSS_PIXELS as Wt, DEFAULT_DISENGAGE_OPTIONS as X, axis1DrawRule as Xt, DEFAULT_DISRUPT_MISSION_TASK_OPTIONS as Y, rectangleDrawRule as Yt, DEFAULT_DELAY_OPTIONS as Z, supportByFireDrawRule as Zt, DEFAULT_PICKUP_ZONE_OPTIONS as _, pointOnMidpointPerpendicularAxis as _n, DEFAULT_BLOCK_MISSION_TASK_OPTIONS as _t, DEFINITIONS as a, attackByFireDrawRule as an, RADAR_SEARCH_DOCTRINE_FILL_COLOR as at, DEFAULT_OBSTACLE_BYPASS_DIFFICULT_OPTIONS as b, calculateMetrics as bn, DEFAULT_LIGHT_LINE_OPTIONS as bt, getDefaultOptions as c, staticPointDrawRule as cn, DEFAULT_GENERIC_SECTOR_OPTIONS as ct, DEFAULT_TURN_OPTIONS as d, centerRadiusDrawRule as dn, DEFAULT_GENERIC_LINE_OPTIONS as dt, line24DrawRule as en, DEFAULT_COUNTERATTACK_BY_FIRE_OPTIONS as et, DEFAULT_SUPPORT_BY_FIRE_OPTIONS as f, disruptDrawRule as fn, DEFAULT_GENERIC_CIRCLE_OPTIONS as ft, DEFAULT_PRINCIPAL_DIRECTION_OF_FIRE_OPTIONS as g, getMidpointPerpendicularSignedDistance as gn, DEFAULT_BREACH_OPTIONS as gt, SECONDARY_DIRECTION_OF_FIRE_DASH as h, createMidpointPerpendicularDrawRule as hn, DEFAULT_BYPASS_OPTIONS as ht, CONTROL_MEASURE_METADATA as i, ambushDrawRule as in, DEFAULT_RADAR_SEARCH_DOCTRINE_OPTIONS as it, DEFAULT_PENETRATE_OPTIONS as j, DEFAULT_AREA_DEFENSE_OPTIONS as jt, DEFAULT_RETIRE_OPTIONS as k, DEFAULT_ASSEMBLY_AREA_OPTIONS as kt, listControlMeasureMetadata as l, point12DrawRule as ln, DEFAULT_GENERIC_RECTANGLE_OPTIONS as lt, DEFAULT_SECONDARY_DIRECTION_OF_FIRE_OPTIONS as m, computeDefaultMidpointPerpendicularPoint as mn, DEFAULT_CANALIZE_OPTIONS as mt, resolveStyleHints as n, turnDrawRule as nn, DEFAULT_SUPPORTING_ATTACK_OPTIONS as nt, getControlMeasureMetadata as o, dynamicPointDrawRule as on, RADAR_SEARCH_DOCTRINE_STROKE_COLOR as ot, DEFAULT_STRONG_POINT_OPTIONS as p, blockDrawRule as pn, DEFAULT_CLASSIC_ARROW_OPTIONS as pt, DEFAULT_DISRUPT_OPTIONS as q, DEFAULT_AMBUSH_OPTIONS as qt, CONTROL_MEASURE_IDS as r, line1DrawRule as rn, DEFAULT_CLEAR_OPTIONS as rt, getControlMeasureMetadataByValue as s, sectorDrawRule as sn, DEFAULT_GENERIC_TEXT_OPTIONS as st, renderControlMeasure as t, line23DrawRule as tn, DEFAULT_COUNTERATTACK_OPTIONS as tt, DEFAULT_TURNING_MOVEMENT_OPTIONS as u, penetrateDrawRule as un, DEFAULT_GENERIC_POLYGON_OPTIONS as ut, DEFAULT_OBSTACLE_BYPASS_IMPOSSIBLE_OPTIONS as v, snapToMidpointPerpendicular as vn, DEFAULT_GENERIC_C2_LINE_OPTIONS as vt, DEFAULT_JOINT_TACTICAL_ACTION_AREA_OPTIONS as w, sphericalBearing as wn, DEFAULT_BATTLE_POSITION_OPTIONS as wt, DEFAULT_MINEFIELD_OPTIONS as x, computeInitialWidthPoint as xn, DEFAULT_BOUNDARY_OPTIONS as xt, DEFAULT_OBSTACLE_BYPASS_EASY_OPTIONS as y, createBaselineFrame as yn, DEFAULT_ENGINEER_WORK_LINE_OPTIONS as yt, DEFAULT_HOLDING_LINE_OPTIONS as z, DEFAULT_LABEL_HEIGHT_CSS_PIXELS as zt };
16474
+ export { DEFAULT_DISRUPT_OPTIONS as $, DEFAULT_STROKE_DASH_CSS_PIXELS as $t, DEFAULT_RETIRE_OPTIONS as A, createBaselineFrame as An, DEFAULT_CONTAIN_OPTIONS as At, DEFAULT_LIMIT_OF_ADVANCE_OPTIONS as B, DEFAULT_AREA_DEFENSE_OPTIONS as Bt, DEFAULT_MAIN_ATTACK_OPTIONS as C, containDrawRule as Cn, DEFAULT_BLOCK_MISSION_TASK_OPTIONS as Ct, DEFAULT_WITHDRAW_OPTIONS as D, getMidpointPerpendicularSignedDistance as Dn, DEFAULT_BOUNDARY_OPTIONS as Dt, DEFAULT_WITHDRAW_UNDER_PRESSURE_OPTIONS as E, createMidpointPerpendicularDrawRule as En, DEFAULT_LIGHT_LINE_OPTIONS as Et, DEFAULT_FORTIFIED_LINE_OPTIONS as F, sphericalBearing as Fn, DEFAULT_ANTITANK_DITCH_OPTIONS as Ft, DEFAULT_HOLDING_LINE_OPTIONS as G, canonicalTextAmplifierKey as Gt, DEFAULT_HANDOVER_LINE_OPTIONS as H, DEFAULT_DIRECTION_OF_MAIN_ATTACK_OPTIONS as Ht, DEFAULT_PROBABLE_LINE_OF_DEPLOYMENT_OPTIONS as I, unproject as In, DEFAULT_ASSEMBLY_AREA_OPTIONS as It, DEFAULT_FLOT_OPTIONS as J, DEFAULT_LABEL_HEIGHT_CSS_PIXELS as Jt, DEFAULT_BRIDGEHEAD_LINE_OPTIONS as K, normalizeTextAmplifiers as Kt, PROBABLE_LINE_OF_DEPLOYMENT_DASH as L, EPSILON as Ln, DEFAULT_AREA_OF_OPERATIONS_OPTIONS as Lt, DEFAULT_PENETRATE_OPTIONS as M, computeInitialWidthPoint as Mn, DEFAULT_ATTACK_HELICOPTER_OPTIONS as Mt, DEFAULT_FRONTAL_ATTACK_OPTIONS as N, haversineDistance as Nn, DEFAULT_ATTACK_BY_FIRE_OPTIONS as Nt, DEFAULT_SEIZE_OPTIONS as O, pointOnMidpointPerpendicularAxis as On, DEFAULT_BLOCK_ARROW_OPTIONS as Ot, DEFAULT_FORTIFIED_AREA_OPTIONS as P, project as Pn, DEFAULT_ANTITANK_WALL_OPTIONS as Pt, DEFAULT_ENCIRCLEMENT_OPTIONS as Q, DEFAULT_PORTRAYAL as Qt, DEFAULT_LINE_OF_DEPARTURE_CONTACT_OPTIONS as R, getMetersPerPixel as Rn, DEFAULT_MOBILE_DEFENSE_OPTIONS as Rt, DEFAULT_MINEFIELD_OPTIONS as S, centerRadiusDrawRule as Sn, DEFAULT_BREACH_OPTIONS as St, DEFAULT_JOINT_TACTICAL_ACTION_AREA_OPTIONS as T, computeDefaultMidpointPerpendicularPoint as Tn, DEFAULT_ENGINEER_WORK_LINE_OPTIONS as Tt, DEFAULT_FORWARD_EDGE_OF_BATTLE_AREA_OPTIONS as U, DEFAULT_DIRECTION_OF_ATTACK_AVIATION_OPTIONS as Ut, DEFAULT_BATTLE_HANDOVER_LINE_OPTIONS as V, DEFAULT_DIRECTION_OF_SUPPORTING_ATTACK_OPTIONS as Vt, DEFAULT_RELEASE_LINE_OPTIONS as W, TEXT_AMPLIFIER_FIELDS as Wt, DEFAULT_FIX_OPTIONS as X, DEFAULT_LINE_CAP as Xt, DEFAULT_FIX_MISSION_TASK_OPTIONS as Y, DEFAULT_LABEL_SIZE_CLAMP_CSS_PIXELS as Yt, DEFAULT_FINAL_PROTECTIVE_FIRE_OPTIONS as Z, DEFAULT_LINE_JOIN as Zt, DEFAULT_PRINCIPAL_DIRECTION_OF_FIRE_OPTIONS as _, staticPointDrawRule as _n, DEFAULT_GENERIC_LINE_OPTIONS as _t, DEFINITIONS as a, axis1DrawRule as an, DEFAULT_TACTICAL_ARROW_OPTIONS as at, DEFAULT_OBSTACLE_BYPASS_EASY_OPTIONS as b, disruptDrawRule as bn, DEFAULT_CANALIZE_OPTIONS as bt, getDefaultOptions as c, line26DrawRule as cn, DEFAULT_SUPPORTING_ATTACK_OPTIONS as ct, DEFAULT_TURN_OPTIONS as d, turnDrawRule as dn, RADAR_SEARCH_DOCTRINE_FILL_COLOR as dt, DEFAULT_STROKE_WIDTH_CSS_PIXELS as en, DEFAULT_GUARD_OPTIONS as et, DEFAULT_SUPPORT_BY_FIRE_OPTIONS as f, line1DrawRule as fn, RADAR_SEARCH_DOCTRINE_STROKE_COLOR as ft, SECONDARY_DIRECTION_OF_FIRE_DASH as g, sectorDrawRule as gn, DEFAULT_GENERIC_POLYGON_OPTIONS as gt, DEFAULT_SECONDARY_DIRECTION_OF_FIRE_OPTIONS as h, dynamicPointDrawRule as hn, DEFAULT_GENERIC_RECTANGLE_OPTIONS as ht, CONTROL_MEASURE_METADATA as i, rectangleDrawRule as in, DEFAULT_COVER_OPTIONS as it, DEFAULT_REARWARD_PASSAGE_OF_LINES_OPTIONS as j, calculateMetrics as jn, DEFAULT_BATTLE_POSITION_OPTIONS as jt, DEFAULT_SCREEN_OPTIONS as k, snapToMidpointPerpendicular as kn, DEFAULT_BLOCK_OPTIONS as kt, listControlMeasureMetadata as l, line24DrawRule as ln, DEFAULT_CLEAR_OPTIONS as lt, DEFAULT_STRONG_POINT_OPTIONS as m, attackByFireDrawRule as mn, DEFAULT_GENERIC_SECTOR_OPTIONS as mt, resolveStyleHints as n, DEFAULT_AMBUSH_OPTIONS as nn, DEFAULT_DISENGAGE_OPTIONS as nt, getControlMeasureMetadata as o, supportByFireDrawRule as on, DEFAULT_COUNTERATTACK_BY_FIRE_OPTIONS as ot, DEFAULT_RETAIN_OPTIONS as p, ambushDrawRule as pn, DEFAULT_GENERIC_TEXT_OPTIONS as pt, DEFAULT_PHASE_LINE_OPTIONS as q, resolveAmplifierPlacement as qt, CONTROL_MEASURE_IDS as r, DEFAULT_AIRBORNE_ATTACK_OPTIONS as rn, DEFAULT_DELAY_OPTIONS as rt, getControlMeasureMetadataByValue as s, line27DrawRule as sn, DEFAULT_COUNTERATTACK_OPTIONS as st, renderControlMeasure as t, DEFAULT_SYMBOL_COLOR as tn, DEFAULT_DISRUPT_MISSION_TASK_OPTIONS as tt, DEFAULT_TURNING_MOVEMENT_OPTIONS as u, line23DrawRule as un, DEFAULT_RADAR_SEARCH_DOCTRINE_OPTIONS as ut, DEFAULT_PICKUP_ZONE_OPTIONS as v, point12DrawRule as vn, DEFAULT_GENERIC_CIRCLE_OPTIONS as vt, DEFAULT_LANDING_ZONE_OPTIONS as w, area27DrawRule as wn, DEFAULT_GENERIC_C2_LINE_OPTIONS as wt, DEFAULT_OBSTACLE_BYPASS_DIFFICULT_OPTIONS as x, blockDrawRule as xn, DEFAULT_BYPASS_OPTIONS as xt, DEFAULT_OBSTACLE_BYPASS_IMPOSSIBLE_OPTIONS as y, penetrateDrawRule as yn, DEFAULT_CLASSIC_ARROW_OPTIONS as yt, DEFAULT_LINE_OF_DEPARTURE_OPTIONS as z, roundToFixed as zn, DEFAULT_ENGAGEMENT_AREA_OPTIONS as zt };