@orbat-mapper/control-measures 0.2.0-alpha.7 → 0.2.0-alpha.9
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.
- package/README.md +9 -3
- package/dist/{index-DUGzYzPy.d.mts → index-26Xb77jE.d.mts} +189 -30
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +5 -5
- package/dist/preview/index.d.mts +1 -1
- package/dist/preview/index.mjs +1 -1
- package/dist/{renderControlMeasure-DGWf44sn.mjs → renderControlMeasure-CFdgTURO.mjs} +318 -121
- package/media/battle-position.svg +1 -1
- package/media/circle.svg +1 -0
- package/media/encirclement.svg +1 -0
- package/media/line.svg +1 -0
- package/media/polygon.svg +1 -0
- package/media/rectangle.svg +1 -0
- package/media/strong-point.svg +1 -0
- package/package.json +1 -1
|
@@ -206,8 +206,8 @@ const SHAFT_MAX_RATIO = .9;
|
|
|
206
206
|
*/
|
|
207
207
|
function processAttackGeometry(coordinates, options = {}) {
|
|
208
208
|
const shaftRatio = clamp(options.shaftWidthRatio ?? .6, SHAFT_MIN_RATIO, SHAFT_MAX_RATIO);
|
|
209
|
-
const
|
|
210
|
-
const
|
|
209
|
+
const smooth = options.smooth ?? false;
|
|
210
|
+
const smoothResolution = options.smoothResolution ?? 5;
|
|
211
211
|
const points = coordinates.map((c) => project(c[0], c[1]));
|
|
212
212
|
const numPoints = points.length;
|
|
213
213
|
const ptTip = points[0];
|
|
@@ -217,9 +217,9 @@ function processAttackGeometry(coordinates, options = {}) {
|
|
|
217
217
|
const p = points[i];
|
|
218
218
|
if (p) spinePoints.push(p);
|
|
219
219
|
}
|
|
220
|
-
return { geometry: calculateSymbolGeometry(ptTip, ptWidth, spinePoints, shaftRatio,
|
|
220
|
+
return { geometry: calculateSymbolGeometry(ptTip, ptWidth, spinePoints, shaftRatio, smooth, smoothResolution) };
|
|
221
221
|
}
|
|
222
|
-
function calculateSymbolGeometry(ptTip, ptWidth, spine, shaftRatio,
|
|
222
|
+
function calculateSymbolGeometry(ptTip, ptWidth, spine, shaftRatio, smooth, smoothResolution) {
|
|
223
223
|
const initialNeck = spine[spine.length - 1];
|
|
224
224
|
if (!initialNeck) return null;
|
|
225
225
|
const initialTipDir = vecNorm(vecSub(ptTip, initialNeck));
|
|
@@ -273,8 +273,8 @@ function calculateSymbolGeometry(ptTip, ptWidth, spine, shaftRatio, roundedBends
|
|
|
273
273
|
];
|
|
274
274
|
const fullSpine = [...remainingSpine, shaftEndCenter];
|
|
275
275
|
return {
|
|
276
|
-
shaftLeft: offsetPolyline(fullSpine, shaftHalfWidth,
|
|
277
|
-
shaftRight: offsetPolyline(fullSpine, -shaftHalfWidth,
|
|
276
|
+
shaftLeft: offsetPolyline(fullSpine, shaftHalfWidth, smooth, smoothResolution),
|
|
277
|
+
shaftRight: offsetPolyline(fullSpine, -shaftHalfWidth, smooth, smoothResolution),
|
|
278
278
|
headRing,
|
|
279
279
|
ptTip,
|
|
280
280
|
ptNeck,
|
|
@@ -975,20 +975,20 @@ const ATTACK_SHAFT_PARAMS = [
|
|
|
975
975
|
step: .05
|
|
976
976
|
},
|
|
977
977
|
{
|
|
978
|
-
key: "
|
|
979
|
-
label: "
|
|
978
|
+
key: "smooth",
|
|
979
|
+
label: "Smooth",
|
|
980
980
|
description: "Round the corners where the shaft changes direction.",
|
|
981
981
|
type: "boolean"
|
|
982
982
|
},
|
|
983
983
|
{
|
|
984
|
-
key: "
|
|
985
|
-
label: "
|
|
986
|
-
description: "Number of segments approximating each rounded bend",
|
|
984
|
+
key: "smoothResolution",
|
|
985
|
+
label: "Smooth resolution",
|
|
986
|
+
description: "Number of segments approximating each rounded bend when smooth mode is enabled.",
|
|
987
987
|
type: "number",
|
|
988
988
|
min: 1,
|
|
989
989
|
max: 20,
|
|
990
990
|
step: 1,
|
|
991
|
-
visibleWhen: (opts) => opts.
|
|
991
|
+
visibleWhen: (opts) => opts.smooth === true
|
|
992
992
|
}
|
|
993
993
|
];
|
|
994
994
|
const FIRE_ARROW_PARAMS = [
|
|
@@ -1002,8 +1002,8 @@ const FIRE_ARROW_PARAMS = [
|
|
|
1002
1002
|
step: .01
|
|
1003
1003
|
},
|
|
1004
1004
|
{
|
|
1005
|
-
key: "
|
|
1006
|
-
label: "
|
|
1005
|
+
key: "rearLineLengthRatio",
|
|
1006
|
+
label: "Rear line length",
|
|
1007
1007
|
description: "Length of the rear flare lines as a ratio of the head",
|
|
1008
1008
|
type: "number",
|
|
1009
1009
|
min: .05,
|
|
@@ -1011,8 +1011,8 @@ const FIRE_ARROW_PARAMS = [
|
|
|
1011
1011
|
step: .01
|
|
1012
1012
|
},
|
|
1013
1013
|
{
|
|
1014
|
-
key: "
|
|
1015
|
-
label: "
|
|
1014
|
+
key: "rearLineAngle",
|
|
1015
|
+
label: "Rear line angle",
|
|
1016
1016
|
description: "Angle of the rear flare lines, in degrees",
|
|
1017
1017
|
type: "number",
|
|
1018
1018
|
min: 0,
|
|
@@ -1021,8 +1021,8 @@ const FIRE_ARROW_PARAMS = [
|
|
|
1021
1021
|
}
|
|
1022
1022
|
];
|
|
1023
1023
|
const FORTIFIED_AREA_SIZE_PARAMS = [{
|
|
1024
|
-
key: "
|
|
1025
|
-
label: "
|
|
1024
|
+
key: "toothSizePixels",
|
|
1025
|
+
label: "Tooth size",
|
|
1026
1026
|
description: "Size of the fortified-area teeth, in pixels",
|
|
1027
1027
|
type: "number",
|
|
1028
1028
|
min: 5,
|
|
@@ -1030,8 +1030,8 @@ const FORTIFIED_AREA_SIZE_PARAMS = [{
|
|
|
1030
1030
|
step: 1,
|
|
1031
1031
|
unit: "px"
|
|
1032
1032
|
}, {
|
|
1033
|
-
key: "
|
|
1034
|
-
label: "
|
|
1033
|
+
key: "toothSize",
|
|
1034
|
+
label: "Tooth size",
|
|
1035
1035
|
description: "Size of the fortified-area teeth, in meters",
|
|
1036
1036
|
type: "number",
|
|
1037
1037
|
min: 1,
|
|
@@ -1098,8 +1098,8 @@ const FORTIFIED_AREA_PARAMS = [
|
|
|
1098
1098
|
//#region src/generators/cm15-maneuver-areas/airborneAttack.ts
|
|
1099
1099
|
const DEFAULT_AIRBORNE_ATTACK_OPTIONS = {
|
|
1100
1100
|
shaftWidthRatio: DEFAULT_SHAFT_WIDTH_RATIO,
|
|
1101
|
-
|
|
1102
|
-
|
|
1101
|
+
smooth: false,
|
|
1102
|
+
smoothResolution: 5
|
|
1103
1103
|
};
|
|
1104
1104
|
const AIRBORNE_ATTACK_METADATA = {
|
|
1105
1105
|
id: "airborne-attack",
|
|
@@ -1194,7 +1194,7 @@ const DEFAULT_AMBUSH_OPTIONS = {
|
|
|
1194
1194
|
hatchLengthRatio: .15,
|
|
1195
1195
|
hatchCount: 5,
|
|
1196
1196
|
curveDepthRatio: .25,
|
|
1197
|
-
|
|
1197
|
+
arcSegments: 30
|
|
1198
1198
|
};
|
|
1199
1199
|
const AMBUSH_METADATA = {
|
|
1200
1200
|
id: "ambush",
|
|
@@ -1255,8 +1255,8 @@ const AMBUSH_METADATA = {
|
|
|
1255
1255
|
step: .01
|
|
1256
1256
|
},
|
|
1257
1257
|
{
|
|
1258
|
-
key: "
|
|
1259
|
-
label: "
|
|
1258
|
+
key: "arcSegments",
|
|
1259
|
+
label: "Arc segments",
|
|
1260
1260
|
description: "Number of segments used to render the curve",
|
|
1261
1261
|
type: "number",
|
|
1262
1262
|
min: 4,
|
|
@@ -1278,11 +1278,11 @@ const quadBezier = (t, a, b, c) => {
|
|
|
1278
1278
|
* @returns GeoJSON FeatureCollection containing LineString components
|
|
1279
1279
|
*/
|
|
1280
1280
|
function createAmbushSymbol(coordinates, options = {}) {
|
|
1281
|
-
const { arrowheadLengthRatio, arrowheadWidthRatio, hatchLengthRatio, hatchCount, curveDepthRatio,
|
|
1281
|
+
const { arrowheadLengthRatio, arrowheadWidthRatio, hatchLengthRatio, hatchCount, curveDepthRatio, arcSegments } = {
|
|
1282
1282
|
...DEFAULT_AMBUSH_OPTIONS,
|
|
1283
1283
|
...options
|
|
1284
1284
|
};
|
|
1285
|
-
const
|
|
1285
|
+
const safeArcSegments = Math.max(1, Math.floor(arcSegments));
|
|
1286
1286
|
const safeHatchCount = Math.max(0, Math.floor(hatchCount));
|
|
1287
1287
|
const safeArrowheadLengthRatio = Math.max(0, arrowheadLengthRatio);
|
|
1288
1288
|
const safeArrowheadWidthRatio = Math.max(0, arrowheadWidthRatio);
|
|
@@ -1315,8 +1315,8 @@ function createAmbushSymbol(coordinates, options = {}) {
|
|
|
1315
1315
|
vecSub(hBase, vecScale(perp, hw))
|
|
1316
1316
|
]);
|
|
1317
1317
|
const curve = [];
|
|
1318
|
-
for (let i = 0; i <=
|
|
1319
|
-
const t = i /
|
|
1318
|
+
for (let i = 0; i <= safeArcSegments; i++) {
|
|
1319
|
+
const t = i / safeArcSegments;
|
|
1320
1320
|
curve.push(quadBezier(t, p2, ctrl, p3));
|
|
1321
1321
|
}
|
|
1322
1322
|
lines.push(curve);
|
|
@@ -1363,8 +1363,8 @@ const FORTIFIED_PARAMS = [
|
|
|
1363
1363
|
visibleWhen: (opts) => Boolean(opts.smooth)
|
|
1364
1364
|
},
|
|
1365
1365
|
{
|
|
1366
|
-
key: "
|
|
1367
|
-
label: "
|
|
1366
|
+
key: "toothSizePixels",
|
|
1367
|
+
label: "Tooth size",
|
|
1368
1368
|
description: "Side length of the square crenellations",
|
|
1369
1369
|
type: "number",
|
|
1370
1370
|
min: 5,
|
|
@@ -1373,8 +1373,8 @@ const FORTIFIED_PARAMS = [
|
|
|
1373
1373
|
unit: "px"
|
|
1374
1374
|
},
|
|
1375
1375
|
{
|
|
1376
|
-
key: "
|
|
1377
|
-
label: "
|
|
1376
|
+
key: "toothSize",
|
|
1377
|
+
label: "Tooth size",
|
|
1378
1378
|
description: "Side length of the square crenellations",
|
|
1379
1379
|
type: "number",
|
|
1380
1380
|
min: 1,
|
|
@@ -1623,8 +1623,8 @@ const ANTITANK_WALL = defineControlMeasure({
|
|
|
1623
1623
|
//#region src/generators/cm15-maneuver-areas/attackByFire.ts
|
|
1624
1624
|
const DEFAULT_ATTACK_BY_FIRE_OPTIONS = {
|
|
1625
1625
|
arrowheadWidthRatio: .3,
|
|
1626
|
-
|
|
1627
|
-
|
|
1626
|
+
rearLineLengthRatio: .3,
|
|
1627
|
+
rearLineAngle: 45
|
|
1628
1628
|
};
|
|
1629
1629
|
const ATTACK_BY_FIRE_METADATA = {
|
|
1630
1630
|
id: "attack-by-fire",
|
|
@@ -1655,8 +1655,8 @@ const ATTACK_BY_FIRE_METADATA = {
|
|
|
1655
1655
|
function createAttackByFire(coordinates, options = {}) {
|
|
1656
1656
|
const featureProps = {};
|
|
1657
1657
|
const arrowheadWidthRatio = options.arrowheadWidthRatio ?? DEFAULT_ATTACK_BY_FIRE_OPTIONS.arrowheadWidthRatio;
|
|
1658
|
-
const
|
|
1659
|
-
const
|
|
1658
|
+
const rearLineLengthRatio = options.rearLineLengthRatio ?? DEFAULT_ATTACK_BY_FIRE_OPTIONS.rearLineLengthRatio;
|
|
1659
|
+
const rearLineAngle = options.rearLineAngle ?? DEFAULT_ATTACK_BY_FIRE_OPTIONS.rearLineAngle;
|
|
1660
1660
|
const c1 = coordinates[0];
|
|
1661
1661
|
const c2 = coordinates[1];
|
|
1662
1662
|
const c3 = coordinates[2];
|
|
@@ -1682,13 +1682,13 @@ function createAttackByFire(coordinates, options = {}) {
|
|
|
1682
1682
|
const lenShaft = Math.abs(signedTipDistance);
|
|
1683
1683
|
const lenBase = frame.length;
|
|
1684
1684
|
const dirBase = frame.direction;
|
|
1685
|
-
const angleRad =
|
|
1685
|
+
const angleRad = rearLineAngle * Math.PI / 180;
|
|
1686
1686
|
const baseAngle = -Math.PI / 2;
|
|
1687
1687
|
const angleLeft = baseAngle - angleRad;
|
|
1688
1688
|
const dirBackLeft = [dirBase[0] * Math.cos(angleLeft) - dirBase[1] * Math.sin(angleLeft), dirBase[0] * Math.sin(angleLeft) + dirBase[1] * Math.cos(angleLeft)];
|
|
1689
1689
|
const angleRight = baseAngle + angleRad;
|
|
1690
1690
|
const dirBackRight = [dirBase[0] * Math.cos(angleRight) - dirBase[1] * Math.sin(angleRight), dirBase[0] * Math.sin(angleRight) + dirBase[1] * Math.cos(angleRight)];
|
|
1691
|
-
const backLen = lenBase *
|
|
1691
|
+
const backLen = lenBase * rearLineLengthRatio;
|
|
1692
1692
|
const p2Back = vecAdd(p2, vecScale(dirBackLeft, backLen));
|
|
1693
1693
|
const p3Back = vecAdd(p3, vecScale(dirBackRight, backLen));
|
|
1694
1694
|
const headWidth = lenShaft * arrowheadWidthRatio;
|
|
@@ -1763,8 +1763,8 @@ const ATTACK_BY_FIRE = defineControlMeasure({
|
|
|
1763
1763
|
//#region src/generators/cm15-maneuver-areas/attackHelicopter.ts
|
|
1764
1764
|
const DEFAULT_ATTACK_HELICOPTER_OPTIONS = {
|
|
1765
1765
|
shaftWidthRatio: DEFAULT_SHAFT_WIDTH_RATIO,
|
|
1766
|
-
|
|
1767
|
-
|
|
1766
|
+
smooth: false,
|
|
1767
|
+
smoothResolution: 5,
|
|
1768
1768
|
symbolHeightRatio: .45,
|
|
1769
1769
|
triangleSizeRatio: .25,
|
|
1770
1770
|
bottomBarWidthRatio: 1.5,
|
|
@@ -2729,7 +2729,7 @@ const DEFAULT_SMOOTH_RESOLUTION$5 = 12;
|
|
|
2729
2729
|
const MIN_SMOOTH_RESOLUTION$1 = 2;
|
|
2730
2730
|
const MAX_SMOOTH_RESOLUTION$1 = 64;
|
|
2731
2731
|
const DEFAULT_BLOCK_ARROW_OPTIONS = {
|
|
2732
|
-
|
|
2732
|
+
shaftWidthRatio: .06,
|
|
2733
2733
|
arrowheadStyle: "triangle",
|
|
2734
2734
|
arrowheadWidthRatio: .18,
|
|
2735
2735
|
arrowheadLengthRatio: .22,
|
|
@@ -2751,7 +2751,7 @@ const BLOCK_ARROW_METADATA = {
|
|
|
2751
2751
|
drawRule: "Axis1",
|
|
2752
2752
|
params: [
|
|
2753
2753
|
{
|
|
2754
|
-
key: "
|
|
2754
|
+
key: "shaftWidthRatio",
|
|
2755
2755
|
label: "Shaft width",
|
|
2756
2756
|
description: "Shaft band width as a fraction of the total path length",
|
|
2757
2757
|
type: "number",
|
|
@@ -2844,7 +2844,7 @@ const BLOCK_ARROW_METADATA = {
|
|
|
2844
2844
|
* right side of the shaft.
|
|
2845
2845
|
*/
|
|
2846
2846
|
function createBlockArrow(coordinates, options = {}) {
|
|
2847
|
-
const {
|
|
2847
|
+
const { shaftWidthRatio, arrowheadStyle, arrowheadWidthRatio, arrowheadLengthRatio, smooth, smoothResolution, filled } = {
|
|
2848
2848
|
...DEFAULT_BLOCK_ARROW_OPTIONS,
|
|
2849
2849
|
...options
|
|
2850
2850
|
};
|
|
@@ -2857,7 +2857,7 @@ function createBlockArrow(coordinates, options = {}) {
|
|
|
2857
2857
|
const { pts, pathLength, tip, dir, perp, segLength } = axis;
|
|
2858
2858
|
const requestedHeadLen = axis1.headLength ?? pathLength * arrowheadLengthRatio;
|
|
2859
2859
|
const headLen = Math.min(requestedHeadLen, segLength * .95);
|
|
2860
|
-
const halfShaft = pathLength *
|
|
2860
|
+
const halfShaft = pathLength * shaftWidthRatio / 2;
|
|
2861
2861
|
const halfHead = axis1.headHalfWidth ?? pathLength * arrowheadWidthRatio / 2;
|
|
2862
2862
|
const onAxis = (d, side = 0) => [tip[0] - dir[0] * d + perp[0] * side, tip[1] - dir[1] * d + perp[1] * side];
|
|
2863
2863
|
let shaftEndDist;
|
|
@@ -2956,7 +2956,7 @@ const BLOCK_ARROW = defineControlMeasure({
|
|
|
2956
2956
|
[-1.5, 0],
|
|
2957
2957
|
[.95, .4]
|
|
2958
2958
|
],
|
|
2959
|
-
options: {
|
|
2959
|
+
options: { shaftWidthRatio: .2 }
|
|
2960
2960
|
}
|
|
2961
2961
|
});
|
|
2962
2962
|
function axis1Geometry(coordinates) {
|
|
@@ -3006,10 +3006,8 @@ const DEFAULT_BOUNDARY_OPTIONS = {
|
|
|
3006
3006
|
echelonPadding: .3,
|
|
3007
3007
|
labelRepetitions: 1,
|
|
3008
3008
|
labelSpacing: 1,
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
unit2Designator: "",
|
|
3012
|
-
unit2Country: "",
|
|
3009
|
+
labelPadding: .4,
|
|
3010
|
+
labelOrientation: "along",
|
|
3013
3011
|
smooth: false,
|
|
3014
3012
|
smoothResolution: DEFAULT_SMOOTH_RESOLUTION$4
|
|
3015
3013
|
};
|
|
@@ -3100,6 +3098,54 @@ const BOUNDARY_METADATA = {
|
|
|
3100
3098
|
max: 2,
|
|
3101
3099
|
step: .05,
|
|
3102
3100
|
visibleWhen: (opts) => Number(opts.labelRepetitions ?? 1) > 1
|
|
3101
|
+
},
|
|
3102
|
+
{
|
|
3103
|
+
key: "labelPadding",
|
|
3104
|
+
label: "Label padding",
|
|
3105
|
+
description: "Gap between the line/echelon and the unit labels, as a ratio of the echelon height",
|
|
3106
|
+
type: "number",
|
|
3107
|
+
min: 0,
|
|
3108
|
+
max: 2,
|
|
3109
|
+
step: .05
|
|
3110
|
+
},
|
|
3111
|
+
{
|
|
3112
|
+
key: "labelOrientation",
|
|
3113
|
+
label: "Label orientation",
|
|
3114
|
+
description: "Unit labels along the line, or per MIL-STD-2525E Figure L-10: on mostly north–south segments the label group rotates perpendicular to the line as a single row crossing it.",
|
|
3115
|
+
type: "enum",
|
|
3116
|
+
options: [{
|
|
3117
|
+
value: "along",
|
|
3118
|
+
label: "Along the line"
|
|
3119
|
+
}, {
|
|
3120
|
+
value: "standard",
|
|
3121
|
+
label: "Standard (MIL-STD-2525E)"
|
|
3122
|
+
}]
|
|
3123
|
+
}
|
|
3124
|
+
],
|
|
3125
|
+
textAmplifiers: [
|
|
3126
|
+
{
|
|
3127
|
+
key: "T",
|
|
3128
|
+
label: "Unit designation",
|
|
3129
|
+
description: "Field T — unique designation of the unit on the left-of-travel side.",
|
|
3130
|
+
maxLength: 32
|
|
3131
|
+
},
|
|
3132
|
+
{
|
|
3133
|
+
key: "AS",
|
|
3134
|
+
label: "Country",
|
|
3135
|
+
description: "Field AS — country code of the unit on the left-of-travel side.",
|
|
3136
|
+
maxLength: 8
|
|
3137
|
+
},
|
|
3138
|
+
{
|
|
3139
|
+
key: "T1",
|
|
3140
|
+
label: "Unit designation (other side)",
|
|
3141
|
+
description: "Field T1 — unique designation of the unit on the right-of-travel side.",
|
|
3142
|
+
maxLength: 32
|
|
3143
|
+
},
|
|
3144
|
+
{
|
|
3145
|
+
key: "AS1",
|
|
3146
|
+
label: "Country (other side)",
|
|
3147
|
+
description: "Field AS1 — country code of the unit on the right-of-travel side.",
|
|
3148
|
+
maxLength: 8
|
|
3103
3149
|
}
|
|
3104
3150
|
]
|
|
3105
3151
|
};
|
|
@@ -3114,30 +3160,33 @@ function labelRotationAlong(dir) {
|
|
|
3114
3160
|
const rendered = -Math.atan2(dir[1], dir[0]);
|
|
3115
3161
|
return normalizeRadians(Math.PI - keepTextLeftToRight(rendered));
|
|
3116
3162
|
}
|
|
3117
|
-
/**
|
|
3163
|
+
/**
|
|
3164
|
+
* Emits a single combined `T (AS)` label per unit side (APP-6/MIL-STD-2525):
|
|
3165
|
+
* designator and country code share one text row rather than stacking.
|
|
3166
|
+
*/
|
|
3118
3167
|
function pushUnitLabels(out, frame, units) {
|
|
3119
|
-
const { center, perp,
|
|
3120
|
-
const
|
|
3121
|
-
|
|
3122
|
-
|
|
3168
|
+
const { center, perp, offset, rotation, labelSize } = frame;
|
|
3169
|
+
for (const { side, designator, country, textAnchor } of units) {
|
|
3170
|
+
let text;
|
|
3171
|
+
if (designator.length > 0 && country.length > 0) text = `${designator} (${country})`;
|
|
3172
|
+
else if (designator.length > 0) text = designator;
|
|
3173
|
+
else if (country.length > 0) text = `(${country})`;
|
|
3174
|
+
else continue;
|
|
3123
3175
|
const q = vecAdd(center, vecScale(perp, side * offset));
|
|
3124
|
-
|
|
3176
|
+
out.push({
|
|
3125
3177
|
type: "Feature",
|
|
3126
3178
|
properties: {
|
|
3127
3179
|
part: "label",
|
|
3128
3180
|
text,
|
|
3129
3181
|
rotation,
|
|
3182
|
+
...textAnchor ? { textAnchor } : {},
|
|
3130
3183
|
...labelSize
|
|
3131
3184
|
},
|
|
3132
3185
|
geometry: {
|
|
3133
3186
|
type: "Point",
|
|
3134
3187
|
coordinates: unproject(q[0], q[1])
|
|
3135
3188
|
}
|
|
3136
|
-
};
|
|
3137
|
-
};
|
|
3138
|
-
for (const { side, designator, country } of units) {
|
|
3139
|
-
if (designator.length > 0) out.push(label(side, designatorOffset, designator));
|
|
3140
|
-
if (country.length > 0) out.push(label(side, countryOffset, country));
|
|
3189
|
+
});
|
|
3141
3190
|
}
|
|
3142
3191
|
}
|
|
3143
3192
|
/**
|
|
@@ -3147,9 +3196,15 @@ function pushUnitLabels(out, frame, units) {
|
|
|
3147
3196
|
* @param positions - Boundary anchor points (≥2); the input contract is
|
|
3148
3197
|
* enforced at the render seam (ADR-0014).
|
|
3149
3198
|
* @param options - {@link BoundaryOptions}.
|
|
3199
|
+
* @param textAmplifiers - Normalized text amplifiers (ADR-0027): `T`/`AS` label
|
|
3200
|
+
* unit 1 (left-of-travel side), `T1`/`AS1` label unit 2 (right-of-travel side).
|
|
3150
3201
|
*/
|
|
3151
|
-
function createBoundary(positions, options = {}) {
|
|
3152
|
-
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,
|
|
3202
|
+
function createBoundary(positions, options = {}, textAmplifiers = {}) {
|
|
3203
|
+
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;
|
|
3204
|
+
const unit1Designator = textAmplifiers.T ?? "";
|
|
3205
|
+
const unit1Country = textAmplifiers.AS ?? "";
|
|
3206
|
+
const unit2Designator = textAmplifiers.T1 ?? "";
|
|
3207
|
+
const unit2Country = textAmplifiers.AS1 ?? "";
|
|
3153
3208
|
let h = echelonSize;
|
|
3154
3209
|
if (echelonSizePixels !== void 0 && metersPerPixel !== void 0 && metersPerPixel > 0) h = echelonSizePixels * metersPerPixel;
|
|
3155
3210
|
h = Math.max(EPSILON, h);
|
|
@@ -3199,32 +3254,50 @@ function createBoundary(positions, options = {}) {
|
|
|
3199
3254
|
}
|
|
3200
3255
|
const d = Math.max(0, Math.min(seg.length, target - acc));
|
|
3201
3256
|
const center = vecAdd(seg.start, vecScale(seg.along, d));
|
|
3202
|
-
const
|
|
3257
|
+
const units = [{
|
|
3258
|
+
side: 1,
|
|
3259
|
+
designator: unit1Designator,
|
|
3260
|
+
country: unit1Country
|
|
3261
|
+
}, {
|
|
3262
|
+
side: -1,
|
|
3263
|
+
designator: unit2Designator,
|
|
3264
|
+
country: unit2Country
|
|
3265
|
+
}];
|
|
3266
|
+
const rotateGroup = labelOrientation === "standard" && Math.abs(seg.along[0]) < Math.abs(seg.along[1]);
|
|
3267
|
+
const readingAxis = seg.perp[0] >= 0 ? seg.perp : vecScale(seg.perp, -1);
|
|
3268
|
+
const glyphAlong = rotateGroup ? readingAxis : seg.along;
|
|
3269
|
+
const glyphPerp = rotateGroup ? [-readingAxis[1], readingAxis[0]] : seg.perp;
|
|
3270
|
+
let glyphWidth = 0;
|
|
3203
3271
|
if (spec) {
|
|
3204
|
-
const { strokes, fills, width } = buildEchelonGlyph(center,
|
|
3272
|
+
const { strokes, fills, width } = buildEchelonGlyph(center, glyphAlong, glyphPerp, h, spec);
|
|
3273
|
+
glyphWidth = width;
|
|
3205
3274
|
glyphStrokes.push(...strokes);
|
|
3206
3275
|
glyphFills.push(...fills);
|
|
3207
|
-
const halfGap = width / 2 + Math.max(0, echelonPadding) * h;
|
|
3276
|
+
const halfGap = (rotateGroup ? h : width) / 2 + Math.max(0, echelonPadding) * h;
|
|
3208
3277
|
gaps.push({
|
|
3209
3278
|
g0: target - halfGap,
|
|
3210
3279
|
g1: target + halfGap
|
|
3211
3280
|
});
|
|
3212
3281
|
}
|
|
3213
|
-
|
|
3282
|
+
if (rotateGroup) {
|
|
3283
|
+
const sign = vecDot(seg.perp, readingAxis) >= 0 ? 1 : -1;
|
|
3284
|
+
const clearance = glyphWidth / 2 + Math.max(0, labelPadding) * h;
|
|
3285
|
+
units[0].textAnchor = sign > 0 ? "start" : "end";
|
|
3286
|
+
units[1].textAnchor = sign > 0 ? "end" : "start";
|
|
3287
|
+
pushUnitLabels(labelFeatures, {
|
|
3288
|
+
center,
|
|
3289
|
+
perp: vecScale(readingAxis, sign),
|
|
3290
|
+
offset: clearance,
|
|
3291
|
+
rotation: labelRotationAlong(readingAxis),
|
|
3292
|
+
labelSize
|
|
3293
|
+
}, units);
|
|
3294
|
+
} else pushUnitLabels(labelFeatures, {
|
|
3214
3295
|
center,
|
|
3215
3296
|
perp: seg.perp,
|
|
3216
|
-
h,
|
|
3217
|
-
rotation,
|
|
3297
|
+
offset: (.45 + Math.max(0, labelPadding)) * h,
|
|
3298
|
+
rotation: labelRotationAlong(seg.along),
|
|
3218
3299
|
labelSize
|
|
3219
|
-
},
|
|
3220
|
-
side: 1,
|
|
3221
|
-
designator: unit1Designator,
|
|
3222
|
-
country: unit1Country
|
|
3223
|
-
}, {
|
|
3224
|
-
side: -1,
|
|
3225
|
-
designator: unit2Designator,
|
|
3226
|
-
country: unit2Country
|
|
3227
|
-
}]);
|
|
3300
|
+
}, units);
|
|
3228
3301
|
}
|
|
3229
3302
|
const boundaryCoords = buildGappedLine(verts, gaps);
|
|
3230
3303
|
const features = [];
|
|
@@ -4990,7 +5063,7 @@ const BLADE_LENGTH_FRACTION = .8;
|
|
|
4990
5063
|
const DEFAULT_FINAL_PROTECTIVE_FIRE_OPTIONS = {
|
|
4991
5064
|
arrowheadLengthRatio: .07,
|
|
4992
5065
|
arrowheadWidthRatio: .67,
|
|
4993
|
-
|
|
5066
|
+
bladeHeightRatio: .03
|
|
4994
5067
|
};
|
|
4995
5068
|
const FPF_SHARED = {
|
|
4996
5069
|
entity: "Maneuver Lines",
|
|
@@ -5020,7 +5093,7 @@ const FPF_SHARED = {
|
|
|
5020
5093
|
step: .01
|
|
5021
5094
|
},
|
|
5022
5095
|
{
|
|
5023
|
-
key: "
|
|
5096
|
+
key: "bladeHeightRatio",
|
|
5024
5097
|
label: "Blade height",
|
|
5025
5098
|
description: "Blade's perpendicular reach into the V, as a fraction of its arm's length",
|
|
5026
5099
|
type: "number",
|
|
@@ -5050,7 +5123,7 @@ const FINAL_PROTECTIVE_FIRE_RIGHT_METADATA = {
|
|
|
5050
5123
|
* Builds the filled "blade": a uniform-width rectangle hugging the inside of
|
|
5051
5124
|
* one arm. One long edge lies on the central 80% of the arm line (connected to
|
|
5052
5125
|
* the line); the strip bulges perpendicularly into the inside of the V by
|
|
5053
|
-
* `
|
|
5126
|
+
* `bladeHeightRatio * armLength`.
|
|
5054
5127
|
*
|
|
5055
5128
|
* @param vertex - PT 1, the shared vertex of the V (projected)
|
|
5056
5129
|
* @param tip - the outward tip of the bladed arm (projected)
|
|
@@ -5058,7 +5131,7 @@ const FINAL_PROTECTIVE_FIRE_RIGHT_METADATA = {
|
|
|
5058
5131
|
* be undefined during live single-arm feedback, in which case a default side
|
|
5059
5132
|
* is chosen
|
|
5060
5133
|
*/
|
|
5061
|
-
function bladeRectangle(vertex, tip, otherTip,
|
|
5134
|
+
function bladeRectangle(vertex, tip, otherTip, bladeHeightRatio) {
|
|
5062
5135
|
const v = vecSub(tip, vertex);
|
|
5063
5136
|
const armLength = vecMag(v);
|
|
5064
5137
|
if (armLength < 1e-6) return null;
|
|
@@ -5071,7 +5144,7 @@ function bladeRectangle(vertex, tip, otherTip, bladeHeight) {
|
|
|
5071
5144
|
const gap = (1 - BLADE_LENGTH_FRACTION) / 2;
|
|
5072
5145
|
const onLineNear = vecAdd(vertex, vecScale(dir, armLength * gap));
|
|
5073
5146
|
const onLineFar = vecAdd(vertex, vecScale(dir, armLength * (1 - gap)));
|
|
5074
|
-
const height = vecScale(perp, armLength *
|
|
5147
|
+
const height = vecScale(perp, armLength * bladeHeightRatio);
|
|
5075
5148
|
return [
|
|
5076
5149
|
onLineNear,
|
|
5077
5150
|
onLineFar,
|
|
@@ -5093,7 +5166,7 @@ function bladeRectangle(vertex, tip, otherTip, bladeHeight) {
|
|
|
5093
5166
|
* @param options - Arrowhead and blade sizing parameters
|
|
5094
5167
|
*/
|
|
5095
5168
|
function createFinalProtectiveFire(coordinates, bladedArm, options = {}) {
|
|
5096
|
-
const { arrowheadLengthRatio, arrowheadWidthRatio,
|
|
5169
|
+
const { arrowheadLengthRatio, arrowheadWidthRatio, bladeHeightRatio } = {
|
|
5097
5170
|
...DEFAULT_FINAL_PROTECTIVE_FIRE_OPTIONS,
|
|
5098
5171
|
...options
|
|
5099
5172
|
};
|
|
@@ -5113,7 +5186,7 @@ function createFinalProtectiveFire(coordinates, bladedArm, options = {}) {
|
|
|
5113
5186
|
const bladeTip = v.tips[bladedArm];
|
|
5114
5187
|
if (bladeTip) {
|
|
5115
5188
|
const otherTip = v.tips[bladedArm === 0 ? 1 : 0];
|
|
5116
|
-
const blade = bladeRectangle(v.vertex, bladeTip, otherTip, Math.max(0,
|
|
5189
|
+
const blade = bladeRectangle(v.vertex, bladeTip, otherTip, Math.max(0, bladeHeightRatio));
|
|
5117
5190
|
if (blade) {
|
|
5118
5191
|
const ring = [...blade, blade[0]].map((c) => unproject(c[0], c[1]));
|
|
5119
5192
|
features.push({
|
|
@@ -5299,14 +5372,14 @@ const FIX = defineControlMeasure({
|
|
|
5299
5372
|
//#region src/generators/cm14-maneuver-lines/flot.ts
|
|
5300
5373
|
const DEFAULT_RADIUS = 50;
|
|
5301
5374
|
const DEFAULT_RADIUS_PIXELS = 10;
|
|
5302
|
-
const
|
|
5375
|
+
const DEFAULT_ARC_SEGMENTS = 16;
|
|
5303
5376
|
/**
|
|
5304
5377
|
* Default options for the FLOT graphic.
|
|
5305
5378
|
*/
|
|
5306
5379
|
const DEFAULT_FLOT_OPTIONS = {
|
|
5307
5380
|
radius: DEFAULT_RADIUS,
|
|
5308
5381
|
radiusPixels: DEFAULT_RADIUS_PIXELS,
|
|
5309
|
-
|
|
5382
|
+
arcSegments: DEFAULT_ARC_SEGMENTS
|
|
5310
5383
|
};
|
|
5311
5384
|
const FLOT_METADATA = {
|
|
5312
5385
|
id: "flot",
|
|
@@ -5341,8 +5414,8 @@ const FLOT_METADATA = {
|
|
|
5341
5414
|
unit: "m"
|
|
5342
5415
|
},
|
|
5343
5416
|
{
|
|
5344
|
-
key: "
|
|
5345
|
-
label: "
|
|
5417
|
+
key: "arcSegments",
|
|
5418
|
+
label: "Arc segments",
|
|
5346
5419
|
description: "Number of points used to render each semicircle",
|
|
5347
5420
|
type: "number",
|
|
5348
5421
|
min: 4,
|
|
@@ -5362,16 +5435,16 @@ const FLOT_METADATA = {
|
|
|
5362
5435
|
* @param segmentDir - Direction vector of the segment (normalized)
|
|
5363
5436
|
* @param perpDir - Perpendicular direction vector (normalized)
|
|
5364
5437
|
* @param radius - Radius of the semicircle
|
|
5365
|
-
* @param
|
|
5438
|
+
* @param arcSegments - Number of points in the arc
|
|
5366
5439
|
* @param includeStart - Whether to include the start point
|
|
5367
5440
|
* @returns Array of arc points forming a true semicircle
|
|
5368
5441
|
*/
|
|
5369
|
-
function generateArcPoints(startPoint, segmentDir, perpDir, radius,
|
|
5442
|
+
function generateArcPoints(startPoint, segmentDir, perpDir, radius, arcSegments, includeStart) {
|
|
5370
5443
|
const points = [];
|
|
5371
5444
|
const startIndex = includeStart ? 0 : 1;
|
|
5372
5445
|
const center = vecAdd(startPoint, vecScale(segmentDir, radius));
|
|
5373
|
-
for (let i = startIndex; i <=
|
|
5374
|
-
const t = i /
|
|
5446
|
+
for (let i = startIndex; i <= arcSegments; i++) {
|
|
5447
|
+
const t = i / arcSegments;
|
|
5375
5448
|
const angle = Math.PI * (1 - t);
|
|
5376
5449
|
const alongSegment = Math.cos(angle) * radius;
|
|
5377
5450
|
const perpOffset = Math.sin(angle) * radius;
|
|
@@ -5386,11 +5459,11 @@ function generateArcPoints(startPoint, segmentDir, perpDir, radius, resolution,
|
|
|
5386
5459
|
* @param p1 - Start position (projected)
|
|
5387
5460
|
* @param p2 - End position (projected)
|
|
5388
5461
|
* @param radius - Radius of each semicircle
|
|
5389
|
-
* @param
|
|
5462
|
+
* @param arcSegments - Points per arc
|
|
5390
5463
|
* @param isFirstSegment - Whether this is the first segment (include start point)
|
|
5391
5464
|
* @returns Array of points forming the scalloped line for this segment
|
|
5392
5465
|
*/
|
|
5393
|
-
function processSegment(p1, p2, radius,
|
|
5466
|
+
function processSegment(p1, p2, radius, arcSegments, isFirstSegment) {
|
|
5394
5467
|
const segmentVec = vecSub(p2, p1);
|
|
5395
5468
|
const segmentLength = vecMag(segmentVec);
|
|
5396
5469
|
if (segmentLength < 1e-6) return isFirstSegment ? [p1] : [];
|
|
@@ -5402,14 +5475,14 @@ function processSegment(p1, p2, radius, resolution, isFirstSegment) {
|
|
|
5402
5475
|
const remainingLength = segmentLength - numFullArcs * diameter;
|
|
5403
5476
|
let currentPos = p1;
|
|
5404
5477
|
for (let arcIndex = 0; arcIndex < numFullArcs; arcIndex++) {
|
|
5405
|
-
const arcPoints = generateArcPoints(currentPos, segmentDir, perpDir, radius,
|
|
5478
|
+
const arcPoints = generateArcPoints(currentPos, segmentDir, perpDir, radius, arcSegments, arcIndex === 0 && isFirstSegment);
|
|
5406
5479
|
points.push(...arcPoints);
|
|
5407
5480
|
currentPos = vecAdd(currentPos, vecScale(segmentDir, diameter));
|
|
5408
5481
|
}
|
|
5409
5482
|
if (remainingLength > diameter * .1) {
|
|
5410
5483
|
const partialRadius = remainingLength / 2;
|
|
5411
|
-
const
|
|
5412
|
-
const arcPoints = generateArcPoints(currentPos, segmentDir, perpDir, partialRadius,
|
|
5484
|
+
const partialArcSegments = Math.max(3, Math.round(arcSegments * (partialRadius / radius)));
|
|
5485
|
+
const arcPoints = generateArcPoints(currentPos, segmentDir, perpDir, partialRadius, partialArcSegments, numFullArcs === 0 && isFirstSegment);
|
|
5413
5486
|
points.push(...arcPoints);
|
|
5414
5487
|
}
|
|
5415
5488
|
return points;
|
|
@@ -5429,22 +5502,22 @@ function processSegment(p1, p2, radius, resolution, isFirstSegment) {
|
|
|
5429
5502
|
* ```typescript
|
|
5430
5503
|
* const flot = createFLOT(
|
|
5431
5504
|
* [[-122.4194, 37.7749], [-122.4100, 37.7800], [-122.4000, 37.7750]],
|
|
5432
|
-
* { radius: 100,
|
|
5505
|
+
* { radius: 100, arcSegments: 20 }
|
|
5433
5506
|
* );
|
|
5434
5507
|
* ```
|
|
5435
5508
|
*/
|
|
5436
5509
|
function createFLOT(positions, options = {}) {
|
|
5437
|
-
const { radius = DEFAULT_RADIUS, radiusPixels, metersPerPixel,
|
|
5510
|
+
const { radius = DEFAULT_RADIUS, radiusPixels, metersPerPixel, arcSegments = DEFAULT_ARC_SEGMENTS } = options;
|
|
5438
5511
|
let effectiveRadius = radius;
|
|
5439
5512
|
if (radiusPixels !== void 0 && metersPerPixel !== void 0 && metersPerPixel > 0) effectiveRadius = radiusPixels * metersPerPixel;
|
|
5440
5513
|
const safeRadius = Math.max(EPSILON, effectiveRadius);
|
|
5441
|
-
const
|
|
5514
|
+
const safeArcSegments = Math.max(3, Math.round(arcSegments));
|
|
5442
5515
|
const projectedPoints = positions.map((p) => project(p[0], p[1]));
|
|
5443
5516
|
const allPoints = [];
|
|
5444
5517
|
for (let i = 0; i < projectedPoints.length - 1; i++) {
|
|
5445
5518
|
const p1 = projectedPoints[i];
|
|
5446
5519
|
const p2 = projectedPoints[i + 1];
|
|
5447
|
-
const segmentPoints = processSegment(p1, p2, safeRadius,
|
|
5520
|
+
const segmentPoints = processSegment(p1, p2, safeRadius, safeArcSegments, i === 0);
|
|
5448
5521
|
allPoints.push(...segmentPoints);
|
|
5449
5522
|
}
|
|
5450
5523
|
if (allPoints.length > 0 && projectedPoints.length > 1) {
|
|
@@ -5473,15 +5546,15 @@ const FLOT = defineControlMeasure({
|
|
|
5473
5546
|
});
|
|
5474
5547
|
//#endregion
|
|
5475
5548
|
//#region src/generators/cm29-protection-lines/fortifiedLine.ts
|
|
5476
|
-
const
|
|
5477
|
-
const
|
|
5549
|
+
const DEFAULT_TOOTH_SIZE = 50;
|
|
5550
|
+
const DEFAULT_TOOTH_SIZE_PIXELS = 10;
|
|
5478
5551
|
const DEFAULT_SMOOTH_RESOLUTION$1 = 16;
|
|
5479
5552
|
/**
|
|
5480
5553
|
* Default options for the Fortified Line graphic.
|
|
5481
5554
|
*/
|
|
5482
5555
|
const DEFAULT_FORTIFIED_LINE_OPTIONS = {
|
|
5483
|
-
|
|
5484
|
-
|
|
5556
|
+
toothSize: DEFAULT_TOOTH_SIZE,
|
|
5557
|
+
toothSizePixels: DEFAULT_TOOTH_SIZE_PIXELS,
|
|
5485
5558
|
smooth: false,
|
|
5486
5559
|
smoothResolution: DEFAULT_SMOOTH_RESOLUTION$1
|
|
5487
5560
|
};
|
|
@@ -5571,9 +5644,9 @@ function processFortifiedSegment(p1, p2, size, isFirstSegment) {
|
|
|
5571
5644
|
* Calculates the effective size based on options and projection.
|
|
5572
5645
|
*/
|
|
5573
5646
|
function calculateEffectiveSize(options) {
|
|
5574
|
-
const {
|
|
5575
|
-
let effectiveSize =
|
|
5576
|
-
if (
|
|
5647
|
+
const { toothSize = DEFAULT_TOOTH_SIZE, toothSizePixels, metersPerPixel } = options;
|
|
5648
|
+
let effectiveSize = toothSize;
|
|
5649
|
+
if (toothSizePixels !== void 0 && metersPerPixel !== void 0 && metersPerPixel > 0) effectiveSize = toothSizePixels * metersPerPixel;
|
|
5577
5650
|
return Math.max(EPSILON, effectiveSize);
|
|
5578
5651
|
}
|
|
5579
5652
|
/**
|
|
@@ -5825,8 +5898,8 @@ const ISOLATE = defineControlMeasure({
|
|
|
5825
5898
|
//#region src/generators/cm15-maneuver-areas/mainAttack.ts
|
|
5826
5899
|
const DEFAULT_MAIN_ATTACK_OPTIONS = {
|
|
5827
5900
|
shaftWidthRatio: DEFAULT_SHAFT_WIDTH_RATIO,
|
|
5828
|
-
|
|
5829
|
-
|
|
5901
|
+
smooth: false,
|
|
5902
|
+
smoothResolution: 5
|
|
5830
5903
|
};
|
|
5831
5904
|
const MAIN_ATTACK_METADATA = {
|
|
5832
5905
|
id: "main-attack",
|
|
@@ -6731,8 +6804,8 @@ const STRONG_POINT = defineControlMeasure({
|
|
|
6731
6804
|
//#region src/generators/cm15-maneuver-areas/supportByFire.ts
|
|
6732
6805
|
const DEFAULT_SUPPORT_BY_FIRE_OPTIONS = {
|
|
6733
6806
|
arrowheadWidthRatio: .3,
|
|
6734
|
-
|
|
6735
|
-
|
|
6807
|
+
rearLineLengthRatio: .3,
|
|
6808
|
+
rearLineAngle: 45
|
|
6736
6809
|
};
|
|
6737
6810
|
const SUPPORT_BY_FIRE_METADATA = {
|
|
6738
6811
|
id: "support-by-fire",
|
|
@@ -6759,8 +6832,8 @@ const SUPPORT_BY_FIRE_METADATA = {
|
|
|
6759
6832
|
function createSupportByFire(coordinates, options = {}) {
|
|
6760
6833
|
const featureProps = {};
|
|
6761
6834
|
const arrowheadWidthRatio = options.arrowheadWidthRatio ?? DEFAULT_SUPPORT_BY_FIRE_OPTIONS.arrowheadWidthRatio;
|
|
6762
|
-
const
|
|
6763
|
-
const
|
|
6835
|
+
const rearLineLengthRatio = options.rearLineLengthRatio ?? DEFAULT_SUPPORT_BY_FIRE_OPTIONS.rearLineLengthRatio;
|
|
6836
|
+
const rearLineAngle = options.rearLineAngle ?? DEFAULT_SUPPORT_BY_FIRE_OPTIONS.rearLineAngle;
|
|
6764
6837
|
const c1 = coordinates[0];
|
|
6765
6838
|
const c2 = coordinates[1];
|
|
6766
6839
|
const c3 = coordinates[2];
|
|
@@ -6776,13 +6849,13 @@ function createSupportByFire(coordinates, options = {}) {
|
|
|
6776
6849
|
type: "FeatureCollection",
|
|
6777
6850
|
features: []
|
|
6778
6851
|
};
|
|
6779
|
-
const angleRad =
|
|
6852
|
+
const angleRad = rearLineAngle * Math.PI / 180;
|
|
6780
6853
|
const baseAngle = -Math.PI / 2;
|
|
6781
6854
|
const angleLeft = baseAngle - angleRad;
|
|
6782
6855
|
const dirBackLeft = [dirBase[0] * Math.cos(angleLeft) - dirBase[1] * Math.sin(angleLeft), dirBase[0] * Math.sin(angleLeft) + dirBase[1] * Math.cos(angleLeft)];
|
|
6783
6856
|
const angleRight = baseAngle + angleRad;
|
|
6784
6857
|
const dirBackRight = [dirBase[0] * Math.cos(angleRight) - dirBase[1] * Math.sin(angleRight), dirBase[0] * Math.sin(angleRight) + dirBase[1] * Math.cos(angleRight)];
|
|
6785
|
-
const backLen = lenBase *
|
|
6858
|
+
const backLen = lenBase * rearLineLengthRatio;
|
|
6786
6859
|
const p1Back = vecAdd(p1, vecScale(dirBackLeft, backLen));
|
|
6787
6860
|
const p2Back = vecAdd(p2, vecScale(dirBackRight, backLen));
|
|
6788
6861
|
const vLeft = vecSub(p3, p1);
|
|
@@ -6877,8 +6950,8 @@ const SUPPORT_BY_FIRE = defineControlMeasure({
|
|
|
6877
6950
|
//#region src/generators/cm15-maneuver-areas/supportingAttack.ts
|
|
6878
6951
|
const DEFAULT_SUPPORTING_ATTACK_OPTIONS = {
|
|
6879
6952
|
shaftWidthRatio: DEFAULT_SHAFT_WIDTH_RATIO,
|
|
6880
|
-
|
|
6881
|
-
|
|
6953
|
+
smooth: false,
|
|
6954
|
+
smoothResolution: 5
|
|
6882
6955
|
};
|
|
6883
6956
|
const SUPPORTING_ATTACK_METADATA = {
|
|
6884
6957
|
id: "supporting-attack",
|
|
@@ -7208,6 +7281,127 @@ function applyLayer(target, layer) {
|
|
|
7208
7281
|
}
|
|
7209
7282
|
}
|
|
7210
7283
|
//#endregion
|
|
7284
|
+
//#region src/text-amplifiers.ts
|
|
7285
|
+
/**
|
|
7286
|
+
* The vocabulary of text amplifier fields: doctrinal code, milsymbol-style
|
|
7287
|
+
* friendly alias, and a human-readable label. The single source of truth for
|
|
7288
|
+
* both the runtime catalog and the derived alias→code lookup used by
|
|
7289
|
+
* {@link normalizeTextAmplifiers}.
|
|
7290
|
+
*/
|
|
7291
|
+
const TEXT_AMPLIFIER_FIELDS = {
|
|
7292
|
+
C: {
|
|
7293
|
+
alias: "quantity",
|
|
7294
|
+
label: "Quantity"
|
|
7295
|
+
},
|
|
7296
|
+
H: {
|
|
7297
|
+
alias: "additionalInformation",
|
|
7298
|
+
label: "Additional information"
|
|
7299
|
+
},
|
|
7300
|
+
N: {
|
|
7301
|
+
alias: "hostile",
|
|
7302
|
+
label: "Hostile (ENY) marker"
|
|
7303
|
+
},
|
|
7304
|
+
T: {
|
|
7305
|
+
alias: "uniqueDesignation",
|
|
7306
|
+
label: "Unique designation"
|
|
7307
|
+
},
|
|
7308
|
+
V: {
|
|
7309
|
+
alias: "type",
|
|
7310
|
+
label: "Equipment/type"
|
|
7311
|
+
},
|
|
7312
|
+
W: {
|
|
7313
|
+
alias: "dtg",
|
|
7314
|
+
label: "Date-time group / O/O"
|
|
7315
|
+
},
|
|
7316
|
+
X: {
|
|
7317
|
+
alias: "altitudeDepth",
|
|
7318
|
+
label: "Altitude/depth"
|
|
7319
|
+
},
|
|
7320
|
+
Y: {
|
|
7321
|
+
alias: "location",
|
|
7322
|
+
label: "Location"
|
|
7323
|
+
},
|
|
7324
|
+
AM: {
|
|
7325
|
+
alias: "distance",
|
|
7326
|
+
label: "Distance/range/radius"
|
|
7327
|
+
},
|
|
7328
|
+
AN: {
|
|
7329
|
+
alias: "azimuth",
|
|
7330
|
+
label: "Azimuth"
|
|
7331
|
+
},
|
|
7332
|
+
AP: {
|
|
7333
|
+
alias: "targetNumber",
|
|
7334
|
+
label: "Target number"
|
|
7335
|
+
},
|
|
7336
|
+
AS: {
|
|
7337
|
+
alias: "country",
|
|
7338
|
+
label: "Country code"
|
|
7339
|
+
}
|
|
7340
|
+
};
|
|
7341
|
+
/** Derived alias→code lookup, so the vocabulary table stays the single source of truth. */
|
|
7342
|
+
const CODE_BY_ALIAS = Object.fromEntries(Object.entries(TEXT_AMPLIFIER_FIELDS).map(([code, { alias }]) => [alias, code]));
|
|
7343
|
+
const CANONICAL_KEYS = new Set(Object.keys(TEXT_AMPLIFIER_FIELDS).flatMap((code) => [
|
|
7344
|
+
code,
|
|
7345
|
+
`${code}1`,
|
|
7346
|
+
`${code}2`
|
|
7347
|
+
]));
|
|
7348
|
+
const EMPTY_TEXT_AMPLIFIERS = Object.freeze({});
|
|
7349
|
+
/** Splits a key like `"country1"` into its base (`"country"`) and suffix (`"1"`). */
|
|
7350
|
+
function splitSuffix(key) {
|
|
7351
|
+
const last = key.charAt(key.length - 1);
|
|
7352
|
+
if (last === "1" || last === "2") return {
|
|
7353
|
+
base: key.slice(0, -1),
|
|
7354
|
+
suffix: last
|
|
7355
|
+
};
|
|
7356
|
+
return {
|
|
7357
|
+
base: key,
|
|
7358
|
+
suffix: ""
|
|
7359
|
+
};
|
|
7360
|
+
}
|
|
7361
|
+
/**
|
|
7362
|
+
* Resolves any accepted key (canonical or friendly alias, with an optional
|
|
7363
|
+
* `1`/`2` suffix) to its canonical key, or `undefined` if unknown — e.g.
|
|
7364
|
+
* `"T"` → `"T"`, `"uniqueDesignation1"` → `"T1"`, `"bogus"` → `undefined`.
|
|
7365
|
+
*/
|
|
7366
|
+
function canonicalTextAmplifierKey(key) {
|
|
7367
|
+
if (CANONICAL_KEYS.has(key)) return key;
|
|
7368
|
+
const { base, suffix } = splitSuffix(key);
|
|
7369
|
+
const code = CODE_BY_ALIAS[base];
|
|
7370
|
+
return code ? `${code}${suffix}` : void 0;
|
|
7371
|
+
}
|
|
7372
|
+
/**
|
|
7373
|
+
* Resolves a `TextAmplifiers` record (canonical keys, friendly aliases, or a
|
|
7374
|
+
* mix) to `CanonicalTextAmplifiers` keyed purely by doctrinal field code.
|
|
7375
|
+
*
|
|
7376
|
+
* - Friendly aliases (with an optional `1`/`2` suffix) map to their canonical
|
|
7377
|
+
* code (e.g. `country1` → `AS1`).
|
|
7378
|
+
* - Values are trimmed; entries that are empty/whitespace-only after
|
|
7379
|
+
* trimming, or not strings, are dropped.
|
|
7380
|
+
* - When both a canonical key and its friendly alias are present for the same
|
|
7381
|
+
* field + suffix, the **canonical key wins** regardless of insertion order.
|
|
7382
|
+
* This holds even for an empty/whitespace canonical value: it suppresses
|
|
7383
|
+
* (clears) the alias entry rather than letting the alias survive.
|
|
7384
|
+
* - Unknown keys are ignored.
|
|
7385
|
+
*
|
|
7386
|
+
* `undefined` or an empty input returns a shared frozen empty object.
|
|
7387
|
+
*/
|
|
7388
|
+
function normalizeTextAmplifiers(input) {
|
|
7389
|
+
if (!input) return EMPTY_TEXT_AMPLIFIERS;
|
|
7390
|
+
const canonicalPresent = /* @__PURE__ */ new Set();
|
|
7391
|
+
for (const [key, rawValue] of Object.entries(input)) if (typeof rawValue === "string" && CANONICAL_KEYS.has(key)) canonicalPresent.add(key);
|
|
7392
|
+
const result = {};
|
|
7393
|
+
for (const [key, rawValue] of Object.entries(input)) {
|
|
7394
|
+
if (typeof rawValue !== "string") continue;
|
|
7395
|
+
const value = rawValue.trim();
|
|
7396
|
+
if (value.length === 0) continue;
|
|
7397
|
+
const canonicalKey = canonicalTextAmplifierKey(key);
|
|
7398
|
+
if (!canonicalKey) continue;
|
|
7399
|
+
if (!CANONICAL_KEYS.has(key) && canonicalPresent.has(canonicalKey)) continue;
|
|
7400
|
+
result[canonicalKey] = value;
|
|
7401
|
+
}
|
|
7402
|
+
return Object.keys(result).length > 0 ? Object.freeze(result) : EMPTY_TEXT_AMPLIFIERS;
|
|
7403
|
+
}
|
|
7404
|
+
//#endregion
|
|
7211
7405
|
//#region src/renderControlMeasure.ts
|
|
7212
7406
|
/**
|
|
7213
7407
|
* Pure render: `ControlMeasure` → `ControlMeasureRender` (a
|
|
@@ -7233,7 +7427,8 @@ function dispatchControlMeasure(cm, opts) {
|
|
|
7233
7427
|
const definition = DEFINITIONS[cm.kind];
|
|
7234
7428
|
if (!validateInputContract(cm.controlPoints, definition.metadata, opts?.validationMode)) return EMPTY_COLLECTION;
|
|
7235
7429
|
const generator = definition.generator;
|
|
7236
|
-
|
|
7430
|
+
const textAmplifiers = normalizeTextAmplifiers(cm.textAmplifiers);
|
|
7431
|
+
return generator(cm.controlPoints, cm.options ?? {}, textAmplifiers);
|
|
7237
7432
|
}
|
|
7238
7433
|
/**
|
|
7239
7434
|
* The control-point *input contract*: at least `metadata.minCoordinates` points,
|
|
@@ -7259,6 +7454,7 @@ function normalizeFeature(id, feature, index, measureStyle, graphicsStyle) {
|
|
|
7259
7454
|
const textSizePixels = typeof sourceProps.textSizePixels === "number" ? sourceProps.textSizePixels : void 0;
|
|
7260
7455
|
const textSizeZoom = typeof sourceProps.textSizeZoom === "number" ? sourceProps.textSizeZoom : void 0;
|
|
7261
7456
|
const textSizeResolution = typeof sourceProps.textSizeResolution === "number" ? sourceProps.textSizeResolution : void 0;
|
|
7457
|
+
const textAnchor = sourceProps.textAnchor === "start" || sourceProps.textAnchor === "end" ? sourceProps.textAnchor : void 0;
|
|
7262
7458
|
return {
|
|
7263
7459
|
type: "Feature",
|
|
7264
7460
|
id: `${id}:${part}:${index}`,
|
|
@@ -7268,6 +7464,7 @@ function normalizeFeature(id, feature, index, measureStyle, graphicsStyle) {
|
|
|
7268
7464
|
style,
|
|
7269
7465
|
...text !== void 0 ? { text } : {},
|
|
7270
7466
|
...rotation !== void 0 ? { rotation } : {},
|
|
7467
|
+
...textAnchor !== void 0 ? { textAnchor } : {},
|
|
7271
7468
|
...textSizePixels !== void 0 ? { textSizePixels } : {},
|
|
7272
7469
|
...textSizeZoom !== void 0 ? { textSizeZoom } : {},
|
|
7273
7470
|
...textSizeResolution !== void 0 ? { textSizeResolution } : {}
|
|
@@ -7321,4 +7518,4 @@ function assertNever(value) {
|
|
|
7321
7518
|
throw new Error(`Unhandled control measure kind: ${String(value)}`);
|
|
7322
7519
|
}
|
|
7323
7520
|
//#endregion
|
|
7324
|
-
export {
|
|
7521
|
+
export { supportByFireDrawRule as $, DEFAULT_DELAY_OPTIONS as A, DEFAULT_BLOCK_MISSION_TASK_OPTIONS as B, DEFAULT_FORTIFIED_AREA_OPTIONS as C, DEFAULT_FINAL_PROTECTIVE_FIRE_OPTIONS as D, DEFAULT_FIX_OPTIONS as E, DEFAULT_GENERIC_CIRCLE_OPTIONS as F, DEFAULT_ATTACK_HELICOPTER_OPTIONS as G, DEFAULT_BLOCK_ARROW_OPTIONS as H, DEFAULT_CLASSIC_ARROW_OPTIONS as I, DEFAULT_ANTITANK_DITCH_OPTIONS as J, DEFAULT_ATTACK_BY_FIRE_OPTIONS as K, DEFAULT_CANALIZE_OPTIONS as L, DEFAULT_GENERIC_RECTANGLE_OPTIONS as M, DEFAULT_GENERIC_POLYGON_OPTIONS as N, DEFAULT_ENCIRCLEMENT_OPTIONS as O, DEFAULT_GENERIC_LINE_OPTIONS as P, axis1DrawRule as Q, DEFAULT_BYPASS_OPTIONS as R, DEFAULT_MAIN_ATTACK_OPTIONS as S, roundToFixed as St, DEFAULT_FLOT_OPTIONS as T, DEFAULT_BLOCK_OPTIONS as U, DEFAULT_BOUNDARY_OPTIONS as V, DEFAULT_BATTLE_POSITION_OPTIONS as W, DEFAULT_AIRBORNE_ATTACK_OPTIONS as X, DEFAULT_AMBUSH_OPTIONS as Y, rectangleDrawRule as Z, DEFAULT_TACTICAL_ARROW_OPTIONS as _, computeInitialWidthPoint as _t, resolveStyleHints as a, attackByFireDrawRule as at, DEFAULT_OBSTACLE_BYPASS_EASY_OPTIONS as b, EPSILON as bt, DEFINITIONS as c, disruptDrawRule as ct, getDefaultOptions as d, createMidpointPerpendicularDrawRule as dt, line24DrawRule as et, listControlMeasureMetadata as f, getMidpointPerpendicularSignedDistance as ft, DEFAULT_STRONG_POINT_OPTIONS as g, calculateMetrics as gt, DEFAULT_SUPPORT_BY_FIRE_OPTIONS as h, createBaselineFrame as ht, normalizeTextAmplifiers as i, ambushDrawRule as it, DEFAULT_CLEAR_OPTIONS as j, DEFAULT_DISRUPT_OPTIONS as k, getControlMeasureMetadata as l, blockDrawRule as lt, DEFAULT_SUPPORTING_ATTACK_OPTIONS as m, snapToMidpointPerpendicular as mt, TEXT_AMPLIFIER_FIELDS as n, turnDrawRule as nt, CONTROL_MEASURE_IDS as o, point12DrawRule as ot, DEFAULT_TURN_OPTIONS as p, pointOnMidpointPerpendicularAxis as pt, DEFAULT_ANTITANK_WALL_OPTIONS as q, canonicalTextAmplifierKey as r, line1DrawRule as rt, CONTROL_MEASURE_METADATA as s, centerRadiusDrawRule as st, renderControlMeasure as t, line23DrawRule as tt, getControlMeasureMetadataByValue as u, computeDefaultMidpointPerpendicularPoint as ut, DEFAULT_PRINCIPAL_DIRECTION_OF_FIRE_OPTIONS as v, project as vt, DEFAULT_FORTIFIED_LINE_OPTIONS as w, DEFAULT_OBSTACLE_BYPASS_DIFFICULT_OPTIONS as x, getMetersPerPixel as xt, DEFAULT_OBSTACLE_BYPASS_IMPOSSIBLE_OPTIONS as y, unproject as yt, DEFAULT_BREACH_OPTIONS as z };
|