@lumikmz/kmz 0.11.1 → 0.11.2

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
@@ -366,7 +366,7 @@ function stabilizeTurnCorridors(segments) {
366
366
  * (entry climb is a missionConfig behaviour, not a grid concern).
367
367
  */
368
368
  function surveyGrid(ring, opts) {
369
- const { directionDeg, marginM, lineSpacing, phaseOffsetM = 0, lengthwiseShiftM = 0 } = opts;
369
+ const { directionDeg, marginM, lineSpacing, lengthwiseShiftM = 0 } = opts;
370
370
  const t = directionDeg * DEG2RAD$1;
371
371
  const rawAlpha = Math.atan2(Math.cos(t), Math.sin(t));
372
372
  const alpha = Math.abs(rawAlpha) < ANGLE_EPS_RAD ? 0 : rawAlpha;
@@ -377,6 +377,7 @@ function surveyGrid(ring, opts) {
377
377
  if (y < minY) minY = y;
378
378
  if (y > maxY) maxY = y;
379
379
  }
380
+ const phaseOffsetM = opts.phaseOffsetM ?? (maxY - minY <= lineSpacing ? (maxY - minY) / 2 : 0);
380
381
  const scanSegments = [];
381
382
  let leftToRight = true;
382
383
  for (let y = minY + phaseOffsetM; y <= maxY + lineSpacing * .5; y += lineSpacing) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumikmz/kmz",
3
- "version": "0.11.1",
3
+ "version": "0.11.2",
4
4
  "description": "DJI KMZ/WPML domain model + plan() algorithm + validate(), spec-aligned types mirroring docs/kmz/",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -71,6 +71,55 @@ describe("rotateEnu", () => {
71
71
  });
72
72
 
73
73
  describe("surveyGrid", () => {
74
+ it.each([
75
+ { directionDeg: 90, lineSpacing: 100, start: [-100, 0], end: [100, 0] },
76
+ { directionDeg: 90, lineSpacing: 200, start: [-100, 0], end: [100, 0] },
77
+ { directionDeg: 0, lineSpacing: 200, start: [0, -50], end: [0, 50] },
78
+ { directionDeg: 0, lineSpacing: 400, start: [0, -50], end: [0, 50] },
79
+ { directionDeg: 270, lineSpacing: 200, start: [100, 0], end: [-100, 0] },
80
+ ])(
81
+ "centers a narrow area's flight line at heading $directionDeg with spacing $lineSpacing",
82
+ ({ directionDeg, lineSpacing, start, end }) => {
83
+ const diamond: EnuPoint[] = [
84
+ [0, -50],
85
+ [100, 0],
86
+ [0, 50],
87
+ [-100, 0],
88
+ ];
89
+ const pts = surveyGrid(diamond, { directionDeg, marginM: 0, lineSpacing });
90
+
91
+ expect(pts).toHaveLength(2);
92
+ expect(pts[0]![0]).toBeCloseTo(start[0]!, 6);
93
+ expect(pts[0]![1]).toBeCloseTo(start[1]!, 6);
94
+ expect(pts[1]![0]).toBeCloseTo(end[0]!, 6);
95
+ expect(pts[1]![1]).toBeCloseTo(end[1]!, 6);
96
+ },
97
+ );
98
+
99
+ it.each([
100
+ { phaseOffsetM: 0, expected: [] },
101
+ {
102
+ phaseOffsetM: 25,
103
+ expected: [
104
+ [-50, -25],
105
+ [50, -25],
106
+ ],
107
+ },
108
+ ])(
109
+ "preserves an explicit phase offset of $phaseOffsetM in a narrow area",
110
+ ({ phaseOffsetM, expected }) => {
111
+ const diamond: EnuPoint[] = [
112
+ [0, -50],
113
+ [100, 0],
114
+ [0, 50],
115
+ [-100, 0],
116
+ ];
117
+ expect(
118
+ surveyGrid(diamond, { directionDeg: 90, marginM: 0, lineSpacing: 200, phaseOffsetM }),
119
+ ).toEqual(expected);
120
+ },
121
+ );
122
+
74
123
  it("scans a square and returns paired flight-line endpoints", () => {
75
124
  const pts = surveyGrid(SQUARE, { directionDeg: 90, marginM: 0, lineSpacing: 25 });
76
125
  expect(pts.length).toBeGreaterThanOrEqual(8); // ≥4 lines × 2 endpoints
@@ -198,7 +198,8 @@ export interface SurveyGridOptions {
198
198
  /**
199
199
  * Across-track phase shift (meters) applied to every flight line.
200
200
  * Used to interleave two grids on the same axis by half a line spacing
201
- * (e.g. mapping3d forward vs backward oblique passes). Default 0.
201
+ * (e.g. mapping3d forward vs backward oblique passes). Defaults to 0,
202
+ * or a centered line when the area is no wider than one line spacing.
202
203
  */
203
204
  phaseOffsetM?: number;
204
205
  /**
@@ -263,7 +264,7 @@ function stabilizeTurnCorridors(segments: ScanSegment[]): EnuPoint[] {
263
264
  * (entry climb is a missionConfig behaviour, not a grid concern).
264
265
  */
265
266
  export function surveyGrid(ring: readonly EnuPoint[], opts: SurveyGridOptions): EnuPoint[] {
266
- const { directionDeg, marginM, lineSpacing, phaseOffsetM = 0, lengthwiseShiftM = 0 } = opts;
267
+ const { directionDeg, marginM, lineSpacing, lengthwiseShiftM = 0 } = opts;
267
268
  // Map the heading clockwise-from-north → CCW angle from +x axis
268
269
  const t = directionDeg * DEG2RAD;
269
270
  const rawAlpha = Math.atan2(Math.cos(t), Math.sin(t));
@@ -277,6 +278,9 @@ export function surveyGrid(ring: readonly EnuPoint[], opts: SurveyGridOptions):
277
278
  if (y > maxY) maxY = y;
278
279
  }
279
280
 
281
+ // A boundary scan may only touch a vertex. Center a single line in narrow
282
+ // areas, while retaining explicit phases used by interleaved/strip grids.
283
+ const phaseOffsetM = opts.phaseOffsetM ?? (maxY - minY <= lineSpacing ? (maxY - minY) / 2 : 0);
280
284
  const scanSegments: ScanSegment[] = [];
281
285
  let leftToRight = true;
282
286
  for (let y = minY + phaseOffsetM; y <= maxY + lineSpacing * 0.5; y += lineSpacing) {