@orbat-mapper/control-measures 0.2.0-alpha.28 → 0.2.0-alpha.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as DEFAULT_BOUNDARY_OPTIONS, A as DEFAULT_FLOT_OPTIONS, At as centerRadiusDrawRule, B as DEFAULT_GENERIC_TEXT_OPTIONS, Bt as computeInitialWidthPoint, C as DEFAULT_SCREEN_OPTIONS, Ct as line24DrawRule, D as DEFAULT_BATTLE_HANDOVER_LINE_OPTIONS, Dt as ambushDrawRule, E as DEFAULT_FORTIFIED_LINE_OPTIONS, Et as line1DrawRule, F as DEFAULT_GUARD_OPTIONS, Ft as getMidpointPerpendicularSignedDistance, G as DEFAULT_CLASSIC_ARROW_OPTIONS, Gt as getMetersPerPixel, H as DEFAULT_GENERIC_POLYGON_OPTIONS, Ht as project, I as DEFAULT_DELAY_OPTIONS, It as pointOnMidpointPerpendicularAxis, J as DEFAULT_BREACH_OPTIONS, K as DEFAULT_CANALIZE_OPTIONS, Kt as roundToFixed, L as DEFAULT_COVER_OPTIONS, Lt as snapToMidpointPerpendicular, M as DEFAULT_FINAL_PROTECTIVE_FIRE_OPTIONS, Mt as blockDrawRule, N as DEFAULT_ENCIRCLEMENT_OPTIONS, Nt as computeDefaultMidpointPerpendicularPoint, O as DEFAULT_HANDOVER_LINE_OPTIONS, Ot as attackByFireDrawRule, P as DEFAULT_DISRUPT_OPTIONS, Pt as createMidpointPerpendicularDrawRule, Q as DEFAULT_LIGHT_LINE_OPTIONS, R as DEFAULT_TACTICAL_ARROW_OPTIONS, Rt as createBaselineFrame, S as DEFAULT_JOINT_TACTICAL_ACTION_AREA_OPTIONS, St as line26DrawRule, T as DEFAULT_FORTIFIED_AREA_OPTIONS, Tt as turnDrawRule, U as DEFAULT_GENERIC_LINE_OPTIONS, Ut as unproject, V as DEFAULT_GENERIC_RECTANGLE_OPTIONS, Vt as haversineDistance, W as DEFAULT_GENERIC_CIRCLE_OPTIONS, Wt as EPSILON, X as DEFAULT_GENERIC_C2_LINE_OPTIONS, Y as DEFAULT_BLOCK_MISSION_TASK_OPTIONS, Z as DEFAULT_ENGINEER_WORK_LINE_OPTIONS, _ as DEFAULT_OBSTACLE_BYPASS_IMPOSSIBLE_OPTIONS, _t as DEFAULT_AMBUSH_OPTIONS, a as DEFINITIONS, at as DEFAULT_ANTITANK_WALL_OPTIONS, b as DEFAULT_MAIN_ATTACK_OPTIONS, bt as axis1DrawRule, c as getDefaultOptions, ct as DEFAULT_AREA_OF_OPERATIONS_OPTIONS, d as DEFAULT_TURN_OPTIONS, dt as canonicalTextAmplifierKey, et as DEFAULT_BLOCK_ARROW_OPTIONS, f as DEFAULT_SUPPORTING_ATTACK_OPTIONS, ft as normalizeTextAmplifiers, g as DEFAULT_PICKUP_ZONE_OPTIONS, gt as DEFAULT_DIRECTION_OF_ATTACK_AVIATION_OPTIONS, h as DEFAULT_PRINCIPAL_DIRECTION_OF_FIRE_OPTIONS, ht as DEFAULT_DIRECTION_OF_MAIN_ATTACK_OPTIONS, i as CONTROL_MEASURE_METADATA, it as DEFAULT_ATTACK_BY_FIRE_OPTIONS, j as DEFAULT_FIX_OPTIONS, jt as disruptDrawRule, k as DEFAULT_PHASE_LINE_OPTIONS, kt as point12DrawRule, l as listControlMeasureMetadata, lt as DEFAULT_AREA_DEFENSE_OPTIONS, m as DEFAULT_STRONG_POINT_OPTIONS, mt as DEFAULT_DIRECTION_OF_SUPPORTING_ATTACK_OPTIONS, n as resolveStyleHints, nt as DEFAULT_BATTLE_POSITION_OPTIONS, o as getControlMeasureMetadata, ot as DEFAULT_ANTITANK_DITCH_OPTIONS, p as DEFAULT_SUPPORT_BY_FIRE_OPTIONS, pt as resolveAmplifierPlacement, q as DEFAULT_BYPASS_OPTIONS, r as CONTROL_MEASURE_IDS, rt as DEFAULT_ATTACK_HELICOPTER_OPTIONS, s as getControlMeasureMetadataByValue, st as DEFAULT_ASSEMBLY_AREA_OPTIONS, t as renderControlMeasure, tt as DEFAULT_BLOCK_OPTIONS, u as DEFAULT_TURNING_MOVEMENT_OPTIONS, ut as TEXT_AMPLIFIER_FIELDS, v as DEFAULT_OBSTACLE_BYPASS_EASY_OPTIONS, vt as DEFAULT_AIRBORNE_ATTACK_OPTIONS, w as DEFAULT_FRONTAL_ATTACK_OPTIONS, wt as line23DrawRule, x as DEFAULT_LANDING_ZONE_OPTIONS, xt as supportByFireDrawRule, y as DEFAULT_OBSTACLE_BYPASS_DIFFICULT_OPTIONS, yt as rectangleDrawRule, z as DEFAULT_CLEAR_OPTIONS, zt as calculateMetrics } from "./renderControlMeasure-CzvFNqY4.mjs";
1
+ import { $ as DEFAULT_BOUNDARY_OPTIONS, A as DEFAULT_FLOT_OPTIONS, At as centerRadiusDrawRule, B as DEFAULT_GENERIC_TEXT_OPTIONS, Bt as computeInitialWidthPoint, C as DEFAULT_SCREEN_OPTIONS, Ct as line24DrawRule, D as DEFAULT_BATTLE_HANDOVER_LINE_OPTIONS, Dt as ambushDrawRule, E as DEFAULT_FORTIFIED_LINE_OPTIONS, Et as line1DrawRule, F as DEFAULT_GUARD_OPTIONS, Ft as getMidpointPerpendicularSignedDistance, G as DEFAULT_CLASSIC_ARROW_OPTIONS, Gt as getMetersPerPixel, H as DEFAULT_GENERIC_POLYGON_OPTIONS, Ht as project, I as DEFAULT_DELAY_OPTIONS, It as pointOnMidpointPerpendicularAxis, J as DEFAULT_BREACH_OPTIONS, K as DEFAULT_CANALIZE_OPTIONS, Kt as roundToFixed, L as DEFAULT_COVER_OPTIONS, Lt as snapToMidpointPerpendicular, M as DEFAULT_FINAL_PROTECTIVE_FIRE_OPTIONS, Mt as blockDrawRule, N as DEFAULT_ENCIRCLEMENT_OPTIONS, Nt as computeDefaultMidpointPerpendicularPoint, O as DEFAULT_HANDOVER_LINE_OPTIONS, Ot as attackByFireDrawRule, P as DEFAULT_DISRUPT_OPTIONS, Pt as createMidpointPerpendicularDrawRule, Q as DEFAULT_LIGHT_LINE_OPTIONS, R as DEFAULT_TACTICAL_ARROW_OPTIONS, Rt as createBaselineFrame, S as DEFAULT_JOINT_TACTICAL_ACTION_AREA_OPTIONS, St as line26DrawRule, T as DEFAULT_FORTIFIED_AREA_OPTIONS, Tt as turnDrawRule, U as DEFAULT_GENERIC_LINE_OPTIONS, Ut as unproject, V as DEFAULT_GENERIC_RECTANGLE_OPTIONS, Vt as haversineDistance, W as DEFAULT_GENERIC_CIRCLE_OPTIONS, Wt as EPSILON, X as DEFAULT_GENERIC_C2_LINE_OPTIONS, Y as DEFAULT_BLOCK_MISSION_TASK_OPTIONS, Z as DEFAULT_ENGINEER_WORK_LINE_OPTIONS, _ as DEFAULT_OBSTACLE_BYPASS_IMPOSSIBLE_OPTIONS, _t as DEFAULT_AMBUSH_OPTIONS, a as DEFINITIONS, at as DEFAULT_ANTITANK_WALL_OPTIONS, b as DEFAULT_MAIN_ATTACK_OPTIONS, bt as axis1DrawRule, c as getDefaultOptions, ct as DEFAULT_AREA_OF_OPERATIONS_OPTIONS, d as DEFAULT_TURN_OPTIONS, dt as canonicalTextAmplifierKey, et as DEFAULT_BLOCK_ARROW_OPTIONS, f as DEFAULT_SUPPORTING_ATTACK_OPTIONS, ft as normalizeTextAmplifiers, g as DEFAULT_PICKUP_ZONE_OPTIONS, gt as DEFAULT_DIRECTION_OF_ATTACK_AVIATION_OPTIONS, h as DEFAULT_PRINCIPAL_DIRECTION_OF_FIRE_OPTIONS, ht as DEFAULT_DIRECTION_OF_MAIN_ATTACK_OPTIONS, i as CONTROL_MEASURE_METADATA, it as DEFAULT_ATTACK_BY_FIRE_OPTIONS, j as DEFAULT_FIX_OPTIONS, jt as disruptDrawRule, k as DEFAULT_PHASE_LINE_OPTIONS, kt as point12DrawRule, l as listControlMeasureMetadata, lt as DEFAULT_AREA_DEFENSE_OPTIONS, m as DEFAULT_STRONG_POINT_OPTIONS, mt as DEFAULT_DIRECTION_OF_SUPPORTING_ATTACK_OPTIONS, n as resolveStyleHints, nt as DEFAULT_BATTLE_POSITION_OPTIONS, o as getControlMeasureMetadata, ot as DEFAULT_ANTITANK_DITCH_OPTIONS, p as DEFAULT_SUPPORT_BY_FIRE_OPTIONS, pt as resolveAmplifierPlacement, q as DEFAULT_BYPASS_OPTIONS, r as CONTROL_MEASURE_IDS, rt as DEFAULT_ATTACK_HELICOPTER_OPTIONS, s as getControlMeasureMetadataByValue, st as DEFAULT_ASSEMBLY_AREA_OPTIONS, t as renderControlMeasure, tt as DEFAULT_BLOCK_OPTIONS, u as DEFAULT_TURNING_MOVEMENT_OPTIONS, ut as TEXT_AMPLIFIER_FIELDS, v as DEFAULT_OBSTACLE_BYPASS_EASY_OPTIONS, vt as DEFAULT_AIRBORNE_ATTACK_OPTIONS, w as DEFAULT_FRONTAL_ATTACK_OPTIONS, wt as line23DrawRule, x as DEFAULT_LANDING_ZONE_OPTIONS, xt as supportByFireDrawRule, y as DEFAULT_OBSTACLE_BYPASS_DIFFICULT_OPTIONS, yt as rectangleDrawRule, z as DEFAULT_CLEAR_OPTIONS, zt as calculateMetrics } from "./renderControlMeasure-BdbObD8B.mjs";
2
2
  //#region src/freeze-orientation.ts
3
3
  /**
4
4
  * Dispatches to a measure kind's `freezeOrientation` hook (see
@@ -1,4 +1,4 @@
1
- import { a as DEFINITIONS, t as renderControlMeasure } from "../renderControlMeasure-CzvFNqY4.mjs";
1
+ import { a as DEFINITIONS, t as renderControlMeasure } from "../renderControlMeasure-BdbObD8B.mjs";
2
2
  //#region src/preview/index.ts
3
3
  /** Side length (SVG units) of the repeating hatch tile in {@link PREVIEW_FILL_PATTERNS}. */
4
4
  const PREVIEW_PATTERN_TILE = 8;
@@ -3228,6 +3228,20 @@ const ROW_PITCH_RATIO = 1.2;
3228
3228
  * ENY marker's own half-width/half-height when carving its boundary
3229
3229
  * knockout gap (see {@link pushAreaLabels}). */
3230
3230
  const ENY_CLEARANCE_RATIO = .3;
3231
+ /** Where within each vertex-latitude band {@link ringLabelAnchor} probes for
3232
+ * interior chords — offset samples flank the midpoint so a self-intersection
3233
+ * pinch sitting exactly mid-band doesn't hide the band's real width. */
3234
+ const BAND_SAMPLE_FRACTIONS = [
3235
+ .25,
3236
+ .5,
3237
+ .75
3238
+ ];
3239
+ /** Cap on how many latitude bands {@link ringLabelAnchor} probes. Each probe
3240
+ * rescans the whole ring, so probing every band of a smoothed or imported
3241
+ * outline would make the fallback quadratic (~190ms at 2k vertices, on every
3242
+ * rerender); striding holds it to a fixed multiple of one ring scan. Rings
3243
+ * with no more bands than this are still probed exhaustively. */
3244
+ const MAX_SAMPLED_BANDS = 48;
3231
3245
  const DEFAULT_SMOOTH_RESOLUTION$9 = 16;
3232
3246
  /**
3233
3247
  * Composes an area's main label text from a `prefix` (e.g. `"AO"`) and a
@@ -3262,11 +3276,10 @@ function composeAreaLabelTexts(textAmplifiers, prefix = "", separator = " ") {
3262
3276
  * Polygon centroid (area-weighted, shoelace-derived) of a closed ring in
3263
3277
  * projected meters, falling back to the plain vertex average when the ring's
3264
3278
  * signed area is ~0 (collinear/degenerate — the area-weighted formula would
3265
- * divide by zero). For a **concave** ring the area centroid can fall outside
3266
- * the shape; that limitation is accepted here, matching the doctrinal
3267
- * template's plain "center of the area" placement rather than a
3268
- * point-in-polygon-safe visual center. Exported for reuse by measures that
3269
- * center a non-label glyph on the area (e.g. Airfield Zone's runway glyph).
3279
+ * divide by zero). For a **concave** or self-intersecting ring this point can
3280
+ * fall outside the shape, so anything that must sit *within* the area should
3281
+ * anchor on {@link ringLabelAnchor} instead; this raw centroid remains exported
3282
+ * for callers that want the plain moment-derived center.
3270
3283
  */
3271
3284
  function ringCentroid(verts) {
3272
3285
  const n = verts.length - 1;
@@ -3327,6 +3340,125 @@ function ringCrossingsAtY(verts, target) {
3327
3340
  totalArc: dist
3328
3341
  };
3329
3342
  }
3343
+ /**
3344
+ * Ascending x of the ring's crossings of the horizontal line `y = target`. The
3345
+ * ring's **interior spans** at that latitude are the consecutive pairs
3346
+ * `[xs[0], xs[1]]`, `[xs[2], xs[3]]`, … — the even-odd fill rule — so a
3347
+ * self-intersecting ring (a bowtie) yields one span per lobe. The half-open
3348
+ * per-edge test (`min(ay,by) <= target < max(ay,by)`, matching
3349
+ * {@link ringCrossingsAtY}) counts a vertex sitting exactly on `target` once, so
3350
+ * the count is even for any non-degenerate ring; an odd count leaves the last
3351
+ * crossing unpaired and callers ignore it.
3352
+ *
3353
+ * Deliberately does not go through {@link ringCrossingsAtY}: that builds a
3354
+ * `RingCrossing` per crossing with an arc length and a normalized tangent, and
3355
+ * {@link ringLabelAnchor} probes up to `3 * MAX_SAMPLED_BANDS` latitudes per
3356
+ * render and reads nothing but x. Scalar arithmetic here keeps those probes free
3357
+ * of per-edge vector allocation and square roots.
3358
+ */
3359
+ function ringCrossingXsAtY(verts, target) {
3360
+ const xs = [];
3361
+ for (let i = 0; i < verts.length - 1; i++) {
3362
+ const a = verts[i];
3363
+ const b = verts[i + 1];
3364
+ const ay = a[1];
3365
+ const by = b[1];
3366
+ if (Math.min(ay, by) <= target && target < Math.max(ay, by)) xs.push(a[0] + (target - ay) / (by - ay) * (b[0] - a[0]));
3367
+ }
3368
+ return xs.sort((left, right) => left - right);
3369
+ }
3370
+ /** Whether `x` falls within one of the interior spans paired off `xs` (see
3371
+ * {@link ringCrossingXsAtY}), ignoring spans pinched to no width. */
3372
+ function spansContainX(xs, x) {
3373
+ for (let i = 0; i + 1 < xs.length; i += 2) {
3374
+ const x0 = xs[i];
3375
+ const x1 = xs[i + 1];
3376
+ if (x1 - x0 > 1e-6 && x0 <= x && x <= x1) return true;
3377
+ }
3378
+ return false;
3379
+ }
3380
+ /** The latitudes {@link ringLabelAnchor} probes for interior chords: each
3381
+ * {@link BAND_SAMPLE_FRACTIONS} offset into every band between consecutive
3382
+ * distinct vertex latitudes, strided to at most {@link MAX_SAMPLED_BANDS}
3383
+ * bands. */
3384
+ function chordSampleLatitudes(verts) {
3385
+ const bandEdges = [...new Set(verts.map((vert) => vert[1]))].sort((a, b) => a - b);
3386
+ const bandCount = bandEdges.length - 1;
3387
+ const stride = Math.max(1, Math.ceil(bandCount / MAX_SAMPLED_BANDS));
3388
+ const latitudes = [];
3389
+ for (let i = 0; i < bandCount; i += stride) {
3390
+ const low = bandEdges[i];
3391
+ const height = bandEdges[i + 1] - low;
3392
+ for (const fraction of BAND_SAMPLE_FRACTIONS) latitudes.push(low + fraction * height);
3393
+ }
3394
+ return latitudes;
3395
+ }
3396
+ /**
3397
+ * Default anchor for an area's centered text — the ring's {@link ringCentroid}
3398
+ * whenever that point actually lies inside the ring, so every convex area (and
3399
+ * every concave one whose centroid still falls within it) keeps the doctrinal
3400
+ * template's plain "center of the area" placement unchanged.
3401
+ *
3402
+ * A strongly concave or self-intersecting ring can put the area-weighted
3403
+ * centroid outside the shape — in a bowtie the two lobes wind oppositely, so
3404
+ * their shoelace contributions partly cancel and the centroid drifts far off,
3405
+ * stranding the label on open ground. For those rings the anchor falls back to
3406
+ * the midpoint of the ring's **widest interior chord**: the interior spans
3407
+ * ({@link ringCrossingXsAtY}) at each probed latitude
3408
+ * ({@link chordSampleLatitudes}) are compared, and the widest wins. Horizontal
3409
+ * chord width is the right thing to maximize because area rows are horizontal
3410
+ * text
3411
+ * ({@link HORIZONTAL_LABEL_ROTATION}), and the result is inside the ring by
3412
+ * construction, which also guarantees {@link pushAreaLabels}'s Field N "ENY"
3413
+ * markers find their two edge crossings.
3414
+ *
3415
+ * Three samples per band rather than one: a band's crossing *count* is constant
3416
+ * within the band, but a self-intersection inside it (which is not a vertex, so
3417
+ * it opens no band of its own) pinches a span to zero width at one latitude —
3418
+ * exactly the midpoint on a symmetric bowtie. Offset samples step around that.
3419
+ * The same zero-width degeneracy is why the centroid must land in a span of
3420
+ * real width to be kept: a bowtie's centroid can sit precisely on the pinch,
3421
+ * which is technically "inside" but the worst spot on the shape. Equal-width
3422
+ * candidates resolve to the one nearest the centroid's latitude, then to the
3423
+ * southernmost, keeping the anchor deterministic.
3424
+ *
3425
+ * Each probe rescans the ring, so bands are strided to at most
3426
+ * {@link MAX_SAMPLED_BANDS} of them, keeping the fallback linear in vertex count
3427
+ * rather than quadratic — a smoothed or imported outline can carry thousands of
3428
+ * vertices and this runs on every rerender. Rings within the cap are probed
3429
+ * exhaustively, so ordinary drawn areas are unaffected. Striding samples bands
3430
+ * rather than uniform latitudes because a probe must land *within* a band to
3431
+ * dodge the pinch above.
3432
+ *
3433
+ * This is not a pole of inaccessibility: the chord midpoint is interior but not
3434
+ * maximally distant from the boundary, so a row wider than the chord can still
3435
+ * overhang. Nor is the chord guaranteed widest once striding kicks in — a wide
3436
+ * chord confined to a single skipped band is missed — but the result is still
3437
+ * interior, which is what correctness rests on. Rings with no interior span of
3438
+ * real width (fully degenerate) return the centroid.
3439
+ */
3440
+ function ringLabelAnchor(verts) {
3441
+ const centroid = ringCentroid(verts);
3442
+ if (spansContainX(ringCrossingXsAtY(verts, centroid[1]), centroid[0])) return centroid;
3443
+ let anchor;
3444
+ let widestSeen = 0;
3445
+ let anchorOffset = Infinity;
3446
+ for (const y of chordSampleLatitudes(verts)) {
3447
+ const offset = Math.abs(y - centroid[1]);
3448
+ const xs = ringCrossingXsAtY(verts, y);
3449
+ for (let i = 0; i + 1 < xs.length; i += 2) {
3450
+ const x0 = xs[i];
3451
+ const x1 = xs[i + 1];
3452
+ const width = x1 - x0;
3453
+ if (width < 1e-6) continue;
3454
+ if (width > widestSeen + 1e-6) widestSeen = width;
3455
+ else if (width < widestSeen - 1e-6 || offset >= anchorOffset) continue;
3456
+ anchorOffset = offset;
3457
+ anchor = [.5 * (x0 + x1), y];
3458
+ }
3459
+ }
3460
+ return anchor ?? centroid;
3461
+ }
3330
3462
  /** Nearest contact point on a closed ring for a dragged marker anchor. */
3331
3463
  function nearestPointOnRing(verts, target) {
3332
3464
  let nearest;
@@ -3366,7 +3498,7 @@ function labelHalfExtent(direction, textWidth, textHeight, rotation) {
3366
3498
  * empty unless `texts.hostile` was placed.
3367
3499
  *
3368
3500
  * Rows are `[name (T), additionalInformation (H), dtgStart (W), dtgEnd (W1)]`,
3369
- * filtered to non-empty text, stacked around the ring's {@link ringCentroid}
3501
+ * filtered to non-empty text, stacked around the ring's {@link ringLabelAnchor}
3370
3502
  * at `pitch = resolveLabelOffsetMeters(options, 1.2 + labelPadding)` (the 50m
3371
3503
  * no-size fallback applies until a live label size is captured — see
3372
3504
  * `capturesLabelSize` — so a transient row overlap is possible right after
@@ -3374,7 +3506,7 @@ function labelHalfExtent(direction, textWidth, textHeight, rotation) {
3374
3506
  * row (the name) reads topmost.
3375
3507
  *
3376
3508
  * When `texts.hostile` is set, the ring's west/east crossings of the
3377
- * horizontal line through the centroid are found ({@link ringCrossingsAtY});
3509
+ * horizontal line through the anchor are found ({@link ringCrossingsAtY});
3378
3510
  * the minimum/maximum-x crossings are the default anchors for the two "ENY"
3379
3511
  * labels. A persisted `label:N:west` / `label:N:east` placement replaces that
3380
3512
  * anchor. Each marker carves a masking {@link ArcGap} at the nearest point on
@@ -3388,10 +3520,12 @@ function labelHalfExtent(direction, textWidth, textHeight, rotation) {
3388
3520
  * `textHeight = resolveLabelOffsetMeters(options, 1)`,
3389
3521
  * `textWidth = text.length * LABEL_CHAR_ASPECT_RATIO * textHeight`, and
3390
3522
  * `clearance = (ENY_CLEARANCE_RATIO + labelPadding) * textHeight` — mirroring
3391
- * {@link placeEchelon}'s glyph-width masking gap (ADR-0023). Fewer than two
3392
- * crossings (e.g. a degenerate ring, or a concave shape whose centroid
3393
- * latitude misses the boundary) skips the markers silently rather than
3394
- * guessing their default placements.
3523
+ * {@link placeEchelon}'s glyph-width masking gap (ADR-0023). Because the anchor
3524
+ * is interior by construction there are normally two crossings; fewer (only a
3525
+ * degenerate ring) skips the markers silently rather than guessing their default
3526
+ * placements. On a self-intersecting ring the extreme crossings can belong to
3527
+ * different lobes than the anchor's, which still reads as the area's west and
3528
+ * east edges.
3395
3529
  *
3396
3530
  * A ring with fewer than 2 vertices is degenerate — nothing is placed.
3397
3531
  *
@@ -3405,7 +3539,7 @@ function pushAreaLabels(out, ringVerts, texts, options = {}, amplifierPlacements
3405
3539
  const sizeProps = labelSizeProps(options);
3406
3540
  const padding = Math.max(0, options.labelPadding ?? 0);
3407
3541
  const maskRingVerts = maskBoundary?.ringVerts ?? ringVerts;
3408
- const [cx, cy] = ringCentroid(ringVerts);
3542
+ const [cx, cy] = ringLabelAnchor(ringVerts);
3409
3543
  const rows = [
3410
3544
  [texts.name, "T"],
3411
3545
  [texts.additionalInformation, "H"],
@@ -4073,7 +4207,7 @@ function createAirfieldZone(positions, options = {}, textAmplifiers = {}, contex
4073
4207
  if (x > maxX) maxX = x;
4074
4208
  }
4075
4209
  const bboxWidth = maxX - minX;
4076
- const [cx, cy] = ringCentroid(verts);
4210
+ const [cx, cy] = ringLabelAnchor(verts);
4077
4211
  if (bboxWidth >= 1e-6) {
4078
4212
  const longHalf = .5 * glyphSizeRatio * bboxWidth;
4079
4213
  const shortHalf = longHalf * SHORT_LINE_RATIO;
@@ -10312,7 +10446,7 @@ const LIMITED_ACCESS_AREA_METADATA = {
10312
10446
  function createLimitedAccessArea(positions, options = {}, textAmplifiers = {}, context = {}) {
10313
10447
  const { smooth = false, smoothResolution = 16 } = options;
10314
10448
  const verts = buildClosedRing(positions, smooth, smoothResolution, 16);
10315
- const center = ringCentroid(verts);
10449
+ const center = ringLabelAnchor(verts);
10316
10450
  const labelFeatures = [{
10317
10451
  type: "Feature",
10318
10452
  properties: {
@@ -10414,7 +10548,7 @@ function makeLabel(out, point, text, options, properties = {}) {
10414
10548
  function createNoFireAreaIrregular(positions, options = {}, textAmplifiers = {}, context = {}) {
10415
10549
  const { smooth = false, smoothResolution = 16 } = options;
10416
10550
  const verts = buildClosedRing(positions, smooth, smoothResolution, 16);
10417
- const [cx, cy] = ringCentroid(verts);
10551
+ const [cx, cy] = ringLabelAnchor(verts);
10418
10552
  const hasWindow = Boolean(textAmplifiers.W || textAmplifiers.W1);
10419
10553
  const rowCount = 1 + (textAmplifiers.T ? 1 : 0) + (hasWindow ? 1 : 0);
10420
10554
  const pitch = resolveLabelOffsetMeters(options, 1.2 + Math.max(0, options.labelPadding ?? .7));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orbat-mapper/control-measures",
3
- "version": "0.2.0-alpha.28",
3
+ "version": "0.2.0-alpha.29",
4
4
  "description": "Library for drawing tactical graphics and control measures according to MIL-STD-2525 and APP-6 standards.",
5
5
  "license": "MIT",
6
6
  "author": "Orbat Mapper",