@orbat-mapper/control-measures 0.2.0-alpha.10 → 0.2.0-alpha.11

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.
Files changed (59) hide show
  1. package/dist/{index-D3uCYKe3.d.mts → index-C6nqkw1A.d.mts} +316 -68
  2. package/dist/index.d.mts +1 -1
  3. package/dist/index.mjs +1 -1
  4. package/dist/preview/index.d.mts +65 -10
  5. package/dist/preview/index.mjs +129 -18
  6. package/dist/{renderControlMeasure-BblcXXLp.mjs → renderControlMeasure-NKEYq3Fg.mjs} +1430 -595
  7. package/media/airborne-attack.svg +1 -1
  8. package/media/ambush.svg +1 -1
  9. package/media/antitank-ditch-completed.svg +1 -1
  10. package/media/antitank-ditch-under-construction.svg +1 -1
  11. package/media/antitank-wall.svg +1 -1
  12. package/media/area-of-operations.svg +1 -0
  13. package/media/assembly-area.svg +1 -0
  14. package/media/attack-by-fire.svg +1 -1
  15. package/media/attack-helicopter.svg +1 -1
  16. package/media/battle-handover-line.svg +1 -0
  17. package/media/battle-position.svg +1 -1
  18. package/media/block-arrow.svg +1 -1
  19. package/media/block-mission-task.svg +1 -1
  20. package/media/block.svg +1 -1
  21. package/media/boundary.svg +1 -1
  22. package/media/breach.svg +1 -1
  23. package/media/bypass.svg +1 -1
  24. package/media/canalize.svg +1 -1
  25. package/media/circle.svg +1 -1
  26. package/media/classic-arrow.svg +1 -1
  27. package/media/clear.svg +1 -1
  28. package/media/delay.svg +1 -1
  29. package/media/disrupt.svg +1 -1
  30. package/media/encirclement.svg +1 -1
  31. package/media/engineer-work-line.svg +1 -0
  32. package/media/final-protective-fire-left.svg +1 -1
  33. package/media/final-protective-fire-right.svg +1 -1
  34. package/media/fix.svg +1 -1
  35. package/media/flot.svg +1 -1
  36. package/media/fortified-area.svg +1 -1
  37. package/media/fortified-line.svg +1 -1
  38. package/media/generic-c2-line.svg +1 -0
  39. package/media/handover-line.svg +1 -0
  40. package/media/isolate.svg +1 -1
  41. package/media/joint-tactical-action-area.svg +1 -0
  42. package/media/landing-zone.svg +1 -0
  43. package/media/light-line.svg +1 -0
  44. package/media/line.svg +1 -1
  45. package/media/main-attack.svg +1 -1
  46. package/media/obstacle-bypass-difficult.svg +1 -1
  47. package/media/obstacle-bypass-easy.svg +1 -1
  48. package/media/obstacle-bypass-impossible.svg +1 -1
  49. package/media/phase-line.svg +1 -0
  50. package/media/pickup-zone.svg +1 -0
  51. package/media/polygon.svg +1 -1
  52. package/media/principal-direction-of-fire.svg +1 -1
  53. package/media/rectangle.svg +1 -1
  54. package/media/search-area.svg +1 -1
  55. package/media/strong-point.svg +1 -1
  56. package/media/support-by-fire.svg +1 -1
  57. package/media/supporting-attack.svg +1 -1
  58. package/media/turn.svg +1 -1
  59. package/package.json +1 -1
@@ -963,7 +963,231 @@ const rectangleDrawRule = {
963
963
  }
964
964
  };
965
965
  //#endregion
966
+ //#region src/internal/echelon.ts
967
+ /**
968
+ * APP-6 echelon (Field B) glyph vocabulary, shared by the control measures that
969
+ * stamp a unit-size amplifier onto their geometry (Boundary, Battle Position).
970
+ *
971
+ * A compact port of the APP-6 echelon designs, modeled on milsymbol but
972
+ * expressed as parametric geometry so it bakes into GeoJSON and strokes
973
+ * monocolor (ADR-0023). Every glyph is built from three primitives — vertical
974
+ * bars, crosses, and dots — plus the team/crew ring and command `++`.
975
+ */
976
+ const ECHELONS = {
977
+ team: {
978
+ kind: "ring",
979
+ count: 1,
980
+ label: "Team/Crew"
981
+ },
982
+ crew: {
983
+ kind: "ring",
984
+ count: 1
985
+ },
986
+ squad: {
987
+ kind: "dots",
988
+ count: 1,
989
+ label: "Squad"
990
+ },
991
+ section: {
992
+ kind: "dots",
993
+ count: 2,
994
+ label: "Section"
995
+ },
996
+ platoon: {
997
+ kind: "dots",
998
+ count: 3,
999
+ label: "Platoon"
1000
+ },
1001
+ company: {
1002
+ kind: "bars",
1003
+ count: 1,
1004
+ label: "Company"
1005
+ },
1006
+ battalion: {
1007
+ kind: "bars",
1008
+ count: 2,
1009
+ label: "Battalion"
1010
+ },
1011
+ regiment: {
1012
+ kind: "bars",
1013
+ count: 3,
1014
+ label: "Regiment"
1015
+ },
1016
+ brigade: {
1017
+ kind: "crosses",
1018
+ count: 1,
1019
+ label: "Brigade"
1020
+ },
1021
+ division: {
1022
+ kind: "crosses",
1023
+ count: 2,
1024
+ label: "Division"
1025
+ },
1026
+ corps: {
1027
+ kind: "crosses",
1028
+ count: 3,
1029
+ label: "Corps"
1030
+ },
1031
+ army: {
1032
+ kind: "crosses",
1033
+ count: 4,
1034
+ label: "Army"
1035
+ },
1036
+ "army-group": {
1037
+ kind: "crosses",
1038
+ count: 5,
1039
+ label: "Army Group"
1040
+ },
1041
+ region: {
1042
+ kind: "crosses",
1043
+ count: 6,
1044
+ label: "Region"
1045
+ },
1046
+ command: {
1047
+ kind: "command",
1048
+ count: 2,
1049
+ label: "Command"
1050
+ }
1051
+ };
1052
+ /** Symbol-form aliases (`"XX"`, `"II"`, `"•••"`, …) → canonical echelon key. */
1053
+ const ECHELON_ALIASES = {
1054
+ ø: "team",
1055
+ "•": "squad",
1056
+ "••": "section",
1057
+ "•••": "platoon",
1058
+ i: "company",
1059
+ ii: "battalion",
1060
+ iii: "regiment",
1061
+ x: "brigade",
1062
+ xx: "division",
1063
+ xxx: "corps",
1064
+ xxxx: "army",
1065
+ xxxxx: "army-group",
1066
+ xxxxxx: "region",
1067
+ "++": "command"
1068
+ };
1069
+ /** Enum options for the params panel, derived from {@link ECHELONS}. */
1070
+ const ECHELON_PARAM_OPTIONS = [{
1071
+ label: "None",
1072
+ value: "none"
1073
+ }, ...Object.entries(ECHELONS).filter(([, spec]) => spec.label !== void 0).map(([value, spec]) => ({
1074
+ label: spec.label,
1075
+ value
1076
+ }))];
1077
+ function resolveEchelon(echelon) {
1078
+ const key = echelon.trim().toLowerCase();
1079
+ if (key.length === 0) return void 0;
1080
+ const alias = ECHELON_ALIASES[key];
1081
+ return ECHELONS[key] ?? (alias ? ECHELONS[alias] : void 0);
1082
+ }
1083
+ /**
1084
+ * Builds the echelon glyph centered at `center`, with its baseline along
1085
+ * `along` and `perp` (both unit vectors), `h` meters tall. Returns the glyph's
1086
+ * `strokes` (open polylines), `fills` (closed rings drawn as solid shapes —
1087
+ * the squad/section/platoon dots), and its intrinsic width along the line; the
1088
+ * caller adds the configurable side padding to derive the line gap.
1089
+ */
1090
+ function buildEchelonGlyph(center, along, perp, h, spec) {
1091
+ const P = (a, p) => {
1092
+ const q = vecAdd(vecAdd(center, vecScale(along, a)), vecScale(perp, p));
1093
+ return unproject(q[0], q[1]);
1094
+ };
1095
+ const ring = (a, r) => {
1096
+ const pts = [];
1097
+ const steps = 16;
1098
+ for (let i = 0; i <= steps; i++) {
1099
+ const t = 2 * Math.PI * i / steps;
1100
+ pts.push(P(a + r * Math.cos(t), r * Math.sin(t)));
1101
+ }
1102
+ return pts;
1103
+ };
1104
+ const strokes = [];
1105
+ const fills = [];
1106
+ const half = h / 2;
1107
+ switch (spec.kind) {
1108
+ case "bars": {
1109
+ const spacing = h * .8;
1110
+ const total = (spec.count - 1) * spacing;
1111
+ for (let j = 0; j < spec.count; j++) {
1112
+ const a = -total / 2 + j * spacing;
1113
+ strokes.push([P(a, -half), P(a, half)]);
1114
+ }
1115
+ return {
1116
+ strokes,
1117
+ fills,
1118
+ width: total
1119
+ };
1120
+ }
1121
+ case "crosses": {
1122
+ const w = h;
1123
+ const spacing = h * 1.4;
1124
+ const total = (spec.count - 1) * spacing;
1125
+ for (let j = 0; j < spec.count; j++) {
1126
+ const a0 = -total / 2 + j * spacing;
1127
+ strokes.push([P(a0 - w / 2, -half), P(a0 + w / 2, half)]);
1128
+ strokes.push([P(a0 - w / 2, half), P(a0 + w / 2, -half)]);
1129
+ }
1130
+ return {
1131
+ strokes,
1132
+ fills,
1133
+ width: total + w
1134
+ };
1135
+ }
1136
+ case "dots": {
1137
+ const r = h * .3;
1138
+ const spacing = h * 1.2;
1139
+ const total = (spec.count - 1) * spacing;
1140
+ for (let j = 0; j < spec.count; j++) fills.push(ring(-total / 2 + j * spacing, r));
1141
+ return {
1142
+ strokes,
1143
+ fills,
1144
+ width: total + 2 * r
1145
+ };
1146
+ }
1147
+ case "ring": {
1148
+ const r = h * .6;
1149
+ strokes.push(ring(0, r));
1150
+ strokes.push([P(-h * .8, -h * .4), P(h * .8, h * .4)]);
1151
+ return {
1152
+ strokes,
1153
+ fills,
1154
+ width: h * 1.6
1155
+ };
1156
+ }
1157
+ case "command": {
1158
+ const arm = h * .5;
1159
+ const spacing = h * 1.4;
1160
+ const total = (spec.count - 1) * spacing;
1161
+ for (let j = 0; j < spec.count; j++) {
1162
+ const a0 = -total / 2 + j * spacing;
1163
+ strokes.push([P(a0 - arm, 0), P(a0 + arm, 0)]);
1164
+ strokes.push([P(a0, -half), P(a0, half)]);
1165
+ }
1166
+ return {
1167
+ strokes,
1168
+ fills,
1169
+ width: total + 2 * arm
1170
+ };
1171
+ }
1172
+ }
1173
+ }
1174
+ //#endregion
966
1175
  //#region src/generators/cm15-maneuver-areas/params.ts
1176
+ const AREA_SMOOTH_PARAMS = [{
1177
+ key: "smooth",
1178
+ label: "Smooth",
1179
+ description: "Round the area's corners by curving the boundary through the control points.",
1180
+ type: "boolean"
1181
+ }, {
1182
+ key: "smoothResolution",
1183
+ label: "Smooth resolution",
1184
+ description: "Number of samples per segment when smooth mode is enabled.",
1185
+ type: "number",
1186
+ min: 2,
1187
+ max: 64,
1188
+ step: 1,
1189
+ visibleWhen: (opts) => Boolean(opts.smooth)
1190
+ }];
967
1191
  const ATTACK_SHAFT_PARAMS = [
968
1192
  {
969
1193
  key: "shaftWidthRatio",
@@ -1075,25 +1299,7 @@ const ENCIRCLEMENT_PARAMS = [
1075
1299
  step: .01
1076
1300
  }
1077
1301
  ];
1078
- const FORTIFIED_AREA_PARAMS = [
1079
- {
1080
- key: "smooth",
1081
- label: "Smooth",
1082
- description: "Round the area's corners by curving the boundary through the control points.",
1083
- type: "boolean"
1084
- },
1085
- {
1086
- key: "smoothResolution",
1087
- label: "Smooth resolution",
1088
- description: "Number of samples per segment when smooth mode is enabled.",
1089
- type: "number",
1090
- min: 2,
1091
- max: 64,
1092
- step: 1,
1093
- visibleWhen: (opts) => Boolean(opts.smooth)
1094
- },
1095
- ...FORTIFIED_AREA_SIZE_PARAMS
1096
- ];
1302
+ const FORTIFIED_AREA_PARAMS = [...AREA_SMOOTH_PARAMS, ...FORTIFIED_AREA_SIZE_PARAMS];
1097
1303
  const AREA_LABEL_PADDING_PARAM = {
1098
1304
  key: "labelPadding",
1099
1305
  label: "Label padding",
@@ -1103,22 +1309,53 @@ const AREA_LABEL_PADDING_PARAM = {
1103
1309
  max: 2,
1104
1310
  step: .05
1105
1311
  };
1106
- const LABELED_AREA_PARAMS = [
1312
+ const LABELED_AREA_PARAMS = [...AREA_SMOOTH_PARAMS, AREA_LABEL_PADDING_PARAM];
1313
+ const ECHELON_AREA_PARAMS = [
1314
+ ...AREA_SMOOTH_PARAMS,
1107
1315
  {
1108
- key: "smooth",
1109
- label: "Smooth",
1110
- description: "Round the area's corners by curving the boundary through the control points.",
1111
- type: "boolean"
1316
+ key: "echelon",
1317
+ label: "Echelon",
1318
+ description: "Field B unit-size amplifier, drawn on the boundary.",
1319
+ type: "enum",
1320
+ options: ECHELON_PARAM_OPTIONS
1112
1321
  },
1113
1322
  {
1114
- key: "smoothResolution",
1115
- label: "Smooth resolution",
1116
- description: "Number of samples per segment when smooth mode is enabled.",
1323
+ key: "echelonSizePixels",
1324
+ label: "Echelon size",
1325
+ description: "Echelon glyph height in screen pixels.",
1117
1326
  type: "number",
1118
- min: 2,
1119
- max: 64,
1327
+ min: 8,
1328
+ max: 48,
1120
1329
  step: 1,
1121
- visibleWhen: (opts) => Boolean(opts.smooth)
1330
+ unit: "px"
1331
+ },
1332
+ {
1333
+ key: "echelonSize",
1334
+ label: "Echelon size",
1335
+ description: "Echelon glyph height in meters.",
1336
+ type: "number",
1337
+ min: 50,
1338
+ max: 2e4,
1339
+ step: 50,
1340
+ unit: "m"
1341
+ },
1342
+ {
1343
+ key: "echelonPadding",
1344
+ label: "Echelon padding",
1345
+ description: "Gap on each side of the echelon, as a ratio of its height.",
1346
+ type: "number",
1347
+ min: 0,
1348
+ max: 2,
1349
+ step: .05
1350
+ },
1351
+ {
1352
+ key: "echelonPosition",
1353
+ label: "Echelon position",
1354
+ description: "Slide the echelon along the boundary, as a fraction of the perimeter from the bottom edge.",
1355
+ type: "number",
1356
+ min: -.5,
1357
+ max: .5,
1358
+ step: .01
1122
1359
  },
1123
1360
  AREA_LABEL_PADDING_PARAM
1124
1361
  ];
@@ -1165,6 +1402,16 @@ const AREA_TEXT_AMPLIFIERS = [
1165
1402
  /** {@link AREA_TEXT_AMPLIFIERS} without the N (hostile) field, for measures
1166
1403
  * whose template carries no ENY marker (Fortified Area). */
1167
1404
  const AREA_TEXT_AMPLIFIERS_NO_HOSTILE = AREA_TEXT_AMPLIFIERS.filter((d) => d.key !== "N");
1405
+ /** {@link AREA_TEXT_AMPLIFIERS} without the H (additional information) field,
1406
+ * for measures whose template omits that row but keeps the W/W1 window and
1407
+ * the N (hostile) marker (Submarine Action Area, Submarine-Generated Action
1408
+ * Area). */
1409
+ const AREA_TEXT_AMPLIFIERS_NO_ADDITIONAL_INFO = AREA_TEXT_AMPLIFIERS.filter((d) => d.key !== "H");
1410
+ /** {@link AREA_TEXT_AMPLIFIERS} reduced to just the designation (Field T) and
1411
+ * hostile marker (Field N), for measures whose template carries no
1412
+ * additional-information or date-time-group rows (Area, Drop Zone,
1413
+ * Extraction Zone). */
1414
+ const AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE = AREA_TEXT_AMPLIFIERS.filter((d) => d.key === "T" || d.key === "N");
1168
1415
  //#endregion
1169
1416
  //#region src/generators/cm15-maneuver-areas/airborneAttack.ts
1170
1417
  const DEFAULT_AIRBORNE_ATTACK_OPTIONS = {
@@ -1405,224 +1652,15 @@ function createAmbushSymbol(coordinates, options = {}) {
1405
1652
  type: "MultiLineString",
1406
1653
  coordinates: lines.map((line) => line.map((c) => unproject(c[0], c[1])))
1407
1654
  }
1408
- }]
1409
- };
1410
- }
1411
- const AMBUSH = defineControlMeasure({
1412
- metadata: AMBUSH_METADATA,
1413
- generator: (controlPoints, options) => createAmbushSymbol(asThreePoints(controlPoints), options),
1414
- defaultOptions: DEFAULT_AMBUSH_OPTIONS,
1415
- rule: ambushDrawRule
1416
- });
1417
- //#endregion
1418
- //#region src/internal/echelon.ts
1419
- /**
1420
- * APP-6 echelon (Field B) glyph vocabulary, shared by the control measures that
1421
- * stamp a unit-size amplifier onto their geometry (Boundary, Battle Position).
1422
- *
1423
- * A compact port of the APP-6 echelon designs, modeled on milsymbol but
1424
- * expressed as parametric geometry so it bakes into GeoJSON and strokes
1425
- * monocolor (ADR-0023). Every glyph is built from three primitives — vertical
1426
- * bars, crosses, and dots — plus the team/crew ring and command `++`.
1427
- */
1428
- const ECHELONS = {
1429
- team: {
1430
- kind: "ring",
1431
- count: 1,
1432
- label: "Team/Crew"
1433
- },
1434
- crew: {
1435
- kind: "ring",
1436
- count: 1
1437
- },
1438
- squad: {
1439
- kind: "dots",
1440
- count: 1,
1441
- label: "Squad"
1442
- },
1443
- section: {
1444
- kind: "dots",
1445
- count: 2,
1446
- label: "Section"
1447
- },
1448
- platoon: {
1449
- kind: "dots",
1450
- count: 3,
1451
- label: "Platoon"
1452
- },
1453
- company: {
1454
- kind: "bars",
1455
- count: 1,
1456
- label: "Company"
1457
- },
1458
- battalion: {
1459
- kind: "bars",
1460
- count: 2,
1461
- label: "Battalion"
1462
- },
1463
- regiment: {
1464
- kind: "bars",
1465
- count: 3,
1466
- label: "Regiment"
1467
- },
1468
- brigade: {
1469
- kind: "crosses",
1470
- count: 1,
1471
- label: "Brigade"
1472
- },
1473
- division: {
1474
- kind: "crosses",
1475
- count: 2,
1476
- label: "Division"
1477
- },
1478
- corps: {
1479
- kind: "crosses",
1480
- count: 3,
1481
- label: "Corps"
1482
- },
1483
- army: {
1484
- kind: "crosses",
1485
- count: 4,
1486
- label: "Army"
1487
- },
1488
- "army-group": {
1489
- kind: "crosses",
1490
- count: 5,
1491
- label: "Army Group"
1492
- },
1493
- region: {
1494
- kind: "crosses",
1495
- count: 6,
1496
- label: "Region"
1497
- },
1498
- command: {
1499
- kind: "command",
1500
- count: 2,
1501
- label: "Command"
1502
- }
1503
- };
1504
- /** Symbol-form aliases (`"XX"`, `"II"`, `"•••"`, …) → canonical echelon key. */
1505
- const ECHELON_ALIASES = {
1506
- ø: "team",
1507
- "•": "squad",
1508
- "••": "section",
1509
- "•••": "platoon",
1510
- i: "company",
1511
- ii: "battalion",
1512
- iii: "regiment",
1513
- x: "brigade",
1514
- xx: "division",
1515
- xxx: "corps",
1516
- xxxx: "army",
1517
- xxxxx: "army-group",
1518
- xxxxxx: "region",
1519
- "++": "command"
1520
- };
1521
- /** Enum options for the params panel, derived from {@link ECHELONS}. */
1522
- const ECHELON_PARAM_OPTIONS = [{
1523
- label: "None",
1524
- value: "none"
1525
- }, ...Object.entries(ECHELONS).filter(([, spec]) => spec.label !== void 0).map(([value, spec]) => ({
1526
- label: spec.label,
1527
- value
1528
- }))];
1529
- function resolveEchelon(echelon) {
1530
- const key = echelon.trim().toLowerCase();
1531
- if (key.length === 0) return void 0;
1532
- const alias = ECHELON_ALIASES[key];
1533
- return ECHELONS[key] ?? (alias ? ECHELONS[alias] : void 0);
1534
- }
1535
- /**
1536
- * Builds the echelon glyph centered at `center`, with its baseline along
1537
- * `along` and `perp` (both unit vectors), `h` meters tall. Returns the glyph's
1538
- * `strokes` (open polylines), `fills` (closed rings drawn as solid shapes —
1539
- * the squad/section/platoon dots), and its intrinsic width along the line; the
1540
- * caller adds the configurable side padding to derive the line gap.
1541
- */
1542
- function buildEchelonGlyph(center, along, perp, h, spec) {
1543
- const P = (a, p) => {
1544
- const q = vecAdd(vecAdd(center, vecScale(along, a)), vecScale(perp, p));
1545
- return unproject(q[0], q[1]);
1546
- };
1547
- const ring = (a, r) => {
1548
- const pts = [];
1549
- const steps = 16;
1550
- for (let i = 0; i <= steps; i++) {
1551
- const t = 2 * Math.PI * i / steps;
1552
- pts.push(P(a + r * Math.cos(t), r * Math.sin(t)));
1553
- }
1554
- return pts;
1555
- };
1556
- const strokes = [];
1557
- const fills = [];
1558
- const half = h / 2;
1559
- switch (spec.kind) {
1560
- case "bars": {
1561
- const spacing = h * .8;
1562
- const total = (spec.count - 1) * spacing;
1563
- for (let j = 0; j < spec.count; j++) {
1564
- const a = -total / 2 + j * spacing;
1565
- strokes.push([P(a, -half), P(a, half)]);
1566
- }
1567
- return {
1568
- strokes,
1569
- fills,
1570
- width: total
1571
- };
1572
- }
1573
- case "crosses": {
1574
- const w = h;
1575
- const spacing = h * 1.4;
1576
- const total = (spec.count - 1) * spacing;
1577
- for (let j = 0; j < spec.count; j++) {
1578
- const a0 = -total / 2 + j * spacing;
1579
- strokes.push([P(a0 - w / 2, -half), P(a0 + w / 2, half)]);
1580
- strokes.push([P(a0 - w / 2, half), P(a0 + w / 2, -half)]);
1581
- }
1582
- return {
1583
- strokes,
1584
- fills,
1585
- width: total + w
1586
- };
1587
- }
1588
- case "dots": {
1589
- const r = h * .3;
1590
- const spacing = h * 1.2;
1591
- const total = (spec.count - 1) * spacing;
1592
- for (let j = 0; j < spec.count; j++) fills.push(ring(-total / 2 + j * spacing, r));
1593
- return {
1594
- strokes,
1595
- fills,
1596
- width: total + 2 * r
1597
- };
1598
- }
1599
- case "ring": {
1600
- const r = h * .6;
1601
- strokes.push(ring(0, r));
1602
- strokes.push([P(-h * .8, -h * .4), P(h * .8, h * .4)]);
1603
- return {
1604
- strokes,
1605
- fills,
1606
- width: h * 1.6
1607
- };
1608
- }
1609
- case "command": {
1610
- const arm = h * .5;
1611
- const spacing = h * 1.4;
1612
- const total = (spec.count - 1) * spacing;
1613
- for (let j = 0; j < spec.count; j++) {
1614
- const a0 = -total / 2 + j * spacing;
1615
- strokes.push([P(a0 - arm, 0), P(a0 + arm, 0)]);
1616
- strokes.push([P(a0, -half), P(a0, half)]);
1617
- }
1618
- return {
1619
- strokes,
1620
- fills,
1621
- width: total + 2 * arm
1622
- };
1623
- }
1624
- }
1655
+ }]
1656
+ };
1625
1657
  }
1658
+ const AMBUSH = defineControlMeasure({
1659
+ metadata: AMBUSH_METADATA,
1660
+ generator: (controlPoints, options) => createAmbushSymbol(asThreePoints(controlPoints), options),
1661
+ defaultOptions: DEFAULT_AMBUSH_OPTIONS,
1662
+ rule: ambushDrawRule
1663
+ });
1626
1664
  //#endregion
1627
1665
  //#region src/internal/gapped-line.ts
1628
1666
  /**
@@ -1760,6 +1798,34 @@ function normalizeSmoothResolution$2(value, fallback) {
1760
1798
  if (!Number.isFinite(value)) return fallback;
1761
1799
  return Math.min(64, Math.max(2, Math.round(value)));
1762
1800
  }
1801
+ /**
1802
+ * Shared param descriptors for {@link SmoothLineOptions}, modeled on
1803
+ * `SMOOTH_PATH_PARAMS` (cm99-generic-graphics/params.ts). Measures that add
1804
+ * their own params should spread these at the top of the array.
1805
+ */
1806
+ const SMOOTH_LINE_PARAMS = [{
1807
+ key: "smooth",
1808
+ label: "Smooth",
1809
+ description: "Round the line's corners by curving the path through the control points.",
1810
+ type: "boolean"
1811
+ }, {
1812
+ key: "smoothResolution",
1813
+ label: "Smooth resolution",
1814
+ description: "Number of samples per segment when smooth mode is enabled.",
1815
+ type: "number",
1816
+ min: 2,
1817
+ max: 64,
1818
+ step: 1,
1819
+ visibleWhen: (opts) => Boolean(opts.smooth)
1820
+ }];
1821
+ /**
1822
+ * Applies {@link SmoothLineOptions} to projected vertices `verts`: runs
1823
+ * {@link catmullRom} at the resolved (clamped) sample resolution when `smooth`
1824
+ * is set, else returns `verts` unchanged.
1825
+ */
1826
+ function smoothLineVerts(verts, options, defaultResolution) {
1827
+ return options.smooth ? catmullRom(verts, normalizeSmoothResolution$2(options.smoothResolution ?? defaultResolution, defaultResolution)) : verts;
1828
+ }
1763
1829
  /** One centripetal Catmull-Rom sample at parameter `t` over control points `p0..p3`. */
1764
1830
  function catmullRomAt(p0, p1, p2, p3, t) {
1765
1831
  const t2 = t * t;
@@ -1934,6 +2000,31 @@ function placeEchelon(perimeter, h, echelon, echelonPadding, echelonPosition, ec
1934
2000
  gaps
1935
2001
  };
1936
2002
  }
2003
+ /**
2004
+ * Factory for the `freezeOrientation` hook (ADR-0023) shared by every
2005
+ * echelon-carrying closed-area measure (Battle Position, Strong Point, Base
2006
+ * Camp, Guerrilla Base): pins the echelon anchor to its current bottom-edge
2007
+ * fraction so a subsequent rigid rotation of `controlPoints` (e.g. the
2008
+ * transform box's rotate grip) does not re-derive a different "bottom edge"
2009
+ * against the rotated geometry. `defaults` supplies the measure's own
2010
+ * `echelon`/`smooth`/`smoothResolution` defaults, since each measure's
2011
+ * `DEFAULT_*_OPTIONS` differ (e.g. Battle Position's `echelon: "battalion"`
2012
+ * vs. Base Camp/Guerrilla Base's `echelon: "none"`).
2013
+ *
2014
+ * The returned hook returns `undefined` — nothing to freeze — when:
2015
+ * - `echelonAnchor` is already set (previously frozen or user-authored), or
2016
+ * - the echelon resolves to none (nothing visible to pin), or
2017
+ * - the ring is degenerate (no segments, or a near-zero perimeter).
2018
+ */
2019
+ function makeFreezeEchelonAnchor(defaults) {
2020
+ return function freezeEchelonAnchor(controlPoints, options) {
2021
+ if (options.echelonAnchor !== void 0) return void 0;
2022
+ if (!resolveEchelon(options.echelon ?? defaults.echelon)) return void 0;
2023
+ const perimeter = buildAreaPerimeter(buildClosedRing(controlPoints, options.smooth ?? defaults.smooth, options.smoothResolution ?? defaults.smoothResolution, defaults.smoothResolution));
2024
+ if (perimeter.segs.length === 0 || perimeter.totalLength < 1e-6) return void 0;
2025
+ return { echelonAnchor: perimeter.anchorTarget / perimeter.totalLength };
2026
+ };
2027
+ }
1937
2028
  //#endregion
1938
2029
  //#region src/internal/angle-utils.ts
1939
2030
  /**
@@ -2095,16 +2186,17 @@ function endTangents(verts) {
2095
2186
  };
2096
2187
  }
2097
2188
  /**
2098
- * Resolves {@link LabelSizeOptions} into the `textSize*` properties adapters
2099
- * expect on a label feature, omitting any key whose input was not supplied.
2189
+ * Resolves {@link LabelSizeOptions} into the single text-size property the
2190
+ * label feature carries (ADR-0028): `textSizePixels` when screen-anchored
2191
+ * (`labelSizePixels` present — pixels win over a stray `labelSize`),
2192
+ * `textSizeMeters` when ground-anchored (`labelSize` alone), or neither, in
2193
+ * which case adapters render their engine default, screen-fixed.
2100
2194
  */
2101
2195
  function labelSizeProps(options) {
2102
- const { labelSizePixels, labelSizeZoom, labelSizeResolution } = options;
2103
- const props = {};
2104
- if (labelSizePixels !== void 0) props.textSizePixels = labelSizePixels;
2105
- if (labelSizeZoom !== void 0) props.textSizeZoom = labelSizeZoom;
2106
- if (labelSizeResolution !== void 0) props.textSizeResolution = labelSizeResolution;
2107
- return props;
2196
+ const { labelSize, labelSizePixels } = options;
2197
+ if (labelSizePixels !== void 0) return { textSizePixels: labelSizePixels };
2198
+ if (labelSize !== void 0) return { textSizeMeters: labelSize };
2199
+ return {};
2108
2200
  }
2109
2201
  /** Pushes a `part: "label"` Point feature at projected `point`. */
2110
2202
  function pushLabel(out, point, text, rotation, sizeProps, textAnchor) {
@@ -2123,20 +2215,21 @@ function pushLabel(out, point, text, rotation, sizeProps, textAnchor) {
2123
2215
  }
2124
2216
  });
2125
2217
  }
2126
- /** Ratio of resolved label height (`labelSizePixels × labelSizeResolution`) used as end-label clearance. */
2218
+ /** Ratio of resolved label height (`labelSize`, or `labelSizePixels × metersPerPixel`) used as end-label clearance. */
2127
2219
  const END_LABEL_CLEARANCE_RATIO = .3;
2128
2220
  /** Ratio of resolved label height used to inset "above" end labels from the tip. */
2129
2221
  const ABOVE_LABEL_INSET_RATIO = .3;
2130
2222
  /** Ratio of resolved label height used as the "above" end labels' perpendicular offset. */
2131
2223
  const ABOVE_LABEL_OFFSET_RATIO = .6;
2132
2224
  function textHeight(options) {
2133
- const { labelSizePixels, labelSizeResolution } = options;
2134
- if (labelSizePixels !== void 0 && labelSizeResolution !== void 0) return labelSizePixels * labelSizeResolution;
2225
+ const { labelSize, labelSizePixels, metersPerPixel } = options;
2226
+ if (labelSizePixels !== void 0) return metersPerPixel !== void 0 && metersPerPixel > 0 ? labelSizePixels * metersPerPixel : void 0;
2227
+ return labelSize;
2135
2228
  }
2136
2229
  /**
2137
- * Ground-anchored label offset in meters: `ratio` × the label's resolved height
2138
- * (`labelSizePixels × labelSizeResolution`), or `fallbackMeters` when no live
2139
- * size was captured (e.g. no map resolution yet). The fallback is small
2230
+ * Label offset in meters: `ratio` × the label's resolved height (`labelSize`
2231
+ * meters, or `labelSizePixels × metersPerPixel` when screen-anchored), or
2232
+ * `fallbackMeters` when no size resolves (e.g. no map resolution yet). The fallback is small
2140
2233
  * relative to typical control measure spans — the same order of magnitude as
2141
2234
  * Boundary's own smallest label offsets — so a fixed label still reads as "just
2142
2235
  * clear of the line" rather than crowding it or floating away. Callers supply
@@ -2230,16 +2323,20 @@ function pushEndLabelsAbove(out, verts, textForEnd, options = {}) {
2230
2323
  * as a complete FeatureCollection.
2231
2324
  */
2232
2325
  function fixedLabelLineFeatures(positions, options, textForEnd) {
2233
- const { part, placement = "beyond", phaseLineName, includePrefix = true, ...sizeOptions } = options;
2326
+ const { part, placement = "beyond", phaseLineName, includePrefix = true, smooth, smoothResolution, ...sizeOptions } = options;
2327
+ const verts = smoothLineVerts(positions.map((p) => project(p[0], p[1])), {
2328
+ smooth,
2329
+ smoothResolution
2330
+ }, 12);
2331
+ const lineCoordinates = smooth ? verts.map((v) => unproject(v[0], v[1])) : positions;
2234
2332
  const features = [{
2235
2333
  type: "Feature",
2236
2334
  properties: { part },
2237
2335
  geometry: {
2238
2336
  type: "LineString",
2239
- coordinates: positions
2337
+ coordinates: lineCoordinates
2240
2338
  }
2241
2339
  }];
2242
- const verts = positions.map((p) => project(p[0], p[1]));
2243
2340
  const labelFeatures = [];
2244
2341
  if (placement === "above") pushEndLabelsAbove(labelFeatures, verts, textForEnd, sizeOptions);
2245
2342
  else pushEndLabels(labelFeatures, verts, textForEnd, sizeOptions);
@@ -2254,7 +2351,9 @@ function fixedLabelLineFeatures(positions, options, textForEnd) {
2254
2351
  //#endregion
2255
2352
  //#region src/internal/area-labels.ts
2256
2353
  /** Stored label rotation for horizontal (screen-upright) area text — adapters
2257
- * render feature rotation as `π − rotation`, so `π` renders as `0`. */
2354
+ * render feature rotation as `π − rotation`, so `π` renders as `0`. Exported
2355
+ * for reuse by measures that place a horizontal label outside the area's
2356
+ * boundary (e.g. Airfield Zone's Field H). */
2258
2357
  const HORIZONTAL_LABEL_ROTATION = Math.PI;
2259
2358
  /** Row pitch, as a ratio of the resolved label height, between stacked
2260
2359
  * name/H/W-W1 rows (see {@link pushAreaLabels}). */
@@ -2268,7 +2367,7 @@ const ENY_CLEARANCE_RATIO = .3;
2268
2367
  * for sizing the boundary knockout gap — no live text-measurement is
2269
2368
  * available at generator time. */
2270
2369
  const ENY_CHAR_ASPECT_RATIO = .6;
2271
- const DEFAULT_SMOOTH_RESOLUTION$7 = 16;
2370
+ const DEFAULT_SMOOTH_RESOLUTION$9 = 16;
2272
2371
  /**
2273
2372
  * Composes an area's main label text from a `prefix` (e.g. `"AO"`) and a
2274
2373
  * `designation` (e.g. `"GREEN"`), mirroring Phase Line's "PL" precedent: a
@@ -2305,7 +2404,8 @@ function composeAreaLabelTexts(textAmplifiers, prefix = "", separator = " ") {
2305
2404
  * divide by zero). For a **concave** ring the area centroid can fall outside
2306
2405
  * the shape; that limitation is accepted here, matching the doctrinal
2307
2406
  * template's plain "center of the area" placement rather than a
2308
- * point-in-polygon-safe visual center.
2407
+ * point-in-polygon-safe visual center. Exported for reuse by measures that
2408
+ * center a non-label glyph on the area (e.g. Airfield Zone's runway glyph).
2309
2409
  */
2310
2410
  function ringCentroid(verts) {
2311
2411
  const n = verts.length - 1;
@@ -2479,8 +2579,8 @@ function boundaryFeature(verts, gaps) {
2479
2579
  * common (no `N`) case is unaffected.
2480
2580
  */
2481
2581
  function labeledAreaFeatures(positions, options, texts) {
2482
- const { smooth = false, smoothResolution = DEFAULT_SMOOTH_RESOLUTION$7, ...labelOptions } = options;
2483
- const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$7);
2582
+ const { smooth = false, smoothResolution = DEFAULT_SMOOTH_RESOLUTION$9, ...labelOptions } = options;
2583
+ const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$9);
2484
2584
  const labelFeatures = [];
2485
2585
  const features = [boundaryFeature(verts, pushAreaLabels(labelFeatures, verts, texts, labelOptions))];
2486
2586
  features.push(...labelFeatures);
@@ -2490,44 +2590,492 @@ function labeledAreaFeatures(positions, options, texts) {
2490
2590
  };
2491
2591
  }
2492
2592
  //#endregion
2493
- //#region src/generators/cm12-command-control-areas/area-of-operations.ts
2494
- /** Default options for the Area of operations control measure. */
2495
- const DEFAULT_AREA_OF_OPERATIONS_OPTIONS = {
2496
- smooth: false,
2497
- smoothResolution: 16
2593
+ //#region src/generators/cm15-maneuver-areas/area.ts
2594
+ /** Default options for the Area control measure. */
2595
+ const DEFAULT_AREA_OPTIONS = {
2596
+ smooth: false,
2597
+ smoothResolution: 16
2598
+ };
2599
+ const AREA_METADATA = {
2600
+ id: "area",
2601
+ name: "Area",
2602
+ description: "A generic maneuver area enclosed by a boundary.",
2603
+ entity: "Maneuver Areas",
2604
+ entityType: "Area",
2605
+ value: "150100",
2606
+ minCoordinates: 3,
2607
+ geometry: "area",
2608
+ geometryTypes: [
2609
+ "Polygon",
2610
+ "MultiLineString",
2611
+ "Point"
2612
+ ],
2613
+ drawRule: "Area1",
2614
+ capturesLabelSize: true,
2615
+ params: LABELED_AREA_PARAMS,
2616
+ textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
2617
+ };
2618
+ /**
2619
+ * Creates an Area control measure: a closed area through `positions` labeled
2620
+ * at its center with an un-prefixed `T` (Field T) plus an `N` (Field N) ENY
2621
+ * marker, per {@link labeledAreaFeatures}.
2622
+ */
2623
+ function createArea(positions, options = {}, textAmplifiers = {}) {
2624
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, ""));
2625
+ }
2626
+ const AREA = defineControlMeasure({
2627
+ metadata: AREA_METADATA,
2628
+ generator: createArea,
2629
+ defaultOptions: DEFAULT_AREA_OPTIONS,
2630
+ rule: area1DrawRule,
2631
+ previewSample: {
2632
+ controlPoints: [
2633
+ [-1, -.5],
2634
+ [1, -.5],
2635
+ [1, .5],
2636
+ [-1, .5]
2637
+ ],
2638
+ textAmplifiers: {},
2639
+ options: { labelSize: 96 }
2640
+ }
2641
+ });
2642
+ //#endregion
2643
+ //#region src/generators/cm12-command-control-areas/area-of-operations.ts
2644
+ /** Default options for the Area of operations control measure. */
2645
+ const DEFAULT_AREA_OF_OPERATIONS_OPTIONS = {
2646
+ smooth: false,
2647
+ smoothResolution: 16
2648
+ };
2649
+ const AREA_OF_OPERATIONS_METADATA = {
2650
+ id: "area-of-operations",
2651
+ name: "Area of operations",
2652
+ description: "An operational area defined by a commander for land and naval forces, within which the assigned force conducts military operations.",
2653
+ entity: "Command and Control Areas",
2654
+ entityType: "Area of Operations",
2655
+ value: "120100",
2656
+ minCoordinates: 3,
2657
+ geometry: "area",
2658
+ geometryTypes: [
2659
+ "Polygon",
2660
+ "MultiLineString",
2661
+ "Point"
2662
+ ],
2663
+ drawRule: "Area1",
2664
+ capturesLabelSize: true,
2665
+ params: LABELED_AREA_PARAMS,
2666
+ textAmplifiers: AREA_TEXT_AMPLIFIERS
2667
+ };
2668
+ /**
2669
+ * Creates an Area of operations control measure: a closed area through
2670
+ * `positions` labeled at its center with "AO "-prefixed `T` (Field T) plus
2671
+ * `H`/`W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
2672
+ */
2673
+ function createAreaOfOperations(positions, options = {}, textAmplifiers = {}) {
2674
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "AO"));
2675
+ }
2676
+ const AREA_OF_OPERATIONS = defineControlMeasure({
2677
+ metadata: AREA_OF_OPERATIONS_METADATA,
2678
+ generator: createAreaOfOperations,
2679
+ defaultOptions: DEFAULT_AREA_OF_OPERATIONS_OPTIONS,
2680
+ rule: area1DrawRule,
2681
+ previewSample: {
2682
+ controlPoints: [
2683
+ [-1, -.5],
2684
+ [1, -.5],
2685
+ [1, .5],
2686
+ [-1, .5]
2687
+ ],
2688
+ textAmplifiers: { T: "GREEN" },
2689
+ options: { labelSize: 96 }
2690
+ }
2691
+ });
2692
+ //#endregion
2693
+ //#region src/generators/cm12-command-control-areas/params.ts
2694
+ const SMOOTH_AREA_PARAMS = AREA_SMOOTH_PARAMS;
2695
+ const AREA_DESIGNATION_ONLY_AMPLIFIER = {
2696
+ key: "T",
2697
+ label: "Unique designation",
2698
+ description: "Field T — the area's unique designation.",
2699
+ maxLength: 21
2700
+ };
2701
+ //#endregion
2702
+ //#region src/generators/cm12-command-control-areas/named-area-of-interest.ts
2703
+ /**
2704
+ * Default options for the Named area of interest control measure. The
2705
+ * standard's example renders a straight-sided polygon, so `smooth` defaults
2706
+ * off (unlike the rounded L-15 templates) while staying available.
2707
+ */
2708
+ const DEFAULT_NAMED_AREA_OF_INTEREST_OPTIONS = {
2709
+ smooth: false,
2710
+ smoothResolution: 16
2711
+ };
2712
+ const NAMED_AREA_OF_INTEREST_METADATA = {
2713
+ id: "named-area-of-interest",
2714
+ name: "Named area of interest",
2715
+ description: "A geographical area where information is gathered to satisfy specific intelligence requirements.",
2716
+ entity: "Command and Control Areas",
2717
+ entityType: "Named Area of Interest",
2718
+ value: "120200",
2719
+ minCoordinates: 3,
2720
+ geometry: "area",
2721
+ geometryTypes: ["Polygon", "Point"],
2722
+ drawRule: "Area1",
2723
+ capturesLabelSize: true,
2724
+ params: SMOOTH_AREA_PARAMS,
2725
+ textAmplifiers: [AREA_DESIGNATION_ONLY_AMPLIFIER]
2726
+ };
2727
+ /**
2728
+ * Creates a Named area of interest control measure: a closed area through
2729
+ * `positions` labeled at its center with "NAI "-prefixed `T` (Field T) —
2730
+ * the sole label row, per {@link labeledAreaFeatures}.
2731
+ */
2732
+ function createNamedAreaOfInterest(positions, options = {}, textAmplifiers = {}) {
2733
+ return labeledAreaFeatures(positions, options, { name: composeAreaName("NAI", textAmplifiers.T) });
2734
+ }
2735
+ const NAMED_AREA_OF_INTEREST = defineControlMeasure({
2736
+ metadata: NAMED_AREA_OF_INTEREST_METADATA,
2737
+ generator: createNamedAreaOfInterest,
2738
+ defaultOptions: DEFAULT_NAMED_AREA_OF_INTEREST_OPTIONS,
2739
+ rule: area1DrawRule,
2740
+ previewSample: {
2741
+ controlPoints: [
2742
+ [-1, -.5],
2743
+ [1, -.5],
2744
+ [1, .5],
2745
+ [-1, .5]
2746
+ ],
2747
+ textAmplifiers: { T: "1" },
2748
+ options: { labelSize: 96 }
2749
+ }
2750
+ });
2751
+ //#endregion
2752
+ //#region src/generators/cm12-command-control-areas/target-area-of-interest.ts
2753
+ /**
2754
+ * Default options for the Target area of interest control measure. The
2755
+ * standard's example renders a straight-sided polygon, so `smooth` defaults
2756
+ * off (unlike the rounded L-15 templates) while staying available.
2757
+ */
2758
+ const DEFAULT_TARGET_AREA_OF_INTEREST_OPTIONS = {
2759
+ smooth: false,
2760
+ smoothResolution: 16
2761
+ };
2762
+ const TARGET_AREA_OF_INTEREST_METADATA = {
2763
+ id: "target-area-of-interest",
2764
+ name: "Target area of interest",
2765
+ description: "The geographical area where high-value targets can be acquired and engaged by friendly forces.",
2766
+ entity: "Command and Control Areas",
2767
+ entityType: "Target Area of Interest",
2768
+ value: "120300",
2769
+ minCoordinates: 3,
2770
+ geometry: "area",
2771
+ geometryTypes: ["Polygon", "Point"],
2772
+ drawRule: "Area1",
2773
+ capturesLabelSize: true,
2774
+ params: SMOOTH_AREA_PARAMS,
2775
+ textAmplifiers: [AREA_DESIGNATION_ONLY_AMPLIFIER]
2776
+ };
2777
+ /**
2778
+ * Creates a Target area of interest control measure: a closed area through
2779
+ * `positions` labeled at its center with "TAI "-prefixed `T` (Field T) —
2780
+ * the sole label row, per {@link labeledAreaFeatures}.
2781
+ */
2782
+ function createTargetAreaOfInterest(positions, options = {}, textAmplifiers = {}) {
2783
+ return labeledAreaFeatures(positions, options, { name: composeAreaName("TAI", textAmplifiers.T) });
2784
+ }
2785
+ const TARGET_AREA_OF_INTEREST = defineControlMeasure({
2786
+ metadata: TARGET_AREA_OF_INTEREST_METADATA,
2787
+ generator: createTargetAreaOfInterest,
2788
+ defaultOptions: DEFAULT_TARGET_AREA_OF_INTEREST_OPTIONS,
2789
+ rule: area1DrawRule,
2790
+ previewSample: {
2791
+ controlPoints: [
2792
+ [-1, -.5],
2793
+ [1, -.5],
2794
+ [1, .5],
2795
+ [-1, .5]
2796
+ ],
2797
+ textAmplifiers: { T: "YUKON" },
2798
+ options: { labelSize: 96 }
2799
+ }
2800
+ });
2801
+ //#endregion
2802
+ //#region src/generators/cm12-command-control-areas/airfield-zone.ts
2803
+ const DEFAULT_SMOOTH_RESOLUTION$8 = 16;
2804
+ /** Long runway line's tilt off the horizontal, in radians (+15°). */
2805
+ const LONG_LINE_ANGLE = 15 * Math.PI / 180;
2806
+ /** Short runway line's tilt off the horizontal, in radians (+50°). */
2807
+ const SHORT_LINE_ANGLE = 50 * Math.PI / 180;
2808
+ /** Short line's half-length as a ratio of the long line's half-length. */
2809
+ const SHORT_LINE_RATIO = .55;
2810
+ /** Field H's offset east of the area's max-x edge, as a ratio of the resolved label offset. */
2811
+ const H_LABEL_OFFSET_RATIO = .75;
2812
+ /**
2813
+ * Default options for the Airfield zone control measure. The standard's
2814
+ * example is a rounded blob, so `smooth` defaults on.
2815
+ */
2816
+ const DEFAULT_AIRFIELD_ZONE_OPTIONS = {
2817
+ smooth: true,
2818
+ smoothResolution: DEFAULT_SMOOTH_RESOLUTION$8,
2819
+ glyphSizeRatio: .55
2820
+ };
2821
+ const AIRFIELD_ZONE_METADATA = {
2822
+ id: "airfield-zone",
2823
+ name: "Airfield zone",
2824
+ description: "A zone enclosing an airfield, marked with a runway glyph; Field H carries the airfield type, runway length and other pertinent information.",
2825
+ entity: "Command and Control Areas",
2826
+ entityType: "Airfield Zone",
2827
+ value: "120400",
2828
+ minCoordinates: 3,
2829
+ geometry: "area",
2830
+ geometryTypes: [
2831
+ "Polygon",
2832
+ "MultiLineString",
2833
+ "Point"
2834
+ ],
2835
+ drawRule: "Area1",
2836
+ capturesLabelSize: true,
2837
+ params: [...SMOOTH_AREA_PARAMS, {
2838
+ key: "glyphSizeRatio",
2839
+ label: "Runway size",
2840
+ description: "Size of the runway glyph as a ratio of the area's width.",
2841
+ type: "number",
2842
+ min: .2,
2843
+ max: .9,
2844
+ step: .05
2845
+ }],
2846
+ textAmplifiers: [{
2847
+ key: "H",
2848
+ label: "Additional information",
2849
+ description: "Field H — airfield type, runway length and other information, placed outside the area's east edge.",
2850
+ maxLength: 20
2851
+ }]
2852
+ };
2853
+ /**
2854
+ * Creates an Airfield zone control measure: a closed area through
2855
+ * `positions` with a runway glyph (two crossed lines forming a shallow
2856
+ * asymmetric X) centered on the ring, and Field H placed outside the area to
2857
+ * the east when set.
2858
+ *
2859
+ * @param positions - Area anchor points (≥3); the input contract is enforced
2860
+ * at the render seam (ADR-0014).
2861
+ * @param options - {@link AirfieldZoneOptions}.
2862
+ * @param textAmplifiers - Field H only.
2863
+ */
2864
+ function createAirfieldZone(positions, options = {}, textAmplifiers = {}) {
2865
+ const { smooth = DEFAULT_AIRFIELD_ZONE_OPTIONS.smooth, smoothResolution = DEFAULT_AIRFIELD_ZONE_OPTIONS.smoothResolution, glyphSizeRatio = DEFAULT_AIRFIELD_ZONE_OPTIONS.glyphSizeRatio, ...labelOptions } = options;
2866
+ const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$8);
2867
+ const features = [boundaryFeature(verts, [])];
2868
+ let minX = Infinity;
2869
+ let maxX = -Infinity;
2870
+ for (const [x] of verts) {
2871
+ if (x < minX) minX = x;
2872
+ if (x > maxX) maxX = x;
2873
+ }
2874
+ const bboxWidth = maxX - minX;
2875
+ const [cx, cy] = ringCentroid(verts);
2876
+ if (bboxWidth >= 1e-6) {
2877
+ const longHalf = .5 * glyphSizeRatio * bboxWidth;
2878
+ const shortHalf = longHalf * SHORT_LINE_RATIO;
2879
+ const center = [cx, cy];
2880
+ const longDir = [Math.cos(LONG_LINE_ANGLE), Math.sin(LONG_LINE_ANGLE)];
2881
+ const shortDir = [Math.cos(SHORT_LINE_ANGLE), Math.sin(SHORT_LINE_ANGLE)];
2882
+ const along = (dir, half) => [[center[0] - dir[0] * half, center[1] - dir[1] * half], [center[0] + dir[0] * half, center[1] + dir[1] * half]];
2883
+ const [longA, longB] = along(longDir, longHalf);
2884
+ const [shortA, shortB] = along(shortDir, shortHalf);
2885
+ features.push({
2886
+ type: "Feature",
2887
+ properties: {},
2888
+ geometry: {
2889
+ type: "MultiLineString",
2890
+ coordinates: [[unproject(longA[0], longA[1]), unproject(longB[0], longB[1])], [unproject(shortA[0], shortA[1]), unproject(shortB[0], shortB[1])]]
2891
+ }
2892
+ });
2893
+ }
2894
+ if (textAmplifiers.H) {
2895
+ const labelFeatures = [];
2896
+ pushLabel(labelFeatures, [maxX + H_LABEL_OFFSET_RATIO * resolveLabelOffsetMeters(labelOptions, 1), cy], textAmplifiers.H, HORIZONTAL_LABEL_ROTATION, labelSizeProps(labelOptions), "start");
2897
+ features.push(...labelFeatures);
2898
+ }
2899
+ return {
2900
+ type: "FeatureCollection",
2901
+ features
2902
+ };
2903
+ }
2904
+ const AIRFIELD_ZONE = defineControlMeasure({
2905
+ metadata: AIRFIELD_ZONE_METADATA,
2906
+ generator: createAirfieldZone,
2907
+ defaultOptions: DEFAULT_AIRFIELD_ZONE_OPTIONS,
2908
+ rule: area1DrawRule,
2909
+ previewSample: {
2910
+ controlPoints: [
2911
+ [-1, -.5],
2912
+ [1, -.5],
2913
+ [1, .5],
2914
+ [-1, .5]
2915
+ ],
2916
+ textAmplifiers: { H: "570M" },
2917
+ options: { labelSize: 96 }
2918
+ }
2919
+ });
2920
+ //#endregion
2921
+ //#region src/generators/cm12-command-control-areas/echelon-labeled-area.ts
2922
+ const DEFAULT_SMOOTH_RESOLUTION$7 = 16;
2923
+ /** Default options shared by Base Camp and Guerrilla Base — echelon off, boundary rounded. */
2924
+ const DEFAULT_ECHELON_LABELED_AREA_OPTIONS = {
2925
+ echelon: "none",
2926
+ echelonSize: 750,
2927
+ echelonSizePixels: 16,
2928
+ echelonPadding: .3,
2929
+ echelonPosition: 0,
2930
+ smooth: true,
2931
+ smoothResolution: DEFAULT_SMOOTH_RESOLUTION$7
2932
+ };
2933
+ /**
2934
+ * Builds the shared Base Camp / Guerrilla Base geometry: a closed area
2935
+ * through `positions` carrying the full L-15 text amplifier template
2936
+ * ({@link pushAreaLabels}) plus an optional Field B echelon glyph straddling
2937
+ * the boundary ({@link placeEchelon}, mirroring Battle Position). The
2938
+ * echelon's masking gap and any Field N "ENY" masking gap are concatenated
2939
+ * before carving the boundary, so either (or both) opens it into a gapped
2940
+ * `MultiLineString`; with neither, the boundary is a closed `Polygon`.
2941
+ */
2942
+ function echelonLabeledAreaFeatures(positions, options, texts) {
2943
+ const { echelon = DEFAULT_ECHELON_LABELED_AREA_OPTIONS.echelon, echelonSize = DEFAULT_ECHELON_LABELED_AREA_OPTIONS.echelonSize, echelonSizePixels, echelonPadding = DEFAULT_ECHELON_LABELED_AREA_OPTIONS.echelonPadding, echelonPosition = DEFAULT_ECHELON_LABELED_AREA_OPTIONS.echelonPosition, echelonAnchor, metersPerPixel, smooth = DEFAULT_ECHELON_LABELED_AREA_OPTIONS.smooth, smoothResolution = DEFAULT_ECHELON_LABELED_AREA_OPTIONS.smoothResolution, ...labelOptions } = options;
2944
+ const h = resolveEchelonHeight(echelonSize, echelonSizePixels, metersPerPixel);
2945
+ const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$7);
2946
+ const { strokes, fills, gaps: echelonGaps } = placeEchelon(buildAreaPerimeter(verts), h, echelon, echelonPadding, echelonPosition, echelonAnchor);
2947
+ const labelFeatures = [];
2948
+ const enyGaps = pushAreaLabels(labelFeatures, verts, texts, labelOptions);
2949
+ const features = [boundaryFeature(verts, [...echelonGaps, ...enyGaps])];
2950
+ if (strokes.length > 0) features.push({
2951
+ type: "Feature",
2952
+ properties: { part: "echelon" },
2953
+ geometry: {
2954
+ type: "MultiLineString",
2955
+ coordinates: strokes
2956
+ }
2957
+ });
2958
+ if (fills.length > 0) features.push({
2959
+ type: "Feature",
2960
+ properties: {
2961
+ part: "echelon",
2962
+ fill: true
2963
+ },
2964
+ geometry: {
2965
+ type: "MultiPolygon",
2966
+ coordinates: fills.map((ring) => [ring])
2967
+ }
2968
+ });
2969
+ features.push(...labelFeatures);
2970
+ return {
2971
+ type: "FeatureCollection",
2972
+ features
2973
+ };
2974
+ }
2975
+ /**
2976
+ * `freezeOrientation` hook factory instance shared by Base Camp and
2977
+ * Guerrilla Base — see {@link makeFreezeEchelonAnchor} (ADR-0023) for the
2978
+ * mechanism; both measures default to `echelon: "none"`, so unlike Battle
2979
+ * Position's own instance (battlePosition.ts) freezing is a no-op until a
2980
+ * user explicitly picks an echelon.
2981
+ */
2982
+ const freezeEchelonLabeledAreaAnchor = makeFreezeEchelonAnchor({
2983
+ echelon: DEFAULT_ECHELON_LABELED_AREA_OPTIONS.echelon,
2984
+ smooth: DEFAULT_ECHELON_LABELED_AREA_OPTIONS.smooth,
2985
+ smoothResolution: DEFAULT_ECHELON_LABELED_AREA_OPTIONS.smoothResolution
2986
+ });
2987
+ //#endregion
2988
+ //#region src/generators/cm12-command-control-areas/base-camp.ts
2989
+ /** Default options for the Base camp control measure. */
2990
+ const DEFAULT_BASE_CAMP_OPTIONS = DEFAULT_ECHELON_LABELED_AREA_OPTIONS;
2991
+ const BASE_CAMP_METADATA = {
2992
+ id: "base-camp",
2993
+ name: "Base camp",
2994
+ description: "An evolving military facility that supports military operations of a deployed unit and provides the necessary support and services for sustained operations.",
2995
+ entity: "Command and Control Areas",
2996
+ entityType: "Base Camp",
2997
+ value: "120500",
2998
+ minCoordinates: 3,
2999
+ geometry: "area",
3000
+ geometryTypes: [
3001
+ "Polygon",
3002
+ "MultiLineString",
3003
+ "MultiPolygon",
3004
+ "Point"
3005
+ ],
3006
+ drawRule: "Area1",
3007
+ capturesLabelSize: true,
3008
+ params: ECHELON_AREA_PARAMS,
3009
+ textAmplifiers: AREA_TEXT_AMPLIFIERS
2498
3010
  };
2499
- const AREA_OF_OPERATIONS_METADATA = {
2500
- id: "area-of-operations",
2501
- name: "Area of operations",
2502
- description: "An operational area defined by a commander for land and naval forces, within which the assigned force conducts military operations.",
3011
+ /**
3012
+ * Creates a Base camp control measure: a closed area through `positions`
3013
+ * labeled at its center with "BC "-prefixed `T` (Field T) plus `H`/`W`/`W1`/`N`
3014
+ * amplifier rows, and an optional Field B echelon glyph straddling the
3015
+ * boundary, per {@link echelonLabeledAreaFeatures}.
3016
+ */
3017
+ function createBaseCamp(positions, options = {}, textAmplifiers = {}) {
3018
+ return echelonLabeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "BC"));
3019
+ }
3020
+ const BASE_CAMP = defineControlMeasure({
3021
+ metadata: BASE_CAMP_METADATA,
3022
+ generator: createBaseCamp,
3023
+ defaultOptions: DEFAULT_BASE_CAMP_OPTIONS,
3024
+ rule: area1DrawRule,
3025
+ freezeOrientation: freezeEchelonLabeledAreaAnchor,
3026
+ previewSample: {
3027
+ controlPoints: [
3028
+ [-1, -.5],
3029
+ [1, -.5],
3030
+ [1, .5],
3031
+ [-1, .5]
3032
+ ],
3033
+ textAmplifiers: { T: "LEATHERNECK" },
3034
+ options: {
3035
+ smooth: true,
3036
+ labelSize: 96
3037
+ }
3038
+ }
3039
+ });
3040
+ //#endregion
3041
+ //#region src/generators/cm12-command-control-areas/guerrilla-base.ts
3042
+ /** Default options for the Guerrilla base control measure. */
3043
+ const DEFAULT_GUERRILLA_BASE_OPTIONS = DEFAULT_ECHELON_LABELED_AREA_OPTIONS;
3044
+ const GUERRILLA_BASE_METADATA = {
3045
+ id: "guerrilla-base",
3046
+ name: "Guerrilla base",
3047
+ description: "A temporary site where guerrilla installations, headquarters, and units are located.",
2503
3048
  entity: "Command and Control Areas",
2504
- entityType: "Area of Operations",
2505
- value: "120100",
3049
+ entityType: "Guerrilla Base",
3050
+ value: "120600",
2506
3051
  minCoordinates: 3,
2507
3052
  geometry: "area",
2508
3053
  geometryTypes: [
2509
3054
  "Polygon",
2510
3055
  "MultiLineString",
3056
+ "MultiPolygon",
2511
3057
  "Point"
2512
3058
  ],
2513
3059
  drawRule: "Area1",
2514
3060
  capturesLabelSize: true,
2515
- params: LABELED_AREA_PARAMS,
3061
+ params: ECHELON_AREA_PARAMS,
2516
3062
  textAmplifiers: AREA_TEXT_AMPLIFIERS
2517
3063
  };
2518
3064
  /**
2519
- * Creates an Area of operations control measure: a closed area through
2520
- * `positions` labeled at its center with "AO "-prefixed `T` (Field T) plus
2521
- * `H`/`W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
3065
+ * Creates a Guerrilla base control measure: a closed area through
3066
+ * `positions` labeled at its center with "GB "-prefixed `T` (Field T) plus
3067
+ * `H`/`W`/`W1`/`N` amplifier rows, and an optional Field B echelon glyph
3068
+ * straddling the boundary, per {@link echelonLabeledAreaFeatures}.
2522
3069
  */
2523
- function createAreaOfOperations(positions, options = {}, textAmplifiers = {}) {
2524
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "AO"));
3070
+ function createGuerrillaBase(positions, options = {}, textAmplifiers = {}) {
3071
+ return echelonLabeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "GB"));
2525
3072
  }
2526
- const AREA_OF_OPERATIONS = defineControlMeasure({
2527
- metadata: AREA_OF_OPERATIONS_METADATA,
2528
- generator: createAreaOfOperations,
2529
- defaultOptions: DEFAULT_AREA_OF_OPERATIONS_OPTIONS,
3073
+ const GUERRILLA_BASE = defineControlMeasure({
3074
+ metadata: GUERRILLA_BASE_METADATA,
3075
+ generator: createGuerrillaBase,
3076
+ defaultOptions: DEFAULT_GUERRILLA_BASE_OPTIONS,
2530
3077
  rule: area1DrawRule,
3078
+ freezeOrientation: freezeEchelonLabeledAreaAnchor,
2531
3079
  previewSample: {
2532
3080
  controlPoints: [
2533
3081
  [-1, -.5],
@@ -2535,14 +3083,67 @@ const AREA_OF_OPERATIONS = defineControlMeasure({
2535
3083
  [1, .5],
2536
3084
  [-1, .5]
2537
3085
  ],
2538
- textAmplifiers: { T: "GREEN" },
3086
+ textAmplifiers: { T: "GUEVARA" },
2539
3087
  options: {
2540
- labelSizePixels: 12,
2541
- labelSizeResolution: 8
3088
+ smooth: true,
3089
+ labelSize: 96
2542
3090
  }
2543
3091
  }
2544
3092
  });
2545
3093
  //#endregion
3094
+ //#region src/generators/cm12-command-control-areas/generic-c2-area.ts
3095
+ /**
3096
+ * Default options for the Generic command and control area control measure.
3097
+ * The standard's example is a rounded blob, so `smooth` defaults on.
3098
+ */
3099
+ const DEFAULT_GENERIC_C2_AREA_OPTIONS = {
3100
+ smooth: true,
3101
+ smoothResolution: 16
3102
+ };
3103
+ const GENERIC_C2_AREA_METADATA = {
3104
+ id: "generic-c2-area",
3105
+ name: "Generic command and control area",
3106
+ description: "A general-purpose command and control area carrying a designation, additional information and an effective date-time-group window.",
3107
+ entity: "Command and Control Areas",
3108
+ entityType: "Generic",
3109
+ value: "120700",
3110
+ minCoordinates: 3,
3111
+ geometry: "area",
3112
+ geometryTypes: [
3113
+ "Polygon",
3114
+ "MultiLineString",
3115
+ "Point"
3116
+ ],
3117
+ drawRule: "Area1",
3118
+ capturesLabelSize: true,
3119
+ params: LABELED_AREA_PARAMS,
3120
+ textAmplifiers: AREA_TEXT_AMPLIFIERS
3121
+ };
3122
+ /**
3123
+ * Creates a Generic command and control area control measure: a closed area
3124
+ * through `positions` labeled at its center with the bare `T` (Field T) plus
3125
+ * `H`/`W`/`W1`/`N` amplifier rows — no prefix, per {@link labeledAreaFeatures}.
3126
+ */
3127
+ function createGenericC2Area(positions, options = {}, textAmplifiers = {}) {
3128
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers));
3129
+ }
3130
+ const GENERIC_C2_AREA = defineControlMeasure({
3131
+ metadata: GENERIC_C2_AREA_METADATA,
3132
+ generator: createGenericC2Area,
3133
+ defaultOptions: DEFAULT_GENERIC_C2_AREA_OPTIONS,
3134
+ rule: area1DrawRule,
3135
+ previewSample: {
3136
+ controlPoints: [
3137
+ [-1, -.5],
3138
+ [1, -.5],
3139
+ [1, .5],
3140
+ [-1, .5]
3141
+ ],
3142
+ textAmplifiers: { T: "RIOT FOODS" },
3143
+ options: { labelSize: 96 }
3144
+ }
3145
+ });
3146
+ //#endregion
2546
3147
  //#region src/generators/cm15-maneuver-areas/assemblyArea.ts
2547
3148
  /** Default options for the Assembly area control measure. */
2548
3149
  const DEFAULT_ASSEMBLY_AREA_OPTIONS = {
@@ -2589,31 +3190,13 @@ const ASSEMBLY_AREA = defineControlMeasure({
2589
3190
  [-1, .5]
2590
3191
  ],
2591
3192
  textAmplifiers: { T: "BLUE" },
2592
- options: {
2593
- labelSizePixels: 12,
2594
- labelSizeResolution: 8
2595
- }
3193
+ options: { labelSize: 96 }
2596
3194
  }
2597
3195
  });
2598
3196
  //#endregion
2599
3197
  //#region src/generators/cm29-protection-lines/params.ts
2600
3198
  const FORTIFIED_PARAMS = [
2601
- {
2602
- key: "smooth",
2603
- label: "Smooth",
2604
- description: "Round the line's corners by curving the path through the control points.",
2605
- type: "boolean"
2606
- },
2607
- {
2608
- key: "smoothResolution",
2609
- label: "Smooth resolution",
2610
- description: "Number of samples per segment when smooth mode is enabled.",
2611
- type: "number",
2612
- min: 2,
2613
- max: 64,
2614
- step: 1,
2615
- visibleWhen: (opts) => Boolean(opts.smooth)
2616
- },
3199
+ ...SMOOTH_LINE_PARAMS,
2617
3200
  {
2618
3201
  key: "toothSizePixels",
2619
3202
  label: "Tooth size",
@@ -2636,6 +3219,7 @@ const FORTIFIED_PARAMS = [
2636
3219
  }
2637
3220
  ];
2638
3221
  const ANTITANK_DITCH_PARAMS = [
3222
+ ...SMOOTH_LINE_PARAMS,
2639
3223
  {
2640
3224
  key: "toothHeightPixels",
2641
3225
  label: "Tooth height",
@@ -2685,7 +3269,9 @@ const DEFAULT_TOOTH_WIDTH_RATIO$1 = 1.15;
2685
3269
  const DEFAULT_ANTITANK_DITCH_OPTIONS = {
2686
3270
  toothHeight: DEFAULT_TOOTH_HEIGHT,
2687
3271
  toothHeightPixels: DEFAULT_TOOTH_HEIGHT_PIXELS,
2688
- toothWidthRatio: DEFAULT_TOOTH_WIDTH_RATIO$1
3272
+ toothWidthRatio: DEFAULT_TOOTH_WIDTH_RATIO$1,
3273
+ smooth: false,
3274
+ smoothResolution: 16
2689
3275
  };
2690
3276
  const ANTITANK_DITCH_UNDER_CONSTRUCTION_METADATA = {
2691
3277
  id: "antitank-ditch-under-construction",
@@ -2746,13 +3332,21 @@ function generateAntitankDitchTriangles(projectedPoints, toothHeight, toothWidth
2746
3332
  }
2747
3333
  return triangles;
2748
3334
  }
2749
- function buildAntitankDitchTriangles(positions, options) {
2750
- const toothHeight = calculateAntitankDitchHeight(options);
2751
- const toothWidthRatio = options.toothWidthRatio ?? DEFAULT_TOOTH_WIDTH_RATIO$1;
2752
- return generateAntitankDitchTriangles(positions.map((position) => project(position[0], position[1])), toothHeight, toothWidthRatio);
3335
+ /**
3336
+ * Projects `positions` and, when `options.smooth` is set, curves the result
3337
+ * through the control points (centripetal Catmull-Rom) before the teeth are
3338
+ * traced — matching Fortified Line's base-path smoothing. Returns the input
3339
+ * unchanged (no project/unproject round trip) when smoothing is off.
3340
+ */
3341
+ function smoothedDitchBasePath(positions, options) {
3342
+ return smoothLineVerts(positions.map((position) => project(position[0], position[1])), options, 16);
3343
+ }
3344
+ function buildAntitankDitchTriangles(basePath, options) {
3345
+ return generateAntitankDitchTriangles(basePath, calculateAntitankDitchHeight(options), options.toothWidthRatio ?? DEFAULT_TOOTH_WIDTH_RATIO$1);
2753
3346
  }
2754
3347
  function createAntitankDitchUnderConstruction(positions, options = {}) {
2755
- const triangles = buildAntitankDitchTriangles(positions, options);
3348
+ const basePath = smoothedDitchBasePath(positions, options);
3349
+ const triangles = buildAntitankDitchTriangles(basePath, options);
2756
3350
  return {
2757
3351
  type: "FeatureCollection",
2758
3352
  features: [{
@@ -2760,7 +3354,7 @@ function createAntitankDitchUnderConstruction(positions, options = {}) {
2760
3354
  properties: { part: "ditch" },
2761
3355
  geometry: {
2762
3356
  type: "MultiLineString",
2763
- coordinates: [positions.map((position) => [position[0], position[1]]), ...triangles.map((triangle) => triangle.map((point) => unproject(point[0], point[1])))]
3357
+ coordinates: [options.smooth ? basePath.map((point) => unproject(point[0], point[1])) : positions.map((position) => [position[0], position[1]]), ...triangles.map((triangle) => triangle.map((point) => unproject(point[0], point[1])))]
2764
3358
  }
2765
3359
  }]
2766
3360
  };
@@ -2776,7 +3370,7 @@ function createAntitankDitchCompleted(positions, options = {}) {
2776
3370
  },
2777
3371
  geometry: {
2778
3372
  type: "MultiPolygon",
2779
- coordinates: buildAntitankDitchTriangles(positions, options).map((triangle) => {
3373
+ coordinates: buildAntitankDitchTriangles(smoothedDitchBasePath(positions, options), options).map((triangle) => {
2780
3374
  return [[...triangle, triangle[0]].map((point) => unproject(point[0], point[1]))];
2781
3375
  })
2782
3376
  }
@@ -2860,7 +3454,7 @@ function createAntitankWall(positions, options = {}) {
2860
3454
  properties: { part: "wall" },
2861
3455
  geometry: {
2862
3456
  type: "MultiLineString",
2863
- coordinates: [generateAntitankWallPoints(positions.map((position) => project(position[0], position[1])), toothHeight, toothWidthRatio, toothSpacingRatio).map((point) => unproject(point[0], point[1]))]
3457
+ coordinates: [generateAntitankWallPoints(smoothLineVerts(positions.map((position) => project(position[0], position[1])), options, 16), toothHeight, toothWidthRatio, toothSpacingRatio).map((point) => unproject(point[0], point[1]))]
2864
3458
  }
2865
3459
  }]
2866
3460
  };
@@ -3255,70 +3849,7 @@ const BATTLE_POSITION_METADATA = {
3255
3849
  ],
3256
3850
  drawRule: "Area1",
3257
3851
  capturesLabelSize: true,
3258
- params: [
3259
- {
3260
- key: "smooth",
3261
- label: "Smooth",
3262
- description: "Round the area's corners by curving the boundary through the control points.",
3263
- type: "boolean"
3264
- },
3265
- {
3266
- key: "smoothResolution",
3267
- label: "Smooth resolution",
3268
- description: "Number of samples per segment when smooth mode is enabled.",
3269
- type: "number",
3270
- min: 2,
3271
- max: 64,
3272
- step: 1,
3273
- visibleWhen: (opts) => Boolean(opts.smooth)
3274
- },
3275
- {
3276
- key: "echelon",
3277
- label: "Echelon",
3278
- description: "Field B unit-size amplifier, drawn on the boundary.",
3279
- type: "enum",
3280
- options: ECHELON_PARAM_OPTIONS
3281
- },
3282
- {
3283
- key: "echelonSizePixels",
3284
- label: "Echelon size",
3285
- description: "Echelon glyph height in screen pixels.",
3286
- type: "number",
3287
- min: 8,
3288
- max: 48,
3289
- step: 1,
3290
- unit: "px"
3291
- },
3292
- {
3293
- key: "echelonSize",
3294
- label: "Echelon size",
3295
- description: "Echelon glyph height in meters.",
3296
- type: "number",
3297
- min: 50,
3298
- max: 2e4,
3299
- step: 50,
3300
- unit: "m"
3301
- },
3302
- {
3303
- key: "echelonPadding",
3304
- label: "Echelon padding",
3305
- description: "Gap on each side of the echelon, as a ratio of its height.",
3306
- type: "number",
3307
- min: 0,
3308
- max: 2,
3309
- step: .05
3310
- },
3311
- {
3312
- key: "echelonPosition",
3313
- label: "Echelon position",
3314
- description: "Slide the echelon along the boundary, as a fraction of the perimeter from the bottom edge.",
3315
- type: "number",
3316
- min: -.5,
3317
- max: .5,
3318
- step: .01
3319
- },
3320
- AREA_LABEL_PADDING_PARAM
3321
- ],
3852
+ params: ECHELON_AREA_PARAMS,
3322
3853
  textAmplifiers: [AREA_DESIGNATION_AMPLIFIER]
3323
3854
  };
3324
3855
  /**
@@ -3380,18 +3911,17 @@ function createBattlePosition(positions, options = {}, textAmplifiers = {}) {
3380
3911
  * Point: pins the echelon anchor to its current bottom-edge fraction so a
3381
3912
  * subsequent rigid rotation of `controlPoints` (e.g. the transform box's
3382
3913
  * rotate grip) does not re-derive a different "bottom edge" against the
3383
- * rotated geometry. Returns `undefined` — nothing to freeze — when:
3384
- * - `echelonAnchor` is already set (previously frozen or user-authored), or
3385
- * - the echelon resolves to none (nothing visible to pin), or
3386
- * - the ring is degenerate (no segments, or a near-zero perimeter).
3387
- */
3388
- function freezeEchelonAnchor(controlPoints, options) {
3389
- if (options.echelonAnchor !== void 0) return void 0;
3390
- if (!resolveEchelon(options.echelon ?? DEFAULT_BATTLE_POSITION_OPTIONS.echelon)) return void 0;
3391
- const perimeter = buildAreaPerimeter(buildClosedRing(controlPoints, options.smooth ?? DEFAULT_BATTLE_POSITION_OPTIONS.smooth, options.smoothResolution ?? DEFAULT_BATTLE_POSITION_OPTIONS.smoothResolution, DEFAULT_SMOOTH_RESOLUTION$6));
3392
- if (perimeter.segs.length === 0 || perimeter.totalLength < 1e-6) return void 0;
3393
- return { echelonAnchor: perimeter.anchorTarget / perimeter.totalLength };
3394
- }
3914
+ * rotated geometry. Delegates to {@link makeFreezeEchelonAnchor}, the shared
3915
+ * factory every echelon-carrying area measure (Battle Position, Strong
3916
+ * Point, Base Camp, Guerrilla Base) builds its own instance from — this
3917
+ * export just binds Battle Position's own defaults, keeping strongPoint.ts's
3918
+ * existing import (and any other importer) unchanged.
3919
+ */
3920
+ const freezeEchelonAnchor = makeFreezeEchelonAnchor({
3921
+ echelon: DEFAULT_BATTLE_POSITION_OPTIONS.echelon,
3922
+ smooth: DEFAULT_BATTLE_POSITION_OPTIONS.smooth,
3923
+ smoothResolution: DEFAULT_BATTLE_POSITION_OPTIONS.smoothResolution
3924
+ });
3395
3925
  const BATTLE_POSITION = defineControlMeasure({
3396
3926
  metadata: BATTLE_POSITION_METADATA,
3397
3927
  generator: createBattlePosition,
@@ -3409,8 +3939,7 @@ const BATTLE_POSITION = defineControlMeasure({
3409
3939
  options: {
3410
3940
  echelonSize: 200,
3411
3941
  smooth: true,
3412
- labelSizePixels: 12,
3413
- labelSizeResolution: 8
3942
+ labelSize: 96
3414
3943
  }
3415
3944
  }
3416
3945
  });
@@ -3762,7 +4291,6 @@ const BOUNDARY_METADATA = {
3762
4291
  "Point"
3763
4292
  ],
3764
4293
  drawRule: "Line1",
3765
- capturesLabelSize: true,
3766
4294
  params: [
3767
4295
  {
3768
4296
  key: "smooth",
@@ -3896,7 +4424,7 @@ const BOUNDARY_METADATA = {
3896
4424
  * unit 1 (left-of-travel side), `T1`/`AS1` label unit 2 (right-of-travel side).
3897
4425
  */
3898
4426
  function createBoundary(positions, options = {}, textAmplifiers = {}) {
3899
- const { echelon = DEFAULT_BOUNDARY_OPTIONS.echelon, echelonSize = DEFAULT_BOUNDARY_OPTIONS.echelonSize, echelonSizePixels, echelonPadding = DEFAULT_BOUNDARY_OPTIONS.echelonPadding, metersPerPixel, labelSizeZoom, labelSizeResolution, labelRepetitions = DEFAULT_BOUNDARY_OPTIONS.labelRepetitions, labelSpacing = DEFAULT_BOUNDARY_OPTIONS.labelSpacing, labelPadding = DEFAULT_BOUNDARY_OPTIONS.labelPadding, labelOrientation = DEFAULT_BOUNDARY_OPTIONS.labelOrientation, smooth = DEFAULT_BOUNDARY_OPTIONS.smooth, smoothResolution = DEFAULT_BOUNDARY_OPTIONS.smoothResolution } = options;
4427
+ const { echelon = DEFAULT_BOUNDARY_OPTIONS.echelon, echelonSize = DEFAULT_BOUNDARY_OPTIONS.echelonSize, echelonSizePixels, echelonPadding = DEFAULT_BOUNDARY_OPTIONS.echelonPadding, metersPerPixel, labelRepetitions = DEFAULT_BOUNDARY_OPTIONS.labelRepetitions, labelSpacing = DEFAULT_BOUNDARY_OPTIONS.labelSpacing, labelPadding = DEFAULT_BOUNDARY_OPTIONS.labelPadding, labelOrientation = DEFAULT_BOUNDARY_OPTIONS.labelOrientation, smooth = DEFAULT_BOUNDARY_OPTIONS.smooth, smoothResolution = DEFAULT_BOUNDARY_OPTIONS.smoothResolution } = options;
3900
4428
  const unit1Designator = textAmplifiers.T ?? "";
3901
4429
  const unit1Country = textAmplifiers.AS ?? "";
3902
4430
  const unit2Designator = textAmplifiers.T1 ?? "";
@@ -3906,14 +4434,7 @@ function createBoundary(positions, options = {}, textAmplifiers = {}) {
3906
4434
  h = Math.max(EPSILON, h);
3907
4435
  const reps = Math.max(1, Math.round(labelRepetitions));
3908
4436
  const spec = resolveEchelon(echelon);
3909
- const screenAnchored = echelonSizePixels !== void 0 && metersPerPixel !== void 0 && metersPerPixel > 0;
3910
- const refResolution = screenAnchored ? metersPerPixel : labelSizeResolution;
3911
- const labelSize = {};
3912
- if (refResolution !== void 0 && refResolution > 0) labelSize.textSizePixels = Math.round(h / refResolution);
3913
- if (!screenAnchored) {
3914
- if (labelSizeResolution !== void 0) labelSize.textSizeResolution = labelSizeResolution;
3915
- if (labelSizeZoom !== void 0) labelSize.textSizeZoom = labelSizeZoom;
3916
- }
4437
+ const labelSize = echelonSizePixels !== void 0 && metersPerPixel !== void 0 && metersPerPixel > 0 ? { textSizePixels: echelonSizePixels } : { textSizeMeters: h };
3917
4438
  const rawVerts = positions.map((p) => project(p[0], p[1]));
3918
4439
  const verts = smooth ? catmullRom(rawVerts, normalizeSmoothResolution$2(smoothResolution, DEFAULT_SMOOTH_RESOLUTION$4)) : rawVerts;
3919
4440
  const glyphStrokes = [];
@@ -4018,13 +4539,24 @@ const BOUNDARY = defineControlMeasure({
4018
4539
  rule: line1DrawRule,
4019
4540
  previewSample: {
4020
4541
  controlPoints: [[-2.5, 0], [2.5, 0]],
4021
- options: { echelonSize: 375 }
4542
+ options: {
4543
+ echelonSize: 375,
4544
+ labelPadding: .5
4545
+ },
4546
+ textAmplifiers: {
4547
+ T: "2INF",
4548
+ T1: "3INF"
4549
+ }
4022
4550
  }
4023
4551
  });
4024
4552
  //#endregion
4025
4553
  //#region src/generators/cm11-command-control-lines/light-line.ts
4026
4554
  /** Default options for the Light line control measure. */
4027
- const DEFAULT_LIGHT_LINE_OPTIONS = { includePrefix: true };
4555
+ const DEFAULT_LIGHT_LINE_OPTIONS = {
4556
+ includePrefix: true,
4557
+ smooth: false,
4558
+ smoothResolution: 12
4559
+ };
4028
4560
  const LIGHT_LINE_METADATA = {
4029
4561
  id: "light-line",
4030
4562
  name: "Light line",
@@ -4038,13 +4570,15 @@ const LIGHT_LINE_METADATA = {
4038
4570
  drawRule: "Line1",
4039
4571
  capturesLabelSize: true,
4040
4572
  params: [
4573
+ ...SMOOTH_LINE_PARAMS,
4041
4574
  {
4042
4575
  key: "phaseLineName",
4043
4576
  label: "Phase line name",
4044
4577
  description: "Name the line as a phase line — rendered \"PL <name>\" beyond each end of the line.",
4045
4578
  type: "text",
4046
4579
  placeholder: "DAVID",
4047
- maxLength: 21
4580
+ maxLength: 21,
4581
+ field: "T"
4048
4582
  },
4049
4583
  {
4050
4584
  key: "includePrefix",
@@ -4082,16 +4616,17 @@ const LIGHT_LINE = defineControlMeasure({
4082
4616
  rule: line1DrawRule,
4083
4617
  previewSample: {
4084
4618
  controlPoints: [[-1, 0], [1, 0]],
4085
- options: {
4086
- labelSizePixels: 12,
4087
- labelSizeResolution: 8
4088
- }
4619
+ options: { labelSize: 96 }
4089
4620
  }
4090
4621
  });
4091
4622
  //#endregion
4092
4623
  //#region src/generators/cm11-command-control-lines/engineer-work-line.ts
4093
4624
  /** Default options for the Engineer work line control measure. */
4094
- const DEFAULT_ENGINEER_WORK_LINE_OPTIONS = { includePrefix: true };
4625
+ const DEFAULT_ENGINEER_WORK_LINE_OPTIONS = {
4626
+ includePrefix: true,
4627
+ smooth: false,
4628
+ smoothResolution: 12
4629
+ };
4095
4630
  const ENGINEER_WORK_LINE_METADATA = {
4096
4631
  id: "engineer-work-line",
4097
4632
  name: "Engineer work line",
@@ -4105,13 +4640,15 @@ const ENGINEER_WORK_LINE_METADATA = {
4105
4640
  drawRule: "Line1",
4106
4641
  capturesLabelSize: true,
4107
4642
  params: [
4643
+ ...SMOOTH_LINE_PARAMS,
4108
4644
  {
4109
4645
  key: "phaseLineName",
4110
4646
  label: "Phase line name",
4111
4647
  description: "Name the line as a phase line — rendered \"PL <name>\" beyond each end of the line.",
4112
4648
  type: "text",
4113
4649
  placeholder: "DAVID",
4114
- maxLength: 21
4650
+ maxLength: 21,
4651
+ field: "T"
4115
4652
  },
4116
4653
  {
4117
4654
  key: "includePrefix",
@@ -4173,7 +4710,7 @@ function createEngineerWorkLine(positions, options = {}, textAmplifiers = {}) {
4173
4710
  placement: "above",
4174
4711
  ...options
4175
4712
  }, "EWL").features];
4176
- const { segments, totalLength } = polylineSegments(positions.map((p) => project(p[0], p[1])));
4713
+ const { segments, totalLength } = polylineSegments(smoothLineVerts(positions.map((p) => project(p[0], p[1])), options, 12));
4177
4714
  const frame = pointAlongPolyline(segments, totalLength, .5);
4178
4715
  if (frame) {
4179
4716
  const units = [{
@@ -4208,21 +4745,20 @@ const ENGINEER_WORK_LINE = defineControlMeasure({
4208
4745
  previewSample: {
4209
4746
  controlPoints: [[-2.5, 0], [2.5, 0]],
4210
4747
  textAmplifiers: {
4211
- T: "326EN BN",
4212
- AS: "USA",
4213
- T1: "127EN BN",
4214
- AS1: "USA"
4748
+ T: "2BN",
4749
+ T1: "3BN"
4215
4750
  },
4216
- options: {
4217
- labelSizePixels: 12,
4218
- labelSizeResolution: 20
4219
- }
4751
+ options: { labelSize: 240 }
4220
4752
  }
4221
4753
  });
4222
4754
  //#endregion
4223
4755
  //#region src/generators/cm11-command-control-lines/generic-c2-line.ts
4224
4756
  /** Default options for the Generic command and control line. */
4225
- const DEFAULT_GENERIC_C2_LINE_OPTIONS = { includePrefix: true };
4757
+ const DEFAULT_GENERIC_C2_LINE_OPTIONS = {
4758
+ includePrefix: true,
4759
+ smooth: false,
4760
+ smoothResolution: 12
4761
+ };
4226
4762
  const GENERIC_C2_LINE_METADATA = {
4227
4763
  id: "generic-c2-line",
4228
4764
  name: "Generic command and control line",
@@ -4236,13 +4772,15 @@ const GENERIC_C2_LINE_METADATA = {
4236
4772
  drawRule: "Line1",
4237
4773
  capturesLabelSize: true,
4238
4774
  params: [
4775
+ ...SMOOTH_LINE_PARAMS,
4239
4776
  {
4240
4777
  key: "phaseLineName",
4241
4778
  label: "Phase line name",
4242
4779
  description: "Name the line as a phase line — rendered \"PL <name>\" beyond each end of the line.",
4243
4780
  type: "text",
4244
4781
  placeholder: "DAVID",
4245
- maxLength: 21
4782
+ maxLength: 21,
4783
+ field: "T"
4246
4784
  },
4247
4785
  {
4248
4786
  key: "includePrefix",
@@ -4298,20 +4836,23 @@ const GENERIC_C2_LINE_METADATA = {
4298
4836
  * `base + perp·offset` and the other at `base − perp·offset`.
4299
4837
  */
4300
4838
  function createGenericC2Line(positions, options = {}, textAmplifiers = {}) {
4301
- const { phaseLineName, includePrefix = true, ...sizeOptions } = options;
4839
+ const { phaseLineName, includePrefix = true, smooth, smoothResolution, ...sizeOptions } = options;
4840
+ const verts = smoothLineVerts(positions.map((p) => project(p[0], p[1])), {
4841
+ smooth,
4842
+ smoothResolution
4843
+ }, 12);
4302
4844
  const features = [{
4303
4845
  type: "Feature",
4304
4846
  properties: { part: "generic-c2-line" },
4305
4847
  geometry: {
4306
4848
  type: "LineString",
4307
- coordinates: positions
4849
+ coordinates: smooth ? verts.map((v) => unproject(v[0], v[1])) : positions
4308
4850
  }
4309
4851
  }];
4310
4852
  const above = [textAmplifiers.H, textAmplifiers.T].filter((v) => Boolean(v)).join(" ");
4311
4853
  const below = [textAmplifiers.W, textAmplifiers.W1].filter((v) => Boolean(v)).join(" - ");
4312
4854
  const trimmedPhaseLineName = phaseLineName?.trim();
4313
4855
  if (above.length > 0 || below.length > 0 || trimmedPhaseLineName) {
4314
- const verts = positions.map((p) => project(p[0], p[1]));
4315
4856
  const labelFeatures = [];
4316
4857
  if (above.length > 0 || below.length > 0) {
4317
4858
  const frames = aboveEndFrames(verts, sizeOptions);
@@ -4340,15 +4881,12 @@ const GENERIC_C2_LINE = defineControlMeasure({
4340
4881
  previewSample: {
4341
4882
  controlPoints: [[-2.5, 0], [2.5, 0]],
4342
4883
  textAmplifiers: {
4343
- H: "IDR",
4344
- T: "METH",
4345
- W: "221735ZAUG2018",
4346
- W1: "312359ZDEC2099"
4884
+ H: "<H>",
4885
+ T: "<T>",
4886
+ W: "<W>",
4887
+ W1: "<W1>"
4347
4888
  },
4348
- options: {
4349
- labelSizePixels: 12,
4350
- labelSizeResolution: 20
4351
- }
4889
+ options: { labelSize: 240 }
4352
4890
  }
4353
4891
  });
4354
4892
  //#endregion
@@ -4371,10 +4909,11 @@ function createArrowHead$1(tip, dir, length, width) {
4371
4909
  ];
4372
4910
  }
4373
4911
  /**
4374
- * Builds the glyph label as a Point feature, forwarding any captured label-size
4375
- * metadata to adapters for styling. `point` is in projected meters; the rotation
4376
- * must already be in the generator's feature-rotation convention. Shared by every
4377
- * mission-task measure that centers a glyph on its symbol.
4912
+ * Builds the glyph label as a Point feature, forwarding the resolved size
4913
+ * anchor (`textSizeMeters`/`textSizePixels`, ADR-0028) to adapters for
4914
+ * styling. `point` is in projected meters; the rotation must already be in
4915
+ * the generator's feature-rotation convention. Shared by every mission-task
4916
+ * measure that centers a glyph on its symbol.
4378
4917
  */
4379
4918
  function createLabelFeature(point, text, rotation, labelSize = {}) {
4380
4919
  return {
@@ -4383,9 +4922,7 @@ function createLabelFeature(point, text, rotation, labelSize = {}) {
4383
4922
  part: "label",
4384
4923
  text,
4385
4924
  rotation,
4386
- ...labelSize.pixels !== void 0 ? { textSizePixels: labelSize.pixels } : {},
4387
- ...labelSize.zoom !== void 0 ? { textSizeZoom: labelSize.zoom } : {},
4388
- ...labelSize.resolution !== void 0 ? { textSizeResolution: labelSize.resolution } : {}
4925
+ ...labelSizeProps(labelSize)
4389
4926
  },
4390
4927
  geometry: {
4391
4928
  type: "Point",
@@ -4550,9 +5087,8 @@ function createBlockMissionTaskSymbol(coordinates, options = {}) {
4550
5087
  ]
4551
5088
  }
4552
5089
  }, createLabelFeature(labelPoint, "B", labelRotation, {
4553
- pixels: options.labelSizePixels,
4554
- zoom: options.labelSizeZoom,
4555
- resolution: options.labelSizeResolution
5090
+ labelSize: options.labelSize,
5091
+ labelSizePixels: options.labelSizePixels
4556
5092
  })]
4557
5093
  };
4558
5094
  }
@@ -4635,9 +5171,8 @@ function createBreachSymbol(coordinates, options = {}) {
4635
5171
  labelText: "B",
4636
5172
  labelGapRatio: options.labelGapRatio ?? DEFAULT_BREACH_OPTIONS.labelGapRatio,
4637
5173
  labelSize: {
4638
- pixels: options.labelSizePixels,
4639
- zoom: options.labelSizeZoom,
4640
- resolution: options.labelSizeResolution
5174
+ labelSize: options.labelSize,
5175
+ labelSizePixels: options.labelSizePixels
4641
5176
  },
4642
5177
  buildEndCaps: ({ p1, p2, dirFront, perpDir, frontLength }) => {
4643
5178
  const halfTick = frontLength * tickLengthRatio / 2;
@@ -4719,9 +5254,8 @@ function createBypassSymbol(coordinates, options = {}) {
4719
5254
  labelText: "B",
4720
5255
  labelGapRatio: options.labelGapRatio ?? DEFAULT_BYPASS_OPTIONS.labelGapRatio,
4721
5256
  labelSize: {
4722
- pixels: options.labelSizePixels,
4723
- zoom: options.labelSizeZoom,
4724
- resolution: options.labelSizeResolution
5257
+ labelSize: options.labelSize,
5258
+ labelSizePixels: options.labelSizePixels
4725
5259
  },
4726
5260
  buildEndCaps: ({ p1, p2, frontLength, rearTop }) => {
4727
5261
  const arrowheadLength = frontLength * arrowheadLengthRatio;
@@ -4808,9 +5342,8 @@ function createCanalizeSymbol(coordinates, options = {}) {
4808
5342
  labelText: "C",
4809
5343
  labelGapRatio: options.labelGapRatio ?? DEFAULT_CANALIZE_OPTIONS.labelGapRatio,
4810
5344
  labelSize: {
4811
- pixels: options.labelSizePixels,
4812
- zoom: options.labelSizeZoom,
4813
- resolution: options.labelSizeResolution
5345
+ labelSize: options.labelSize,
5346
+ labelSizePixels: options.labelSizePixels
4814
5347
  },
4815
5348
  buildEndCaps: ({ p1, p2, dirFront, perpDir, frontLength }) => {
4816
5349
  const halfTick = frontLength * tickLengthRatio / 2;
@@ -5539,9 +6072,8 @@ function createClearSymbol(coordinates, options = {}) {
5539
6072
  return {
5540
6073
  type: "FeatureCollection",
5541
6074
  features: [mainFeature, createLabelFeature(shaftLength > 1e-6 ? vecAdd(labelTip, vecScale(perpToRear, shaftLength * .5)) : labelTip, "C", labelRotation, {
5542
- pixels: options.labelSizePixels,
5543
- zoom: options.labelSizeZoom,
5544
- resolution: options.labelSizeResolution
6075
+ labelSize: options.labelSize,
6076
+ labelSizePixels: options.labelSizePixels
5545
6077
  })]
5546
6078
  };
5547
6079
  }
@@ -5649,9 +6181,8 @@ function createDelaySymbol(coordinates, options = {}) {
5649
6181
  return {
5650
6182
  type: "FeatureCollection",
5651
6183
  features: [lineFeature, createLabelFeature(labelPoint, "D", normalizeRadians(Math.PI + lineRotation), {
5652
- pixels: options.labelSizePixels,
5653
- zoom: options.labelSizeZoom,
5654
- resolution: options.labelSizeResolution
6184
+ labelSize: options.labelSize,
6185
+ labelSizePixels: options.labelSizePixels
5655
6186
  })]
5656
6187
  };
5657
6188
  }
@@ -5883,6 +6414,56 @@ const DISRUPT = defineControlMeasure({
5883
6414
  previewSample: { controlPoints: [[0, .4], [0, -.4]] }
5884
6415
  });
5885
6416
  //#endregion
6417
+ //#region src/generators/cm15-maneuver-areas/dropZone.ts
6418
+ /** Default options for the Drop zone control measure. */
6419
+ const DEFAULT_DROP_ZONE_OPTIONS = {
6420
+ smooth: false,
6421
+ smoothResolution: 16
6422
+ };
6423
+ const DROP_ZONE_METADATA = {
6424
+ id: "drop-zone",
6425
+ name: "Drop zone",
6426
+ description: "A specified area upon which airborne troops, equipment, or supplies are airdropped.",
6427
+ entity: "Maneuver Areas",
6428
+ entityType: "Drop Zone",
6429
+ value: "150600",
6430
+ minCoordinates: 3,
6431
+ geometry: "area",
6432
+ geometryTypes: [
6433
+ "Polygon",
6434
+ "MultiLineString",
6435
+ "Point"
6436
+ ],
6437
+ drawRule: "Area1",
6438
+ capturesLabelSize: true,
6439
+ params: LABELED_AREA_PARAMS,
6440
+ textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
6441
+ };
6442
+ /**
6443
+ * Creates a Drop zone control measure: a closed area through `positions`
6444
+ * labeled at its center with "DZ "-prefixed `T` (Field T) plus an `N`
6445
+ * (Field N) ENY marker, per {@link labeledAreaFeatures}.
6446
+ */
6447
+ function createDropZone(positions, options = {}, textAmplifiers = {}) {
6448
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "DZ"));
6449
+ }
6450
+ const DROP_ZONE = defineControlMeasure({
6451
+ metadata: DROP_ZONE_METADATA,
6452
+ generator: createDropZone,
6453
+ defaultOptions: DEFAULT_DROP_ZONE_OPTIONS,
6454
+ rule: area1DrawRule,
6455
+ previewSample: {
6456
+ controlPoints: [
6457
+ [-1, -.5],
6458
+ [1, -.5],
6459
+ [1, .5],
6460
+ [-1, .5]
6461
+ ],
6462
+ textAmplifiers: { T: "HAWK" },
6463
+ options: { labelSize: 96 }
6464
+ }
6465
+ });
6466
+ //#endregion
5886
6467
  //#region src/generators/cm15-maneuver-areas/encirclement.ts
5887
6468
  const DEFAULT_BARB_SPACING_RATIO = .4;
5888
6469
  const DEFAULT_BARB_LENGTH_RATIO = .16;
@@ -6030,17 +6611,66 @@ const ENCIRCLEMENT = defineControlMeasure({
6030
6611
  [-.4, .85],
6031
6612
  [-1, .5]
6032
6613
  ],
6033
- textAmplifiers: { N: "ENY" },
6614
+ textAmplifiers: {},
6034
6615
  options: {
6035
6616
  barbSpacingRatio: .4,
6036
6617
  barbLengthRatio: .18,
6037
6618
  smooth: true,
6038
- labelSizePixels: 12,
6039
- labelSizeResolution: 8
6619
+ labelSize: 96
6040
6620
  }
6041
6621
  }
6042
6622
  });
6043
6623
  //#endregion
6624
+ //#region src/generators/cm15-maneuver-areas/extractionZone.ts
6625
+ /** Default options for the Extraction zone control measure. */
6626
+ const DEFAULT_EXTRACTION_ZONE_OPTIONS = {
6627
+ smooth: false,
6628
+ smoothResolution: 16
6629
+ };
6630
+ const EXTRACTION_ZONE_METADATA = {
6631
+ id: "extraction-zone",
6632
+ name: "Extraction zone",
6633
+ description: "A specified drop zone used for the delivery of supplies and/or equipment by means of an extraction technique from an aircraft flying very close to the ground.",
6634
+ entity: "Maneuver Areas",
6635
+ entityType: "Extraction Zone",
6636
+ value: "150700",
6637
+ minCoordinates: 3,
6638
+ geometry: "area",
6639
+ geometryTypes: [
6640
+ "Polygon",
6641
+ "MultiLineString",
6642
+ "Point"
6643
+ ],
6644
+ drawRule: "Area1",
6645
+ capturesLabelSize: true,
6646
+ params: LABELED_AREA_PARAMS,
6647
+ textAmplifiers: AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
6648
+ };
6649
+ /**
6650
+ * Creates an Extraction zone control measure: a closed area through
6651
+ * `positions` labeled at its center with "EZ "-prefixed `T` (Field T) plus
6652
+ * an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
6653
+ */
6654
+ function createExtractionZone(positions, options = {}, textAmplifiers = {}) {
6655
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "EZ"));
6656
+ }
6657
+ const EXTRACTION_ZONE = defineControlMeasure({
6658
+ metadata: EXTRACTION_ZONE_METADATA,
6659
+ generator: createExtractionZone,
6660
+ defaultOptions: DEFAULT_EXTRACTION_ZONE_OPTIONS,
6661
+ rule: area1DrawRule,
6662
+ previewSample: {
6663
+ controlPoints: [
6664
+ [-1, -.5],
6665
+ [1, -.5],
6666
+ [1, .5],
6667
+ [-1, .5]
6668
+ ],
6669
+ textAmplifiers: { T: "ROCK" },
6670
+ options: { labelSize: 96 }
6671
+ }
6672
+ });
6673
+ //#endregion
6044
6674
  //#region src/internal/field-of-fire.ts
6045
6675
  /**
6046
6676
  * Builds the open (stroked) arrowhead at the outward tip of one arm. The two
@@ -6411,7 +7041,9 @@ const DEFAULT_ARC_SEGMENTS = 16;
6411
7041
  const DEFAULT_FLOT_OPTIONS = {
6412
7042
  radius: DEFAULT_RADIUS,
6413
7043
  radiusPixels: DEFAULT_RADIUS_PIXELS,
6414
- arcSegments: DEFAULT_ARC_SEGMENTS
7044
+ arcSegments: DEFAULT_ARC_SEGMENTS,
7045
+ smooth: false,
7046
+ smoothResolution: 16
6415
7047
  };
6416
7048
  const FLOT_METADATA = {
6417
7049
  id: "flot",
@@ -6425,6 +7057,7 @@ const FLOT_METADATA = {
6425
7057
  geometryTypes: ["LineString"],
6426
7058
  drawRule: "Line1",
6427
7059
  params: [
7060
+ ...SMOOTH_LINE_PARAMS,
6428
7061
  {
6429
7062
  key: "radiusPixels",
6430
7063
  label: "Radius",
@@ -6539,21 +7172,29 @@ function processSegment(p1, p2, radius, arcSegments, isFirstSegment) {
6539
7172
  * ```
6540
7173
  */
6541
7174
  function createFLOT(positions, options = {}) {
6542
- const { radius = DEFAULT_RADIUS, radiusPixels, metersPerPixel, arcSegments = DEFAULT_ARC_SEGMENTS } = options;
7175
+ const { radius = DEFAULT_RADIUS, radiusPixels, metersPerPixel, arcSegments = DEFAULT_ARC_SEGMENTS, smooth = DEFAULT_FLOT_OPTIONS.smooth, smoothResolution = DEFAULT_FLOT_OPTIONS.smoothResolution } = options;
6543
7176
  let effectiveRadius = radius;
6544
7177
  if (radiusPixels !== void 0 && metersPerPixel !== void 0 && metersPerPixel > 0) effectiveRadius = radiusPixels * metersPerPixel;
6545
7178
  const safeRadius = Math.max(EPSILON, effectiveRadius);
6546
7179
  const safeArcSegments = Math.max(3, Math.round(arcSegments));
6547
- const projectedPoints = positions.map((p) => project(p[0], p[1]));
7180
+ const basePath = smoothLineVerts(positions.map((p) => project(p[0], p[1])), {
7181
+ smooth,
7182
+ smoothResolution
7183
+ }, 16);
6548
7184
  const allPoints = [];
6549
- for (let i = 0; i < projectedPoints.length - 1; i++) {
6550
- const p1 = projectedPoints[i];
6551
- const p2 = projectedPoints[i + 1];
7185
+ for (let i = 0; i < basePath.length - 1; i++) {
7186
+ const p1 = basePath[i];
7187
+ const p2 = basePath[i + 1];
6552
7188
  const segmentPoints = processSegment(p1, p2, safeRadius, safeArcSegments, i === 0);
6553
7189
  allPoints.push(...segmentPoints);
6554
7190
  }
6555
- if (allPoints.length > 0 && projectedPoints.length > 1) {
6556
- const lastOriginal = projectedPoints[projectedPoints.length - 1];
7191
+ if (allPoints.length > 0 && basePath.length > 1) {
7192
+ const firstOriginal = basePath[0];
7193
+ const firstGenerated = allPoints[0];
7194
+ if (vecMag(vecSub(firstOriginal, firstGenerated)) > 1e-6) allPoints.unshift(firstOriginal);
7195
+ }
7196
+ if (allPoints.length > 0 && basePath.length > 1) {
7197
+ const lastOriginal = basePath[basePath.length - 1];
6557
7198
  const lastGenerated = allPoints[allPoints.length - 1];
6558
7199
  if (vecMag(vecSub(lastOriginal, lastGenerated)) > 1e-6) allPoints.push(lastOriginal);
6559
7200
  }
@@ -6579,7 +7220,11 @@ const FLOT = defineControlMeasure({
6579
7220
  //#endregion
6580
7221
  //#region src/generators/cm14-maneuver-lines/phase-line.ts
6581
7222
  /** Default options for the Phase line control measure. */
6582
- const DEFAULT_PHASE_LINE_OPTIONS = { includePrefix: true };
7223
+ const DEFAULT_PHASE_LINE_OPTIONS = {
7224
+ includePrefix: true,
7225
+ smooth: false,
7226
+ smoothResolution: 12
7227
+ };
6583
7228
  const PHASE_LINE_METADATA = {
6584
7229
  id: "phase-line",
6585
7230
  name: "Phase line",
@@ -6592,20 +7237,24 @@ const PHASE_LINE_METADATA = {
6592
7237
  geometryTypes: ["LineString", "Point"],
6593
7238
  drawRule: "Line1",
6594
7239
  capturesLabelSize: true,
6595
- params: [{
6596
- key: "includePrefix",
6597
- label: "Show PL prefix",
6598
- description: "Prefix the end labels with \"PL\" (or show a bare \"PL\" with no designation).",
6599
- type: "boolean"
6600
- }, {
6601
- key: "labelPadding",
6602
- label: "Label padding",
6603
- description: "Extra clearance between the line and its labels, as a ratio of the label height",
6604
- type: "number",
6605
- min: 0,
6606
- max: 2,
6607
- step: .05
6608
- }],
7240
+ params: [
7241
+ ...SMOOTH_LINE_PARAMS,
7242
+ {
7243
+ key: "includePrefix",
7244
+ label: "Show PL prefix",
7245
+ description: "Prefix the end labels with \"PL\" (or show a bare \"PL\" with no designation).",
7246
+ type: "boolean"
7247
+ },
7248
+ {
7249
+ key: "labelPadding",
7250
+ label: "Label padding",
7251
+ description: "Extra clearance between the line and its labels, as a ratio of the label height",
7252
+ type: "number",
7253
+ min: 0,
7254
+ max: 2,
7255
+ step: .05
7256
+ }
7257
+ ],
6609
7258
  textAmplifiers: [{
6610
7259
  key: "T",
6611
7260
  label: "Unique designation",
@@ -6635,16 +7284,17 @@ const PHASE_LINE = defineControlMeasure({
6635
7284
  previewSample: {
6636
7285
  controlPoints: [[-1, 0], [1, 0]],
6637
7286
  textAmplifiers: { T: "ECHO" },
6638
- options: {
6639
- labelSizePixels: 12,
6640
- labelSizeResolution: 8
6641
- }
7287
+ options: { labelSize: 96 }
6642
7288
  }
6643
7289
  });
6644
7290
  //#endregion
6645
7291
  //#region src/generators/cm14-maneuver-lines/handover-line.ts
6646
7292
  /** Default options for the Handover line control measure. */
6647
- const DEFAULT_HANDOVER_LINE_OPTIONS = { includePrefix: true };
7293
+ const DEFAULT_HANDOVER_LINE_OPTIONS = {
7294
+ includePrefix: true,
7295
+ smooth: false,
7296
+ smoothResolution: 12
7297
+ };
6648
7298
  const HANDOVER_LINE_METADATA = {
6649
7299
  id: "handover-line",
6650
7300
  name: "Handover line",
@@ -6658,13 +7308,15 @@ const HANDOVER_LINE_METADATA = {
6658
7308
  drawRule: "Line1",
6659
7309
  capturesLabelSize: true,
6660
7310
  params: [
7311
+ ...SMOOTH_LINE_PARAMS,
6661
7312
  {
6662
7313
  key: "phaseLineName",
6663
7314
  label: "Phase line name",
6664
7315
  description: "Name the line as a phase line — rendered \"PL <name>\" beyond each end of the line.",
6665
7316
  type: "text",
6666
7317
  placeholder: "DAVID",
6667
- maxLength: 21
7318
+ maxLength: 21,
7319
+ field: "T"
6668
7320
  },
6669
7321
  {
6670
7322
  key: "includePrefix",
@@ -6702,16 +7354,17 @@ const HANDOVER_LINE = defineControlMeasure({
6702
7354
  rule: line1DrawRule,
6703
7355
  previewSample: {
6704
7356
  controlPoints: [[-1, 0], [1, 0]],
6705
- options: {
6706
- labelSizePixels: 12,
6707
- labelSizeResolution: 8
6708
- }
7357
+ options: { labelSize: 96 }
6709
7358
  }
6710
7359
  });
6711
7360
  //#endregion
6712
7361
  //#region src/generators/cm14-maneuver-lines/battle-handover-line.ts
6713
7362
  /** Default options for the Battle handover line control measure. */
6714
- const DEFAULT_BATTLE_HANDOVER_LINE_OPTIONS = { includePrefix: true };
7363
+ const DEFAULT_BATTLE_HANDOVER_LINE_OPTIONS = {
7364
+ includePrefix: true,
7365
+ smooth: false,
7366
+ smoothResolution: 12
7367
+ };
6715
7368
  const BATTLE_HANDOVER_LINE_METADATA = {
6716
7369
  id: "battle-handover-line",
6717
7370
  name: "Battle handover line",
@@ -6725,13 +7378,15 @@ const BATTLE_HANDOVER_LINE_METADATA = {
6725
7378
  drawRule: "Line1",
6726
7379
  capturesLabelSize: true,
6727
7380
  params: [
7381
+ ...SMOOTH_LINE_PARAMS,
6728
7382
  {
6729
7383
  key: "phaseLineName",
6730
7384
  label: "Phase line name",
6731
7385
  description: "Name the line as a phase line — rendered \"PL <name>\" beyond each end of the line.",
6732
7386
  type: "text",
6733
7387
  placeholder: "DAVID",
6734
- maxLength: 21
7388
+ maxLength: 21,
7389
+ field: "T"
6735
7390
  },
6736
7391
  {
6737
7392
  key: "includePrefix",
@@ -6769,10 +7424,7 @@ const BATTLE_HANDOVER_LINE = defineControlMeasure({
6769
7424
  rule: line1DrawRule,
6770
7425
  previewSample: {
6771
7426
  controlPoints: [[-1, 0], [1, 0]],
6772
- options: {
6773
- labelSizePixels: 12,
6774
- labelSizeResolution: 8
6775
- }
7427
+ options: { labelSize: 96 }
6776
7428
  }
6777
7429
  });
6778
7430
  //#endregion
@@ -7013,10 +7665,7 @@ const FORTIFIED_AREA = defineControlMeasure({
7013
7665
  [-1, .5]
7014
7666
  ],
7015
7667
  textAmplifiers: { T: "TANGO" },
7016
- options: {
7017
- labelSizePixels: 12,
7018
- labelSizeResolution: 8
7019
- }
7668
+ options: { labelSize: 96 }
7020
7669
  }
7021
7670
  });
7022
7671
  //#endregion
@@ -7199,14 +7848,10 @@ const JOINT_TACTICAL_ACTION_AREA = defineControlMeasure({
7199
7848
  ],
7200
7849
  textAmplifiers: {
7201
7850
  T: "02",
7202
- N: "ENY",
7203
7851
  W: "051030Z",
7204
7852
  W1: "051600Z"
7205
7853
  },
7206
- options: {
7207
- labelSizePixels: 12,
7208
- labelSizeResolution: 8
7209
- }
7854
+ options: { labelSize: 96 }
7210
7855
  }
7211
7856
  });
7212
7857
  //#endregion
@@ -7256,10 +7901,7 @@ const LANDING_ZONE = defineControlMeasure({
7256
7901
  [-1, .5]
7257
7902
  ],
7258
7903
  textAmplifiers: { T: "SILVER" },
7259
- options: {
7260
- labelSizePixels: 12,
7261
- labelSizeResolution: 8
7262
- }
7904
+ options: { labelSize: 96 }
7263
7905
  }
7264
7906
  });
7265
7907
  //#endregion
@@ -7779,10 +8421,145 @@ const PICKUP_ZONE = defineControlMeasure({
7779
8421
  [-1, .5]
7780
8422
  ],
7781
8423
  textAmplifiers: { T: "WOLF" },
7782
- options: {
7783
- labelSizePixels: 12,
7784
- labelSizeResolution: 8
7785
- }
8424
+ options: { labelSize: 96 }
8425
+ }
8426
+ });
8427
+ //#endregion
8428
+ //#region src/generators/cm15-maneuver-areas/assaultPosition.ts
8429
+ /** Default options for the Assault position control measure. */
8430
+ const DEFAULT_ASSAULT_POSITION_OPTIONS = {
8431
+ smooth: false,
8432
+ smoothResolution: 16
8433
+ };
8434
+ const ASSAULT_POSITION_METADATA = {
8435
+ id: "assault-position",
8436
+ name: "Assault position",
8437
+ description: "That position between the line of departure and the objective in an attack from which forces assault the objective. Ideally, it is the last covered and concealed position before reaching the objective.",
8438
+ entity: "Maneuver Areas",
8439
+ entityType: "Assault Position",
8440
+ value: "151500",
8441
+ minCoordinates: 3,
8442
+ geometry: "area",
8443
+ geometryTypes: ["Polygon", "Point"],
8444
+ drawRule: "Area1",
8445
+ capturesLabelSize: true,
8446
+ params: SMOOTH_AREA_PARAMS,
8447
+ textAmplifiers: [AREA_DESIGNATION_ONLY_AMPLIFIER]
8448
+ };
8449
+ /**
8450
+ * Creates an Assault position control measure: a closed area through
8451
+ * `positions` labeled at its center with "ASLT "-prefixed `T` (Field T) —
8452
+ * the sole label row, per {@link labeledAreaFeatures}.
8453
+ */
8454
+ function createAssaultPosition(positions, options = {}, textAmplifiers = {}) {
8455
+ return labeledAreaFeatures(positions, options, { name: composeAreaName("ASLT", textAmplifiers.T) });
8456
+ }
8457
+ const ASSAULT_POSITION = defineControlMeasure({
8458
+ metadata: ASSAULT_POSITION_METADATA,
8459
+ generator: createAssaultPosition,
8460
+ defaultOptions: DEFAULT_ASSAULT_POSITION_OPTIONS,
8461
+ rule: area1DrawRule,
8462
+ previewSample: {
8463
+ controlPoints: [
8464
+ [-1, -.5],
8465
+ [1, -.5],
8466
+ [1, .5],
8467
+ [-1, .5]
8468
+ ],
8469
+ textAmplifiers: { T: "DANUBE" },
8470
+ options: { labelSize: 96 }
8471
+ }
8472
+ });
8473
+ //#endregion
8474
+ //#region src/generators/cm15-maneuver-areas/attackPosition.ts
8475
+ /** Default options for the Attack position control measure. */
8476
+ const DEFAULT_ATTACK_POSITION_OPTIONS = {
8477
+ smooth: false,
8478
+ smoothResolution: 16
8479
+ };
8480
+ const ATTACK_POSITION_METADATA = {
8481
+ id: "attack-position",
8482
+ name: "Attack position",
8483
+ description: "The last position occupied by the assault echelon before crossing the start line/line of departure.",
8484
+ entity: "Maneuver Areas",
8485
+ entityType: "Attack Position",
8486
+ value: "151600",
8487
+ minCoordinates: 3,
8488
+ geometry: "area",
8489
+ geometryTypes: ["Polygon", "Point"],
8490
+ drawRule: "Area1",
8491
+ capturesLabelSize: true,
8492
+ params: SMOOTH_AREA_PARAMS,
8493
+ textAmplifiers: [AREA_DESIGNATION_ONLY_AMPLIFIER]
8494
+ };
8495
+ /**
8496
+ * Creates an Attack position control measure: a closed area through
8497
+ * `positions` labeled at its center with "ATK "-prefixed `T` (Field T) —
8498
+ * the sole label row, per {@link labeledAreaFeatures}.
8499
+ */
8500
+ function createAttackPosition(positions, options = {}, textAmplifiers = {}) {
8501
+ return labeledAreaFeatures(positions, options, { name: composeAreaName("ATK", textAmplifiers.T) });
8502
+ }
8503
+ const ATTACK_POSITION = defineControlMeasure({
8504
+ metadata: ATTACK_POSITION_METADATA,
8505
+ generator: createAttackPosition,
8506
+ defaultOptions: DEFAULT_ATTACK_POSITION_OPTIONS,
8507
+ rule: area1DrawRule,
8508
+ previewSample: {
8509
+ controlPoints: [
8510
+ [-1, -.5],
8511
+ [1, -.5],
8512
+ [1, .5],
8513
+ [-1, .5]
8514
+ ],
8515
+ textAmplifiers: { T: "NILE" },
8516
+ options: { labelSize: 96 }
8517
+ }
8518
+ });
8519
+ //#endregion
8520
+ //#region src/generators/cm15-maneuver-areas/objectiveArea.ts
8521
+ /** Default options for the Objective area control measure. */
8522
+ const DEFAULT_OBJECTIVE_AREA_OPTIONS = {
8523
+ smooth: false,
8524
+ smoothResolution: 16
8525
+ };
8526
+ const OBJECTIVE_AREA_METADATA = {
8527
+ id: "objective-area",
8528
+ name: "Objective area",
8529
+ description: "A defined geographical area within which is located an objective to be captured or reached by the military forces. This area is defined by competent authority for purposes of command and control.",
8530
+ entity: "Maneuver Areas",
8531
+ entityType: "Objective Area",
8532
+ value: "151700",
8533
+ minCoordinates: 3,
8534
+ geometry: "area",
8535
+ geometryTypes: ["Polygon", "Point"],
8536
+ drawRule: "Area1",
8537
+ capturesLabelSize: true,
8538
+ params: SMOOTH_AREA_PARAMS,
8539
+ textAmplifiers: [AREA_DESIGNATION_ONLY_AMPLIFIER]
8540
+ };
8541
+ /**
8542
+ * Creates an Objective area control measure: a closed area through
8543
+ * `positions` labeled at its center with "OBJ "-prefixed `T` (Field T) —
8544
+ * the sole label row, per {@link labeledAreaFeatures}.
8545
+ */
8546
+ function createObjectiveArea(positions, options = {}, textAmplifiers = {}) {
8547
+ return labeledAreaFeatures(positions, options, { name: composeAreaName("OBJ", textAmplifiers.T) });
8548
+ }
8549
+ const OBJECTIVE_AREA = defineControlMeasure({
8550
+ metadata: OBJECTIVE_AREA_METADATA,
8551
+ generator: createObjectiveArea,
8552
+ defaultOptions: DEFAULT_OBJECTIVE_AREA_OPTIONS,
8553
+ rule: area1DrawRule,
8554
+ previewSample: {
8555
+ controlPoints: [
8556
+ [-1, -.5],
8557
+ [1, -.5],
8558
+ [1, .5],
8559
+ [-1, .5]
8560
+ ],
8561
+ textAmplifiers: { T: "FIVE" },
8562
+ options: { labelSize: 96 }
7786
8563
  }
7787
8564
  });
7788
8565
  //#endregion
@@ -8056,70 +8833,7 @@ const STRONG_POINT_METADATA = {
8056
8833
  ],
8057
8834
  drawRule: "Area1",
8058
8835
  capturesLabelSize: true,
8059
- params: [
8060
- {
8061
- key: "smooth",
8062
- label: "Smooth",
8063
- description: "Round the area's corners by curving the boundary through the control points.",
8064
- type: "boolean"
8065
- },
8066
- {
8067
- key: "smoothResolution",
8068
- label: "Smooth resolution",
8069
- description: "Number of samples per segment when smooth mode is enabled.",
8070
- type: "number",
8071
- min: 2,
8072
- max: 64,
8073
- step: 1,
8074
- visibleWhen: (opts) => Boolean(opts.smooth)
8075
- },
8076
- {
8077
- key: "echelon",
8078
- label: "Echelon",
8079
- description: "Field B unit-size amplifier, drawn on the boundary.",
8080
- type: "enum",
8081
- options: ECHELON_PARAM_OPTIONS
8082
- },
8083
- {
8084
- key: "echelonSizePixels",
8085
- label: "Echelon size",
8086
- description: "Echelon glyph height in screen pixels.",
8087
- type: "number",
8088
- min: 8,
8089
- max: 48,
8090
- step: 1,
8091
- unit: "px"
8092
- },
8093
- {
8094
- key: "echelonSize",
8095
- label: "Echelon size",
8096
- description: "Echelon glyph height in meters.",
8097
- type: "number",
8098
- min: 50,
8099
- max: 2e4,
8100
- step: 50,
8101
- unit: "m"
8102
- },
8103
- {
8104
- key: "echelonPadding",
8105
- label: "Echelon padding",
8106
- description: "Gap on each side of the echelon, as a ratio of its height.",
8107
- type: "number",
8108
- min: 0,
8109
- max: 2,
8110
- step: .05
8111
- },
8112
- {
8113
- key: "echelonPosition",
8114
- label: "Echelon position",
8115
- description: "Slide the echelon along the boundary, as a fraction of the perimeter from the bottom edge.",
8116
- type: "number",
8117
- min: -.5,
8118
- max: .5,
8119
- step: .01
8120
- },
8121
- AREA_LABEL_PADDING_PARAM
8122
- ],
8836
+ params: ECHELON_AREA_PARAMS,
8123
8837
  textAmplifiers: [AREA_DESIGNATION_AMPLIFIER]
8124
8838
  };
8125
8839
  /**
@@ -8227,12 +8941,121 @@ const STRONG_POINT = defineControlMeasure({
8227
8941
  options: {
8228
8942
  echelonSize: 200,
8229
8943
  smooth: true,
8230
- labelSizePixels: 12,
8231
- labelSizeResolution: 8
8944
+ labelSize: 96
8232
8945
  }
8233
8946
  }
8234
8947
  });
8235
8948
  //#endregion
8949
+ //#region src/generators/cm15-maneuver-areas/submarineActionArea.ts
8950
+ /** Default options for the Submarine action area control measure. */
8951
+ const DEFAULT_SUBMARINE_ACTION_AREA_OPTIONS = {
8952
+ smooth: false,
8953
+ smoothResolution: 16
8954
+ };
8955
+ const SUBMARINE_ACTION_AREA_METADATA = {
8956
+ id: "submarine-action-area",
8957
+ name: "Submarine action area",
8958
+ description: "An area, including the water space and airspace above it, reserved for submarine operations.",
8959
+ entity: "Maneuver Areas",
8960
+ entityType: "Action Area",
8961
+ entitySubtype: "Submarine Action Area",
8962
+ value: "150502",
8963
+ minCoordinates: 3,
8964
+ geometry: "area",
8965
+ geometryTypes: [
8966
+ "Polygon",
8967
+ "MultiLineString",
8968
+ "Point"
8969
+ ],
8970
+ drawRule: "Area1",
8971
+ capturesLabelSize: true,
8972
+ params: LABELED_AREA_PARAMS,
8973
+ textAmplifiers: AREA_TEXT_AMPLIFIERS_NO_ADDITIONAL_INFO
8974
+ };
8975
+ /**
8976
+ * Creates a Submarine action area control measure: a closed area through
8977
+ * `positions` labeled at its center with "SAA -"-prefixed `T` (Field T) plus
8978
+ * `W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
8979
+ */
8980
+ function createSubmarineActionArea(positions, options = {}, textAmplifiers = {}) {
8981
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "SAA -", ""));
8982
+ }
8983
+ const SUBMARINE_ACTION_AREA = defineControlMeasure({
8984
+ metadata: SUBMARINE_ACTION_AREA_METADATA,
8985
+ generator: createSubmarineActionArea,
8986
+ defaultOptions: DEFAULT_SUBMARINE_ACTION_AREA_OPTIONS,
8987
+ rule: area1DrawRule,
8988
+ previewSample: {
8989
+ controlPoints: [
8990
+ [-1, -.5],
8991
+ [1, -.5],
8992
+ [1, .5],
8993
+ [-1, .5]
8994
+ ],
8995
+ textAmplifiers: {
8996
+ T: "02",
8997
+ W: "051030Z",
8998
+ W1: "051600Z"
8999
+ },
9000
+ options: { labelSize: 96 }
9001
+ }
9002
+ });
9003
+ //#endregion
9004
+ //#region src/generators/cm15-maneuver-areas/submarineGeneratedActionArea.ts
9005
+ /** Default options for the Submarine-generated action area control measure. */
9006
+ const DEFAULT_SUBMARINE_GENERATED_ACTION_AREA_OPTIONS = {
9007
+ smooth: false,
9008
+ smoothResolution: 16
9009
+ };
9010
+ const SUBMARINE_GENERATED_ACTION_AREA_METADATA = {
9011
+ id: "submarine-generated-action-area",
9012
+ name: "Submarine-generated action area",
9013
+ description: "An action area generated around a submarine's reported position, reserved for its operations.",
9014
+ entity: "Maneuver Areas",
9015
+ entityType: "Action Area",
9016
+ entitySubtype: "Submarine-Generated Action Area",
9017
+ value: "150503",
9018
+ minCoordinates: 3,
9019
+ geometry: "area",
9020
+ geometryTypes: [
9021
+ "Polygon",
9022
+ "MultiLineString",
9023
+ "Point"
9024
+ ],
9025
+ drawRule: "Area1",
9026
+ capturesLabelSize: true,
9027
+ params: LABELED_AREA_PARAMS,
9028
+ textAmplifiers: AREA_TEXT_AMPLIFIERS_NO_ADDITIONAL_INFO
9029
+ };
9030
+ /**
9031
+ * Creates a Submarine-generated action area control measure: a closed area
9032
+ * through `positions` labeled at its center with "SGSA -"-prefixed `T`
9033
+ * (Field T) plus `W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
9034
+ */
9035
+ function createSubmarineGeneratedActionArea(positions, options = {}, textAmplifiers = {}) {
9036
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "SGSA -", ""));
9037
+ }
9038
+ const SUBMARINE_GENERATED_ACTION_AREA = defineControlMeasure({
9039
+ metadata: SUBMARINE_GENERATED_ACTION_AREA_METADATA,
9040
+ generator: createSubmarineGeneratedActionArea,
9041
+ defaultOptions: DEFAULT_SUBMARINE_GENERATED_ACTION_AREA_OPTIONS,
9042
+ rule: area1DrawRule,
9043
+ previewSample: {
9044
+ controlPoints: [
9045
+ [-1, -.5],
9046
+ [1, -.5],
9047
+ [1, .5],
9048
+ [-1, .5]
9049
+ ],
9050
+ textAmplifiers: {
9051
+ T: "02",
9052
+ W: "051030Z",
9053
+ W1: "051600Z"
9054
+ },
9055
+ options: { labelSize: 96 }
9056
+ }
9057
+ });
9058
+ //#endregion
8236
9059
  //#region src/generators/cm15-maneuver-areas/supportByFire.ts
8237
9060
  const DEFAULT_SUPPORT_BY_FIRE_OPTIONS = {
8238
9061
  arrowheadWidthRatio: .3,
@@ -8571,11 +9394,25 @@ const DEFINITIONS = {
8571
9394
  "final-protective-fire-right": FINAL_PROTECTIVE_FIRE_RIGHT,
8572
9395
  "search-area": SEARCH_AREA,
8573
9396
  encirclement: ENCIRCLEMENT,
9397
+ area: AREA,
8574
9398
  "assembly-area": ASSEMBLY_AREA,
8575
9399
  "joint-tactical-action-area": JOINT_TACTICAL_ACTION_AREA,
9400
+ "submarine-action-area": SUBMARINE_ACTION_AREA,
9401
+ "submarine-generated-action-area": SUBMARINE_GENERATED_ACTION_AREA,
9402
+ "drop-zone": DROP_ZONE,
9403
+ "extraction-zone": EXTRACTION_ZONE,
8576
9404
  "landing-zone": LANDING_ZONE,
8577
9405
  "pickup-zone": PICKUP_ZONE,
9406
+ "assault-position": ASSAULT_POSITION,
9407
+ "attack-position": ATTACK_POSITION,
9408
+ "objective-area": OBJECTIVE_AREA,
8578
9409
  "area-of-operations": AREA_OF_OPERATIONS,
9410
+ "named-area-of-interest": NAMED_AREA_OF_INTEREST,
9411
+ "target-area-of-interest": TARGET_AREA_OF_INTEREST,
9412
+ "airfield-zone": AIRFIELD_ZONE,
9413
+ "base-camp": BASE_CAMP,
9414
+ "guerrilla-base": GUERRILLA_BASE,
9415
+ "generic-c2-area": GENERIC_C2_AREA,
8579
9416
  "main-attack": MAIN_ATTACK,
8580
9417
  "supporting-attack": SUPPORTING_ATTACK,
8581
9418
  "classic-arrow": CLASSIC_ARROW,
@@ -8895,8 +9732,7 @@ function normalizeFeature(id, feature, index, measureStyle, graphicsStyle) {
8895
9732
  const text = typeof sourceProps.text === "string" ? sourceProps.text : void 0;
8896
9733
  const rotation = typeof sourceProps.rotation === "number" ? sourceProps.rotation : void 0;
8897
9734
  const textSizePixels = typeof sourceProps.textSizePixels === "number" ? sourceProps.textSizePixels : void 0;
8898
- const textSizeZoom = typeof sourceProps.textSizeZoom === "number" ? sourceProps.textSizeZoom : void 0;
8899
- const textSizeResolution = typeof sourceProps.textSizeResolution === "number" ? sourceProps.textSizeResolution : void 0;
9735
+ const textSizeMeters = typeof sourceProps.textSizeMeters === "number" ? sourceProps.textSizeMeters : void 0;
8900
9736
  const textAnchor = sourceProps.textAnchor === "start" || sourceProps.textAnchor === "end" ? sourceProps.textAnchor : void 0;
8901
9737
  return {
8902
9738
  type: "Feature",
@@ -8909,8 +9745,7 @@ function normalizeFeature(id, feature, index, measureStyle, graphicsStyle) {
8909
9745
  ...rotation !== void 0 ? { rotation } : {},
8910
9746
  ...textAnchor !== void 0 ? { textAnchor } : {},
8911
9747
  ...textSizePixels !== void 0 ? { textSizePixels } : {},
8912
- ...textSizeZoom !== void 0 ? { textSizeZoom } : {},
8913
- ...textSizeResolution !== void 0 ? { textSizeResolution } : {}
9748
+ ...textSizeMeters !== void 0 ? { textSizeMeters } : {}
8914
9749
  },
8915
9750
  geometry: feature.geometry
8916
9751
  };