@lumikmz/kmz 0.10.0 → 0.11.1
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.d.mts +177 -130
- package/dist/index.mjs +367 -117
- package/package.json +2 -3
- package/src/plan/geometry.test.ts +1 -1
- package/src/plan/plan-mapping-strip-corners.test.ts +349 -0
- package/src/plan/plan-mapping-strip.test.ts +134 -3
- package/src/plan/plan-mapping-strip.ts +366 -159
- package/src/plan/plan-target-detection.test.ts +44 -1
- package/src/plan/plan-target-detection.ts +27 -2
- package/src/plan/plan.test.ts +1 -1
- package/src/plan/strip-route-order.test.ts +241 -0
- package/src/plan/strip-route-order.ts +192 -0
- package/src/types/action.ts +26 -0
- package/src/types/drone-payload.ts +2 -0
- package/src/types/enums.ts +1 -0
- package/src/types/placemark.ts +4 -1
- package/vite.config.ts +7 -0
package/dist/index.mjs
CHANGED
|
@@ -944,13 +944,14 @@ function planMapping3d(template, options) {
|
|
|
944
944
|
missionConfig: template.missionConfig,
|
|
945
945
|
Folder: PASSES.map((pass, i) => {
|
|
946
946
|
const passSpacing = pass.pitch === "inclined" ? obliqueSpacing : lineSpacing;
|
|
947
|
-
|
|
947
|
+
const enuWps = orientPathToHome(surveyGrid(ringEnu, {
|
|
948
948
|
directionDeg: baseDirection + pass.axisOffsetDeg,
|
|
949
949
|
marginM: folder.Placemark.margin,
|
|
950
950
|
lineSpacing: passSpacing,
|
|
951
951
|
phaseOffsetM: pass.halfPhase ? passSpacing / 2 : 0,
|
|
952
952
|
lengthwiseShiftM: -pass.lookSign * standoff
|
|
953
|
-
}), origin, template.missionConfig.takeOffRefPoint)
|
|
953
|
+
}), origin, template.missionConfig.takeOffRefPoint);
|
|
954
|
+
return buildPass(folder, i, origin, enuWps, {
|
|
954
955
|
headingMode: pass.headingMode,
|
|
955
956
|
headingAngle: degrees(wrap180(baseDirection + pass.headingOffsetDeg)),
|
|
956
957
|
gimbalPitch: pass.pitch === "nadir" ? degrees(-90) : inclined,
|
|
@@ -1053,9 +1054,113 @@ function heightModeToExecuteMode$2(mode) {
|
|
|
1053
1054
|
}
|
|
1054
1055
|
}
|
|
1055
1056
|
//#endregion
|
|
1057
|
+
//#region src/plan/strip-route-order.ts
|
|
1058
|
+
const HEADING_COST_M = 10;
|
|
1059
|
+
const JUNCTION_DETOUR_WEIGHT = .5;
|
|
1060
|
+
const NEAR_EQUAL_TIME_S = 2;
|
|
1061
|
+
const TIME_EPS_S = 1e-8;
|
|
1062
|
+
const distance = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1]);
|
|
1063
|
+
const headingChange = (a, b) => 1 - Math.max(-1, Math.min(1, a[0] * b[0] + a[1] * b[1]));
|
|
1064
|
+
const DEFAULT_TIMING = {
|
|
1065
|
+
flightSpeed: 1,
|
|
1066
|
+
transitSpeed: 1,
|
|
1067
|
+
finishAction: "noAction"
|
|
1068
|
+
};
|
|
1069
|
+
function validateTiming(timing) {
|
|
1070
|
+
if (![timing.flightSpeed, timing.transitSpeed].every((speed) => Number.isFinite(speed) && speed > 0)) throw new PlannerError("mappingStrip flight and transit speeds must be finite and greater than zero");
|
|
1071
|
+
}
|
|
1072
|
+
function finishDistance(end, first, home, finish) {
|
|
1073
|
+
const target = finish === "goHome" ? home : finish === "gotoFirstWaypoint" ? first : void 0;
|
|
1074
|
+
return target ? distance(end, target) : 0;
|
|
1075
|
+
}
|
|
1076
|
+
/** Horizontal-flight estimate; vertical motion, wind and acceleration are not modeled. */
|
|
1077
|
+
function measureStripMission(routes, home, timing) {
|
|
1078
|
+
validateTiming(timing);
|
|
1079
|
+
if (routes.length === 0) return {
|
|
1080
|
+
distance: 0,
|
|
1081
|
+
duration: 0
|
|
1082
|
+
};
|
|
1083
|
+
const first = routes[0].start;
|
|
1084
|
+
const workLength = routes.reduce((sum, route) => sum + route.length, 0);
|
|
1085
|
+
let transitLength = home ? distance(home, first) : 0;
|
|
1086
|
+
for (let index = 1; index < routes.length; index++) transitLength += distance(routes[index - 1].end, routes[index].start);
|
|
1087
|
+
transitLength += finishDistance(routes.at(-1).end, first, home, timing.finishAction);
|
|
1088
|
+
return {
|
|
1089
|
+
distance: workLength + transitLength,
|
|
1090
|
+
duration: workLength / timing.flightSpeed + transitLength / timing.transitSpeed
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
function transitionPreference(previous, next, junction) {
|
|
1094
|
+
const length = distance(previous.end, next.start);
|
|
1095
|
+
const direction = length > .05 ? [(next.start[0] - previous.end[0]) / length, (next.start[1] - previous.end[1]) / length] : void 0;
|
|
1096
|
+
const turn = direction ? headingChange(previous.exitDirection, direction) + headingChange(direction, next.entryDirection) : headingChange(previous.exitDirection, next.entryDirection);
|
|
1097
|
+
const detour = junction ? Math.max(0, distance(previous.end, junction) + distance(junction, next.start) - length) : 0;
|
|
1098
|
+
return HEADING_COST_M * turn + JUNCTION_DETOUR_WEIGHT * detour;
|
|
1099
|
+
}
|
|
1100
|
+
/** Optimize mission time, then favor nearer entries within a two-second total budget. */
|
|
1101
|
+
function selectStripRouteCandidates(groups, home, junctions = [], timing = DEFAULT_TIMING) {
|
|
1102
|
+
if (groups.length === 0) return [];
|
|
1103
|
+
validateTiming(timing);
|
|
1104
|
+
if (groups.some((group) => group.length === 0)) throw new PlannerError("mappingStrip region has no route candidates");
|
|
1105
|
+
const finals = [];
|
|
1106
|
+
groups[0].forEach((first, firstIndex) => {
|
|
1107
|
+
const ingress = home ? distance(home, first.start) : 0;
|
|
1108
|
+
let times = groups[0].map((candidate, index) => index === firstIndex ? candidate.length / timing.flightSpeed + ingress / timing.transitSpeed : Infinity);
|
|
1109
|
+
let preferences = groups[0].map((_, index) => index === firstIndex ? 0 : Infinity);
|
|
1110
|
+
const back = [groups[0].map(() => -1)];
|
|
1111
|
+
for (let index = 1; index < groups.length; index++) {
|
|
1112
|
+
const parents = [];
|
|
1113
|
+
const nextPreferences = [];
|
|
1114
|
+
times = groups[index].map((candidate) => {
|
|
1115
|
+
let bestTime = Infinity;
|
|
1116
|
+
let bestPreference = Infinity;
|
|
1117
|
+
let parent = 0;
|
|
1118
|
+
groups[index - 1].forEach((previous, previousIndex) => {
|
|
1119
|
+
const time = times[previousIndex] + candidate.length / timing.flightSpeed + distance(previous.end, candidate.start) / timing.transitSpeed;
|
|
1120
|
+
const preference = preferences[previousIndex] + transitionPreference(previous, candidate, junctions[index - 1]);
|
|
1121
|
+
if (time < bestTime - TIME_EPS_S || Math.abs(time - bestTime) <= TIME_EPS_S && preference < bestPreference) {
|
|
1122
|
+
bestTime = time;
|
|
1123
|
+
bestPreference = preference;
|
|
1124
|
+
parent = previousIndex;
|
|
1125
|
+
}
|
|
1126
|
+
});
|
|
1127
|
+
parents.push(parent);
|
|
1128
|
+
nextPreferences.push(bestPreference);
|
|
1129
|
+
return bestTime;
|
|
1130
|
+
});
|
|
1131
|
+
preferences = nextPreferences;
|
|
1132
|
+
back.push(parents);
|
|
1133
|
+
}
|
|
1134
|
+
groups.at(-1).forEach((last, lastIndex) => {
|
|
1135
|
+
if (!Number.isFinite(times[lastIndex])) return;
|
|
1136
|
+
finals.push({
|
|
1137
|
+
lastIndex,
|
|
1138
|
+
ingress,
|
|
1139
|
+
back,
|
|
1140
|
+
time: times[lastIndex] + finishDistance(last.end, first.start, home, timing.finishAction) / timing.transitSpeed,
|
|
1141
|
+
preference: preferences[lastIndex]
|
|
1142
|
+
});
|
|
1143
|
+
});
|
|
1144
|
+
});
|
|
1145
|
+
const fastest = Math.min(...finals.map((choice) => choice.time));
|
|
1146
|
+
const chosen = finals.filter((choice) => choice.time <= fastest + NEAR_EQUAL_TIME_S + TIME_EPS_S).reduce((best, candidate) => {
|
|
1147
|
+
if (candidate.ingress < best.ingress - .05) return candidate;
|
|
1148
|
+
if (Math.abs(candidate.ingress - best.ingress) > .05) return best;
|
|
1149
|
+
if (candidate.preference < best.preference - TIME_EPS_S) return candidate;
|
|
1150
|
+
if (Math.abs(candidate.preference - best.preference) <= TIME_EPS_S && candidate.time < best.time) return candidate;
|
|
1151
|
+
return best;
|
|
1152
|
+
});
|
|
1153
|
+
let selected = chosen.lastIndex;
|
|
1154
|
+
const result = groups.map(() => 0);
|
|
1155
|
+
for (let index = groups.length - 1; index >= 0; index--) {
|
|
1156
|
+
result[index] = selected;
|
|
1157
|
+
selected = chosen.back[index][selected];
|
|
1158
|
+
}
|
|
1159
|
+
return result;
|
|
1160
|
+
}
|
|
1161
|
+
//#endregion
|
|
1056
1162
|
//#region src/plan/plan-mapping-strip.ts
|
|
1057
1163
|
const POINT_EPS_M = .05;
|
|
1058
|
-
const MITER_LIMIT = 2.5;
|
|
1059
1164
|
const MAX_ROUTE_COUNT = 1e3;
|
|
1060
1165
|
const TURN_DAMPING_M = 10;
|
|
1061
1166
|
function parseTriples(s) {
|
|
@@ -1122,47 +1227,17 @@ function localRouteCandidate(lines, centerLine, elevationOptimizeEnable) {
|
|
|
1122
1227
|
});
|
|
1123
1228
|
}
|
|
1124
1229
|
const executable = joinSegmentPoints(segments);
|
|
1230
|
+
const distinct = cleanLine(executable);
|
|
1125
1231
|
return {
|
|
1126
1232
|
lines,
|
|
1127
1233
|
segments,
|
|
1128
1234
|
start: executable[0],
|
|
1129
|
-
end: executable.at(-1)
|
|
1235
|
+
end: executable.at(-1),
|
|
1236
|
+
entryDirection: segmentUnit(distinct[0], distinct[1]),
|
|
1237
|
+
exitDirection: segmentUnit(distinct.at(-2), distinct.at(-1)),
|
|
1238
|
+
length: lineLength(executable)
|
|
1130
1239
|
};
|
|
1131
1240
|
}
|
|
1132
|
-
function shortestCandidateIndexes(candidateGroups, startPoint) {
|
|
1133
|
-
if (candidateGroups.length === 0) return [];
|
|
1134
|
-
let costs = candidateGroups[0].map((candidate) => startPoint ? Math.hypot(candidate.start[0] - startPoint[0], candidate.start[1] - startPoint[1]) : 0);
|
|
1135
|
-
const previousIndexes = [candidateGroups[0].map(() => -1)];
|
|
1136
|
-
for (let regionIndex = 1; regionIndex < candidateGroups.length; regionIndex++) {
|
|
1137
|
-
const previousCandidates = candidateGroups[regionIndex - 1];
|
|
1138
|
-
const candidates = candidateGroups[regionIndex];
|
|
1139
|
-
const nextCosts = [];
|
|
1140
|
-
const nextPreviousIndexes = [];
|
|
1141
|
-
for (const candidate of candidates) {
|
|
1142
|
-
let bestCost = Number.POSITIVE_INFINITY;
|
|
1143
|
-
let bestPreviousIndex = 0;
|
|
1144
|
-
previousCandidates.forEach((previous, previousIndex) => {
|
|
1145
|
-
const transition = Math.hypot(candidate.start[0] - previous.end[0], candidate.start[1] - previous.end[1]);
|
|
1146
|
-
const cost = costs[previousIndex] + transition;
|
|
1147
|
-
if (cost < bestCost) {
|
|
1148
|
-
bestCost = cost;
|
|
1149
|
-
bestPreviousIndex = previousIndex;
|
|
1150
|
-
}
|
|
1151
|
-
});
|
|
1152
|
-
nextCosts.push(bestCost);
|
|
1153
|
-
nextPreviousIndexes.push(bestPreviousIndex);
|
|
1154
|
-
}
|
|
1155
|
-
costs = nextCosts;
|
|
1156
|
-
previousIndexes.push(nextPreviousIndexes);
|
|
1157
|
-
}
|
|
1158
|
-
let selectedIndex = costs.reduce((best, cost, index) => cost < costs[best] ? index : best, 0);
|
|
1159
|
-
const selected = Array.from({ length: candidateGroups.length }, () => 0);
|
|
1160
|
-
for (let regionIndex = candidateGroups.length - 1; regionIndex >= 0; regionIndex--) {
|
|
1161
|
-
selected[regionIndex] = selectedIndex;
|
|
1162
|
-
selectedIndex = previousIndexes[regionIndex][selectedIndex];
|
|
1163
|
-
}
|
|
1164
|
-
return selected;
|
|
1165
|
-
}
|
|
1166
1241
|
function joinSegmentPoints(segments) {
|
|
1167
1242
|
const result = [];
|
|
1168
1243
|
for (const segment of segments) if (result.length === 0) result.push(...segment.points);
|
|
@@ -1271,35 +1346,41 @@ function segmentUnit(a, b) {
|
|
|
1271
1346
|
function leftNormal(direction) {
|
|
1272
1347
|
return [-direction[1], direction[0]];
|
|
1273
1348
|
}
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
const
|
|
1277
|
-
const out = [];
|
|
1278
|
-
const firstNormal = leftNormal(directions[0]);
|
|
1279
|
-
out.push([points[0][0] + firstNormal[0] * distance, points[0][1] + firstNormal[1] * distance]);
|
|
1349
|
+
/** Simplify geometry only; source control points still determine distance cuts. */
|
|
1350
|
+
function simplifyStraightRuns(points) {
|
|
1351
|
+
const result = [points[0]];
|
|
1280
1352
|
for (let index = 1; index < points.length - 1; index++) {
|
|
1281
|
-
const
|
|
1282
|
-
const
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
const previousOffset = [points[index][0] + previousNormal[0] * distance, points[index][1] + previousNormal[1] * distance];
|
|
1286
|
-
const nextOffset = [points[index][0] + nextNormal[0] * distance, points[index][1] + nextNormal[1] * distance];
|
|
1287
|
-
const denominator = cross(previousDirection, nextDirection);
|
|
1288
|
-
if (Math.abs(denominator) < 1e-9) {
|
|
1289
|
-
out.push(previousOffset);
|
|
1290
|
-
continue;
|
|
1291
|
-
}
|
|
1292
|
-
const t = cross([nextOffset[0] - previousOffset[0], nextOffset[1] - previousOffset[1]], nextDirection) / denominator;
|
|
1293
|
-
const intersection = [previousOffset[0] + previousDirection[0] * t, previousOffset[1] + previousDirection[1] * t];
|
|
1294
|
-
if (Math.hypot(intersection[0] - points[index][0], intersection[1] - points[index][1]) <= Math.abs(distance) * MITER_LIMIT) out.push(intersection);
|
|
1295
|
-
else out.push(previousOffset, nextOffset);
|
|
1353
|
+
const incoming = segmentUnit(result.at(-1), points[index]);
|
|
1354
|
+
const outgoing = segmentUnit(points[index], points[index + 1]);
|
|
1355
|
+
if (Math.abs(cross(incoming, outgoing)) < 1e-8 && incoming[0] * outgoing[0] + incoming[1] * outgoing[1] > 0) continue;
|
|
1356
|
+
result.push(points[index]);
|
|
1296
1357
|
}
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1358
|
+
result.push(points.at(-1));
|
|
1359
|
+
return result;
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* FlightHub square joins extend both outer offset edges to a line perpendicular
|
|
1363
|
+
* to their bisector, one offset distance from the source vertex. Inner joins
|
|
1364
|
+
* keep their true intersection: beveling a long inner miter creates a bow tie.
|
|
1365
|
+
*/
|
|
1366
|
+
function offsetJoint(point, incoming, outgoing, distance) {
|
|
1367
|
+
const previousNormal = leftNormal(incoming);
|
|
1368
|
+
const nextNormal = leftNormal(outgoing);
|
|
1369
|
+
const previousOffset = [point[0] + previousNormal[0] * distance, point[1] + previousNormal[1] * distance];
|
|
1370
|
+
const nextOffset = [point[0] + nextNormal[0] * distance, point[1] + nextNormal[1] * distance];
|
|
1371
|
+
const denominator = cross(incoming, outgoing);
|
|
1372
|
+
if (Math.abs(denominator) < 1e-9) return [previousOffset];
|
|
1373
|
+
if (denominator * distance < 0) {
|
|
1374
|
+
const turn = Math.atan2(denominator, incoming[0] * outgoing[0] + incoming[1] * outgoing[1]);
|
|
1375
|
+
const extension = Math.abs(distance) * Math.tan(Math.abs(turn) / 4);
|
|
1376
|
+
return [[previousOffset[0] + incoming[0] * extension, previousOffset[1] + incoming[1] * extension], [nextOffset[0] - outgoing[0] * extension, nextOffset[1] - outgoing[1] * extension]];
|
|
1377
|
+
}
|
|
1378
|
+
const t = cross([nextOffset[0] - previousOffset[0], nextOffset[1] - previousOffset[1]], outgoing) / denominator;
|
|
1379
|
+
return [[previousOffset[0] + incoming[0] * t, previousOffset[1] + incoming[1] * t]];
|
|
1301
1380
|
}
|
|
1302
|
-
function
|
|
1381
|
+
function offsetSide(points, distance, beforePoint, afterPoint) {
|
|
1382
|
+
if (distance === 0) return points.map((point) => [point[0], point[1]]);
|
|
1383
|
+
points = simplifyStraightRuns(points);
|
|
1303
1384
|
const out = [];
|
|
1304
1385
|
for (let index = 0; index < points.length; index++) {
|
|
1305
1386
|
const point = points[index];
|
|
@@ -1316,56 +1397,119 @@ function corridorSide(points, distance, beforePoint, afterPoint) {
|
|
|
1316
1397
|
continue;
|
|
1317
1398
|
}
|
|
1318
1399
|
if (!previous || !next) continue;
|
|
1319
|
-
const
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1400
|
+
const joint = offsetJoint(point, segmentUnit(previous, point), segmentUnit(point, next), distance);
|
|
1401
|
+
if (joint.length === 2 && (index === 0 || index === points.length - 1)) {
|
|
1402
|
+
const midpoint = [(joint[0][0] + joint[1][0]) / 2, (joint[0][1] + joint[1][1]) / 2];
|
|
1403
|
+
out.push(...index === 0 ? [midpoint, joint[1]] : [joint[0], midpoint]);
|
|
1404
|
+
} else out.push(...joint);
|
|
1405
|
+
}
|
|
1406
|
+
return cleanLine(out);
|
|
1407
|
+
}
|
|
1408
|
+
function validateCorridorJoins(points, left, right) {
|
|
1409
|
+
points = simplifyStraightRuns(points);
|
|
1410
|
+
const directions = points.slice(1).map((point, index) => segmentUnit(points[index], point));
|
|
1411
|
+
const leftCuts = points.map(() => 0);
|
|
1412
|
+
const rightCuts = points.map(() => 0);
|
|
1413
|
+
for (let index = 1; index < points.length - 1; index++) {
|
|
1414
|
+
const incoming = directions[index - 1];
|
|
1415
|
+
const outgoing = directions[index];
|
|
1416
|
+
const dot = incoming[0] * outgoing[0] + incoming[1] * outgoing[1];
|
|
1417
|
+
const turn = Math.atan2(cross(incoming, outgoing), dot);
|
|
1418
|
+
if (Math.PI - Math.abs(turn) < 1e-6) throw new PlannerError("mappingStrip center line reverses direction at a corner");
|
|
1419
|
+
const tangent = Math.tan(Math.abs(turn) / 2);
|
|
1420
|
+
if (turn > 0) leftCuts[index] = left * tangent;
|
|
1421
|
+
else rightCuts[index] = right * tangent;
|
|
1422
|
+
}
|
|
1423
|
+
for (let index = 0; index < directions.length; index++) {
|
|
1424
|
+
const length = Math.hypot(points[index + 1][0] - points[index][0], points[index + 1][1] - points[index][1]);
|
|
1425
|
+
if (Math.max(leftCuts[index] + leftCuts[index + 1], rightCuts[index] + rightCuts[index + 1]) >= length - POINT_EPS_M) throw new PlannerError("mappingStrip corner offsets exceed the available center-line segment");
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
function corridorPolygon(points, left, right, beforePoint, afterPoint) {
|
|
1429
|
+
return [...offsetSide(points, left, beforePoint, afterPoint), ...offsetSide(points, -right, beforePoint, afterPoint).reverse()];
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Bisector normal of the joint shared by two regions at a cut vertex. The seam is
|
|
1433
|
+
* the line through the vertex perpendicular to this normal, i.e. the angle
|
|
1434
|
+
* bisector: the incoming region keeps `dot <= 0`, the outgoing region `dot >= 0`.
|
|
1435
|
+
*/
|
|
1436
|
+
function jointSeamNormal(incoming, outgoing) {
|
|
1437
|
+
const normal = [incoming[0] + outgoing[0], incoming[1] + outgoing[1]];
|
|
1438
|
+
const length = Math.hypot(normal[0], normal[1]);
|
|
1439
|
+
return length < 1e-9 ? void 0 : [normal[0] / length, normal[1] / length];
|
|
1440
|
+
}
|
|
1441
|
+
/** Keep the half-plane `dot(normal, point - origin) <= 0` (Sutherland–Hodgman). */
|
|
1442
|
+
function clipHalfPlane(ring, origin, normal) {
|
|
1443
|
+
const signedDistance = (point) => {
|
|
1444
|
+
const distance = normal[0] * (point[0] - origin[0]) + normal[1] * (point[1] - origin[1]);
|
|
1445
|
+
return Math.abs(distance) < 1e-8 ? 0 : distance;
|
|
1446
|
+
};
|
|
1447
|
+
const out = [];
|
|
1448
|
+
const push = (point) => {
|
|
1449
|
+
const last = out.at(-1);
|
|
1450
|
+
if (last && Math.hypot(point[0] - last[0], point[1] - last[1]) < POINT_EPS_M) return;
|
|
1451
|
+
out.push(point);
|
|
1452
|
+
};
|
|
1453
|
+
for (let index = 0; index < ring.length; index++) {
|
|
1454
|
+
const a = ring[index];
|
|
1455
|
+
const b = ring[(index + 1) % ring.length];
|
|
1456
|
+
const da = signedDistance(a);
|
|
1457
|
+
const db = signedDistance(b);
|
|
1458
|
+
if (da <= 0) push([a[0], a[1]]);
|
|
1459
|
+
if (da * db < 0) {
|
|
1460
|
+
const t = da / (da - db);
|
|
1461
|
+
push([a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]);
|
|
1333
1462
|
}
|
|
1334
|
-
const t = cross([nextOffset[0] - previousOffset[0], nextOffset[1] - previousOffset[1]], nextDirection) / denominator;
|
|
1335
|
-
const intersection = [previousOffset[0] + previousDirection[0] * t, previousOffset[1] + previousDirection[1] * t];
|
|
1336
|
-
if (Math.hypot(intersection[0] - point[0], intersection[1] - point[1]) <= Math.abs(distance) * MITER_LIMIT) out.push(intersection);
|
|
1337
|
-
else out.push(previousOffset, nextOffset);
|
|
1338
1463
|
}
|
|
1464
|
+
while (out.length > 1 && Math.hypot(out[0][0] - out.at(-1)[0], out[0][1] - out.at(-1)[1]) < POINT_EPS_M) out.pop();
|
|
1339
1465
|
return out;
|
|
1340
1466
|
}
|
|
1341
|
-
|
|
1342
|
-
|
|
1467
|
+
/**
|
|
1468
|
+
* Adjacent regions share a cut vertex and each included the full corridor joint
|
|
1469
|
+
* there, so their rings used to overlap at the corner. Cut every shared end
|
|
1470
|
+
* along the bisector seam instead: both rings then tile the corridor edge to
|
|
1471
|
+
* edge while their union (the outer contour) is unchanged. A shorter join is not
|
|
1472
|
+
* enough here — when the path folds back, the two corridor bands cross and the
|
|
1473
|
+
* overlap extends far beyond the corner patch, which only a half-plane cut
|
|
1474
|
+
* removes.
|
|
1475
|
+
*/
|
|
1476
|
+
function trimJointOverlap(polygon, centerLine, beforePoint, afterPoint) {
|
|
1477
|
+
if (simplifyStraightRuns(centerLine).length > 2) return [...polygon];
|
|
1478
|
+
let trimmed = [...polygon];
|
|
1479
|
+
const first = centerLine[0];
|
|
1480
|
+
const last = centerLine.at(-1);
|
|
1481
|
+
if (beforePoint) {
|
|
1482
|
+
const normal = jointSeamNormal(segmentUnit(beforePoint, first), segmentUnit(first, centerLine[1]));
|
|
1483
|
+
if (normal) trimmed = clipHalfPlane(trimmed, first, [-normal[0], -normal[1]]);
|
|
1484
|
+
}
|
|
1485
|
+
if (afterPoint) {
|
|
1486
|
+
const normal = jointSeamNormal(segmentUnit(centerLine.at(-2), last), segmentUnit(last, afterPoint));
|
|
1487
|
+
if (normal) trimmed = clipHalfPlane(trimmed, last, normal);
|
|
1488
|
+
}
|
|
1489
|
+
return trimmed.length >= 3 ? trimmed : [...polygon];
|
|
1343
1490
|
}
|
|
1344
1491
|
function perpendicularRouteLines(centerLine, leftExtend, rightExtend, margin, lineSpacing) {
|
|
1345
|
-
|
|
1346
|
-
const right = offsetPolyline(centerLine, -rightExtend);
|
|
1347
|
-
if (left.length !== centerLine.length || right.length !== centerLine.length) throw new PlannerError("mappingStrip offset corridor cannot be segmented");
|
|
1492
|
+
centerLine = simplifyStraightRuns(centerLine);
|
|
1348
1493
|
const lines = [];
|
|
1349
1494
|
for (let index = 0; index < centerLine.length - 1; index++) {
|
|
1350
1495
|
const segment = [centerLine[index], centerLine[index + 1]];
|
|
1351
|
-
const cell = [
|
|
1352
|
-
left[index],
|
|
1353
|
-
left[index + 1],
|
|
1354
|
-
right[index + 1],
|
|
1355
|
-
right[index]
|
|
1356
|
-
];
|
|
1357
1496
|
const direction = segmentUnit(segment[0], segment[1]);
|
|
1358
|
-
const projections = cell.map((point) => point[0] * direction[0] + point[1] * direction[1]);
|
|
1359
|
-
const longitudinalSpan = Math.max(...projections) - Math.min(...projections);
|
|
1360
1497
|
const sourceSegmentLength = lineLength(segment);
|
|
1361
1498
|
const previousDirection = index > 0 ? segmentUnit(centerLine[index - 1], segment[0]) : void 0;
|
|
1362
1499
|
const hasSharpStart = previousDirection !== void 0 && Math.abs(cross(previousDirection, direction)) > Math.sin(5 * Math.PI / 180);
|
|
1363
|
-
const
|
|
1364
|
-
const
|
|
1365
|
-
const
|
|
1500
|
+
const before = index > 0 ? centerLine[index - 1] : void 0;
|
|
1501
|
+
const after = centerLine[index + 2];
|
|
1502
|
+
const scanCell = trimJointOverlap(corridorPolygon(segment, leftExtend, rightExtend, before, after), segment, before, after);
|
|
1503
|
+
const scanProjections = scanCell.map((point) => point[0] * direction[0] + point[1] * direction[1]);
|
|
1504
|
+
const scanSpan = Math.max(...scanProjections) - Math.min(...scanProjections);
|
|
1505
|
+
const lineCount = Math.max(1, Math.ceil(sourceSegmentLength / lineSpacing), hasSharpStart ? Math.ceil(scanSpan / lineSpacing) + 1 : Math.round(scanSpan / lineSpacing));
|
|
1506
|
+
if (lines.length + lineCount > MAX_ROUTE_COUNT) throw new PlannerError("mappingStrip route count exceeds limit");
|
|
1507
|
+
const scanSpacing = lineCount > 1 ? Math.min(lineSpacing, Math.max(POINT_EPS_M, scanSpan - POINT_EPS_M) / (lineCount - 1)) : lineSpacing;
|
|
1508
|
+
const phaseOffset = Math.max(0, (scanSpan - (lineCount - 1) * scanSpacing) / 2);
|
|
1509
|
+
const grid = surveyGrid(scanCell, {
|
|
1366
1510
|
directionDeg: (bearingDeg(segment[0], segment[1]) + 90) % 360,
|
|
1367
1511
|
marginM: margin,
|
|
1368
|
-
lineSpacing,
|
|
1512
|
+
lineSpacing: scanSpacing,
|
|
1369
1513
|
phaseOffsetM: phaseOffset
|
|
1370
1514
|
});
|
|
1371
1515
|
const flightDirection = [direction[1], -direction[0]];
|
|
@@ -1378,6 +1522,75 @@ function perpendicularRouteLines(centerLine, leftExtend, rightExtend, margin, li
|
|
|
1378
1522
|
}
|
|
1379
1523
|
return lines;
|
|
1380
1524
|
}
|
|
1525
|
+
function pointInRing(point, polygon) {
|
|
1526
|
+
let inside = false;
|
|
1527
|
+
for (let index = 0; index < polygon.length; index++) {
|
|
1528
|
+
const a = polygon[index];
|
|
1529
|
+
const b = polygon[(index + 1) % polygon.length];
|
|
1530
|
+
const edge = [b[0] - a[0], b[1] - a[1]];
|
|
1531
|
+
const delta = [point[0] - a[0], point[1] - a[1]];
|
|
1532
|
+
const length = Math.hypot(...edge);
|
|
1533
|
+
const projection = edge[0] * delta[0] + edge[1] * delta[1];
|
|
1534
|
+
if (length > 0 && Math.abs(cross(edge, delta)) < length * 1e-7 && projection >= -1e-7 && projection <= length * length + 1e-7) return true;
|
|
1535
|
+
if (a[1] > point[1] !== b[1] > point[1] && point[0] < a[0] + (b[0] - a[0]) * (point[1] - a[1]) / (b[1] - a[1])) inside = !inside;
|
|
1536
|
+
}
|
|
1537
|
+
return inside;
|
|
1538
|
+
}
|
|
1539
|
+
/** Clip an open work polyline to a possibly concave region, retaining each span. */
|
|
1540
|
+
function clipWorkLine(line, polygon) {
|
|
1541
|
+
if (polygonArea(polygon) < 1e-8) return [[...line]];
|
|
1542
|
+
const result = [];
|
|
1543
|
+
let current;
|
|
1544
|
+
for (let index = 1; index < line.length; index++) {
|
|
1545
|
+
const a = line[index - 1];
|
|
1546
|
+
const b = line[index];
|
|
1547
|
+
const direction = [b[0] - a[0], b[1] - a[1]];
|
|
1548
|
+
const length = Math.hypot(...direction);
|
|
1549
|
+
if (length < POINT_EPS_M) continue;
|
|
1550
|
+
const cuts = [0, 1];
|
|
1551
|
+
for (let edgeIndex = 0; edgeIndex < polygon.length; edgeIndex++) {
|
|
1552
|
+
const c = polygon[edgeIndex];
|
|
1553
|
+
const d = polygon[(edgeIndex + 1) % polygon.length];
|
|
1554
|
+
const edge = [d[0] - c[0], d[1] - c[1]];
|
|
1555
|
+
const denominator = cross(direction, edge);
|
|
1556
|
+
if (Math.abs(denominator) < 1e-9) continue;
|
|
1557
|
+
const delta = [c[0] - a[0], c[1] - a[1]];
|
|
1558
|
+
const t = cross(delta, edge) / denominator;
|
|
1559
|
+
const u = cross(delta, direction) / denominator;
|
|
1560
|
+
if (t > 0 && t < 1 && u >= -1e-9 && u <= 1 + 1e-9) cuts.push(t);
|
|
1561
|
+
}
|
|
1562
|
+
cuts.sort((x, y) => x - y);
|
|
1563
|
+
const at = (t) => [a[0] + t * direction[0], a[1] + t * direction[1]];
|
|
1564
|
+
for (let cutIndex = 1; cutIndex < cuts.length; cutIndex++) {
|
|
1565
|
+
const start = cuts[cutIndex - 1];
|
|
1566
|
+
const end = cuts[cutIndex];
|
|
1567
|
+
if ((end - start) * length < POINT_EPS_M) continue;
|
|
1568
|
+
if (!pointInRing(at((start + end) / 2), polygon)) {
|
|
1569
|
+
current = void 0;
|
|
1570
|
+
continue;
|
|
1571
|
+
}
|
|
1572
|
+
const first = at(start);
|
|
1573
|
+
const last = at(end);
|
|
1574
|
+
if (current && Math.hypot(current.at(-1)[0] - first[0], current.at(-1)[1] - first[1]) < POINT_EPS_M) current.push(last);
|
|
1575
|
+
else {
|
|
1576
|
+
current = [first, last];
|
|
1577
|
+
result.push(current);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
return result.map((points) => cleanLine(points));
|
|
1582
|
+
}
|
|
1583
|
+
function extendWorkLine(line, margin) {
|
|
1584
|
+
if (margin === 0) return line;
|
|
1585
|
+
const first = line[0];
|
|
1586
|
+
const last = line.at(-1);
|
|
1587
|
+
const direction = segmentUnit(first, last);
|
|
1588
|
+
return [
|
|
1589
|
+
[first[0] - direction[0] * margin, first[1] - direction[1] * margin],
|
|
1590
|
+
...line.slice(1, -1),
|
|
1591
|
+
[last[0] + direction[0] * margin, last[1] + direction[1] * margin]
|
|
1592
|
+
];
|
|
1593
|
+
}
|
|
1381
1594
|
function polygonArea(points) {
|
|
1382
1595
|
let area2 = 0;
|
|
1383
1596
|
for (let index = 0; index < points.length; index++) {
|
|
@@ -1427,6 +1640,7 @@ function deriveMappingStrip(template, options) {
|
|
|
1427
1640
|
const origin = centroid(triples);
|
|
1428
1641
|
const centerLine = cleanLine(triples.map((point) => lngLatToEnu(point, origin)));
|
|
1429
1642
|
if (centerLine.length < 2) throw new PlannerError("mappingStrip requires two distinct center points");
|
|
1643
|
+
validateCorridorJoins(centerLine, folder.Placemark.leftExtend, folder.Placemark.rightExtend);
|
|
1430
1644
|
const inputs = normalizeInputs(folder, options);
|
|
1431
1645
|
if (!(inputs.crossTrackFootprint > 0 && Number.isFinite(inputs.crossTrackFootprint))) throw new PlannerError("mappingStrip crossTrackFootprint must be finite and greater than zero");
|
|
1432
1646
|
if (!(inputs.photoSpacing > 0 && Number.isFinite(inputs.photoSpacing))) throw new PlannerError("mappingStrip photoSpacing must be finite and greater than zero");
|
|
@@ -1435,20 +1649,40 @@ function deriveMappingStrip(template, options) {
|
|
|
1435
1649
|
...region,
|
|
1436
1650
|
beforePoint: index > 0 ? splitRegions[index - 1].centerLine.at(-2) : void 0,
|
|
1437
1651
|
afterPoint: index + 1 < splitRegions.length ? splitRegions[index + 1].centerLine[1] : void 0
|
|
1438
|
-
})), folder.Placemark.stripMergedRegions).map((region) =>
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1652
|
+
})), folder.Placemark.stripMergedRegions).map((region) => {
|
|
1653
|
+
const polygon = corridorPolygon(region.centerLine, folder.Placemark.leftExtend, folder.Placemark.rightExtend, region.beforePoint, region.afterPoint);
|
|
1654
|
+
return {
|
|
1655
|
+
...region,
|
|
1656
|
+
polygon: trimJointOverlap(polygon, region.centerLine, region.beforePoint, region.afterPoint)
|
|
1657
|
+
};
|
|
1658
|
+
});
|
|
1442
1659
|
const offsets = routeOffsets(folder, inputs);
|
|
1443
1660
|
const executionRegions = options?.regionOrderReversed ? [...localRegions].reverse() : localRegions;
|
|
1444
1661
|
const perpendicular = options?.stripDirection === "perpendicular" && !folder.Placemark.singleLineEnable;
|
|
1445
|
-
const
|
|
1446
|
-
const
|
|
1662
|
+
const regionGeometry = executionRegions.map((region) => {
|
|
1663
|
+
const routeOffsets = [];
|
|
1664
|
+
const rawLines = perpendicular ? perpendicularRouteLines(region.centerLine, folder.Placemark.leftExtend, folder.Placemark.rightExtend, 0, inputs.lineSpacing).flatMap((line) => clipWorkLine(line, region.polygon)).map((line) => extendWorkLine(line, folder.Placemark.margin)) : offsets.flatMap((offset) => {
|
|
1665
|
+
const clipped = clipWorkLine(offsetSide(region.centerLine, offset, region.beforePoint, region.afterPoint), region.polygon);
|
|
1666
|
+
routeOffsets.push(...clipped.map(() => offset));
|
|
1667
|
+
return clipped;
|
|
1668
|
+
});
|
|
1447
1669
|
if (rawLines.length === 0) throw new PlannerError("mappingStrip region has no available flight routes");
|
|
1448
|
-
return
|
|
1670
|
+
return {
|
|
1671
|
+
lines: rawLines,
|
|
1672
|
+
offsets: routeOffsets
|
|
1673
|
+
};
|
|
1674
|
+
});
|
|
1675
|
+
const candidateGroups = regionGeometry.map((geometry, index) => {
|
|
1676
|
+
return serpentineRouteVariants(geometry.lines).map((lines) => localRouteCandidate(lines, executionRegions[index].centerLine, folder.Placemark.elevationOptimizeEnable));
|
|
1449
1677
|
});
|
|
1450
1678
|
const takeOffRefPoint = template.missionConfig.takeOffRefPoint;
|
|
1451
|
-
const
|
|
1679
|
+
const home = takeOffRefPoint ? lngLatToEnu(takeOffRefPoint, origin) : void 0;
|
|
1680
|
+
const timing = {
|
|
1681
|
+
flightSpeed: Number(folder.autoFlightSpeed),
|
|
1682
|
+
transitSpeed: Number(template.missionConfig.globalTransitionalSpeed),
|
|
1683
|
+
finishAction: template.missionConfig.finishAction
|
|
1684
|
+
};
|
|
1685
|
+
const selectedCandidateIndexes = selectStripRouteCandidates(candidateGroups, home, executionRegions.slice(1).map((region) => options?.regionOrderReversed ? region.centerLine.at(-1) : region.centerLine[0]), timing);
|
|
1452
1686
|
const flightRoutes = [];
|
|
1453
1687
|
let previousRegionEnd;
|
|
1454
1688
|
executionRegions.forEach((region, regionIndex) => {
|
|
@@ -1462,7 +1696,7 @@ function deriveMappingStrip(template, options) {
|
|
|
1462
1696
|
const executable = joinSegmentPoints(candidate.segments);
|
|
1463
1697
|
flightRoutes.push({
|
|
1464
1698
|
regionId: region.id,
|
|
1465
|
-
routeOffsets:
|
|
1699
|
+
routeOffsets: regionGeometry[regionIndex].offsets.map(meters),
|
|
1466
1700
|
routeLines: candidate.lines.map((line) => line.map((point) => enuToLngLat(point, origin))),
|
|
1467
1701
|
segments: localSegments.map((segment) => ({
|
|
1468
1702
|
...segment,
|
|
@@ -1474,7 +1708,7 @@ function deriveMappingStrip(template, options) {
|
|
|
1474
1708
|
previousRegionEnd = candidate.end;
|
|
1475
1709
|
});
|
|
1476
1710
|
const surveyAreaLocal = corridorPolygon(centerLine, folder.Placemark.leftExtend, folder.Placemark.rightExtend);
|
|
1477
|
-
const
|
|
1711
|
+
const travel = measureStripMission(candidateGroups.map((group, index) => group[selectedCandidateIndexes[index]]), home, timing);
|
|
1478
1712
|
const estimatedPhotoCount = flightRoutes.reduce((sum, route) => sum + route.segments.filter(({ photoEnabled }) => photoEnabled).reduce((lineSum, segment) => {
|
|
1479
1713
|
let length = 0;
|
|
1480
1714
|
for (let index = 1; index < segment.points.length; index++) length += haversineMeters(segment.points[index - 1], segment.points[index]);
|
|
@@ -1493,10 +1727,10 @@ function deriveMappingStrip(template, options) {
|
|
|
1493
1727
|
polygon: region.polygon.map((point) => enuToLngLat(point, origin))
|
|
1494
1728
|
})),
|
|
1495
1729
|
flightRoutes,
|
|
1496
|
-
totalFlightLength: meters(
|
|
1730
|
+
totalFlightLength: meters(travel.distance),
|
|
1497
1731
|
photoSpacing: meters(inputs.photoSpacing),
|
|
1498
1732
|
estimatedPhotoCount,
|
|
1499
|
-
estimatedDuration:
|
|
1733
|
+
estimatedDuration: travel.duration
|
|
1500
1734
|
};
|
|
1501
1735
|
}
|
|
1502
1736
|
function heightModeToExecuteMode$1(folder) {
|
|
@@ -1510,7 +1744,7 @@ function heightModeToExecuteMode$1(folder) {
|
|
|
1510
1744
|
function turnDamping$1(points, index) {
|
|
1511
1745
|
const previous = haversineMeters(points[index - 1], points[index]);
|
|
1512
1746
|
const next = haversineMeters(points[index], points[index + 1]);
|
|
1513
|
-
return meters(Math.min(TURN_DAMPING_M, previous
|
|
1747
|
+
return meters(Math.min(TURN_DAMPING_M, previous / 3, next / 3));
|
|
1514
1748
|
}
|
|
1515
1749
|
function shootingGroups(folder, startIndex, endIndex, firstGroupId, inputs) {
|
|
1516
1750
|
const lenses = folder.payloadParam?.imageFormat ?? ["visable"];
|
|
@@ -1866,6 +2100,17 @@ function gimbalAction(pitch) {
|
|
|
1866
2100
|
}
|
|
1867
2101
|
};
|
|
1868
2102
|
}
|
|
2103
|
+
function startRecordAction(payloadPositionIndex, actionId) {
|
|
2104
|
+
return {
|
|
2105
|
+
actionId,
|
|
2106
|
+
actionActuatorFunc: "startRecord",
|
|
2107
|
+
actionActuatorFuncParam: {
|
|
2108
|
+
payloadPositionIndex,
|
|
2109
|
+
useGlobalPayloadLensIndex: true,
|
|
2110
|
+
payloadLensIndex: ["visable"]
|
|
2111
|
+
}
|
|
2112
|
+
};
|
|
2113
|
+
}
|
|
1869
2114
|
function pathPoints(template, lineSpacingM, preferredStartPoint) {
|
|
1870
2115
|
const placemark = template.Folder.Placemark;
|
|
1871
2116
|
const ring = parseCoordinatesString(placemark.Polygon.outerBoundaryIs.LinearRing.coordinates);
|
|
@@ -1920,6 +2165,13 @@ function planTargetDetection(template, options) {
|
|
|
1920
2165
|
if (points.length < 2) throw new PlannerError("targetdetection polygon produced no flight path");
|
|
1921
2166
|
const lastIndex = points.length - 1;
|
|
1922
2167
|
const action = gimbalAction(pitch);
|
|
2168
|
+
const payloadPositionIndex = folder.payloadParam?.payloadPositionIndex ?? 0;
|
|
2169
|
+
const routeActions = [action];
|
|
2170
|
+
if (folder.Placemark.recordEnable) routeActions.push(startRecordAction(payloadPositionIndex, routeActions.length));
|
|
2171
|
+
if (folder.Placemark.action) routeActions.push({
|
|
2172
|
+
...folder.Placemark.action,
|
|
2173
|
+
actionId: routeActions.length
|
|
2174
|
+
});
|
|
1923
2175
|
const placemarks = points.map((point, index) => ({
|
|
1924
2176
|
Point: { coordinates: point },
|
|
1925
2177
|
index,
|
|
@@ -1955,7 +2207,7 @@ function planTargetDetection(template, options) {
|
|
|
1955
2207
|
actionGroupEndIndex: lastIndex,
|
|
1956
2208
|
actionGroupMode: "sequence",
|
|
1957
2209
|
actionTrigger: { actionTriggerType: "betweenAdjacentPointsIncludeFirstPoint" },
|
|
1958
|
-
action:
|
|
2210
|
+
action: routeActions
|
|
1959
2211
|
}] } : {}
|
|
1960
2212
|
}));
|
|
1961
2213
|
return {
|
|
@@ -1965,7 +2217,7 @@ function planTargetDetection(template, options) {
|
|
|
1965
2217
|
waylineId: 0,
|
|
1966
2218
|
executeHeightMode: executeHeightMode(folder),
|
|
1967
2219
|
autoFlightSpeed: folder.autoFlightSpeed,
|
|
1968
|
-
startActionGroup: { action: [gimbalAction(pitch)] },
|
|
2220
|
+
startActionGroup: { action: folder.Placemark.recordEnable ? [gimbalAction(pitch), startRecordAction(payloadPositionIndex, 1)] : [gimbalAction(pitch)] },
|
|
1969
2221
|
Placemark: placemarks
|
|
1970
2222
|
}]
|
|
1971
2223
|
};
|
|
@@ -2051,9 +2303,7 @@ function validateTemplate(template) {
|
|
|
2051
2303
|
case "mappingStrip":
|
|
2052
2304
|
issues.push(...validateMappingStripFolder(template, path));
|
|
2053
2305
|
break;
|
|
2054
|
-
case "targetdetection":
|
|
2055
|
-
issues.push(...validateTargetDetectionFolder(template, path));
|
|
2056
|
-
break;
|
|
2306
|
+
case "targetdetection": issues.push(...validateTargetDetectionFolder(template, path));
|
|
2057
2307
|
}
|
|
2058
2308
|
return issues;
|
|
2059
2309
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lumikmz/kmz",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
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",
|
|
@@ -15,8 +15,7 @@
|
|
|
15
15
|
"./package.json": "./package.json"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
|
-
"vite-plus": "
|
|
19
|
-
"vitest": "npm:@voidzero-dev/vite-plus-test@^0.1.20"
|
|
18
|
+
"vite-plus": "1.3.0"
|
|
20
19
|
},
|
|
21
20
|
"scripts": {
|
|
22
21
|
"build": "vp pack"
|