@orbat-mapper/control-measures 0.2.0-alpha.6 → 0.2.0-alpha.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as point12DrawRule, A as DEFAULT_GENERIC_POLYGON_OPTIONS, B as DEFAULT_ANTITANK_WALL_OPTIONS, C as DEFAULT_FIX_OPTIONS, D as DEFAULT_DELAY_OPTIONS, E as DEFAULT_DISRUPT_OPTIONS, F as DEFAULT_BREACH_OPTIONS, G as axis1DrawRule, H as DEFAULT_AMBUSH_OPTIONS, I as DEFAULT_BLOCK_MISSION_TASK_OPTIONS, J as line23DrawRule, K as supportByFireDrawRule, L as DEFAULT_BLOCK_OPTIONS, M as DEFAULT_GENERIC_CIRCLE_OPTIONS, N as DEFAULT_CANALIZE_OPTIONS, O as DEFAULT_CLEAR_OPTIONS, P as DEFAULT_BYPASS_OPTIONS, Q as attackByFireDrawRule, R as DEFAULT_ATTACK_HELICOPTER_OPTIONS, S as DEFAULT_FLOT_OPTIONS, T as DEFAULT_ENCIRCLEMENT_OPTIONS, U as DEFAULT_AIRBORNE_ATTACK_OPTIONS, V as DEFAULT_ANTITANK_DITCH_OPTIONS, W as rectangleDrawRule, X as line1DrawRule, Y as turnDrawRule, Z as ambushDrawRule, _ as DEFAULT_OBSTACLE_BYPASS_EASY_OPTIONS, a as DEFINITIONS, at as getMidpointPerpendicularSignedDistance, b as DEFAULT_FORTIFIED_AREA_OPTIONS, c as getDefaultOptions, ct as createBaselineFrame, d as DEFAULT_SUPPORTING_ATTACK_OPTIONS, dt as project, et as centerRadiusDrawRule, f as DEFAULT_SUPPORT_BY_FIRE_OPTIONS, ft as unproject, g as DEFAULT_OBSTACLE_BYPASS_IMPOSSIBLE_OPTIONS, h as DEFAULT_PRINCIPAL_DIRECTION_OF_FIRE_OPTIONS, ht as roundToFixed, i as CONTROL_MEASURE_METADATA, it as createMidpointPerpendicularDrawRule, j as DEFAULT_GENERIC_LINE_OPTIONS, k as DEFAULT_GENERIC_RECTANGLE_OPTIONS, l as listControlMeasureMetadata, lt as calculateMetrics, m as DEFAULT_TACTICAL_ARROW_OPTIONS, mt as getMetersPerPixel, n as resolveStyleHints, nt as blockDrawRule, o as getControlMeasureMetadata, ot as pointOnMidpointPerpendicularAxis, p as DEFAULT_STRONG_POINT_OPTIONS, pt as EPSILON, q as line24DrawRule, r as CONTROL_MEASURE_IDS, rt as computeDefaultMidpointPerpendicularPoint, s as getControlMeasureMetadataByValue, st as snapToMidpointPerpendicular, t as renderControlMeasure, tt as disruptDrawRule, u as DEFAULT_TURN_OPTIONS, ut as computeInitialWidthPoint, v as DEFAULT_OBSTACLE_BYPASS_DIFFICULT_OPTIONS, w as DEFAULT_FINAL_PROTECTIVE_FIRE_OPTIONS, x as DEFAULT_FORTIFIED_LINE_OPTIONS, y as DEFAULT_MAIN_ATTACK_OPTIONS, z as DEFAULT_ATTACK_BY_FIRE_OPTIONS } from "./renderControlMeasure-D5NKuws0.mjs";
1
+ import { $ as point12DrawRule, A as DEFAULT_GENERIC_POLYGON_OPTIONS, B as DEFAULT_ANTITANK_WALL_OPTIONS, C as DEFAULT_FIX_OPTIONS, D as DEFAULT_DELAY_OPTIONS, E as DEFAULT_DISRUPT_OPTIONS, F as DEFAULT_BREACH_OPTIONS, G as axis1DrawRule, H as DEFAULT_AMBUSH_OPTIONS, I as DEFAULT_BLOCK_MISSION_TASK_OPTIONS, J as line23DrawRule, K as supportByFireDrawRule, L as DEFAULT_BLOCK_OPTIONS, M as DEFAULT_GENERIC_CIRCLE_OPTIONS, N as DEFAULT_CANALIZE_OPTIONS, O as DEFAULT_CLEAR_OPTIONS, P as DEFAULT_BYPASS_OPTIONS, Q as attackByFireDrawRule, R as DEFAULT_ATTACK_HELICOPTER_OPTIONS, S as DEFAULT_FLOT_OPTIONS, T as DEFAULT_ENCIRCLEMENT_OPTIONS, U as DEFAULT_AIRBORNE_ATTACK_OPTIONS, V as DEFAULT_ANTITANK_DITCH_OPTIONS, W as rectangleDrawRule, X as line1DrawRule, Y as turnDrawRule, Z as ambushDrawRule, _ as DEFAULT_OBSTACLE_BYPASS_EASY_OPTIONS, a as DEFINITIONS, at as getMidpointPerpendicularSignedDistance, b as DEFAULT_FORTIFIED_AREA_OPTIONS, c as getDefaultOptions, ct as createBaselineFrame, d as DEFAULT_SUPPORTING_ATTACK_OPTIONS, dt as project, et as centerRadiusDrawRule, f as DEFAULT_SUPPORT_BY_FIRE_OPTIONS, ft as unproject, g as DEFAULT_OBSTACLE_BYPASS_IMPOSSIBLE_OPTIONS, h as DEFAULT_PRINCIPAL_DIRECTION_OF_FIRE_OPTIONS, ht as roundToFixed, i as CONTROL_MEASURE_METADATA, it as createMidpointPerpendicularDrawRule, j as DEFAULT_GENERIC_LINE_OPTIONS, k as DEFAULT_GENERIC_RECTANGLE_OPTIONS, l as listControlMeasureMetadata, lt as calculateMetrics, m as DEFAULT_TACTICAL_ARROW_OPTIONS, mt as getMetersPerPixel, n as resolveStyleHints, nt as blockDrawRule, o as getControlMeasureMetadata, ot as pointOnMidpointPerpendicularAxis, p as DEFAULT_STRONG_POINT_OPTIONS, pt as EPSILON, q as line24DrawRule, r as CONTROL_MEASURE_IDS, rt as computeDefaultMidpointPerpendicularPoint, s as getControlMeasureMetadataByValue, st as snapToMidpointPerpendicular, t as renderControlMeasure, tt as disruptDrawRule, u as DEFAULT_TURN_OPTIONS, ut as computeInitialWidthPoint, v as DEFAULT_OBSTACLE_BYPASS_DIFFICULT_OPTIONS, w as DEFAULT_FINAL_PROTECTIVE_FIRE_OPTIONS, x as DEFAULT_FORTIFIED_LINE_OPTIONS, y as DEFAULT_MAIN_ATTACK_OPTIONS, z as DEFAULT_ATTACK_BY_FIRE_OPTIONS } from "./renderControlMeasure-DGWf44sn.mjs";
2
2
  //#region src/freeze-orientation.ts
3
3
  /**
4
4
  * Dispatches to a measure kind's `freezeOrientation` hook (see
@@ -1,4 +1,4 @@
1
- import { a as DEFINITIONS, t as renderControlMeasure } from "../renderControlMeasure-D5NKuws0.mjs";
1
+ import { a as DEFINITIONS, t as renderControlMeasure } from "../renderControlMeasure-DGWf44sn.mjs";
2
2
  //#region src/preview/index.ts
3
3
  /**
4
4
  * Unitless `previewSample` points (~`[-1, 1]`) are scaled by this degree offset
@@ -181,7 +181,7 @@ function pointToInfiniteLineDistance(p, a, b) {
181
181
  }
182
182
  /**
183
183
  * Calculates intersection of two line segments p1-p2 and p3-p4.
184
- * Returns null if parallel. Assuming they cross as per design.
184
+ * Returns null if parallel or if the infinite lines intersect outside either segment.
185
185
  */
186
186
  function lineIntersection(p1, p2, p3, p4) {
187
187
  const x1 = p1[0], y1 = p1[1];
@@ -191,6 +191,8 @@ function lineIntersection(p1, p2, p3, p4) {
191
191
  const denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
192
192
  if (Math.abs(denom) < 1e-6) return null;
193
193
  const ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denom;
194
+ const ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denom;
195
+ if (ua < -1e-6 || ua > 1.000001 || ub < -1e-6 || ub > 1.000001) return null;
194
196
  return [x1 + ua * (x2 - x1), y1 + ua * (y2 - y1)];
195
197
  }
196
198
  //#endregion
@@ -218,15 +220,38 @@ function processAttackGeometry(coordinates, options = {}) {
218
220
  return { geometry: calculateSymbolGeometry(ptTip, ptWidth, spinePoints, shaftRatio, roundedBends, bendSegments) };
219
221
  }
220
222
  function calculateSymbolGeometry(ptTip, ptWidth, spine, shaftRatio, roundedBends, bendSegments) {
221
- const ptNeck = spine[spine.length - 1];
223
+ const initialNeck = spine[spine.length - 1];
224
+ if (!initialNeck) return null;
225
+ const initialTipDir = vecNorm(vecSub(ptTip, initialNeck));
226
+ if (vecMag(initialTipDir) < 1e-6) return null;
227
+ const headHalfWidth = pointToInfiniteLineDistance(ptWidth, initialNeck, ptTip);
228
+ const vecTipToWidth = vecSub(ptTip, ptWidth);
229
+ const requestedHeadLength = Math.max(EPSILON, vecDot(vecTipToWidth, initialTipDir));
230
+ const path = [...spine, ptTip];
231
+ let totalArcLength = 0;
232
+ for (let i = 1; i < path.length; i++) totalArcLength += vecMag(vecSub(path[i], path[i - 1]));
233
+ const minimumShaftLength = Math.max(EPSILON, totalArcLength * .01);
234
+ let distanceRemaining = clamp(requestedHeadLength, EPSILON, Math.max(EPSILON, totalArcLength - minimumShaftLength));
235
+ let ptBase = ptTip;
236
+ let remainingSpine = spine;
237
+ for (let i = path.length - 1; i > 0; i--) {
238
+ const segmentEnd = path[i];
239
+ const segmentStart = path[i - 1];
240
+ const segment = vecSub(segmentStart, segmentEnd);
241
+ const segmentLength = vecMag(segment);
242
+ if (segmentLength < 1e-6) continue;
243
+ if (distanceRemaining < segmentLength) {
244
+ ptBase = vecAdd(segmentEnd, vecScale(segment, distanceRemaining / segmentLength));
245
+ remainingSpine = path.slice(0, i);
246
+ break;
247
+ }
248
+ distanceRemaining -= segmentLength;
249
+ }
250
+ const ptNeck = remainingSpine[remainingSpine.length - 1];
222
251
  if (!ptNeck) return null;
223
252
  const tipDir = vecNorm(vecSub(ptTip, ptNeck));
224
253
  if (vecMag(tipDir) < 1e-6) return null;
225
- const headHalfWidth = pointToInfiniteLineDistance(ptWidth, ptNeck, ptTip);
226
- const vecTipToWidth = vecSub(ptTip, ptWidth);
227
- const maxHeadLength = Math.max(0, vecMag(vecSub(ptTip, ptNeck)) - .1);
228
- const headLength = clamp(vecDot(vecTipToWidth, tipDir), EPSILON, maxHeadLength);
229
- const ptBase = vecSub(ptTip, vecScale(tipDir, headLength));
254
+ const headLength = vecMag(vecSub(ptTip, ptBase));
230
255
  const shaftHalfWidth = headHalfWidth * shaftRatio;
231
256
  const perpDir = [-tipDir[1], tipDir[0]];
232
257
  const outerLeft = vecAdd(ptBase, vecScale(perpDir, headHalfWidth));
@@ -234,7 +259,8 @@ function calculateSymbolGeometry(ptTip, ptWidth, spine, shaftRatio, roundedBends
234
259
  const innerLeft = vecAdd(ptBase, vecScale(perpDir, shaftHalfWidth));
235
260
  const innerRight = vecAdd(ptBase, vecScale(perpDir, -shaftHalfWidth));
236
261
  const notchDepth = headLength * shaftRatio;
237
- const innerTip = vecAdd(ptBase, vecScale(tipDir, Math.min(notchDepth, headLength - EPSILON)));
262
+ const safeNotchDepth = Math.min(notchDepth, headLength - EPSILON);
263
+ const innerTip = vecAdd(ptBase, vecScale(tipDir, safeNotchDepth));
238
264
  const shaftEndCenter = ptBase;
239
265
  const headRing = [
240
266
  outerLeft,
@@ -245,7 +271,7 @@ function calculateSymbolGeometry(ptTip, ptWidth, spine, shaftRatio, roundedBends
245
271
  innerLeft,
246
272
  outerLeft
247
273
  ];
248
- const fullSpine = [...spine, shaftEndCenter];
274
+ const fullSpine = [...remainingSpine, shaftEndCenter];
249
275
  return {
250
276
  shaftLeft: offsetPolyline(fullSpine, shaftHalfWidth, roundedBends, bendSegments),
251
277
  shaftRight: offsetPolyline(fullSpine, -shaftHalfWidth, roundedBends, bendSegments),
@@ -1826,7 +1852,7 @@ function createAttackHelicopter(coordinates, options = {}) {
1826
1852
  type: "FeatureCollection",
1827
1853
  features: []
1828
1854
  };
1829
- const center = lineIntersection(pL, pIR, pR, pIL);
1855
+ const center = lineIntersection(pL, pIR, pR, pIL) ?? geometry.ptBase;
1830
1856
  const features = [{
1831
1857
  type: "Feature",
1832
1858
  properties: {},
@@ -2338,15 +2364,18 @@ function closedCatmullRom(points, samples) {
2338
2364
  }
2339
2365
  //#endregion
2340
2366
  //#region src/internal/area-echelon.ts
2341
- /** Mean of the ring's distinct vertices, used as the outward-normal reference. */
2342
- function ringCentroid(verts, count) {
2343
- let cx = 0;
2344
- let cy = 0;
2345
- for (let i = 0; i < count; i++) {
2346
- cx += verts[i][0];
2347
- cy += verts[i][1];
2367
+ /**
2368
+ * Twice the ring's signed area (shoelace). Positive when the ring winds
2369
+ * counter-clockwise in projected (y-up) coordinates.
2370
+ */
2371
+ function ringSignedArea2(verts) {
2372
+ let area2 = 0;
2373
+ for (let i = 0; i < verts.length - 1; i++) {
2374
+ const a = verts[i];
2375
+ const b = verts[i + 1];
2376
+ area2 += a[0] * b[1] - b[0] * a[1];
2348
2377
  }
2349
- return [cx / count, cy / count];
2378
+ return area2;
2350
2379
  }
2351
2380
  /**
2352
2381
  * Projects `positions` and returns a closed ring (first == last), optionally
@@ -2366,7 +2395,7 @@ function buildClosedRing(positions, smooth, smoothResolution, defaultSmoothResol
2366
2395
  * projected y), the side opposite the front.
2367
2396
  */
2368
2397
  function buildAreaPerimeter(verts) {
2369
- const centroid = ringCentroid(verts, verts.length - 1);
2398
+ const ccw = ringSignedArea2(verts) > 0;
2370
2399
  const segs = [];
2371
2400
  let totalLength = 0;
2372
2401
  let anchorTarget = 0;
@@ -2379,8 +2408,7 @@ function buildAreaPerimeter(verts) {
2379
2408
  if (length < 1e-6) continue;
2380
2409
  const along = vecNorm(edge);
2381
2410
  const mid = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
2382
- let perp = [-along[1], along[0]];
2383
- if (perp[0] * (mid[0] - centroid[0]) + perp[1] * (mid[1] - centroid[1]) < 0) perp = [-perp[0], -perp[1]];
2411
+ const perp = ccw ? [along[1], -along[0]] : [-along[1], along[0]];
2384
2412
  segs.push({
2385
2413
  start: a,
2386
2414
  along,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orbat-mapper/control-measures",
3
- "version": "0.2.0-alpha.6",
3
+ "version": "0.2.0-alpha.7",
4
4
  "description": "Library for drawing tactical graphics and control measures according to MIL-STD-2525 and APP-6 standards.",
5
5
  "license": "MIT",
6
6
  "author": "Orbat Mapper",