@orbat-mapper/control-measures 0.5.2 → 0.7.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.
@@ -560,7 +560,7 @@ function samePosition(a, b) {
560
560
  }
561
561
  //#endregion
562
562
  //#region src/draw-rules/area1.ts
563
- function derive$11(points) {
563
+ function derive$12(points) {
564
564
  return clonePositions(points);
565
565
  }
566
566
  /**
@@ -580,9 +580,9 @@ const area1DrawRule = {
580
580
  id: "area1",
581
581
  minimumUserPoints: 3,
582
582
  closedRing: true,
583
- derive: derive$11,
583
+ derive: derive$12,
584
584
  transform(event) {
585
- return derive$11(event.next);
585
+ return derive$12(event.next);
586
586
  }
587
587
  };
588
588
  //#endregion
@@ -608,14 +608,14 @@ const disruptDrawRule = createMidpointPerpendicularDrawRule({
608
608
  * that fixes both the radius and the bearing of the symbol's opening. Both
609
609
  * points are user-clicked, so the canonical array is just the (clamped) input.
610
610
  */
611
- function derive$10(points) {
611
+ function derive$11(points) {
612
612
  return clonePositions(points.slice(0, 2));
613
613
  }
614
614
  const centerRadiusDrawRule = {
615
615
  id: "area15:center-radius",
616
616
  minimumUserPoints: 2,
617
617
  canonicalPointCount: 2,
618
- derive: derive$10,
618
+ derive: derive$11,
619
619
  transform(event) {
620
620
  const { previous, next, activePointIndex } = event;
621
621
  if (activePointIndex === 0 && previous.length >= 2 && next.length >= 2) {
@@ -624,7 +624,7 @@ const centerRadiusDrawRule = {
624
624
  const origin = previous[1];
625
625
  return [clonePosition(next[0]), [origin[0] + dx, origin[1] + dy]];
626
626
  }
627
- return derive$10(next);
627
+ return derive$11(next);
628
628
  }
629
629
  };
630
630
  /** Backward-compatible doctrinal name for Area15's shared center-radius rule. */
@@ -660,16 +660,16 @@ const point12DrawRule = createMidpointPerpendicularDrawRule({ id: "point12:obsta
660
660
  * Point1 — a single anchor point (e.g. Text's center). One user click commits
661
661
  * the measure; dragging the point simply translates it.
662
662
  */
663
- function derive$9(points) {
663
+ function derive$10(points) {
664
664
  return clonePositions(points.slice(0, 1));
665
665
  }
666
666
  const pointDrawRule = {
667
667
  id: "point1:anchor",
668
668
  minimumUserPoints: 1,
669
669
  canonicalPointCount: 1,
670
- derive: derive$9,
670
+ derive: derive$10,
671
671
  transform(event) {
672
- return derive$9(event.next);
672
+ return derive$10(event.next);
673
673
  }
674
674
  };
675
675
  //#endregion
@@ -689,6 +689,81 @@ const staticPointDrawRule = {
689
689
  id: "point2:static-anchor"
690
690
  };
691
691
  //#endregion
692
+ //#region src/draw-rules/sector.ts
693
+ const SECTOR_ANGLE_SNAP_RADIANS = Math.PI / 36;
694
+ /**
695
+ * Sector — anchor + inner-left edge + outer-right edge. The two radial points
696
+ * independently control a radius and a true-north azimuth. Dragging the anchor
697
+ * translates both edge points so the sector retains its size and orientation.
698
+ */
699
+ function derive$9(points) {
700
+ return clonePositions(points.slice(0, 3));
701
+ }
702
+ function guidePoints$1(points) {
703
+ return points.length < 3 ? clonePositions(points) : [];
704
+ }
705
+ /** Snap one radial Sector handle to a true-north angular increment. */
706
+ function snapSectorAngle(points, activePointIndex, incrementRadians) {
707
+ const next = clonePositions(points);
708
+ if (activePointIndex !== 1 && activePointIndex !== 2) return next;
709
+ const anchor = next[0];
710
+ const point = next[activePointIndex];
711
+ if (!anchor || !point) return next;
712
+ const range = haversineDistance(anchor, point);
713
+ const bearing = sphericalBearing(anchor, point);
714
+ next[activePointIndex] = [...destinationPoint(anchor, range, Math.round(bearing / incrementRadians) * incrementRadians), ...point.slice(2)];
715
+ return next;
716
+ }
717
+ function constrainAngles(points, activePointIndex) {
718
+ return snapSectorAngle(points, activePointIndex, SECTOR_ANGLE_SNAP_RADIANS);
719
+ }
720
+ const sectorDrawRule = {
721
+ id: "sector:anchor-radii",
722
+ minimumUserPoints: 3,
723
+ canonicalPointCount: 3,
724
+ showGuide: true,
725
+ guidePoints: guidePoints$1,
726
+ constrainAngles,
727
+ derive: derive$9,
728
+ transform(event) {
729
+ const { previous, next, activePointIndex } = event;
730
+ if (activePointIndex === 0 && previous.length >= 3 && next.length >= 3) {
731
+ const dx = next[0][0] - previous[0][0];
732
+ const dy = next[0][1] - previous[0][1];
733
+ return [
734
+ clonePosition(next[0]),
735
+ [previous[1][0] + dx, previous[1][1] + dy],
736
+ [previous[2][0] + dx, previous[2][1] + dy]
737
+ ];
738
+ }
739
+ return derive$9(next);
740
+ }
741
+ };
742
+ //#endregion
743
+ //#region src/draw-rules/point18.ts
744
+ const WHOLE_DEGREE_RADIANS = Math.PI / 180;
745
+ function snapToWholeDegrees(points) {
746
+ let snapped = snapSectorAngle(points, 1, WHOLE_DEGREE_RADIANS);
747
+ if (snapped.length >= 3) snapped = snapSectorAngle(snapped, 2, WHOLE_DEGREE_RADIANS);
748
+ return snapped;
749
+ }
750
+ /**
751
+ * Point18 — doctrinally a numeric, single-anchor symbol. The interactive
752
+ * authoring model exposes those dimensions as Sector-style radial handles:
753
+ * P1 is the anchor, P2 fixes the start range/left bearing, and P3 fixes the
754
+ * stop range/right bearing.
755
+ */
756
+ const dynamicPointDrawRule = {
757
+ ...sectorDrawRule,
758
+ id: "point18:anchor-radii",
759
+ derive(points) {
760
+ return snapToWholeDegrees(sectorDrawRule.derive(points));
761
+ },
762
+ transform(event) {
763
+ return snapSectorAngle(sectorDrawRule.transform(event), event.activePointIndex, WHOLE_DEGREE_RADIANS);
764
+ }
765
+ };
766
+ //#endregion
692
767
  //#region src/draw-rules/area7.ts
693
768
  /**
694
769
  * Area7 anchor draw rule for Attack By Fire.
@@ -2056,6 +2131,43 @@ function buildGappedLine(verts, gaps) {
2056
2131
  return parts;
2057
2132
  }
2058
2133
  //#endregion
2134
+ //#region src/style.ts
2135
+ /**
2136
+ * Per-feature style hint pinning a filled part to a solid interior. Attach to
2137
+ * a generator's intrinsic doctrinal accents — arrowheads, teeth, echelon
2138
+ * glyphs, barbs, blades — so a patterned (`hatch`, `dots`, …) fill set at the graphicsStyle
2139
+ * or measure layer never bleeds into a small silhouette and ruins its
2140
+ * legibility. The renderer only reads style hints, so one shared instance is
2141
+ * safe to reuse across every accent feature.
2142
+ */
2143
+ const SOLID_ACCENT_FILL = { fillPattern: "solid" };
2144
+ /**
2145
+ * Ultimate fallback for the symbol color when no `color` or per-channel
2146
+ * override is supplied at any layer. Keeps a zero-config render monocolor
2147
+ * black and guarantees filled parts still render filled. See ADR-0011.
2148
+ */
2149
+ const DEFAULT_SYMBOL_COLOR = "#000000";
2150
+ //#endregion
2151
+ //#region src/portrayal.ts
2152
+ const DEFAULT_STROKE_WIDTH_CSS_PIXELS = 2;
2153
+ const DEFAULT_STROKE_DASH_CSS_PIXELS = Object.freeze([]);
2154
+ const DEFAULT_LINE_CAP = "round";
2155
+ const DEFAULT_LINE_JOIN = "round";
2156
+ const DEFAULT_LABEL_HEIGHT_CSS_PIXELS = 14;
2157
+ const DEFAULT_LABEL_SIZE_CLAMP_CSS_PIXELS = {
2158
+ min: 8,
2159
+ max: 24
2160
+ };
2161
+ /** Shared absent-value portrayal defaults; render output remains sparse. */
2162
+ const DEFAULT_PORTRAYAL = Object.freeze({
2163
+ symbolColor: DEFAULT_SYMBOL_COLOR,
2164
+ strokeWidthCssPixels: 2,
2165
+ strokeDashCssPixels: DEFAULT_STROKE_DASH_CSS_PIXELS,
2166
+ lineCap: DEFAULT_LINE_CAP,
2167
+ lineJoin: DEFAULT_LINE_JOIN,
2168
+ labelHeightCssPixels: 14
2169
+ });
2170
+ //#endregion
2059
2171
  //#region src/internal/angle-utils.ts
2060
2172
  /**
2061
2173
  * Wraps an angle (radians) into the half-open range (-π, π].
@@ -2566,6 +2678,39 @@ const LABEL_CHAR_ASPECT_RATIO = .6;
2566
2678
  function estimatedTextWidth(text, textHeightMeters) {
2567
2679
  return text.length * LABEL_CHAR_ASPECT_RATIO * textHeightMeters;
2568
2680
  }
2681
+ /** Resolve the label's final display size, then measure and convert it to construction metres. */
2682
+ function resolveTextMetrics(text, options, context, style = "regular") {
2683
+ const legacyHeight = resolveLabelOffsetMeters(options, 1);
2684
+ const constructionScale = context?.constructionMetersPerCssPixel;
2685
+ if (!(constructionScale !== void 0 && constructionScale > 0)) return {
2686
+ width: estimatedTextWidth(text, legacyHeight),
2687
+ height: legacyHeight
2688
+ };
2689
+ let sizeCssPixels;
2690
+ if (options.labelSizePixels !== void 0) sizeCssPixels = options.labelSizePixels;
2691
+ else if (options.labelSize !== void 0) {
2692
+ const requested = options.labelSize / constructionScale;
2693
+ const requestedBand = context?.labelSizeClampCssPixels;
2694
+ const band = requestedBand && Number.isFinite(requestedBand.min) && requestedBand.min >= 0 && Number.isFinite(requestedBand.max) && requestedBand.max >= 0 ? requestedBand : DEFAULT_LABEL_SIZE_CLAMP_CSS_PIXELS;
2695
+ const min = Math.min(band.min, band.max);
2696
+ const max = Math.max(band.min, band.max);
2697
+ sizeCssPixels = Math.min(max, Math.max(min, requested));
2698
+ } else sizeCssPixels = 14;
2699
+ const fallbackHeight = sizeCssPixels * constructionScale;
2700
+ const measured = context?.measureText?.({
2701
+ text,
2702
+ style,
2703
+ sizeCssPixels
2704
+ });
2705
+ if (!measured || !Number.isFinite(measured.widthCssPixels) || measured.widthCssPixels < 0 || !Number.isFinite(measured.heightCssPixels) || measured.heightCssPixels < 0) return {
2706
+ width: estimatedTextWidth(text, fallbackHeight),
2707
+ height: fallbackHeight
2708
+ };
2709
+ return {
2710
+ width: measured.widthCssPixels * constructionScale,
2711
+ height: measured.heightCssPixels * constructionScale
2712
+ };
2713
+ }
2569
2714
  /** The end-label text for a phase-line naming, or `""` when unnamed. */
2570
2715
  function phaseLineLabelText(name, includePrefix) {
2571
2716
  if (!name) return "";
@@ -2577,17 +2722,17 @@ function phaseLineLabelText(name, includePrefix) {
2577
2722
  * {@link pushEndLabels}. When another beyond-tip label is present, its
2578
2723
  * estimated width is added so the hostile marker remains outermost.
2579
2724
  */
2580
- function pushHostileEndLabels(out, verts, hostileText, options = {}, occupiedTextForEnd = "") {
2725
+ function pushHostileEndLabels(out, verts, hostileText, options = {}, occupiedTextForEnd = "", context) {
2581
2726
  if (!hostileText) return;
2582
2727
  const frames = endFrames(verts);
2583
2728
  const sizeProps = labelSizeProps(options);
2584
- const textHeight = resolveLabelOffsetMeters(options, 1);
2585
- const clearance = resolveLabelOffsetMeters(options, END_LABEL_CLEARANCE_RATIO + Math.max(0, options.labelPadding ?? 0));
2586
2729
  for (const end of ["start", "end"]) {
2587
2730
  const frame = frames[end];
2588
2731
  if (!frame) continue;
2589
2732
  const occupiedText = typeof occupiedTextForEnd === "string" ? occupiedTextForEnd : occupiedTextForEnd(end);
2590
- const distance = clearance + (occupiedText ? estimatedTextWidth(occupiedText, textHeight) + clearance : 0);
2733
+ const metrics = resolveTextMetrics(occupiedText || hostileText, options, context);
2734
+ const clearance = (END_LABEL_CLEARANCE_RATIO + Math.max(0, options.labelPadding ?? 0)) * metrics.height;
2735
+ const distance = clearance + (occupiedText ? metrics.width + clearance : 0);
2591
2736
  pushLabel(out, vecAdd(frame.point, vecScale(frame.along, distance)), hostileText, labelRotationAlong(frame.along), sizeProps, frame.along[0] >= 0 ? "start" : "end", "N", `label:N:${end}`);
2592
2737
  }
2593
2738
  }
@@ -2646,7 +2791,7 @@ function pushEndLabelsAbove(out, verts, textForEnd, options = {}) {
2646
2791
  * (via {@link pushEndLabels} or {@link pushEndLabelsAbove}, per `placement`)
2647
2792
  * as a complete FeatureCollection.
2648
2793
  */
2649
- function fixedLabelLineFeatures(positions, options, textForEnd, hostileText) {
2794
+ function fixedLabelLineFeatures(positions, options, textForEnd, hostileText, context) {
2650
2795
  const { part, placement = "beyond", phaseLineName, includePrefix = true, smooth, smoothResolution, ...sizeOptions } = options;
2651
2796
  const verts = smoothLineVerts(positions.map((p) => project(p[0], p[1])), {
2652
2797
  smooth,
@@ -2666,7 +2811,7 @@ function fixedLabelLineFeatures(positions, options, textForEnd, hostileText) {
2666
2811
  else pushEndLabels(labelFeatures, verts, textForEnd, sizeOptions);
2667
2812
  const nameText = phaseLineLabelText(phaseLineName?.trim(), includePrefix);
2668
2813
  if (nameText) pushEndLabels(labelFeatures, verts, nameText, sizeOptions);
2669
- pushHostileEndLabels(labelFeatures, verts, hostileText, sizeOptions, nameText || (placement === "beyond" ? textForEnd : ""));
2814
+ pushHostileEndLabels(labelFeatures, verts, hostileText, sizeOptions, nameText || (placement === "beyond" ? textForEnd : ""), context);
2670
2815
  features.push(...labelFeatures);
2671
2816
  return {
2672
2817
  type: "FeatureCollection",
@@ -2832,10 +2977,9 @@ function nonNegative$1(value, fallback) {
2832
2977
  * later `buildGappedLine` walk); `segments`/`totalLength` describe the full
2833
2978
  * route that anchors the default label placement.
2834
2979
  */
2835
- function directionLabelGap(verts, segments, totalLength, fraction, text, options, placement = {}) {
2980
+ function directionLabelGap(verts, segments, totalLength, fraction, text, options, placement = {}, context) {
2836
2981
  if (!text) return void 0;
2837
- const textHeight = resolveLabelOffsetMeters(options, 1);
2838
- const textWidth = estimatedTextWidth(text, textHeight);
2982
+ const { width: textWidth, height: textHeight } = resolveTextMetrics(text, options, context);
2839
2983
  const clearance = (LABEL_GAP_CLEARANCE_RATIO + Math.max(0, options.labelPadding ?? 0)) * textHeight;
2840
2984
  const frame = pointAlongPolyline(segments, totalLength, fraction);
2841
2985
  const rotation = placement.rotation ?? (frame ? labelRotationAlong(frame.along) : 0);
@@ -2896,7 +3040,7 @@ function createDirectionOfAttackLine(coordinates, options, textAmplifiers, confi
2896
3040
  shaftVerts.push(innerTip);
2897
3041
  }
2898
3042
  const labelPosition = clampLinePosition(options.labelPosition, DEFAULT_DIRECTION_LABEL_POSITION);
2899
- const labelGap = directionLabelGap(shaftVerts, segments, totalLength, labelPosition, textAmplifiers.T, options, resolveAmplifierPlacement(context.amplifierPlacements?.T));
3043
+ const labelGap = directionLabelGap(shaftVerts, segments, totalLength, labelPosition, textAmplifiers.T, options, resolveAmplifierPlacement(context.amplifierPlacements?.T), context);
2900
3044
  const lines = [...buildGappedLine(shaftVerts, labelGap ? [labelGap] : []), arrowhead.map((point) => unproject(point[0], point[1]))];
2901
3045
  const features = [{
2902
3046
  type: "Feature",
@@ -3180,7 +3324,7 @@ function createDirectionOfAttackAviation(coordinates, options = {}, textAmplifie
3180
3324
  const bowTieB = vecSub(bowTieStart, vecScale(bowTiePerp, bowTieHalfWidth));
3181
3325
  const bowTieC = vecAdd(bowTieEnd, vecScale(bowTiePerp, bowTieHalfWidth));
3182
3326
  const bowTieD = vecSub(bowTieEnd, vecScale(bowTiePerp, bowTieHalfWidth));
3183
- const labelGap = directionLabelGap(verts, segments, totalLength, labelPosition, textAmplifiers.T, resolved, resolveAmplifierPlacement(context.amplifierPlacements?.T));
3327
+ const labelGap = directionLabelGap(verts, segments, totalLength, labelPosition, textAmplifiers.T, resolved, resolveAmplifierPlacement(context.amplifierPlacements?.T), context);
3184
3328
  const features = [{
3185
3329
  type: "Feature",
3186
3330
  properties: { part: "direction-of-attack-aviation" },
@@ -3760,9 +3904,12 @@ function ringLabelAnchor(verts) {
3760
3904
  * overlaps the finite contacted edge; intrinsic markers always mask. Callers
3761
3905
  * own marker names, anchors, rotation, clearance, placement keys, and fields.
3762
3906
  */
3763
- function pushBoundaryMarkers(out, maskVerts, text, anchors, options, amplifierPlacements) {
3907
+ function pushBoundaryMarkers(out, maskVerts, text, anchors, options, amplifierPlacements, context, labelOptions) {
3764
3908
  const { rotation, textHeight, clearance, sizeProps } = options;
3765
- const textWidth = estimatedTextWidth(text, textHeight);
3909
+ const metrics = resolveTextMetrics(text, labelOptions ?? { labelSize: textHeight }, context);
3910
+ const textWidth = metrics.width;
3911
+ const resolvedTextHeight = metrics.height;
3912
+ const resolvedClearance = textHeight > 0 ? clearance * (resolvedTextHeight / textHeight) : clearance;
3766
3913
  const gaps = [];
3767
3914
  for (const anchor of anchors) {
3768
3915
  const labelPlacementKey = options.placementKeyPrefix ? `${options.placementKeyPrefix}:${anchor.side}` : void 0;
@@ -3772,21 +3919,21 @@ function pushBoundaryMarkers(out, maskVerts, text, anchors, options, amplifierPl
3772
3919
  pushLabel(out, labelPoint, text, options.followPlacementRotation ? boxRotation : rotation, sizeProps, void 0, options.amplifierField, labelPlacementKey);
3773
3920
  const contact = placement.position !== void 0 || options.rescanDefaultAnchors === true || anchor.contact === void 0 ? nearestPointOnPolyline(maskVerts, labelPoint) : anchor.contact;
3774
3921
  if (!contact) continue;
3775
- if (placement.position && !labelBoxIntersectsSegment(labelPoint, ...contact.edge, textWidth, textHeight, boxRotation)) continue;
3776
- const tangentExtent = labelHalfExtent(contact.along, textWidth, textHeight, boxRotation) + clearance;
3922
+ if (placement.position && !labelBoxIntersectsSegment(labelPoint, ...contact.edge, textWidth, resolvedTextHeight, boxRotation)) continue;
3923
+ const tangentExtent = labelHalfExtent(contact.along, textWidth, resolvedTextHeight, boxRotation) + resolvedClearance;
3777
3924
  pushWrappedGap(gaps, contact.arc - tangentExtent, contact.arc + tangentExtent, contact.totalArc);
3778
3925
  }
3779
3926
  return gaps;
3780
3927
  }
3781
3928
  /** Emits movable Field N markers using the generic boundary-marker knockout path. */
3782
- function pushHostileMarkers(out, maskVerts, text, anchors, options, amplifierPlacements) {
3929
+ function pushHostileMarkers(out, maskVerts, text, anchors, options, amplifierPlacements, context, labelOptions) {
3783
3930
  return pushBoundaryMarkers(out, maskVerts, text, anchors, {
3784
3931
  ...options,
3785
3932
  placementKeyPrefix: "label:N",
3786
3933
  amplifierField: "N"
3787
- }, amplifierPlacements);
3934
+ }, amplifierPlacements, context, labelOptions);
3788
3935
  }
3789
- function pushAreaLabels(out, ringVerts, texts, options = {}, amplifierPlacements, maskBoundary) {
3936
+ function pushAreaLabels(out, ringVerts, texts, options = {}, amplifierPlacements, maskBoundary, context) {
3790
3937
  if (ringVerts.length < 2) return [];
3791
3938
  const sizeProps = labelSizeProps(options);
3792
3939
  const padding = Math.max(0, options.labelPadding ?? 0);
@@ -3829,7 +3976,7 @@ function pushAreaLabels(out, ringVerts, texts, options = {}, amplifierPlacements
3829
3976
  clearance: (ENY_CLEARANCE_RATIO + padding) * textHeight + Math.max(0, maskBoundary?.extraClearanceMeters ?? 0),
3830
3977
  sizeProps,
3831
3978
  rescanDefaultAnchors: maskRingVerts !== ringVerts
3832
- }, amplifierPlacements);
3979
+ }, amplifierPlacements, context, options);
3833
3980
  }
3834
3981
  /**
3835
3982
  * Boundary + fill features for a pattern-filled area (e.g. Limited Access Area,
@@ -3908,11 +4055,11 @@ function boundaryFeature(verts, gaps) {
3908
4055
  * echelon-masked boundary, ADR-0023) instead of a closed `Polygon` — the
3909
4056
  * common (no `N`) case is unaffected.
3910
4057
  */
3911
- function labeledAreaFeatures(positions, options, texts, amplifierPlacements) {
4058
+ function labeledAreaFeatures(positions, options, texts, amplifierPlacements, context) {
3912
4059
  const { smooth = false, smoothResolution = DEFAULT_SMOOTH_RESOLUTION$9, ...labelOptions } = options;
3913
4060
  const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$9);
3914
4061
  const labelFeatures = [];
3915
- const features = [boundaryFeature(verts, pushAreaLabels(labelFeatures, verts, texts, labelOptions, amplifierPlacements))];
4062
+ const features = [boundaryFeature(verts, pushAreaLabels(labelFeatures, verts, texts, labelOptions, amplifierPlacements, void 0, context))];
3916
4063
  features.push(...labelFeatures);
3917
4064
  return {
3918
4065
  type: "FeatureCollection",
@@ -3956,7 +4103,7 @@ const AREA_METADATA = {
3956
4103
  * marker, per {@link labeledAreaFeatures}.
3957
4104
  */
3958
4105
  function createArea(positions, options = {}, textAmplifiers = {}, context = {}) {
3959
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, ""), context.amplifierPlacements);
4106
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, ""), context.amplifierPlacements, context);
3960
4107
  }
3961
4108
  const AREA = defineControlMeasure({
3962
4109
  metadata: AREA_METADATA,
@@ -3975,23 +4122,6 @@ const AREA = defineControlMeasure({
3975
4122
  }
3976
4123
  });
3977
4124
  //#endregion
3978
- //#region src/style.ts
3979
- /**
3980
- * Per-feature style hint pinning a filled part to a solid interior. Attach to
3981
- * a generator's intrinsic doctrinal accents — arrowheads, teeth, echelon
3982
- * glyphs, barbs, blades — so a patterned (`hatch`, `dots`, …) fill set at the graphicsStyle
3983
- * or measure layer never bleeds into a small silhouette and ruins its
3984
- * legibility. The renderer only reads style hints, so one shared instance is
3985
- * safe to reuse across every accent feature.
3986
- */
3987
- const SOLID_ACCENT_FILL = { fillPattern: "solid" };
3988
- /**
3989
- * Ultimate fallback for the symbol color when no `color` or per-channel
3990
- * override is supplied at any layer. Keeps a zero-config render monocolor
3991
- * black and guarantees filled parts still render filled. See ADR-0011.
3992
- */
3993
- const DEFAULT_SYMBOL_COLOR = "#000000";
3994
- //#endregion
3995
4125
  //#region src/generators/cm15-maneuver-areas/area-defense.ts
3996
4126
  const DEFAULT_AREA_DEFENSE_OPTIONS = {
3997
4127
  arrowOpeningAngle: 30,
@@ -4078,7 +4208,7 @@ const AREA_DEFENSE_METADATA = {
4078
4208
  function pointOnCircle(center, radius, angle) {
4079
4209
  return vecAdd(center, [radius * Math.cos(angle), radius * Math.sin(angle)]);
4080
4210
  }
4081
- function sampleArc(center, radius, start, end) {
4211
+ function sampleArc$1(center, radius, start, end) {
4082
4212
  const span = end - start;
4083
4213
  const segments = Math.max(8, Math.round(48 * Math.abs(span) / Math.PI));
4084
4214
  return Array.from({ length: segments + 1 }, (_, index) => pointOnCircle(center, radius, start + span * index / segments));
@@ -4112,8 +4242,8 @@ function createAreaDefense(coordinates, options = {}) {
4112
4242
  const upperEnd = upperTipAngle;
4113
4243
  const lowerStart = labelCenter - labelHalf;
4114
4244
  const lowerEnd = upperTipAngle + arrowHalf * 2 - Math.PI * 2;
4115
- const upperArc = sampleArc(center, radius, upperStart, upperEnd);
4116
- const lowerArc = sampleArc(center, radius, lowerStart, lowerEnd);
4245
+ const upperArc = sampleArc$1(center, radius, upperStart, upperEnd);
4246
+ const lowerArc = sampleArc$1(center, radius, lowerStart, lowerEnd);
4117
4247
  const arrowLength = radius * Math.max(0, resolved.arrowheadLengthRatio);
4118
4248
  const upperDirection = [-Math.sin(upperEnd), Math.cos(upperEnd)];
4119
4249
  const lowerDirection = [Math.sin(lowerEnd), -Math.cos(lowerEnd)];
@@ -4236,7 +4366,7 @@ const AREA_OF_OPERATIONS_METADATA = {
4236
4366
  * `H`/`W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
4237
4367
  */
4238
4368
  function createAreaOfOperations(positions, options = {}, textAmplifiers = {}, context = {}) {
4239
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "AO"), context.amplifierPlacements);
4369
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "AO"), context.amplifierPlacements, context);
4240
4370
  }
4241
4371
  const AREA_OF_OPERATIONS = defineControlMeasure({
4242
4372
  metadata: AREA_OF_OPERATIONS_METADATA,
@@ -4295,7 +4425,7 @@ const NAMED_AREA_OF_INTEREST_METADATA = {
4295
4425
  * an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
4296
4426
  */
4297
4427
  function createNamedAreaOfInterest(positions, options = {}, textAmplifiers = {}, context = {}) {
4298
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "NAI"), context.amplifierPlacements);
4428
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "NAI"), context.amplifierPlacements, context);
4299
4429
  }
4300
4430
  const NAMED_AREA_OF_INTEREST = defineControlMeasure({
4301
4431
  metadata: NAMED_AREA_OF_INTEREST_METADATA,
@@ -4354,7 +4484,7 @@ const TARGET_AREA_OF_INTEREST_METADATA = {
4354
4484
  * an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
4355
4485
  */
4356
4486
  function createTargetAreaOfInterest(positions, options = {}, textAmplifiers = {}, context = {}) {
4357
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "TAI"), context.amplifierPlacements);
4487
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "TAI"), context.amplifierPlacements, context);
4358
4488
  }
4359
4489
  const TARGET_AREA_OF_INTEREST = defineControlMeasure({
4360
4490
  metadata: TARGET_AREA_OF_INTEREST_METADATA,
@@ -4449,7 +4579,7 @@ function createAirfieldZone(positions, options = {}, textAmplifiers = {}, contex
4449
4579
  const { smooth = DEFAULT_AIRFIELD_ZONE_OPTIONS.smooth, smoothResolution = DEFAULT_AIRFIELD_ZONE_OPTIONS.smoothResolution, glyphSizeRatio = DEFAULT_AIRFIELD_ZONE_OPTIONS.glyphSizeRatio, ...labelOptions } = options;
4450
4580
  const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$8);
4451
4581
  const enyLabels = [];
4452
- const features = [boundaryFeature(verts, textAmplifiers.N ? pushAreaLabels(enyLabels, verts, { hostile: textAmplifiers.N }, labelOptions, context.amplifierPlacements) : [])];
4582
+ const features = [boundaryFeature(verts, textAmplifiers.N ? pushAreaLabels(enyLabels, verts, { hostile: textAmplifiers.N }, labelOptions, context.amplifierPlacements, void 0, context) : [])];
4453
4583
  let minX = Infinity;
4454
4584
  let maxX = -Infinity;
4455
4585
  for (const [x] of verts) {
@@ -4525,13 +4655,13 @@ const DEFAULT_ECHELON_LABELED_AREA_OPTIONS = {
4525
4655
  * before carving the boundary, so either (or both) opens it into a gapped
4526
4656
  * `MultiLineString`; with neither, the boundary is a closed `Polygon`.
4527
4657
  */
4528
- function echelonLabeledAreaFeatures(positions, options, texts, amplifierPlacements) {
4658
+ function echelonLabeledAreaFeatures(positions, options, texts, amplifierPlacements, context) {
4529
4659
  const { echelon = DEFAULT_ECHELON_LABELED_AREA_OPTIONS.echelon, echelonSize = DEFAULT_ECHELON_LABELED_AREA_OPTIONS.echelonSize, echelonSizePixels, echelonPadding = DEFAULT_ECHELON_LABELED_AREA_OPTIONS.echelonPadding, echelonPosition = DEFAULT_ECHELON_LABELED_AREA_OPTIONS.echelonPosition, echelonAnchor, metersPerPixel, smooth = DEFAULT_ECHELON_LABELED_AREA_OPTIONS.smooth, smoothResolution = DEFAULT_ECHELON_LABELED_AREA_OPTIONS.smoothResolution, ...labelOptions } = options;
4530
4660
  const h = resolveEchelonHeight(echelonSize, echelonSizePixels, metersPerPixel);
4531
4661
  const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$7);
4532
4662
  const { strokes, fills, gaps: echelonGaps } = placeEchelon(buildAreaPerimeter(verts), h, echelon, echelonPadding, echelonPosition, echelonAnchor);
4533
4663
  const labelFeatures = [];
4534
- const enyGaps = pushAreaLabels(labelFeatures, verts, texts, labelOptions, amplifierPlacements);
4664
+ const enyGaps = pushAreaLabels(labelFeatures, verts, texts, labelOptions, amplifierPlacements, void 0, context);
4535
4665
  const features = [boundaryFeature(verts, [...echelonGaps, ...enyGaps])];
4536
4666
  if (strokes.length > 0) features.push({
4537
4667
  type: "Feature",
@@ -4607,7 +4737,7 @@ const BASE_CAMP_METADATA = {
4607
4737
  * boundary, per {@link echelonLabeledAreaFeatures}.
4608
4738
  */
4609
4739
  function createBaseCamp(positions, options = {}, textAmplifiers = {}, context = {}) {
4610
- return echelonLabeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "BC"), context.amplifierPlacements);
4740
+ return echelonLabeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "BC"), context.amplifierPlacements, context);
4611
4741
  }
4612
4742
  const BASE_CAMP = defineControlMeasure({
4613
4743
  metadata: BASE_CAMP_METADATA,
@@ -4665,7 +4795,7 @@ const GUERRILLA_BASE_METADATA = {
4665
4795
  * straddling the boundary, per {@link echelonLabeledAreaFeatures}.
4666
4796
  */
4667
4797
  function createGuerrillaBase(positions, options = {}, textAmplifiers = {}, context = {}) {
4668
- return echelonLabeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "GB"), context.amplifierPlacements);
4798
+ return echelonLabeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "GB"), context.amplifierPlacements, context);
4669
4799
  }
4670
4800
  const GUERRILLA_BASE = defineControlMeasure({
4671
4801
  metadata: GUERRILLA_BASE_METADATA,
@@ -4727,7 +4857,7 @@ const GENERIC_C2_AREA_METADATA = {
4727
4857
  * `H`/`W`/`W1`/`N` amplifier rows — no prefix, per {@link labeledAreaFeatures}.
4728
4858
  */
4729
4859
  function createGenericC2Area(positions, options = {}, textAmplifiers = {}, context = {}) {
4730
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers), context.amplifierPlacements);
4860
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers), context.amplifierPlacements, context);
4731
4861
  }
4732
4862
  const GENERIC_C2_AREA = defineControlMeasure({
4733
4863
  metadata: GENERIC_C2_AREA_METADATA,
@@ -4782,7 +4912,7 @@ const ASSEMBLY_AREA_METADATA = {
4782
4912
  * `H`/`W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
4783
4913
  */
4784
4914
  function createAssemblyArea(positions, options = {}, textAmplifiers = {}, context = {}) {
4785
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "AA"), context.amplifierPlacements);
4915
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "AA"), context.amplifierPlacements, context);
4786
4916
  }
4787
4917
  const ASSEMBLY_AREA = defineControlMeasure({
4788
4918
  metadata: ASSEMBLY_AREA_METADATA,
@@ -4974,13 +5104,13 @@ function ditchPaths(projectedPoints, options) {
4974
5104
  function buildAntitankDitchTriangles(basePath, options) {
4975
5105
  return generateAntitankDitchTriangles(basePath, calculateAntitankDitchHeight(options), options.toothWidthRatio ?? DEFAULT_TOOTH_WIDTH_RATIO$1);
4976
5106
  }
4977
- function createAntitankDitchUnderConstruction(positions, options = {}, textAmplifiers = {}) {
5107
+ function createAntitankDitchUnderConstruction(positions, options = {}, textAmplifiers = {}, context = {}) {
4978
5108
  const { smoothedPath, basePath } = ditchPaths(positions.map((position) => project(position[0], position[1])), options);
4979
5109
  const triangles = buildAntitankDitchTriangles(basePath, options);
4980
5110
  const baseLine = options.smooth ? basePath.map((point) => unproject(point[0], point[1])) : positions.map((position) => [position[0], position[1]]);
4981
5111
  const triangleLines = triangles.map((triangle) => triangle.map((point) => unproject(point[0], point[1])));
4982
5112
  const labels = [];
4983
- pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options);
5113
+ pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options, "", context);
4984
5114
  return {
4985
5115
  type: "FeatureCollection",
4986
5116
  features: [{
@@ -4993,13 +5123,13 @@ function createAntitankDitchUnderConstruction(positions, options = {}, textAmpli
4993
5123
  }, ...labels]
4994
5124
  };
4995
5125
  }
4996
- function createAntitankDitchCompleted(positions, options = {}, textAmplifiers = {}) {
5126
+ function createAntitankDitchCompleted(positions, options = {}, textAmplifiers = {}, context = {}) {
4997
5127
  const { smoothedPath, basePath } = ditchPaths(positions.map((position) => project(position[0], position[1])), options);
4998
5128
  const polygons = buildAntitankDitchTriangles(basePath, options).map((triangle) => {
4999
5129
  return [[...triangle, triangle[0]].map((point) => unproject(point[0], point[1]))];
5000
5130
  });
5001
5131
  const labels = [];
5002
- pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options);
5132
+ pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options, "", context);
5003
5133
  return {
5004
5134
  type: "FeatureCollection",
5005
5135
  features: [{
@@ -5089,7 +5219,7 @@ function generateAntitankWallPoints(projectedPoints, toothHeight, toothWidthRati
5089
5219
  }
5090
5220
  return wallPoints;
5091
5221
  }
5092
- function createAntitankWall(positions, options = {}, textAmplifiers = {}) {
5222
+ function createAntitankWall(positions, options = {}, textAmplifiers = {}, context = {}) {
5093
5223
  const toothHeight = calculateAntitankDitchHeight(options);
5094
5224
  const toothWidthRatio = options.toothWidthRatio ?? DEFAULT_TOOTH_WIDTH_RATIO;
5095
5225
  const toothSpacingRatio = options.toothSpacingRatio ?? DEFAULT_TOOTH_SPACING_RATIO;
@@ -5097,7 +5227,7 @@ function createAntitankWall(positions, options = {}, textAmplifiers = {}) {
5097
5227
  const smoothedPath = smoothLineVerts(projectedPoints, options, 16);
5098
5228
  const wallLine = generateAntitankWallPoints(options.smooth && projectedPoints.length >= 3 ? evenlySpacePath(smoothedPath, Math.max(EPSILON, toothWidthRatio) * toothHeight) : smoothedPath, toothHeight, toothWidthRatio, toothSpacingRatio).map((point) => unproject(point[0], point[1]));
5099
5229
  const labels = [];
5100
- pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options);
5230
+ pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options, "", context);
5101
5231
  return {
5102
5232
  type: "FeatureCollection",
5103
5233
  features: [{
@@ -5544,7 +5674,7 @@ function createBattlePosition(positions, options = {}, textAmplifiers = {}, cont
5544
5674
  const enyGaps = pushAreaLabels(labelFeatures, verts, {
5545
5675
  name: textAmplifiers.T,
5546
5676
  hostile: textAmplifiers.N
5547
- }, options, context.amplifierPlacements);
5677
+ }, options, context.amplifierPlacements, void 0, context);
5548
5678
  const boundaryCoords = buildGappedLine(verts, [...gaps, ...enyGaps]);
5549
5679
  const features = [];
5550
5680
  if (boundaryCoords.length > 0) features.push({
@@ -6142,7 +6272,7 @@ const BOUNDARY_METADATA = {
6142
6272
  * @param textAmplifiers - Normalized text amplifiers (ADR-0027): `T`/`AS` label
6143
6273
  * unit 1 (left-of-travel side), `T1`/`AS1` label unit 2 (right-of-travel side).
6144
6274
  */
6145
- function createBoundary(positions, options = {}, textAmplifiers = {}) {
6275
+ function createBoundary(positions, options = {}, textAmplifiers = {}, context = {}) {
6146
6276
  const { echelon = DEFAULT_BOUNDARY_OPTIONS.echelon, echelonSize = DEFAULT_BOUNDARY_OPTIONS.echelonSize, echelonSizePixels, echelonPadding = DEFAULT_BOUNDARY_OPTIONS.echelonPadding, metersPerPixel, labelRepetitions = DEFAULT_BOUNDARY_OPTIONS.labelRepetitions, labelSpacing = DEFAULT_BOUNDARY_OPTIONS.labelSpacing, labelPosition = DEFAULT_BOUNDARY_OPTIONS.labelPosition, labelPadding = DEFAULT_BOUNDARY_OPTIONS.labelPadding, labelOrientation = DEFAULT_BOUNDARY_OPTIONS.labelOrientation, smooth = DEFAULT_BOUNDARY_OPTIONS.smooth, smoothResolution = DEFAULT_BOUNDARY_OPTIONS.smoothResolution } = options;
6147
6277
  const unit1Designator = textAmplifiers.T ?? "";
6148
6278
  const unit1Country = textAmplifiers.AS ?? "";
@@ -6226,7 +6356,7 @@ function createBoundary(positions, options = {}, textAmplifiers = {}) {
6226
6356
  labelSize: h,
6227
6357
  labelPadding
6228
6358
  };
6229
- pushHostileEndLabels(labelFeatures, verts, textAmplifiers.N, hostileLabelOptions);
6359
+ pushHostileEndLabels(labelFeatures, verts, textAmplifiers.N, hostileLabelOptions, "", context);
6230
6360
  const boundaryCoords = buildGappedLine(verts, gaps);
6231
6361
  const features = [];
6232
6362
  if (boundaryCoords.length > 0) features.push({
@@ -6342,12 +6472,12 @@ const LIGHT_LINE_METADATA = {
6342
6472
  * Creates a Light line control measure: a polyline through `positions`
6343
6473
  * labeled "LL" above the line near each end.
6344
6474
  */
6345
- function createLightLine(positions, options = {}, textAmplifiers = {}) {
6475
+ function createLightLine(positions, options = {}, textAmplifiers = {}, context = {}) {
6346
6476
  return fixedLabelLineFeatures(positions, {
6347
6477
  part: "light-line",
6348
6478
  placement: "above",
6349
6479
  ...options
6350
- }, "LL", textAmplifiers.N);
6480
+ }, "LL", textAmplifiers.N, context);
6351
6481
  }
6352
6482
  const LIGHT_LINE = defineControlMeasure({
6353
6483
  metadata: LIGHT_LINE_METADATA,
@@ -6453,12 +6583,12 @@ const CENTER_LABEL_OFFSET_RATIO = .7;
6453
6583
  * above the line, T1/AS1 below). No center group is emitted when all four
6454
6584
  * amplifiers are empty.
6455
6585
  */
6456
- function createEngineerWorkLine(positions, options = {}, textAmplifiers = {}) {
6586
+ function createEngineerWorkLine(positions, options = {}, textAmplifiers = {}, context = {}) {
6457
6587
  const features = [...fixedLabelLineFeatures(positions, {
6458
6588
  part: "engineer-work-line",
6459
6589
  placement: "above",
6460
6590
  ...options
6461
- }, "EWL", textAmplifiers.N).features];
6591
+ }, "EWL", textAmplifiers.N, context).features];
6462
6592
  const { segments, totalLength } = polylineSegments(smoothLineVerts(positions.map((p) => project(p[0], p[1])), options, 12));
6463
6593
  const frame = pointAlongPolyline(segments, totalLength, .5);
6464
6594
  if (frame) {
@@ -6595,7 +6725,7 @@ const GENERIC_C2_LINE_METADATA = {
6595
6725
  * (inset from the tip, anchored toward the line's interior), one at
6596
6726
  * `base + perp·offset` and the other at `base − perp·offset`.
6597
6727
  */
6598
- function createGenericC2Line(positions, options = {}, textAmplifiers = {}) {
6728
+ function createGenericC2Line(positions, options = {}, textAmplifiers = {}, context = {}) {
6599
6729
  const { phaseLineName, includePrefix = true, smooth, smoothResolution, ...sizeOptions } = options;
6600
6730
  const verts = smoothLineVerts(positions.map((p) => project(p[0], p[1])), {
6601
6731
  smooth,
@@ -6627,7 +6757,7 @@ function createGenericC2Line(positions, options = {}, textAmplifiers = {}) {
6627
6757
  }
6628
6758
  const nameText = phaseLineLabelText(trimmedPhaseLineName, includePrefix);
6629
6759
  if (nameText) pushEndLabels(labelFeatures, verts, nameText, sizeOptions);
6630
- pushHostileEndLabels(labelFeatures, verts, textAmplifiers.N, sizeOptions, nameText);
6760
+ pushHostileEndLabels(labelFeatures, verts, textAmplifiers.N, sizeOptions, nameText, context);
6631
6761
  features.push(...labelFeatures);
6632
6762
  }
6633
6763
  return {
@@ -7465,6 +7595,20 @@ function normalizeSmoothResolution(value) {
7465
7595
  if (!Number.isFinite(value)) return DEFAULT_SMOOTH_RESOLUTION$3;
7466
7596
  return Math.min(MAX_SMOOTH_RESOLUTION, Math.max(MIN_SMOOTH_RESOLUTION, Math.round(value)));
7467
7597
  }
7598
+ function normalizeArcResolution(value) {
7599
+ if (typeof value !== "number" || !Number.isFinite(value)) return 64;
7600
+ return Math.min(360, Math.max(4, Math.round(value)));
7601
+ }
7602
+ const ARC_RESOLUTION_PARAMS = [{
7603
+ key: "resolution",
7604
+ presentationTier: "advanced",
7605
+ label: "Resolution",
7606
+ description: "Number of segments in a full 360° sweep.",
7607
+ type: "number",
7608
+ min: 4,
7609
+ max: 360,
7610
+ step: 1
7611
+ }];
7468
7612
  //#endregion
7469
7613
  //#region src/generators/cm99-generic-graphics/params.ts
7470
7614
  const SMOOTH_PATH_PARAMS = [{
@@ -7491,7 +7635,6 @@ const FILLED_AREA_PARAMS = [{
7491
7635
  }];
7492
7636
  //#endregion
7493
7637
  //#region src/generators/cm99-generic-graphics/circle.ts
7494
- const CIRCLE_SEGMENTS$1 = 64;
7495
7638
  const MAX_SEGMENT_ANGLE = 6;
7496
7639
  const MAX_SUBDIVISION_DEPTH = 5;
7497
7640
  /**
@@ -7517,7 +7660,10 @@ const densifySegment = (center, radius, bearingA, posA, bearingB, posB, depth, o
7517
7660
  densifySegment(center, radius, midBearing, midPos, bearingB, posB, depth + 1, out);
7518
7661
  } else out.push(posA);
7519
7662
  };
7520
- const DEFAULT_GENERIC_CIRCLE_OPTIONS = { filled: false };
7663
+ const DEFAULT_GENERIC_CIRCLE_OPTIONS = {
7664
+ filled: false,
7665
+ resolution: 64
7666
+ };
7521
7667
  const GENERIC_CIRCLE_METADATA = {
7522
7668
  id: "circle",
7523
7669
  name: "Circle",
@@ -7536,7 +7682,7 @@ const GENERIC_CIRCLE_METADATA = {
7536
7682
  text: false
7537
7683
  },
7538
7684
  drawRule: "Area15",
7539
- params: FILLED_AREA_PARAMS
7685
+ params: [...FILLED_AREA_PARAMS, ...ARC_RESOLUTION_PARAMS]
7540
7686
  };
7541
7687
  function createGenericCircle(coordinates, options = {}) {
7542
7688
  const center = coordinates[0];
@@ -7546,12 +7692,13 @@ function createGenericCircle(coordinates, options = {}) {
7546
7692
  type: "FeatureCollection",
7547
7693
  features: []
7548
7694
  };
7695
+ const resolution = normalizeArcResolution(options.resolution);
7549
7696
  const startBearing = sphericalBearing(center, radiusPoint);
7550
7697
  const ring = [];
7551
7698
  let previousBearing = startBearing;
7552
7699
  let previousPoint = destinationPoint(center, radius, previousBearing);
7553
- for (let index = 1; index <= CIRCLE_SEGMENTS$1; index++) {
7554
- const bearing = startBearing - index / CIRCLE_SEGMENTS$1 * 2 * Math.PI;
7700
+ for (let index = 1; index <= resolution; index++) {
7701
+ const bearing = startBearing - index / resolution * 2 * Math.PI;
7555
7702
  const point = destinationPoint(center, radius, bearing);
7556
7703
  densifySegment(center, radius, previousBearing, previousPoint, bearing, point, 0, ring);
7557
7704
  previousBearing = bearing;
@@ -7800,6 +7947,117 @@ const GENERIC_RECTANGLE = defineControlMeasure({
7800
7947
  ] }
7801
7948
  });
7802
7949
  //#endregion
7950
+ //#region src/internal/annular-sector.ts
7951
+ /**
7952
+ * Clockwise sweep in degrees from `leftAzimuth` to `rightAzimuth`. A non-zero
7953
+ * raw difference that normalizes to 0 means a full 360° fan.
7954
+ *
7955
+ * `collapseToleranceDegrees` (default 0, i.e. off) treats a near-zero or
7956
+ * near-360 sweep as a collapsed sector. Callers whose handle coordinates are
7957
+ * rounded need this: two handles placed on the same ray can recover bearings a
7958
+ * few thousandths of a degree apart, which would otherwise read as an
7959
+ * almost-360° fan.
7960
+ */
7961
+ function clockwiseSweepDegrees(leftAzimuth, rightAzimuth, collapseToleranceDegrees = 0) {
7962
+ const rawSweep = rightAzimuth - leftAzimuth;
7963
+ const sweep = normalizeDegrees(rawSweep);
7964
+ if (collapseToleranceDegrees > 0 && Math.min(sweep, 360 - sweep) < collapseToleranceDegrees) return 0;
7965
+ return sweep === 0 && rawSweep !== 0 ? 360 : sweep;
7966
+ }
7967
+ /** Samples an arc of `sweepDegrees` at `radius`, starting from `leftAzimuth`. */
7968
+ function sampleArc(anchor, radius, leftAzimuth, sweepDegrees, resolution) {
7969
+ const segmentCount = Math.max(1, Math.ceil(sweepDegrees / 360 * resolution));
7970
+ return Array.from({ length: segmentCount + 1 }, (_, index) => {
7971
+ return destinationPoint(anchor, radius, (leftAzimuth + index / segmentCount * sweepDegrees) * Math.PI / 180);
7972
+ });
7973
+ }
7974
+ /** Closed ring tracing the outer arc, back along the inner arc, and closed. */
7975
+ function annularSectorRing(anchor, innerRadius, outerRadius, leftAzimuth, sweepDegrees, resolution) {
7976
+ const outerArc = sampleArc(anchor, outerRadius, leftAzimuth, sweepDegrees, resolution);
7977
+ const innerArc = sampleArc(anchor, innerRadius, leftAzimuth, sweepDegrees, resolution).reverse();
7978
+ return [
7979
+ ...outerArc,
7980
+ ...innerArc,
7981
+ outerArc[0]
7982
+ ];
7983
+ }
7984
+ //#endregion
7985
+ //#region src/generators/cm99-generic-graphics/sector.ts
7986
+ const DEFAULT_GENERIC_SECTOR_OPTIONS = {
7987
+ filled: false,
7988
+ resolution: 64
7989
+ };
7990
+ const GENERIC_SECTOR_METADATA = {
7991
+ id: "sector",
7992
+ name: "Sector",
7993
+ description: "A non-doctrinal range-fan sector defined by an anchor and two radial edge points.",
7994
+ entity: "Generic Graphics",
7995
+ entityType: "Basic Shape",
7996
+ entitySubtype: "Sector",
7997
+ value: "990105",
7998
+ minCoordinates: 3,
7999
+ maxCoordinates: 3,
8000
+ geometry: "area",
8001
+ geometryTypes: ["Polygon"],
8002
+ paints: {
8003
+ stroke: true,
8004
+ fill: "user",
8005
+ text: false
8006
+ },
8007
+ drawRule: "Sector",
8008
+ params: [...FILLED_AREA_PARAMS, ...ARC_RESOLUTION_PARAMS]
8009
+ };
8010
+ function createGenericSector(coordinates, options = {}) {
8011
+ const anchor = coordinates[0];
8012
+ const innerLeftPoint = coordinates[1];
8013
+ const outerRightPoint = coordinates[2];
8014
+ if (!anchor || !innerLeftPoint || !outerRightPoint) return {
8015
+ type: "FeatureCollection",
8016
+ features: []
8017
+ };
8018
+ const firstRadius = haversineDistance(anchor, innerLeftPoint);
8019
+ const secondRadius = haversineDistance(anchor, outerRightPoint);
8020
+ const innerRadius = Math.min(firstRadius, secondRadius);
8021
+ const outerRadius = Math.max(firstRadius, secondRadius);
8022
+ const leftAzimuth = normalizeDegrees(sphericalBearing(anchor, innerLeftPoint) * 180 / Math.PI);
8023
+ const rightAzimuth = normalizeDegrees(sphericalBearing(anchor, outerRightPoint) * 180 / Math.PI);
8024
+ if (outerRadius - innerRadius < 1e-6 || innerRadius < 1e-6) return {
8025
+ type: "FeatureCollection",
8026
+ features: []
8027
+ };
8028
+ const sweepDegrees = clockwiseSweepDegrees(leftAzimuth, rightAzimuth);
8029
+ if (sweepDegrees === 0) return {
8030
+ type: "FeatureCollection",
8031
+ features: []
8032
+ };
8033
+ const ring = annularSectorRing(anchor, innerRadius, outerRadius, leftAzimuth, sweepDegrees, normalizeArcResolution(options.resolution));
8034
+ return {
8035
+ type: "FeatureCollection",
8036
+ features: [{
8037
+ type: "Feature",
8038
+ properties: {
8039
+ part: "sector",
8040
+ fill: options.filled ?? DEFAULT_GENERIC_SECTOR_OPTIONS.filled
8041
+ },
8042
+ geometry: {
8043
+ type: "Polygon",
8044
+ coordinates: [ring]
8045
+ }
8046
+ }]
8047
+ };
8048
+ }
8049
+ const GENERIC_SECTOR = defineControlMeasure({
8050
+ metadata: GENERIC_SECTOR_METADATA,
8051
+ generator: createGenericSector,
8052
+ defaultOptions: DEFAULT_GENERIC_SECTOR_OPTIONS,
8053
+ rule: sectorDrawRule,
8054
+ previewSample: { controlPoints: [
8055
+ [-.8, -.7],
8056
+ [-.5, .2],
8057
+ [.7, -.1]
8058
+ ] }
8059
+ });
8060
+ //#endregion
7803
8061
  //#region src/generators/cm99-generic-graphics/text.ts
7804
8062
  const DEFAULT_GENERIC_TEXT_OPTIONS = {
7805
8063
  text: "Text",
@@ -7975,6 +8233,114 @@ const GENERIC_TEXT = defineControlMeasure({
7975
8233
  }
7976
8234
  });
7977
8235
  //#endregion
8236
+ //#region src/generators/cm20-maritime-control-areas/radar-search-doctrine.ts
8237
+ const DEFAULT_RADAR_SEARCH_DOCTRINE_OPTIONS = { resolution: 64 };
8238
+ /** Doctrinal RSD colors from the symbol specification. */
8239
+ const RADAR_SEARCH_DOCTRINE_STROKE_COLOR = "rgb(51, 136, 136)";
8240
+ const RADAR_SEARCH_DOCTRINE_FILL_COLOR = "rgba(51, 136, 136, 0.25)";
8241
+ const RADAR_SEARCH_DOCTRINE_METADATA = {
8242
+ id: "radar-search-doctrine",
8243
+ name: "Radar Search Doctrine",
8244
+ description: "A maritime radar search area defined by an axis and annular-sector limits.",
8245
+ entity: "Maritime Control Areas",
8246
+ entityType: "Radar Search Doctrine",
8247
+ value: "200700",
8248
+ minCoordinates: 3,
8249
+ maxCoordinates: 3,
8250
+ geometry: "area",
8251
+ geometryTypes: ["Polygon", "Point"],
8252
+ paints: {
8253
+ stroke: true,
8254
+ fill: "fixed",
8255
+ text: true
8256
+ },
8257
+ drawRule: "Point18",
8258
+ capturesLabelSize: true,
8259
+ params: ARC_RESOLUTION_PARAMS,
8260
+ textAmplifiers: [{
8261
+ key: "T",
8262
+ label: "Unique designation",
8263
+ description: "Field T — designation centered in the search area along its axis.",
8264
+ placeholder: "FF",
8265
+ maxLength: 20
8266
+ }]
8267
+ };
8268
+ /**
8269
+ * GeoJSON positions are rounded by `destinationPoint`, so two handles placed on
8270
+ * the same ray can recover bearings a few thousandths of a degree apart. Treat
8271
+ * that numerical noise as a collapsed sector, not an almost-360° fan.
8272
+ */
8273
+ const SWEEP_COLLAPSE_TOLERANCE_DEGREES = .01;
8274
+ function createRadarSearchDoctrine(coordinates, options = {}, textAmplifiers = {}) {
8275
+ const anchor = coordinates[0];
8276
+ const startLimit = coordinates[1];
8277
+ const stopLimit = coordinates[2];
8278
+ const firstRange = haversineDistance(anchor, startLimit);
8279
+ const secondRange = haversineDistance(anchor, stopLimit);
8280
+ const startRange = Math.min(firstRange, secondRange);
8281
+ const stopRange = Math.max(firstRange, secondRange);
8282
+ const leftAzimuth = normalizeDegrees(sphericalBearing(anchor, startLimit) * 180 / Math.PI);
8283
+ const sweepDegrees = clockwiseSweepDegrees(leftAzimuth, normalizeDegrees(sphericalBearing(anchor, stopLimit) * 180 / Math.PI), SWEEP_COLLAPSE_TOLERANCE_DEGREES);
8284
+ if (stopRange - startRange < 1e-6 || sweepDegrees < 1e-6) return {
8285
+ type: "FeatureCollection",
8286
+ features: []
8287
+ };
8288
+ const axis = normalizeDegrees(leftAzimuth + sweepDegrees / 2) * Math.PI / 180;
8289
+ const ring = annularSectorRing(anchor, startRange, stopRange, leftAzimuth, sweepDegrees, normalizeArcResolution(options.resolution));
8290
+ const features = [{
8291
+ type: "Feature",
8292
+ properties: {
8293
+ part: "search-area",
8294
+ fill: true,
8295
+ style: {
8296
+ strokeColor: RADAR_SEARCH_DOCTRINE_STROKE_COLOR,
8297
+ fillColor: RADAR_SEARCH_DOCTRINE_FILL_COLOR,
8298
+ fillPattern: "solid"
8299
+ }
8300
+ },
8301
+ geometry: {
8302
+ type: "Polygon",
8303
+ coordinates: [ring]
8304
+ }
8305
+ }];
8306
+ if (textAmplifiers.T) {
8307
+ const labelAnchor = destinationPoint(anchor, (startRange + stopRange) / 2, axis);
8308
+ features.push({
8309
+ type: "Feature",
8310
+ properties: {
8311
+ part: "label",
8312
+ text: textAmplifiers.T,
8313
+ amplifierField: "T",
8314
+ labelPlacementKey: "T",
8315
+ ...labelSizeProps(options),
8316
+ rotation: labelRotationAlong([Math.cos(axis), -Math.sin(axis)])
8317
+ },
8318
+ geometry: {
8319
+ type: "Point",
8320
+ coordinates: labelAnchor
8321
+ }
8322
+ });
8323
+ }
8324
+ return {
8325
+ type: "FeatureCollection",
8326
+ features
8327
+ };
8328
+ }
8329
+ const RADAR_SEARCH_DOCTRINE = defineControlMeasure({
8330
+ metadata: RADAR_SEARCH_DOCTRINE_METADATA,
8331
+ generator: createRadarSearchDoctrine,
8332
+ defaultOptions: DEFAULT_RADAR_SEARCH_DOCTRINE_OPTIONS,
8333
+ rule: dynamicPointDrawRule,
8334
+ previewSample: {
8335
+ controlPoints: [
8336
+ [0, -1],
8337
+ [-.55, -.15],
8338
+ [1, .45]
8339
+ ],
8340
+ textAmplifiers: { T: "FF" }
8341
+ }
8342
+ });
8343
+ //#endregion
7978
8344
  //#region src/generators/cm34-mission-tasks/clear.ts
7979
8345
  /**
7980
8346
  * Default options for the CLEAR symbol.
@@ -9546,7 +9912,7 @@ const DROP_ZONE_METADATA = {
9546
9912
  * (Field N) ENY marker, per {@link labeledAreaFeatures}.
9547
9913
  */
9548
9914
  function createDropZone(positions, options = {}, textAmplifiers = {}, context = {}) {
9549
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "DZ"), context.amplifierPlacements);
9915
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "DZ"), context.amplifierPlacements, context);
9550
9916
  }
9551
9917
  const DROP_ZONE = defineControlMeasure({
9552
9918
  metadata: DROP_ZONE_METADATA,
@@ -9682,7 +10048,7 @@ function createEncirclement(positions, options = {}, textAmplifiers = {}, contex
9682
10048
  const verts = buildClosedRing(positions, smooth, smoothResolution, DEFAULT_SMOOTH_RESOLUTION$2);
9683
10049
  const perimeter = buildAreaPerimeter(verts);
9684
10050
  const labelFeatures = [];
9685
- const gaps = pushAreaLabels(labelFeatures, verts, { hostile: textAmplifiers.N }, options, context.amplifierPlacements);
10051
+ const gaps = pushAreaLabels(labelFeatures, verts, { hostile: textAmplifiers.N }, options, context.amplifierPlacements, void 0, context);
9686
10052
  const radius = meanRadius(verts);
9687
10053
  const barbLength = radius * Math.max(0, barbLengthRatio);
9688
10054
  const barbs = buildBarbs(perimeter, radius * Math.max(0, barbSpacingRatio), barbLength, gaps);
@@ -9763,7 +10129,7 @@ const EXTRACTION_ZONE_METADATA = {
9763
10129
  * an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
9764
10130
  */
9765
10131
  function createExtractionZone(positions, options = {}, textAmplifiers = {}, context = {}) {
9766
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "EZ"), context.amplifierPlacements);
10132
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "EZ"), context.amplifierPlacements, context);
9767
10133
  }
9768
10134
  const EXTRACTION_ZONE = defineControlMeasure({
9769
10135
  metadata: EXTRACTION_ZONE_METADATA,
@@ -10435,7 +10801,7 @@ function processSegment(p1, p2, radius, arcSegments, isFirstSegment) {
10435
10801
  * );
10436
10802
  * ```
10437
10803
  */
10438
- function createFLOT(positions, options = {}, textAmplifiers = {}) {
10804
+ function createFLOT(positions, options = {}, textAmplifiers = {}, context = {}) {
10439
10805
  const { radius = DEFAULT_RADIUS, radiusPixels, metersPerPixel, arcSegments = DEFAULT_ARC_SEGMENTS, smooth = DEFAULT_FLOT_OPTIONS.smooth, smoothResolution = DEFAULT_FLOT_OPTIONS.smoothResolution } = options;
10440
10806
  let effectiveRadius = radius;
10441
10807
  if (radiusPixels !== void 0 && metersPerPixel !== void 0 && metersPerPixel > 0) effectiveRadius = radiusPixels * metersPerPixel;
@@ -10473,7 +10839,7 @@ function createFLOT(positions, options = {}, textAmplifiers = {}) {
10473
10839
  }
10474
10840
  };
10475
10841
  const labels = [];
10476
- pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options);
10842
+ pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options, "", context);
10477
10843
  return {
10478
10844
  type: "FeatureCollection",
10479
10845
  features: [feature, ...labels]
@@ -10542,14 +10908,14 @@ const PHASE_LINE_METADATA = {
10542
10908
  * Creates a Phase line control measure: a polyline through `positions`
10543
10909
  * labeled at each end with `T` (optionally "PL"-prefixed).
10544
10910
  */
10545
- function createPhaseLine(positions, options = {}, textAmplifiers = {}) {
10911
+ function createPhaseLine(positions, options = {}, textAmplifiers = {}, context = {}) {
10546
10912
  const { includePrefix = true, ...sizeOptions } = options;
10547
10913
  const designation = textAmplifiers.T ?? "";
10548
10914
  const text = includePrefix ? designation.length > 0 ? `PL ${designation}` : "PL" : designation;
10549
10915
  return fixedLabelLineFeatures(positions, {
10550
10916
  part: "phase-line",
10551
10917
  ...sizeOptions
10552
- }, text, textAmplifiers.N);
10918
+ }, text, textAmplifiers.N, context);
10553
10919
  }
10554
10920
  const PHASE_LINE = defineControlMeasure({
10555
10921
  metadata: PHASE_LINE_METADATA,
@@ -10621,12 +10987,12 @@ const BRIDGEHEAD_LINE_METADATA = {
10621
10987
  textAmplifiers: [HOSTILE_LINE_AMPLIFIER]
10622
10988
  };
10623
10989
  /** Creates a Bridgehead line labeled "BL" above both ends. */
10624
- function createBridgeheadLine(positions, options = {}, textAmplifiers = {}) {
10990
+ function createBridgeheadLine(positions, options = {}, textAmplifiers = {}, context = {}) {
10625
10991
  return fixedLabelLineFeatures(positions, {
10626
10992
  part: "bridgehead-line",
10627
10993
  placement: "above",
10628
10994
  ...options
10629
- }, "BL", textAmplifiers.N);
10995
+ }, "BL", textAmplifiers.N, context);
10630
10996
  }
10631
10997
  const BRIDGEHEAD_LINE = defineControlMeasure({
10632
10998
  metadata: BRIDGEHEAD_LINE_METADATA,
@@ -10697,12 +11063,12 @@ const HOLDING_LINE_METADATA = {
10697
11063
  textAmplifiers: [HOSTILE_LINE_AMPLIFIER]
10698
11064
  };
10699
11065
  /** Creates a Holding line labeled "HL" above both ends. */
10700
- function createHoldingLine(positions, options = {}, textAmplifiers = {}) {
11066
+ function createHoldingLine(positions, options = {}, textAmplifiers = {}, context = {}) {
10701
11067
  return fixedLabelLineFeatures(positions, {
10702
11068
  part: "holding-line",
10703
11069
  placement: "above",
10704
11070
  ...options
10705
- }, "HL", textAmplifiers.N);
11071
+ }, "HL", textAmplifiers.N, context);
10706
11072
  }
10707
11073
  const HOLDING_LINE = defineControlMeasure({
10708
11074
  metadata: HOLDING_LINE_METADATA,
@@ -10773,12 +11139,12 @@ const RELEASE_LINE_METADATA = {
10773
11139
  textAmplifiers: [HOSTILE_LINE_AMPLIFIER]
10774
11140
  };
10775
11141
  /** Creates a Release line labeled "RL" above both ends. */
10776
- function createReleaseLine(positions, options = {}, textAmplifiers = {}) {
11142
+ function createReleaseLine(positions, options = {}, textAmplifiers = {}, context = {}) {
10777
11143
  return fixedLabelLineFeatures(positions, {
10778
11144
  part: "release-line",
10779
11145
  placement: "above",
10780
11146
  ...options
10781
- }, "RL", textAmplifiers.N);
11147
+ }, "RL", textAmplifiers.N, context);
10782
11148
  }
10783
11149
  const RELEASE_LINE = defineControlMeasure({
10784
11150
  metadata: RELEASE_LINE_METADATA,
@@ -10852,12 +11218,12 @@ const FORWARD_EDGE_OF_BATTLE_AREA_METADATA = {
10852
11218
  * Creates a Forward edge of the battle area control measure: a polyline
10853
11219
  * through `positions` labeled "FEBA" above the line near each end.
10854
11220
  */
10855
- function createForwardEdgeOfBattleArea(positions, options = {}, textAmplifiers = {}) {
11221
+ function createForwardEdgeOfBattleArea(positions, options = {}, textAmplifiers = {}, context = {}) {
10856
11222
  return fixedLabelLineFeatures(positions, {
10857
11223
  part: "forward-edge-of-battle-area",
10858
11224
  placement: "above",
10859
11225
  ...options
10860
- }, "FEBA", textAmplifiers.N);
11226
+ }, "FEBA", textAmplifiers.N, context);
10861
11227
  }
10862
11228
  const FORWARD_EDGE_OF_BATTLE_AREA = defineControlMeasure({
10863
11229
  metadata: FORWARD_EDGE_OF_BATTLE_AREA_METADATA,
@@ -10931,12 +11297,12 @@ const HANDOVER_LINE_METADATA = {
10931
11297
  * Creates a Handover line control measure: a polyline through `positions`
10932
11298
  * labeled "HOL" above the line near each end.
10933
11299
  */
10934
- function createHandoverLine(positions, options = {}, textAmplifiers = {}) {
11300
+ function createHandoverLine(positions, options = {}, textAmplifiers = {}, context = {}) {
10935
11301
  return fixedLabelLineFeatures(positions, {
10936
11302
  part: "handover-line",
10937
11303
  placement: "above",
10938
11304
  ...options
10939
- }, "HOL", textAmplifiers.N);
11305
+ }, "HOL", textAmplifiers.N, context);
10940
11306
  }
10941
11307
  const HANDOVER_LINE = defineControlMeasure({
10942
11308
  metadata: HANDOVER_LINE_METADATA,
@@ -11010,12 +11376,12 @@ const BATTLE_HANDOVER_LINE_METADATA = {
11010
11376
  * Creates a Battle handover line control measure: a polyline through
11011
11377
  * `positions` labeled "BHL" above the line near each end.
11012
11378
  */
11013
- function createBattleHandoverLine(positions, options = {}, textAmplifiers = {}) {
11379
+ function createBattleHandoverLine(positions, options = {}, textAmplifiers = {}, context = {}) {
11014
11380
  return fixedLabelLineFeatures(positions, {
11015
11381
  part: "battle-handover-line",
11016
11382
  placement: "above",
11017
11383
  ...options
11018
- }, "BHL", textAmplifiers.N);
11384
+ }, "BHL", textAmplifiers.N, context);
11019
11385
  }
11020
11386
  const BATTLE_HANDOVER_LINE = defineControlMeasure({
11021
11387
  metadata: BATTLE_HANDOVER_LINE_METADATA,
@@ -11167,7 +11533,7 @@ function generateFortifiedPoints(projectedPoints, effectiveSize) {
11167
11533
  * @param options - Configuration options for the graphic
11168
11534
  * @returns A GeoJSON FeatureCollection containing the castellated LineString
11169
11535
  */
11170
- function createFortifiedLine(positions, options = {}, textAmplifiers = {}) {
11536
+ function createFortifiedLine(positions, options = {}, textAmplifiers = {}, context = {}) {
11171
11537
  const { smooth = DEFAULT_FORTIFIED_LINE_OPTIONS.smooth, smoothResolution = DEFAULT_FORTIFIED_LINE_OPTIONS.smoothResolution } = options;
11172
11538
  const safeSize = calculateEffectiveSize(options);
11173
11539
  const projectedPoints = positions.map((p) => project(p[0], p[1]));
@@ -11181,7 +11547,7 @@ function createFortifiedLine(positions, options = {}, textAmplifiers = {}) {
11181
11547
  }
11182
11548
  };
11183
11549
  const labels = [];
11184
- pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options);
11550
+ pushHostileEndLabels(labels, smoothedPath, textAmplifiers.N, options, "", context);
11185
11551
  return {
11186
11552
  type: "FeatureCollection",
11187
11553
  features: [feature, ...labels]
@@ -11265,7 +11631,7 @@ function createFortifiedArea(positions, options = {}, textAmplifiers = {}, conte
11265
11631
  }, options, context.amplifierPlacements, {
11266
11632
  ringVerts: boundaryPoints,
11267
11633
  extraClearanceMeters: 3 * safeSize
11268
- })),
11634
+ }, context)),
11269
11635
  properties: {}
11270
11636
  }];
11271
11637
  features.push(...labelFeatures);
@@ -12190,7 +12556,7 @@ const JOINT_TACTICAL_ACTION_AREA_METADATA = {
12190
12556
  * {@link labeledAreaFeatures}.
12191
12557
  */
12192
12558
  function createJointTacticalActionArea(positions, options = {}, textAmplifiers = {}, context = {}) {
12193
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "JTAA -", ""), context.amplifierPlacements);
12559
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "JTAA -", ""), context.amplifierPlacements, context);
12194
12560
  }
12195
12561
  const JOINT_TACTICAL_ACTION_AREA = defineControlMeasure({
12196
12562
  metadata: JOINT_TACTICAL_ACTION_AREA_METADATA,
@@ -12249,7 +12615,7 @@ const LANDING_ZONE_METADATA = {
12249
12615
  * amplifier rows, per {@link labeledAreaFeatures}.
12250
12616
  */
12251
12617
  function createLandingZone(positions, options = {}, textAmplifiers = {}, context = {}) {
12252
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "LZ"), context.amplifierPlacements);
12618
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "LZ"), context.amplifierPlacements, context);
12253
12619
  }
12254
12620
  const LANDING_ZONE = defineControlMeasure({
12255
12621
  metadata: LANDING_ZONE_METADATA,
@@ -12320,7 +12686,7 @@ function createLimitedAccessArea(positions, options = {}, textAmplifiers = {}, c
12320
12686
  }];
12321
12687
  return {
12322
12688
  type: "FeatureCollection",
12323
- features: [...patternedAreaFeatures(verts, pushAreaLabels(labelFeatures, verts, { hostile: textAmplifiers.N }, options, context.amplifierPlacements), "reverse-hatch"), ...labelFeatures]
12689
+ features: [...patternedAreaFeatures(verts, pushAreaLabels(labelFeatures, verts, { hostile: textAmplifiers.N }, options, context.amplifierPlacements, void 0, context), "reverse-hatch"), ...labelFeatures]
12324
12690
  };
12325
12691
  }
12326
12692
  const LIMITED_ACCESS_AREA = defineControlMeasure({
@@ -12431,7 +12797,7 @@ function createNoFireAreaIrregular(positions, options = {}, textAmplifiers = {},
12431
12797
  }
12432
12798
  return {
12433
12799
  type: "FeatureCollection",
12434
- features: [...patternedAreaFeatures(verts, pushAreaLabels(labels, verts, { hostile: textAmplifiers.N }, options, context.amplifierPlacements), "reverse-hatch"), ...labels]
12800
+ features: [...patternedAreaFeatures(verts, pushAreaLabels(labels, verts, { hostile: textAmplifiers.N }, options, context.amplifierPlacements, void 0, context), "reverse-hatch"), ...labels]
12435
12801
  };
12436
12802
  }
12437
12803
  const NO_FIRE_AREA_IRREGULAR = defineControlMeasure({
@@ -12776,7 +13142,7 @@ function createMinefield(coordinates, options = {}, textAmplifiers = {}, context
12776
13142
  clearance: ENY_CLEARANCE_RATIO * labelSize,
12777
13143
  sizeProps,
12778
13144
  followPlacementRotation: true
12779
- }, context.amplifierPlacements);
13145
+ }, context.amplifierPlacements, context, { labelSize });
12780
13146
  if (textAmplifiers.W) pushLabel(labelFeatures, offset(center, 0, -halfHeight - width * LABEL_CLEARANCE_RATIO), textAmplifiers.W, labelRotation, sizeProps, void 0, "W");
12781
13147
  const features = [{
12782
13148
  type: "Feature",
@@ -12993,7 +13359,7 @@ function createMineArea(positions, options, minedArea, textAmplifiers, context)
12993
13359
  textHeight: resolveLabelOffsetMeters(options, 1),
12994
13360
  clearance: resolveLabelOffsetMeters(options, .12 + labelPadding),
12995
13361
  sizeProps: labelSizeProps(options)
12996
- }) : [], ...pushAreaLabels(labelFeatures, ring, { hostile: textAmplifiers.N }, options, context.amplifierPlacements)], mineFillPattern(mineType)), ...labelFeatures]
13362
+ }, void 0, context, options) : [], ...pushAreaLabels(labelFeatures, ring, { hostile: textAmplifiers.N }, options, context.amplifierPlacements, void 0, context)], mineFillPattern(mineType)), ...labelFeatures]
12997
13363
  };
12998
13364
  }
12999
13365
  function createDynamicMinefield(positions, options = {}, textAmplifiers = {}, context = {}) {
@@ -13496,7 +13862,7 @@ const PICKUP_ZONE_METADATA = {
13496
13862
  * amplifier rows, per {@link labeledAreaFeatures}.
13497
13863
  */
13498
13864
  function createPickupZone(positions, options = {}, textAmplifiers = {}, context = {}) {
13499
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "PZ"), context.amplifierPlacements);
13865
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "PZ"), context.amplifierPlacements, context);
13500
13866
  }
13501
13867
  const PICKUP_ZONE = defineControlMeasure({
13502
13868
  metadata: PICKUP_ZONE_METADATA,
@@ -13551,7 +13917,7 @@ const ASSAULT_POSITION_METADATA = {
13551
13917
  * an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
13552
13918
  */
13553
13919
  function createAssaultPosition(positions, options = {}, textAmplifiers = {}, context = {}) {
13554
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "ASLT"), context.amplifierPlacements);
13920
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "ASLT"), context.amplifierPlacements, context);
13555
13921
  }
13556
13922
  const ASSAULT_POSITION = defineControlMeasure({
13557
13923
  metadata: ASSAULT_POSITION_METADATA,
@@ -13606,7 +13972,7 @@ const ATTACK_POSITION_METADATA = {
13606
13972
  * an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
13607
13973
  */
13608
13974
  function createAttackPosition(positions, options = {}, textAmplifiers = {}, context = {}) {
13609
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "ATK"), context.amplifierPlacements);
13975
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "ATK"), context.amplifierPlacements, context);
13610
13976
  }
13611
13977
  const ATTACK_POSITION = defineControlMeasure({
13612
13978
  metadata: ATTACK_POSITION_METADATA,
@@ -13661,7 +14027,7 @@ const OBJECTIVE_AREA_METADATA = {
13661
14027
  * an `N` (Field N) ENY marker, per {@link labeledAreaFeatures}.
13662
14028
  */
13663
14029
  function createObjectiveArea(positions, options = {}, textAmplifiers = {}, context = {}) {
13664
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "OBJ"), context.amplifierPlacements);
14030
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "OBJ"), context.amplifierPlacements, context);
13665
14031
  }
13666
14032
  const OBJECTIVE_AREA = defineControlMeasure({
13667
14033
  metadata: OBJECTIVE_AREA_METADATA,
@@ -13944,7 +14310,7 @@ function createStrongPoint(positions, options = {}, textAmplifiers = {}, context
13944
14310
  const enyGaps = pushAreaLabels(labelFeatures, verts, {
13945
14311
  name: textAmplifiers.T,
13946
14312
  hostile: textAmplifiers.N
13947
- }, options, context.amplifierPlacements);
14313
+ }, options, context.amplifierPlacements, void 0, context);
13948
14314
  const allGaps = [...gaps, ...enyGaps];
13949
14315
  const boundaryCoords = buildGappedLine(verts, allGaps);
13950
14316
  const tics = buildStrongPointTics(perimeter, allGaps, h);
@@ -14050,7 +14416,7 @@ const SUBMARINE_ACTION_AREA_METADATA = {
14050
14416
  * `W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
14051
14417
  */
14052
14418
  function createSubmarineActionArea(positions, options = {}, textAmplifiers = {}, context = {}) {
14053
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "SAA -", ""), context.amplifierPlacements);
14419
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "SAA -", ""), context.amplifierPlacements, context);
14054
14420
  }
14055
14421
  const SUBMARINE_ACTION_AREA = defineControlMeasure({
14056
14422
  metadata: SUBMARINE_ACTION_AREA_METADATA,
@@ -14110,7 +14476,7 @@ const SUBMARINE_GENERATED_ACTION_AREA_METADATA = {
14110
14476
  * (Field T) plus `W`/`W1`/`N` amplifier rows, per {@link labeledAreaFeatures}.
14111
14477
  */
14112
14478
  function createSubmarineGeneratedActionArea(positions, options = {}, textAmplifiers = {}, context = {}) {
14113
- return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "SGSA -", ""), context.amplifierPlacements);
14479
+ return labeledAreaFeatures(positions, options, composeAreaLabelTexts(textAmplifiers, "SGSA -", ""), context.amplifierPlacements, context);
14114
14480
  }
14115
14481
  const SUBMARINE_GENERATED_ACTION_AREA = defineControlMeasure({
14116
14482
  metadata: SUBMARINE_GENERATED_ACTION_AREA_METADATA,
@@ -14514,7 +14880,9 @@ const DEFINITIONS = {
14514
14880
  polygon: GENERIC_POLYGON,
14515
14881
  rectangle: GENERIC_RECTANGLE,
14516
14882
  circle: GENERIC_CIRCLE,
14883
+ sector: GENERIC_SECTOR,
14517
14884
  text: GENERIC_TEXT,
14885
+ "radar-search-doctrine": RADAR_SEARCH_DOCTRINE,
14518
14886
  "airborne-attack": AIRBORNE_ATTACK,
14519
14887
  "attack-helicopter": ATTACK_HELICOPTER,
14520
14888
  "support-by-fire": SUPPORT_BY_FIRE,
@@ -14806,7 +15174,31 @@ function dispatchControlMeasure(cm, opts) {
14806
15174
  if (!validateInputContract(cm.controlPoints, definition.metadata, opts?.validationMode)) return EMPTY_COLLECTION;
14807
15175
  const generator = definition.generator;
14808
15176
  const textAmplifiers = normalizeTextAmplifiers(cm.textAmplifiers);
14809
- return generator(cm.controlPoints, cm.options ?? {}, textAmplifiers, { amplifierPlacements: cm.amplifierPlacements });
15177
+ const constructionMetersPerCssPixel = resolveConstructionScale(cm.controlPoints, opts?.context?.groundMetersPerCssPixel);
15178
+ const authoredOptions = cm.options ?? {};
15179
+ const generatorOptions = constructionMetersPerCssPixel === void 0 ? authoredOptions : {
15180
+ ...authoredOptions,
15181
+ metersPerPixel: constructionMetersPerCssPixel
15182
+ };
15183
+ return generator(cm.controlPoints, generatorOptions, textAmplifiers, {
15184
+ amplifierPlacements: cm.amplifierPlacements,
15185
+ constructionMetersPerCssPixel,
15186
+ labelSizeClampCssPixels: opts?.context?.labelSizeClampCssPixels,
15187
+ measureText: opts?.context?.measureText
15188
+ });
15189
+ }
15190
+ /** Convert true-ground scale to the Web Mercator metres legacy generators construct in. */
15191
+ function resolveConstructionScale(points, groundMetersPerCssPixel) {
15192
+ if (!(groundMetersPerCssPixel !== void 0 && groundMetersPerCssPixel > 0)) return void 0;
15193
+ let minLatitude = Infinity;
15194
+ let maxLatitude = -Infinity;
15195
+ for (const point of points) {
15196
+ minLatitude = Math.min(minLatitude, point[1]);
15197
+ maxLatitude = Math.max(maxLatitude, point[1]);
15198
+ }
15199
+ const midpointLatitude = (minLatitude + maxLatitude) / 2;
15200
+ const mercatorScale = Math.cos(midpointLatitude * Math.PI / 180);
15201
+ return groundMetersPerCssPixel / Math.max(mercatorScale, Number.EPSILON);
14810
15202
  }
14811
15203
  /**
14812
15204
  * The control-point *input contract*: `controlPoints` must be present and an
@@ -14945,4 +15337,4 @@ function assertNever(value) {
14945
15337
  throw new Error(`Unhandled control measure kind: ${String(value)}`);
14946
15338
  }
14947
15339
  //#endregion
14948
- export { DEFAULT_TACTICAL_ARROW_OPTIONS as $, blockDrawRule as $t, DEFAULT_REARWARD_PASSAGE_OF_LINES_OPTIONS as A, DEFAULT_DIRECTION_OF_ATTACK_AVIATION_OPTIONS as At, DEFAULT_BRIDGEHEAD_LINE_OPTIONS as B, line27DrawRule as Bt, DEFAULT_LANDING_ZONE_OPTIONS as C, DEFAULT_ANTITANK_WALL_OPTIONS as Ct, DEFAULT_SEIZE_OPTIONS as D, DEFAULT_AREA_DEFENSE_OPTIONS as Dt, DEFAULT_WITHDRAW_OPTIONS as E, DEFAULT_AREA_OF_OPERATIONS_OPTIONS as Et, DEFAULT_BATTLE_HANDOVER_LINE_OPTIONS as F, DEFAULT_AMBUSH_OPTIONS as Ft, DEFAULT_FINAL_PROTECTIVE_FIRE_OPTIONS as G, line1DrawRule as Gt, DEFAULT_FLOT_OPTIONS as H, line24DrawRule as Ht, DEFAULT_HANDOVER_LINE_OPTIONS as I, DEFAULT_AIRBORNE_ATTACK_OPTIONS as It, DEFAULT_GUARD_OPTIONS as J, staticPointDrawRule as Jt, DEFAULT_ENCIRCLEMENT_OPTIONS as K, ambushDrawRule as Kt, DEFAULT_FORWARD_EDGE_OF_BATTLE_AREA_OPTIONS as L, rectangleDrawRule as Lt, DEFAULT_FRONTAL_ATTACK_OPTIONS as M, canonicalTextAmplifierKey as Mt, DEFAULT_FORTIFIED_AREA_OPTIONS as N, normalizeTextAmplifiers as Nt, DEFAULT_SCREEN_OPTIONS as O, DEFAULT_DIRECTION_OF_SUPPORTING_ATTACK_OPTIONS as Ot, DEFAULT_FORTIFIED_LINE_OPTIONS as P, resolveAmplifierPlacement as Pt, DEFAULT_COVER_OPTIONS as Q, disruptDrawRule as Qt, DEFAULT_RELEASE_LINE_OPTIONS as R, axis1DrawRule as Rt, DEFAULT_MAIN_ATTACK_OPTIONS as S, DEFAULT_ATTACK_BY_FIRE_OPTIONS as St, DEFAULT_WITHDRAW_UNDER_PRESSURE_OPTIONS as T, DEFAULT_ASSEMBLY_AREA_OPTIONS as Tt, DEFAULT_FIX_MISSION_TASK_OPTIONS as U, line23DrawRule as Ut, DEFAULT_PHASE_LINE_OPTIONS as V, line26DrawRule as Vt, DEFAULT_FIX_OPTIONS as W, turnDrawRule as Wt, DEFAULT_DISENGAGE_OPTIONS as X, penetrateDrawRule as Xt, DEFAULT_DISRUPT_MISSION_TASK_OPTIONS as Y, point12DrawRule as Yt, DEFAULT_DELAY_OPTIONS as Z, centerRadiusDrawRule as Zt, DEFAULT_PICKUP_ZONE_OPTIONS as _, DEFAULT_BOUNDARY_OPTIONS as _t, DEFINITIONS as a, createBaselineFrame as an, DEFAULT_GENERIC_RECTANGLE_OPTIONS as at, DEFAULT_OBSTACLE_BYPASS_DIFFICULT_OPTIONS as b, DEFAULT_BATTLE_POSITION_OPTIONS as bt, getDefaultOptions as c, haversineDistance as cn, DEFAULT_GENERIC_CIRCLE_OPTIONS as ct, DEFAULT_TURN_OPTIONS as d, EPSILON as dn, DEFAULT_BYPASS_OPTIONS as dt, computeDefaultMidpointPerpendicularPoint as en, DEFAULT_COUNTERATTACK_BY_FIRE_OPTIONS as et, DEFAULT_SUPPORT_BY_FIRE_OPTIONS as f, getMetersPerPixel as fn, DEFAULT_BREACH_OPTIONS as ft, DEFAULT_PRINCIPAL_DIRECTION_OF_FIRE_OPTIONS as g, DEFAULT_LIGHT_LINE_OPTIONS as gt, SECONDARY_DIRECTION_OF_FIRE_DASH as h, DEFAULT_ENGINEER_WORK_LINE_OPTIONS as ht, CONTROL_MEASURE_METADATA as i, snapToMidpointPerpendicular as in, DEFAULT_GENERIC_TEXT_OPTIONS as it, DEFAULT_PENETRATE_OPTIONS as j, TEXT_AMPLIFIER_FIELDS as jt, DEFAULT_RETIRE_OPTIONS as k, DEFAULT_DIRECTION_OF_MAIN_ATTACK_OPTIONS as kt, listControlMeasureMetadata as l, project as ln, DEFAULT_CLASSIC_ARROW_OPTIONS as lt, DEFAULT_SECONDARY_DIRECTION_OF_FIRE_OPTIONS as m, DEFAULT_GENERIC_C2_LINE_OPTIONS as mt, resolveStyleHints as n, getMidpointPerpendicularSignedDistance as nn, DEFAULT_SUPPORTING_ATTACK_OPTIONS as nt, getControlMeasureMetadata as o, calculateMetrics as on, DEFAULT_GENERIC_POLYGON_OPTIONS as ot, DEFAULT_STRONG_POINT_OPTIONS as p, roundToFixed as pn, DEFAULT_BLOCK_MISSION_TASK_OPTIONS as pt, DEFAULT_DISRUPT_OPTIONS as q, attackByFireDrawRule as qt, CONTROL_MEASURE_IDS as r, pointOnMidpointPerpendicularAxis as rn, DEFAULT_CLEAR_OPTIONS as rt, getControlMeasureMetadataByValue as s, computeInitialWidthPoint as sn, DEFAULT_GENERIC_LINE_OPTIONS as st, renderControlMeasure as t, createMidpointPerpendicularDrawRule as tn, DEFAULT_COUNTERATTACK_OPTIONS as tt, DEFAULT_TURNING_MOVEMENT_OPTIONS as u, unproject as un, DEFAULT_CANALIZE_OPTIONS as ut, DEFAULT_OBSTACLE_BYPASS_IMPOSSIBLE_OPTIONS as v, DEFAULT_BLOCK_ARROW_OPTIONS as vt, DEFAULT_JOINT_TACTICAL_ACTION_AREA_OPTIONS as w, DEFAULT_ANTITANK_DITCH_OPTIONS as wt, DEFAULT_MINEFIELD_OPTIONS as x, DEFAULT_ATTACK_HELICOPTER_OPTIONS as xt, DEFAULT_OBSTACLE_BYPASS_EASY_OPTIONS as y, DEFAULT_BLOCK_OPTIONS as yt, DEFAULT_HOLDING_LINE_OPTIONS as z, supportByFireDrawRule as zt };
15340
+ export { DEFAULT_TACTICAL_ARROW_OPTIONS as $, line26DrawRule as $t, DEFAULT_REARWARD_PASSAGE_OF_LINES_OPTIONS as A, DEFAULT_AREA_OF_OPERATIONS_OPTIONS as At, DEFAULT_BRIDGEHEAD_LINE_OPTIONS as B, DEFAULT_LABEL_SIZE_CLAMP_CSS_PIXELS as Bt, DEFAULT_LANDING_ZONE_OPTIONS as C, project as Cn, DEFAULT_BLOCK_OPTIONS as Ct, DEFAULT_SEIZE_OPTIONS as D, getMetersPerPixel as Dn, DEFAULT_ANTITANK_WALL_OPTIONS as Dt, DEFAULT_WITHDRAW_OPTIONS as E, EPSILON as En, DEFAULT_ATTACK_BY_FIRE_OPTIONS as Et, DEFAULT_BATTLE_HANDOVER_LINE_OPTIONS as F, TEXT_AMPLIFIER_FIELDS as Ft, DEFAULT_FINAL_PROTECTIVE_FIRE_OPTIONS as G, DEFAULT_STROKE_WIDTH_CSS_PIXELS as Gt, DEFAULT_FLOT_OPTIONS as H, DEFAULT_LINE_JOIN as Ht, DEFAULT_HANDOVER_LINE_OPTIONS as I, canonicalTextAmplifierKey as It, DEFAULT_GUARD_OPTIONS as J, DEFAULT_AIRBORNE_ATTACK_OPTIONS as Jt, DEFAULT_ENCIRCLEMENT_OPTIONS as K, DEFAULT_SYMBOL_COLOR as Kt, DEFAULT_FORWARD_EDGE_OF_BATTLE_AREA_OPTIONS as L, normalizeTextAmplifiers as Lt, DEFAULT_FRONTAL_ATTACK_OPTIONS as M, DEFAULT_DIRECTION_OF_SUPPORTING_ATTACK_OPTIONS as Mt, DEFAULT_FORTIFIED_AREA_OPTIONS as N, DEFAULT_DIRECTION_OF_MAIN_ATTACK_OPTIONS as Nt, DEFAULT_SCREEN_OPTIONS as O, roundToFixed as On, DEFAULT_ANTITANK_DITCH_OPTIONS as Ot, DEFAULT_FORTIFIED_LINE_OPTIONS as P, DEFAULT_DIRECTION_OF_ATTACK_AVIATION_OPTIONS as Pt, DEFAULT_COVER_OPTIONS as Q, line27DrawRule as Qt, DEFAULT_RELEASE_LINE_OPTIONS as R, resolveAmplifierPlacement as Rt, DEFAULT_MAIN_ATTACK_OPTIONS as S, haversineDistance as Sn, DEFAULT_BLOCK_ARROW_OPTIONS as St, DEFAULT_WITHDRAW_UNDER_PRESSURE_OPTIONS as T, unproject as Tn, DEFAULT_ATTACK_HELICOPTER_OPTIONS as Tt, DEFAULT_FIX_MISSION_TASK_OPTIONS as U, DEFAULT_PORTRAYAL as Ut, DEFAULT_PHASE_LINE_OPTIONS as V, DEFAULT_LINE_CAP as Vt, DEFAULT_FIX_OPTIONS as W, DEFAULT_STROKE_DASH_CSS_PIXELS as Wt, DEFAULT_DISENGAGE_OPTIONS as X, axis1DrawRule as Xt, DEFAULT_DISRUPT_MISSION_TASK_OPTIONS as Y, rectangleDrawRule as Yt, DEFAULT_DELAY_OPTIONS as Z, supportByFireDrawRule as Zt, DEFAULT_PICKUP_ZONE_OPTIONS as _, pointOnMidpointPerpendicularAxis as _n, DEFAULT_BLOCK_MISSION_TASK_OPTIONS as _t, DEFINITIONS as a, attackByFireDrawRule as an, RADAR_SEARCH_DOCTRINE_FILL_COLOR as at, DEFAULT_OBSTACLE_BYPASS_DIFFICULT_OPTIONS as b, calculateMetrics as bn, DEFAULT_LIGHT_LINE_OPTIONS as bt, getDefaultOptions as c, staticPointDrawRule as cn, DEFAULT_GENERIC_SECTOR_OPTIONS as ct, DEFAULT_TURN_OPTIONS as d, centerRadiusDrawRule as dn, DEFAULT_GENERIC_LINE_OPTIONS as dt, line24DrawRule as en, DEFAULT_COUNTERATTACK_BY_FIRE_OPTIONS as et, DEFAULT_SUPPORT_BY_FIRE_OPTIONS as f, disruptDrawRule as fn, DEFAULT_GENERIC_CIRCLE_OPTIONS as ft, DEFAULT_PRINCIPAL_DIRECTION_OF_FIRE_OPTIONS as g, getMidpointPerpendicularSignedDistance as gn, DEFAULT_BREACH_OPTIONS as gt, SECONDARY_DIRECTION_OF_FIRE_DASH as h, createMidpointPerpendicularDrawRule as hn, DEFAULT_BYPASS_OPTIONS as ht, CONTROL_MEASURE_METADATA as i, ambushDrawRule as in, DEFAULT_RADAR_SEARCH_DOCTRINE_OPTIONS as it, DEFAULT_PENETRATE_OPTIONS as j, DEFAULT_AREA_DEFENSE_OPTIONS as jt, DEFAULT_RETIRE_OPTIONS as k, DEFAULT_ASSEMBLY_AREA_OPTIONS as kt, listControlMeasureMetadata as l, point12DrawRule as ln, DEFAULT_GENERIC_RECTANGLE_OPTIONS as lt, DEFAULT_SECONDARY_DIRECTION_OF_FIRE_OPTIONS as m, computeDefaultMidpointPerpendicularPoint as mn, DEFAULT_CANALIZE_OPTIONS as mt, resolveStyleHints as n, turnDrawRule as nn, DEFAULT_SUPPORTING_ATTACK_OPTIONS as nt, getControlMeasureMetadata as o, dynamicPointDrawRule as on, RADAR_SEARCH_DOCTRINE_STROKE_COLOR as ot, DEFAULT_STRONG_POINT_OPTIONS as p, blockDrawRule as pn, DEFAULT_CLASSIC_ARROW_OPTIONS as pt, DEFAULT_DISRUPT_OPTIONS as q, DEFAULT_AMBUSH_OPTIONS as qt, CONTROL_MEASURE_IDS as r, line1DrawRule as rn, DEFAULT_CLEAR_OPTIONS as rt, getControlMeasureMetadataByValue as s, sectorDrawRule as sn, DEFAULT_GENERIC_TEXT_OPTIONS as st, renderControlMeasure as t, line23DrawRule as tn, DEFAULT_COUNTERATTACK_OPTIONS as tt, DEFAULT_TURNING_MOVEMENT_OPTIONS as u, penetrateDrawRule as un, DEFAULT_GENERIC_POLYGON_OPTIONS as ut, DEFAULT_OBSTACLE_BYPASS_IMPOSSIBLE_OPTIONS as v, snapToMidpointPerpendicular as vn, DEFAULT_GENERIC_C2_LINE_OPTIONS as vt, DEFAULT_JOINT_TACTICAL_ACTION_AREA_OPTIONS as w, sphericalBearing as wn, DEFAULT_BATTLE_POSITION_OPTIONS as wt, DEFAULT_MINEFIELD_OPTIONS as x, computeInitialWidthPoint as xn, DEFAULT_BOUNDARY_OPTIONS as xt, DEFAULT_OBSTACLE_BYPASS_EASY_OPTIONS as y, createBaselineFrame as yn, DEFAULT_ENGINEER_WORK_LINE_OPTIONS as yt, DEFAULT_HOLDING_LINE_OPTIONS as z, DEFAULT_LABEL_HEIGHT_CSS_PIXELS as zt };