@orbat-mapper/control-measures 0.19.0 → 0.21.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.
- package/README.md +4 -0
- package/dist/{index-CGoJeN1B.d.mts → index-DuWUI8Se.d.mts} +145 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +83 -2
- package/dist/preview/index.d.mts +1 -1
- package/dist/preview/index.mjs +1 -1
- package/dist/{renderControlMeasure-C2YRB8uJ.mjs → renderControlMeasure-Cn4Yh917.mjs} +2346 -1858
- package/media/defeat.svg +1 -0
- package/media/destroy.svg +1 -0
- package/media/neutralize.svg +1 -0
- package/media/suppress.svg +1 -0
- package/package.json +1 -1
|
@@ -1,3 +1,50 @@
|
|
|
1
|
+
//#region src/style.ts
|
|
2
|
+
/**
|
|
3
|
+
* Per-feature style hint pinning a filled part to a solid interior. Attach to
|
|
4
|
+
* a generator's intrinsic doctrinal accents — arrowheads, teeth, echelon
|
|
5
|
+
* glyphs, barbs, blades — so a patterned (`hatch`, `dots`, …) fill set at the graphicsStyle
|
|
6
|
+
* or measure layer never bleeds into a small silhouette and ruins its
|
|
7
|
+
* legibility. The renderer only reads style hints, so one shared instance is
|
|
8
|
+
* safe to reuse across every accent feature.
|
|
9
|
+
*/
|
|
10
|
+
const SOLID_ACCENT_FILL = { fillPattern: "solid" };
|
|
11
|
+
/**
|
|
12
|
+
* Ultimate fallback for the symbol color when no `color` or per-channel
|
|
13
|
+
* override is supplied at any layer. Keeps a zero-config render monocolor
|
|
14
|
+
* black and guarantees filled parts still render filled. See ADR-0011.
|
|
15
|
+
*/
|
|
16
|
+
const DEFAULT_SYMBOL_COLOR = "#000000";
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/portrayal.ts
|
|
19
|
+
const DEFAULT_STROKE_WIDTH_CSS_PIXELS = 2;
|
|
20
|
+
const DEFAULT_STROKE_DASH_CSS_PIXELS = Object.freeze([]);
|
|
21
|
+
const DEFAULT_LINE_CAP = "round";
|
|
22
|
+
const DEFAULT_LINE_JOIN = "round";
|
|
23
|
+
const DEFAULT_LABEL_HEIGHT_CSS_PIXELS = 14;
|
|
24
|
+
const DEFAULT_LABEL_SIZE_CLAMP_CSS_PIXELS = {
|
|
25
|
+
min: 8,
|
|
26
|
+
max: 24
|
|
27
|
+
};
|
|
28
|
+
/** Shared absent-value portrayal defaults; render output remains sparse. */
|
|
29
|
+
const DEFAULT_PORTRAYAL = Object.freeze({
|
|
30
|
+
symbolColor: DEFAULT_SYMBOL_COLOR,
|
|
31
|
+
strokeWidthCssPixels: 2,
|
|
32
|
+
strokeDashCssPixels: DEFAULT_STROKE_DASH_CSS_PIXELS,
|
|
33
|
+
lineCap: DEFAULT_LINE_CAP,
|
|
34
|
+
lineJoin: DEFAULT_LINE_JOIN,
|
|
35
|
+
labelHeightCssPixels: 14
|
|
36
|
+
});
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/define.ts
|
|
39
|
+
/**
|
|
40
|
+
* Identity helper that infers a definition's literal `Id` and precise generator
|
|
41
|
+
* type `G`, so the registry's `typeof DEFINITIONS` carries each measure's exact
|
|
42
|
+
* options type. Internal authoring API — not part of the published surface.
|
|
43
|
+
*/
|
|
44
|
+
function defineControlMeasure(definition) {
|
|
45
|
+
return definition;
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
1
48
|
//#region src/internal/graphics-utils.ts
|
|
2
49
|
/**
|
|
3
50
|
* Small value used to avoid division by zero and floating-point precision issues.
|
|
@@ -275,334 +322,100 @@ function lineIntersection(p1, p2, p3, p4) {
|
|
|
275
322
|
return [x1 + ua * (x2 - x1), y1 + ua * (y2 - y1)];
|
|
276
323
|
}
|
|
277
324
|
//#endregion
|
|
278
|
-
//#region src/
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
* matching centerline segment, over the reference half-width) live here once.
|
|
291
|
-
*/
|
|
292
|
-
function createWidthOptionHandles(config) {
|
|
325
|
+
//#region src/draw-rules/baseline-frame.ts
|
|
326
|
+
function createBaselineFrame(p1, p2, options = {}) {
|
|
327
|
+
return createProjectedBaselineFrame(project(p1[0], p1[1]), project(p2[0], p2[1]), options);
|
|
328
|
+
}
|
|
329
|
+
function createProjectedBaselineFrame(p1, p2, options = {}) {
|
|
330
|
+
const baseline = vecSub(p2, p1);
|
|
331
|
+
const length = vecMag(baseline);
|
|
332
|
+
if (length < 1e-6) return null;
|
|
333
|
+
const direction = vecNorm(baseline);
|
|
334
|
+
const normalSign = options.normal === "left" ? -1 : 1;
|
|
335
|
+
const normal = vecScale([direction[1], -direction[0]], normalSign);
|
|
336
|
+
const origin = getOrigin(p1, p2, options.origin ?? "midpoint");
|
|
293
337
|
return {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}, {
|
|
303
|
-
id: SHAFT_WIDTH_OPTION_HANDLE_ID,
|
|
304
|
-
position: unproject(neckRight[0], neckRight[1])
|
|
305
|
-
}];
|
|
338
|
+
p1,
|
|
339
|
+
p2,
|
|
340
|
+
origin,
|
|
341
|
+
direction,
|
|
342
|
+
normal,
|
|
343
|
+
length,
|
|
344
|
+
signedNormalDistance(point) {
|
|
345
|
+
return vecDot(vecSub(project(point[0], point[1]), origin), normal);
|
|
306
346
|
},
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
if (!geometry || geometry.referenceHalfWidth < 1e-6) return void 0;
|
|
312
|
-
const from = rear ? geometry.spine[0] : geometry.spine.at(-2);
|
|
313
|
-
const to = rear ? geometry.spine[1] : geometry.spine.at(-1);
|
|
314
|
-
if (!from || !to) return void 0;
|
|
315
|
-
const ratio = clamp(pointToInfiniteLineDistance(project(position[0], position[1]), from, to) / geometry.referenceHalfWidth, config.minRatio, rear ? config.maxRearRatio : config.maxShaftRatio);
|
|
316
|
-
return rear ? { rearWidthRatio: ratio } : { shaftWidthRatio: ratio };
|
|
347
|
+
pointAtNormalDistance(distance) {
|
|
348
|
+
if (!Number.isFinite(distance)) return null;
|
|
349
|
+
const point = vecAdd(origin, vecScale(normal, distance));
|
|
350
|
+
return unproject(point[0], point[1]);
|
|
317
351
|
}
|
|
318
352
|
};
|
|
319
353
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
* points to the body coordinates first.
|
|
325
|
-
*/
|
|
326
|
-
function createVariableWidthAttackOptionHandles(resolveCoordinates = (points) => points) {
|
|
327
|
-
return createWidthOptionHandles({
|
|
328
|
-
geometry(controlPoints, options) {
|
|
329
|
-
const coordinates = resolveCoordinates(controlPoints);
|
|
330
|
-
if (!coordinates) return null;
|
|
331
|
-
const { geometry } = processAttackGeometry(coordinates, options);
|
|
332
|
-
const outerLeft = geometry?.headRing[0];
|
|
333
|
-
if (!geometry || !outerLeft) return null;
|
|
334
|
-
return {
|
|
335
|
-
spine: geometry.shaftCenterline,
|
|
336
|
-
rightEdge: geometry.shaftRight,
|
|
337
|
-
referenceHalfWidth: vecMag(vecSub(outerLeft, geometry.ptBase))
|
|
338
|
-
};
|
|
339
|
-
},
|
|
340
|
-
minRatio: SHAFT_MIN_RATIO,
|
|
341
|
-
maxRearRatio: 3,
|
|
342
|
-
maxShaftRatio: SHAFT_MAX_RATIO
|
|
343
|
-
});
|
|
354
|
+
function getOrigin(p1, p2, origin) {
|
|
355
|
+
if (origin === "p1") return p1;
|
|
356
|
+
if (origin === "p2") return p2;
|
|
357
|
+
return vecScale(vecAdd(p1, p2), .5);
|
|
344
358
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
* variable-width body resolves the option through here so the rule cannot drift.
|
|
350
|
-
*/
|
|
351
|
-
function resolveRearWidthRatio(options, shaftRatio) {
|
|
352
|
-
return options.rearWidthRatio ?? shaftRatio;
|
|
359
|
+
//#endregion
|
|
360
|
+
//#region src/draw-rules/util.ts
|
|
361
|
+
function clonePosition(position) {
|
|
362
|
+
return [...position];
|
|
353
363
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
* This handles validation, default values, projection, and geometry calculation.
|
|
357
|
-
*/
|
|
358
|
-
function processAttackGeometry(coordinates, options = {}) {
|
|
359
|
-
const shaftRatio = clamp(options.shaftWidthRatio ?? .6, SHAFT_MIN_RATIO, SHAFT_MAX_RATIO);
|
|
360
|
-
const smooth = options.smooth ?? false;
|
|
361
|
-
const smoothResolution = options.smoothResolution ?? 5;
|
|
362
|
-
const rearRatio = clamp(resolveRearWidthRatio(options, shaftRatio), SHAFT_MIN_RATIO, 3);
|
|
363
|
-
const points = coordinates.map((c) => project(c[0], c[1]));
|
|
364
|
-
const numPoints = points.length;
|
|
365
|
-
const ptTip = points[0];
|
|
366
|
-
const ptWidth = points[numPoints - 1];
|
|
367
|
-
const spinePoints = [];
|
|
368
|
-
for (let i = numPoints - 2; i >= 1; i--) {
|
|
369
|
-
const p = points[i];
|
|
370
|
-
if (p) spinePoints.push(p);
|
|
371
|
-
}
|
|
372
|
-
return { geometry: calculateSymbolGeometry(ptTip, ptWidth, spinePoints, shaftRatio, rearRatio, smooth, smoothResolution) };
|
|
364
|
+
function clonePositions(positions) {
|
|
365
|
+
return positions.map(clonePosition);
|
|
373
366
|
}
|
|
374
|
-
function
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
const
|
|
381
|
-
const
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
367
|
+
function samePosition(a, b) {
|
|
368
|
+
return a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
|
|
369
|
+
}
|
|
370
|
+
//#endregion
|
|
371
|
+
//#region src/draw-rules/midpoint-perpendicular.ts
|
|
372
|
+
function createMidpointPerpendicularDrawRule(options) {
|
|
373
|
+
const defaultDistanceRatio = options.defaultDistanceRatio ?? 1.5;
|
|
374
|
+
const variableLength = options.variableLength ?? false;
|
|
375
|
+
const constrainedIndex = options.constrainedIndex ?? 2;
|
|
376
|
+
const baselineIndexA = constrainedIndex === 0 ? 1 : 0;
|
|
377
|
+
const baselineIndexB = constrainedIndex === 0 ? 2 : 1;
|
|
378
|
+
const frameOrigin = options.origin ?? "midpoint";
|
|
379
|
+
const frameNormal = options.normal ?? "right";
|
|
380
|
+
function frameFor(p1, p2) {
|
|
381
|
+
return createBaselineFrame(p1, p2, {
|
|
382
|
+
origin: frameOrigin,
|
|
383
|
+
normal: frameNormal
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
/** Slot holding the constrained point in an array of `count` points. */
|
|
387
|
+
function constrainedIndexIn(count) {
|
|
388
|
+
return variableLength ? count - 1 : constrainedIndex;
|
|
389
|
+
}
|
|
390
|
+
function packSlots(points, baselineA, baselineB, constrained) {
|
|
391
|
+
const out = points.length > 3 ? clonePositions(points) : new Array(3);
|
|
392
|
+
out[baselineIndexA] = baselineA;
|
|
393
|
+
out[baselineIndexB] = baselineB;
|
|
394
|
+
out[constrainedIndexIn(out.length)] = constrained;
|
|
395
|
+
return out;
|
|
396
|
+
}
|
|
397
|
+
function snap(baselineA, baselineB, point) {
|
|
398
|
+
const frame = frameFor(baselineA, baselineB);
|
|
399
|
+
if (!frame) return null;
|
|
400
|
+
return frame.pointAtNormalDistance(frame.signedNormalDistance(point));
|
|
401
|
+
}
|
|
402
|
+
function derive(points) {
|
|
403
|
+
if (points.length < 2) return clonePositions(points);
|
|
404
|
+
if (points.length === 2) {
|
|
405
|
+
const baselineA = clonePosition(points[0]);
|
|
406
|
+
const baselineB = clonePosition(points[1]);
|
|
407
|
+
const frame = frameFor(baselineA, baselineB);
|
|
408
|
+
const derived = frame ? frame.pointAtNormalDistance(frame.length * defaultDistanceRatio) : null;
|
|
409
|
+
return derived ? packSlots([
|
|
410
|
+
baselineA,
|
|
411
|
+
baselineB,
|
|
412
|
+
derived
|
|
413
|
+
], baselineA, baselineB, derived) : [baselineA, baselineB];
|
|
399
414
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
const tipDir = vecNorm(vecSub(ptTip, ptNeck));
|
|
405
|
-
if (vecMag(tipDir) < 1e-6) return null;
|
|
406
|
-
const headLength = vecMag(vecSub(ptTip, ptBase));
|
|
407
|
-
const shaftHalfWidth = headHalfWidth * shaftRatio;
|
|
408
|
-
const perpDir = [-tipDir[1], tipDir[0]];
|
|
409
|
-
const outerLeft = vecAdd(ptBase, vecScale(perpDir, headHalfWidth));
|
|
410
|
-
const outerRight = vecAdd(ptBase, vecScale(perpDir, -headHalfWidth));
|
|
411
|
-
const innerLeft = vecAdd(ptBase, vecScale(perpDir, shaftHalfWidth));
|
|
412
|
-
const innerRight = vecAdd(ptBase, vecScale(perpDir, -shaftHalfWidth));
|
|
413
|
-
const notchDepth = headLength * shaftRatio;
|
|
414
|
-
const safeNotchDepth = Math.min(notchDepth, headLength - EPSILON);
|
|
415
|
-
const innerTip = vecAdd(ptBase, vecScale(tipDir, safeNotchDepth));
|
|
416
|
-
const shaftEndCenter = ptBase;
|
|
417
|
-
const headRing = [
|
|
418
|
-
outerLeft,
|
|
419
|
-
ptTip,
|
|
420
|
-
outerRight,
|
|
421
|
-
innerRight,
|
|
422
|
-
innerTip,
|
|
423
|
-
innerLeft,
|
|
424
|
-
outerLeft
|
|
425
|
-
];
|
|
426
|
-
const fullSpine = [...remainingSpine, shaftEndCenter];
|
|
427
|
-
const { left: shaftLeft, right: shaftRight } = offsetPolylineSides(fullSpine, interpolatePolylineValues(fullSpine, headHalfWidth * rearWidthRatio, shaftHalfWidth), smooth, smoothResolution);
|
|
428
|
-
return {
|
|
429
|
-
shaftCenterline: fullSpine,
|
|
430
|
-
shaftLeft,
|
|
431
|
-
shaftRight,
|
|
432
|
-
headRing,
|
|
433
|
-
ptTip,
|
|
434
|
-
ptNeck,
|
|
435
|
-
ptBase
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
|
-
/**
|
|
439
|
-
* Calculates the relative metrics (longitudinal and lateral) of the width point
|
|
440
|
-
* relative to the Tip-Neck axis.
|
|
441
|
-
*
|
|
442
|
-
* @param pts - Array of positions [Tip, Neck, ..., WidthPoint]
|
|
443
|
-
* @returns { longitudinal: number, lateral: number } | null
|
|
444
|
-
*/
|
|
445
|
-
function calculateMetrics(pts) {
|
|
446
|
-
const tip = pts[0];
|
|
447
|
-
const neck = pts[1];
|
|
448
|
-
const widthPt = pts[pts.length - 1];
|
|
449
|
-
if (!tip || !neck || !widthPt) return null;
|
|
450
|
-
const tipM = project(tip[0], tip[1]);
|
|
451
|
-
const neckM = project(neck[0], neck[1]);
|
|
452
|
-
const widthM = project(widthPt[0], widthPt[1]);
|
|
453
|
-
const tx = tipM[0];
|
|
454
|
-
const ty = tipM[1];
|
|
455
|
-
const nx = neckM[0];
|
|
456
|
-
const ny = neckM[1];
|
|
457
|
-
const wx = widthM[0];
|
|
458
|
-
const wy = widthM[1];
|
|
459
|
-
const dx = tx - nx;
|
|
460
|
-
const dy = ty - ny;
|
|
461
|
-
const len = Math.sqrt(dx * dx + dy * dy);
|
|
462
|
-
if (len < 1e-6) return null;
|
|
463
|
-
const ax = dx / len;
|
|
464
|
-
const ay = dy / len;
|
|
465
|
-
const px = -ay;
|
|
466
|
-
const py = ax;
|
|
467
|
-
const vx = wx - tx;
|
|
468
|
-
const vy = wy - ty;
|
|
469
|
-
return {
|
|
470
|
-
longitudinal: vx * ax + vy * ay,
|
|
471
|
-
lateral: vx * px + vy * py
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
|
-
/**
|
|
475
|
-
* Computes an initial Width Point based on the Tip and Neck positions.
|
|
476
|
-
* It places the point perpendicular to the neck at a default distance relative to the length.
|
|
477
|
-
*
|
|
478
|
-
* @param tip - Tip position (Lon/Lat)
|
|
479
|
-
* @param neck - Neck position (Lon/Lat)
|
|
480
|
-
* @returns Width Point position (Lon/Lat)
|
|
481
|
-
*/
|
|
482
|
-
function computeInitialWidthPoint(tip, neck) {
|
|
483
|
-
const tipM = project(tip[0], tip[1]);
|
|
484
|
-
const neckM = project(neck[0], neck[1]);
|
|
485
|
-
const tx = tipM[0];
|
|
486
|
-
const ty = tipM[1];
|
|
487
|
-
const nx = neckM[0];
|
|
488
|
-
const ny = neckM[1];
|
|
489
|
-
const dx = tx - nx;
|
|
490
|
-
const dy = ty - ny;
|
|
491
|
-
const len = Math.sqrt(dx * dx + dy * dy);
|
|
492
|
-
if (len < 1e-6) return unproject(nx + 100, ny);
|
|
493
|
-
const ax = dx / len;
|
|
494
|
-
const ay = dy / len;
|
|
495
|
-
const widthRatio = .35;
|
|
496
|
-
const recessionRatio = .25;
|
|
497
|
-
const px = -ay;
|
|
498
|
-
const py = ax;
|
|
499
|
-
return unproject(tx - dx * recessionRatio + len * widthRatio * px, ty - dy * recessionRatio + len * widthRatio * py);
|
|
500
|
-
}
|
|
501
|
-
//#endregion
|
|
502
|
-
//#region src/define.ts
|
|
503
|
-
/**
|
|
504
|
-
* Identity helper that infers a definition's literal `Id` and precise generator
|
|
505
|
-
* type `G`, so the registry's `typeof DEFINITIONS` carries each measure's exact
|
|
506
|
-
* options type. Internal authoring API — not part of the published surface.
|
|
507
|
-
*/
|
|
508
|
-
function defineControlMeasure(definition) {
|
|
509
|
-
return definition;
|
|
510
|
-
}
|
|
511
|
-
//#endregion
|
|
512
|
-
//#region src/draw-rules/baseline-frame.ts
|
|
513
|
-
function createBaselineFrame(p1, p2, options = {}) {
|
|
514
|
-
return createProjectedBaselineFrame(project(p1[0], p1[1]), project(p2[0], p2[1]), options);
|
|
515
|
-
}
|
|
516
|
-
function createProjectedBaselineFrame(p1, p2, options = {}) {
|
|
517
|
-
const baseline = vecSub(p2, p1);
|
|
518
|
-
const length = vecMag(baseline);
|
|
519
|
-
if (length < 1e-6) return null;
|
|
520
|
-
const direction = vecNorm(baseline);
|
|
521
|
-
const normalSign = options.normal === "left" ? -1 : 1;
|
|
522
|
-
const normal = vecScale([direction[1], -direction[0]], normalSign);
|
|
523
|
-
const origin = getOrigin(p1, p2, options.origin ?? "midpoint");
|
|
524
|
-
return {
|
|
525
|
-
p1,
|
|
526
|
-
p2,
|
|
527
|
-
origin,
|
|
528
|
-
direction,
|
|
529
|
-
normal,
|
|
530
|
-
length,
|
|
531
|
-
signedNormalDistance(point) {
|
|
532
|
-
return vecDot(vecSub(project(point[0], point[1]), origin), normal);
|
|
533
|
-
},
|
|
534
|
-
pointAtNormalDistance(distance) {
|
|
535
|
-
if (!Number.isFinite(distance)) return null;
|
|
536
|
-
const point = vecAdd(origin, vecScale(normal, distance));
|
|
537
|
-
return unproject(point[0], point[1]);
|
|
538
|
-
}
|
|
539
|
-
};
|
|
540
|
-
}
|
|
541
|
-
function getOrigin(p1, p2, origin) {
|
|
542
|
-
if (origin === "p1") return p1;
|
|
543
|
-
if (origin === "p2") return p2;
|
|
544
|
-
return vecScale(vecAdd(p1, p2), .5);
|
|
545
|
-
}
|
|
546
|
-
//#endregion
|
|
547
|
-
//#region src/draw-rules/util.ts
|
|
548
|
-
function clonePosition(position) {
|
|
549
|
-
return [...position];
|
|
550
|
-
}
|
|
551
|
-
function clonePositions(positions) {
|
|
552
|
-
return positions.map(clonePosition);
|
|
553
|
-
}
|
|
554
|
-
function samePosition(a, b) {
|
|
555
|
-
return a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
|
|
556
|
-
}
|
|
557
|
-
//#endregion
|
|
558
|
-
//#region src/draw-rules/midpoint-perpendicular.ts
|
|
559
|
-
function createMidpointPerpendicularDrawRule(options) {
|
|
560
|
-
const defaultDistanceRatio = options.defaultDistanceRatio ?? 1.5;
|
|
561
|
-
const variableLength = options.variableLength ?? false;
|
|
562
|
-
const constrainedIndex = options.constrainedIndex ?? 2;
|
|
563
|
-
const baselineIndexA = constrainedIndex === 0 ? 1 : 0;
|
|
564
|
-
const baselineIndexB = constrainedIndex === 0 ? 2 : 1;
|
|
565
|
-
const frameOrigin = options.origin ?? "midpoint";
|
|
566
|
-
const frameNormal = options.normal ?? "right";
|
|
567
|
-
function frameFor(p1, p2) {
|
|
568
|
-
return createBaselineFrame(p1, p2, {
|
|
569
|
-
origin: frameOrigin,
|
|
570
|
-
normal: frameNormal
|
|
571
|
-
});
|
|
572
|
-
}
|
|
573
|
-
/** Slot holding the constrained point in an array of `count` points. */
|
|
574
|
-
function constrainedIndexIn(count) {
|
|
575
|
-
return variableLength ? count - 1 : constrainedIndex;
|
|
576
|
-
}
|
|
577
|
-
function packSlots(points, baselineA, baselineB, constrained) {
|
|
578
|
-
const out = points.length > 3 ? clonePositions(points) : new Array(3);
|
|
579
|
-
out[baselineIndexA] = baselineA;
|
|
580
|
-
out[baselineIndexB] = baselineB;
|
|
581
|
-
out[constrainedIndexIn(out.length)] = constrained;
|
|
582
|
-
return out;
|
|
583
|
-
}
|
|
584
|
-
function snap(baselineA, baselineB, point) {
|
|
585
|
-
const frame = frameFor(baselineA, baselineB);
|
|
586
|
-
if (!frame) return null;
|
|
587
|
-
return frame.pointAtNormalDistance(frame.signedNormalDistance(point));
|
|
588
|
-
}
|
|
589
|
-
function derive(points) {
|
|
590
|
-
if (points.length < 2) return clonePositions(points);
|
|
591
|
-
if (points.length === 2) {
|
|
592
|
-
const baselineA = clonePosition(points[0]);
|
|
593
|
-
const baselineB = clonePosition(points[1]);
|
|
594
|
-
const frame = frameFor(baselineA, baselineB);
|
|
595
|
-
const derived = frame ? frame.pointAtNormalDistance(frame.length * defaultDistanceRatio) : null;
|
|
596
|
-
return derived ? packSlots([
|
|
597
|
-
baselineA,
|
|
598
|
-
baselineB,
|
|
599
|
-
derived
|
|
600
|
-
], baselineA, baselineB, derived) : [baselineA, baselineB];
|
|
601
|
-
}
|
|
602
|
-
const baselineA = clonePosition(points[baselineIndexA]);
|
|
603
|
-
const baselineB = clonePosition(points[baselineIndexB]);
|
|
604
|
-
const rawConstrained = points[constrainedIndexIn(points.length)];
|
|
605
|
-
return packSlots(points, baselineA, baselineB, snap(baselineA, baselineB, rawConstrained) ?? clonePosition(rawConstrained));
|
|
415
|
+
const baselineA = clonePosition(points[baselineIndexA]);
|
|
416
|
+
const baselineB = clonePosition(points[baselineIndexB]);
|
|
417
|
+
const rawConstrained = points[constrainedIndexIn(points.length)];
|
|
418
|
+
return packSlots(points, baselineA, baselineB, snap(baselineA, baselineB, rawConstrained) ?? clonePosition(rawConstrained));
|
|
606
419
|
}
|
|
607
420
|
return {
|
|
608
421
|
id: options.id,
|
|
@@ -1558,1177 +1371,495 @@ const supportByFireDrawRule = {
|
|
|
1558
1371
|
}
|
|
1559
1372
|
};
|
|
1560
1373
|
//#endregion
|
|
1561
|
-
//#region src/
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
tip,
|
|
1569
|
-
neck,
|
|
1570
|
-
computeInitialWidthPoint(tip, neck)
|
|
1571
|
-
];
|
|
1572
|
-
}
|
|
1573
|
-
return clonePositions(points);
|
|
1574
|
-
}
|
|
1575
|
-
function projectWidthIntoFrame(next, longitudinal, lateral) {
|
|
1576
|
-
const tip = next[0];
|
|
1577
|
-
const neck = next[1];
|
|
1578
|
-
if (!tip || !neck) return null;
|
|
1579
|
-
const tipM = project(tip[0], tip[1]);
|
|
1580
|
-
const neckM = project(neck[0], neck[1]);
|
|
1581
|
-
const dx = tipM[0] - neckM[0];
|
|
1582
|
-
const dy = tipM[1] - neckM[1];
|
|
1583
|
-
const len = Math.sqrt(dx * dx + dy * dy);
|
|
1584
|
-
if (len < 1e-6) return null;
|
|
1585
|
-
const ax = dx / len;
|
|
1586
|
-
const ay = dy / len;
|
|
1587
|
-
const px = -ay;
|
|
1588
|
-
const py = ax;
|
|
1589
|
-
return unproject(tipM[0] + longitudinal * ax + lateral * px, tipM[1] + longitudinal * ay + lateral * py);
|
|
1590
|
-
}
|
|
1591
|
-
function packWithWidth(next, width) {
|
|
1592
|
-
const out = clonePositions(next);
|
|
1593
|
-
out[out.length - 1] = width;
|
|
1594
|
-
return out;
|
|
1595
|
-
}
|
|
1596
|
-
const axis1DrawRule = {
|
|
1597
|
-
id: "axis1",
|
|
1598
|
-
minimumUserPoints: 2,
|
|
1599
|
-
trailingFixedSlots: 1,
|
|
1600
|
-
derive: derive$2,
|
|
1601
|
-
transform(event) {
|
|
1602
|
-
const { previous, next, activePointIndex } = event;
|
|
1603
|
-
if (next.length < 3 || previous.length < 3) return derive$2(next);
|
|
1604
|
-
if (!(activePointIndex === 0 || activePointIndex === 1)) return clonePositions(next);
|
|
1605
|
-
const metrics = calculateMetrics(previous);
|
|
1606
|
-
if (!metrics) return clonePositions(next);
|
|
1607
|
-
const projected = projectWidthIntoFrame(next, metrics.longitudinal, metrics.lateral);
|
|
1608
|
-
if (!projected) return clonePositions(next);
|
|
1609
|
-
return packWithWidth(next, projected);
|
|
1610
|
-
}
|
|
1611
|
-
};
|
|
1374
|
+
//#region src/attack-utils.ts
|
|
1375
|
+
const DEFAULT_SHAFT_WIDTH_RATIO$1 = .6;
|
|
1376
|
+
const DEFAULT_REAR_WIDTH_RATIO = DEFAULT_SHAFT_WIDTH_RATIO$1;
|
|
1377
|
+
const SHAFT_MIN_RATIO = .1;
|
|
1378
|
+
const SHAFT_MAX_RATIO = .9;
|
|
1379
|
+
const REAR_WIDTH_OPTION_HANDLE_ID = "rear-width";
|
|
1380
|
+
const SHAFT_WIDTH_OPTION_HANDLE_ID = "shaft-width";
|
|
1612
1381
|
/**
|
|
1613
|
-
*
|
|
1614
|
-
* arrow
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
const neckCoordinate = points[1];
|
|
1619
|
-
if (!fireTipCoordinate || !neckCoordinate) return null;
|
|
1620
|
-
const fireTip = project(fireTipCoordinate[0], fireTipCoordinate[1]);
|
|
1621
|
-
const terminal = vecSub(fireTip, project(neckCoordinate[0], neckCoordinate[1]));
|
|
1622
|
-
if (vecMag(terminal) < 1e-6) return null;
|
|
1623
|
-
const bodyTip = vecAdd(fireTip, vecScale(terminal, -.3));
|
|
1624
|
-
return [unproject(bodyTip[0], bodyTip[1]), ...points.slice(1)];
|
|
1625
|
-
}
|
|
1626
|
-
function restoreFireTip(points, fireTip) {
|
|
1627
|
-
if (points.length > 0 && fireTip) points[0] = clonePosition(fireTip);
|
|
1628
|
-
return points;
|
|
1629
|
-
}
|
|
1630
|
-
/**
|
|
1631
|
-
* Axis1 interaction for Counterattack by Fire, with its width frame anchored
|
|
1632
|
-
* to the recessed CATK body tip rather than P1, which belongs to the separate
|
|
1633
|
-
* fire arrow.
|
|
1382
|
+
* Creates the definition-owned rear-/shaft-width handle pair shared by every
|
|
1383
|
+
* variable-width arrow body. Callers supply only how their own geometry maps
|
|
1384
|
+
* onto {@link WidthHandleGeometry}; placement (the two ends of the right edge)
|
|
1385
|
+
* and the drag math (perpendicular distance from the dragged point to the
|
|
1386
|
+
* matching centerline segment, over the reference half-width) live here once.
|
|
1634
1387
|
*/
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
return clonePositions(points);
|
|
1388
|
+
function createWidthOptionHandles(config) {
|
|
1389
|
+
return {
|
|
1390
|
+
get(controlPoints, options) {
|
|
1391
|
+
const geometry = config.geometry(controlPoints, options);
|
|
1392
|
+
const rearRight = geometry?.rightEdge[0];
|
|
1393
|
+
const neckRight = geometry?.rightEdge.at(-1);
|
|
1394
|
+
if (!rearRight || !neckRight) return [];
|
|
1395
|
+
return [{
|
|
1396
|
+
id: REAR_WIDTH_OPTION_HANDLE_ID,
|
|
1397
|
+
position: unproject(rearRight[0], rearRight[1])
|
|
1398
|
+
}, {
|
|
1399
|
+
id: SHAFT_WIDTH_OPTION_HANDLE_ID,
|
|
1400
|
+
position: unproject(neckRight[0], neckRight[1])
|
|
1401
|
+
}];
|
|
1402
|
+
},
|
|
1403
|
+
drag({ controlPoints, options, handleId, position }) {
|
|
1404
|
+
const rear = handleId === REAR_WIDTH_OPTION_HANDLE_ID;
|
|
1405
|
+
if (!rear && !(handleId === "shaft-width")) return void 0;
|
|
1406
|
+
const geometry = config.geometry(controlPoints, options);
|
|
1407
|
+
if (!geometry || geometry.referenceHalfWidth < 1e-6) return void 0;
|
|
1408
|
+
const from = rear ? geometry.spine[0] : geometry.spine.at(-2);
|
|
1409
|
+
const to = rear ? geometry.spine[1] : geometry.spine.at(-1);
|
|
1410
|
+
if (!from || !to) return void 0;
|
|
1411
|
+
const ratio = clamp(pointToInfiniteLineDistance(project(position[0], position[1]), from, to) / geometry.referenceHalfWidth, config.minRatio, rear ? config.maxRearRatio : config.maxShaftRatio);
|
|
1412
|
+
return rear ? { rearWidthRatio: ratio } : { shaftWidthRatio: ratio };
|
|
1413
|
+
}
|
|
1414
|
+
};
|
|
1663
1415
|
}
|
|
1664
|
-
const searchAreaDrawRule = {
|
|
1665
|
-
id: "area21:search-area",
|
|
1666
|
-
minimumUserPoints: 3,
|
|
1667
|
-
minimumPreviewPoints: 2,
|
|
1668
|
-
canonicalPointCount: 3,
|
|
1669
|
-
derive: derive$1,
|
|
1670
|
-
transform(event) {
|
|
1671
|
-
return derive$1(event.next);
|
|
1672
|
-
}
|
|
1673
|
-
};
|
|
1674
|
-
//#endregion
|
|
1675
|
-
//#region src/draw-rules/rectangle.ts
|
|
1676
1416
|
/**
|
|
1677
|
-
*
|
|
1678
|
-
*
|
|
1679
|
-
*
|
|
1417
|
+
* The width handles for attack bodies built by {@link processAttackGeometry},
|
|
1418
|
+
* whose ratios are fractions of the arrowhead half-width. A coordinate resolver
|
|
1419
|
+
* lets derivative measures (Counterattack by Fire) map their authored Axis1
|
|
1420
|
+
* points to the body coordinates first.
|
|
1680
1421
|
*/
|
|
1681
|
-
function
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1422
|
+
function createVariableWidthAttackOptionHandles(resolveCoordinates = (points) => points) {
|
|
1423
|
+
return createWidthOptionHandles({
|
|
1424
|
+
geometry(controlPoints, options) {
|
|
1425
|
+
const coordinates = resolveCoordinates(controlPoints);
|
|
1426
|
+
if (!coordinates) return null;
|
|
1427
|
+
const { geometry } = processAttackGeometry(coordinates, options);
|
|
1428
|
+
const outerLeft = geometry?.headRing[0];
|
|
1429
|
+
if (!geometry || !outerLeft) return null;
|
|
1430
|
+
return {
|
|
1431
|
+
spine: geometry.shaftCenterline,
|
|
1432
|
+
rightEdge: geometry.shaftRight,
|
|
1433
|
+
referenceHalfWidth: vecMag(vecSub(outerLeft, geometry.ptBase))
|
|
1434
|
+
};
|
|
1435
|
+
},
|
|
1436
|
+
minRatio: SHAFT_MIN_RATIO,
|
|
1437
|
+
maxRearRatio: 3,
|
|
1438
|
+
maxShaftRatio: SHAFT_MAX_RATIO
|
|
1693
1439
|
});
|
|
1694
|
-
return [
|
|
1695
|
-
p1,
|
|
1696
|
-
p2,
|
|
1697
|
-
frame?.pointAtNormalDistance(frame.signedNormalDistance(points[2])) ?? clonePosition(points[2])
|
|
1698
|
-
];
|
|
1699
1440
|
}
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
const p4 = vecAdd(p1, vecSub(project(c3[0], c3[1]), p2));
|
|
1709
|
-
return [
|
|
1710
|
-
c1,
|
|
1711
|
-
c2,
|
|
1712
|
-
c3,
|
|
1713
|
-
unproject(p4[0], p4[1]),
|
|
1714
|
-
clonePosition(c1)
|
|
1715
|
-
];
|
|
1441
|
+
const variableWidthAttackOptionHandles = createVariableWidthAttackOptionHandles();
|
|
1442
|
+
/**
|
|
1443
|
+
* The single home of the "rear inherits shaft" rule: an omitted `rearWidthRatio`
|
|
1444
|
+
* leaves the body parallel by matching the effective shaft ratio. Every
|
|
1445
|
+
* variable-width body resolves the option through here so the rule cannot drift.
|
|
1446
|
+
*/
|
|
1447
|
+
function resolveRearWidthRatio(options, shaftRatio) {
|
|
1448
|
+
return options.rearWidthRatio ?? shaftRatio;
|
|
1716
1449
|
}
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1450
|
+
/**
|
|
1451
|
+
* Processes input coordinates and options to generate the core symbol geometry.
|
|
1452
|
+
* This handles validation, default values, projection, and geometry calculation.
|
|
1453
|
+
*/
|
|
1454
|
+
function processAttackGeometry(coordinates, options = {}) {
|
|
1455
|
+
const shaftRatio = clamp(options.shaftWidthRatio ?? .6, SHAFT_MIN_RATIO, SHAFT_MAX_RATIO);
|
|
1456
|
+
const smooth = options.smooth ?? false;
|
|
1457
|
+
const smoothResolution = options.smoothResolution ?? 5;
|
|
1458
|
+
const rearRatio = clamp(resolveRearWidthRatio(options, shaftRatio), SHAFT_MIN_RATIO, 3);
|
|
1459
|
+
const points = coordinates.map((c) => project(c[0], c[1]));
|
|
1460
|
+
const numPoints = points.length;
|
|
1461
|
+
const ptTip = points[0];
|
|
1462
|
+
const ptWidth = points[numPoints - 1];
|
|
1463
|
+
const spinePoints = [];
|
|
1464
|
+
for (let i = numPoints - 2; i >= 1; i--) {
|
|
1465
|
+
const p = points[i];
|
|
1466
|
+
if (p) spinePoints.push(p);
|
|
1467
|
+
}
|
|
1468
|
+
return { geometry: calculateSymbolGeometry(ptTip, ptWidth, spinePoints, shaftRatio, rearRatio, smooth, smoothResolution) };
|
|
1720
1469
|
}
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
if (
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
const p3 = unproject(projectedP3[0], projectedP3[1]);
|
|
1747
|
-
return [
|
|
1748
|
-
clonePosition(previous[0]),
|
|
1749
|
-
p2,
|
|
1750
|
-
p3
|
|
1751
|
-
];
|
|
1752
|
-
}
|
|
1753
|
-
if (activePointIndex === 2) {
|
|
1754
|
-
const p3 = frame.pointAtNormalDistance(frame.signedNormalDistance(next[2]));
|
|
1755
|
-
if (p3) return [
|
|
1756
|
-
clonePosition(previous[0]),
|
|
1757
|
-
clonePosition(previous[1]),
|
|
1758
|
-
p3
|
|
1759
|
-
];
|
|
1470
|
+
function calculateSymbolGeometry(ptTip, ptWidth, spine, shaftRatio, rearWidthRatio, smooth, smoothResolution) {
|
|
1471
|
+
const initialNeck = spine[spine.length - 1];
|
|
1472
|
+
if (!initialNeck) return null;
|
|
1473
|
+
const initialTipDir = vecNorm(vecSub(ptTip, initialNeck));
|
|
1474
|
+
if (vecMag(initialTipDir) < 1e-6) return null;
|
|
1475
|
+
const headHalfWidth = pointToInfiniteLineDistance(ptWidth, initialNeck, ptTip);
|
|
1476
|
+
const vecTipToWidth = vecSub(ptTip, ptWidth);
|
|
1477
|
+
const requestedHeadLength = Math.max(EPSILON, vecDot(vecTipToWidth, initialTipDir));
|
|
1478
|
+
const path = [...spine, ptTip];
|
|
1479
|
+
let totalArcLength = 0;
|
|
1480
|
+
for (let i = 1; i < path.length; i++) totalArcLength += vecMag(vecSub(path[i], path[i - 1]));
|
|
1481
|
+
const minimumShaftLength = Math.max(EPSILON, totalArcLength * .01);
|
|
1482
|
+
let distanceRemaining = clamp(requestedHeadLength, EPSILON, Math.max(EPSILON, totalArcLength - minimumShaftLength));
|
|
1483
|
+
let ptBase = ptTip;
|
|
1484
|
+
let remainingSpine = spine;
|
|
1485
|
+
for (let i = path.length - 1; i > 0; i--) {
|
|
1486
|
+
const segmentEnd = path[i];
|
|
1487
|
+
const segmentStart = path[i - 1];
|
|
1488
|
+
const segment = vecSub(segmentStart, segmentEnd);
|
|
1489
|
+
const segmentLength = vecMag(segment);
|
|
1490
|
+
if (segmentLength < 1e-6) continue;
|
|
1491
|
+
if (distanceRemaining < segmentLength) {
|
|
1492
|
+
ptBase = vecAdd(segmentEnd, vecScale(segment, distanceRemaining / segmentLength));
|
|
1493
|
+
remainingSpine = path.slice(0, i);
|
|
1494
|
+
break;
|
|
1760
1495
|
}
|
|
1761
|
-
|
|
1496
|
+
distanceRemaining -= segmentLength;
|
|
1762
1497
|
}
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
const
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1498
|
+
const ptNeck = remainingSpine[remainingSpine.length - 1];
|
|
1499
|
+
if (!ptNeck) return null;
|
|
1500
|
+
const tipDir = vecNorm(vecSub(ptTip, ptNeck));
|
|
1501
|
+
if (vecMag(tipDir) < 1e-6) return null;
|
|
1502
|
+
const headLength = vecMag(vecSub(ptTip, ptBase));
|
|
1503
|
+
const shaftHalfWidth = headHalfWidth * shaftRatio;
|
|
1504
|
+
const perpDir = [-tipDir[1], tipDir[0]];
|
|
1505
|
+
const outerLeft = vecAdd(ptBase, vecScale(perpDir, headHalfWidth));
|
|
1506
|
+
const outerRight = vecAdd(ptBase, vecScale(perpDir, -headHalfWidth));
|
|
1507
|
+
const innerLeft = vecAdd(ptBase, vecScale(perpDir, shaftHalfWidth));
|
|
1508
|
+
const innerRight = vecAdd(ptBase, vecScale(perpDir, -shaftHalfWidth));
|
|
1509
|
+
const notchDepth = headLength * shaftRatio;
|
|
1510
|
+
const safeNotchDepth = Math.min(notchDepth, headLength - EPSILON);
|
|
1511
|
+
const innerTip = vecAdd(ptBase, vecScale(tipDir, safeNotchDepth));
|
|
1512
|
+
const shaftEndCenter = ptBase;
|
|
1513
|
+
const headRing = [
|
|
1514
|
+
outerLeft,
|
|
1515
|
+
ptTip,
|
|
1516
|
+
outerRight,
|
|
1517
|
+
innerRight,
|
|
1518
|
+
innerTip,
|
|
1519
|
+
innerLeft,
|
|
1520
|
+
outerLeft
|
|
1521
|
+
];
|
|
1522
|
+
const fullSpine = [...remainingSpine, shaftEndCenter];
|
|
1523
|
+
const { left: shaftLeft, right: shaftRight } = offsetPolylineSides(fullSpine, interpolatePolylineValues(fullSpine, headHalfWidth * rearWidthRatio, shaftHalfWidth), smooth, smoothResolution);
|
|
1524
|
+
return {
|
|
1525
|
+
shaftCenterline: fullSpine,
|
|
1526
|
+
shaftLeft,
|
|
1527
|
+
shaftRight,
|
|
1528
|
+
headRing,
|
|
1529
|
+
ptTip,
|
|
1530
|
+
ptNeck,
|
|
1531
|
+
ptBase
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1776
1534
|
/**
|
|
1777
|
-
*
|
|
1778
|
-
*
|
|
1535
|
+
* Calculates the relative metrics (longitudinal and lateral) of the width point
|
|
1536
|
+
* relative to the Tip-Neck axis.
|
|
1779
1537
|
*
|
|
1780
|
-
*
|
|
1781
|
-
*
|
|
1782
|
-
* monocolor (ADR-0023). Every glyph is built from three primitives — vertical
|
|
1783
|
-
* bars, crosses, and dots — plus the team/crew ring and command `++`.
|
|
1538
|
+
* @param pts - Array of positions [Tip, Neck, ..., WidthPoint]
|
|
1539
|
+
* @returns { longitudinal: number, lateral: number } | null
|
|
1784
1540
|
*/
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
},
|
|
1815
|
-
battalion: {
|
|
1816
|
-
kind: "bars",
|
|
1817
|
-
count: 2,
|
|
1818
|
-
label: "Battalion"
|
|
1819
|
-
},
|
|
1820
|
-
regiment: {
|
|
1821
|
-
kind: "bars",
|
|
1822
|
-
count: 3,
|
|
1823
|
-
label: "Regiment"
|
|
1824
|
-
},
|
|
1825
|
-
brigade: {
|
|
1826
|
-
kind: "crosses",
|
|
1827
|
-
count: 1,
|
|
1828
|
-
label: "Brigade"
|
|
1829
|
-
},
|
|
1830
|
-
division: {
|
|
1831
|
-
kind: "crosses",
|
|
1832
|
-
count: 2,
|
|
1833
|
-
label: "Division"
|
|
1834
|
-
},
|
|
1835
|
-
corps: {
|
|
1836
|
-
kind: "crosses",
|
|
1837
|
-
count: 3,
|
|
1838
|
-
label: "Corps"
|
|
1839
|
-
},
|
|
1840
|
-
army: {
|
|
1841
|
-
kind: "crosses",
|
|
1842
|
-
count: 4,
|
|
1843
|
-
label: "Army"
|
|
1844
|
-
},
|
|
1845
|
-
"army-group": {
|
|
1846
|
-
kind: "crosses",
|
|
1847
|
-
count: 5,
|
|
1848
|
-
label: "Army Group"
|
|
1849
|
-
},
|
|
1850
|
-
region: {
|
|
1851
|
-
kind: "crosses",
|
|
1852
|
-
count: 6,
|
|
1853
|
-
label: "Region"
|
|
1854
|
-
},
|
|
1855
|
-
command: {
|
|
1856
|
-
kind: "command",
|
|
1857
|
-
count: 2,
|
|
1858
|
-
label: "Command"
|
|
1859
|
-
}
|
|
1860
|
-
};
|
|
1861
|
-
/** Symbol-form aliases (`"XX"`, `"II"`, `"•••"`, …) → canonical echelon key. */
|
|
1862
|
-
const ECHELON_ALIASES = {
|
|
1863
|
-
ø: "team",
|
|
1864
|
-
"•": "squad",
|
|
1865
|
-
"••": "section",
|
|
1866
|
-
"•••": "platoon",
|
|
1867
|
-
i: "company",
|
|
1868
|
-
ii: "battalion",
|
|
1869
|
-
iii: "regiment",
|
|
1870
|
-
x: "brigade",
|
|
1871
|
-
xx: "division",
|
|
1872
|
-
xxx: "corps",
|
|
1873
|
-
xxxx: "army",
|
|
1874
|
-
xxxxx: "army-group",
|
|
1875
|
-
xxxxxx: "region",
|
|
1876
|
-
"++": "command"
|
|
1877
|
-
};
|
|
1878
|
-
/** Enum options for the params panel, derived from {@link ECHELONS}. */
|
|
1879
|
-
const ECHELON_PARAM_OPTIONS = [{
|
|
1880
|
-
label: "None",
|
|
1881
|
-
value: "none"
|
|
1882
|
-
}, ...Object.entries(ECHELONS).filter(([, spec]) => spec.label !== void 0).map(([value, spec]) => ({
|
|
1883
|
-
label: spec.label,
|
|
1884
|
-
value
|
|
1885
|
-
}))];
|
|
1541
|
+
function calculateMetrics(pts) {
|
|
1542
|
+
const tip = pts[0];
|
|
1543
|
+
const neck = pts[1];
|
|
1544
|
+
const widthPt = pts[pts.length - 1];
|
|
1545
|
+
if (!tip || !neck || !widthPt) return null;
|
|
1546
|
+
const tipM = project(tip[0], tip[1]);
|
|
1547
|
+
const neckM = project(neck[0], neck[1]);
|
|
1548
|
+
const widthM = project(widthPt[0], widthPt[1]);
|
|
1549
|
+
const tx = tipM[0];
|
|
1550
|
+
const ty = tipM[1];
|
|
1551
|
+
const nx = neckM[0];
|
|
1552
|
+
const ny = neckM[1];
|
|
1553
|
+
const wx = widthM[0];
|
|
1554
|
+
const wy = widthM[1];
|
|
1555
|
+
const dx = tx - nx;
|
|
1556
|
+
const dy = ty - ny;
|
|
1557
|
+
const len = Math.sqrt(dx * dx + dy * dy);
|
|
1558
|
+
if (len < 1e-6) return null;
|
|
1559
|
+
const ax = dx / len;
|
|
1560
|
+
const ay = dy / len;
|
|
1561
|
+
const px = -ay;
|
|
1562
|
+
const py = ax;
|
|
1563
|
+
const vx = wx - tx;
|
|
1564
|
+
const vy = wy - ty;
|
|
1565
|
+
return {
|
|
1566
|
+
longitudinal: vx * ax + vy * ay,
|
|
1567
|
+
lateral: vx * px + vy * py
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1886
1570
|
/**
|
|
1887
|
-
*
|
|
1888
|
-
*
|
|
1889
|
-
*
|
|
1571
|
+
* Computes an initial Width Point based on the Tip and Neck positions.
|
|
1572
|
+
* It places the point perpendicular to the neck at a default distance relative to the length.
|
|
1573
|
+
*
|
|
1574
|
+
* @param tip - Tip position (Lon/Lat)
|
|
1575
|
+
* @param neck - Neck position (Lon/Lat)
|
|
1576
|
+
* @returns Width Point position (Lon/Lat)
|
|
1890
1577
|
*/
|
|
1891
|
-
function
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1578
|
+
function computeInitialWidthPoint(tip, neck) {
|
|
1579
|
+
const tipM = project(tip[0], tip[1]);
|
|
1580
|
+
const neckM = project(neck[0], neck[1]);
|
|
1581
|
+
const tx = tipM[0];
|
|
1582
|
+
const ty = tipM[1];
|
|
1583
|
+
const nx = neckM[0];
|
|
1584
|
+
const ny = neckM[1];
|
|
1585
|
+
const dx = tx - nx;
|
|
1586
|
+
const dy = ty - ny;
|
|
1587
|
+
const len = Math.sqrt(dx * dx + dy * dy);
|
|
1588
|
+
if (len < 1e-6) return unproject(nx + 100, ny);
|
|
1589
|
+
const ax = dx / len;
|
|
1590
|
+
const ay = dy / len;
|
|
1591
|
+
const widthRatio = .35;
|
|
1592
|
+
const recessionRatio = .25;
|
|
1593
|
+
const px = -ay;
|
|
1594
|
+
const py = ax;
|
|
1595
|
+
return unproject(tx - dx * recessionRatio + len * widthRatio * px, ty - dy * recessionRatio + len * widthRatio * py);
|
|
1895
1596
|
}
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1597
|
+
//#endregion
|
|
1598
|
+
//#region src/draw-rules/axis1.ts
|
|
1599
|
+
function derive$2(points) {
|
|
1600
|
+
if (points.length < 2) return clonePositions(points);
|
|
1601
|
+
if (points.length === 2) {
|
|
1602
|
+
const tip = clonePosition(points[0]);
|
|
1603
|
+
const neck = clonePosition(points[1]);
|
|
1604
|
+
return [
|
|
1605
|
+
tip,
|
|
1606
|
+
neck,
|
|
1607
|
+
computeInitialWidthPoint(tip, neck)
|
|
1608
|
+
];
|
|
1609
|
+
}
|
|
1610
|
+
return clonePositions(points);
|
|
1901
1611
|
}
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
const
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
const
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
width: total
|
|
1938
|
-
};
|
|
1939
|
-
}
|
|
1940
|
-
case "crosses": {
|
|
1941
|
-
const w = h;
|
|
1942
|
-
const spacing = h * 1.4;
|
|
1943
|
-
const total = (spec.count - 1) * spacing;
|
|
1944
|
-
for (let j = 0; j < spec.count; j++) {
|
|
1945
|
-
const a0 = -total / 2 + j * spacing;
|
|
1946
|
-
strokes.push([P(a0 - w / 2, -half), P(a0 + w / 2, half)]);
|
|
1947
|
-
strokes.push([P(a0 - w / 2, half), P(a0 + w / 2, -half)]);
|
|
1948
|
-
}
|
|
1949
|
-
return {
|
|
1950
|
-
strokes,
|
|
1951
|
-
fills,
|
|
1952
|
-
width: total + w
|
|
1953
|
-
};
|
|
1954
|
-
}
|
|
1955
|
-
case "dots": {
|
|
1956
|
-
const r = h * .3;
|
|
1957
|
-
const spacing = h * 1.2;
|
|
1958
|
-
const total = (spec.count - 1) * spacing;
|
|
1959
|
-
for (let j = 0; j < spec.count; j++) fills.push(ring(-total / 2 + j * spacing, r));
|
|
1960
|
-
return {
|
|
1961
|
-
strokes,
|
|
1962
|
-
fills,
|
|
1963
|
-
width: total + 2 * r
|
|
1964
|
-
};
|
|
1965
|
-
}
|
|
1966
|
-
case "ring": {
|
|
1967
|
-
const r = h * .6;
|
|
1968
|
-
strokes.push(ring(0, r));
|
|
1969
|
-
strokes.push([P(-h * .8, -h * .4), P(h * .8, h * .4)]);
|
|
1970
|
-
return {
|
|
1971
|
-
strokes,
|
|
1972
|
-
fills,
|
|
1973
|
-
width: h * 1.6
|
|
1974
|
-
};
|
|
1975
|
-
}
|
|
1976
|
-
case "command": {
|
|
1977
|
-
const arm = h * .5;
|
|
1978
|
-
const spacing = h * 1.4;
|
|
1979
|
-
const total = (spec.count - 1) * spacing;
|
|
1980
|
-
for (let j = 0; j < spec.count; j++) {
|
|
1981
|
-
const a0 = -total / 2 + j * spacing;
|
|
1982
|
-
strokes.push([P(a0 - arm, 0), P(a0 + arm, 0)]);
|
|
1983
|
-
strokes.push([P(a0, -half), P(a0, half)]);
|
|
1984
|
-
}
|
|
1985
|
-
return {
|
|
1986
|
-
strokes,
|
|
1987
|
-
fills,
|
|
1988
|
-
width: total + 2 * arm
|
|
1989
|
-
};
|
|
1990
|
-
}
|
|
1612
|
+
function projectWidthIntoFrame(next, longitudinal, lateral) {
|
|
1613
|
+
const tip = next[0];
|
|
1614
|
+
const neck = next[1];
|
|
1615
|
+
if (!tip || !neck) return null;
|
|
1616
|
+
const tipM = project(tip[0], tip[1]);
|
|
1617
|
+
const neckM = project(neck[0], neck[1]);
|
|
1618
|
+
const dx = tipM[0] - neckM[0];
|
|
1619
|
+
const dy = tipM[1] - neckM[1];
|
|
1620
|
+
const len = Math.sqrt(dx * dx + dy * dy);
|
|
1621
|
+
if (len < 1e-6) return null;
|
|
1622
|
+
const ax = dx / len;
|
|
1623
|
+
const ay = dy / len;
|
|
1624
|
+
const px = -ay;
|
|
1625
|
+
const py = ax;
|
|
1626
|
+
return unproject(tipM[0] + longitudinal * ax + lateral * px, tipM[1] + longitudinal * ay + lateral * py);
|
|
1627
|
+
}
|
|
1628
|
+
function packWithWidth(next, width) {
|
|
1629
|
+
const out = clonePositions(next);
|
|
1630
|
+
out[out.length - 1] = width;
|
|
1631
|
+
return out;
|
|
1632
|
+
}
|
|
1633
|
+
const axis1DrawRule = {
|
|
1634
|
+
id: "axis1",
|
|
1635
|
+
minimumUserPoints: 2,
|
|
1636
|
+
trailingFixedSlots: 1,
|
|
1637
|
+
derive: derive$2,
|
|
1638
|
+
transform(event) {
|
|
1639
|
+
const { previous, next, activePointIndex } = event;
|
|
1640
|
+
if (next.length < 3 || previous.length < 3) return derive$2(next);
|
|
1641
|
+
if (!(activePointIndex === 0 || activePointIndex === 1)) return clonePositions(next);
|
|
1642
|
+
const metrics = calculateMetrics(previous);
|
|
1643
|
+
if (!metrics) return clonePositions(next);
|
|
1644
|
+
const projected = projectWidthIntoFrame(next, metrics.longitudinal, metrics.lateral);
|
|
1645
|
+
if (!projected) return clonePositions(next);
|
|
1646
|
+
return packWithWidth(next, projected);
|
|
1991
1647
|
}
|
|
1648
|
+
};
|
|
1649
|
+
/**
|
|
1650
|
+
* Recesses the CATK body from P1. The original P1 remains the tip of the fire
|
|
1651
|
+
* arrow, while P2 and the remaining Axis1 points continue to shape the body.
|
|
1652
|
+
*/
|
|
1653
|
+
function counterattackByFireBodyCoordinates(points) {
|
|
1654
|
+
const fireTipCoordinate = points[0];
|
|
1655
|
+
const neckCoordinate = points[1];
|
|
1656
|
+
if (!fireTipCoordinate || !neckCoordinate) return null;
|
|
1657
|
+
const fireTip = project(fireTipCoordinate[0], fireTipCoordinate[1]);
|
|
1658
|
+
const terminal = vecSub(fireTip, project(neckCoordinate[0], neckCoordinate[1]));
|
|
1659
|
+
if (vecMag(terminal) < 1e-6) return null;
|
|
1660
|
+
const bodyTip = vecAdd(fireTip, vecScale(terminal, -.3));
|
|
1661
|
+
return [unproject(bodyTip[0], bodyTip[1]), ...points.slice(1)];
|
|
1992
1662
|
}
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
visibleWhen: (opts) => Boolean(opts.smooth)
|
|
2010
|
-
}];
|
|
2011
|
-
const ATTACK_SHAFT_PARAMS = [
|
|
2012
|
-
{
|
|
2013
|
-
key: "shaftWidthRatio",
|
|
2014
|
-
presentationTier: "advanced",
|
|
2015
|
-
label: "Shaft width",
|
|
2016
|
-
description: "Arrow shaft width as a ratio of the arrowhead width",
|
|
2017
|
-
type: "number",
|
|
2018
|
-
min: .1,
|
|
2019
|
-
max: 1,
|
|
2020
|
-
step: .05
|
|
2021
|
-
},
|
|
2022
|
-
{
|
|
2023
|
-
key: "rearWidthRatio",
|
|
2024
|
-
presentationTier: "advanced",
|
|
2025
|
-
label: "Rear width",
|
|
2026
|
-
description: "Width at the rear of the shaft as a ratio of the arrowhead width.",
|
|
2027
|
-
type: "number",
|
|
2028
|
-
min: .1,
|
|
2029
|
-
max: 2,
|
|
2030
|
-
step: .05
|
|
2031
|
-
},
|
|
2032
|
-
{
|
|
2033
|
-
key: "smooth",
|
|
2034
|
-
label: "Smooth",
|
|
2035
|
-
description: "Round the corners where the shaft changes direction.",
|
|
2036
|
-
type: "boolean"
|
|
1663
|
+
function restoreFireTip(points, fireTip) {
|
|
1664
|
+
if (points.length > 0 && fireTip) points[0] = clonePosition(fireTip);
|
|
1665
|
+
return points;
|
|
1666
|
+
}
|
|
1667
|
+
/**
|
|
1668
|
+
* Axis1 interaction for Counterattack by Fire, with its width frame anchored
|
|
1669
|
+
* to the recessed CATK body tip rather than P1, which belongs to the separate
|
|
1670
|
+
* fire arrow.
|
|
1671
|
+
*/
|
|
1672
|
+
const counterattackByFireDrawRule = {
|
|
1673
|
+
...axis1DrawRule,
|
|
1674
|
+
id: "axis1:counterattack-by-fire",
|
|
1675
|
+
derive(points) {
|
|
1676
|
+
const bodyPoints = counterattackByFireBodyCoordinates(points);
|
|
1677
|
+
if (!bodyPoints) return axis1DrawRule.derive(points);
|
|
1678
|
+
return restoreFireTip(axis1DrawRule.derive(bodyPoints), points[0]);
|
|
2037
1679
|
},
|
|
2038
|
-
{
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
visibleWhen: (opts) => opts.smooth === true
|
|
1680
|
+
transform(event) {
|
|
1681
|
+
const previousBody = counterattackByFireBodyCoordinates(event.previous);
|
|
1682
|
+
const nextBody = counterattackByFireBodyCoordinates(event.next);
|
|
1683
|
+
if (!previousBody || !nextBody) return axis1DrawRule.transform(event);
|
|
1684
|
+
return restoreFireTip(axis1DrawRule.transform({
|
|
1685
|
+
...event,
|
|
1686
|
+
previous: previousBody,
|
|
1687
|
+
next: nextBody
|
|
1688
|
+
}), event.next[0]);
|
|
2048
1689
|
}
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
max: 1,
|
|
2069
|
-
step: .01
|
|
2070
|
-
},
|
|
2071
|
-
{
|
|
2072
|
-
key: "rearLineAngle",
|
|
2073
|
-
presentationTier: "advanced",
|
|
2074
|
-
label: "Rear line angle",
|
|
2075
|
-
description: "Angle of the rear flare lines, in degrees",
|
|
2076
|
-
type: "number",
|
|
2077
|
-
min: 0,
|
|
2078
|
-
max: 90,
|
|
2079
|
-
step: 1
|
|
1690
|
+
};
|
|
1691
|
+
//#endregion
|
|
1692
|
+
//#region src/draw-rules/area21.ts
|
|
1693
|
+
function derive$1(points) {
|
|
1694
|
+
if (points.length === 2) return clonePositions([
|
|
1695
|
+
points[0],
|
|
1696
|
+
points[1],
|
|
1697
|
+
points[1]
|
|
1698
|
+
]);
|
|
1699
|
+
return clonePositions(points);
|
|
1700
|
+
}
|
|
1701
|
+
const searchAreaDrawRule = {
|
|
1702
|
+
id: "area21:search-area",
|
|
1703
|
+
minimumUserPoints: 3,
|
|
1704
|
+
minimumPreviewPoints: 2,
|
|
1705
|
+
canonicalPointCount: 3,
|
|
1706
|
+
derive: derive$1,
|
|
1707
|
+
transform(event) {
|
|
1708
|
+
return derive$1(event.next);
|
|
2080
1709
|
}
|
|
2081
|
-
];
|
|
2082
|
-
const FORTIFIED_AREA_SIZE_PARAMS = [{
|
|
2083
|
-
key: "toothSizePixels",
|
|
2084
|
-
label: "Tooth size",
|
|
2085
|
-
description: "Size of the fortified-area teeth, in pixels",
|
|
2086
|
-
type: "number",
|
|
2087
|
-
min: 5,
|
|
2088
|
-
max: 50,
|
|
2089
|
-
step: 1,
|
|
2090
|
-
unit: "px"
|
|
2091
|
-
}, {
|
|
2092
|
-
key: "toothSize",
|
|
2093
|
-
label: "Tooth size",
|
|
2094
|
-
description: "Size of the fortified-area teeth, in meters",
|
|
2095
|
-
type: "number",
|
|
2096
|
-
min: 1,
|
|
2097
|
-
max: 500,
|
|
2098
|
-
step: 1,
|
|
2099
|
-
unit: "m"
|
|
2100
|
-
}];
|
|
2101
|
-
const ENCIRCLEMENT_PARAMS = [
|
|
2102
|
-
{
|
|
2103
|
-
key: "smooth",
|
|
2104
|
-
label: "Smooth",
|
|
2105
|
-
description: "Round the boundary by curving it through the control points.",
|
|
2106
|
-
type: "boolean"
|
|
2107
|
-
},
|
|
2108
|
-
{
|
|
2109
|
-
key: "smoothResolution",
|
|
2110
|
-
presentationTier: "advanced",
|
|
2111
|
-
label: "Smooth resolution",
|
|
2112
|
-
description: "Number of samples per segment when smooth mode is enabled.",
|
|
2113
|
-
type: "number",
|
|
2114
|
-
min: 2,
|
|
2115
|
-
max: 64,
|
|
2116
|
-
step: 1,
|
|
2117
|
-
visibleWhen: (opts) => Boolean(opts.smooth)
|
|
2118
|
-
},
|
|
2119
|
-
{
|
|
2120
|
-
key: "barbSpacingRatio",
|
|
2121
|
-
presentationTier: "advanced",
|
|
2122
|
-
label: "Spacing",
|
|
2123
|
-
description: "Spacing between barbs as a ratio of the symbol radius (smaller is denser).",
|
|
2124
|
-
type: "number",
|
|
2125
|
-
min: .15,
|
|
2126
|
-
max: 1.5,
|
|
2127
|
-
step: .05
|
|
2128
|
-
},
|
|
2129
|
-
{
|
|
2130
|
-
key: "barbLengthRatio",
|
|
2131
|
-
presentationTier: "advanced",
|
|
2132
|
-
label: "Barb length",
|
|
2133
|
-
description: "Barb height as a ratio of the symbol radius (amplitude).",
|
|
2134
|
-
type: "number",
|
|
2135
|
-
min: .02,
|
|
2136
|
-
max: .5,
|
|
2137
|
-
step: .01
|
|
2138
|
-
}
|
|
2139
|
-
];
|
|
2140
|
-
const FORTIFIED_AREA_PARAMS = [...AREA_SMOOTH_PARAMS, ...FORTIFIED_AREA_SIZE_PARAMS];
|
|
2141
|
-
const AREA_LABEL_PADDING_PARAM = {
|
|
2142
|
-
key: "labelPadding",
|
|
2143
|
-
presentationTier: "advanced",
|
|
2144
|
-
label: "Label padding",
|
|
2145
|
-
description: "Extra spacing between or around the area's labels, as a ratio of label height",
|
|
2146
|
-
type: "number",
|
|
2147
|
-
min: 0,
|
|
2148
|
-
max: 2,
|
|
2149
|
-
step: .05
|
|
2150
|
-
};
|
|
2151
|
-
const MULTILINE_AREA_LABEL_PARAM = {
|
|
2152
|
-
key: "multilineLabel",
|
|
2153
|
-
label: "Multiline label",
|
|
2154
|
-
description: "Place the doctrinal label above Field T on separate centered lines",
|
|
2155
|
-
type: "boolean"
|
|
2156
|
-
};
|
|
2157
|
-
const LABELED_AREA_PARAMS = [...AREA_SMOOTH_PARAMS, AREA_LABEL_PADDING_PARAM];
|
|
2158
|
-
const PREFIXED_LABELED_AREA_PARAMS = [
|
|
2159
|
-
...AREA_SMOOTH_PARAMS,
|
|
2160
|
-
MULTILINE_AREA_LABEL_PARAM,
|
|
2161
|
-
AREA_LABEL_PADDING_PARAM
|
|
2162
|
-
];
|
|
2163
|
-
const ECHELON_GLYPH_PARAMS = [
|
|
2164
|
-
{
|
|
2165
|
-
key: "echelon",
|
|
2166
|
-
label: "Echelon",
|
|
2167
|
-
description: "Field B unit-size amplifier, drawn on the boundary.",
|
|
2168
|
-
type: "enum",
|
|
2169
|
-
presentationTier: "primary",
|
|
2170
|
-
semanticRole: "doctrinal",
|
|
2171
|
-
options: ECHELON_PARAM_OPTIONS
|
|
2172
|
-
},
|
|
2173
|
-
{
|
|
2174
|
-
key: "echelonSizePixels",
|
|
2175
|
-
label: "Echelon size",
|
|
2176
|
-
description: "Echelon glyph height in screen pixels.",
|
|
2177
|
-
type: "number",
|
|
2178
|
-
min: 8,
|
|
2179
|
-
max: 48,
|
|
2180
|
-
step: 1,
|
|
2181
|
-
unit: "px"
|
|
2182
|
-
},
|
|
2183
|
-
{
|
|
2184
|
-
key: "echelonSize",
|
|
2185
|
-
label: "Echelon size",
|
|
2186
|
-
description: "Echelon glyph height in meters.",
|
|
2187
|
-
type: "number",
|
|
2188
|
-
min: 50,
|
|
2189
|
-
max: 2e4,
|
|
2190
|
-
step: 50,
|
|
2191
|
-
unit: "m"
|
|
2192
|
-
},
|
|
2193
|
-
{
|
|
2194
|
-
key: "echelonPadding",
|
|
2195
|
-
presentationTier: "advanced",
|
|
2196
|
-
label: "Echelon padding",
|
|
2197
|
-
description: "Gap on each side of the echelon, as a ratio of its height.",
|
|
2198
|
-
type: "number",
|
|
2199
|
-
min: 0,
|
|
2200
|
-
max: 2,
|
|
2201
|
-
step: .05
|
|
2202
|
-
},
|
|
2203
|
-
{
|
|
2204
|
-
key: "echelonPosition",
|
|
2205
|
-
presentationTier: "advanced",
|
|
2206
|
-
label: "Echelon position",
|
|
2207
|
-
description: "Slide the echelon along the boundary, as a fraction of the perimeter from the bottom edge.",
|
|
2208
|
-
type: "number",
|
|
2209
|
-
min: -.5,
|
|
2210
|
-
max: .5,
|
|
2211
|
-
step: .01
|
|
2212
|
-
}
|
|
2213
|
-
];
|
|
2214
|
-
const ECHELON_AREA_PARAMS = [
|
|
2215
|
-
...AREA_SMOOTH_PARAMS,
|
|
2216
|
-
...ECHELON_GLYPH_PARAMS,
|
|
2217
|
-
AREA_LABEL_PADDING_PARAM
|
|
2218
|
-
];
|
|
2219
|
-
const PREFIXED_ECHELON_AREA_PARAMS = [
|
|
2220
|
-
...AREA_SMOOTH_PARAMS,
|
|
2221
|
-
MULTILINE_AREA_LABEL_PARAM,
|
|
2222
|
-
...ECHELON_GLYPH_PARAMS,
|
|
2223
|
-
AREA_LABEL_PADDING_PARAM
|
|
2224
|
-
];
|
|
2225
|
-
const AREA_DESIGNATION_AMPLIFIER = {
|
|
2226
|
-
key: "T",
|
|
2227
|
-
label: "Unique designation",
|
|
2228
|
-
description: "Field T — the unit's designation, centered in the area.",
|
|
2229
|
-
maxLength: 21
|
|
2230
|
-
};
|
|
2231
|
-
const AREA_HOSTILE_AMPLIFIER = {
|
|
2232
|
-
key: "N",
|
|
2233
|
-
label: "Hostile (ENY) marker",
|
|
2234
|
-
description: "Field N — literal \"ENY\" marker at the area's west and east edges.",
|
|
2235
|
-
placeholder: "ENY",
|
|
2236
|
-
maxLength: 3
|
|
2237
1710
|
};
|
|
2238
|
-
const AREA_TEXT_AMPLIFIERS = [
|
|
2239
|
-
{
|
|
2240
|
-
key: "T",
|
|
2241
|
-
label: "Unique designation",
|
|
2242
|
-
description: "Field T — the area's unique designation.",
|
|
2243
|
-
maxLength: 21
|
|
2244
|
-
},
|
|
2245
|
-
{
|
|
2246
|
-
key: "H",
|
|
2247
|
-
label: "Additional information",
|
|
2248
|
-
description: "Field H — additional free-text information, shown below the designation.",
|
|
2249
|
-
maxLength: 20
|
|
2250
|
-
},
|
|
2251
|
-
{
|
|
2252
|
-
key: "W",
|
|
2253
|
-
label: "Effective from (DTG)",
|
|
2254
|
-
description: "Field W — date-time group the area becomes effective.",
|
|
2255
|
-
placeholder: "051030Z",
|
|
2256
|
-
maxLength: 16
|
|
2257
|
-
},
|
|
2258
|
-
{
|
|
2259
|
-
key: "W1",
|
|
2260
|
-
label: "Effective to (DTG)",
|
|
2261
|
-
description: "Field W1 — date-time group the area ceases to be effective.",
|
|
2262
|
-
maxLength: 16
|
|
2263
|
-
},
|
|
2264
|
-
AREA_HOSTILE_AMPLIFIER
|
|
2265
|
-
];
|
|
2266
|
-
const AREA_UNIT_DESIGNATION_HOSTILE_AMPLIFIERS = [AREA_DESIGNATION_AMPLIFIER, AREA_HOSTILE_AMPLIFIER];
|
|
2267
|
-
/** {@link AREA_TEXT_AMPLIFIERS} without the H (additional information) field,
|
|
2268
|
-
* for measures whose template omits that row but keeps the W/W1 window and
|
|
2269
|
-
* the N (hostile) marker (Submarine Action Area, Submarine-Generated Action
|
|
2270
|
-
* Area). */
|
|
2271
|
-
const AREA_TEXT_AMPLIFIERS_NO_ADDITIONAL_INFO = AREA_TEXT_AMPLIFIERS.filter((d) => d.key !== "H");
|
|
2272
|
-
/** {@link AREA_TEXT_AMPLIFIERS} reduced to just the designation (Field T) and
|
|
2273
|
-
* hostile marker (Field N), for measures whose template carries no
|
|
2274
|
-
* additional-information or date-time-group rows (Area, Drop Zone,
|
|
2275
|
-
* Extraction Zone). */
|
|
2276
|
-
const AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE = AREA_TEXT_AMPLIFIERS.filter((d) => d.key === "T" || d.key === "N");
|
|
2277
1711
|
//#endregion
|
|
2278
|
-
//#region src/
|
|
2279
|
-
const DEFAULT_AIRBORNE_ATTACK_OPTIONS = {
|
|
2280
|
-
shaftWidthRatio: DEFAULT_SHAFT_WIDTH_RATIO$1,
|
|
2281
|
-
rearWidthRatio: DEFAULT_REAR_WIDTH_RATIO,
|
|
2282
|
-
smooth: false,
|
|
2283
|
-
smoothResolution: 5
|
|
2284
|
-
};
|
|
2285
|
-
const AIRBORNE_ATTACK_METADATA = {
|
|
2286
|
-
id: "airborne-attack",
|
|
2287
|
-
name: "Airborne Attack",
|
|
2288
|
-
description: "Airborne or aviation axis of advance.",
|
|
2289
|
-
entity: "Maneuver Areas",
|
|
2290
|
-
entityType: "Axis of Advance",
|
|
2291
|
-
entitySubtype: "Airborne Attack",
|
|
2292
|
-
value: "151401",
|
|
2293
|
-
minCoordinates: 3,
|
|
2294
|
-
geometry: "line",
|
|
2295
|
-
geometryTypes: ["LineString"],
|
|
2296
|
-
paints: {
|
|
2297
|
-
stroke: true,
|
|
2298
|
-
fill: "none",
|
|
2299
|
-
text: false
|
|
2300
|
-
},
|
|
2301
|
-
drawRule: "Axis1",
|
|
2302
|
-
params: ATTACK_SHAFT_PARAMS
|
|
2303
|
-
};
|
|
1712
|
+
//#region src/draw-rules/rectangle.ts
|
|
2304
1713
|
/**
|
|
2305
|
-
*
|
|
2306
|
-
*
|
|
2307
|
-
* The
|
|
2308
|
-
* between the shaft and the head (Points 1 and 2/Neck).
|
|
2309
|
-
*
|
|
2310
|
-
* @param coordinates - An array of GeoJSON Positions.
|
|
2311
|
-
* @param options - Configuration options.
|
|
2312
|
-
* @returns A GeoJSON FeatureCollection<LineString>.
|
|
1714
|
+
* Oriented rectangle: P1 and P2 are adjacent corners and define the first
|
|
1715
|
+
* edge. P3 is the next corner, constrained to the perpendicular through P2.
|
|
1716
|
+
* The generator derives the fourth corner.
|
|
2313
1717
|
*/
|
|
2314
|
-
function
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
geometry.headRing[2],
|
|
2333
|
-
geometry.headRing[1],
|
|
2334
|
-
geometry.headRing[0],
|
|
2335
|
-
geometry.headRing[5],
|
|
2336
|
-
...[...shaftRightToNeck].reverse()
|
|
2337
|
-
].map((p) => unproject(p[0], p[1]))
|
|
2338
|
-
}
|
|
2339
|
-
}]
|
|
2340
|
-
};
|
|
2341
|
-
}
|
|
2342
|
-
const AIRBORNE_ATTACK = defineControlMeasure({
|
|
2343
|
-
metadata: AIRBORNE_ATTACK_METADATA,
|
|
2344
|
-
generator: createAirborneAttack,
|
|
2345
|
-
defaultOptions: DEFAULT_AIRBORNE_ATTACK_OPTIONS,
|
|
2346
|
-
rule: axis1DrawRule,
|
|
2347
|
-
optionHandles: variableWidthAttackOptionHandles
|
|
2348
|
-
});
|
|
2349
|
-
/**
|
|
2350
|
-
* Reacts to an **input-contract** violation according to `mode`: `throw`
|
|
2351
|
-
* raises, `warn` logs, `silent` (the default) does neither. Always returns
|
|
2352
|
-
* `false` so a caller can `return reportValidationIssue(...)` from a guard.
|
|
2353
|
-
*
|
|
2354
|
-
* Governs the *input contract* only (control-point count and finiteness),
|
|
2355
|
-
* checked once at the render seam — not geometric degeneracy, which always
|
|
2356
|
-
* yields an empty render silently. See ADR-0014 and the `CONTEXT.md` terms.
|
|
2357
|
-
*/
|
|
2358
|
-
function reportValidationIssue(mode, message) {
|
|
2359
|
-
const resolved = mode ?? "silent";
|
|
2360
|
-
if (resolved === "throw") throw new Error(message);
|
|
2361
|
-
if (resolved === "warn") console.warn(message);
|
|
2362
|
-
return false;
|
|
1718
|
+
function derive(points) {
|
|
1719
|
+
if (points.length < 2) return clonePositions(points);
|
|
1720
|
+
const p1 = clonePosition(points[0]);
|
|
1721
|
+
const p2 = clonePosition(points[1]);
|
|
1722
|
+
if (points.length === 2) return [
|
|
1723
|
+
p1,
|
|
1724
|
+
p2,
|
|
1725
|
+
clonePosition(p2)
|
|
1726
|
+
];
|
|
1727
|
+
const frame = createBaselineFrame(p1, p2, {
|
|
1728
|
+
origin: "p2",
|
|
1729
|
+
normal: "right"
|
|
1730
|
+
});
|
|
1731
|
+
return [
|
|
1732
|
+
p1,
|
|
1733
|
+
p2,
|
|
1734
|
+
frame?.pointAtNormalDistance(frame.signedNormalDistance(points[2])) ?? clonePosition(points[2])
|
|
1735
|
+
];
|
|
2363
1736
|
}
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
1737
|
+
function guidePoints(points) {
|
|
1738
|
+
const canonical = derive(points);
|
|
1739
|
+
if (canonical.length < 3) return canonical;
|
|
1740
|
+
const [c1, c2, c3] = canonical;
|
|
1741
|
+
if (!c1 || !c2 || !c3) return canonical;
|
|
1742
|
+
if (c2[0] === c3[0] && c2[1] === c3[1]) return [c1, c2];
|
|
1743
|
+
const p1 = project(c1[0], c1[1]);
|
|
1744
|
+
const p2 = project(c2[0], c2[1]);
|
|
1745
|
+
const p4 = vecAdd(p1, vecSub(project(c3[0], c3[1]), p2));
|
|
2372
1746
|
return [
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
1747
|
+
c1,
|
|
1748
|
+
c2,
|
|
1749
|
+
c3,
|
|
1750
|
+
unproject(p4[0], p4[1]),
|
|
1751
|
+
clonePosition(c1)
|
|
2376
1752
|
];
|
|
2377
1753
|
}
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
},
|
|
2414
|
-
{
|
|
2415
|
-
key: "arrowheadWidthRatio",
|
|
2416
|
-
presentationTier: "advanced",
|
|
2417
|
-
label: "Arrowhead width",
|
|
2418
|
-
description: "Arrowhead base width as a multiple of the arrowhead length",
|
|
2419
|
-
type: "number",
|
|
2420
|
-
min: .05,
|
|
2421
|
-
max: 2,
|
|
2422
|
-
step: .01
|
|
2423
|
-
},
|
|
2424
|
-
{
|
|
2425
|
-
key: "hatchLengthRatio",
|
|
2426
|
-
presentationTier: "advanced",
|
|
2427
|
-
label: "Hatch length",
|
|
2428
|
-
description: "Length of the parallel hatches as a fraction of the chord span",
|
|
2429
|
-
type: "number",
|
|
2430
|
-
min: .01,
|
|
2431
|
-
max: .5,
|
|
2432
|
-
step: .01
|
|
2433
|
-
},
|
|
2434
|
-
{
|
|
2435
|
-
key: "hatchCount",
|
|
2436
|
-
presentationTier: "advanced",
|
|
2437
|
-
label: "Hatch count",
|
|
2438
|
-
description: "Number of parallel hatches spaced along the curve",
|
|
2439
|
-
type: "number",
|
|
2440
|
-
min: 0,
|
|
2441
|
-
max: 20,
|
|
2442
|
-
step: 1
|
|
2443
|
-
},
|
|
2444
|
-
{
|
|
2445
|
-
key: "curveDepthRatio",
|
|
2446
|
-
presentationTier: "advanced",
|
|
2447
|
-
label: "Curve depth",
|
|
2448
|
-
description: "How far the back line bends toward the tip, as a fraction of the chord span",
|
|
2449
|
-
type: "number",
|
|
2450
|
-
min: 0,
|
|
2451
|
-
max: 1,
|
|
2452
|
-
step: .01
|
|
2453
|
-
},
|
|
2454
|
-
{
|
|
2455
|
-
key: "arcSegments",
|
|
2456
|
-
label: "Arc segments",
|
|
2457
|
-
description: "Number of segments used to render the curve",
|
|
2458
|
-
type: "number",
|
|
2459
|
-
presentationTier: "advanced",
|
|
2460
|
-
min: 4,
|
|
2461
|
-
max: 96,
|
|
2462
|
-
step: 1
|
|
1754
|
+
function snapToAxis(point, origin, direction) {
|
|
1755
|
+
const snapped = vecAdd(origin, vecScale(direction, vecDot(vecSub(project(point[0], point[1]), origin), direction)));
|
|
1756
|
+
return unproject(snapped[0], snapped[1]);
|
|
1757
|
+
}
|
|
1758
|
+
const rectangleDrawRule = {
|
|
1759
|
+
id: "rectangle",
|
|
1760
|
+
minimumUserPoints: 3,
|
|
1761
|
+
minimumPreviewPoints: 2,
|
|
1762
|
+
canonicalPointCount: 3,
|
|
1763
|
+
showGuide: true,
|
|
1764
|
+
guidePoints,
|
|
1765
|
+
derive,
|
|
1766
|
+
transform(event) {
|
|
1767
|
+
const { previous, next, activePointIndex } = event;
|
|
1768
|
+
if (previous.length < 3 || next.length < 3) return derive(next);
|
|
1769
|
+
const frame = createBaselineFrame(previous[0], previous[1], {
|
|
1770
|
+
origin: "p2",
|
|
1771
|
+
normal: "right"
|
|
1772
|
+
});
|
|
1773
|
+
if (!frame) return derive(next);
|
|
1774
|
+
if (activePointIndex === 0) return [
|
|
1775
|
+
snapToAxis(next[0], frame.p2, frame.direction),
|
|
1776
|
+
clonePosition(previous[1]),
|
|
1777
|
+
clonePosition(previous[2])
|
|
1778
|
+
];
|
|
1779
|
+
if (activePointIndex === 1) {
|
|
1780
|
+
const p2 = snapToAxis(next[1], frame.p1, frame.direction);
|
|
1781
|
+
const delta = vecSub(project(p2[0], p2[1]), frame.p2);
|
|
1782
|
+
const projectedP3 = vecAdd(project(previous[2][0], previous[2][1]), delta);
|
|
1783
|
+
const p3 = unproject(projectedP3[0], projectedP3[1]);
|
|
1784
|
+
return [
|
|
1785
|
+
clonePosition(previous[0]),
|
|
1786
|
+
p2,
|
|
1787
|
+
p3
|
|
1788
|
+
];
|
|
2463
1789
|
}
|
|
2464
|
-
|
|
1790
|
+
if (activePointIndex === 2) {
|
|
1791
|
+
const p3 = frame.pointAtNormalDistance(frame.signedNormalDistance(next[2]));
|
|
1792
|
+
if (p3) return [
|
|
1793
|
+
clonePosition(previous[0]),
|
|
1794
|
+
clonePosition(previous[1]),
|
|
1795
|
+
p3
|
|
1796
|
+
];
|
|
1797
|
+
}
|
|
1798
|
+
return derive(next);
|
|
1799
|
+
}
|
|
2465
1800
|
};
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
1801
|
+
//#endregion
|
|
1802
|
+
//#region src/draw-rules/line25.ts
|
|
1803
|
+
/** Line25: PT. 1 is the arrow tip; PT. 2 is the rear of the symbol. */
|
|
1804
|
+
const line25DrawRule = {
|
|
1805
|
+
id: "line25",
|
|
1806
|
+
minimumUserPoints: 2,
|
|
1807
|
+
canonicalPointCount: 2,
|
|
1808
|
+
derive: (points) => clonePositions(points.slice(0, 2)),
|
|
1809
|
+
transform: (event) => clonePositions(event.next.slice(0, 2))
|
|
2469
1810
|
};
|
|
1811
|
+
//#endregion
|
|
1812
|
+
//#region src/internal/angle-utils.ts
|
|
2470
1813
|
/**
|
|
2471
|
-
*
|
|
2472
|
-
* Handles internal projection to maintain strict perpendicularity and parallel hatching.
|
|
2473
|
-
*
|
|
2474
|
-
* @param coordinates - [PT 1 (Target Tip), PT 2 (Back Chord End), PT 3 (Back Chord End)]
|
|
2475
|
-
* @param options - Geometric and styling parameters
|
|
2476
|
-
* @returns GeoJSON FeatureCollection containing LineString components
|
|
1814
|
+
* Wraps an angle (radians) into the half-open range (-π, π].
|
|
2477
1815
|
*/
|
|
2478
|
-
function
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
const
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
}
|
|
2526
|
-
return {
|
|
2527
|
-
type: "FeatureCollection",
|
|
2528
|
-
features: [{
|
|
2529
|
-
type: "Feature",
|
|
2530
|
-
properties: {},
|
|
2531
|
-
geometry: {
|
|
2532
|
-
type: "MultiLineString",
|
|
2533
|
-
coordinates: lines.map((line) => line.map((c) => unproject(c[0], c[1])))
|
|
2534
|
-
}
|
|
2535
|
-
}]
|
|
2536
|
-
};
|
|
2537
|
-
}
|
|
2538
|
-
const AMBUSH = defineControlMeasure({
|
|
2539
|
-
metadata: AMBUSH_METADATA,
|
|
2540
|
-
generator: (controlPoints, options) => createAmbushSymbol(asThreePoints(controlPoints), options),
|
|
2541
|
-
defaultOptions: DEFAULT_AMBUSH_OPTIONS,
|
|
2542
|
-
rule: ambushDrawRule
|
|
2543
|
-
});
|
|
2544
|
-
//#endregion
|
|
2545
|
-
//#region src/internal/gapped-line.ts
|
|
2546
|
-
/**
|
|
2547
|
-
* Pushes the arc-length gap `[g0, g1]` onto `gaps`, normalizing it onto
|
|
2548
|
-
* `[0, total)` and splitting it into two intervals when it straddles a ring
|
|
2549
|
-
* seam ({@link buildGappedLine} walks a flat array and does not wrap on its
|
|
2550
|
-
* own). Use this for closed rings; open polylines can push {@link ArcGap}s
|
|
2551
|
-
* directly.
|
|
2552
|
-
*/
|
|
2553
|
-
function pushWrappedGap(gaps, g0, g1, total) {
|
|
2554
|
-
if (g1 - g0 >= total) {
|
|
2555
|
-
gaps.push({
|
|
2556
|
-
g0: 0,
|
|
2557
|
-
g1: total
|
|
2558
|
-
});
|
|
2559
|
-
return;
|
|
2560
|
-
}
|
|
2561
|
-
const lo = (g0 % total + total) % total;
|
|
2562
|
-
const hi = (g1 % total + total) % total;
|
|
2563
|
-
if (lo <= hi) gaps.push({
|
|
2564
|
-
g0: lo,
|
|
2565
|
-
g1: hi
|
|
2566
|
-
});
|
|
2567
|
-
else {
|
|
2568
|
-
gaps.push({
|
|
2569
|
-
g0: lo,
|
|
2570
|
-
g1: total
|
|
2571
|
-
});
|
|
2572
|
-
gaps.push({
|
|
2573
|
-
g0: 0,
|
|
2574
|
-
g1: hi
|
|
2575
|
-
});
|
|
2576
|
-
}
|
|
2577
|
-
}
|
|
2578
|
-
/** Whether the global arc position `arc` falls inside any of `gaps` (each
|
|
2579
|
-
* already normalized onto `[0, total)` by {@link pushWrappedGap}, so a plain
|
|
2580
|
-
* per-gap interval test suffices). Shared by the boundary-decoration measures
|
|
2581
|
-
* that suppress a glyph/tic/barb sitting on top of a masking gap. */
|
|
2582
|
-
function arcInGaps(arc, gaps) {
|
|
2583
|
-
return gaps.some((g) => arc >= g.g0 && arc <= g.g1);
|
|
2584
|
-
}
|
|
2585
|
-
/** Appends `position` unless it coincides with the buffer's current tail. */
|
|
2586
|
-
function appendUnique(buffer, position) {
|
|
2587
|
-
const tail = buffer[buffer.length - 1];
|
|
2588
|
-
if (tail && Math.abs(tail[0] - position[0]) < 1e-9 && Math.abs(tail[1] - position[1]) < 1e-9) return;
|
|
2589
|
-
buffer.push(position);
|
|
2590
|
-
}
|
|
2591
|
-
/**
|
|
2592
|
-
* Builds the polyline as MultiLineString rings, splitting it with a gap at each
|
|
2593
|
-
* echelon position. `gaps` holds global arc-length intervals (distance from the
|
|
2594
|
-
* start of the whole polyline) to carve out — a single gap can span several
|
|
2595
|
-
* short segments, which matters when the line has been smoothed into many
|
|
2596
|
-
* samples. Works for both open polylines and closed rings (pass a vertex array
|
|
2597
|
-
* whose first and last positions coincide).
|
|
2598
|
-
*/
|
|
2599
|
-
function buildGappedLine(verts, gaps) {
|
|
2600
|
-
const parts = [];
|
|
2601
|
-
let buffer = [];
|
|
2602
|
-
const flush = () => {
|
|
2603
|
-
if (buffer.length >= 2) parts.push(buffer);
|
|
2604
|
-
buffer = [];
|
|
2605
|
-
};
|
|
2606
|
-
if (verts.length === 0) return parts;
|
|
2607
|
-
const merged = [];
|
|
2608
|
-
for (const g of [...gaps].sort((x, y) => x.g0 - y.g0)) {
|
|
2609
|
-
const last = merged[merged.length - 1];
|
|
2610
|
-
if (last && g.g0 <= last.g1) last.g1 = Math.max(last.g1, g.g1);
|
|
2611
|
-
else merged.push({ ...g });
|
|
2612
|
-
}
|
|
2613
|
-
appendUnique(buffer, unproject(verts[0][0], verts[0][1]));
|
|
2614
|
-
let dist = 0;
|
|
2615
|
-
for (let i = 0; i < verts.length - 1; i++) {
|
|
2616
|
-
const a = verts[i];
|
|
2617
|
-
const b = verts[i + 1];
|
|
2618
|
-
const seg = vecSub(b, a);
|
|
2619
|
-
const L = vecMag(seg);
|
|
2620
|
-
if (L < 1e-6) continue;
|
|
2621
|
-
const segStart = dist;
|
|
2622
|
-
const segEnd = dist + L;
|
|
2623
|
-
const along = vecNorm(seg);
|
|
2624
|
-
const pointAt = (globalD) => {
|
|
2625
|
-
const q = vecAdd(a, vecScale(along, globalD - segStart));
|
|
2626
|
-
return unproject(q[0], q[1]);
|
|
2627
|
-
};
|
|
2628
|
-
for (const g of merged) {
|
|
2629
|
-
if (g.g1 <= segStart || g.g0 >= segEnd) continue;
|
|
2630
|
-
const g0 = Math.max(segStart, g.g0);
|
|
2631
|
-
const g1 = Math.min(segEnd, g.g1);
|
|
2632
|
-
if (g1 <= g0) continue;
|
|
2633
|
-
appendUnique(buffer, pointAt(g0));
|
|
2634
|
-
flush();
|
|
2635
|
-
buffer.push(pointAt(g1));
|
|
2636
|
-
}
|
|
2637
|
-
appendUnique(buffer, unproject(b[0], b[1]));
|
|
2638
|
-
dist = segEnd;
|
|
2639
|
-
}
|
|
2640
|
-
flush();
|
|
2641
|
-
return parts;
|
|
2642
|
-
}
|
|
2643
|
-
//#endregion
|
|
2644
|
-
//#region src/style.ts
|
|
2645
|
-
/**
|
|
2646
|
-
* Per-feature style hint pinning a filled part to a solid interior. Attach to
|
|
2647
|
-
* a generator's intrinsic doctrinal accents — arrowheads, teeth, echelon
|
|
2648
|
-
* glyphs, barbs, blades — so a patterned (`hatch`, `dots`, …) fill set at the graphicsStyle
|
|
2649
|
-
* or measure layer never bleeds into a small silhouette and ruins its
|
|
2650
|
-
* legibility. The renderer only reads style hints, so one shared instance is
|
|
2651
|
-
* safe to reuse across every accent feature.
|
|
2652
|
-
*/
|
|
2653
|
-
const SOLID_ACCENT_FILL = { fillPattern: "solid" };
|
|
2654
|
-
/**
|
|
2655
|
-
* Ultimate fallback for the symbol color when no `color` or per-channel
|
|
2656
|
-
* override is supplied at any layer. Keeps a zero-config render monocolor
|
|
2657
|
-
* black and guarantees filled parts still render filled. See ADR-0011.
|
|
2658
|
-
*/
|
|
2659
|
-
const DEFAULT_SYMBOL_COLOR = "#000000";
|
|
2660
|
-
//#endregion
|
|
2661
|
-
//#region src/portrayal.ts
|
|
2662
|
-
const DEFAULT_STROKE_WIDTH_CSS_PIXELS = 2;
|
|
2663
|
-
const DEFAULT_STROKE_DASH_CSS_PIXELS = Object.freeze([]);
|
|
2664
|
-
const DEFAULT_LINE_CAP = "round";
|
|
2665
|
-
const DEFAULT_LINE_JOIN = "round";
|
|
2666
|
-
const DEFAULT_LABEL_HEIGHT_CSS_PIXELS = 14;
|
|
2667
|
-
const DEFAULT_LABEL_SIZE_CLAMP_CSS_PIXELS = {
|
|
2668
|
-
min: 8,
|
|
2669
|
-
max: 24
|
|
2670
|
-
};
|
|
2671
|
-
/** Shared absent-value portrayal defaults; render output remains sparse. */
|
|
2672
|
-
const DEFAULT_PORTRAYAL = Object.freeze({
|
|
2673
|
-
symbolColor: DEFAULT_SYMBOL_COLOR,
|
|
2674
|
-
strokeWidthCssPixels: 2,
|
|
2675
|
-
strokeDashCssPixels: DEFAULT_STROKE_DASH_CSS_PIXELS,
|
|
2676
|
-
lineCap: DEFAULT_LINE_CAP,
|
|
2677
|
-
lineJoin: DEFAULT_LINE_JOIN,
|
|
2678
|
-
labelHeightCssPixels: 14
|
|
2679
|
-
});
|
|
2680
|
-
//#endregion
|
|
2681
|
-
//#region src/internal/angle-utils.ts
|
|
2682
|
-
/**
|
|
2683
|
-
* Wraps an angle (radians) into the half-open range (-π, π].
|
|
2684
|
-
*/
|
|
2685
|
-
function normalizeRadians(angle) {
|
|
2686
|
-
let normalized = angle % (Math.PI * 2);
|
|
2687
|
-
if (normalized > Math.PI) normalized -= Math.PI * 2;
|
|
2688
|
-
if (normalized <= -Math.PI) normalized += Math.PI * 2;
|
|
2689
|
-
return normalized;
|
|
2690
|
-
}
|
|
2691
|
-
/** Wraps a degree value into the canonical clockwise `[0, 360)` range. */
|
|
2692
|
-
function normalizeDegrees(degrees) {
|
|
2693
|
-
return (degrees % 360 + 360) % 360;
|
|
2694
|
-
}
|
|
2695
|
-
/**
|
|
2696
|
-
* Flips a text rotation by π when it would otherwise read upside-down, keeping
|
|
2697
|
-
* the baseline pointing left-to-right (within ±90° of horizontal).
|
|
2698
|
-
*/
|
|
2699
|
-
function keepTextLeftToRight(rotation) {
|
|
2700
|
-
const normalized = normalizeRadians(rotation);
|
|
2701
|
-
if (normalized > Math.PI / 2) return normalized - Math.PI;
|
|
2702
|
-
if (normalized < -Math.PI / 2) return normalized + Math.PI;
|
|
2703
|
-
return normalized;
|
|
2704
|
-
}
|
|
2705
|
-
//#endregion
|
|
2706
|
-
//#region src/internal/sketch.ts
|
|
2707
|
-
/**
|
|
2708
|
-
* Projects `coordinates` and derives the arrow's tip, heading and total length.
|
|
2709
|
-
* Returns `null` for degenerate input (fewer than two points, a zero-length
|
|
2710
|
-
* path, or a zero-length final segment) so the caller can decide what to emit.
|
|
2711
|
-
*/
|
|
2712
|
-
function arrowAxis(coordinates) {
|
|
2713
|
-
if (coordinates.length < 2) return null;
|
|
2714
|
-
const pts = coordinates.map((c) => project(c[0], c[1]));
|
|
2715
|
-
let pathLength = 0;
|
|
2716
|
-
for (let i = 0; i < pts.length - 1; i++) pathLength += Math.hypot(pts[i + 1][0] - pts[i][0], pts[i + 1][1] - pts[i][1]);
|
|
2717
|
-
const tip = pts[pts.length - 1];
|
|
2718
|
-
const prev = pts[pts.length - 2];
|
|
2719
|
-
const hx = tip[0] - prev[0];
|
|
2720
|
-
const hy = tip[1] - prev[1];
|
|
2721
|
-
const segLength = Math.hypot(hx, hy);
|
|
2722
|
-
if (segLength === 0 || pathLength === 0) return null;
|
|
2723
|
-
const dir = [hx / segLength, hy / segLength];
|
|
2724
|
-
const perp = [-dir[1], dir[0]];
|
|
2725
|
-
return {
|
|
2726
|
-
pts,
|
|
2727
|
-
pathLength,
|
|
2728
|
-
tip,
|
|
2729
|
-
dir,
|
|
2730
|
-
perp,
|
|
2731
|
-
segLength
|
|
1816
|
+
function normalizeRadians(angle) {
|
|
1817
|
+
let normalized = angle % (Math.PI * 2);
|
|
1818
|
+
if (normalized > Math.PI) normalized -= Math.PI * 2;
|
|
1819
|
+
if (normalized <= -Math.PI) normalized += Math.PI * 2;
|
|
1820
|
+
return normalized;
|
|
1821
|
+
}
|
|
1822
|
+
/** Wraps a degree value into the canonical clockwise `[0, 360)` range. */
|
|
1823
|
+
function normalizeDegrees(degrees) {
|
|
1824
|
+
return (degrees % 360 + 360) % 360;
|
|
1825
|
+
}
|
|
1826
|
+
/**
|
|
1827
|
+
* Flips a text rotation by π when it would otherwise read upside-down, keeping
|
|
1828
|
+
* the baseline pointing left-to-right (within ±90° of horizontal).
|
|
1829
|
+
*/
|
|
1830
|
+
function keepTextLeftToRight(rotation) {
|
|
1831
|
+
const normalized = normalizeRadians(rotation);
|
|
1832
|
+
if (normalized > Math.PI / 2) return normalized - Math.PI;
|
|
1833
|
+
if (normalized < -Math.PI / 2) return normalized + Math.PI;
|
|
1834
|
+
return normalized;
|
|
1835
|
+
}
|
|
1836
|
+
//#endregion
|
|
1837
|
+
//#region src/internal/sketch.ts
|
|
1838
|
+
/**
|
|
1839
|
+
* Projects `coordinates` and derives the arrow's tip, heading and total length.
|
|
1840
|
+
* Returns `null` for degenerate input (fewer than two points, a zero-length
|
|
1841
|
+
* path, or a zero-length final segment) so the caller can decide what to emit.
|
|
1842
|
+
*/
|
|
1843
|
+
function arrowAxis(coordinates) {
|
|
1844
|
+
if (coordinates.length < 2) return null;
|
|
1845
|
+
const pts = coordinates.map((c) => project(c[0], c[1]));
|
|
1846
|
+
let pathLength = 0;
|
|
1847
|
+
for (let i = 0; i < pts.length - 1; i++) pathLength += Math.hypot(pts[i + 1][0] - pts[i][0], pts[i + 1][1] - pts[i][1]);
|
|
1848
|
+
const tip = pts[pts.length - 1];
|
|
1849
|
+
const prev = pts[pts.length - 2];
|
|
1850
|
+
const hx = tip[0] - prev[0];
|
|
1851
|
+
const hy = tip[1] - prev[1];
|
|
1852
|
+
const segLength = Math.hypot(hx, hy);
|
|
1853
|
+
if (segLength === 0 || pathLength === 0) return null;
|
|
1854
|
+
const dir = [hx / segLength, hy / segLength];
|
|
1855
|
+
const perp = [-dir[1], dir[0]];
|
|
1856
|
+
return {
|
|
1857
|
+
pts,
|
|
1858
|
+
pathLength,
|
|
1859
|
+
tip,
|
|
1860
|
+
dir,
|
|
1861
|
+
perp,
|
|
1862
|
+
segLength
|
|
2732
1863
|
};
|
|
2733
1864
|
}
|
|
2734
1865
|
/**
|
|
@@ -3209,286 +2340,1699 @@ const DEFAULT_LABEL_KNOCKOUT_PADDING = .3;
|
|
|
3209
2340
|
const HOSTILE_LINE_AMPLIFIER = {
|
|
3210
2341
|
key: "N",
|
|
3211
2342
|
label: "Hostile (ENY) marker",
|
|
3212
|
-
description: "Field N — literal \"ENY\" marker beyond both ends of the line.",
|
|
2343
|
+
description: "Field N — literal \"ENY\" marker beyond both ends of the line.",
|
|
2344
|
+
placeholder: "ENY",
|
|
2345
|
+
maxLength: 3
|
|
2346
|
+
};
|
|
2347
|
+
/** Label-padding control shared by label-bearing graphics. */
|
|
2348
|
+
const LABEL_PADDING_PARAM = {
|
|
2349
|
+
key: "labelPadding",
|
|
2350
|
+
presentationTier: "advanced",
|
|
2351
|
+
label: "Label padding",
|
|
2352
|
+
description: "Extra clearance around labels, as a ratio of the label height",
|
|
2353
|
+
type: "number",
|
|
2354
|
+
min: 0,
|
|
2355
|
+
max: 2,
|
|
2356
|
+
step: .05
|
|
2357
|
+
};
|
|
2358
|
+
/**
|
|
2359
|
+
* Resolves {@link LabelSizeOptions} into the single text-size property the
|
|
2360
|
+
* label feature carries (ADR-0028): `textSizePixels` when screen-anchored
|
|
2361
|
+
* (`labelSizePixels` present — pixels win over a stray `labelSize`),
|
|
2362
|
+
* `textSizeMeters` when ground-anchored (`labelSize` alone), or neither, in
|
|
2363
|
+
* which case adapters render their engine default, screen-fixed.
|
|
2364
|
+
*/
|
|
2365
|
+
function labelSizeProps(options) {
|
|
2366
|
+
const { labelSize, labelSizePixels } = options;
|
|
2367
|
+
if (labelSizePixels !== void 0) return { textSizePixels: labelSizePixels };
|
|
2368
|
+
if (labelSize !== void 0) return { textSizeMeters: labelSize };
|
|
2369
|
+
return {};
|
|
2370
|
+
}
|
|
2371
|
+
/** Pushes a `part: "label"` Point feature at projected `point`. */
|
|
2372
|
+
function pushLabel(out, point, text, rotation, sizeProps, textAnchor, amplifierField, labelPlacementKey) {
|
|
2373
|
+
out.push({
|
|
2374
|
+
type: "Feature",
|
|
2375
|
+
properties: {
|
|
2376
|
+
part: "label",
|
|
2377
|
+
text,
|
|
2378
|
+
rotation,
|
|
2379
|
+
...textAnchor ? { textAnchor } : {},
|
|
2380
|
+
...amplifierField ? { amplifierField } : {},
|
|
2381
|
+
...labelPlacementKey ? { labelPlacementKey } : {},
|
|
2382
|
+
...sizeProps
|
|
2383
|
+
},
|
|
2384
|
+
geometry: {
|
|
2385
|
+
type: "Point",
|
|
2386
|
+
coordinates: unproject(point[0], point[1])
|
|
2387
|
+
}
|
|
2388
|
+
});
|
|
2389
|
+
}
|
|
2390
|
+
/** Ratio of resolved label height (`labelSize`, or `labelSizePixels × metersPerPixel`) used as end-label clearance. */
|
|
2391
|
+
const END_LABEL_CLEARANCE_RATIO = .3;
|
|
2392
|
+
/** Ratio of resolved label height used to inset "above" end labels from the tip. */
|
|
2393
|
+
const ABOVE_LABEL_INSET_RATIO = .3;
|
|
2394
|
+
/** Ratio of resolved label height used as the "above" end labels' perpendicular offset. */
|
|
2395
|
+
const ABOVE_LABEL_OFFSET_RATIO = .6;
|
|
2396
|
+
function textHeight(options) {
|
|
2397
|
+
const { labelSize, labelSizePixels, metersPerPixel } = options;
|
|
2398
|
+
if (labelSizePixels !== void 0) return metersPerPixel !== void 0 && metersPerPixel > 0 ? labelSizePixels * metersPerPixel : void 0;
|
|
2399
|
+
return labelSize;
|
|
2400
|
+
}
|
|
2401
|
+
/**
|
|
2402
|
+
* Label offset in meters: `ratio` × the label's resolved height (`labelSize`
|
|
2403
|
+
* meters, or `labelSizePixels × metersPerPixel` when screen-anchored), or
|
|
2404
|
+
* `fallbackMeters` when no size resolves (e.g. no map resolution yet). The fallback is small
|
|
2405
|
+
* relative to typical control measure spans — the same order of magnitude as
|
|
2406
|
+
* Boundary's own smallest label offsets — so a fixed label still reads as "just
|
|
2407
|
+
* clear of the line" rather than crowding it or floating away. Callers supply
|
|
2408
|
+
* their own ratio (end-label clearance, "above" inset/offset, center-group
|
|
2409
|
+
* offset) but share this single resolution+fallback rule.
|
|
2410
|
+
*/
|
|
2411
|
+
function resolveLabelOffsetMeters(options, ratio, fallbackMeters = 50) {
|
|
2412
|
+
const h = textHeight(options);
|
|
2413
|
+
return h !== void 0 ? ratio * h : fallbackMeters;
|
|
2414
|
+
}
|
|
2415
|
+
/**
|
|
2416
|
+
* Places one label just beyond each end of `verts` (projected meters), along
|
|
2417
|
+
* the outward direction of that end's segment, reading along the line
|
|
2418
|
+
* ({@link labelRotationAlong} is sign-independent, so the outward-pointing
|
|
2419
|
+
* `along` reads correctly). `textForEnd` supplies each end's text — a shared
|
|
2420
|
+
* string, or a function keyed by which end; an empty string skips that end.
|
|
2421
|
+
*
|
|
2422
|
+
* `textAnchor` is chosen so the text grows AWAY from the line: adapters flip
|
|
2423
|
+
* a label's rendered baseline (via {@link keepTextLeftToRight}) whenever its
|
|
2424
|
+
* direction points leftward, and that flip happens exactly when `along[0] <
|
|
2425
|
+
* 0` (same quadrant test `labelRotationAlong` relies on) — mirroring
|
|
2426
|
+
* Boundary's `vecDot` sign trick, "start" anchors the text at the point when
|
|
2427
|
+
* there is no flip, "end" when there is, so the anchored edge always sits at
|
|
2428
|
+
* the point and the text body always extends outward.
|
|
2429
|
+
*/
|
|
2430
|
+
function pushEndLabels(out, verts, textForEnd, options = {}) {
|
|
2431
|
+
const frames = endFrames(verts);
|
|
2432
|
+
const sizeProps = labelSizeProps(options);
|
|
2433
|
+
const clearance = resolveLabelOffsetMeters(options, END_LABEL_CLEARANCE_RATIO + Math.max(0, options.labelPadding ?? 0));
|
|
2434
|
+
for (const end of ["start", "end"]) {
|
|
2435
|
+
const frame = frames[end];
|
|
2436
|
+
if (!frame) continue;
|
|
2437
|
+
const text = typeof textForEnd === "string" ? textForEnd : textForEnd(end);
|
|
2438
|
+
if (text.length === 0) continue;
|
|
2439
|
+
pushLabel(out, vecAdd(frame.point, vecScale(frame.along, clearance)), text, labelRotationAlong(frame.along), sizeProps, frame.along[0] >= 0 ? "start" : "end");
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
/**
|
|
2443
|
+
* Approximate sans-serif character width/height ratio, used to estimate a
|
|
2444
|
+
* label's rendered text width from its character count when no live text
|
|
2445
|
+
* measurement is available at generator time.
|
|
2446
|
+
*/
|
|
2447
|
+
const LABEL_CHAR_ASPECT_RATIO = .6;
|
|
2448
|
+
/**
|
|
2449
|
+
* Half a single glyph's width, as a ratio of its height — the base boundary
|
|
2450
|
+
* gap a measure reserves for one intrinsic capital letter (Retain's R,
|
|
2451
|
+
* Contain's C). Wider than half `LABEL_CHAR_ASPECT_RATIO` because these
|
|
2452
|
+
* glyphs are stamped alone rather than averaged over a word.
|
|
2453
|
+
*/
|
|
2454
|
+
const LABEL_HALF_GLYPH_RATIO = .45;
|
|
2455
|
+
/** Estimated rendered width of `text` given its resolved label height in meters. */
|
|
2456
|
+
function estimatedTextWidth(text, textHeightMeters) {
|
|
2457
|
+
return text.length * LABEL_CHAR_ASPECT_RATIO * textHeightMeters;
|
|
2458
|
+
}
|
|
2459
|
+
/** Resolve the label's final display size, then measure and convert it to construction metres. */
|
|
2460
|
+
function resolveTextMetrics(text, options, context, style = "regular", fallbackHeightMeters = 50) {
|
|
2461
|
+
const legacyHeight = resolveLabelOffsetMeters(options, 1, fallbackHeightMeters);
|
|
2462
|
+
const constructionScale = context?.constructionMetersPerCssPixel;
|
|
2463
|
+
if (!(constructionScale !== void 0 && constructionScale > 0)) return {
|
|
2464
|
+
width: estimatedTextWidth(text, legacyHeight),
|
|
2465
|
+
height: legacyHeight
|
|
2466
|
+
};
|
|
2467
|
+
let sizeCssPixels;
|
|
2468
|
+
if (options.labelSizePixels !== void 0) sizeCssPixels = options.labelSizePixels;
|
|
2469
|
+
else if (options.labelSize !== void 0) {
|
|
2470
|
+
const requested = options.labelSize / constructionScale;
|
|
2471
|
+
const requestedBand = context?.labelSizeClampCssPixels;
|
|
2472
|
+
const band = requestedBand && Number.isFinite(requestedBand.min) && requestedBand.min >= 0 && Number.isFinite(requestedBand.max) && requestedBand.max >= 0 ? requestedBand : DEFAULT_LABEL_SIZE_CLAMP_CSS_PIXELS;
|
|
2473
|
+
const min = Math.min(band.min, band.max);
|
|
2474
|
+
const max = Math.max(band.min, band.max);
|
|
2475
|
+
sizeCssPixels = Math.min(max, Math.max(min, requested));
|
|
2476
|
+
} else sizeCssPixels = 14;
|
|
2477
|
+
const fallbackHeight = sizeCssPixels * constructionScale;
|
|
2478
|
+
const measured = context?.measureText?.({
|
|
2479
|
+
text,
|
|
2480
|
+
style,
|
|
2481
|
+
sizeCssPixels
|
|
2482
|
+
});
|
|
2483
|
+
if (!measured || !Number.isFinite(measured.widthCssPixels) || measured.widthCssPixels < 0 || !Number.isFinite(measured.heightCssPixels) || measured.heightCssPixels < 0) return {
|
|
2484
|
+
width: estimatedTextWidth(text, fallbackHeight),
|
|
2485
|
+
height: fallbackHeight
|
|
2486
|
+
};
|
|
2487
|
+
return {
|
|
2488
|
+
width: measured.widthCssPixels * constructionScale,
|
|
2489
|
+
height: measured.heightCssPixels * constructionScale
|
|
2490
|
+
};
|
|
2491
|
+
}
|
|
2492
|
+
/**
|
|
2493
|
+
* Half-length of the knockout required for a label along `direction`.
|
|
2494
|
+
*
|
|
2495
|
+
* The text box itself is always cleared; `labelPadding` adds symmetric clearance expressed in
|
|
2496
|
+
* resolved label heights. Keeping this calculation here gives every generator the same public
|
|
2497
|
+
* label-clearance interface while hiding font measurement and rotated-box projection details.
|
|
2498
|
+
*/
|
|
2499
|
+
function labelKnockoutHalfExtent(text, direction, rotation, options, context, defaultPadding = 0) {
|
|
2500
|
+
const metrics = resolveTextMetrics(text, options, context);
|
|
2501
|
+
const padding = Math.max(0, options.labelPadding ?? defaultPadding) * metrics.height;
|
|
2502
|
+
return labelHalfExtent(direction, metrics.width, metrics.height, rotation) + padding;
|
|
2503
|
+
}
|
|
2504
|
+
/**
|
|
2505
|
+
* {@link labelKnockoutHalfExtent} expressed as a fraction of an arc's angular
|
|
2506
|
+
* sweep, for generators that carve their knockout in arc-progress space rather
|
|
2507
|
+
* than in meters. Clamped to `0.5` so the gap can never exceed the arc.
|
|
2508
|
+
*/
|
|
2509
|
+
function arcKnockoutHalfFraction(halfExtentMeters, radius, sweepAngle) {
|
|
2510
|
+
const arcLength = radius * Math.abs(sweepAngle);
|
|
2511
|
+
if (!(arcLength > 1e-6)) return .5;
|
|
2512
|
+
return Math.min(.5, Math.max(0, halfExtentMeters) / arcLength);
|
|
2513
|
+
}
|
|
2514
|
+
/** The end-label text for a phase-line naming, or `""` when unnamed. */
|
|
2515
|
+
function phaseLineLabelText(name, includePrefix) {
|
|
2516
|
+
if (!name) return "";
|
|
2517
|
+
return includePrefix ? `PL ${name}` : name;
|
|
2518
|
+
}
|
|
2519
|
+
/**
|
|
2520
|
+
* Places a Field N hostile marker beyond each end of `verts`, following the
|
|
2521
|
+
* same tangent, rotation, anchor, size, and padding conventions as
|
|
2522
|
+
* {@link pushEndLabels}. When another beyond-tip label is present, its
|
|
2523
|
+
* estimated width is added so the hostile marker remains outermost.
|
|
2524
|
+
*/
|
|
2525
|
+
function pushHostileEndLabels(out, verts, hostileText, options = {}, occupiedTextForEnd = "", context) {
|
|
2526
|
+
if (!hostileText) return;
|
|
2527
|
+
const frames = endFrames(verts);
|
|
2528
|
+
const sizeProps = labelSizeProps(options);
|
|
2529
|
+
for (const end of ["start", "end"]) {
|
|
2530
|
+
const frame = frames[end];
|
|
2531
|
+
if (!frame) continue;
|
|
2532
|
+
const occupiedText = typeof occupiedTextForEnd === "string" ? occupiedTextForEnd : occupiedTextForEnd(end);
|
|
2533
|
+
const metrics = resolveTextMetrics(occupiedText || hostileText, options, context);
|
|
2534
|
+
const clearance = (END_LABEL_CLEARANCE_RATIO + Math.max(0, options.labelPadding ?? 0)) * metrics.height;
|
|
2535
|
+
const distance = clearance + (occupiedText ? metrics.width + clearance : 0);
|
|
2536
|
+
pushLabel(out, vecAdd(frame.point, vecScale(frame.along, distance)), hostileText, labelRotationAlong(frame.along), sizeProps, frame.along[0] >= 0 ? "start" : "end", "N", `label:N:${end}`);
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
/**
|
|
2540
|
+
* Resolves the {@link AboveEndFrame} at each tip of `verts` (projected meters):
|
|
2541
|
+
* a base point inset from the tip toward the interior, the segment's own
|
|
2542
|
+
* natural `perp` (so "above" = `+perp` reads the same at both ends), the
|
|
2543
|
+
* one-row perpendicular `offset`, the along-line `rotation`, and the
|
|
2544
|
+
* interior-growing `textAnchor`. Shared by {@link pushEndLabelsAbove} (one
|
|
2545
|
+
* label per tip, at `base + perp·offset`) and generic-c2-line's dual
|
|
2546
|
+
* above/below amplifier labels (`base ± perp·offset`), so both build on the
|
|
2547
|
+
* same tangent/inset/anchor math instead of re-deriving it.
|
|
2548
|
+
*/
|
|
2549
|
+
function aboveEndFrames(verts, options) {
|
|
2550
|
+
const tangents = endTangents(verts);
|
|
2551
|
+
const inset = resolveLabelOffsetMeters(options, ABOVE_LABEL_INSET_RATIO);
|
|
2552
|
+
const offset = resolveLabelOffsetMeters(options, ABOVE_LABEL_OFFSET_RATIO + Math.max(0, options.labelPadding ?? 0));
|
|
2553
|
+
const frames = {};
|
|
2554
|
+
for (const end of ["start", "end"]) {
|
|
2555
|
+
const tangent = tangents[end];
|
|
2556
|
+
if (!tangent) continue;
|
|
2557
|
+
frames[end] = {
|
|
2558
|
+
base: vecAdd(tangent.point, vecScale(tangent.interior, inset)),
|
|
2559
|
+
perp: tangent.perp,
|
|
2560
|
+
offset,
|
|
2561
|
+
rotation: labelRotationAlong(tangent.interior),
|
|
2562
|
+
textAnchor: tangent.outwardX >= 0 ? "end" : "start"
|
|
2563
|
+
};
|
|
2564
|
+
}
|
|
2565
|
+
return frames;
|
|
2566
|
+
}
|
|
2567
|
+
/**
|
|
2568
|
+
* Places one label above each end of `verts` (projected meters), inset
|
|
2569
|
+
* slightly from the tip and anchored toward the line's interior — the MIL-
|
|
2570
|
+
* STD-2525E template convention for fixed end labels like "LL"/"HOL"/"BHL"
|
|
2571
|
+
* (as opposed to Phase Line's "beyond the tip" placement, see
|
|
2572
|
+
* {@link pushEndLabels}). Placement comes from {@link aboveEndFrames}, which
|
|
2573
|
+
* generic-c2-line's H/T-above + W/W1-below labels share. `textForEnd` supplies
|
|
2574
|
+
* each end's text — a shared string, or a function keyed by which end; an empty
|
|
2575
|
+
* string skips that end.
|
|
2576
|
+
*/
|
|
2577
|
+
function pushEndLabelsAbove(out, verts, textForEnd, options = {}) {
|
|
2578
|
+
const frames = aboveEndFrames(verts, options);
|
|
2579
|
+
const sizeProps = labelSizeProps(options);
|
|
2580
|
+
for (const end of ["start", "end"]) {
|
|
2581
|
+
const frame = frames[end];
|
|
2582
|
+
if (!frame) continue;
|
|
2583
|
+
const text = typeof textForEnd === "string" ? textForEnd : textForEnd(end);
|
|
2584
|
+
if (text.length === 0) continue;
|
|
2585
|
+
pushLabel(out, vecAdd(frame.base, vecScale(frame.perp, frame.offset)), text, frame.rotation, sizeProps, frame.textAnchor);
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
/**
|
|
2589
|
+
* Convenience for measures that are just a polyline with a fixed label at
|
|
2590
|
+
* each end: builds the LineString through `positions` plus its end labels
|
|
2591
|
+
* (via {@link pushEndLabels} or {@link pushEndLabelsAbove}, per `placement`)
|
|
2592
|
+
* as a complete FeatureCollection.
|
|
2593
|
+
*/
|
|
2594
|
+
function fixedLabelLineFeatures(positions, options, textForEnd, hostileText, context) {
|
|
2595
|
+
const { part, placement = "beyond", phaseLineName, includePrefix = true, lineStyle, smooth, smoothResolution, ...sizeOptions } = options;
|
|
2596
|
+
const verts = smoothLineVerts(positions.map((p) => project(p[0], p[1])), {
|
|
2597
|
+
smooth,
|
|
2598
|
+
smoothResolution
|
|
2599
|
+
}, 12);
|
|
2600
|
+
const lineCoordinates = smooth ? verts.map((v) => unproject(v[0], v[1])) : positions;
|
|
2601
|
+
const features = [{
|
|
2602
|
+
type: "Feature",
|
|
2603
|
+
properties: lineStyle === void 0 ? { part } : {
|
|
2604
|
+
part,
|
|
2605
|
+
style: lineStyle
|
|
2606
|
+
},
|
|
2607
|
+
geometry: {
|
|
2608
|
+
type: "LineString",
|
|
2609
|
+
coordinates: lineCoordinates
|
|
2610
|
+
}
|
|
2611
|
+
}];
|
|
2612
|
+
const labelFeatures = [];
|
|
2613
|
+
if (placement === "above") pushEndLabelsAbove(labelFeatures, verts, textForEnd, sizeOptions);
|
|
2614
|
+
else pushEndLabels(labelFeatures, verts, textForEnd, sizeOptions);
|
|
2615
|
+
const nameText = phaseLineLabelText(phaseLineName?.trim(), includePrefix);
|
|
2616
|
+
if (nameText) pushEndLabels(labelFeatures, verts, nameText, sizeOptions);
|
|
2617
|
+
pushHostileEndLabels(labelFeatures, verts, hostileText, sizeOptions, nameText || (placement === "beyond" ? textForEnd : ""), context);
|
|
2618
|
+
features.push(...labelFeatures);
|
|
2619
|
+
return {
|
|
2620
|
+
type: "FeatureCollection",
|
|
2621
|
+
features
|
|
2622
|
+
};
|
|
2623
|
+
}
|
|
2624
|
+
//#endregion
|
|
2625
|
+
//#region src/generators/cm34-mission-tasks/shared.ts
|
|
2626
|
+
/**
|
|
2627
|
+
* Builds an open chevron arrowhead (left → tip → right) as unprojected
|
|
2628
|
+
* coordinates. The tip sits at `tip`, pointing along `dir`, with the base set
|
|
2629
|
+
* back by `length` and spanning `width` perpendicular to the shaft. Mirrors the
|
|
2630
|
+
* `createArrowHead` helper used by the attack-family generators.
|
|
2631
|
+
*/
|
|
2632
|
+
function createArrowHead$2(tip, dir, length, width) {
|
|
2633
|
+
const norm = [-dir[1], dir[0]];
|
|
2634
|
+
const base = vecSub(tip, vecScale(dir, length));
|
|
2635
|
+
const left = vecAdd(base, vecScale(norm, width / 2));
|
|
2636
|
+
const right = vecSub(base, vecScale(norm, width / 2));
|
|
2637
|
+
return [
|
|
2638
|
+
unproject(left[0], left[1]),
|
|
2639
|
+
unproject(tip[0], tip[1]),
|
|
2640
|
+
unproject(right[0], right[1])
|
|
2641
|
+
];
|
|
2642
|
+
}
|
|
2643
|
+
/**
|
|
2644
|
+
* Builds the glyph label as a Point feature, forwarding the resolved size
|
|
2645
|
+
* anchor (`textSizeMeters`/`textSizePixels`, ADR-0028) to adapters for
|
|
2646
|
+
* styling. `point` is in projected meters; the rotation must already be in
|
|
2647
|
+
* the generator's feature-rotation convention. Shared by every mission-task
|
|
2648
|
+
* measure that centers a glyph on its symbol.
|
|
2649
|
+
*/
|
|
2650
|
+
function createLabelFeature(point, text, rotation, labelSize = {}) {
|
|
2651
|
+
return {
|
|
2652
|
+
type: "Feature",
|
|
2653
|
+
properties: {
|
|
2654
|
+
part: "label",
|
|
2655
|
+
labelPlacement: false,
|
|
2656
|
+
text,
|
|
2657
|
+
rotation,
|
|
2658
|
+
...labelSizeProps(labelSize)
|
|
2659
|
+
},
|
|
2660
|
+
geometry: {
|
|
2661
|
+
type: "Point",
|
|
2662
|
+
coordinates: unproject(point[0], point[1])
|
|
2663
|
+
}
|
|
2664
|
+
};
|
|
2665
|
+
}
|
|
2666
|
+
/**
|
|
2667
|
+
* Shared scaffold for the "bracket" mission-task symbols (breach, bypass,
|
|
2668
|
+
* canalize). Owns the three-sided bracket: a front opening (PT. 1 / PT. 2), a
|
|
2669
|
+
* rear line parallel to it through PT. 3, and a glyph centered on the rear line.
|
|
2670
|
+
* Each measure supplies only its end caps and label glyph via
|
|
2671
|
+
* {@link BracketSymbolConfig}.
|
|
2672
|
+
*
|
|
2673
|
+
* Input Points:
|
|
2674
|
+
* - PT. 1: Front opening endpoint (top/left)
|
|
2675
|
+
* - PT. 2: Front opening endpoint (bottom/right) - defines height with PT. 1
|
|
2676
|
+
* - PT. 3: Rear point - defines length and orientation
|
|
2677
|
+
*
|
|
2678
|
+
* Geometric Construction:
|
|
2679
|
+
* 1. PT. 1 and PT. 2 define the front opening (facing enemy/objective)
|
|
2680
|
+
* 2. A parallel line is projected through PT. 3 with identical height
|
|
2681
|
+
* 3. LineString follows: PT. 1 → Rear Top → Rear Bottom → PT. 2
|
|
2682
|
+
* 4. Measure-specific end caps at PT. 1 and PT. 2
|
|
2683
|
+
* 5. Label point at midpoint of rear line (styled with OpenLayers Text)
|
|
2684
|
+
*
|
|
2685
|
+
* @param coordinates - [PT. 1, PT. 2, PT. 3] as Position arrays
|
|
2686
|
+
* @param config - Per-measure end caps, label glyph, and sizing
|
|
2687
|
+
* @returns GeoJSON FeatureCollection containing MultiLineString and Point features
|
|
2688
|
+
*/
|
|
2689
|
+
function createBracketSymbol(coordinates, config, context) {
|
|
2690
|
+
const [c1, c2, c3] = coordinates;
|
|
2691
|
+
const p1 = project(c1[0], c1[1]);
|
|
2692
|
+
const p2 = project(c2[0], c2[1]);
|
|
2693
|
+
const p3 = project(c3[0], c3[1]);
|
|
2694
|
+
const vFront = vecSub(p2, p1);
|
|
2695
|
+
const frontLength = vecMag(vFront);
|
|
2696
|
+
if (frontLength < 1e-6) return {
|
|
2697
|
+
type: "FeatureCollection",
|
|
2698
|
+
features: []
|
|
2699
|
+
};
|
|
2700
|
+
const dirFront = vecNorm(vFront);
|
|
2701
|
+
const frontMidpoint = vecScale(vecAdd(p1, p2), .5);
|
|
2702
|
+
const vToRear = vecSub(p3, frontMidpoint);
|
|
2703
|
+
const perpDir = [-dirFront[1], dirFront[0]];
|
|
2704
|
+
const perpDistance = vToRear[0] * perpDir[0] + vToRear[1] * perpDir[1];
|
|
2705
|
+
const halfFront = vecScale(dirFront, frontLength / 2);
|
|
2706
|
+
const rearMidpoint = vecAdd(frontMidpoint, vecScale(perpDir, perpDistance));
|
|
2707
|
+
const rearTop = vecSub(rearMidpoint, halfFront);
|
|
2708
|
+
const rearBottom = vecAdd(rearMidpoint, halfFront);
|
|
2709
|
+
const rearLineRotation = Math.atan2(dirFront[1], dirFront[0]);
|
|
2710
|
+
const renderedLabelRotation = Math.PI - (rearLineRotation - Math.PI / 2);
|
|
2711
|
+
const labelRotation = normalizeRadians(Math.PI - keepTextLeftToRight(renderedLabelRotation));
|
|
2712
|
+
const gapHalf = Math.min(frontLength / 2, labelKnockoutHalfExtent(config.labelText, dirFront, labelRotation, config.labelOptions ?? {}, context));
|
|
2713
|
+
const gapPt1 = vecAdd(rearMidpoint, vecScale(dirFront, -gapHalf));
|
|
2714
|
+
const gapPt2 = vecAdd(rearMidpoint, vecScale(dirFront, gapHalf));
|
|
2715
|
+
const [topCap, bottomCap] = config.buildEndCaps({
|
|
2716
|
+
p1,
|
|
2717
|
+
p2,
|
|
2718
|
+
dirFront,
|
|
2719
|
+
perpDir,
|
|
2720
|
+
frontLength,
|
|
2721
|
+
rearTop
|
|
2722
|
+
});
|
|
2723
|
+
return {
|
|
2724
|
+
type: "FeatureCollection",
|
|
2725
|
+
features: [{
|
|
2726
|
+
type: "Feature",
|
|
2727
|
+
properties: { part: config.part },
|
|
2728
|
+
geometry: {
|
|
2729
|
+
type: "MultiLineString",
|
|
2730
|
+
coordinates: [
|
|
2731
|
+
topCap,
|
|
2732
|
+
[
|
|
2733
|
+
unproject(p1[0], p1[1]),
|
|
2734
|
+
unproject(rearTop[0], rearTop[1]),
|
|
2735
|
+
unproject(gapPt1[0], gapPt1[1])
|
|
2736
|
+
],
|
|
2737
|
+
[
|
|
2738
|
+
unproject(gapPt2[0], gapPt2[1]),
|
|
2739
|
+
unproject(rearBottom[0], rearBottom[1]),
|
|
2740
|
+
unproject(p2[0], p2[1])
|
|
2741
|
+
],
|
|
2742
|
+
bottomCap
|
|
2743
|
+
]
|
|
2744
|
+
}
|
|
2745
|
+
}, createLabelFeature(rearMidpoint, config.labelText, labelRotation, config.labelOptions)]
|
|
2746
|
+
};
|
|
2747
|
+
}
|
|
2748
|
+
/** The single sized dimension every cross-symbol mission task shares. */
|
|
2749
|
+
const CROSS_TASK_SIZE_PAIR = {
|
|
2750
|
+
id: "size",
|
|
2751
|
+
label: "Size",
|
|
2752
|
+
pixels: "sizePixels",
|
|
2753
|
+
meters: "sizeMeters",
|
|
2754
|
+
defaultUnit: "px",
|
|
2755
|
+
rendering: "geometry",
|
|
2756
|
+
fallbackMeters: 1e3
|
|
2757
|
+
};
|
|
2758
|
+
const DEFAULT_CROSS_TASK_OPTIONS = {
|
|
2759
|
+
sizePixels: 80,
|
|
2760
|
+
rotation: 0,
|
|
2761
|
+
crossAngle: 2 * Math.atan(.55) * 180 / Math.PI,
|
|
2762
|
+
labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING
|
|
2763
|
+
};
|
|
2764
|
+
const CROSS_TASK_PARAMS = [
|
|
2765
|
+
{
|
|
2766
|
+
key: "crossAngle",
|
|
2767
|
+
presentationTier: "advanced",
|
|
2768
|
+
label: "Cross angle",
|
|
2769
|
+
description: "Opening angle between the two right-hand arms. Changes height while preserving width.",
|
|
2770
|
+
type: "number",
|
|
2771
|
+
min: 10,
|
|
2772
|
+
max: 150,
|
|
2773
|
+
step: 1,
|
|
2774
|
+
unit: "°"
|
|
2775
|
+
},
|
|
2776
|
+
{
|
|
2777
|
+
key: "sizePixels",
|
|
2778
|
+
label: "Size",
|
|
2779
|
+
description: "Width of the complete symbol in screen pixels.",
|
|
2780
|
+
type: "number",
|
|
2781
|
+
min: 16,
|
|
2782
|
+
max: 400,
|
|
2783
|
+
step: 1,
|
|
2784
|
+
unit: "px"
|
|
2785
|
+
},
|
|
2786
|
+
{
|
|
2787
|
+
key: "labelPadding",
|
|
2788
|
+
presentationTier: "advanced",
|
|
2789
|
+
label: "Label padding",
|
|
2790
|
+
description: "Extra clearance around the doctrinal letter, as a ratio of its resolved height.",
|
|
2791
|
+
type: "number",
|
|
2792
|
+
min: 0,
|
|
2793
|
+
max: 2,
|
|
2794
|
+
step: .05
|
|
2795
|
+
},
|
|
2796
|
+
{
|
|
2797
|
+
key: "sizeMeters",
|
|
2798
|
+
label: "Size",
|
|
2799
|
+
description: "Overall width of the static symbol.",
|
|
2800
|
+
type: "number",
|
|
2801
|
+
min: 10,
|
|
2802
|
+
max: 1e5,
|
|
2803
|
+
step: 10,
|
|
2804
|
+
unit: "m"
|
|
2805
|
+
},
|
|
2806
|
+
{
|
|
2807
|
+
key: "rotation",
|
|
2808
|
+
label: "Rotation",
|
|
2809
|
+
description: "Clockwise rotation of the static symbol.",
|
|
2810
|
+
type: "number",
|
|
2811
|
+
min: 0,
|
|
2812
|
+
max: 360,
|
|
2813
|
+
step: 1,
|
|
2814
|
+
unit: "°"
|
|
2815
|
+
}
|
|
2816
|
+
];
|
|
2817
|
+
function transformCrossTaskOptions(options, { scale, rotationRadians }) {
|
|
2818
|
+
const patch = {};
|
|
2819
|
+
if (scale !== 1 && Number.isFinite(scale) && scale > 0) if (options.sizePixels !== void 0 || options.sizeMeters === void 0) patch.sizePixels = (options.sizePixels ?? DEFAULT_CROSS_TASK_OPTIONS.sizePixels) * scale;
|
|
2820
|
+
else patch.sizeMeters = options.sizeMeters * scale;
|
|
2821
|
+
if (rotationRadians !== 0 && Number.isFinite(rotationRadians)) patch.rotation = normalizeDegrees((options.rotation ?? 0) + rotationRadians * 180 / Math.PI);
|
|
2822
|
+
return Object.keys(patch).length ? patch : void 0;
|
|
2823
|
+
}
|
|
2824
|
+
/** The doctrinal letter each cross task centers. */
|
|
2825
|
+
const CROSS_TASK_GLYPHS = {
|
|
2826
|
+
destroy: "D",
|
|
2827
|
+
defeat: "D",
|
|
2828
|
+
neutralize: "N",
|
|
2829
|
+
suppress: "S"
|
|
2830
|
+
};
|
|
2831
|
+
/** Milsymbol 3.0.4, iconparts/tactical-points.js: TP.DESTROY / TP.NEUTRALIZE / TP.SUPPRESS.
|
|
2832
|
+
* Default to the 200 × 110 cross and 45-unit glyph; crossAngle adjusts its height. The center gap
|
|
2833
|
+
* follows resolved text metrics and padding rather than milsymbol’s fixed 30 units.
|
|
2834
|
+
* SVG's y=103 text anchor compensates for its font baseline; our labels are centered.
|
|
2835
|
+
*/
|
|
2836
|
+
function createCrossTaskSymbol(coordinates, options, kind, context = {}) {
|
|
2837
|
+
const anchor = coordinates[0];
|
|
2838
|
+
if (!anchor) return {
|
|
2839
|
+
type: "FeatureCollection",
|
|
2840
|
+
features: []
|
|
2841
|
+
};
|
|
2842
|
+
const center = project(anchor[0], anchor[1]);
|
|
2843
|
+
const pixels = options.sizePixels ?? (options.sizeMeters === void 0 ? DEFAULT_CROSS_TASK_OPTIONS.sizePixels : void 0);
|
|
2844
|
+
const width = pixels !== void 0 && options.metersPerPixel !== void 0 && options.metersPerPixel > 0 ? pixels * options.metersPerPixel : options.sizeMeters ?? 1e3;
|
|
2845
|
+
if (!Number.isFinite(width) || width <= 0) return {
|
|
2846
|
+
type: "FeatureCollection",
|
|
2847
|
+
features: []
|
|
2848
|
+
};
|
|
2849
|
+
const scale = width / 200;
|
|
2850
|
+
const rotation = (options.rotation ?? 0) * Math.PI / 180;
|
|
2851
|
+
const cos = Math.cos(rotation);
|
|
2852
|
+
const sin = Math.sin(rotation);
|
|
2853
|
+
const local = (x, y) => unproject(center[0] + scale * (x * cos + y * sin), center[1] + scale * (-x * sin + y * cos));
|
|
2854
|
+
const glyph = CROSS_TASK_GLYPHS[kind];
|
|
2855
|
+
const labelOptions = {
|
|
2856
|
+
labelSize: 45 * scale,
|
|
2857
|
+
labelPadding: options.labelPadding
|
|
2858
|
+
};
|
|
2859
|
+
const labelContext = {
|
|
2860
|
+
...context,
|
|
2861
|
+
labelSizeClampCssPixels: {
|
|
2862
|
+
min: 0,
|
|
2863
|
+
max: Number.MAX_VALUE
|
|
2864
|
+
}
|
|
2865
|
+
};
|
|
2866
|
+
const requestedAngle = options.crossAngle ?? DEFAULT_CROSS_TASK_OPTIONS.crossAngle;
|
|
2867
|
+
const angle = Number.isFinite(requestedAngle) ? Math.min(150, Math.max(10, requestedAngle)) : DEFAULT_CROSS_TASK_OPTIONS.crossAngle;
|
|
2868
|
+
const crossSlope = Math.tan(angle * Math.PI / 360);
|
|
2869
|
+
const diagonalScale = Math.hypot(1, crossSlope);
|
|
2870
|
+
const gap = Math.min(100, labelKnockoutHalfExtent(glyph, [1 / diagonalScale, crossSlope / diagonalScale], Math.PI, labelOptions, labelContext, DEFAULT_CROSS_TASK_OPTIONS.labelPadding) / (scale * diagonalScale));
|
|
2871
|
+
const label = createLabelFeature(center, glyph, Math.PI - rotation, labelOptions);
|
|
2872
|
+
if (pixels !== void 0) {
|
|
2873
|
+
delete label.properties.textSizeMeters;
|
|
2874
|
+
label.properties.textSizePixels = pixels * 45 / 200;
|
|
2875
|
+
} else label.properties.textMaxSizePixels = Number.MAX_SAFE_INTEGER;
|
|
2876
|
+
const resolution = context.constructionMetersPerCssPixel ?? options.metersPerPixel;
|
|
2877
|
+
const strokeWidth = (pixels ?? (resolution && resolution > 0 ? width / resolution : 80)) * (kind === "defeat" ? 8 : 4) / 200;
|
|
2878
|
+
if (gap >= 100) return {
|
|
2879
|
+
type: "FeatureCollection",
|
|
2880
|
+
features: [label]
|
|
2881
|
+
};
|
|
2882
|
+
if (kind === "defeat") {
|
|
2883
|
+
const shafts = [];
|
|
2884
|
+
const heads = [];
|
|
2885
|
+
const headLength = Math.min(32, (100 - gap) * diagonalScale * .65);
|
|
2886
|
+
const halfHeadWidth = headLength * .56;
|
|
2887
|
+
for (const xSign of [-1, 1]) for (const ySign of [-1, 1]) {
|
|
2888
|
+
const ux = xSign / diagonalScale;
|
|
2889
|
+
const uy = ySign * crossSlope / diagonalScale;
|
|
2890
|
+
const tipX = xSign * gap;
|
|
2891
|
+
const tipY = ySign * gap * crossSlope;
|
|
2892
|
+
const baseX = tipX + ux * headLength;
|
|
2893
|
+
const baseY = tipY + uy * headLength;
|
|
2894
|
+
shafts.push([local(xSign * 100, ySign * 100 * crossSlope), local(baseX, baseY)]);
|
|
2895
|
+
const tip = local(tipX, tipY);
|
|
2896
|
+
heads.push([[
|
|
2897
|
+
tip,
|
|
2898
|
+
local(baseX - uy * halfHeadWidth, baseY + ux * halfHeadWidth),
|
|
2899
|
+
local(baseX + uy * halfHeadWidth, baseY - ux * halfHeadWidth),
|
|
2900
|
+
tip
|
|
2901
|
+
]]);
|
|
2902
|
+
}
|
|
2903
|
+
return {
|
|
2904
|
+
type: "FeatureCollection",
|
|
2905
|
+
features: [
|
|
2906
|
+
{
|
|
2907
|
+
type: "Feature",
|
|
2908
|
+
properties: {
|
|
2909
|
+
part: "defeat",
|
|
2910
|
+
style: { strokeWidth }
|
|
2911
|
+
},
|
|
2912
|
+
geometry: {
|
|
2913
|
+
type: "MultiLineString",
|
|
2914
|
+
coordinates: shafts
|
|
2915
|
+
}
|
|
2916
|
+
},
|
|
2917
|
+
{
|
|
2918
|
+
type: "Feature",
|
|
2919
|
+
properties: {
|
|
2920
|
+
part: "arrowheads",
|
|
2921
|
+
fill: true,
|
|
2922
|
+
style: {
|
|
2923
|
+
...SOLID_ACCENT_FILL,
|
|
2924
|
+
strokeWidth: 0,
|
|
2925
|
+
strokeDash: []
|
|
2926
|
+
}
|
|
2927
|
+
},
|
|
2928
|
+
geometry: {
|
|
2929
|
+
type: "MultiPolygon",
|
|
2930
|
+
coordinates: heads
|
|
2931
|
+
}
|
|
2932
|
+
},
|
|
2933
|
+
label
|
|
2934
|
+
]
|
|
2935
|
+
};
|
|
2936
|
+
}
|
|
2937
|
+
const dashLength = 24;
|
|
2938
|
+
const diagonalLines = (slope, dashed) => {
|
|
2939
|
+
const lines = [];
|
|
2940
|
+
for (const [start, end] of [[gap, 100], [-100, -gap]]) {
|
|
2941
|
+
if (!dashed) {
|
|
2942
|
+
lines.push([local(start, start * slope), local(end, end * slope)]);
|
|
2943
|
+
continue;
|
|
2944
|
+
}
|
|
2945
|
+
const length = (end - start) * diagonalScale;
|
|
2946
|
+
for (let distance = 0; distance < length; distance += 34) {
|
|
2947
|
+
const x1 = start + distance / diagonalScale;
|
|
2948
|
+
const x2 = start + Math.min(distance + dashLength, length) / diagonalScale;
|
|
2949
|
+
lines.push([local(x1, x1 * slope), local(x2, x2 * slope)]);
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
return lines;
|
|
2953
|
+
};
|
|
2954
|
+
return {
|
|
2955
|
+
type: "FeatureCollection",
|
|
2956
|
+
features: [
|
|
2957
|
+
{
|
|
2958
|
+
type: "Feature",
|
|
2959
|
+
properties: {
|
|
2960
|
+
part: kind,
|
|
2961
|
+
style: {
|
|
2962
|
+
strokeWidth,
|
|
2963
|
+
...kind === "suppress" ? { strokeDash: [] } : {}
|
|
2964
|
+
}
|
|
2965
|
+
},
|
|
2966
|
+
geometry: {
|
|
2967
|
+
type: "MultiLineString",
|
|
2968
|
+
coordinates: diagonalLines(-crossSlope, kind === "suppress")
|
|
2969
|
+
}
|
|
2970
|
+
},
|
|
2971
|
+
{
|
|
2972
|
+
type: "Feature",
|
|
2973
|
+
properties: {
|
|
2974
|
+
part: `${kind}-rising`,
|
|
2975
|
+
style: {
|
|
2976
|
+
strokeWidth,
|
|
2977
|
+
...kind !== "destroy" ? { strokeDash: [] } : {}
|
|
2978
|
+
}
|
|
2979
|
+
},
|
|
2980
|
+
geometry: {
|
|
2981
|
+
type: "MultiLineString",
|
|
2982
|
+
coordinates: diagonalLines(crossSlope, kind !== "destroy")
|
|
2983
|
+
}
|
|
2984
|
+
},
|
|
2985
|
+
label
|
|
2986
|
+
]
|
|
2987
|
+
};
|
|
2988
|
+
}
|
|
2989
|
+
//#endregion
|
|
2990
|
+
//#region src/generators/cm34-mission-tasks/defeat.ts
|
|
2991
|
+
const DEFAULT_DEFEAT_OPTIONS = {
|
|
2992
|
+
...DEFAULT_CROSS_TASK_OPTIONS,
|
|
2993
|
+
crossAngle: 80
|
|
2994
|
+
};
|
|
2995
|
+
const DEFEAT_METADATA = {
|
|
2996
|
+
id: "defeat",
|
|
2997
|
+
sizePairs: [CROSS_TASK_SIZE_PAIR],
|
|
2998
|
+
name: "Defeat",
|
|
2999
|
+
description: "Renders an enemy force unable or unwilling to achieve its objectives.",
|
|
3000
|
+
entity: "Mission Tasks",
|
|
3001
|
+
entityType: "Defeat",
|
|
3002
|
+
value: "344300",
|
|
3003
|
+
minCoordinates: 1,
|
|
3004
|
+
maxCoordinates: 1,
|
|
3005
|
+
geometry: "point",
|
|
3006
|
+
geometryTypes: [
|
|
3007
|
+
"MultiLineString",
|
|
3008
|
+
"MultiPolygon",
|
|
3009
|
+
"Point"
|
|
3010
|
+
],
|
|
3011
|
+
paints: {
|
|
3012
|
+
stroke: true,
|
|
3013
|
+
fill: "fixed",
|
|
3014
|
+
text: true
|
|
3015
|
+
},
|
|
3016
|
+
drawRule: "Point2",
|
|
3017
|
+
uniformSizing: true,
|
|
3018
|
+
labelGeometryUsesResolvedMetrics: true,
|
|
3019
|
+
params: CROSS_TASK_PARAMS
|
|
3020
|
+
};
|
|
3021
|
+
/** Generates the Defeat mission task (344300) around its sole center anchor. */
|
|
3022
|
+
function createDefeatSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
|
|
3023
|
+
return createCrossTaskSymbol(coordinates, {
|
|
3024
|
+
...options,
|
|
3025
|
+
crossAngle: Number.isFinite(options.crossAngle) ? options.crossAngle : DEFAULT_DEFEAT_OPTIONS.crossAngle
|
|
3026
|
+
}, "defeat", context);
|
|
3027
|
+
}
|
|
3028
|
+
const DEFEAT = defineControlMeasure({
|
|
3029
|
+
metadata: DEFEAT_METADATA,
|
|
3030
|
+
generator: createDefeatSymbol,
|
|
3031
|
+
defaultOptions: DEFAULT_DEFEAT_OPTIONS,
|
|
3032
|
+
rule: staticPointDrawRule,
|
|
3033
|
+
transformOptions: transformCrossTaskOptions,
|
|
3034
|
+
previewSample: {
|
|
3035
|
+
controlPoints: [[0, 0]],
|
|
3036
|
+
options: { sizeMeters: 1e3 }
|
|
3037
|
+
}
|
|
3038
|
+
});
|
|
3039
|
+
//#endregion
|
|
3040
|
+
//#region src/generators/cm34-mission-tasks/suppress.ts
|
|
3041
|
+
const DEFAULT_SUPPRESS_OPTIONS = DEFAULT_CROSS_TASK_OPTIONS;
|
|
3042
|
+
const SUPPRESS_METADATA = {
|
|
3043
|
+
id: "suppress",
|
|
3044
|
+
sizePairs: [CROSS_TASK_SIZE_PAIR],
|
|
3045
|
+
name: "Suppress",
|
|
3046
|
+
description: "Temporarily degrades an enemy force or weapon system below the level needed to accomplish its mission.",
|
|
3047
|
+
entity: "Mission Tasks",
|
|
3048
|
+
entityType: "Suppress",
|
|
3049
|
+
value: "342800",
|
|
3050
|
+
minCoordinates: 1,
|
|
3051
|
+
maxCoordinates: 1,
|
|
3052
|
+
geometry: "point",
|
|
3053
|
+
geometryTypes: ["MultiLineString", "Point"],
|
|
3054
|
+
paints: {
|
|
3055
|
+
stroke: true,
|
|
3056
|
+
fill: "none",
|
|
3057
|
+
text: true
|
|
3058
|
+
},
|
|
3059
|
+
drawRule: "Point2",
|
|
3060
|
+
uniformSizing: true,
|
|
3061
|
+
labelGeometryUsesResolvedMetrics: true,
|
|
3062
|
+
params: CROSS_TASK_PARAMS
|
|
3063
|
+
};
|
|
3064
|
+
/** Generates the Suppress mission task (342800) around its sole center anchor. */
|
|
3065
|
+
function createSuppressSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
|
|
3066
|
+
return createCrossTaskSymbol(coordinates, options, "suppress", context);
|
|
3067
|
+
}
|
|
3068
|
+
const SUPPRESS = defineControlMeasure({
|
|
3069
|
+
metadata: SUPPRESS_METADATA,
|
|
3070
|
+
generator: createSuppressSymbol,
|
|
3071
|
+
defaultOptions: DEFAULT_SUPPRESS_OPTIONS,
|
|
3072
|
+
rule: staticPointDrawRule,
|
|
3073
|
+
transformOptions: transformCrossTaskOptions,
|
|
3074
|
+
previewSample: {
|
|
3075
|
+
controlPoints: [[0, 0]],
|
|
3076
|
+
options: { sizeMeters: 1e3 }
|
|
3077
|
+
}
|
|
3078
|
+
});
|
|
3079
|
+
//#endregion
|
|
3080
|
+
//#region src/generators/cm34-mission-tasks/neutralize.ts
|
|
3081
|
+
const DEFAULT_NEUTRALIZE_OPTIONS = DEFAULT_CROSS_TASK_OPTIONS;
|
|
3082
|
+
const NEUTRALIZE_METADATA = {
|
|
3083
|
+
id: "neutralize",
|
|
3084
|
+
sizePairs: [CROSS_TASK_SIZE_PAIR],
|
|
3085
|
+
name: "Neutralize",
|
|
3086
|
+
description: "Renders enemy personnel or materiel incapable of interfering with an operation.",
|
|
3087
|
+
entity: "Mission Tasks",
|
|
3088
|
+
entityType: "Neutralize",
|
|
3089
|
+
value: "341600",
|
|
3090
|
+
minCoordinates: 1,
|
|
3091
|
+
maxCoordinates: 1,
|
|
3092
|
+
geometry: "point",
|
|
3093
|
+
geometryTypes: ["MultiLineString", "Point"],
|
|
3094
|
+
paints: {
|
|
3095
|
+
stroke: true,
|
|
3096
|
+
fill: "none",
|
|
3097
|
+
text: true
|
|
3098
|
+
},
|
|
3099
|
+
drawRule: "Point2",
|
|
3100
|
+
uniformSizing: true,
|
|
3101
|
+
labelGeometryUsesResolvedMetrics: true,
|
|
3102
|
+
params: CROSS_TASK_PARAMS
|
|
3103
|
+
};
|
|
3104
|
+
/** Generates the Neutralize mission task (341600) around its sole center anchor. */
|
|
3105
|
+
function createNeutralizeSymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
|
|
3106
|
+
return createCrossTaskSymbol(coordinates, options, "neutralize", context);
|
|
3107
|
+
}
|
|
3108
|
+
const NEUTRALIZE = defineControlMeasure({
|
|
3109
|
+
metadata: NEUTRALIZE_METADATA,
|
|
3110
|
+
generator: createNeutralizeSymbol,
|
|
3111
|
+
defaultOptions: DEFAULT_NEUTRALIZE_OPTIONS,
|
|
3112
|
+
rule: staticPointDrawRule,
|
|
3113
|
+
transformOptions: transformCrossTaskOptions,
|
|
3114
|
+
previewSample: {
|
|
3115
|
+
controlPoints: [[0, 0]],
|
|
3116
|
+
options: { sizeMeters: 1e3 }
|
|
3117
|
+
}
|
|
3118
|
+
});
|
|
3119
|
+
//#endregion
|
|
3120
|
+
//#region src/generators/cm34-mission-tasks/destroy.ts
|
|
3121
|
+
const DEFAULT_DESTROY_OPTIONS = DEFAULT_CROSS_TASK_OPTIONS;
|
|
3122
|
+
const DESTROY_METADATA = {
|
|
3123
|
+
id: "destroy",
|
|
3124
|
+
sizePairs: [CROSS_TASK_SIZE_PAIR],
|
|
3125
|
+
name: "Destroy",
|
|
3126
|
+
description: "Renders an enemy force or capability physically combat-ineffective until reconstituted.",
|
|
3127
|
+
entity: "Mission Tasks",
|
|
3128
|
+
entityType: "Destroy",
|
|
3129
|
+
value: "340900",
|
|
3130
|
+
minCoordinates: 1,
|
|
3131
|
+
maxCoordinates: 1,
|
|
3132
|
+
geometry: "point",
|
|
3133
|
+
geometryTypes: ["MultiLineString", "Point"],
|
|
3134
|
+
paints: {
|
|
3135
|
+
stroke: true,
|
|
3136
|
+
fill: "none",
|
|
3137
|
+
text: true
|
|
3138
|
+
},
|
|
3139
|
+
drawRule: "Point2",
|
|
3140
|
+
uniformSizing: true,
|
|
3141
|
+
labelGeometryUsesResolvedMetrics: true,
|
|
3142
|
+
params: CROSS_TASK_PARAMS
|
|
3143
|
+
};
|
|
3144
|
+
/** Generates the Destroy mission task (340900) around its sole center anchor. */
|
|
3145
|
+
function createDestroySymbol(coordinates, options = {}, _textAmplifiers = {}, context = {}) {
|
|
3146
|
+
return createCrossTaskSymbol(coordinates, options, "destroy", context);
|
|
3147
|
+
}
|
|
3148
|
+
const DESTROY = defineControlMeasure({
|
|
3149
|
+
metadata: DESTROY_METADATA,
|
|
3150
|
+
generator: createDestroySymbol,
|
|
3151
|
+
defaultOptions: DEFAULT_DESTROY_OPTIONS,
|
|
3152
|
+
rule: staticPointDrawRule,
|
|
3153
|
+
transformOptions: transformCrossTaskOptions,
|
|
3154
|
+
previewSample: {
|
|
3155
|
+
controlPoints: [[0, 0]],
|
|
3156
|
+
options: { sizeMeters: 1e3 }
|
|
3157
|
+
}
|
|
3158
|
+
});
|
|
3159
|
+
//#endregion
|
|
3160
|
+
//#region src/internal/echelon.ts
|
|
3161
|
+
/**
|
|
3162
|
+
* APP-6 echelon (Field B) glyph vocabulary, shared by the control measures that
|
|
3163
|
+
* stamp a unit-size amplifier onto their geometry (Boundary, Battle Position).
|
|
3164
|
+
*
|
|
3165
|
+
* A compact port of the APP-6 echelon designs, modeled on milsymbol but
|
|
3166
|
+
* expressed as parametric geometry so it bakes into GeoJSON and strokes
|
|
3167
|
+
* monocolor (ADR-0023). Every glyph is built from three primitives — vertical
|
|
3168
|
+
* bars, crosses, and dots — plus the team/crew ring and command `++`.
|
|
3169
|
+
*/
|
|
3170
|
+
/** Shared echelon dimension for every measure that stamps a unit-size amplifier. */
|
|
3171
|
+
const ECHELON_SIZE_PAIR = {
|
|
3172
|
+
id: "echelon",
|
|
3173
|
+
label: "Echelon size",
|
|
3174
|
+
pixels: "echelonSizePixels",
|
|
3175
|
+
meters: "echelonSize",
|
|
3176
|
+
defaultUnit: "m",
|
|
3177
|
+
rendering: "geometry"
|
|
3178
|
+
};
|
|
3179
|
+
const ECHELONS = {
|
|
3180
|
+
team: {
|
|
3181
|
+
kind: "ring",
|
|
3182
|
+
count: 1,
|
|
3183
|
+
label: "Team/Crew"
|
|
3184
|
+
},
|
|
3185
|
+
crew: {
|
|
3186
|
+
kind: "ring",
|
|
3187
|
+
count: 1
|
|
3188
|
+
},
|
|
3189
|
+
squad: {
|
|
3190
|
+
kind: "dots",
|
|
3191
|
+
count: 1,
|
|
3192
|
+
label: "Squad"
|
|
3193
|
+
},
|
|
3194
|
+
section: {
|
|
3195
|
+
kind: "dots",
|
|
3196
|
+
count: 2,
|
|
3197
|
+
label: "Section"
|
|
3198
|
+
},
|
|
3199
|
+
platoon: {
|
|
3200
|
+
kind: "dots",
|
|
3201
|
+
count: 3,
|
|
3202
|
+
label: "Platoon"
|
|
3203
|
+
},
|
|
3204
|
+
company: {
|
|
3205
|
+
kind: "bars",
|
|
3206
|
+
count: 1,
|
|
3207
|
+
label: "Company"
|
|
3208
|
+
},
|
|
3209
|
+
battalion: {
|
|
3210
|
+
kind: "bars",
|
|
3211
|
+
count: 2,
|
|
3212
|
+
label: "Battalion"
|
|
3213
|
+
},
|
|
3214
|
+
regiment: {
|
|
3215
|
+
kind: "bars",
|
|
3216
|
+
count: 3,
|
|
3217
|
+
label: "Regiment"
|
|
3218
|
+
},
|
|
3219
|
+
brigade: {
|
|
3220
|
+
kind: "crosses",
|
|
3221
|
+
count: 1,
|
|
3222
|
+
label: "Brigade"
|
|
3223
|
+
},
|
|
3224
|
+
division: {
|
|
3225
|
+
kind: "crosses",
|
|
3226
|
+
count: 2,
|
|
3227
|
+
label: "Division"
|
|
3228
|
+
},
|
|
3229
|
+
corps: {
|
|
3230
|
+
kind: "crosses",
|
|
3231
|
+
count: 3,
|
|
3232
|
+
label: "Corps"
|
|
3233
|
+
},
|
|
3234
|
+
army: {
|
|
3235
|
+
kind: "crosses",
|
|
3236
|
+
count: 4,
|
|
3237
|
+
label: "Army"
|
|
3238
|
+
},
|
|
3239
|
+
"army-group": {
|
|
3240
|
+
kind: "crosses",
|
|
3241
|
+
count: 5,
|
|
3242
|
+
label: "Army Group"
|
|
3243
|
+
},
|
|
3244
|
+
region: {
|
|
3245
|
+
kind: "crosses",
|
|
3246
|
+
count: 6,
|
|
3247
|
+
label: "Region"
|
|
3248
|
+
},
|
|
3249
|
+
command: {
|
|
3250
|
+
kind: "command",
|
|
3251
|
+
count: 2,
|
|
3252
|
+
label: "Command"
|
|
3253
|
+
}
|
|
3254
|
+
};
|
|
3255
|
+
/** Symbol-form aliases (`"XX"`, `"II"`, `"•••"`, …) → canonical echelon key. */
|
|
3256
|
+
const ECHELON_ALIASES = {
|
|
3257
|
+
ø: "team",
|
|
3258
|
+
"•": "squad",
|
|
3259
|
+
"••": "section",
|
|
3260
|
+
"•••": "platoon",
|
|
3261
|
+
i: "company",
|
|
3262
|
+
ii: "battalion",
|
|
3263
|
+
iii: "regiment",
|
|
3264
|
+
x: "brigade",
|
|
3265
|
+
xx: "division",
|
|
3266
|
+
xxx: "corps",
|
|
3267
|
+
xxxx: "army",
|
|
3268
|
+
xxxxx: "army-group",
|
|
3269
|
+
xxxxxx: "region",
|
|
3270
|
+
"++": "command"
|
|
3271
|
+
};
|
|
3272
|
+
/** Enum options for the params panel, derived from {@link ECHELONS}. */
|
|
3273
|
+
const ECHELON_PARAM_OPTIONS = [{
|
|
3274
|
+
label: "None",
|
|
3275
|
+
value: "none"
|
|
3276
|
+
}, ...Object.entries(ECHELONS).filter(([, spec]) => spec.label !== void 0).map(([value, spec]) => ({
|
|
3277
|
+
label: spec.label,
|
|
3278
|
+
value
|
|
3279
|
+
}))];
|
|
3280
|
+
/**
|
|
3281
|
+
* Effective echelon glyph height in meters. Screen-anchored when a pixel size
|
|
3282
|
+
* is supplied with a live resolution; ground-anchored (meters) otherwise.
|
|
3283
|
+
* Clamped strictly positive.
|
|
3284
|
+
*/
|
|
3285
|
+
function resolveEchelonHeight(echelonSize, echelonSizePixels, metersPerPixel) {
|
|
3286
|
+
let h = echelonSize;
|
|
3287
|
+
if (echelonSizePixels !== void 0 && metersPerPixel !== void 0 && metersPerPixel > 0) h = echelonSizePixels * metersPerPixel;
|
|
3288
|
+
return Math.max(EPSILON, h);
|
|
3289
|
+
}
|
|
3290
|
+
function resolveEchelon(echelon) {
|
|
3291
|
+
const key = echelon.trim().toLowerCase();
|
|
3292
|
+
if (key.length === 0) return void 0;
|
|
3293
|
+
const alias = ECHELON_ALIASES[key];
|
|
3294
|
+
return ECHELONS[key] ?? (alias ? ECHELONS[alias] : void 0);
|
|
3295
|
+
}
|
|
3296
|
+
/**
|
|
3297
|
+
* Builds the echelon glyph centered at `center`, with its baseline along
|
|
3298
|
+
* `along` and `perp` (both unit vectors), `h` meters tall. Returns the glyph's
|
|
3299
|
+
* `strokes` (open polylines), `fills` (closed rings drawn as solid shapes —
|
|
3300
|
+
* the squad/section/platoon dots), and its intrinsic width along the line; the
|
|
3301
|
+
* caller adds the configurable side padding to derive the line gap.
|
|
3302
|
+
*/
|
|
3303
|
+
function buildEchelonGlyph(center, along, perp, h, spec) {
|
|
3304
|
+
const P = (a, p) => {
|
|
3305
|
+
const q = vecAdd(vecAdd(center, vecScale(along, a)), vecScale(perp, p));
|
|
3306
|
+
return unproject(q[0], q[1]);
|
|
3307
|
+
};
|
|
3308
|
+
const ring = (a, r) => {
|
|
3309
|
+
const pts = [];
|
|
3310
|
+
const steps = 16;
|
|
3311
|
+
for (let i = 0; i <= steps; i++) {
|
|
3312
|
+
const t = 2 * Math.PI * i / steps;
|
|
3313
|
+
pts.push(P(a + r * Math.cos(t), r * Math.sin(t)));
|
|
3314
|
+
}
|
|
3315
|
+
return pts;
|
|
3316
|
+
};
|
|
3317
|
+
const strokes = [];
|
|
3318
|
+
const fills = [];
|
|
3319
|
+
const half = h / 2;
|
|
3320
|
+
switch (spec.kind) {
|
|
3321
|
+
case "bars": {
|
|
3322
|
+
const spacing = h * .8;
|
|
3323
|
+
const total = (spec.count - 1) * spacing;
|
|
3324
|
+
for (let j = 0; j < spec.count; j++) {
|
|
3325
|
+
const a = -total / 2 + j * spacing;
|
|
3326
|
+
strokes.push([P(a, -half), P(a, half)]);
|
|
3327
|
+
}
|
|
3328
|
+
return {
|
|
3329
|
+
strokes,
|
|
3330
|
+
fills,
|
|
3331
|
+
width: total
|
|
3332
|
+
};
|
|
3333
|
+
}
|
|
3334
|
+
case "crosses": {
|
|
3335
|
+
const w = h;
|
|
3336
|
+
const spacing = h * 1.4;
|
|
3337
|
+
const total = (spec.count - 1) * spacing;
|
|
3338
|
+
for (let j = 0; j < spec.count; j++) {
|
|
3339
|
+
const a0 = -total / 2 + j * spacing;
|
|
3340
|
+
strokes.push([P(a0 - w / 2, -half), P(a0 + w / 2, half)]);
|
|
3341
|
+
strokes.push([P(a0 - w / 2, half), P(a0 + w / 2, -half)]);
|
|
3342
|
+
}
|
|
3343
|
+
return {
|
|
3344
|
+
strokes,
|
|
3345
|
+
fills,
|
|
3346
|
+
width: total + w
|
|
3347
|
+
};
|
|
3348
|
+
}
|
|
3349
|
+
case "dots": {
|
|
3350
|
+
const r = h * .3;
|
|
3351
|
+
const spacing = h * 1.2;
|
|
3352
|
+
const total = (spec.count - 1) * spacing;
|
|
3353
|
+
for (let j = 0; j < spec.count; j++) fills.push(ring(-total / 2 + j * spacing, r));
|
|
3354
|
+
return {
|
|
3355
|
+
strokes,
|
|
3356
|
+
fills,
|
|
3357
|
+
width: total + 2 * r
|
|
3358
|
+
};
|
|
3359
|
+
}
|
|
3360
|
+
case "ring": {
|
|
3361
|
+
const r = h * .6;
|
|
3362
|
+
strokes.push(ring(0, r));
|
|
3363
|
+
strokes.push([P(-h * .8, -h * .4), P(h * .8, h * .4)]);
|
|
3364
|
+
return {
|
|
3365
|
+
strokes,
|
|
3366
|
+
fills,
|
|
3367
|
+
width: h * 1.6
|
|
3368
|
+
};
|
|
3369
|
+
}
|
|
3370
|
+
case "command": {
|
|
3371
|
+
const arm = h * .5;
|
|
3372
|
+
const spacing = h * 1.4;
|
|
3373
|
+
const total = (spec.count - 1) * spacing;
|
|
3374
|
+
for (let j = 0; j < spec.count; j++) {
|
|
3375
|
+
const a0 = -total / 2 + j * spacing;
|
|
3376
|
+
strokes.push([P(a0 - arm, 0), P(a0 + arm, 0)]);
|
|
3377
|
+
strokes.push([P(a0, -half), P(a0, half)]);
|
|
3378
|
+
}
|
|
3379
|
+
return {
|
|
3380
|
+
strokes,
|
|
3381
|
+
fills,
|
|
3382
|
+
width: total + 2 * arm
|
|
3383
|
+
};
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3387
|
+
//#endregion
|
|
3388
|
+
//#region src/generators/cm15-maneuver-areas/params.ts
|
|
3389
|
+
const AREA_SMOOTH_PARAMS = [{
|
|
3390
|
+
key: "smooth",
|
|
3391
|
+
label: "Smooth",
|
|
3392
|
+
description: "Round the area's corners by curving the boundary through the control points.",
|
|
3393
|
+
type: "boolean"
|
|
3394
|
+
}, {
|
|
3395
|
+
key: "smoothResolution",
|
|
3396
|
+
presentationTier: "advanced",
|
|
3397
|
+
label: "Smooth resolution",
|
|
3398
|
+
description: "Number of samples per segment when smooth mode is enabled.",
|
|
3399
|
+
type: "number",
|
|
3400
|
+
min: 2,
|
|
3401
|
+
max: 64,
|
|
3402
|
+
step: 1,
|
|
3403
|
+
visibleWhen: (opts) => Boolean(opts.smooth)
|
|
3404
|
+
}];
|
|
3405
|
+
const ATTACK_SHAFT_PARAMS = [
|
|
3406
|
+
{
|
|
3407
|
+
key: "shaftWidthRatio",
|
|
3408
|
+
presentationTier: "advanced",
|
|
3409
|
+
label: "Shaft width",
|
|
3410
|
+
description: "Arrow shaft width as a ratio of the arrowhead width",
|
|
3411
|
+
type: "number",
|
|
3412
|
+
min: .1,
|
|
3413
|
+
max: 1,
|
|
3414
|
+
step: .05
|
|
3415
|
+
},
|
|
3416
|
+
{
|
|
3417
|
+
key: "rearWidthRatio",
|
|
3418
|
+
presentationTier: "advanced",
|
|
3419
|
+
label: "Rear width",
|
|
3420
|
+
description: "Width at the rear of the shaft as a ratio of the arrowhead width.",
|
|
3421
|
+
type: "number",
|
|
3422
|
+
min: .1,
|
|
3423
|
+
max: 2,
|
|
3424
|
+
step: .05
|
|
3425
|
+
},
|
|
3426
|
+
{
|
|
3427
|
+
key: "smooth",
|
|
3428
|
+
label: "Smooth",
|
|
3429
|
+
description: "Round the corners where the shaft changes direction.",
|
|
3430
|
+
type: "boolean"
|
|
3431
|
+
},
|
|
3432
|
+
{
|
|
3433
|
+
key: "smoothResolution",
|
|
3434
|
+
presentationTier: "advanced",
|
|
3435
|
+
label: "Smooth resolution",
|
|
3436
|
+
description: "Number of segments approximating each rounded bend when smooth mode is enabled.",
|
|
3437
|
+
type: "number",
|
|
3438
|
+
min: 1,
|
|
3439
|
+
max: 20,
|
|
3440
|
+
step: 1,
|
|
3441
|
+
visibleWhen: (opts) => opts.smooth === true
|
|
3442
|
+
}
|
|
3443
|
+
];
|
|
3444
|
+
const FIRE_ARROW_PARAMS = [
|
|
3445
|
+
{
|
|
3446
|
+
key: "arrowheadWidthRatio",
|
|
3447
|
+
presentationTier: "advanced",
|
|
3448
|
+
label: "Arrowhead width",
|
|
3449
|
+
description: "Arrowhead width as a ratio of the shaft length",
|
|
3450
|
+
type: "number",
|
|
3451
|
+
min: .05,
|
|
3452
|
+
max: 1,
|
|
3453
|
+
step: .01
|
|
3454
|
+
},
|
|
3455
|
+
{
|
|
3456
|
+
key: "rearLineLengthRatio",
|
|
3457
|
+
presentationTier: "advanced",
|
|
3458
|
+
label: "Rear line length",
|
|
3459
|
+
description: "Length of the rear flare lines as a ratio of the head",
|
|
3460
|
+
type: "number",
|
|
3461
|
+
min: .05,
|
|
3462
|
+
max: 1,
|
|
3463
|
+
step: .01
|
|
3464
|
+
},
|
|
3465
|
+
{
|
|
3466
|
+
key: "rearLineAngle",
|
|
3467
|
+
presentationTier: "advanced",
|
|
3468
|
+
label: "Rear line angle",
|
|
3469
|
+
description: "Angle of the rear flare lines, in degrees",
|
|
3470
|
+
type: "number",
|
|
3471
|
+
min: 0,
|
|
3472
|
+
max: 90,
|
|
3473
|
+
step: 1
|
|
3474
|
+
}
|
|
3475
|
+
];
|
|
3476
|
+
const FORTIFIED_AREA_SIZE_PARAMS = [{
|
|
3477
|
+
key: "toothSizePixels",
|
|
3478
|
+
label: "Tooth size",
|
|
3479
|
+
description: "Size of the fortified-area teeth, in pixels",
|
|
3480
|
+
type: "number",
|
|
3481
|
+
min: 5,
|
|
3482
|
+
max: 50,
|
|
3483
|
+
step: 1,
|
|
3484
|
+
unit: "px"
|
|
3485
|
+
}, {
|
|
3486
|
+
key: "toothSize",
|
|
3487
|
+
label: "Tooth size",
|
|
3488
|
+
description: "Size of the fortified-area teeth, in meters",
|
|
3489
|
+
type: "number",
|
|
3490
|
+
min: 1,
|
|
3491
|
+
max: 500,
|
|
3492
|
+
step: 1,
|
|
3493
|
+
unit: "m"
|
|
3494
|
+
}];
|
|
3495
|
+
const ENCIRCLEMENT_PARAMS = [
|
|
3496
|
+
{
|
|
3497
|
+
key: "smooth",
|
|
3498
|
+
label: "Smooth",
|
|
3499
|
+
description: "Round the boundary by curving it through the control points.",
|
|
3500
|
+
type: "boolean"
|
|
3501
|
+
},
|
|
3502
|
+
{
|
|
3503
|
+
key: "smoothResolution",
|
|
3504
|
+
presentationTier: "advanced",
|
|
3505
|
+
label: "Smooth resolution",
|
|
3506
|
+
description: "Number of samples per segment when smooth mode is enabled.",
|
|
3507
|
+
type: "number",
|
|
3508
|
+
min: 2,
|
|
3509
|
+
max: 64,
|
|
3510
|
+
step: 1,
|
|
3511
|
+
visibleWhen: (opts) => Boolean(opts.smooth)
|
|
3512
|
+
},
|
|
3513
|
+
{
|
|
3514
|
+
key: "barbSpacingRatio",
|
|
3515
|
+
presentationTier: "advanced",
|
|
3516
|
+
label: "Spacing",
|
|
3517
|
+
description: "Spacing between barbs as a ratio of the symbol radius (smaller is denser).",
|
|
3518
|
+
type: "number",
|
|
3519
|
+
min: .15,
|
|
3520
|
+
max: 1.5,
|
|
3521
|
+
step: .05
|
|
3522
|
+
},
|
|
3523
|
+
{
|
|
3524
|
+
key: "barbLengthRatio",
|
|
3525
|
+
presentationTier: "advanced",
|
|
3526
|
+
label: "Barb length",
|
|
3527
|
+
description: "Barb height as a ratio of the symbol radius (amplitude).",
|
|
3528
|
+
type: "number",
|
|
3529
|
+
min: .02,
|
|
3530
|
+
max: .5,
|
|
3531
|
+
step: .01
|
|
3532
|
+
}
|
|
3533
|
+
];
|
|
3534
|
+
const FORTIFIED_AREA_PARAMS = [...AREA_SMOOTH_PARAMS, ...FORTIFIED_AREA_SIZE_PARAMS];
|
|
3535
|
+
const AREA_LABEL_PADDING_PARAM = {
|
|
3536
|
+
key: "labelPadding",
|
|
3537
|
+
presentationTier: "advanced",
|
|
3538
|
+
label: "Label padding",
|
|
3539
|
+
description: "Extra spacing between or around the area's labels, as a ratio of label height",
|
|
3540
|
+
type: "number",
|
|
3541
|
+
min: 0,
|
|
3542
|
+
max: 2,
|
|
3543
|
+
step: .05
|
|
3544
|
+
};
|
|
3545
|
+
const MULTILINE_AREA_LABEL_PARAM = {
|
|
3546
|
+
key: "multilineLabel",
|
|
3547
|
+
label: "Multiline label",
|
|
3548
|
+
description: "Place the doctrinal label above Field T on separate centered lines",
|
|
3549
|
+
type: "boolean"
|
|
3550
|
+
};
|
|
3551
|
+
const LABELED_AREA_PARAMS = [...AREA_SMOOTH_PARAMS, AREA_LABEL_PADDING_PARAM];
|
|
3552
|
+
const PREFIXED_LABELED_AREA_PARAMS = [
|
|
3553
|
+
...AREA_SMOOTH_PARAMS,
|
|
3554
|
+
MULTILINE_AREA_LABEL_PARAM,
|
|
3555
|
+
AREA_LABEL_PADDING_PARAM
|
|
3556
|
+
];
|
|
3557
|
+
const ECHELON_GLYPH_PARAMS = [
|
|
3558
|
+
{
|
|
3559
|
+
key: "echelon",
|
|
3560
|
+
label: "Echelon",
|
|
3561
|
+
description: "Field B unit-size amplifier, drawn on the boundary.",
|
|
3562
|
+
type: "enum",
|
|
3563
|
+
presentationTier: "primary",
|
|
3564
|
+
semanticRole: "doctrinal",
|
|
3565
|
+
options: ECHELON_PARAM_OPTIONS
|
|
3566
|
+
},
|
|
3567
|
+
{
|
|
3568
|
+
key: "echelonSizePixels",
|
|
3569
|
+
label: "Echelon size",
|
|
3570
|
+
description: "Echelon glyph height in screen pixels.",
|
|
3571
|
+
type: "number",
|
|
3572
|
+
min: 8,
|
|
3573
|
+
max: 48,
|
|
3574
|
+
step: 1,
|
|
3575
|
+
unit: "px"
|
|
3576
|
+
},
|
|
3577
|
+
{
|
|
3578
|
+
key: "echelonSize",
|
|
3579
|
+
label: "Echelon size",
|
|
3580
|
+
description: "Echelon glyph height in meters.",
|
|
3581
|
+
type: "number",
|
|
3582
|
+
min: 50,
|
|
3583
|
+
max: 2e4,
|
|
3584
|
+
step: 50,
|
|
3585
|
+
unit: "m"
|
|
3586
|
+
},
|
|
3587
|
+
{
|
|
3588
|
+
key: "echelonPadding",
|
|
3589
|
+
presentationTier: "advanced",
|
|
3590
|
+
label: "Echelon padding",
|
|
3591
|
+
description: "Gap on each side of the echelon, as a ratio of its height.",
|
|
3592
|
+
type: "number",
|
|
3593
|
+
min: 0,
|
|
3594
|
+
max: 2,
|
|
3595
|
+
step: .05
|
|
3596
|
+
},
|
|
3597
|
+
{
|
|
3598
|
+
key: "echelonPosition",
|
|
3599
|
+
presentationTier: "advanced",
|
|
3600
|
+
label: "Echelon position",
|
|
3601
|
+
description: "Slide the echelon along the boundary, as a fraction of the perimeter from the bottom edge.",
|
|
3602
|
+
type: "number",
|
|
3603
|
+
min: -.5,
|
|
3604
|
+
max: .5,
|
|
3605
|
+
step: .01
|
|
3606
|
+
}
|
|
3607
|
+
];
|
|
3608
|
+
const ECHELON_AREA_PARAMS = [
|
|
3609
|
+
...AREA_SMOOTH_PARAMS,
|
|
3610
|
+
...ECHELON_GLYPH_PARAMS,
|
|
3611
|
+
AREA_LABEL_PADDING_PARAM
|
|
3612
|
+
];
|
|
3613
|
+
const PREFIXED_ECHELON_AREA_PARAMS = [
|
|
3614
|
+
...AREA_SMOOTH_PARAMS,
|
|
3615
|
+
MULTILINE_AREA_LABEL_PARAM,
|
|
3616
|
+
...ECHELON_GLYPH_PARAMS,
|
|
3617
|
+
AREA_LABEL_PADDING_PARAM
|
|
3618
|
+
];
|
|
3619
|
+
const AREA_DESIGNATION_AMPLIFIER = {
|
|
3620
|
+
key: "T",
|
|
3621
|
+
label: "Unique designation",
|
|
3622
|
+
description: "Field T — the unit's designation, centered in the area.",
|
|
3623
|
+
maxLength: 21
|
|
3624
|
+
};
|
|
3625
|
+
const AREA_HOSTILE_AMPLIFIER = {
|
|
3626
|
+
key: "N",
|
|
3627
|
+
label: "Hostile (ENY) marker",
|
|
3628
|
+
description: "Field N — literal \"ENY\" marker at the area's west and east edges.",
|
|
3213
3629
|
placeholder: "ENY",
|
|
3214
3630
|
maxLength: 3
|
|
3215
3631
|
};
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3632
|
+
const AREA_TEXT_AMPLIFIERS = [
|
|
3633
|
+
{
|
|
3634
|
+
key: "T",
|
|
3635
|
+
label: "Unique designation",
|
|
3636
|
+
description: "Field T — the area's unique designation.",
|
|
3637
|
+
maxLength: 21
|
|
3638
|
+
},
|
|
3639
|
+
{
|
|
3640
|
+
key: "H",
|
|
3641
|
+
label: "Additional information",
|
|
3642
|
+
description: "Field H — additional free-text information, shown below the designation.",
|
|
3643
|
+
maxLength: 20
|
|
3644
|
+
},
|
|
3645
|
+
{
|
|
3646
|
+
key: "W",
|
|
3647
|
+
label: "Effective from (DTG)",
|
|
3648
|
+
description: "Field W — date-time group the area becomes effective.",
|
|
3649
|
+
placeholder: "051030Z",
|
|
3650
|
+
maxLength: 16
|
|
3651
|
+
},
|
|
3652
|
+
{
|
|
3653
|
+
key: "W1",
|
|
3654
|
+
label: "Effective to (DTG)",
|
|
3655
|
+
description: "Field W1 — date-time group the area ceases to be effective.",
|
|
3656
|
+
maxLength: 16
|
|
3657
|
+
},
|
|
3658
|
+
AREA_HOSTILE_AMPLIFIER
|
|
3659
|
+
];
|
|
3660
|
+
const AREA_UNIT_DESIGNATION_HOSTILE_AMPLIFIERS = [AREA_DESIGNATION_AMPLIFIER, AREA_HOSTILE_AMPLIFIER];
|
|
3661
|
+
/** {@link AREA_TEXT_AMPLIFIERS} without the H (additional information) field,
|
|
3662
|
+
* for measures whose template omits that row but keeps the W/W1 window and
|
|
3663
|
+
* the N (hostile) marker (Submarine Action Area, Submarine-Generated Action
|
|
3664
|
+
* Area). */
|
|
3665
|
+
const AREA_TEXT_AMPLIFIERS_NO_ADDITIONAL_INFO = AREA_TEXT_AMPLIFIERS.filter((d) => d.key !== "H");
|
|
3666
|
+
/** {@link AREA_TEXT_AMPLIFIERS} reduced to just the designation (Field T) and
|
|
3667
|
+
* hostile marker (Field N), for measures whose template carries no
|
|
3668
|
+
* additional-information or date-time-group rows (Area, Drop Zone,
|
|
3669
|
+
* Extraction Zone). */
|
|
3670
|
+
const AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE = AREA_TEXT_AMPLIFIERS.filter((d) => d.key === "T" || d.key === "N");
|
|
3671
|
+
//#endregion
|
|
3672
|
+
//#region src/generators/cm15-maneuver-areas/airborneAttack.ts
|
|
3673
|
+
const DEFAULT_AIRBORNE_ATTACK_OPTIONS = {
|
|
3674
|
+
shaftWidthRatio: DEFAULT_SHAFT_WIDTH_RATIO$1,
|
|
3675
|
+
rearWidthRatio: DEFAULT_REAR_WIDTH_RATIO,
|
|
3676
|
+
smooth: false,
|
|
3677
|
+
smoothResolution: 5
|
|
3678
|
+
};
|
|
3679
|
+
const AIRBORNE_ATTACK_METADATA = {
|
|
3680
|
+
id: "airborne-attack",
|
|
3681
|
+
name: "Airborne Attack",
|
|
3682
|
+
description: "Airborne or aviation axis of advance.",
|
|
3683
|
+
entity: "Maneuver Areas",
|
|
3684
|
+
entityType: "Axis of Advance",
|
|
3685
|
+
entitySubtype: "Airborne Attack",
|
|
3686
|
+
value: "151401",
|
|
3687
|
+
minCoordinates: 3,
|
|
3688
|
+
geometry: "line",
|
|
3689
|
+
geometryTypes: ["LineString"],
|
|
3690
|
+
paints: {
|
|
3691
|
+
stroke: true,
|
|
3692
|
+
fill: "none",
|
|
3693
|
+
text: false
|
|
3694
|
+
},
|
|
3695
|
+
drawRule: "Axis1",
|
|
3696
|
+
params: ATTACK_SHAFT_PARAMS
|
|
3226
3697
|
};
|
|
3227
3698
|
/**
|
|
3228
|
-
*
|
|
3229
|
-
* label feature carries (ADR-0028): `textSizePixels` when screen-anchored
|
|
3230
|
-
* (`labelSizePixels` present — pixels win over a stray `labelSize`),
|
|
3231
|
-
* `textSizeMeters` when ground-anchored (`labelSize` alone), or neither, in
|
|
3232
|
-
* which case adapters render their engine default, screen-fixed.
|
|
3233
|
-
*/
|
|
3234
|
-
function labelSizeProps(options) {
|
|
3235
|
-
const { labelSize, labelSizePixels } = options;
|
|
3236
|
-
if (labelSizePixels !== void 0) return { textSizePixels: labelSizePixels };
|
|
3237
|
-
if (labelSize !== void 0) return { textSizeMeters: labelSize };
|
|
3238
|
-
return {};
|
|
3239
|
-
}
|
|
3240
|
-
/** Pushes a `part: "label"` Point feature at projected `point`. */
|
|
3241
|
-
function pushLabel(out, point, text, rotation, sizeProps, textAnchor, amplifierField, labelPlacementKey) {
|
|
3242
|
-
out.push({
|
|
3243
|
-
type: "Feature",
|
|
3244
|
-
properties: {
|
|
3245
|
-
part: "label",
|
|
3246
|
-
text,
|
|
3247
|
-
rotation,
|
|
3248
|
-
...textAnchor ? { textAnchor } : {},
|
|
3249
|
-
...amplifierField ? { amplifierField } : {},
|
|
3250
|
-
...labelPlacementKey ? { labelPlacementKey } : {},
|
|
3251
|
-
...sizeProps
|
|
3252
|
-
},
|
|
3253
|
-
geometry: {
|
|
3254
|
-
type: "Point",
|
|
3255
|
-
coordinates: unproject(point[0], point[1])
|
|
3256
|
-
}
|
|
3257
|
-
});
|
|
3258
|
-
}
|
|
3259
|
-
/** Ratio of resolved label height (`labelSize`, or `labelSizePixels × metersPerPixel`) used as end-label clearance. */
|
|
3260
|
-
const END_LABEL_CLEARANCE_RATIO = .3;
|
|
3261
|
-
/** Ratio of resolved label height used to inset "above" end labels from the tip. */
|
|
3262
|
-
const ABOVE_LABEL_INSET_RATIO = .3;
|
|
3263
|
-
/** Ratio of resolved label height used as the "above" end labels' perpendicular offset. */
|
|
3264
|
-
const ABOVE_LABEL_OFFSET_RATIO = .6;
|
|
3265
|
-
function textHeight(options) {
|
|
3266
|
-
const { labelSize, labelSizePixels, metersPerPixel } = options;
|
|
3267
|
-
if (labelSizePixels !== void 0) return metersPerPixel !== void 0 && metersPerPixel > 0 ? labelSizePixels * metersPerPixel : void 0;
|
|
3268
|
-
return labelSize;
|
|
3269
|
-
}
|
|
3270
|
-
/**
|
|
3271
|
-
* Label offset in meters: `ratio` × the label's resolved height (`labelSize`
|
|
3272
|
-
* meters, or `labelSizePixels × metersPerPixel` when screen-anchored), or
|
|
3273
|
-
* `fallbackMeters` when no size resolves (e.g. no map resolution yet). The fallback is small
|
|
3274
|
-
* relative to typical control measure spans — the same order of magnitude as
|
|
3275
|
-
* Boundary's own smallest label offsets — so a fixed label still reads as "just
|
|
3276
|
-
* clear of the line" rather than crowding it or floating away. Callers supply
|
|
3277
|
-
* their own ratio (end-label clearance, "above" inset/offset, center-group
|
|
3278
|
-
* offset) but share this single resolution+fallback rule.
|
|
3279
|
-
*/
|
|
3280
|
-
function resolveLabelOffsetMeters(options, ratio, fallbackMeters = 50) {
|
|
3281
|
-
const h = textHeight(options);
|
|
3282
|
-
return h !== void 0 ? ratio * h : fallbackMeters;
|
|
3283
|
-
}
|
|
3284
|
-
/**
|
|
3285
|
-
* Places one label just beyond each end of `verts` (projected meters), along
|
|
3286
|
-
* the outward direction of that end's segment, reading along the line
|
|
3287
|
-
* ({@link labelRotationAlong} is sign-independent, so the outward-pointing
|
|
3288
|
-
* `along` reads correctly). `textForEnd` supplies each end's text — a shared
|
|
3289
|
-
* string, or a function keyed by which end; an empty string skips that end.
|
|
3699
|
+
* Generates a GeoJSON FeatureCollection for an Airborne Attack tactical symbol.
|
|
3290
3700
|
*
|
|
3291
|
-
*
|
|
3292
|
-
*
|
|
3293
|
-
*
|
|
3294
|
-
*
|
|
3295
|
-
*
|
|
3296
|
-
*
|
|
3297
|
-
* the point and the text body always extends outward.
|
|
3298
|
-
*/
|
|
3299
|
-
function pushEndLabels(out, verts, textForEnd, options = {}) {
|
|
3300
|
-
const frames = endFrames(verts);
|
|
3301
|
-
const sizeProps = labelSizeProps(options);
|
|
3302
|
-
const clearance = resolveLabelOffsetMeters(options, END_LABEL_CLEARANCE_RATIO + Math.max(0, options.labelPadding ?? 0));
|
|
3303
|
-
for (const end of ["start", "end"]) {
|
|
3304
|
-
const frame = frames[end];
|
|
3305
|
-
if (!frame) continue;
|
|
3306
|
-
const text = typeof textForEnd === "string" ? textForEnd : textForEnd(end);
|
|
3307
|
-
if (text.length === 0) continue;
|
|
3308
|
-
pushLabel(out, vecAdd(frame.point, vecScale(frame.along, clearance)), text, labelRotationAlong(frame.along), sizeProps, frame.along[0] >= 0 ? "start" : "end");
|
|
3309
|
-
}
|
|
3310
|
-
}
|
|
3311
|
-
/**
|
|
3312
|
-
* Approximate sans-serif character width/height ratio, used to estimate a
|
|
3313
|
-
* label's rendered text width from its character count when no live text
|
|
3314
|
-
* measurement is available at generator time.
|
|
3315
|
-
*/
|
|
3316
|
-
const LABEL_CHAR_ASPECT_RATIO = .6;
|
|
3317
|
-
/**
|
|
3318
|
-
* Half a single glyph's width, as a ratio of its height — the base boundary
|
|
3319
|
-
* gap a measure reserves for one intrinsic capital letter (Retain's R,
|
|
3320
|
-
* Contain's C). Wider than half `LABEL_CHAR_ASPECT_RATIO` because these
|
|
3321
|
-
* glyphs are stamped alone rather than averaged over a word.
|
|
3701
|
+
* The symbol is similar to Supporting Attack but features a crossover point
|
|
3702
|
+
* between the shaft and the head (Points 1 and 2/Neck).
|
|
3703
|
+
*
|
|
3704
|
+
* @param coordinates - An array of GeoJSON Positions.
|
|
3705
|
+
* @param options - Configuration options.
|
|
3706
|
+
* @returns A GeoJSON FeatureCollection<LineString>.
|
|
3322
3707
|
*/
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
/** Resolve the label's final display size, then measure and convert it to construction metres. */
|
|
3329
|
-
function resolveTextMetrics(text, options, context, style = "regular", fallbackHeightMeters = 50) {
|
|
3330
|
-
const legacyHeight = resolveLabelOffsetMeters(options, 1, fallbackHeightMeters);
|
|
3331
|
-
const constructionScale = context?.constructionMetersPerCssPixel;
|
|
3332
|
-
if (!(constructionScale !== void 0 && constructionScale > 0)) return {
|
|
3333
|
-
width: estimatedTextWidth(text, legacyHeight),
|
|
3334
|
-
height: legacyHeight
|
|
3335
|
-
};
|
|
3336
|
-
let sizeCssPixels;
|
|
3337
|
-
if (options.labelSizePixels !== void 0) sizeCssPixels = options.labelSizePixels;
|
|
3338
|
-
else if (options.labelSize !== void 0) {
|
|
3339
|
-
const requested = options.labelSize / constructionScale;
|
|
3340
|
-
const requestedBand = context?.labelSizeClampCssPixels;
|
|
3341
|
-
const band = requestedBand && Number.isFinite(requestedBand.min) && requestedBand.min >= 0 && Number.isFinite(requestedBand.max) && requestedBand.max >= 0 ? requestedBand : DEFAULT_LABEL_SIZE_CLAMP_CSS_PIXELS;
|
|
3342
|
-
const min = Math.min(band.min, band.max);
|
|
3343
|
-
const max = Math.max(band.min, band.max);
|
|
3344
|
-
sizeCssPixels = Math.min(max, Math.max(min, requested));
|
|
3345
|
-
} else sizeCssPixels = 14;
|
|
3346
|
-
const fallbackHeight = sizeCssPixels * constructionScale;
|
|
3347
|
-
const measured = context?.measureText?.({
|
|
3348
|
-
text,
|
|
3349
|
-
style,
|
|
3350
|
-
sizeCssPixels
|
|
3351
|
-
});
|
|
3352
|
-
if (!measured || !Number.isFinite(measured.widthCssPixels) || measured.widthCssPixels < 0 || !Number.isFinite(measured.heightCssPixels) || measured.heightCssPixels < 0) return {
|
|
3353
|
-
width: estimatedTextWidth(text, fallbackHeight),
|
|
3354
|
-
height: fallbackHeight
|
|
3708
|
+
function createAirborneAttack(coordinates, options = {}) {
|
|
3709
|
+
const { geometry } = processAttackGeometry(coordinates, options);
|
|
3710
|
+
if (!geometry) return {
|
|
3711
|
+
type: "FeatureCollection",
|
|
3712
|
+
features: []
|
|
3355
3713
|
};
|
|
3714
|
+
const shaftLeftToNeck = geometry.shaftLeft.slice(0, -1);
|
|
3715
|
+
const shaftRightToNeck = geometry.shaftRight.slice(0, -1);
|
|
3356
3716
|
return {
|
|
3357
|
-
|
|
3358
|
-
|
|
3717
|
+
type: "FeatureCollection",
|
|
3718
|
+
features: [{
|
|
3719
|
+
type: "Feature",
|
|
3720
|
+
properties: {},
|
|
3721
|
+
geometry: {
|
|
3722
|
+
type: "LineString",
|
|
3723
|
+
coordinates: [
|
|
3724
|
+
...shaftLeftToNeck,
|
|
3725
|
+
geometry.headRing[3],
|
|
3726
|
+
geometry.headRing[2],
|
|
3727
|
+
geometry.headRing[1],
|
|
3728
|
+
geometry.headRing[0],
|
|
3729
|
+
geometry.headRing[5],
|
|
3730
|
+
...[...shaftRightToNeck].reverse()
|
|
3731
|
+
].map((p) => unproject(p[0], p[1]))
|
|
3732
|
+
}
|
|
3733
|
+
}]
|
|
3359
3734
|
};
|
|
3360
3735
|
}
|
|
3736
|
+
const AIRBORNE_ATTACK = defineControlMeasure({
|
|
3737
|
+
metadata: AIRBORNE_ATTACK_METADATA,
|
|
3738
|
+
generator: createAirborneAttack,
|
|
3739
|
+
defaultOptions: DEFAULT_AIRBORNE_ATTACK_OPTIONS,
|
|
3740
|
+
rule: axis1DrawRule,
|
|
3741
|
+
optionHandles: variableWidthAttackOptionHandles
|
|
3742
|
+
});
|
|
3361
3743
|
/**
|
|
3362
|
-
*
|
|
3744
|
+
* Reacts to an **input-contract** violation according to `mode`: `throw`
|
|
3745
|
+
* raises, `warn` logs, `silent` (the default) does neither. Always returns
|
|
3746
|
+
* `false` so a caller can `return reportValidationIssue(...)` from a guard.
|
|
3363
3747
|
*
|
|
3364
|
-
*
|
|
3365
|
-
*
|
|
3366
|
-
*
|
|
3748
|
+
* Governs the *input contract* only (control-point count and finiteness),
|
|
3749
|
+
* checked once at the render seam — not geometric degeneracy, which always
|
|
3750
|
+
* yields an empty render silently. See ADR-0014 and the `CONTEXT.md` terms.
|
|
3367
3751
|
*/
|
|
3368
|
-
function
|
|
3369
|
-
const
|
|
3370
|
-
|
|
3371
|
-
|
|
3752
|
+
function reportValidationIssue(mode, message) {
|
|
3753
|
+
const resolved = mode ?? "silent";
|
|
3754
|
+
if (resolved === "throw") throw new Error(message);
|
|
3755
|
+
if (resolved === "warn") console.warn(message);
|
|
3756
|
+
return false;
|
|
3372
3757
|
}
|
|
3373
3758
|
/**
|
|
3374
|
-
*
|
|
3375
|
-
*
|
|
3376
|
-
*
|
|
3759
|
+
* Narrows a control-point array to the fixed `[PT1, PT2, PT3]` tuple the ambush
|
|
3760
|
+
* and search-area generators require. The render seam has already validated
|
|
3761
|
+
* that at least three valid points are present (ADR-0014), so this is a pure
|
|
3762
|
+
* cast with no runtime guard. Used by those measures' definition adapters so
|
|
3763
|
+
* the generators keep their precise tuple parameter (ADR-0013).
|
|
3377
3764
|
*/
|
|
3378
|
-
function
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
function phaseLineLabelText(name, includePrefix) {
|
|
3385
|
-
if (!name) return "";
|
|
3386
|
-
return includePrefix ? `PL ${name}` : name;
|
|
3765
|
+
function asThreePoints(points) {
|
|
3766
|
+
return [
|
|
3767
|
+
points[0],
|
|
3768
|
+
points[1],
|
|
3769
|
+
points[2]
|
|
3770
|
+
];
|
|
3387
3771
|
}
|
|
3772
|
+
const DEFAULT_AMBUSH_OPTIONS = {
|
|
3773
|
+
arrowheadLengthRatio: .15,
|
|
3774
|
+
arrowheadWidthRatio: .5,
|
|
3775
|
+
hatchLengthRatio: .15,
|
|
3776
|
+
hatchCount: 5,
|
|
3777
|
+
curveDepthRatio: .25,
|
|
3778
|
+
arcSegments: 30
|
|
3779
|
+
};
|
|
3780
|
+
const AMBUSH_METADATA = {
|
|
3781
|
+
id: "ambush",
|
|
3782
|
+
name: "Ambush",
|
|
3783
|
+
description: "A variation of attack from concealed positions against a moving or temporarily halted enemy.",
|
|
3784
|
+
entity: "Maneuver Lines",
|
|
3785
|
+
entityType: "Ambush",
|
|
3786
|
+
value: "141700",
|
|
3787
|
+
minCoordinates: 3,
|
|
3788
|
+
maxCoordinates: 3,
|
|
3789
|
+
geometry: "line",
|
|
3790
|
+
geometryTypes: ["MultiLineString"],
|
|
3791
|
+
paints: {
|
|
3792
|
+
stroke: true,
|
|
3793
|
+
fill: "none",
|
|
3794
|
+
text: false
|
|
3795
|
+
},
|
|
3796
|
+
drawRule: "Line29",
|
|
3797
|
+
params: [
|
|
3798
|
+
{
|
|
3799
|
+
key: "arrowheadLengthRatio",
|
|
3800
|
+
presentationTier: "advanced",
|
|
3801
|
+
label: "Arrowhead length",
|
|
3802
|
+
description: "Arrowhead length as a ratio of the chord span",
|
|
3803
|
+
type: "number",
|
|
3804
|
+
min: .01,
|
|
3805
|
+
max: .5,
|
|
3806
|
+
step: .01
|
|
3807
|
+
},
|
|
3808
|
+
{
|
|
3809
|
+
key: "arrowheadWidthRatio",
|
|
3810
|
+
presentationTier: "advanced",
|
|
3811
|
+
label: "Arrowhead width",
|
|
3812
|
+
description: "Arrowhead base width as a multiple of the arrowhead length",
|
|
3813
|
+
type: "number",
|
|
3814
|
+
min: .05,
|
|
3815
|
+
max: 2,
|
|
3816
|
+
step: .01
|
|
3817
|
+
},
|
|
3818
|
+
{
|
|
3819
|
+
key: "hatchLengthRatio",
|
|
3820
|
+
presentationTier: "advanced",
|
|
3821
|
+
label: "Hatch length",
|
|
3822
|
+
description: "Length of the parallel hatches as a fraction of the chord span",
|
|
3823
|
+
type: "number",
|
|
3824
|
+
min: .01,
|
|
3825
|
+
max: .5,
|
|
3826
|
+
step: .01
|
|
3827
|
+
},
|
|
3828
|
+
{
|
|
3829
|
+
key: "hatchCount",
|
|
3830
|
+
presentationTier: "advanced",
|
|
3831
|
+
label: "Hatch count",
|
|
3832
|
+
description: "Number of parallel hatches spaced along the curve",
|
|
3833
|
+
type: "number",
|
|
3834
|
+
min: 0,
|
|
3835
|
+
max: 20,
|
|
3836
|
+
step: 1
|
|
3837
|
+
},
|
|
3838
|
+
{
|
|
3839
|
+
key: "curveDepthRatio",
|
|
3840
|
+
presentationTier: "advanced",
|
|
3841
|
+
label: "Curve depth",
|
|
3842
|
+
description: "How far the back line bends toward the tip, as a fraction of the chord span",
|
|
3843
|
+
type: "number",
|
|
3844
|
+
min: 0,
|
|
3845
|
+
max: 1,
|
|
3846
|
+
step: .01
|
|
3847
|
+
},
|
|
3848
|
+
{
|
|
3849
|
+
key: "arcSegments",
|
|
3850
|
+
label: "Arc segments",
|
|
3851
|
+
description: "Number of segments used to render the curve",
|
|
3852
|
+
type: "number",
|
|
3853
|
+
presentationTier: "advanced",
|
|
3854
|
+
min: 4,
|
|
3855
|
+
max: 96,
|
|
3856
|
+
step: 1
|
|
3857
|
+
}
|
|
3858
|
+
]
|
|
3859
|
+
};
|
|
3860
|
+
const quadBezier = (t, a, b, c) => {
|
|
3861
|
+
const it = 1 - t;
|
|
3862
|
+
return [it * it * a[0] + 2 * it * t * b[0] + t * t * c[0], it * it * a[1] + 2 * it * t * b[1] + t * t * c[1]];
|
|
3863
|
+
};
|
|
3388
3864
|
/**
|
|
3389
|
-
*
|
|
3390
|
-
*
|
|
3391
|
-
*
|
|
3392
|
-
*
|
|
3865
|
+
* Generates a GeoJSON FeatureCollection for an AMBUSH tactical symbol.
|
|
3866
|
+
* Handles internal projection to maintain strict perpendicularity and parallel hatching.
|
|
3867
|
+
*
|
|
3868
|
+
* @param coordinates - [PT 1 (Target Tip), PT 2 (Back Chord End), PT 3 (Back Chord End)]
|
|
3869
|
+
* @param options - Geometric and styling parameters
|
|
3870
|
+
* @returns GeoJSON FeatureCollection containing LineString components
|
|
3393
3871
|
*/
|
|
3394
|
-
function
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3872
|
+
function createAmbushSymbol(coordinates, options = {}) {
|
|
3873
|
+
const { arrowheadLengthRatio, arrowheadWidthRatio, hatchLengthRatio, hatchCount, curveDepthRatio, arcSegments } = {
|
|
3874
|
+
...DEFAULT_AMBUSH_OPTIONS,
|
|
3875
|
+
...options
|
|
3876
|
+
};
|
|
3877
|
+
const safeArcSegments = Math.max(1, Math.floor(arcSegments));
|
|
3878
|
+
const safeHatchCount = Math.max(0, Math.floor(hatchCount));
|
|
3879
|
+
const safeArrowheadLengthRatio = Math.max(0, arrowheadLengthRatio);
|
|
3880
|
+
const safeArrowheadWidthRatio = Math.max(0, arrowheadWidthRatio);
|
|
3881
|
+
const safeHatchLengthRatio = Math.max(0, hatchLengthRatio);
|
|
3882
|
+
const safeCurveDepthRatio = Math.max(0, curveDepthRatio);
|
|
3883
|
+
const [p1, p2, p3] = coordinates.map((coord) => project(coord[0], coord[1]));
|
|
3884
|
+
const mid = vecScale(vecAdd(p2, p3), .5);
|
|
3885
|
+
const v23 = vecSub(p3, p2);
|
|
3886
|
+
const span = vecMag(v23);
|
|
3887
|
+
if (span < 1e-6) return {
|
|
3888
|
+
type: "FeatureCollection",
|
|
3889
|
+
features: []
|
|
3890
|
+
};
|
|
3891
|
+
const dir23 = vecNorm(v23);
|
|
3892
|
+
const n = [-dir23[1], dir23[0]];
|
|
3893
|
+
const dist = vecDot(vecSub(p1, mid), n);
|
|
3894
|
+
const side = dist >= 0 ? 1 : -1;
|
|
3895
|
+
const ctrl = vecAdd(mid, vecScale(n, side * span * safeCurveDepthRatio));
|
|
3896
|
+
const shaftOrigin = quadBezier(.5, p2, ctrl, p3);
|
|
3897
|
+
const adjP1 = vecAdd(shaftOrigin, vecScale(n, side * Math.max(0, Math.abs(dist) - span * safeCurveDepthRatio * .5)));
|
|
3898
|
+
const lines = [];
|
|
3899
|
+
lines.push([shaftOrigin, adjP1]);
|
|
3900
|
+
const hL = span * safeArrowheadLengthRatio;
|
|
3901
|
+
const hw = hL * safeArrowheadWidthRatio;
|
|
3902
|
+
const hBase = vecSub(adjP1, vecScale(n, side * hL));
|
|
3903
|
+
const perp = [-n[1], n[0]];
|
|
3904
|
+
lines.push([
|
|
3905
|
+
vecAdd(hBase, vecScale(perp, hw)),
|
|
3906
|
+
adjP1,
|
|
3907
|
+
vecSub(hBase, vecScale(perp, hw))
|
|
3908
|
+
]);
|
|
3909
|
+
const curve = [];
|
|
3910
|
+
for (let i = 0; i <= safeArcSegments; i++) {
|
|
3911
|
+
const t = i / safeArcSegments;
|
|
3912
|
+
curve.push(quadBezier(t, p2, ctrl, p3));
|
|
3913
|
+
}
|
|
3914
|
+
lines.push(curve);
|
|
3915
|
+
const hatchLengthActual = span * safeHatchLengthRatio;
|
|
3916
|
+
for (let i = 1; i <= safeHatchCount; i++) {
|
|
3917
|
+
const pt = quadBezier(i / (safeHatchCount + 1), p2, ctrl, p3);
|
|
3918
|
+
lines.push([pt, vecSub(pt, vecScale(n, side * hatchLengthActual))]);
|
|
3406
3919
|
}
|
|
3920
|
+
return {
|
|
3921
|
+
type: "FeatureCollection",
|
|
3922
|
+
features: [{
|
|
3923
|
+
type: "Feature",
|
|
3924
|
+
properties: {},
|
|
3925
|
+
geometry: {
|
|
3926
|
+
type: "MultiLineString",
|
|
3927
|
+
coordinates: lines.map((line) => line.map((c) => unproject(c[0], c[1])))
|
|
3928
|
+
}
|
|
3929
|
+
}]
|
|
3930
|
+
};
|
|
3407
3931
|
}
|
|
3932
|
+
const AMBUSH = defineControlMeasure({
|
|
3933
|
+
metadata: AMBUSH_METADATA,
|
|
3934
|
+
generator: (controlPoints, options) => createAmbushSymbol(asThreePoints(controlPoints), options),
|
|
3935
|
+
defaultOptions: DEFAULT_AMBUSH_OPTIONS,
|
|
3936
|
+
rule: ambushDrawRule
|
|
3937
|
+
});
|
|
3938
|
+
//#endregion
|
|
3939
|
+
//#region src/internal/gapped-line.ts
|
|
3408
3940
|
/**
|
|
3409
|
-
*
|
|
3410
|
-
*
|
|
3411
|
-
*
|
|
3412
|
-
*
|
|
3413
|
-
*
|
|
3414
|
-
* label per tip, at `base + perp·offset`) and generic-c2-line's dual
|
|
3415
|
-
* above/below amplifier labels (`base ± perp·offset`), so both build on the
|
|
3416
|
-
* same tangent/inset/anchor math instead of re-deriving it.
|
|
3941
|
+
* Pushes the arc-length gap `[g0, g1]` onto `gaps`, normalizing it onto
|
|
3942
|
+
* `[0, total)` and splitting it into two intervals when it straddles a ring
|
|
3943
|
+
* seam ({@link buildGappedLine} walks a flat array and does not wrap on its
|
|
3944
|
+
* own). Use this for closed rings; open polylines can push {@link ArcGap}s
|
|
3945
|
+
* directly.
|
|
3417
3946
|
*/
|
|
3418
|
-
function
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3947
|
+
function pushWrappedGap(gaps, g0, g1, total) {
|
|
3948
|
+
if (g1 - g0 >= total) {
|
|
3949
|
+
gaps.push({
|
|
3950
|
+
g0: 0,
|
|
3951
|
+
g1: total
|
|
3952
|
+
});
|
|
3953
|
+
return;
|
|
3954
|
+
}
|
|
3955
|
+
const lo = (g0 % total + total) % total;
|
|
3956
|
+
const hi = (g1 % total + total) % total;
|
|
3957
|
+
if (lo <= hi) gaps.push({
|
|
3958
|
+
g0: lo,
|
|
3959
|
+
g1: hi
|
|
3960
|
+
});
|
|
3961
|
+
else {
|
|
3962
|
+
gaps.push({
|
|
3963
|
+
g0: lo,
|
|
3964
|
+
g1: total
|
|
3965
|
+
});
|
|
3966
|
+
gaps.push({
|
|
3967
|
+
g0: 0,
|
|
3968
|
+
g1: hi
|
|
3969
|
+
});
|
|
3433
3970
|
}
|
|
3434
|
-
return frames;
|
|
3435
3971
|
}
|
|
3436
|
-
/**
|
|
3437
|
-
*
|
|
3438
|
-
*
|
|
3439
|
-
*
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
const sizeProps = labelSizeProps(options);
|
|
3449
|
-
for (const end of ["start", "end"]) {
|
|
3450
|
-
const frame = frames[end];
|
|
3451
|
-
if (!frame) continue;
|
|
3452
|
-
const text = typeof textForEnd === "string" ? textForEnd : textForEnd(end);
|
|
3453
|
-
if (text.length === 0) continue;
|
|
3454
|
-
pushLabel(out, vecAdd(frame.base, vecScale(frame.perp, frame.offset)), text, frame.rotation, sizeProps, frame.textAnchor);
|
|
3455
|
-
}
|
|
3972
|
+
/** Whether the global arc position `arc` falls inside any of `gaps` (each
|
|
3973
|
+
* already normalized onto `[0, total)` by {@link pushWrappedGap}, so a plain
|
|
3974
|
+
* per-gap interval test suffices). Shared by the boundary-decoration measures
|
|
3975
|
+
* that suppress a glyph/tic/barb sitting on top of a masking gap. */
|
|
3976
|
+
function arcInGaps(arc, gaps) {
|
|
3977
|
+
return gaps.some((g) => arc >= g.g0 && arc <= g.g1);
|
|
3978
|
+
}
|
|
3979
|
+
/** Appends `position` unless it coincides with the buffer's current tail. */
|
|
3980
|
+
function appendUnique(buffer, position) {
|
|
3981
|
+
const tail = buffer[buffer.length - 1];
|
|
3982
|
+
if (tail && Math.abs(tail[0] - position[0]) < 1e-9 && Math.abs(tail[1] - position[1]) < 1e-9) return;
|
|
3983
|
+
buffer.push(position);
|
|
3456
3984
|
}
|
|
3457
3985
|
/**
|
|
3458
|
-
*
|
|
3459
|
-
*
|
|
3460
|
-
*
|
|
3461
|
-
*
|
|
3986
|
+
* Builds the polyline as MultiLineString rings, splitting it with a gap at each
|
|
3987
|
+
* echelon position. `gaps` holds global arc-length intervals (distance from the
|
|
3988
|
+
* start of the whole polyline) to carve out — a single gap can span several
|
|
3989
|
+
* short segments, which matters when the line has been smoothed into many
|
|
3990
|
+
* samples. Works for both open polylines and closed rings (pass a vertex array
|
|
3991
|
+
* whose first and last positions coincide).
|
|
3462
3992
|
*/
|
|
3463
|
-
function
|
|
3464
|
-
const
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
const lineCoordinates = smooth ? verts.map((v) => unproject(v[0], v[1])) : positions;
|
|
3470
|
-
const features = [{
|
|
3471
|
-
type: "Feature",
|
|
3472
|
-
properties: lineStyle === void 0 ? { part } : {
|
|
3473
|
-
part,
|
|
3474
|
-
style: lineStyle
|
|
3475
|
-
},
|
|
3476
|
-
geometry: {
|
|
3477
|
-
type: "LineString",
|
|
3478
|
-
coordinates: lineCoordinates
|
|
3479
|
-
}
|
|
3480
|
-
}];
|
|
3481
|
-
const labelFeatures = [];
|
|
3482
|
-
if (placement === "above") pushEndLabelsAbove(labelFeatures, verts, textForEnd, sizeOptions);
|
|
3483
|
-
else pushEndLabels(labelFeatures, verts, textForEnd, sizeOptions);
|
|
3484
|
-
const nameText = phaseLineLabelText(phaseLineName?.trim(), includePrefix);
|
|
3485
|
-
if (nameText) pushEndLabels(labelFeatures, verts, nameText, sizeOptions);
|
|
3486
|
-
pushHostileEndLabels(labelFeatures, verts, hostileText, sizeOptions, nameText || (placement === "beyond" ? textForEnd : ""), context);
|
|
3487
|
-
features.push(...labelFeatures);
|
|
3488
|
-
return {
|
|
3489
|
-
type: "FeatureCollection",
|
|
3490
|
-
features
|
|
3993
|
+
function buildGappedLine(verts, gaps) {
|
|
3994
|
+
const parts = [];
|
|
3995
|
+
let buffer = [];
|
|
3996
|
+
const flush = () => {
|
|
3997
|
+
if (buffer.length >= 2) parts.push(buffer);
|
|
3998
|
+
buffer = [];
|
|
3491
3999
|
};
|
|
4000
|
+
if (verts.length === 0) return parts;
|
|
4001
|
+
const merged = [];
|
|
4002
|
+
for (const g of [...gaps].sort((x, y) => x.g0 - y.g0)) {
|
|
4003
|
+
const last = merged[merged.length - 1];
|
|
4004
|
+
if (last && g.g0 <= last.g1) last.g1 = Math.max(last.g1, g.g1);
|
|
4005
|
+
else merged.push({ ...g });
|
|
4006
|
+
}
|
|
4007
|
+
appendUnique(buffer, unproject(verts[0][0], verts[0][1]));
|
|
4008
|
+
let dist = 0;
|
|
4009
|
+
for (let i = 0; i < verts.length - 1; i++) {
|
|
4010
|
+
const a = verts[i];
|
|
4011
|
+
const b = verts[i + 1];
|
|
4012
|
+
const seg = vecSub(b, a);
|
|
4013
|
+
const L = vecMag(seg);
|
|
4014
|
+
if (L < 1e-6) continue;
|
|
4015
|
+
const segStart = dist;
|
|
4016
|
+
const segEnd = dist + L;
|
|
4017
|
+
const along = vecNorm(seg);
|
|
4018
|
+
const pointAt = (globalD) => {
|
|
4019
|
+
const q = vecAdd(a, vecScale(along, globalD - segStart));
|
|
4020
|
+
return unproject(q[0], q[1]);
|
|
4021
|
+
};
|
|
4022
|
+
for (const g of merged) {
|
|
4023
|
+
if (g.g1 <= segStart || g.g0 >= segEnd) continue;
|
|
4024
|
+
const g0 = Math.max(segStart, g.g0);
|
|
4025
|
+
const g1 = Math.min(segEnd, g.g1);
|
|
4026
|
+
if (g1 <= g0) continue;
|
|
4027
|
+
appendUnique(buffer, pointAt(g0));
|
|
4028
|
+
flush();
|
|
4029
|
+
buffer.push(pointAt(g1));
|
|
4030
|
+
}
|
|
4031
|
+
appendUnique(buffer, unproject(b[0], b[1]));
|
|
4032
|
+
dist = segEnd;
|
|
4033
|
+
}
|
|
4034
|
+
flush();
|
|
4035
|
+
return parts;
|
|
3492
4036
|
}
|
|
3493
4037
|
//#endregion
|
|
3494
4038
|
//#region src/text-amplifiers.ts
|
|
@@ -5693,6 +6237,7 @@ const freezeEchelonLabeledAreaAnchor = makeFreezeEchelonAnchor({
|
|
|
5693
6237
|
const DEFAULT_BASE_CAMP_OPTIONS = DEFAULT_ECHELON_LABELED_AREA_OPTIONS;
|
|
5694
6238
|
const BASE_CAMP_METADATA = {
|
|
5695
6239
|
id: "base-camp",
|
|
6240
|
+
sizePairs: [ECHELON_SIZE_PAIR],
|
|
5696
6241
|
name: "Base camp",
|
|
5697
6242
|
description: "An evolving military facility that supports military operations of a deployed unit and provides the necessary support and services for sustained operations.",
|
|
5698
6243
|
entity: "Command and Control Areas",
|
|
@@ -5752,6 +6297,7 @@ const BASE_CAMP = defineControlMeasure({
|
|
|
5752
6297
|
const DEFAULT_GUERRILLA_BASE_OPTIONS = DEFAULT_ECHELON_LABELED_AREA_OPTIONS;
|
|
5753
6298
|
const GUERRILLA_BASE_METADATA = {
|
|
5754
6299
|
id: "guerrilla-base",
|
|
6300
|
+
sizePairs: [ECHELON_SIZE_PAIR],
|
|
5755
6301
|
name: "Guerrilla base",
|
|
5756
6302
|
description: "A temporary site where guerrilla installations, headquarters, and units are located.",
|
|
5757
6303
|
entity: "Command and Control Areas",
|
|
@@ -5997,6 +6543,15 @@ const ANTITANK_WALL_PARAMS = [...ANTITANK_DITCH_PARAMS, {
|
|
|
5997
6543
|
const DEFAULT_TOOTH_HEIGHT = 50;
|
|
5998
6544
|
const DEFAULT_TOOTH_HEIGHT_PIXELS = 10;
|
|
5999
6545
|
const DEFAULT_TOOTH_WIDTH_RATIO$1 = 1.15;
|
|
6546
|
+
/** Shared by both ditch variants and by the antitank wall built on the same teeth. */
|
|
6547
|
+
const TOOTH_HEIGHT_SIZE_PAIR = {
|
|
6548
|
+
id: "tooth-height",
|
|
6549
|
+
label: "Tooth height",
|
|
6550
|
+
pixels: "toothHeightPixels",
|
|
6551
|
+
meters: "toothHeight",
|
|
6552
|
+
defaultUnit: "m",
|
|
6553
|
+
rendering: "geometry"
|
|
6554
|
+
};
|
|
6000
6555
|
const DEFAULT_ANTITANK_DITCH_OPTIONS = {
|
|
6001
6556
|
toothHeight: DEFAULT_TOOTH_HEIGHT,
|
|
6002
6557
|
toothHeightPixels: DEFAULT_TOOTH_HEIGHT_PIXELS,
|
|
@@ -6006,6 +6561,7 @@ const DEFAULT_ANTITANK_DITCH_OPTIONS = {
|
|
|
6006
6561
|
};
|
|
6007
6562
|
const ANTITANK_DITCH_UNDER_CONSTRUCTION_METADATA = {
|
|
6008
6563
|
id: "antitank-ditch-under-construction",
|
|
6564
|
+
sizePairs: [TOOTH_HEIGHT_SIZE_PAIR],
|
|
6009
6565
|
name: "Ditch - Under Construction",
|
|
6010
6566
|
description: "Antitank ditch obstacle under construction.",
|
|
6011
6567
|
entity: "Protection Lines",
|
|
@@ -6028,6 +6584,7 @@ const ANTITANK_DITCH_UNDER_CONSTRUCTION_METADATA = {
|
|
|
6028
6584
|
};
|
|
6029
6585
|
const ANTITANK_DITCH_COMPLETED_METADATA = {
|
|
6030
6586
|
id: "antitank-ditch-completed",
|
|
6587
|
+
sizePairs: [TOOTH_HEIGHT_SIZE_PAIR],
|
|
6031
6588
|
name: "Ditch - Completed",
|
|
6032
6589
|
description: "Completed antitank ditch obstacle.",
|
|
6033
6590
|
entity: "Protection Lines",
|
|
@@ -6161,6 +6718,7 @@ const DEFAULT_ANTITANK_WALL_OPTIONS = {
|
|
|
6161
6718
|
};
|
|
6162
6719
|
const ANTITANK_WALL_METADATA = {
|
|
6163
6720
|
id: "antitank-wall",
|
|
6721
|
+
sizePairs: [TOOTH_HEIGHT_SIZE_PAIR],
|
|
6164
6722
|
name: "Antitank Wall",
|
|
6165
6723
|
description: "Antitank wall obstacle.",
|
|
6166
6724
|
entity: "Protection Lines",
|
|
@@ -6317,7 +6875,7 @@ function createAttackByFire(coordinates, options = {}) {
|
|
|
6317
6875
|
const p2Back = vecAdd(p2, vecScale(dirBackLeft, backLen));
|
|
6318
6876
|
const p3Back = vecAdd(p3, vecScale(dirBackRight, backLen));
|
|
6319
6877
|
const headWidth = lenShaft * arrowheadWidthRatio;
|
|
6320
|
-
const headFeatures = createArrowHead$
|
|
6878
|
+
const headFeatures = createArrowHead$1(pTip, dirShaft, headWidth, headWidth);
|
|
6321
6879
|
return {
|
|
6322
6880
|
type: "FeatureCollection",
|
|
6323
6881
|
features: [
|
|
@@ -6360,7 +6918,7 @@ function createAttackByFire(coordinates, options = {}) {
|
|
|
6360
6918
|
]
|
|
6361
6919
|
};
|
|
6362
6920
|
}
|
|
6363
|
-
function createArrowHead$
|
|
6921
|
+
function createArrowHead$1(tip, dir, length, width) {
|
|
6364
6922
|
const norm = [-dir[1], dir[0]];
|
|
6365
6923
|
const base = vecSub(tip, vecScale(dir, length));
|
|
6366
6924
|
const left = vecAdd(base, vecScale(norm, width / 2));
|
|
@@ -6625,6 +7183,7 @@ const DEFAULT_BATTLE_POSITION_OPTIONS = {
|
|
|
6625
7183
|
};
|
|
6626
7184
|
const BATTLE_POSITION_METADATA = {
|
|
6627
7185
|
id: "battle-position",
|
|
7186
|
+
sizePairs: [ECHELON_SIZE_PAIR],
|
|
6628
7187
|
name: "Battle Position",
|
|
6629
7188
|
description: "A defensive location oriented on a likely enemy avenue of approach, labeled with the echelon of the occupying unit.",
|
|
6630
7189
|
entity: "Maneuver Areas",
|
|
@@ -6768,6 +7327,7 @@ const BATTLE_POSITION_PREPARED_DASH = [6, 4];
|
|
|
6768
7327
|
const DEFAULT_BATTLE_POSITION_PREPARED_OPTIONS = { ...DEFAULT_BATTLE_POSITION_OPTIONS };
|
|
6769
7328
|
const BATTLE_POSITION_PREPARED_METADATA = {
|
|
6770
7329
|
id: "battle-position-prepared",
|
|
7330
|
+
sizePairs: [ECHELON_SIZE_PAIR],
|
|
6771
7331
|
name: "Battle Position Prepared (P) but not Occupied",
|
|
6772
7332
|
description: "A battle position that has been prepared for occupation but is not yet occupied, drawn with a dashed boundary and a (P)-prefixed designation.",
|
|
6773
7333
|
entity: "Maneuver Areas",
|
|
@@ -7735,6 +8295,7 @@ const DEFAULT_BOUNDARY_OPTIONS = {
|
|
|
7735
8295
|
};
|
|
7736
8296
|
const BOUNDARY_METADATA = {
|
|
7737
8297
|
id: "boundary",
|
|
8298
|
+
sizePairs: [ECHELON_SIZE_PAIR],
|
|
7738
8299
|
name: "Boundary",
|
|
7739
8300
|
description: "Delineates the area of responsibility between two adjacent units, labeled with the echelon and the designators of the units it separates.",
|
|
7740
8301
|
entity: "Command and Control Lines",
|
|
@@ -8152,6 +8713,7 @@ const DEFAULT_GENERIC_C2_LINE_OPTIONS = {
|
|
|
8152
8713
|
};
|
|
8153
8714
|
const GENERIC_C2_LINE_METADATA = {
|
|
8154
8715
|
id: "generic-c2-line",
|
|
8716
|
+
sizePairs: [ECHELON_SIZE_PAIR],
|
|
8155
8717
|
name: "Generic command and control line",
|
|
8156
8718
|
description: "Illustrative command-and-control-style line (not part of MIL-STD-2525E) carrying optional echelon glyphs, additional-information/designation text above it, and an effective date-time-group window below it.",
|
|
8157
8719
|
entity: "Command and Control Lines",
|
|
@@ -8357,130 +8919,6 @@ const GENERIC_C2_LINE = defineControlMeasure({
|
|
|
8357
8919
|
}
|
|
8358
8920
|
});
|
|
8359
8921
|
//#endregion
|
|
8360
|
-
//#region src/generators/cm34-mission-tasks/shared.ts
|
|
8361
|
-
/**
|
|
8362
|
-
* Builds an open chevron arrowhead (left → tip → right) as unprojected
|
|
8363
|
-
* coordinates. The tip sits at `tip`, pointing along `dir`, with the base set
|
|
8364
|
-
* back by `length` and spanning `width` perpendicular to the shaft. Mirrors the
|
|
8365
|
-
* `createArrowHead` helper used by the attack-family generators.
|
|
8366
|
-
*/
|
|
8367
|
-
function createArrowHead$1(tip, dir, length, width) {
|
|
8368
|
-
const norm = [-dir[1], dir[0]];
|
|
8369
|
-
const base = vecSub(tip, vecScale(dir, length));
|
|
8370
|
-
const left = vecAdd(base, vecScale(norm, width / 2));
|
|
8371
|
-
const right = vecSub(base, vecScale(norm, width / 2));
|
|
8372
|
-
return [
|
|
8373
|
-
unproject(left[0], left[1]),
|
|
8374
|
-
unproject(tip[0], tip[1]),
|
|
8375
|
-
unproject(right[0], right[1])
|
|
8376
|
-
];
|
|
8377
|
-
}
|
|
8378
|
-
/**
|
|
8379
|
-
* Builds the glyph label as a Point feature, forwarding the resolved size
|
|
8380
|
-
* anchor (`textSizeMeters`/`textSizePixels`, ADR-0028) to adapters for
|
|
8381
|
-
* styling. `point` is in projected meters; the rotation must already be in
|
|
8382
|
-
* the generator's feature-rotation convention. Shared by every mission-task
|
|
8383
|
-
* measure that centers a glyph on its symbol.
|
|
8384
|
-
*/
|
|
8385
|
-
function createLabelFeature(point, text, rotation, labelSize = {}) {
|
|
8386
|
-
return {
|
|
8387
|
-
type: "Feature",
|
|
8388
|
-
properties: {
|
|
8389
|
-
part: "label",
|
|
8390
|
-
labelPlacement: false,
|
|
8391
|
-
text,
|
|
8392
|
-
rotation,
|
|
8393
|
-
...labelSizeProps(labelSize)
|
|
8394
|
-
},
|
|
8395
|
-
geometry: {
|
|
8396
|
-
type: "Point",
|
|
8397
|
-
coordinates: unproject(point[0], point[1])
|
|
8398
|
-
}
|
|
8399
|
-
};
|
|
8400
|
-
}
|
|
8401
|
-
/**
|
|
8402
|
-
* Shared scaffold for the "bracket" mission-task symbols (breach, bypass,
|
|
8403
|
-
* canalize). Owns the three-sided bracket: a front opening (PT. 1 / PT. 2), a
|
|
8404
|
-
* rear line parallel to it through PT. 3, and a glyph centered on the rear line.
|
|
8405
|
-
* Each measure supplies only its end caps and label glyph via
|
|
8406
|
-
* {@link BracketSymbolConfig}.
|
|
8407
|
-
*
|
|
8408
|
-
* Input Points:
|
|
8409
|
-
* - PT. 1: Front opening endpoint (top/left)
|
|
8410
|
-
* - PT. 2: Front opening endpoint (bottom/right) - defines height with PT. 1
|
|
8411
|
-
* - PT. 3: Rear point - defines length and orientation
|
|
8412
|
-
*
|
|
8413
|
-
* Geometric Construction:
|
|
8414
|
-
* 1. PT. 1 and PT. 2 define the front opening (facing enemy/objective)
|
|
8415
|
-
* 2. A parallel line is projected through PT. 3 with identical height
|
|
8416
|
-
* 3. LineString follows: PT. 1 → Rear Top → Rear Bottom → PT. 2
|
|
8417
|
-
* 4. Measure-specific end caps at PT. 1 and PT. 2
|
|
8418
|
-
* 5. Label point at midpoint of rear line (styled with OpenLayers Text)
|
|
8419
|
-
*
|
|
8420
|
-
* @param coordinates - [PT. 1, PT. 2, PT. 3] as Position arrays
|
|
8421
|
-
* @param config - Per-measure end caps, label glyph, and sizing
|
|
8422
|
-
* @returns GeoJSON FeatureCollection containing MultiLineString and Point features
|
|
8423
|
-
*/
|
|
8424
|
-
function createBracketSymbol(coordinates, config, context) {
|
|
8425
|
-
const [c1, c2, c3] = coordinates;
|
|
8426
|
-
const p1 = project(c1[0], c1[1]);
|
|
8427
|
-
const p2 = project(c2[0], c2[1]);
|
|
8428
|
-
const p3 = project(c3[0], c3[1]);
|
|
8429
|
-
const vFront = vecSub(p2, p1);
|
|
8430
|
-
const frontLength = vecMag(vFront);
|
|
8431
|
-
if (frontLength < 1e-6) return {
|
|
8432
|
-
type: "FeatureCollection",
|
|
8433
|
-
features: []
|
|
8434
|
-
};
|
|
8435
|
-
const dirFront = vecNorm(vFront);
|
|
8436
|
-
const frontMidpoint = vecScale(vecAdd(p1, p2), .5);
|
|
8437
|
-
const vToRear = vecSub(p3, frontMidpoint);
|
|
8438
|
-
const perpDir = [-dirFront[1], dirFront[0]];
|
|
8439
|
-
const perpDistance = vToRear[0] * perpDir[0] + vToRear[1] * perpDir[1];
|
|
8440
|
-
const halfFront = vecScale(dirFront, frontLength / 2);
|
|
8441
|
-
const rearMidpoint = vecAdd(frontMidpoint, vecScale(perpDir, perpDistance));
|
|
8442
|
-
const rearTop = vecSub(rearMidpoint, halfFront);
|
|
8443
|
-
const rearBottom = vecAdd(rearMidpoint, halfFront);
|
|
8444
|
-
const rearLineRotation = Math.atan2(dirFront[1], dirFront[0]);
|
|
8445
|
-
const renderedLabelRotation = Math.PI - (rearLineRotation - Math.PI / 2);
|
|
8446
|
-
const labelRotation = normalizeRadians(Math.PI - keepTextLeftToRight(renderedLabelRotation));
|
|
8447
|
-
const gapHalf = Math.min(frontLength / 2, labelKnockoutHalfExtent(config.labelText, dirFront, labelRotation, config.labelOptions ?? {}, context));
|
|
8448
|
-
const gapPt1 = vecAdd(rearMidpoint, vecScale(dirFront, -gapHalf));
|
|
8449
|
-
const gapPt2 = vecAdd(rearMidpoint, vecScale(dirFront, gapHalf));
|
|
8450
|
-
const [topCap, bottomCap] = config.buildEndCaps({
|
|
8451
|
-
p1,
|
|
8452
|
-
p2,
|
|
8453
|
-
dirFront,
|
|
8454
|
-
perpDir,
|
|
8455
|
-
frontLength,
|
|
8456
|
-
rearTop
|
|
8457
|
-
});
|
|
8458
|
-
return {
|
|
8459
|
-
type: "FeatureCollection",
|
|
8460
|
-
features: [{
|
|
8461
|
-
type: "Feature",
|
|
8462
|
-
properties: { part: config.part },
|
|
8463
|
-
geometry: {
|
|
8464
|
-
type: "MultiLineString",
|
|
8465
|
-
coordinates: [
|
|
8466
|
-
topCap,
|
|
8467
|
-
[
|
|
8468
|
-
unproject(p1[0], p1[1]),
|
|
8469
|
-
unproject(rearTop[0], rearTop[1]),
|
|
8470
|
-
unproject(gapPt1[0], gapPt1[1])
|
|
8471
|
-
],
|
|
8472
|
-
[
|
|
8473
|
-
unproject(gapPt2[0], gapPt2[1]),
|
|
8474
|
-
unproject(rearBottom[0], rearBottom[1]),
|
|
8475
|
-
unproject(p2[0], p2[1])
|
|
8476
|
-
],
|
|
8477
|
-
bottomCap
|
|
8478
|
-
]
|
|
8479
|
-
}
|
|
8480
|
-
}, createLabelFeature(rearMidpoint, config.labelText, labelRotation, config.labelOptions)]
|
|
8481
|
-
};
|
|
8482
|
-
}
|
|
8483
|
-
//#endregion
|
|
8484
8922
|
//#region src/generators/cm34-mission-tasks/block.ts
|
|
8485
8923
|
const DEFAULT_STEM_RATIO = 1.5;
|
|
8486
8924
|
const DEFAULT_BLOCK_MISSION_TASK_OPTIONS = { labelPadding: DEFAULT_LABEL_KNOCKOUT_PADDING };
|
|
@@ -8721,7 +9159,7 @@ function createBypassSymbol(coordinates, options = {}, _textAmplifiers = {}, con
|
|
|
8721
9159
|
buildEndCaps: ({ p1, p2, frontLength, rearTop }) => {
|
|
8722
9160
|
const arrowheadLength = frontLength * arrowheadLengthRatio;
|
|
8723
9161
|
const arrowDir = vecNorm(vecSub(p1, rearTop));
|
|
8724
|
-
return [createArrowHead$
|
|
9162
|
+
return [createArrowHead$2(p1, arrowDir, arrowheadLength, arrowheadLength), createArrowHead$2(p2, arrowDir, arrowheadLength, arrowheadLength)];
|
|
8725
9163
|
}
|
|
8726
9164
|
}, context);
|
|
8727
9165
|
}
|
|
@@ -9607,6 +10045,14 @@ const DEFAULT_GENERIC_TEXT_OPTIONS = {
|
|
|
9607
10045
|
};
|
|
9608
10046
|
const GENERIC_TEXT_METADATA = {
|
|
9609
10047
|
id: "text",
|
|
10048
|
+
sizePairs: [{
|
|
10049
|
+
id: "size",
|
|
10050
|
+
label: "Size",
|
|
10051
|
+
pixels: "sizePixels",
|
|
10052
|
+
meters: "sizeMeters",
|
|
10053
|
+
defaultUnit: "px",
|
|
10054
|
+
rendering: "text"
|
|
10055
|
+
}],
|
|
9610
10056
|
name: "Text",
|
|
9611
10057
|
description: "A non-doctrinal free-standing text label anchored at a single point.",
|
|
9612
10058
|
entity: "Generic Graphics",
|
|
@@ -10007,7 +10453,7 @@ function createClearSymbol(coordinates, options = {}, _textAmplifiers = {}, cont
|
|
|
10007
10453
|
lines.push([unproject(rearEnd[0], rearEnd[1]), unproject(gapRear[0], gapRear[1])]);
|
|
10008
10454
|
lines.push([unproject(gapTip[0], gapTip[1]), unproject(tip[0], tip[1])]);
|
|
10009
10455
|
} else if (shaftLength > 1e-6) lines.push([unproject(rearEnd[0], rearEnd[1]), unproject(tip[0], tip[1])]);
|
|
10010
|
-
lines.push(createArrowHead$
|
|
10456
|
+
lines.push(createArrowHead$2(tip, arrowDir, arrowheadLength, arrowheadWidth));
|
|
10011
10457
|
});
|
|
10012
10458
|
const mainFeature = {
|
|
10013
10459
|
type: "Feature",
|
|
@@ -10168,7 +10614,7 @@ function createIsolateVariantSymbol(coordinates, options, config, context = {})
|
|
|
10168
10614
|
const barbLength = radius * barbLengthRatio;
|
|
10169
10615
|
const lines = [sampleArc(0, labelSweep - labelHalfGap), sampleArc(labelSweep + labelHalfGap, arcSpan)];
|
|
10170
10616
|
const tipDir = [Math.sin(endAngle), -Math.cos(endAngle)];
|
|
10171
|
-
lines.push(createArrowHead$
|
|
10617
|
+
lines.push(createArrowHead$2(pointOnCircle(endAngle), tipDir, barbLength, barbLength));
|
|
10172
10618
|
const halfWidth = Math.asin(Math.min(1, barbLength / 2 / radius));
|
|
10173
10619
|
const tipRadius = Math.max(0, radius - barbLength);
|
|
10174
10620
|
const steps = Math.max(1, barbCount);
|
|
@@ -11176,7 +11622,7 @@ function createDelayTaskSymbol(coordinates, config, options, context) {
|
|
|
11176
11622
|
const lines = [
|
|
11177
11623
|
[unproject(p1[0], p1[1]), unproject(gapStart[0], gapStart[1])],
|
|
11178
11624
|
[unproject(gapEnd[0], gapEnd[1]), unproject(p2[0], p2[1])],
|
|
11179
|
-
createArrowHead$
|
|
11625
|
+
createArrowHead$2(p1, vecScale(dirLine, -1), lineLength * (options.arrowheadLengthRatio ?? DEFAULT_DELAY_OPTIONS.arrowheadLengthRatio), lineLength * (options.arrowheadWidthRatio ?? DEFAULT_DELAY_OPTIONS.arrowheadWidthRatio))
|
|
11180
11626
|
];
|
|
11181
11627
|
if (diameter >= 1e-6) lines.push(createSemicircleArc(p2, diameterEnd, dirLine, options.arcSegments ?? DEFAULT_DELAY_OPTIONS.arcSegments));
|
|
11182
11628
|
return {
|
|
@@ -11259,8 +11705,8 @@ function createHairpinSymbol(coordinates, config, options = {}) {
|
|
|
11259
11705
|
...upperArm,
|
|
11260
11706
|
createSemicircleArc(p2, p3, upperDirection, options.arcSegments ?? DEFAULT_DELAY_OPTIONS.arcSegments),
|
|
11261
11707
|
[unproject(p4[0], p4[1]), unproject(p3[0], p3[1])],
|
|
11262
|
-
createArrowHead$
|
|
11263
|
-
createArrowHead$
|
|
11708
|
+
createArrowHead$2(p1, vecScale(upperDirection, -1), upperLength * arrowheadLengthRatio, upperLength * arrowheadWidthRatio),
|
|
11709
|
+
createArrowHead$2(p3, lowerDirection, lowerLength * arrowheadLengthRatio, lowerLength * arrowheadWidthRatio)
|
|
11264
11710
|
]
|
|
11265
11711
|
}
|
|
11266
11712
|
}, label]
|
|
@@ -11496,12 +11942,12 @@ function createDisruptMissionTaskSymbol(coordinates, options = {}, _textAmplifie
|
|
|
11496
11942
|
coordinates: [
|
|
11497
11943
|
[p1LonLat, p2LonLat],
|
|
11498
11944
|
[[...p1LonLat], unproject(topTip[0], topTip[1])],
|
|
11499
|
-
createArrowHead$
|
|
11945
|
+
createArrowHead$2(topTip, arrowDirection, headLength, headWidth),
|
|
11500
11946
|
[unproject(rearEnd[0], rearEnd[1]), unproject(gapStart[0], gapStart[1])],
|
|
11501
11947
|
[unproject(gapEnd[0], gapEnd[1]), unproject(middleTip[0], middleTip[1])],
|
|
11502
|
-
createArrowHead$
|
|
11948
|
+
createArrowHead$2(middleTip, arrowDirection, headLength, headWidth),
|
|
11503
11949
|
[[...p2LonLat], unproject(bottomTip[0], bottomTip[1])],
|
|
11504
|
-
createArrowHead$
|
|
11950
|
+
createArrowHead$2(bottomTip, arrowDirection, headLength, headWidth)
|
|
11505
11951
|
]
|
|
11506
11952
|
}
|
|
11507
11953
|
}, createLabelFeature(labelPoint, "D", labelRotation, {
|
|
@@ -11607,7 +12053,7 @@ function createEnvelopment(coordinates, options = {}, _textAmplifiers = {}, cont
|
|
|
11607
12053
|
let bulgeRadius = [-startRadius[1], startRadius[0]];
|
|
11608
12054
|
if (vecDot(vecSub(p4, center), bulgeRadius) < 0) bulgeRadius = vecScale(bulgeRadius, -1);
|
|
11609
12055
|
const arc = sampleProjectedArc(center, startRadius, bulgeRadius, 0, Math.PI, normalizeArcResolution(options.resolution) / 2);
|
|
11610
|
-
const arrowhead = createArrowHead$
|
|
12056
|
+
const arrowhead = createArrowHead$2(p3, vecNorm(vecScale(bulgeRadius, -1)), diameter * Math.max(0, options.arrowheadLengthRatio ?? DEFAULT_ENVELOPMENT_OPTIONS.arrowheadLengthRatio), diameter * Math.max(0, options.arrowheadWidthRatio ?? DEFAULT_ENVELOPMENT_OPTIONS.arrowheadWidthRatio));
|
|
11611
12057
|
return {
|
|
11612
12058
|
type: "FeatureCollection",
|
|
11613
12059
|
features: [{
|
|
@@ -11676,7 +12122,7 @@ function createExploitation(coordinates, _options = {}) {
|
|
|
11676
12122
|
features: []
|
|
11677
12123
|
};
|
|
11678
12124
|
const direction = vecNorm(axis);
|
|
11679
|
-
const chevron = (tip) => createArrowHead$
|
|
12125
|
+
const chevron = (tip) => createArrowHead$2(tip, direction, finLength * Math.SQRT1_2, finLength * Math.SQRT2);
|
|
11680
12126
|
const [finLeft, , finRight] = chevron(p2);
|
|
11681
12127
|
return {
|
|
11682
12128
|
type: "FeatureCollection",
|
|
@@ -12046,7 +12492,7 @@ function createInfiltration(coordinates, options = {}, _textAmplifiers = {}, con
|
|
|
12046
12492
|
const gapHalf = Math.min(straightLength * Math.min(labelPosition, 1 - labelPosition), labelKnockoutHalfExtent("IN", along, labelRotation, options, context, DEFAULT_INFILTRATION_OPTIONS.labelPadding));
|
|
12047
12493
|
const gapStart = vecAdd(labelPoint, vecScale(along, -gapHalf));
|
|
12048
12494
|
const gapEnd = vecAdd(labelPoint, vecScale(along, gapHalf));
|
|
12049
|
-
const arrowhead = createArrowHead$
|
|
12495
|
+
const arrowhead = createArrowHead$2(p3, along, spanLength * Math.max(0, options.arrowheadLengthRatio ?? DEFAULT_INFILTRATION_OPTIONS.arrowheadLengthRatio), spanLength * Math.max(0, options.arrowheadWidthRatio ?? DEFAULT_INFILTRATION_OPTIONS.arrowheadWidthRatio));
|
|
12050
12496
|
return {
|
|
12051
12497
|
type: "FeatureCollection",
|
|
12052
12498
|
features: [{
|
|
@@ -13077,7 +13523,7 @@ function createFixMissionTaskSymbol(coordinates, options = {}, _textAmplifiers =
|
|
|
13077
13523
|
toPosition(zigzagExit),
|
|
13078
13524
|
toPosition(tip)
|
|
13079
13525
|
],
|
|
13080
|
-
createArrowHead$
|
|
13526
|
+
createArrowHead$2(tip, along, arrowheadLength, arrowheadWidth)
|
|
13081
13527
|
]
|
|
13082
13528
|
}
|
|
13083
13529
|
}, createLabelFeature(labelCenter, "F", labelRotation, options)]
|
|
@@ -13110,6 +13556,14 @@ const DEFAULT_FLOT_OPTIONS = {
|
|
|
13110
13556
|
};
|
|
13111
13557
|
const FLOT_METADATA = {
|
|
13112
13558
|
id: "flot",
|
|
13559
|
+
sizePairs: [{
|
|
13560
|
+
id: "radius",
|
|
13561
|
+
label: "Radius",
|
|
13562
|
+
pixels: "radiusPixels",
|
|
13563
|
+
meters: "radius",
|
|
13564
|
+
defaultUnit: "m",
|
|
13565
|
+
rendering: "geometry"
|
|
13566
|
+
}],
|
|
13113
13567
|
name: "Forward line of own troops (FLOT)",
|
|
13114
13568
|
description: "Indicates the most forward positions of friendly forces at a specific time.",
|
|
13115
13569
|
entity: "Maneuver Lines",
|
|
@@ -14280,6 +14734,15 @@ const PROBABLE_LINE_OF_DEPLOYMENT = defineControlMeasure({
|
|
|
14280
14734
|
});
|
|
14281
14735
|
//#endregion
|
|
14282
14736
|
//#region src/generators/cm29-protection-lines/fortifiedLine.ts
|
|
14737
|
+
/** Shared by the fortified line and the fortified area built from it. */
|
|
14738
|
+
const TOOTH_SIZE_SIZE_PAIR = {
|
|
14739
|
+
id: "tooth-size",
|
|
14740
|
+
label: "Tooth size",
|
|
14741
|
+
pixels: "toothSizePixels",
|
|
14742
|
+
meters: "toothSize",
|
|
14743
|
+
defaultUnit: "m",
|
|
14744
|
+
rendering: "geometry"
|
|
14745
|
+
};
|
|
14283
14746
|
const DEFAULT_TOOTH_SIZE = 50;
|
|
14284
14747
|
const DEFAULT_TOOTH_SIZE_PIXELS = 10;
|
|
14285
14748
|
const DEFAULT_SMOOTH_RESOLUTION$1 = 16;
|
|
@@ -14294,6 +14757,7 @@ const DEFAULT_FORTIFIED_LINE_OPTIONS = {
|
|
|
14294
14757
|
};
|
|
14295
14758
|
const FORTIFIED_LINE_METADATA = {
|
|
14296
14759
|
id: "fortified-line",
|
|
14760
|
+
sizePairs: [TOOTH_SIZE_SIZE_PAIR],
|
|
14297
14761
|
name: "Fortified Line",
|
|
14298
14762
|
description: "Fortified line with castellated square-wave boundary.",
|
|
14299
14763
|
entity: "Protection Lines",
|
|
@@ -14451,6 +14915,7 @@ const FORTIFIED_LINE = defineControlMeasure({
|
|
|
14451
14915
|
const DEFAULT_FORTIFIED_AREA_OPTIONS = DEFAULT_FORTIFIED_LINE_OPTIONS;
|
|
14452
14916
|
const FORTIFIED_AREA_METADATA = {
|
|
14453
14917
|
id: "fortified-area",
|
|
14918
|
+
sizePairs: [TOOTH_SIZE_SIZE_PAIR],
|
|
14454
14919
|
name: "Fortified Area",
|
|
14455
14920
|
description: "A specified geographical locality with constructed defensive works as protection against attack.",
|
|
14456
14921
|
entity: "Maneuver Areas",
|
|
@@ -15047,7 +15512,7 @@ function createPenetrateSymbol(coordinates, options = {}, _textAmplifiers = {},
|
|
|
15047
15512
|
[coordinates[0], coordinates[1]],
|
|
15048
15513
|
[unproject(tail[0], tail[1]), unproject(gapStart[0], gapStart[1])],
|
|
15049
15514
|
[unproject(gapEnd[0], gapEnd[1]), unproject(tip[0], tip[1])],
|
|
15050
|
-
createArrowHead$
|
|
15515
|
+
createArrowHead$2(tip, arrowDirection, headLength, headWidth)
|
|
15051
15516
|
]
|
|
15052
15517
|
}
|
|
15053
15518
|
}, createLabelFeature(labelPoint, "P", labelRotation, {
|
|
@@ -15173,7 +15638,7 @@ function createPursuit(coordinates, options = {}, _textAmplifiers = {}, context
|
|
|
15173
15638
|
const arc = sampleProjectedArc(center, vecSub(p2, center), vecScale(straightDirection, diameter / 2), 0, Math.PI, normalizeArcResolution(options.resolution) / 2);
|
|
15174
15639
|
const arrowheadLength = diameter * Math.max(0, options.arrowheadLengthRatio ?? DEFAULT_PURSUIT_OPTIONS.arrowheadLengthRatio);
|
|
15175
15640
|
const arrowheadWidth = diameter * Math.max(0, options.arrowheadWidthRatio ?? DEFAULT_PURSUIT_OPTIONS.arrowheadWidthRatio);
|
|
15176
|
-
const arrowhead = createArrowHead$
|
|
15641
|
+
const arrowhead = createArrowHead$2(p3, vecScale(straightDirection, -1), arrowheadLength, arrowheadWidth);
|
|
15177
15642
|
const terminalBarHalf = arrowheadWidth * Math.max(0, options.terminalBarLengthRatio ?? DEFAULT_PURSUIT_OPTIONS.terminalBarLengthRatio) / 2;
|
|
15178
15643
|
const terminalStart = vecAdd(p3, vecScale(diameterAxis, terminalBarHalf));
|
|
15179
15644
|
const terminalEnd = vecSub(p3, vecScale(diameterAxis, terminalBarHalf));
|
|
@@ -15489,7 +15954,7 @@ function createSecureSymbol(coordinates, options = {}, _textAmplifiers = {}, con
|
|
|
15489
15954
|
const lines = [
|
|
15490
15955
|
sampleArc(0, labelSweep - labelHalfGap),
|
|
15491
15956
|
sampleArc(labelSweep + labelHalfGap, boundarySpan),
|
|
15492
|
-
createArrowHead$
|
|
15957
|
+
createArrowHead$2(terminal, tangent, radius * Math.max(0, resolved.arrowheadLengthRatio), radius * Math.max(0, resolved.arrowheadWidthRatio))
|
|
15493
15958
|
];
|
|
15494
15959
|
const labelPoint = arcPointAt(center, radiusVector, bulgeVector, labelSweep);
|
|
15495
15960
|
return {
|
|
@@ -15684,7 +16149,7 @@ function createSeizeLikeSymbol(coordinates, options = {}, label = "S", part = "s
|
|
|
15684
16149
|
circle,
|
|
15685
16150
|
firstArcPart,
|
|
15686
16151
|
secondArcPart,
|
|
15687
|
-
createArrowHead$
|
|
16152
|
+
createArrowHead$2(tip, arrowDirection, span * Math.max(0, options.arrowheadLengthRatio ?? DEFAULT_SEIZE_OPTIONS.arrowheadLengthRatio), span * Math.max(0, options.arrowheadWidthRatio ?? DEFAULT_SEIZE_OPTIONS.arrowheadWidthRatio))
|
|
15688
16153
|
];
|
|
15689
16154
|
return {
|
|
15690
16155
|
type: "FeatureCollection",
|
|
@@ -16561,6 +17026,15 @@ const DEFAULT_MINEFIELD_OPTIONS = {
|
|
|
16561
17026
|
const DEFAULT_MINEFIELD_SIZE_METERS = 1e3;
|
|
16562
17027
|
const MINEFIELD_METADATA = {
|
|
16563
17028
|
id: "minefield",
|
|
17029
|
+
sizePairs: [{
|
|
17030
|
+
id: "size",
|
|
17031
|
+
label: "Size",
|
|
17032
|
+
pixels: "sizePixels",
|
|
17033
|
+
meters: "sizeMeters",
|
|
17034
|
+
defaultUnit: "px",
|
|
17035
|
+
rendering: "geometry",
|
|
17036
|
+
fallbackMeters: DEFAULT_MINEFIELD_SIZE_METERS
|
|
17037
|
+
}],
|
|
16564
17038
|
name: "Minefield",
|
|
16565
17039
|
description: "A static point symbol identifying a minefield and its mine type.",
|
|
16566
17040
|
entity: "Protection Areas",
|
|
@@ -17954,6 +18428,7 @@ const DEFAULT_SMOOTH_RESOLUTION = 16;
|
|
|
17954
18428
|
const DEFAULT_STRONG_POINT_OPTIONS = { ...DEFAULT_BATTLE_POSITION_OPTIONS };
|
|
17955
18429
|
const STRONG_POINT_METADATA = {
|
|
17956
18430
|
id: "strong-point",
|
|
18431
|
+
sizePairs: [ECHELON_SIZE_PAIR],
|
|
17957
18432
|
name: "Strong Point",
|
|
17958
18433
|
description: "A key point in a defensive position, usually strongly fortified and heavily armed, around which other positions are grouped for its protection.",
|
|
17959
18434
|
entity: "Maneuver Areas",
|
|
@@ -18698,6 +19173,10 @@ const DEFINITIONS = {
|
|
|
18698
19173
|
guard: GUARD,
|
|
18699
19174
|
isolate: ISOLATE,
|
|
18700
19175
|
"movement-to-contact": MOVEMENT_TO_CONTACT,
|
|
19176
|
+
destroy: DESTROY,
|
|
19177
|
+
defeat: DEFEAT,
|
|
19178
|
+
neutralize: NEUTRALIZE,
|
|
19179
|
+
suppress: SUPPRESS,
|
|
18701
19180
|
occupy: OCCUPY,
|
|
18702
19181
|
penetrate: PENETRATE,
|
|
18703
19182
|
pursuit: PURSUIT,
|
|
@@ -18751,18 +19230,27 @@ const DEFINITIONS = {
|
|
|
18751
19230
|
};
|
|
18752
19231
|
/** Insertion-ordered list of every control-measure id. */
|
|
18753
19232
|
const CONTROL_MEASURE_IDS = Object.keys(DEFINITIONS);
|
|
18754
|
-
/**
|
|
18755
|
-
|
|
18756
|
-
|
|
18757
|
-
|
|
18758
|
-
|
|
18759
|
-
|
|
18760
|
-
|
|
19233
|
+
/** The shared label dimension materialized for capture-enabled definitions. */
|
|
19234
|
+
const LABEL_SIZE_PAIR = {
|
|
19235
|
+
id: "label",
|
|
19236
|
+
label: "Label size",
|
|
19237
|
+
pixels: "labelSizePixels",
|
|
19238
|
+
meters: "labelSize",
|
|
19239
|
+
defaultUnit: "px",
|
|
19240
|
+
defaultValue: 14,
|
|
19241
|
+
rendering: "text",
|
|
19242
|
+
captureOnDraw: true
|
|
19243
|
+
};
|
|
19244
|
+
/** Resolve definition-owned metadata and the shared label-capture capability. */
|
|
19245
|
+
function resolveMetadata(definition) {
|
|
19246
|
+
const sizePairs = [...definition.metadata.sizePairs ?? [], ...definition.metadata.capturesLabelSize ? [LABEL_SIZE_PAIR] : []];
|
|
19247
|
+
return {
|
|
18761
19248
|
...definition.metadata,
|
|
18762
|
-
rule: definition.rule
|
|
18763
|
-
|
|
19249
|
+
...definition.rule ? { rule: definition.rule } : {},
|
|
19250
|
+
sizePairs
|
|
19251
|
+
};
|
|
18764
19252
|
}
|
|
18765
|
-
const CONTROL_MEASURE_METADATA = Object.fromEntries(Object.entries(DEFINITIONS).map(([id, definition]) => [id,
|
|
19253
|
+
const CONTROL_MEASURE_METADATA = Object.fromEntries(Object.entries(DEFINITIONS).map(([id, definition]) => [id, resolveMetadata(definition)]));
|
|
18766
19254
|
const CONTROL_MEASURE_METADATA_LIST = Object.values(CONTROL_MEASURE_METADATA);
|
|
18767
19255
|
const CONTROL_MEASURE_METADATA_BY_VALUE = CONTROL_MEASURE_METADATA_LIST.reduce((acc, metadata) => {
|
|
18768
19256
|
acc[metadata.value] = metadata;
|
|
@@ -19135,4 +19623,4 @@ function assertNever(value) {
|
|
|
19135
19623
|
throw new Error(`Unhandled control measure kind: ${String(value)}`);
|
|
19136
19624
|
}
|
|
19137
19625
|
//#endregion
|
|
19138
|
-
export { DEFAULT_PHASE_LINE_OPTIONS as $,
|
|
19626
|
+
export { DEFAULT_PHASE_LINE_OPTIONS as $, DEFAULT_LABEL_SIZE_CLAMP_CSS_PIXELS as $n, DEFAULT_DIRECTION_OF_SUPPORTING_ATTACK_OPTIONS as $t, DEFAULT_SECURE_OPTIONS as A, secureDrawRule as An, DEFAULT_GENERIC_CIRCLE_OPTIONS as At, DEFAULT_FORTIFIED_LINE_OPTIONS as B, getMidpointPerpendicularSignedDistance as Bn, DEFAULT_BLOCK_ARROW_OPTIONS as Bt, DEFAULT_LANDING_ZONE_OPTIONS as C, line1DrawRule as Cn, RADAR_SEARCH_DOCTRINE_FILL_COLOR as Ct, DEFAULT_WITHDRAW_OPTIONS as D, sectorDrawRule as Dn, DEFAULT_GENERIC_RECTANGLE_OPTIONS as Dt, DEFAULT_WITHDRAW_UNDER_PRESSURE_OPTIONS as E, dynamicPointDrawRule as En, DEFAULT_GENERIC_SECTOR_OPTIONS as Et, DEFAULT_PENETRATE_OPTIONS as F, centerRadiusDrawRule as Fn, DEFAULT_BLOCK_MISSION_TASK_OPTIONS as Ft, DEFAULT_LIMIT_OF_ADVANCE_OPTIONS as G, project as Gn, DEFAULT_ATTACK_BY_FIRE_OPTIONS as Gt, PROBABLE_LINE_OF_DEPLOYMENT_DASH as H, snapToMidpointPerpendicular as Hn, DEFAULT_CONTAIN_OPTIONS as Ht, DEFAULT_OCCUPY_OPTIONS as I, containDrawRule as In, DEFAULT_GENERIC_C2_LINE_OPTIONS as It, DEFAULT_FORWARD_EDGE_OF_BATTLE_AREA_OPTIONS as J, EPSILON as Jn, DEFAULT_ASSEMBLY_AREA_OPTIONS as Jt, DEFAULT_BATTLE_HANDOVER_LINE_OPTIONS as K, sphericalBearing as Kn, DEFAULT_ANTITANK_WALL_OPTIONS as Kt, DEFAULT_MOVEMENT_TO_CONTACT_OPTIONS as L, area27DrawRule as Ln, DEFAULT_ENGINEER_WORK_LINE_OPTIONS as Lt, DEFAULT_RETIRE_OPTIONS as M, occupyDrawRule as Mn, DEFAULT_CANALIZE_OPTIONS as Mt, DEFAULT_RELIEF_IN_PLACE_OPTIONS as N, disruptDrawRule as Nn, DEFAULT_BYPASS_OPTIONS as Nt, DEFAULT_EVACUATE_OPTIONS as O, staticPointDrawRule as On, DEFAULT_GENERIC_POLYGON_OPTIONS as Ot, DEFAULT_REARWARD_PASSAGE_OF_LINES_OPTIONS as P, blockDrawRule as Pn, DEFAULT_BREACH_OPTIONS as Pt, DEFAULT_BRIDGEHEAD_LINE_OPTIONS as Q, DEFAULT_LABEL_HEIGHT_CSS_PIXELS as Qn, DEFAULT_AREA_DEFENSE_OPTIONS as Qt, DEFAULT_FRONTAL_ATTACK_OPTIONS as R, computeDefaultMidpointPerpendicularPoint as Rn, DEFAULT_LIGHT_LINE_OPTIONS as Rt, DEFAULT_MAIN_ATTACK_OPTIONS as S, turnDrawRule as Sn, DEFAULT_RADAR_SEARCH_DOCTRINE_OPTIONS as St, DEFAULT_TURN_OPTIONS as T, attackByFireDrawRule as Tn, DEFAULT_GENERIC_TEXT_OPTIONS as Tt, DEFAULT_LINE_OF_DEPARTURE_CONTACT_OPTIONS as U, createBaselineFrame as Un, DEFAULT_BATTLE_POSITION_OPTIONS as Ut, DEFAULT_PROBABLE_LINE_OF_DEPLOYMENT_OPTIONS as V, pointOnMidpointPerpendicularAxis as Vn, DEFAULT_BLOCK_OPTIONS as Vt, DEFAULT_LINE_OF_DEPARTURE_OPTIONS as W, haversineDistance as Wn, DEFAULT_ATTACK_HELICOPTER_OPTIONS as Wt, DEFAULT_HOLDING_LINE_OPTIONS as X, isFiniteNumber as Xn, DEFAULT_MOBILE_DEFENSE_OPTIONS as Xt, DEFAULT_RELEASE_LINE_OPTIONS as Y, getMetersPerPixel as Yn, DEFAULT_AREA_OF_OPERATIONS_OPTIONS as Yt, DEFAULT_INFILTRATION_LANE_OPTIONS as Z, roundToFixed as Zn, DEFAULT_ENGAGEMENT_AREA_OPTIONS as Zt, DEFAULT_PICKUP_ZONE_OPTIONS as _, polyline1DrawRule as _n, DEFAULT_SUPPORTING_ATTACK_OPTIONS as _t, DEFINITIONS as a, resolveAmplifierPlacement as an, DEFAULT_SYMBOL_COLOR as ar, DEFAULT_DISRUPT_OPTIONS as at, DEFAULT_OBSTACLE_BYPASS_DIFFICULT_OPTIONS as b, line24DrawRule as bn, DEFAULT_ISOLATE_OPTIONS as bt, getDefaultOptions as c, DEFAULT_DESTROY_OPTIONS as cn, createFollowAndAssume as ct, DEFAULT_SUPPORT_BY_FIRE_OPTIONS as d, DEFAULT_DEFEAT_OPTIONS as dn, DEFAULT_DEMONSTRATE_OPTIONS as dt, DEFAULT_DIRECTION_OF_MAIN_ATTACK_OPTIONS as en, DEFAULT_LINE_CAP as er, DEFAULT_FLOT_OPTIONS as et, DEFAULT_RETAIN_OPTIONS as f, rectangleDrawRule as fn, DEFAULT_DELAY_OPTIONS as ft, DEFAULT_PRINCIPAL_DIRECTION_OF_FIRE_OPTIONS as g, supportByFireDrawRule as gn, DEFAULT_COUNTERATTACK_OPTIONS as gt, SECONDARY_DIRECTION_OF_FIRE_DASH as h, computeInitialWidthPoint as hn, DEFAULT_COUNTERATTACK_BY_FIRE_OPTIONS as ht, CONTROL_MEASURE_METADATA as i, normalizeTextAmplifiers as in, DEFAULT_STROKE_WIDTH_CSS_PIXELS as ir, DEFAULT_ENCIRCLEMENT_OPTIONS as it, DEFAULT_SCREEN_OPTIONS as j, penetrateDrawRule as jn, DEFAULT_CLASSIC_ARROW_OPTIONS as jt, DEFAULT_SEIZE_OPTIONS as k, point12DrawRule as kn, DEFAULT_GENERIC_LINE_OPTIONS as kt, listControlMeasureMetadata as l, DEFAULT_NEUTRALIZE_OPTIONS as ln, DEFAULT_DISRUPT_MISSION_TASK_OPTIONS as lt, DEFAULT_SECONDARY_DIRECTION_OF_FIRE_OPTIONS as m, calculateMetrics as mn, DEFAULT_TACTICAL_ARROW_OPTIONS as mt, resolveStyleHints as n, TEXT_AMPLIFIER_FIELDS as nn, DEFAULT_PORTRAYAL as nr, DEFAULT_FIX_OPTIONS as nt, getControlMeasureMetadata as o, DEFAULT_AMBUSH_OPTIONS as on, DEFAULT_GUARD_OPTIONS as ot, DEFAULT_STRONG_POINT_OPTIONS as p, axis1DrawRule as pn, DEFAULT_COVER_OPTIONS as pt, DEFAULT_HANDOVER_LINE_OPTIONS as q, unproject as qn, DEFAULT_ANTITANK_DITCH_OPTIONS as qt, CONTROL_MEASURE_IDS as r, canonicalTextAmplifierKey as rn, DEFAULT_STROKE_DASH_CSS_PIXELS as rr, DEFAULT_FINAL_PROTECTIVE_FIRE_OPTIONS as rt, getControlMeasureMetadataByValue as s, DEFAULT_AIRBORNE_ATTACK_OPTIONS as sn, createFollowAndSupport as st, renderControlMeasure as t, DEFAULT_DIRECTION_OF_ATTACK_AVIATION_OPTIONS as tn, DEFAULT_LINE_JOIN as tr, DEFAULT_FIX_MISSION_TASK_OPTIONS as tt, DEFAULT_TURNING_MOVEMENT_OPTIONS as u, DEFAULT_SUPPRESS_OPTIONS as un, DEFAULT_DISENGAGE_OPTIONS as ut, DEFAULT_OBSTACLE_BYPASS_IMPOSSIBLE_OPTIONS as v, line27DrawRule as vn, DEFAULT_CORDON_AND_SEARCH_OPTIONS as vt, DEFAULT_JOINT_TACTICAL_ACTION_AREA_OPTIONS as w, ambushDrawRule as wn, RADAR_SEARCH_DOCTRINE_STROKE_COLOR as wt, DEFAULT_MINEFIELD_OPTIONS as x, line23DrawRule as xn, DEFAULT_CLEAR_OPTIONS as xt, DEFAULT_OBSTACLE_BYPASS_EASY_OPTIONS as y, line26DrawRule as yn, DEFAULT_CORDON_AND_KNOCK_OPTIONS as yt, DEFAULT_FORTIFIED_AREA_OPTIONS as z, createMidpointPerpendicularDrawRule as zn, DEFAULT_BOUNDARY_OPTIONS as zt };
|