@orbat-mapper/control-measures 0.20.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2621,6 +2621,408 @@ function fixedLabelLineFeatures(positions, options, textForEnd, hostileText, con
2621
2621
  features
2622
2622
  };
2623
2623
  }
2624
+ function normalizeArcResolution(value) {
2625
+ if (typeof value !== "number" || !Number.isFinite(value)) return 64;
2626
+ return Math.min(360, Math.max(4, Math.round(value)));
2627
+ }
2628
+ const ARC_RESOLUTION_PARAMS = [{
2629
+ key: "resolution",
2630
+ presentationTier: "advanced",
2631
+ label: "Resolution",
2632
+ description: "Number of segments in a full 360° sweep.",
2633
+ type: "number",
2634
+ min: 4,
2635
+ max: 360,
2636
+ step: 1
2637
+ }];
2638
+ //#endregion
2639
+ //#region src/generators/cm29-protection-lines/params.ts
2640
+ const FORTIFIED_PARAMS = [
2641
+ ...SMOOTH_LINE_PARAMS,
2642
+ {
2643
+ key: "toothSizePixels",
2644
+ label: "Tooth size",
2645
+ description: "Side length of the square crenellations",
2646
+ type: "number",
2647
+ min: 5,
2648
+ max: 50,
2649
+ step: 1,
2650
+ unit: "px"
2651
+ },
2652
+ {
2653
+ key: "toothSize",
2654
+ label: "Tooth size",
2655
+ description: "Side length of the square crenellations",
2656
+ type: "number",
2657
+ min: 1,
2658
+ max: 500,
2659
+ step: 1,
2660
+ unit: "m"
2661
+ },
2662
+ LABEL_PADDING_PARAM
2663
+ ];
2664
+ const ANTITANK_DITCH_PARAMS = [
2665
+ ...SMOOTH_LINE_PARAMS,
2666
+ {
2667
+ key: "toothHeightPixels",
2668
+ label: "Tooth height",
2669
+ description: "Perpendicular depth of each triangular tooth",
2670
+ type: "number",
2671
+ min: 5,
2672
+ max: 50,
2673
+ step: 1,
2674
+ unit: "px"
2675
+ },
2676
+ {
2677
+ key: "toothHeight",
2678
+ label: "Tooth height",
2679
+ description: "Perpendicular depth of each triangular tooth",
2680
+ type: "number",
2681
+ min: 1,
2682
+ max: 500,
2683
+ step: 1,
2684
+ unit: "m"
2685
+ },
2686
+ {
2687
+ key: "toothWidthRatio",
2688
+ presentationTier: "advanced",
2689
+ label: "Tooth width ratio",
2690
+ description: "Tooth base span as a multiple of height",
2691
+ type: "number",
2692
+ min: .5,
2693
+ max: 3,
2694
+ step: .05,
2695
+ unit: "x"
2696
+ },
2697
+ LABEL_PADDING_PARAM
2698
+ ];
2699
+ const ANTITANK_WALL_PARAMS = [...ANTITANK_DITCH_PARAMS, {
2700
+ key: "toothSpacingRatio",
2701
+ presentationTier: "advanced",
2702
+ label: "Tooth spacing ratio",
2703
+ description: "Gap between teeth as a multiple of base width",
2704
+ type: "number",
2705
+ min: 0,
2706
+ max: 3,
2707
+ step: .05,
2708
+ unit: "x"
2709
+ }];
2710
+ const WIRE_MARKER_PARAMS = [{
2711
+ key: "markerSize",
2712
+ label: "Marker size",
2713
+ description: "Full height of each wire marker in meters",
2714
+ type: "number",
2715
+ min: 1,
2716
+ max: 500,
2717
+ step: 1,
2718
+ unit: "m"
2719
+ }, {
2720
+ key: "markerSizePixels",
2721
+ label: "Marker size",
2722
+ description: "Full height of each wire marker in pixels",
2723
+ type: "number",
2724
+ min: 1,
2725
+ max: 50,
2726
+ step: 1,
2727
+ unit: "px"
2728
+ }];
2729
+ /** The lower bound varies per wire kind, so callers spread this and override `min`. */
2730
+ const WIRE_SPACING_PARAM = {
2731
+ key: "markerSpacingRatio",
2732
+ label: "Marker spacing",
2733
+ description: "Target distance between marker groups as a multiple of marker height",
2734
+ presentationTier: "advanced",
2735
+ type: "number",
2736
+ min: .8,
2737
+ max: 10,
2738
+ step: .1
2739
+ };
2740
+ //#endregion
2741
+ //#region src/generators/cm29-protection-lines/wire-obstacles.ts
2742
+ const DEFAULT_MARKER_SIZE = 100;
2743
+ const DEFAULT_MARKER_SIZE_PIXELS = 20;
2744
+ const WIRE_KINDS = {
2745
+ unspecified: {
2746
+ subtype: "Unspecified",
2747
+ value: "290301",
2748
+ description: "A wire obstacle whose fence type is unspecified.",
2749
+ spacing: 1.5,
2750
+ minSpacing: .8,
2751
+ concertina: false,
2752
+ oneSided: false,
2753
+ paired: false,
2754
+ rails: 0,
2755
+ railSeparation: 0,
2756
+ previewMarkerSize: 96
2757
+ },
2758
+ single: {
2759
+ subtype: "Single Fence",
2760
+ value: "290302",
2761
+ description: "A wire obstacle consisting of a single fence.",
2762
+ spacing: 3,
2763
+ minSpacing: .8,
2764
+ concertina: false,
2765
+ oneSided: false,
2766
+ paired: false,
2767
+ rails: 1,
2768
+ railSeparation: 0,
2769
+ previewMarkerSize: 115
2770
+ },
2771
+ double: {
2772
+ subtype: "Double Fence",
2773
+ value: "290303",
2774
+ description: "A wire obstacle consisting of a double fence.",
2775
+ spacing: 3.6,
2776
+ minSpacing: 1.6,
2777
+ concertina: false,
2778
+ oneSided: false,
2779
+ paired: true,
2780
+ rails: 1,
2781
+ railSeparation: 0,
2782
+ previewMarkerSize: 115
2783
+ },
2784
+ apron: {
2785
+ subtype: "Double Apron Fence",
2786
+ value: "290304",
2787
+ description: "A wire fence with an apron on each side.",
2788
+ spacing: 1.5,
2789
+ minSpacing: .8,
2790
+ concertina: false,
2791
+ oneSided: false,
2792
+ paired: false,
2793
+ rails: 1,
2794
+ railSeparation: 0,
2795
+ previewMarkerSize: 96
2796
+ },
2797
+ low: {
2798
+ subtype: "Low Wire Fence",
2799
+ value: "290305",
2800
+ description: "A low wire fence obstacle.",
2801
+ spacing: 1.5,
2802
+ minSpacing: .8,
2803
+ concertina: false,
2804
+ oneSided: true,
2805
+ paired: false,
2806
+ rails: 1,
2807
+ railSeparation: 0,
2808
+ previewMarkerSize: 96
2809
+ },
2810
+ high: {
2811
+ subtype: "High Wire Fence",
2812
+ value: "290306",
2813
+ description: "A high wire fence obstacle.",
2814
+ spacing: 1.5,
2815
+ minSpacing: .8,
2816
+ concertina: false,
2817
+ oneSided: true,
2818
+ paired: false,
2819
+ rails: 2,
2820
+ railSeparation: 1,
2821
+ previewMarkerSize: 96
2822
+ },
2823
+ concertina: {
2824
+ subtype: "Single Concertina",
2825
+ value: "290307",
2826
+ description: "A wire obstacle consisting of a single concertina coil.",
2827
+ spacing: 1.25,
2828
+ minSpacing: .8,
2829
+ concertina: true,
2830
+ oneSided: true,
2831
+ paired: false,
2832
+ rails: 1,
2833
+ railSeparation: 0,
2834
+ previewMarkerSize: 96
2835
+ },
2836
+ "double-concertina": {
2837
+ subtype: "Double Strand Concertina",
2838
+ value: "290308",
2839
+ description: "A wire obstacle consisting of double-strand concertina.",
2840
+ spacing: 1.25,
2841
+ minSpacing: .8,
2842
+ concertina: true,
2843
+ oneSided: true,
2844
+ paired: false,
2845
+ rails: 2,
2846
+ railSeparation: .5,
2847
+ previewMarkerSize: 96
2848
+ },
2849
+ "triple-concertina": {
2850
+ subtype: "Triple Strand Concertina",
2851
+ value: "290309",
2852
+ description: "A wire obstacle consisting of triple-strand concertina.",
2853
+ spacing: 1.25,
2854
+ minSpacing: .8,
2855
+ concertina: true,
2856
+ oneSided: true,
2857
+ paired: false,
2858
+ rails: 2,
2859
+ railSeparation: 1,
2860
+ previewMarkerSize: 96
2861
+ }
2862
+ };
2863
+ function createWireObstacle(positions, options, text, context, kind) {
2864
+ const spec = WIRE_KINDS[kind];
2865
+ const projected = positions.map(([x, y]) => project(x, y));
2866
+ const path = smoothLineVerts(projected.filter((point, index) => index === 0 || Math.hypot(point[0] - projected[index - 1][0], point[1] - projected[index - 1][1]) >= 1e-6), options, 16);
2867
+ const { segments, totalLength } = polylineSegments(path);
2868
+ if (totalLength < 1e-6) return {
2869
+ type: "FeatureCollection",
2870
+ features: []
2871
+ };
2872
+ const resolution = normalizeArcResolution(options.resolution);
2873
+ const size = Math.max(EPSILON, options.markerSizePixels !== void 0 && (options.metersPerPixel ?? 0) > 0 ? options.markerSizePixels * options.metersPerPixel : options.markerSize ?? DEFAULT_MARKER_SIZE);
2874
+ const pitch = Math.max(spec.minSpacing, options.markerSpacingRatio ?? spec.spacing) * size;
2875
+ const halfWidth = .4 * size;
2876
+ const pairOffset = spec.paired ? halfWidth : 0;
2877
+ const corners = [];
2878
+ let distance = 0;
2879
+ for (let i = 0; i < segments.length - 1; i++) {
2880
+ const current = segments[i];
2881
+ const next = segments[i + 1];
2882
+ distance += current.length;
2883
+ const dot = current.along[0] * next.along[0] + current.along[1] * next.along[1];
2884
+ if (dot >= .95) continue;
2885
+ const turn = current.along[0] * next.along[1] - current.along[1] * next.along[0];
2886
+ const tangent = Math.sqrt((1 - dot) / Math.max(EPSILON, 1 + dot));
2887
+ const insideTangent = spec.oneSided && turn >= 0 ? 0 : tangent;
2888
+ const clearance = spec.concertina ? size * (insideTangent / 2 + Math.hypot(.25, insideTangent / 2)) : halfWidth + pairOffset + size * (spec.oneSided ? 1 : .5) * insideTangent;
2889
+ corners.push({
2890
+ distance,
2891
+ clearance: Math.max(clearance, pitch / 2)
2892
+ });
2893
+ }
2894
+ const inset = Math.min(totalLength / 2, halfWidth + pairOffset + (spec.rails > 0 ? .25 * size : 0));
2895
+ const centers = [];
2896
+ const spread = (start, end, count) => {
2897
+ for (let i = 0; i < count; i++) centers.push(count === 1 ? (start + end) / 2 : start + i * (end - start) / (count - 1));
2898
+ };
2899
+ if (corners.length === 0) spread(inset, totalLength - inset, Math.max(1, Math.round(totalLength / pitch)));
2900
+ else {
2901
+ let start = inset;
2902
+ for (const corner of [...corners, {
2903
+ distance: totalLength,
2904
+ clearance: inset
2905
+ }]) {
2906
+ const end = corner.distance - corner.clearance;
2907
+ if (end >= start) spread(start, end, Math.max(1, Math.round((end - start) / pitch) + 1));
2908
+ start = corner.distance + corner.clearance;
2909
+ }
2910
+ }
2911
+ const coil = spec.concertina ? Array.from({ length: resolution + 1 }, (_, index) => {
2912
+ const angle = index / resolution * Math.PI * 2;
2913
+ return [.25 * size * Math.cos(angle), -size / 2 + size / 2 * Math.sin(angle)];
2914
+ }) : [];
2915
+ const markers = [];
2916
+ for (const center of centers) for (const offset of spec.paired ? [-pairOffset, pairOffset] : [0]) {
2917
+ const { point, along, perp } = pointAlongPolyline(segments, totalLength, (center + offset) / totalLength);
2918
+ const at = (x, y) => toPosition([point[0] + along[0] * x + perp[0] * y, point[1] + along[1] * x + perp[1] * y]);
2919
+ if (spec.concertina) {
2920
+ const loop = coil.map(([x, y]) => at(x, y));
2921
+ loop[loop.length - 1] = loop[0];
2922
+ markers.push(loop);
2923
+ continue;
2924
+ }
2925
+ const lift = spec.oneSided ? -size / 2 : 0;
2926
+ markers.push([at(-halfWidth, lift - size / 2), at(halfWidth, lift + size / 2)], [at(-halfWidth, lift + size / 2), at(halfWidth, lift - size / 2)]);
2927
+ }
2928
+ const features = [];
2929
+ if (spec.rails > 0) {
2930
+ const coordinates = [path.map(toPosition)];
2931
+ if (spec.rails === 2) {
2932
+ const { right } = offsetPolylineSides(path, path.map(() => spec.railSeparation * size));
2933
+ coordinates.push(right.map(toPosition));
2934
+ }
2935
+ features.push({
2936
+ type: "Feature",
2937
+ properties: { part: "wire" },
2938
+ geometry: {
2939
+ type: "MultiLineString",
2940
+ coordinates
2941
+ }
2942
+ });
2943
+ }
2944
+ features.push({
2945
+ type: "Feature",
2946
+ properties: { part: spec.concertina ? "loops" : "crosses" },
2947
+ geometry: {
2948
+ type: "MultiLineString",
2949
+ coordinates: markers
2950
+ }
2951
+ });
2952
+ const labels = [];
2953
+ pushHostileEndLabels(labels, path, text.N, options, "", context);
2954
+ return {
2955
+ type: "FeatureCollection",
2956
+ features: [...features, ...labels]
2957
+ };
2958
+ }
2959
+ function definition$1(id, name, kind) {
2960
+ const spec = WIRE_KINDS[kind];
2961
+ return defineControlMeasure({
2962
+ metadata: {
2963
+ id,
2964
+ name,
2965
+ description: spec.description,
2966
+ entity: "Protection Lines",
2967
+ entityType: "Wire Obstacles",
2968
+ entitySubtype: spec.subtype,
2969
+ value: spec.value,
2970
+ minCoordinates: 2,
2971
+ geometry: "line",
2972
+ geometryTypes: ["MultiLineString", "Point"],
2973
+ paints: {
2974
+ stroke: true,
2975
+ fill: "none",
2976
+ text: true
2977
+ },
2978
+ drawRule: "Line1",
2979
+ capturesLabelSize: true,
2980
+ labelGeometryUsesResolvedMetrics: ["N"],
2981
+ textAmplifiers: [HOSTILE_LINE_AMPLIFIER],
2982
+ sizePairs: [{
2983
+ id: "cross-size",
2984
+ label: "Marker size",
2985
+ pixels: "markerSizePixels",
2986
+ meters: "markerSize",
2987
+ defaultUnit: "m",
2988
+ rendering: "geometry"
2989
+ }],
2990
+ params: [
2991
+ ...SMOOTH_LINE_PARAMS,
2992
+ ...spec.concertina ? ARC_RESOLUTION_PARAMS : [],
2993
+ ...WIRE_MARKER_PARAMS,
2994
+ {
2995
+ ...WIRE_SPACING_PARAM,
2996
+ min: spec.minSpacing
2997
+ },
2998
+ LABEL_PADDING_PARAM
2999
+ ]
3000
+ },
3001
+ generator: (positions, options = {}, text = {}, context = {}) => createWireObstacle(positions, options, text, context, kind),
3002
+ defaultOptions: {
3003
+ ...spec.concertina ? { resolution: 64 } : {},
3004
+ markerSize: DEFAULT_MARKER_SIZE,
3005
+ markerSizePixels: DEFAULT_MARKER_SIZE_PIXELS,
3006
+ markerSpacingRatio: spec.spacing,
3007
+ smooth: false,
3008
+ smoothResolution: 16
3009
+ },
3010
+ rule: line1DrawRule,
3011
+ previewSample: {
3012
+ controlPoints: [[1, 0], [-1, 0]],
3013
+ options: { markerSize: spec.previewMarkerSize }
3014
+ }
3015
+ });
3016
+ }
3017
+ const WIRE_OBSTACLE_UNSPECIFIED = definition$1("wire-obstacle-unspecified", "Wire Obstacles – Unspecified", "unspecified");
3018
+ const SINGLE_FENCE = definition$1("single-fence", "Single Fence", "single");
3019
+ const DOUBLE_FENCE = definition$1("double-fence", "Double Fence", "double");
3020
+ const DOUBLE_APRON_FENCE = definition$1("double-apron-fence", "Double Apron Fence", "apron");
3021
+ const LOW_WIRE_FENCE = definition$1("low-wire-fence", "Low Wire Fence", "low");
3022
+ const HIGH_WIRE_FENCE = definition$1("high-wire-fence", "High Wire Fence", "high");
3023
+ const SINGLE_CONCERTINA = definition$1("single-concertina", "Single Concertina", "concertina");
3024
+ const DOUBLE_STRAND_CONCERTINA = definition$1("double-strand-concertina", "Double Strand Concertina", "double-concertina");
3025
+ const TRIPLE_STRAND_CONCERTINA = definition$1("triple-strand-concertina", "Triple Strand Concertina", "triple-concertina");
2624
3026
  //#endregion
2625
3027
  //#region src/generators/cm34-mission-tasks/shared.ts
2626
3028
  /**
@@ -2745,8 +3147,16 @@ function createBracketSymbol(coordinates, config, context) {
2745
3147
  }, createLabelFeature(rearMidpoint, config.labelText, labelRotation, config.labelOptions)]
2746
3148
  };
2747
3149
  }
2748
- //#endregion
2749
- //#region src/generators/cm34-mission-tasks/cross-task-shared.ts
3150
+ /** The single sized dimension every cross-symbol mission task shares. */
3151
+ const CROSS_TASK_SIZE_PAIR = {
3152
+ id: "size",
3153
+ label: "Size",
3154
+ pixels: "sizePixels",
3155
+ meters: "sizeMeters",
3156
+ defaultUnit: "px",
3157
+ rendering: "geometry",
3158
+ fallbackMeters: 1e3
3159
+ };
2750
3160
  const DEFAULT_CROSS_TASK_OPTIONS = {
2751
3161
  sizePixels: 80,
2752
3162
  rotation: 0,
@@ -2986,6 +3396,7 @@ const DEFAULT_DEFEAT_OPTIONS = {
2986
3396
  };
2987
3397
  const DEFEAT_METADATA = {
2988
3398
  id: "defeat",
3399
+ sizePairs: [CROSS_TASK_SIZE_PAIR],
2989
3400
  name: "Defeat",
2990
3401
  description: "Renders an enemy force unable or unwilling to achieve its objectives.",
2991
3402
  entity: "Mission Tasks",
@@ -3032,6 +3443,7 @@ const DEFEAT = defineControlMeasure({
3032
3443
  const DEFAULT_SUPPRESS_OPTIONS = DEFAULT_CROSS_TASK_OPTIONS;
3033
3444
  const SUPPRESS_METADATA = {
3034
3445
  id: "suppress",
3446
+ sizePairs: [CROSS_TASK_SIZE_PAIR],
3035
3447
  name: "Suppress",
3036
3448
  description: "Temporarily degrades an enemy force or weapon system below the level needed to accomplish its mission.",
3037
3449
  entity: "Mission Tasks",
@@ -3071,6 +3483,7 @@ const SUPPRESS = defineControlMeasure({
3071
3483
  const DEFAULT_NEUTRALIZE_OPTIONS = DEFAULT_CROSS_TASK_OPTIONS;
3072
3484
  const NEUTRALIZE_METADATA = {
3073
3485
  id: "neutralize",
3486
+ sizePairs: [CROSS_TASK_SIZE_PAIR],
3074
3487
  name: "Neutralize",
3075
3488
  description: "Renders enemy personnel or materiel incapable of interfering with an operation.",
3076
3489
  entity: "Mission Tasks",
@@ -3110,6 +3523,7 @@ const NEUTRALIZE = defineControlMeasure({
3110
3523
  const DEFAULT_DESTROY_OPTIONS = DEFAULT_CROSS_TASK_OPTIONS;
3111
3524
  const DESTROY_METADATA = {
3112
3525
  id: "destroy",
3526
+ sizePairs: [CROSS_TASK_SIZE_PAIR],
3113
3527
  name: "Destroy",
3114
3528
  description: "Renders an enemy force or capability physically combat-ineffective until reconstituted.",
3115
3529
  entity: "Mission Tasks",
@@ -3155,6 +3569,15 @@ const DESTROY = defineControlMeasure({
3155
3569
  * monocolor (ADR-0023). Every glyph is built from three primitives — vertical
3156
3570
  * bars, crosses, and dots — plus the team/crew ring and command `++`.
3157
3571
  */
3572
+ /** Shared echelon dimension for every measure that stamps a unit-size amplifier. */
3573
+ const ECHELON_SIZE_PAIR = {
3574
+ id: "echelon",
3575
+ label: "Echelon size",
3576
+ pixels: "echelonSizePixels",
3577
+ meters: "echelonSize",
3578
+ defaultUnit: "m",
3579
+ rendering: "geometry"
3580
+ };
3158
3581
  const ECHELONS = {
3159
3582
  team: {
3160
3583
  kind: "ring",
@@ -6216,6 +6639,7 @@ const freezeEchelonLabeledAreaAnchor = makeFreezeEchelonAnchor({
6216
6639
  const DEFAULT_BASE_CAMP_OPTIONS = DEFAULT_ECHELON_LABELED_AREA_OPTIONS;
6217
6640
  const BASE_CAMP_METADATA = {
6218
6641
  id: "base-camp",
6642
+ sizePairs: [ECHELON_SIZE_PAIR],
6219
6643
  name: "Base camp",
6220
6644
  description: "An evolving military facility that supports military operations of a deployed unit and provides the necessary support and services for sustained operations.",
6221
6645
  entity: "Command and Control Areas",
@@ -6275,6 +6699,7 @@ const BASE_CAMP = defineControlMeasure({
6275
6699
  const DEFAULT_GUERRILLA_BASE_OPTIONS = DEFAULT_ECHELON_LABELED_AREA_OPTIONS;
6276
6700
  const GUERRILLA_BASE_METADATA = {
6277
6701
  id: "guerrilla-base",
6702
+ sizePairs: [ECHELON_SIZE_PAIR],
6278
6703
  name: "Guerrilla base",
6279
6704
  description: "A temporary site where guerrilla installations, headquarters, and units are located.",
6280
6705
  entity: "Command and Control Areas",
@@ -6444,82 +6869,19 @@ const ASSEMBLY_AREA = defineControlMeasure({
6444
6869
  }
6445
6870
  });
6446
6871
  //#endregion
6447
- //#region src/generators/cm29-protection-lines/params.ts
6448
- const FORTIFIED_PARAMS = [
6449
- ...SMOOTH_LINE_PARAMS,
6450
- {
6451
- key: "toothSizePixels",
6452
- label: "Tooth size",
6453
- description: "Side length of the square crenellations",
6454
- type: "number",
6455
- min: 5,
6456
- max: 50,
6457
- step: 1,
6458
- unit: "px"
6459
- },
6460
- {
6461
- key: "toothSize",
6462
- label: "Tooth size",
6463
- description: "Side length of the square crenellations",
6464
- type: "number",
6465
- min: 1,
6466
- max: 500,
6467
- step: 1,
6468
- unit: "m"
6469
- },
6470
- LABEL_PADDING_PARAM
6471
- ];
6472
- const ANTITANK_DITCH_PARAMS = [
6473
- ...SMOOTH_LINE_PARAMS,
6474
- {
6475
- key: "toothHeightPixels",
6476
- label: "Tooth height",
6477
- description: "Perpendicular depth of each triangular tooth",
6478
- type: "number",
6479
- min: 5,
6480
- max: 50,
6481
- step: 1,
6482
- unit: "px"
6483
- },
6484
- {
6485
- key: "toothHeight",
6486
- label: "Tooth height",
6487
- description: "Perpendicular depth of each triangular tooth",
6488
- type: "number",
6489
- min: 1,
6490
- max: 500,
6491
- step: 1,
6492
- unit: "m"
6493
- },
6494
- {
6495
- key: "toothWidthRatio",
6496
- presentationTier: "advanced",
6497
- label: "Tooth width ratio",
6498
- description: "Tooth base span as a multiple of height",
6499
- type: "number",
6500
- min: .5,
6501
- max: 3,
6502
- step: .05,
6503
- unit: "x"
6504
- },
6505
- LABEL_PADDING_PARAM
6506
- ];
6507
- const ANTITANK_WALL_PARAMS = [...ANTITANK_DITCH_PARAMS, {
6508
- key: "toothSpacingRatio",
6509
- presentationTier: "advanced",
6510
- label: "Tooth spacing ratio",
6511
- description: "Gap between teeth as a multiple of base width",
6512
- type: "number",
6513
- min: 0,
6514
- max: 3,
6515
- step: .05,
6516
- unit: "x"
6517
- }];
6518
- //#endregion
6519
6872
  //#region src/generators/cm29-protection-lines/antitankDitch.ts
6520
6873
  const DEFAULT_TOOTH_HEIGHT = 50;
6521
6874
  const DEFAULT_TOOTH_HEIGHT_PIXELS = 10;
6522
6875
  const DEFAULT_TOOTH_WIDTH_RATIO$1 = 1.15;
6876
+ /** Shared by both ditch variants and by the antitank wall built on the same teeth. */
6877
+ const TOOTH_HEIGHT_SIZE_PAIR = {
6878
+ id: "tooth-height",
6879
+ label: "Tooth height",
6880
+ pixels: "toothHeightPixels",
6881
+ meters: "toothHeight",
6882
+ defaultUnit: "m",
6883
+ rendering: "geometry"
6884
+ };
6523
6885
  const DEFAULT_ANTITANK_DITCH_OPTIONS = {
6524
6886
  toothHeight: DEFAULT_TOOTH_HEIGHT,
6525
6887
  toothHeightPixels: DEFAULT_TOOTH_HEIGHT_PIXELS,
@@ -6529,6 +6891,7 @@ const DEFAULT_ANTITANK_DITCH_OPTIONS = {
6529
6891
  };
6530
6892
  const ANTITANK_DITCH_UNDER_CONSTRUCTION_METADATA = {
6531
6893
  id: "antitank-ditch-under-construction",
6894
+ sizePairs: [TOOTH_HEIGHT_SIZE_PAIR],
6532
6895
  name: "Ditch - Under Construction",
6533
6896
  description: "Antitank ditch obstacle under construction.",
6534
6897
  entity: "Protection Lines",
@@ -6551,6 +6914,7 @@ const ANTITANK_DITCH_UNDER_CONSTRUCTION_METADATA = {
6551
6914
  };
6552
6915
  const ANTITANK_DITCH_COMPLETED_METADATA = {
6553
6916
  id: "antitank-ditch-completed",
6917
+ sizePairs: [TOOTH_HEIGHT_SIZE_PAIR],
6554
6918
  name: "Ditch - Completed",
6555
6919
  description: "Completed antitank ditch obstacle.",
6556
6920
  entity: "Protection Lines",
@@ -6684,6 +7048,7 @@ const DEFAULT_ANTITANK_WALL_OPTIONS = {
6684
7048
  };
6685
7049
  const ANTITANK_WALL_METADATA = {
6686
7050
  id: "antitank-wall",
7051
+ sizePairs: [TOOTH_HEIGHT_SIZE_PAIR],
6687
7052
  name: "Antitank Wall",
6688
7053
  description: "Antitank wall obstacle.",
6689
7054
  entity: "Protection Lines",
@@ -7148,6 +7513,7 @@ const DEFAULT_BATTLE_POSITION_OPTIONS = {
7148
7513
  };
7149
7514
  const BATTLE_POSITION_METADATA = {
7150
7515
  id: "battle-position",
7516
+ sizePairs: [ECHELON_SIZE_PAIR],
7151
7517
  name: "Battle Position",
7152
7518
  description: "A defensive location oriented on a likely enemy avenue of approach, labeled with the echelon of the occupying unit.",
7153
7519
  entity: "Maneuver Areas",
@@ -7291,6 +7657,7 @@ const BATTLE_POSITION_PREPARED_DASH = [6, 4];
7291
7657
  const DEFAULT_BATTLE_POSITION_PREPARED_OPTIONS = { ...DEFAULT_BATTLE_POSITION_OPTIONS };
7292
7658
  const BATTLE_POSITION_PREPARED_METADATA = {
7293
7659
  id: "battle-position-prepared",
7660
+ sizePairs: [ECHELON_SIZE_PAIR],
7294
7661
  name: "Battle Position Prepared (P) but not Occupied",
7295
7662
  description: "A battle position that has been prepared for occupation but is not yet occupied, drawn with a dashed boundary and a (P)-prefixed designation.",
7296
7663
  entity: "Maneuver Areas",
@@ -8258,6 +8625,7 @@ const DEFAULT_BOUNDARY_OPTIONS = {
8258
8625
  };
8259
8626
  const BOUNDARY_METADATA = {
8260
8627
  id: "boundary",
8628
+ sizePairs: [ECHELON_SIZE_PAIR],
8261
8629
  name: "Boundary",
8262
8630
  description: "Delineates the area of responsibility between two adjacent units, labeled with the echelon and the designators of the units it separates.",
8263
8631
  entity: "Command and Control Lines",
@@ -8675,6 +9043,7 @@ const DEFAULT_GENERIC_C2_LINE_OPTIONS = {
8675
9043
  };
8676
9044
  const GENERIC_C2_LINE_METADATA = {
8677
9045
  id: "generic-c2-line",
9046
+ sizePairs: [ECHELON_SIZE_PAIR],
8678
9047
  name: "Generic command and control line",
8679
9048
  description: "Illustrative command-and-control-style line (not part of MIL-STD-2525E) carrying optional echelon glyphs, additional-information/designation text above it, and an effective date-time-group window below it.",
8680
9049
  entity: "Command and Control Lines",
@@ -9532,20 +9901,6 @@ function normalizeSmoothResolution(value) {
9532
9901
  if (!Number.isFinite(value)) return DEFAULT_SMOOTH_RESOLUTION$3;
9533
9902
  return Math.min(MAX_SMOOTH_RESOLUTION, Math.max(MIN_SMOOTH_RESOLUTION, Math.round(value)));
9534
9903
  }
9535
- function normalizeArcResolution(value) {
9536
- if (typeof value !== "number" || !Number.isFinite(value)) return 64;
9537
- return Math.min(360, Math.max(4, Math.round(value)));
9538
- }
9539
- const ARC_RESOLUTION_PARAMS = [{
9540
- key: "resolution",
9541
- presentationTier: "advanced",
9542
- label: "Resolution",
9543
- description: "Number of segments in a full 360° sweep.",
9544
- type: "number",
9545
- min: 4,
9546
- max: 360,
9547
- step: 1
9548
- }];
9549
9904
  //#endregion
9550
9905
  //#region src/generators/cm99-generic-graphics/params.ts
9551
9906
  const SMOOTH_PATH_PARAMS = [{
@@ -10006,6 +10361,14 @@ const DEFAULT_GENERIC_TEXT_OPTIONS = {
10006
10361
  };
10007
10362
  const GENERIC_TEXT_METADATA = {
10008
10363
  id: "text",
10364
+ sizePairs: [{
10365
+ id: "size",
10366
+ label: "Size",
10367
+ pixels: "sizePixels",
10368
+ meters: "sizeMeters",
10369
+ defaultUnit: "px",
10370
+ rendering: "text"
10371
+ }],
10009
10372
  name: "Text",
10010
10373
  description: "A non-doctrinal free-standing text label anchored at a single point.",
10011
10374
  entity: "Generic Graphics",
@@ -11388,7 +11751,7 @@ function createSecurityTaskSymbol(coordinates, config, options = {}) {
11388
11751
  ...options
11389
11752
  };
11390
11753
  const arrowOptions = resolveTacticalArrowOptions(resolved);
11391
- const lines = [createTacticalArrow(p2, p1, p3, arrowOptions), createTacticalArrow(p3, p4, p2, arrowOptions)].flatMap((arrow) => arrow ?? []);
11754
+ const lines = [createTacticalArrow(p2, p1, arrowOptions, -1), createTacticalArrow(p3, p4, arrowOptions, 1)].flatMap((arrow) => arrow ?? []);
11392
11755
  if (lines.length === 0) return {
11393
11756
  type: "FeatureCollection",
11394
11757
  features: []
@@ -11418,13 +11781,12 @@ function createSecurityTaskSymbol(coordinates, config, options = {}) {
11418
11781
  };
11419
11782
  }
11420
11783
  /** A Search Area-style lightning shaft with a Principal Direction of Fire open head. */
11421
- function createTacticalArrow(origin, tip, opposite, options) {
11784
+ function createTacticalArrow(origin, tip, options, bendSide) {
11422
11785
  const leg = vecSub(tip, origin);
11423
11786
  const totalLegLength = vecMag(leg);
11424
11787
  if (totalLegLength < 1e-6) return void 0;
11425
11788
  const legDirection = vecNorm(leg);
11426
- let normal = [-legDirection[1], legDirection[0]];
11427
- if (vecDot(normal, vecSub(opposite, origin)) > 0) normal = vecScale(normal, -1);
11789
+ const normal = [-legDirection[1] * bendSide, legDirection[0] * bendSide];
11428
11790
  const amplitude = totalLegLength * options.zigzagAmplitudeRatio;
11429
11791
  const zigzagPointA = vecAdd(vecAdd(origin, vecScale(leg, options.zigzagStart)), vecScale(normal, amplitude));
11430
11792
  const zigzagPointB = vecAdd(vecAdd(origin, vecScale(leg, options.zigzagEnd)), vecScale(normal, -amplitude));
@@ -13509,6 +13871,14 @@ const DEFAULT_FLOT_OPTIONS = {
13509
13871
  };
13510
13872
  const FLOT_METADATA = {
13511
13873
  id: "flot",
13874
+ sizePairs: [{
13875
+ id: "radius",
13876
+ label: "Radius",
13877
+ pixels: "radiusPixels",
13878
+ meters: "radius",
13879
+ defaultUnit: "m",
13880
+ rendering: "geometry"
13881
+ }],
13512
13882
  name: "Forward line of own troops (FLOT)",
13513
13883
  description: "Indicates the most forward positions of friendly forces at a specific time.",
13514
13884
  entity: "Maneuver Lines",
@@ -14679,6 +15049,15 @@ const PROBABLE_LINE_OF_DEPLOYMENT = defineControlMeasure({
14679
15049
  });
14680
15050
  //#endregion
14681
15051
  //#region src/generators/cm29-protection-lines/fortifiedLine.ts
15052
+ /** Shared by the fortified line and the fortified area built from it. */
15053
+ const TOOTH_SIZE_SIZE_PAIR = {
15054
+ id: "tooth-size",
15055
+ label: "Tooth size",
15056
+ pixels: "toothSizePixels",
15057
+ meters: "toothSize",
15058
+ defaultUnit: "m",
15059
+ rendering: "geometry"
15060
+ };
14682
15061
  const DEFAULT_TOOTH_SIZE = 50;
14683
15062
  const DEFAULT_TOOTH_SIZE_PIXELS = 10;
14684
15063
  const DEFAULT_SMOOTH_RESOLUTION$1 = 16;
@@ -14693,6 +15072,7 @@ const DEFAULT_FORTIFIED_LINE_OPTIONS = {
14693
15072
  };
14694
15073
  const FORTIFIED_LINE_METADATA = {
14695
15074
  id: "fortified-line",
15075
+ sizePairs: [TOOTH_SIZE_SIZE_PAIR],
14696
15076
  name: "Fortified Line",
14697
15077
  description: "Fortified line with castellated square-wave boundary.",
14698
15078
  entity: "Protection Lines",
@@ -14850,6 +15230,7 @@ const FORTIFIED_LINE = defineControlMeasure({
14850
15230
  const DEFAULT_FORTIFIED_AREA_OPTIONS = DEFAULT_FORTIFIED_LINE_OPTIONS;
14851
15231
  const FORTIFIED_AREA_METADATA = {
14852
15232
  id: "fortified-area",
15233
+ sizePairs: [TOOTH_SIZE_SIZE_PAIR],
14853
15234
  name: "Fortified Area",
14854
15235
  description: "A specified geographical locality with constructed defensive works as protection against attack.",
14855
15236
  entity: "Maneuver Areas",
@@ -16960,6 +17341,15 @@ const DEFAULT_MINEFIELD_OPTIONS = {
16960
17341
  const DEFAULT_MINEFIELD_SIZE_METERS = 1e3;
16961
17342
  const MINEFIELD_METADATA = {
16962
17343
  id: "minefield",
17344
+ sizePairs: [{
17345
+ id: "size",
17346
+ label: "Size",
17347
+ pixels: "sizePixels",
17348
+ meters: "sizeMeters",
17349
+ defaultUnit: "px",
17350
+ rendering: "geometry",
17351
+ fallbackMeters: DEFAULT_MINEFIELD_SIZE_METERS
17352
+ }],
16963
17353
  name: "Minefield",
16964
17354
  description: "A static point symbol identifying a minefield and its mine type.",
16965
17355
  entity: "Protection Areas",
@@ -17262,6 +17652,206 @@ const MINEFIELD = defineControlMeasure({
17262
17652
  }
17263
17653
  });
17264
17654
  //#endregion
17655
+ //#region src/generators/cm27-protection-areas/obstacle-areas.ts
17656
+ const DEFAULT_OBSTACLE_AREA_OPTIONS = {
17657
+ toothSize: 50,
17658
+ toothSizePixels: 10,
17659
+ smooth: false,
17660
+ smoothResolution: 16
17661
+ };
17662
+ /** Preserve control-point corners and keep tooth direction independent of winding. */
17663
+ function toothedRing(ring, size, inward) {
17664
+ const side = (ringSignedArea2(ring) >= 0 ? -1 : 1) * (inward ? -1 : 1);
17665
+ const width = size * (inward ? 1 : .8);
17666
+ const pitch = size * (inward ? 2.4 : 1.6);
17667
+ const result = [ring[0]];
17668
+ for (let i = 1; i < ring.length; i++) {
17669
+ const a = ring[i - 1];
17670
+ const b = ring[i];
17671
+ const dx = b[0] - a[0];
17672
+ const dy = b[1] - a[1];
17673
+ const length = Math.hypot(dx, dy);
17674
+ if (length < 1e-6) continue;
17675
+ const margin = inward ? Math.min(length / 4, .75 * size) : 0;
17676
+ const available = length - 2 * margin;
17677
+ const count = Math.max(1, Math.round(available / pitch));
17678
+ const fittedWidth = Math.min(width, available / count);
17679
+ const offsets = [
17680
+ -fittedWidth / 2,
17681
+ 0,
17682
+ fittedWidth / 2
17683
+ ];
17684
+ for (let tooth = 0; tooth < count; tooth++) {
17685
+ const center = margin + (tooth + .5) / count * available;
17686
+ for (const offset of offsets) {
17687
+ const t = (center + offset) / length;
17688
+ const height = offset === 0 ? side * size : 0;
17689
+ result.push([a[0] + dx * t - dy / length * height, a[1] + dy * t + dx / length * height]);
17690
+ }
17691
+ }
17692
+ result.push(b);
17693
+ }
17694
+ result[result.length - 1] = result[0];
17695
+ return result;
17696
+ }
17697
+ const OBSTACLE_AREA_KINDS = {
17698
+ belt: {
17699
+ inward: false,
17700
+ previewToothSize: 170,
17701
+ description: "A brigade-level control measure within an obstacle zone that limits friendly obstacle employment and focuses the defense."
17702
+ },
17703
+ zone: {
17704
+ inward: false,
17705
+ previewToothSize: 140,
17706
+ description: "A division-level control measure designating land areas where subordinate units are authorized to employ tactical obstacles."
17707
+ },
17708
+ free: {
17709
+ inward: true,
17710
+ previewToothSize: 130,
17711
+ description: "An area designated to remain completely free of reinforcing obstacles."
17712
+ },
17713
+ restricted: {
17714
+ inward: true,
17715
+ previewToothSize: 130,
17716
+ description: "A control measure limiting the types or number of obstacles permitted within an area."
17717
+ }
17718
+ };
17719
+ function createObstacleArea(positions, options, text, context, kind) {
17720
+ const ring = buildClosedRing(positions, options.smooth ?? false, options.smoothResolution ?? 16, 16);
17721
+ const size = calculateAntitankDitchHeight({
17722
+ toothHeight: options.toothSize,
17723
+ toothHeightPixels: options.toothSizePixels,
17724
+ metersPerPixel: options.metersPerPixel
17725
+ });
17726
+ const { inward } = OBSTACLE_AREA_KINDS[kind];
17727
+ const boundary = toothedRing(options.smooth ? evenlySpacePath(ring, inward ? 8 * size : 4 * size) : ring, size, inward);
17728
+ const labels = [];
17729
+ const [cx, cy] = ringLabelAnchor(ring);
17730
+ const hasWindow = inward && Boolean(text.W || text.W1);
17731
+ const rows = Number(kind === "free") + Number(Boolean(text.T)) + Number(hasWindow);
17732
+ const pitch = resolveLabelOffsetMeters(options, 1.2 + Math.max(0, options.labelPadding ?? 0));
17733
+ let row = 0;
17734
+ const rowY = () => cy + ((rows - 1) / 2 - row++) * pitch;
17735
+ const sizeProps = labelSizeProps(options);
17736
+ const addLabel = (x, y, value, props = {}) => {
17737
+ pushLabel(labels, [x, y], value, HORIZONTAL_LABEL_ROTATION, sizeProps, props.anchor, props.field, props.key);
17738
+ if (props.labelPlacement === false) labels[labels.length - 1].properties.labelPlacement = false;
17739
+ };
17740
+ if (kind === "free") addLabel(cx, rowY(), "FREE", { key: "label:free" });
17741
+ if (text.T) addLabel(cx, rowY(), text.T, { field: "T" });
17742
+ if (hasWindow) {
17743
+ const y = rowY();
17744
+ if (text.W && text.W1) {
17745
+ const gap = resolveLabelOffsetMeters(options, 1.8);
17746
+ addLabel(cx - gap, y, text.W, {
17747
+ field: "W",
17748
+ anchor: "end"
17749
+ });
17750
+ addLabel(cx, y, "–", { labelPlacement: false });
17751
+ addLabel(cx + gap, y, text.W1, {
17752
+ field: "W1",
17753
+ anchor: "start"
17754
+ });
17755
+ } else if (text.W) addLabel(cx, y, text.W, { field: "W" });
17756
+ else if (text.W1) addLabel(cx, y, text.W1, { field: "W1" });
17757
+ }
17758
+ const gaps = pushAreaLabels(labels, ring, { hostile: text.N }, options, context.amplifierPlacements, {
17759
+ ringVerts: boundary,
17760
+ extraClearanceMeters: size
17761
+ }, context);
17762
+ if (kind === "restricted") for (const label of labels) label.properties.textBackground = true;
17763
+ return {
17764
+ type: "FeatureCollection",
17765
+ features: [...kind === "restricted" ? patternedAreaFeatures(boundary, gaps, "reverse-hatch") : [boundaryFeature(boundary, gaps)], ...labels]
17766
+ };
17767
+ }
17768
+ const COMMON_METADATA$1 = {
17769
+ entity: "Protection Areas",
17770
+ minCoordinates: 3,
17771
+ geometry: "area",
17772
+ geometryTypes: [
17773
+ "Polygon",
17774
+ "MultiLineString",
17775
+ "Point"
17776
+ ],
17777
+ drawRule: "Area1",
17778
+ capturesLabelSize: true,
17779
+ labelGeometryUsesResolvedMetrics: ["N"],
17780
+ sizePairs: [TOOTH_SIZE_SIZE_PAIR],
17781
+ params: [
17782
+ {
17783
+ key: "toothSize",
17784
+ label: "Tooth size",
17785
+ description: "Boundary tooth height in meters",
17786
+ type: "number",
17787
+ min: 1,
17788
+ max: 500,
17789
+ step: 1,
17790
+ unit: "m"
17791
+ },
17792
+ {
17793
+ key: "toothSizePixels",
17794
+ label: "Tooth size",
17795
+ description: "Boundary tooth height in pixels",
17796
+ type: "number",
17797
+ min: 1,
17798
+ max: 50,
17799
+ step: 1,
17800
+ unit: "px"
17801
+ },
17802
+ ...AREA_SMOOTH_PARAMS,
17803
+ AREA_LABEL_PADDING_PARAM
17804
+ ]
17805
+ };
17806
+ function definition(id, name, value, kind) {
17807
+ const { inward, previewToothSize, description } = OBSTACLE_AREA_KINDS[kind];
17808
+ return defineControlMeasure({
17809
+ metadata: {
17810
+ ...COMMON_METADATA$1,
17811
+ id,
17812
+ name,
17813
+ value,
17814
+ entityType: name,
17815
+ description,
17816
+ paints: {
17817
+ stroke: true,
17818
+ fill: kind === "restricted" ? "fixed" : "none",
17819
+ text: true
17820
+ },
17821
+ textAmplifiers: inward ? AREA_TEXT_AMPLIFIERS_NO_ADDITIONAL_INFO : AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE
17822
+ },
17823
+ generator: (positions, options = {}, text = {}, context = {}) => createObstacleArea(positions, options, text, context, kind),
17824
+ defaultOptions: DEFAULT_OBSTACLE_AREA_OPTIONS,
17825
+ rule: area1DrawRule,
17826
+ previewSample: {
17827
+ controlPoints: kind === "belt" ? [
17828
+ [-1, -.5],
17829
+ [1, -.5],
17830
+ [1, .5],
17831
+ [-1, .5]
17832
+ ] : [
17833
+ [-1, -.7],
17834
+ [1, -.7],
17835
+ [.95, .25],
17836
+ [-1, .8]
17837
+ ],
17838
+ options: {
17839
+ toothSize: previewToothSize,
17840
+ labelSize: inward ? 70 : 96
17841
+ },
17842
+ textAmplifiers: inward ? {
17843
+ T: "T",
17844
+ W: "W",
17845
+ W1: "W1"
17846
+ } : { T: "T" }
17847
+ }
17848
+ });
17849
+ }
17850
+ const OBSTACLE_BELT = definition("obstacle-belt", "Obstacle Belt", "270100", "belt");
17851
+ const OBSTACLE_ZONE = definition("obstacle-zone", "Obstacle Zone", "270200", "zone");
17852
+ const OBSTACLE_FREE_ZONE = definition("obstacle-free-zone", "Obstacle Free Zone", "270300", "free");
17853
+ const OBSTACLE_RESTRICTED_ZONE = definition("obstacle-restricted-zone", "Obstacle Restricted Zone", "270400", "restricted");
17854
+ //#endregion
17265
17855
  //#region src/generators/cm27-protection-areas/mine-areas.ts
17266
17856
  const DEFAULT_MINE_AREA_OPTIONS = {
17267
17857
  mineType: "unspecified",
@@ -18278,14 +18868,12 @@ function createSearchArea(coordinates, options = {}) {
18278
18868
  ];
18279
18869
  const shaftSegments = [];
18280
18870
  const headPolygons = [];
18281
- const addLeg = (origin, tip, opposite) => {
18871
+ const addLeg = (origin, tip, bendSide) => {
18282
18872
  const v = vecSub(tip, origin);
18283
18873
  const totalLen = vecMag(v);
18284
18874
  if (totalLen < 1e-6) return;
18285
18875
  const u = vecNorm(v);
18286
- let n = [-u[1], u[0]];
18287
- const toOpp = vecSub(opposite, origin);
18288
- if (vecDot(n, toOpp) > 0) n = vecScale(n, -1);
18876
+ const n = [-u[1] * bendSide, u[0] * bendSide];
18289
18877
  const offset = totalLen * safeZigzagAmplitudeRatio;
18290
18878
  const ptA = vecAdd(vecAdd(origin, vecScale(v, safeZigzagStart)), vecScale(n, offset));
18291
18879
  const ptB = vecAdd(vecAdd(origin, vecScale(v, safeZigzagEnd)), vecScale(n, -offset));
@@ -18312,8 +18900,8 @@ function createSearchArea(coordinates, options = {}) {
18312
18900
  tip
18313
18901
  ].map((p) => unproject(p[0], p[1]))]);
18314
18902
  };
18315
- addLeg(p1, p2, p3);
18316
- addLeg(p1, p3, p2);
18903
+ addLeg(p1, p2, 1);
18904
+ addLeg(p1, p3, -1);
18317
18905
  const features = [];
18318
18906
  if (shaftSegments.length > 0) features.push({
18319
18907
  type: "Feature",
@@ -18353,6 +18941,7 @@ const DEFAULT_SMOOTH_RESOLUTION = 16;
18353
18941
  const DEFAULT_STRONG_POINT_OPTIONS = { ...DEFAULT_BATTLE_POSITION_OPTIONS };
18354
18942
  const STRONG_POINT_METADATA = {
18355
18943
  id: "strong-point",
18944
+ sizePairs: [ECHELON_SIZE_PAIR],
18356
18945
  name: "Strong Point",
18357
18946
  description: "A key point in a defensive position, usually strongly fortified and heavily armed, around which other positions are grouped for its protection.",
18358
18947
  entity: "Maneuver Areas",
@@ -19033,6 +19622,10 @@ const DEFINITIONS = {
19033
19622
  "no-fire-area-irregular": NO_FIRE_AREA_IRREGULAR,
19034
19623
  "minefield-dynamic": DYNAMIC_MINEFIELD,
19035
19624
  "mined-area": MINED_AREA,
19625
+ "obstacle-belt": OBSTACLE_BELT,
19626
+ "obstacle-zone": OBSTACLE_ZONE,
19627
+ "obstacle-free-zone": OBSTACLE_FREE_ZONE,
19628
+ "obstacle-restricted-zone": OBSTACLE_RESTRICTED_ZONE,
19036
19629
  "pickup-zone": PICKUP_ZONE,
19037
19630
  "assault-position": ASSAULT_POSITION,
19038
19631
  "attack-position": ATTACK_POSITION,
@@ -19117,6 +19710,15 @@ const DEFINITIONS = {
19117
19710
  "antitank-ditch-under-construction": ANTITANK_DITCH_UNDER_CONSTRUCTION,
19118
19711
  "antitank-ditch-completed": ANTITANK_DITCH_COMPLETED,
19119
19712
  "antitank-wall": ANTITANK_WALL,
19713
+ "wire-obstacle-unspecified": WIRE_OBSTACLE_UNSPECIFIED,
19714
+ "single-fence": SINGLE_FENCE,
19715
+ "double-fence": DOUBLE_FENCE,
19716
+ "double-apron-fence": DOUBLE_APRON_FENCE,
19717
+ "low-wire-fence": LOW_WIRE_FENCE,
19718
+ "high-wire-fence": HIGH_WIRE_FENCE,
19719
+ "single-concertina": SINGLE_CONCERTINA,
19720
+ "double-strand-concertina": DOUBLE_STRAND_CONCERTINA,
19721
+ "triple-strand-concertina": TRIPLE_STRAND_CONCERTINA,
19120
19722
  "fortified-line": FORTIFIED_LINE,
19121
19723
  "fortified-area": FORTIFIED_AREA,
19122
19724
  "frontal-attack": FRONTAL_ATTACK,
@@ -19154,18 +19756,27 @@ const DEFINITIONS = {
19154
19756
  };
19155
19757
  /** Insertion-ordered list of every control-measure id. */
19156
19758
  const CONTROL_MEASURE_IDS = Object.keys(DEFINITIONS);
19157
- /**
19158
- * Merges a definition's resolved `rule` object back onto its metadata,
19159
- * preserving ADR-0005's `getControlMeasureMetadata` shape. Definitions whose
19160
- * `drawRule` id has no implementation leave `rule` undefined.
19161
- */
19162
- function withRule(definition) {
19163
- return definition.rule ? {
19759
+ /** The shared label dimension materialized for capture-enabled definitions. */
19760
+ const LABEL_SIZE_PAIR = {
19761
+ id: "label",
19762
+ label: "Label size",
19763
+ pixels: "labelSizePixels",
19764
+ meters: "labelSize",
19765
+ defaultUnit: "px",
19766
+ defaultValue: 14,
19767
+ rendering: "text",
19768
+ captureOnDraw: true
19769
+ };
19770
+ /** Resolve definition-owned metadata and the shared label-capture capability. */
19771
+ function resolveMetadata(definition) {
19772
+ const sizePairs = [...definition.metadata.sizePairs ?? [], ...definition.metadata.capturesLabelSize ? [LABEL_SIZE_PAIR] : []];
19773
+ return {
19164
19774
  ...definition.metadata,
19165
- rule: definition.rule
19166
- } : definition.metadata;
19775
+ ...definition.rule ? { rule: definition.rule } : {},
19776
+ sizePairs
19777
+ };
19167
19778
  }
19168
- const CONTROL_MEASURE_METADATA = Object.fromEntries(Object.entries(DEFINITIONS).map(([id, definition]) => [id, withRule(definition)]));
19779
+ const CONTROL_MEASURE_METADATA = Object.fromEntries(Object.entries(DEFINITIONS).map(([id, definition]) => [id, resolveMetadata(definition)]));
19169
19780
  const CONTROL_MEASURE_METADATA_LIST = Object.values(CONTROL_MEASURE_METADATA);
19170
19781
  const CONTROL_MEASURE_METADATA_BY_VALUE = CONTROL_MEASURE_METADATA_LIST.reduce((acc, metadata) => {
19171
19782
  acc[metadata.value] = metadata;
@@ -19538,4 +20149,4 @@ function assertNever(value) {
19538
20149
  throw new Error(`Unhandled control measure kind: ${String(value)}`);
19539
20150
  }
19540
20151
  //#endregion
19541
- export { DEFAULT_PHASE_LINE_OPTIONS as $, project as $n, DEFAULT_DIRECTION_OF_SUPPORTING_ATTACK_OPTIONS as $t, DEFAULT_SECURE_OPTIONS as A, turnDrawRule as An, DEFAULT_GENERIC_CIRCLE_OPTIONS as At, DEFAULT_FORTIFIED_LINE_OPTIONS as B, occupyDrawRule as Bn, DEFAULT_BLOCK_ARROW_OPTIONS as Bt, DEFAULT_LANDING_ZONE_OPTIONS as C, computeInitialWidthPoint as Cn, RADAR_SEARCH_DOCTRINE_FILL_COLOR as Ct, DEFAULT_WITHDRAW_OPTIONS as D, line26DrawRule as Dn, DEFAULT_GENERIC_RECTANGLE_OPTIONS as Dt, DEFAULT_WITHDRAW_UNDER_PRESSURE_OPTIONS as E, line27DrawRule as En, DEFAULT_GENERIC_SECTOR_OPTIONS as Et, DEFAULT_PENETRATE_OPTIONS as F, sectorDrawRule as Fn, DEFAULT_BLOCK_MISSION_TASK_OPTIONS as Ft, DEFAULT_LIMIT_OF_ADVANCE_OPTIONS as G, area27DrawRule as Gn, DEFAULT_ATTACK_BY_FIRE_OPTIONS as Gt, PROBABLE_LINE_OF_DEPLOYMENT_DASH as H, blockDrawRule as Hn, DEFAULT_CONTAIN_OPTIONS as Ht, DEFAULT_OCCUPY_OPTIONS as I, staticPointDrawRule as In, DEFAULT_GENERIC_C2_LINE_OPTIONS as It, DEFAULT_FORWARD_EDGE_OF_BATTLE_AREA_OPTIONS as J, getMidpointPerpendicularSignedDistance as Jn, DEFAULT_ASSEMBLY_AREA_OPTIONS as Jt, DEFAULT_BATTLE_HANDOVER_LINE_OPTIONS as K, computeDefaultMidpointPerpendicularPoint as Kn, DEFAULT_ANTITANK_WALL_OPTIONS as Kt, DEFAULT_MOVEMENT_TO_CONTACT_OPTIONS as L, point12DrawRule as Ln, DEFAULT_ENGINEER_WORK_LINE_OPTIONS as Lt, DEFAULT_RETIRE_OPTIONS as M, ambushDrawRule as Mn, DEFAULT_CANALIZE_OPTIONS as Mt, DEFAULT_RELIEF_IN_PLACE_OPTIONS as N, attackByFireDrawRule as Nn, DEFAULT_BYPASS_OPTIONS as Nt, DEFAULT_EVACUATE_OPTIONS as O, line24DrawRule as On, DEFAULT_GENERIC_POLYGON_OPTIONS as Ot, DEFAULT_REARWARD_PASSAGE_OF_LINES_OPTIONS as P, dynamicPointDrawRule as Pn, DEFAULT_BREACH_OPTIONS as Pt, DEFAULT_BRIDGEHEAD_LINE_OPTIONS as Q, haversineDistance as Qn, DEFAULT_AREA_DEFENSE_OPTIONS as Qt, DEFAULT_FRONTAL_ATTACK_OPTIONS as R, secureDrawRule as Rn, DEFAULT_LIGHT_LINE_OPTIONS as Rt, DEFAULT_MAIN_ATTACK_OPTIONS as S, calculateMetrics as Sn, DEFAULT_RADAR_SEARCH_DOCTRINE_OPTIONS as St, DEFAULT_TURN_OPTIONS as T, polyline1DrawRule as Tn, DEFAULT_GENERIC_TEXT_OPTIONS as Tt, DEFAULT_LINE_OF_DEPARTURE_CONTACT_OPTIONS as U, centerRadiusDrawRule as Un, DEFAULT_BATTLE_POSITION_OPTIONS as Ut, DEFAULT_PROBABLE_LINE_OF_DEPLOYMENT_OPTIONS as V, disruptDrawRule as Vn, DEFAULT_BLOCK_OPTIONS as Vt, DEFAULT_LINE_OF_DEPARTURE_OPTIONS as W, containDrawRule as Wn, DEFAULT_ATTACK_HELICOPTER_OPTIONS as Wt, DEFAULT_HOLDING_LINE_OPTIONS as X, snapToMidpointPerpendicular as Xn, DEFAULT_MOBILE_DEFENSE_OPTIONS as Xt, DEFAULT_RELEASE_LINE_OPTIONS as Y, pointOnMidpointPerpendicularAxis as Yn, DEFAULT_AREA_OF_OPERATIONS_OPTIONS as Yt, DEFAULT_INFILTRATION_LANE_OPTIONS as Z, createBaselineFrame as Zn, DEFAULT_ENGAGEMENT_AREA_OPTIONS as Zt, DEFAULT_PICKUP_ZONE_OPTIONS as _, DEFAULT_STROKE_DASH_CSS_PIXELS as _n, DEFAULT_SUPPORTING_ATTACK_OPTIONS as _t, DEFINITIONS as a, resolveAmplifierPlacement as an, DEFAULT_DISRUPT_OPTIONS as at, DEFAULT_OBSTACLE_BYPASS_DIFFICULT_OPTIONS as b, rectangleDrawRule as bn, DEFAULT_ISOLATE_OPTIONS as bt, getDefaultOptions as c, DEFAULT_DESTROY_OPTIONS as cn, createFollowAndAssume as ct, DEFAULT_SUPPORT_BY_FIRE_OPTIONS as d, DEFAULT_DEFEAT_OPTIONS as dn, DEFAULT_DEMONSTRATE_OPTIONS as dt, DEFAULT_DIRECTION_OF_MAIN_ATTACK_OPTIONS as en, sphericalBearing as er, DEFAULT_FLOT_OPTIONS as et, DEFAULT_RETAIN_OPTIONS as f, DEFAULT_LABEL_HEIGHT_CSS_PIXELS as fn, DEFAULT_DELAY_OPTIONS as ft, DEFAULT_PRINCIPAL_DIRECTION_OF_FIRE_OPTIONS as g, DEFAULT_PORTRAYAL as gn, DEFAULT_COUNTERATTACK_OPTIONS as gt, SECONDARY_DIRECTION_OF_FIRE_DASH as h, DEFAULT_LINE_JOIN as hn, DEFAULT_COUNTERATTACK_BY_FIRE_OPTIONS as ht, CONTROL_MEASURE_METADATA as i, normalizeTextAmplifiers as in, roundToFixed as ir, DEFAULT_ENCIRCLEMENT_OPTIONS as it, DEFAULT_SCREEN_OPTIONS as j, line1DrawRule as jn, DEFAULT_CLASSIC_ARROW_OPTIONS as jt, DEFAULT_SEIZE_OPTIONS as k, line23DrawRule as kn, DEFAULT_GENERIC_LINE_OPTIONS as kt, listControlMeasureMetadata as l, DEFAULT_NEUTRALIZE_OPTIONS as ln, DEFAULT_DISRUPT_MISSION_TASK_OPTIONS as lt, DEFAULT_SECONDARY_DIRECTION_OF_FIRE_OPTIONS as m, DEFAULT_LINE_CAP as mn, DEFAULT_TACTICAL_ARROW_OPTIONS as mt, resolveStyleHints as n, TEXT_AMPLIFIER_FIELDS as nn, EPSILON as nr, DEFAULT_FIX_OPTIONS as nt, getControlMeasureMetadata as o, DEFAULT_AMBUSH_OPTIONS as on, DEFAULT_GUARD_OPTIONS as ot, DEFAULT_STRONG_POINT_OPTIONS as p, DEFAULT_LABEL_SIZE_CLAMP_CSS_PIXELS as pn, DEFAULT_COVER_OPTIONS as pt, DEFAULT_HANDOVER_LINE_OPTIONS as q, createMidpointPerpendicularDrawRule as qn, DEFAULT_ANTITANK_DITCH_OPTIONS as qt, CONTROL_MEASURE_IDS as r, canonicalTextAmplifierKey as rn, getMetersPerPixel as rr, DEFAULT_FINAL_PROTECTIVE_FIRE_OPTIONS as rt, getControlMeasureMetadataByValue as s, DEFAULT_AIRBORNE_ATTACK_OPTIONS as sn, createFollowAndSupport as st, renderControlMeasure as t, DEFAULT_DIRECTION_OF_ATTACK_AVIATION_OPTIONS as tn, unproject as tr, DEFAULT_FIX_MISSION_TASK_OPTIONS as tt, DEFAULT_TURNING_MOVEMENT_OPTIONS as u, DEFAULT_SUPPRESS_OPTIONS as un, DEFAULT_DISENGAGE_OPTIONS as ut, DEFAULT_OBSTACLE_BYPASS_IMPOSSIBLE_OPTIONS as v, DEFAULT_STROKE_WIDTH_CSS_PIXELS as vn, DEFAULT_CORDON_AND_SEARCH_OPTIONS as vt, DEFAULT_JOINT_TACTICAL_ACTION_AREA_OPTIONS as w, supportByFireDrawRule as wn, RADAR_SEARCH_DOCTRINE_STROKE_COLOR as wt, DEFAULT_MINEFIELD_OPTIONS as x, axis1DrawRule as xn, DEFAULT_CLEAR_OPTIONS as xt, DEFAULT_OBSTACLE_BYPASS_EASY_OPTIONS as y, DEFAULT_SYMBOL_COLOR as yn, DEFAULT_CORDON_AND_KNOCK_OPTIONS as yt, DEFAULT_FORTIFIED_AREA_OPTIONS as z, penetrateDrawRule as zn, DEFAULT_BOUNDARY_OPTIONS as zt };
20152
+ export { DEFAULT_PHASE_LINE_OPTIONS as $, project as $n, DEFAULT_DIRECTION_OF_SUPPORTING_ATTACK_OPTIONS as $t, DEFAULT_SECURE_OPTIONS as A, turnDrawRule as An, DEFAULT_GENERIC_CIRCLE_OPTIONS as At, DEFAULT_FORTIFIED_LINE_OPTIONS as B, occupyDrawRule as Bn, DEFAULT_BLOCK_ARROW_OPTIONS as Bt, DEFAULT_LANDING_ZONE_OPTIONS as C, computeInitialWidthPoint as Cn, RADAR_SEARCH_DOCTRINE_FILL_COLOR as Ct, DEFAULT_WITHDRAW_OPTIONS as D, line26DrawRule as Dn, DEFAULT_GENERIC_RECTANGLE_OPTIONS as Dt, DEFAULT_WITHDRAW_UNDER_PRESSURE_OPTIONS as E, line27DrawRule as En, DEFAULT_GENERIC_SECTOR_OPTIONS as Et, DEFAULT_PENETRATE_OPTIONS as F, sectorDrawRule as Fn, DEFAULT_BLOCK_MISSION_TASK_OPTIONS as Ft, DEFAULT_LIMIT_OF_ADVANCE_OPTIONS as G, area27DrawRule as Gn, DEFAULT_ATTACK_BY_FIRE_OPTIONS as Gt, PROBABLE_LINE_OF_DEPLOYMENT_DASH as H, blockDrawRule as Hn, DEFAULT_CONTAIN_OPTIONS as Ht, DEFAULT_OCCUPY_OPTIONS as I, staticPointDrawRule as In, DEFAULT_GENERIC_C2_LINE_OPTIONS as It, DEFAULT_FORWARD_EDGE_OF_BATTLE_AREA_OPTIONS as J, getMidpointPerpendicularSignedDistance as Jn, DEFAULT_ASSEMBLY_AREA_OPTIONS as Jt, DEFAULT_BATTLE_HANDOVER_LINE_OPTIONS as K, computeDefaultMidpointPerpendicularPoint as Kn, DEFAULT_ANTITANK_WALL_OPTIONS as Kt, DEFAULT_MOVEMENT_TO_CONTACT_OPTIONS as L, point12DrawRule as Ln, DEFAULT_ENGINEER_WORK_LINE_OPTIONS as Lt, DEFAULT_RETIRE_OPTIONS as M, ambushDrawRule as Mn, DEFAULT_CANALIZE_OPTIONS as Mt, DEFAULT_RELIEF_IN_PLACE_OPTIONS as N, attackByFireDrawRule as Nn, DEFAULT_BYPASS_OPTIONS as Nt, DEFAULT_EVACUATE_OPTIONS as O, line24DrawRule as On, DEFAULT_GENERIC_POLYGON_OPTIONS as Ot, DEFAULT_REARWARD_PASSAGE_OF_LINES_OPTIONS as P, dynamicPointDrawRule as Pn, DEFAULT_BREACH_OPTIONS as Pt, DEFAULT_BRIDGEHEAD_LINE_OPTIONS as Q, haversineDistance as Qn, DEFAULT_AREA_DEFENSE_OPTIONS as Qt, DEFAULT_FRONTAL_ATTACK_OPTIONS as R, secureDrawRule as Rn, DEFAULT_LIGHT_LINE_OPTIONS as Rt, DEFAULT_MAIN_ATTACK_OPTIONS as S, calculateMetrics as Sn, DEFAULT_RADAR_SEARCH_DOCTRINE_OPTIONS as St, DEFAULT_TURN_OPTIONS as T, polyline1DrawRule as Tn, DEFAULT_GENERIC_TEXT_OPTIONS as Tt, DEFAULT_LINE_OF_DEPARTURE_CONTACT_OPTIONS as U, centerRadiusDrawRule as Un, DEFAULT_BATTLE_POSITION_OPTIONS as Ut, DEFAULT_PROBABLE_LINE_OF_DEPLOYMENT_OPTIONS as V, disruptDrawRule as Vn, DEFAULT_BLOCK_OPTIONS as Vt, DEFAULT_LINE_OF_DEPARTURE_OPTIONS as W, containDrawRule as Wn, DEFAULT_ATTACK_HELICOPTER_OPTIONS as Wt, DEFAULT_HOLDING_LINE_OPTIONS as X, snapToMidpointPerpendicular as Xn, DEFAULT_MOBILE_DEFENSE_OPTIONS as Xt, DEFAULT_RELEASE_LINE_OPTIONS as Y, pointOnMidpointPerpendicularAxis as Yn, DEFAULT_AREA_OF_OPERATIONS_OPTIONS as Yt, DEFAULT_INFILTRATION_LANE_OPTIONS as Z, createBaselineFrame as Zn, DEFAULT_ENGAGEMENT_AREA_OPTIONS as Zt, DEFAULT_PICKUP_ZONE_OPTIONS as _, DEFAULT_STROKE_DASH_CSS_PIXELS as _n, DEFAULT_SUPPORTING_ATTACK_OPTIONS as _t, DEFINITIONS as a, resolveAmplifierPlacement as an, roundToFixed as ar, DEFAULT_DISRUPT_OPTIONS as at, DEFAULT_OBSTACLE_BYPASS_DIFFICULT_OPTIONS as b, rectangleDrawRule as bn, DEFAULT_ISOLATE_OPTIONS as bt, getDefaultOptions as c, DEFAULT_DESTROY_OPTIONS as cn, createFollowAndAssume as ct, DEFAULT_SUPPORT_BY_FIRE_OPTIONS as d, DEFAULT_DEFEAT_OPTIONS as dn, DEFAULT_DEMONSTRATE_OPTIONS as dt, DEFAULT_DIRECTION_OF_MAIN_ATTACK_OPTIONS as en, sphericalBearing as er, DEFAULT_FLOT_OPTIONS as et, DEFAULT_RETAIN_OPTIONS as f, DEFAULT_LABEL_HEIGHT_CSS_PIXELS as fn, DEFAULT_DELAY_OPTIONS as ft, DEFAULT_PRINCIPAL_DIRECTION_OF_FIRE_OPTIONS as g, DEFAULT_PORTRAYAL as gn, DEFAULT_COUNTERATTACK_OPTIONS as gt, SECONDARY_DIRECTION_OF_FIRE_DASH as h, DEFAULT_LINE_JOIN as hn, DEFAULT_COUNTERATTACK_BY_FIRE_OPTIONS as ht, CONTROL_MEASURE_METADATA as i, normalizeTextAmplifiers as in, isFiniteNumber as ir, DEFAULT_ENCIRCLEMENT_OPTIONS as it, DEFAULT_SCREEN_OPTIONS as j, line1DrawRule as jn, DEFAULT_CLASSIC_ARROW_OPTIONS as jt, DEFAULT_SEIZE_OPTIONS as k, line23DrawRule as kn, DEFAULT_GENERIC_LINE_OPTIONS as kt, listControlMeasureMetadata as l, DEFAULT_NEUTRALIZE_OPTIONS as ln, DEFAULT_DISRUPT_MISSION_TASK_OPTIONS as lt, DEFAULT_SECONDARY_DIRECTION_OF_FIRE_OPTIONS as m, DEFAULT_LINE_CAP as mn, DEFAULT_TACTICAL_ARROW_OPTIONS as mt, resolveStyleHints as n, TEXT_AMPLIFIER_FIELDS as nn, EPSILON as nr, DEFAULT_FIX_OPTIONS as nt, getControlMeasureMetadata as o, DEFAULT_AMBUSH_OPTIONS as on, DEFAULT_GUARD_OPTIONS as ot, DEFAULT_STRONG_POINT_OPTIONS as p, DEFAULT_LABEL_SIZE_CLAMP_CSS_PIXELS as pn, DEFAULT_COVER_OPTIONS as pt, DEFAULT_HANDOVER_LINE_OPTIONS as q, createMidpointPerpendicularDrawRule as qn, DEFAULT_ANTITANK_DITCH_OPTIONS as qt, CONTROL_MEASURE_IDS as r, canonicalTextAmplifierKey as rn, getMetersPerPixel as rr, DEFAULT_FINAL_PROTECTIVE_FIRE_OPTIONS as rt, getControlMeasureMetadataByValue as s, DEFAULT_AIRBORNE_ATTACK_OPTIONS as sn, createFollowAndSupport as st, renderControlMeasure as t, DEFAULT_DIRECTION_OF_ATTACK_AVIATION_OPTIONS as tn, unproject as tr, DEFAULT_FIX_MISSION_TASK_OPTIONS as tt, DEFAULT_TURNING_MOVEMENT_OPTIONS as u, DEFAULT_SUPPRESS_OPTIONS as un, DEFAULT_DISENGAGE_OPTIONS as ut, DEFAULT_OBSTACLE_BYPASS_IMPOSSIBLE_OPTIONS as v, DEFAULT_STROKE_WIDTH_CSS_PIXELS as vn, DEFAULT_CORDON_AND_SEARCH_OPTIONS as vt, DEFAULT_JOINT_TACTICAL_ACTION_AREA_OPTIONS as w, supportByFireDrawRule as wn, RADAR_SEARCH_DOCTRINE_STROKE_COLOR as wt, DEFAULT_MINEFIELD_OPTIONS as x, axis1DrawRule as xn, DEFAULT_CLEAR_OPTIONS as xt, DEFAULT_OBSTACLE_BYPASS_EASY_OPTIONS as y, DEFAULT_SYMBOL_COLOR as yn, DEFAULT_CORDON_AND_KNOCK_OPTIONS as yt, DEFAULT_FORTIFIED_AREA_OPTIONS as z, penetrateDrawRule as zn, DEFAULT_BOUNDARY_OPTIONS as zt };