@kimap/indoor-positioning-sdk-vue2 4.2.4 → 4.2.6
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/package.json +1 -1
- package/src/KMUtil.js +380 -113
- package/src/KimapCore.browser.js +509 -25
- package/src/core/KimapSDK.js +17 -8
- package/src/core/SceneCore.js +5 -7
- package/QUICK_REFERENCE.md +0 -351
package/package.json
CHANGED
package/src/KMUtil.js
CHANGED
|
@@ -39,10 +39,12 @@
|
|
|
39
39
|
|
|
40
40
|
// 遍历所有图层
|
|
41
41
|
floorData.layers.forEach(function (layer) {
|
|
42
|
-
|
|
42
|
+
// 只跳过明确设置为不可见的图层(visible === false),undefined视为可见
|
|
43
|
+
if (layer.visible === false || !layer.elements) return;
|
|
43
44
|
|
|
44
45
|
layer.elements.forEach(function (element) {
|
|
45
|
-
|
|
46
|
+
// 只跳过明确设置为不可见的元素(visible === false),undefined视为可见
|
|
47
|
+
if (element.visible === false) return;
|
|
46
48
|
|
|
47
49
|
// 检查是否可穿行
|
|
48
50
|
var passable = element.passable === true;
|
|
@@ -50,19 +52,32 @@
|
|
|
50
52
|
// 处理墙体(始终作为障碍物)
|
|
51
53
|
if (element.type === 'wall') {
|
|
52
54
|
if (element.points && element.points.length >= 2) {
|
|
53
|
-
// thickness
|
|
54
|
-
var thickness =
|
|
55
|
-
|
|
55
|
+
// thickness: 如果是米单位(< 1),直接使用;如果是毫米(>= 1),转换为米
|
|
56
|
+
var thickness = element.thickness;
|
|
57
|
+
if (thickness >= 1) {
|
|
58
|
+
thickness = thickness / 1000; // 毫米 → 米
|
|
59
|
+
}
|
|
60
|
+
var halfThickness = (thickness || 0.1) / 2;
|
|
56
61
|
|
|
57
62
|
for (var i = 0; i < element.points.length - 1; i++) {
|
|
58
63
|
var p1 = element.points[i];
|
|
59
64
|
var p2 = element.points[i + 1];
|
|
60
65
|
|
|
61
|
-
//
|
|
62
|
-
var x1
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
// 判断坐标格式:如果有z属性,说明已经是米坐标;如果有y属性,说明是像素坐标
|
|
67
|
+
var x1, z1, x2, z2;
|
|
68
|
+
if (p1.z !== undefined) {
|
|
69
|
+
// 新格式:已经是米坐标
|
|
70
|
+
x1 = p1.x;
|
|
71
|
+
z1 = p1.z;
|
|
72
|
+
x2 = p2.x;
|
|
73
|
+
z2 = p2.z;
|
|
74
|
+
} else {
|
|
75
|
+
// 旧格式:像素坐标,需要转换
|
|
76
|
+
x1 = p1.x / 100;
|
|
77
|
+
z1 = p1.y / 100;
|
|
78
|
+
x2 = p2.x / 100;
|
|
79
|
+
z2 = p2.y / 100;
|
|
80
|
+
}
|
|
66
81
|
|
|
67
82
|
// 判断墙的方向
|
|
68
83
|
var dx = Math.abs(x2 - x1);
|
|
@@ -103,11 +118,21 @@
|
|
|
103
118
|
var p1 = element.points[0];
|
|
104
119
|
var p2 = element.points[1];
|
|
105
120
|
|
|
106
|
-
//
|
|
107
|
-
var x1
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
121
|
+
// 判断坐标格式
|
|
122
|
+
var x1, z1, x2, z2;
|
|
123
|
+
if (p1.z !== undefined) {
|
|
124
|
+
// 新格式:已经是米坐标
|
|
125
|
+
x1 = p1.x;
|
|
126
|
+
z1 = p1.z;
|
|
127
|
+
x2 = p2.x;
|
|
128
|
+
z2 = p2.z;
|
|
129
|
+
} else {
|
|
130
|
+
// 旧格式:像素坐标,需要转换
|
|
131
|
+
x1 = p1.x / 100;
|
|
132
|
+
z1 = p1.y / 100;
|
|
133
|
+
x2 = p2.x / 100;
|
|
134
|
+
z2 = p2.y / 100;
|
|
135
|
+
}
|
|
111
136
|
|
|
112
137
|
obstacles.push({
|
|
113
138
|
type: 'rectangle',
|
|
@@ -124,11 +149,24 @@
|
|
|
124
149
|
else if (element.type === 'circle' && !passable) {
|
|
125
150
|
if (element.points && element.points.length >= 1) {
|
|
126
151
|
var center = element.points[0];
|
|
127
|
-
var centerX = center.x / 100;
|
|
128
|
-
var centerZ = center.y / 100;
|
|
129
152
|
|
|
130
|
-
//
|
|
131
|
-
var
|
|
153
|
+
// 判断坐标格式
|
|
154
|
+
var centerX, centerZ;
|
|
155
|
+
if (center.z !== undefined) {
|
|
156
|
+
// 新格式:已经是米坐标
|
|
157
|
+
centerX = center.x;
|
|
158
|
+
centerZ = center.z;
|
|
159
|
+
} else {
|
|
160
|
+
// 旧格式:像素坐标,需要转换
|
|
161
|
+
centerX = center.x / 100;
|
|
162
|
+
centerZ = center.y / 100;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// radius: 如果是米单位(< 1),直接使用;如果是毫米(>= 1),转换为米
|
|
166
|
+
var radius = element.radius || 0.5;
|
|
167
|
+
if (radius >= 1) {
|
|
168
|
+
radius = radius / 1000; // 毫米 → 米
|
|
169
|
+
}
|
|
132
170
|
|
|
133
171
|
obstacles.push({
|
|
134
172
|
type: 'circle',
|
|
@@ -151,8 +189,17 @@
|
|
|
151
189
|
var points3D = [];
|
|
152
190
|
|
|
153
191
|
element.points.forEach(function (p) {
|
|
154
|
-
|
|
155
|
-
var z
|
|
192
|
+
// 判断坐标格式
|
|
193
|
+
var x, z;
|
|
194
|
+
if (p.z !== undefined) {
|
|
195
|
+
// 新格式:已经是米坐标
|
|
196
|
+
x = p.x;
|
|
197
|
+
z = p.z;
|
|
198
|
+
} else {
|
|
199
|
+
// 旧格式:像素坐标,需要转换
|
|
200
|
+
x = p.x / 100;
|
|
201
|
+
z = p.y / 100;
|
|
202
|
+
}
|
|
156
203
|
points3D.push({ x: x, z: z });
|
|
157
204
|
minX = Math.min(minX, x);
|
|
158
205
|
minZ = Math.min(minZ, z);
|
|
@@ -190,6 +237,39 @@
|
|
|
190
237
|
});
|
|
191
238
|
}
|
|
192
239
|
}
|
|
240
|
+
// 处理家具(furniture)
|
|
241
|
+
else if (element.type === 'furniture' && !passable) {
|
|
242
|
+
// 家具可能使用position或points来定义位置
|
|
243
|
+
var centerX, centerZ, width, depth;
|
|
244
|
+
|
|
245
|
+
if (element.position) {
|
|
246
|
+
// 使用position定义(米坐标)
|
|
247
|
+
centerX = element.position.x;
|
|
248
|
+
centerZ = element.position.z;
|
|
249
|
+
width = element.targetSize ? element.targetSize.width : 0.5;
|
|
250
|
+
depth = element.targetSize ? element.targetSize.depth : 0.5;
|
|
251
|
+
} else if (element.points && element.points.length >= 1) {
|
|
252
|
+
// 使用points定义(像素坐标)
|
|
253
|
+
var center = element.points[0];
|
|
254
|
+
centerX = center.x / 100;
|
|
255
|
+
centerZ = center.y / 100;
|
|
256
|
+
width = (element.width || 50) / 100;
|
|
257
|
+
depth = (element.depth || 50) / 100;
|
|
258
|
+
} else {
|
|
259
|
+
return; // 没有位置信息,跳过
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
var halfDiag = Math.sqrt(width * width + depth * depth) / 2;
|
|
263
|
+
obstacles.push({
|
|
264
|
+
type: 'furniture',
|
|
265
|
+
bounds: {
|
|
266
|
+
minX: centerX - halfDiag,
|
|
267
|
+
minZ: centerZ - halfDiag,
|
|
268
|
+
maxX: centerX + halfDiag,
|
|
269
|
+
maxZ: centerZ + halfDiag
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
}
|
|
193
273
|
});
|
|
194
274
|
});
|
|
195
275
|
|
|
@@ -228,15 +308,28 @@
|
|
|
228
308
|
|
|
229
309
|
var obstacles = [];
|
|
230
310
|
|
|
231
|
-
// 1.
|
|
232
|
-
if (kidata.
|
|
311
|
+
// 1. 优先从kidata的floors中提取障碍物
|
|
312
|
+
if (kidata.floors && Array.isArray(kidata.floors)) {
|
|
313
|
+
console.log('KMUtil: 从kidata.floors提取障碍物,楼层数量:', kidata.floors.length);
|
|
314
|
+
// 使用第一个楼层的数据
|
|
315
|
+
if (kidata.floors.length > 0) {
|
|
316
|
+
var floorData = { layers: kidata.floors[0].layers };
|
|
317
|
+
obstacles = obstacles.concat(
|
|
318
|
+
KMUtil.extractObstaclesFromFloor(floorData, true)
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// 2. 如果没有floors数据,回退到layers(兼容旧版本)
|
|
323
|
+
else if (kidata.layers && Array.isArray(kidata.layers)) {
|
|
324
|
+
console.log('KMUtil: 从kidata.layers提取障碍物(兼容模式)');
|
|
233
325
|
obstacles = obstacles.concat(
|
|
234
326
|
KMUtil.extractObstaclesFromFloor(kidata, true)
|
|
235
327
|
);
|
|
236
328
|
}
|
|
237
329
|
|
|
238
|
-
//
|
|
330
|
+
// 3. 处理furnitures(3D模型)
|
|
239
331
|
if (kidata.furnitures && Array.isArray(kidata.furnitures)) {
|
|
332
|
+
console.log('KMUtil: 从kidata.furnitures提取家具障碍物,家具数量:', kidata.furnitures.length);
|
|
240
333
|
kidata.furnitures.forEach(function (furniture) {
|
|
241
334
|
var passable = furniture.passable === true;
|
|
242
335
|
if (passable) return;
|
|
@@ -546,38 +639,26 @@
|
|
|
546
639
|
}
|
|
547
640
|
|
|
548
641
|
/**
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
* @param {Object} end - 终点 {x, z}(米)
|
|
552
|
-
* @param {Array} obstacles - 障碍物列表
|
|
553
|
-
* @param {number} gridSize - 网格大小(米),默认0.5米
|
|
554
|
-
*/
|
|
555
|
-
/**
|
|
556
|
-
* A* 路径规划(已修复水平镜像 / 三维坐标系映射)
|
|
557
|
-
* start/end: { x, z } 世界坐标(米)
|
|
642
|
+
* A* 路径规划(适配你的坐标系:X 右、Z 下、单位米)
|
|
643
|
+
* start/end: { x, z }
|
|
558
644
|
* obstacles: [{ type, bounds:{minX,minZ,maxX,maxZ} }, ...]
|
|
559
|
-
*
|
|
560
|
-
* options: { inflationCells } 障碍膨胀(格数),默认 1
|
|
645
|
+
* gridSize: 每步移动距离(米),推荐 0.2〜0.5
|
|
561
646
|
*/
|
|
562
|
-
function aStarPathfinding(start, end, obstacles, gridSize) {
|
|
563
|
-
|
|
647
|
+
function aStarPathfinding(start, end, obstacles, gridSize = 0.2) {
|
|
648
|
+
const openList = [];
|
|
649
|
+
const closed = new Set();
|
|
564
650
|
|
|
565
|
-
|
|
566
|
-
var closedSet = {};
|
|
567
|
-
|
|
568
|
-
var startNode = {
|
|
651
|
+
const startNode = {
|
|
569
652
|
x: start.x,
|
|
570
653
|
z: start.z,
|
|
571
654
|
g: 0,
|
|
572
655
|
h: heuristic(start, end),
|
|
573
|
-
|
|
574
|
-
parent: null
|
|
656
|
+
parent: null,
|
|
575
657
|
};
|
|
576
658
|
startNode.f = startNode.g + startNode.h;
|
|
577
|
-
|
|
578
659
|
openList.push(startNode);
|
|
579
660
|
|
|
580
|
-
|
|
661
|
+
const dirs = [
|
|
581
662
|
{ dx: gridSize, dz: 0 },
|
|
582
663
|
{ dx: -gridSize, dz: 0 },
|
|
583
664
|
{ dx: 0, dz: gridSize },
|
|
@@ -585,97 +666,267 @@
|
|
|
585
666
|
{ dx: gridSize, dz: gridSize },
|
|
586
667
|
{ dx: gridSize, dz: -gridSize },
|
|
587
668
|
{ dx: -gridSize, dz: gridSize },
|
|
588
|
-
{ dx: -gridSize, dz: -gridSize }
|
|
669
|
+
{ dx: -gridSize, dz: -gridSize },
|
|
589
670
|
];
|
|
590
671
|
|
|
591
|
-
|
|
592
|
-
|
|
672
|
+
let iter = 0;
|
|
673
|
+
const maxIter = 20000;
|
|
593
674
|
|
|
594
|
-
while (openList.length > 0 &&
|
|
595
|
-
|
|
675
|
+
while (openList.length > 0 && iter < maxIter) {
|
|
676
|
+
iter++;
|
|
596
677
|
|
|
597
|
-
//
|
|
598
|
-
openList.sort(
|
|
599
|
-
|
|
678
|
+
// 找 f 最小的节点
|
|
679
|
+
openList.sort((a, b) => a.f - b.f);
|
|
680
|
+
const current = openList.shift();
|
|
600
681
|
|
|
601
|
-
|
|
602
|
-
if (
|
|
603
|
-
|
|
604
|
-
var node = current;
|
|
682
|
+
const key = toKey(current, gridSize);
|
|
683
|
+
if (closed.has(key)) continue;
|
|
684
|
+
closed.add(key);
|
|
605
685
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
path.push(end);
|
|
612
|
-
return simplifyPath(path, obstacles);
|
|
686
|
+
// 是否到终点(误差小于 gridSize)
|
|
687
|
+
if (distance(current, end) <= gridSize * 1.1) {
|
|
688
|
+
return rebuildPath(current, end);
|
|
613
689
|
}
|
|
614
690
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
691
|
+
// 扩展邻居
|
|
692
|
+
for (const d of dirs) {
|
|
693
|
+
const nx = current.x + d.dx;
|
|
694
|
+
const nz = current.z + d.dz;
|
|
618
695
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
var dir = directions[d];
|
|
622
|
-
var newX = current.x + dir.dx;
|
|
623
|
-
var newZ = current.z + dir.dz;
|
|
696
|
+
const nk = `${Math.round(nx / gridSize)},${Math.round(nz / gridSize)}`;
|
|
697
|
+
if (closed.has(nk)) continue;
|
|
624
698
|
|
|
625
|
-
|
|
626
|
-
if (
|
|
699
|
+
// 点是否落在障碍物区域
|
|
700
|
+
if (isPointInObstacles(nx, nz, obstacles)) continue;
|
|
627
701
|
|
|
628
|
-
//
|
|
629
|
-
|
|
630
|
-
for (var o = 0; o < obstacles.length; o++) {
|
|
631
|
-
if (isPointInObstacle(newX, newZ, obstacles[o], gridSize / 2)) {
|
|
632
|
-
blocked = true;
|
|
633
|
-
break;
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
if (blocked) continue;
|
|
637
|
-
|
|
638
|
-
// 检查路径是否穿过障碍物
|
|
639
|
-
if (pathIntersectsObstacles(current.x, current.z, newX, newZ, obstacles, gridSize / 4)) {
|
|
702
|
+
// 本段路径是否穿越障碍物(含斜线)
|
|
703
|
+
if (segmentIntersectsObstacles(current.x, current.z, nx, nz, obstacles)) {
|
|
640
704
|
continue;
|
|
641
705
|
}
|
|
642
706
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
var f = g + h;
|
|
707
|
+
const g = current.g + Math.sqrt(d.dx * d.dx + d.dz * d.dz);
|
|
708
|
+
const h = heuristic({ x: nx, z: nz }, end);
|
|
646
709
|
|
|
647
|
-
|
|
648
|
-
var existingIndex = -1;
|
|
649
|
-
for (var e = 0; e < openList.length; e++) {
|
|
650
|
-
if (Math.abs(openList[e].x - newX) < 0.01 && Math.abs(openList[e].z - newZ) < 0.01) {
|
|
651
|
-
existingIndex = e;
|
|
652
|
-
break;
|
|
653
|
-
}
|
|
654
|
-
}
|
|
710
|
+
const exist = openList.find((n) => Math.abs(n.x - nx) < 1e-3 && Math.abs(n.z - nz) < 1e-3);
|
|
655
711
|
|
|
656
|
-
if (
|
|
657
|
-
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
if (existingIndex >= 0) {
|
|
661
|
-
openList.splice(existingIndex, 1);
|
|
662
|
-
}
|
|
712
|
+
if (exist && exist.g <= g) continue;
|
|
713
|
+
if (exist) openList.splice(openList.indexOf(exist), 1);
|
|
663
714
|
|
|
664
715
|
openList.push({
|
|
665
|
-
x:
|
|
666
|
-
z:
|
|
667
|
-
g
|
|
668
|
-
h
|
|
669
|
-
f:
|
|
670
|
-
parent: current
|
|
716
|
+
x: nx,
|
|
717
|
+
z: nz,
|
|
718
|
+
g,
|
|
719
|
+
h,
|
|
720
|
+
f: g + h,
|
|
721
|
+
parent: current,
|
|
671
722
|
});
|
|
672
723
|
}
|
|
673
724
|
}
|
|
674
725
|
|
|
675
|
-
console.warn(
|
|
726
|
+
console.warn("A* 未找到路径,返回直线。");
|
|
676
727
|
return [start, end];
|
|
677
728
|
}
|
|
678
729
|
|
|
730
|
+
/* ======== 工具函数 ========= */
|
|
731
|
+
|
|
732
|
+
// 曼哈顿 + 欧式启发
|
|
733
|
+
function heuristic(a, b) {
|
|
734
|
+
return Math.sqrt((a.x - b.x) ** 2 + (a.z - b.z) ** 2);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// 键
|
|
738
|
+
function toKey(n, grid) {
|
|
739
|
+
return `${Math.round(n.x / grid)},${Math.round(n.z / grid)}`;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// 回溯路径
|
|
743
|
+
function rebuildPath(node, end) {
|
|
744
|
+
const path = [];
|
|
745
|
+
while (node) {
|
|
746
|
+
path.unshift({ x: node.x, z: node.z });
|
|
747
|
+
node = node.parent;
|
|
748
|
+
}
|
|
749
|
+
path.push(end);
|
|
750
|
+
return path;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// 点是否在任意障碍物中
|
|
754
|
+
function isPointInObstacles(x, z, obstacles) {
|
|
755
|
+
return obstacles.some((o) => {
|
|
756
|
+
const b = o.bounds;
|
|
757
|
+
return x >= b.minX && x <= b.maxX && z >= b.minZ && z <= b.maxZ;
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// 判断线段是否穿过障碍(矩形)
|
|
762
|
+
function segmentIntersectsObstacles(x1, z1, x2, z2, obstacles) {
|
|
763
|
+
return obstacles.some((o) => {
|
|
764
|
+
const b = o.bounds;
|
|
765
|
+
return segmentIntersectsRect(x1, z1, x2, z2, b);
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// 线段 vs 轴对齐矩形
|
|
770
|
+
function segmentIntersectsRect(x1, z1, x2, z2, b) {
|
|
771
|
+
// 若两端点都在外侧,但线段穿过 bbox
|
|
772
|
+
if (lineIntersectsAABB(x1, z1, x2, z2, b)) return true;
|
|
773
|
+
|
|
774
|
+
// 若任意端点在矩形内,也算穿越
|
|
775
|
+
if (x1 >= b.minX && x1 <= b.maxX && z1 >= b.minZ && z1 <= b.maxZ) return true;
|
|
776
|
+
if (x2 >= b.minX && x2 <= b.maxX && z2 >= b.maxZ && z2 <= b.maxZ) return true;
|
|
777
|
+
|
|
778
|
+
return false;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// 利用 Liang–Barsky 算法判断线段与 AABB 是否相交
|
|
782
|
+
function lineIntersectsAABB(x1, z1, x2, z2, b) {
|
|
783
|
+
let t0 = 0, t1 = 1;
|
|
784
|
+
const dx = x2 - x1, dz = z2 - z1;
|
|
785
|
+
|
|
786
|
+
const checks = [
|
|
787
|
+
[-dx, x1 - b.minX],
|
|
788
|
+
[dx, b.maxX - x1],
|
|
789
|
+
[-dz, z1 - b.minZ],
|
|
790
|
+
[dz, b.maxZ - z1],
|
|
791
|
+
];
|
|
792
|
+
|
|
793
|
+
for (const [p, q] of checks) {
|
|
794
|
+
if (p === 0 && q < 0) return false;
|
|
795
|
+
const r = q / p;
|
|
796
|
+
if (p < 0) { if (r > t1) return false; else if (r > t0) t0 = r; }
|
|
797
|
+
else if (p > 0) { if (r < t0) return false; else if (r < t1) t1 = r; }
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
return true;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// 距离
|
|
804
|
+
function distance(a, b) {
|
|
805
|
+
return Math.sqrt((a.x - b.x) ** 2 + (a.z - b.z) ** 2);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* 简化路径 - 移除不必要的中间点
|
|
810
|
+
* 使用Douglas-Peucker算法的简化版本
|
|
811
|
+
* @param {Array} path - 原始路径点 [{x, z}, ...]
|
|
812
|
+
* @param {Array} obstacles - 障碍物列表
|
|
813
|
+
* @returns {Array} 简化后的路径点
|
|
814
|
+
*/
|
|
815
|
+
function simplifyPath(path, obstacles) {
|
|
816
|
+
if (path.length <= 2) return path;
|
|
817
|
+
|
|
818
|
+
const simplified = [path[0]];
|
|
819
|
+
let current = 0;
|
|
820
|
+
|
|
821
|
+
// 贪心算法:尽可能跳过中间点
|
|
822
|
+
while (current < path.length - 1) {
|
|
823
|
+
let farthest = current + 1;
|
|
824
|
+
|
|
825
|
+
// 从最远的点开始尝试,看能否直接连接
|
|
826
|
+
for (let i = path.length - 1; i > current + 1; i--) {
|
|
827
|
+
const canConnect = !segmentIntersectsObstacles(
|
|
828
|
+
path[current].x,
|
|
829
|
+
path[current].z,
|
|
830
|
+
path[i].x,
|
|
831
|
+
path[i].z,
|
|
832
|
+
obstacles
|
|
833
|
+
);
|
|
834
|
+
|
|
835
|
+
if (canConnect) {
|
|
836
|
+
farthest = i;
|
|
837
|
+
break;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
simplified.push(path[farthest]);
|
|
842
|
+
current = farthest;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
return simplified;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* 使用Catmull-Rom样条曲线平滑路径
|
|
850
|
+
* @param {Array} path - 原始路径点 [{x, z}, ...]
|
|
851
|
+
* @param {Object} obstacles - 障碍物列表
|
|
852
|
+
* @param {number} tension - 张力参数(0-1),越小越平滑,默认0.5
|
|
853
|
+
* @param {number} segments - 每段曲线的分段数,默认10
|
|
854
|
+
* @returns {Array} 平滑后的路径点
|
|
855
|
+
*/
|
|
856
|
+
function smoothPath(path, obstacles, tension = 0.5, segments = 10) {
|
|
857
|
+
if (path.length <= 2) return path;
|
|
858
|
+
|
|
859
|
+
const smoothed = [];
|
|
860
|
+
|
|
861
|
+
// 添加起点
|
|
862
|
+
smoothed.push(path[0]);
|
|
863
|
+
|
|
864
|
+
// 对每个路径段进行平滑
|
|
865
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
866
|
+
const p0 = path[Math.max(0, i - 1)];
|
|
867
|
+
const p1 = path[i];
|
|
868
|
+
const p2 = path[i + 1];
|
|
869
|
+
const p3 = path[Math.min(path.length - 1, i + 2)];
|
|
870
|
+
|
|
871
|
+
// 生成Catmull-Rom曲线点
|
|
872
|
+
for (let t = 0; t < segments; t++) {
|
|
873
|
+
const u = t / segments;
|
|
874
|
+
const point = catmullRom(p0, p1, p2, p3, u, tension);
|
|
875
|
+
|
|
876
|
+
// 检查平滑后的点是否穿过障碍物
|
|
877
|
+
if (smoothed.length > 0) {
|
|
878
|
+
const lastPoint = smoothed[smoothed.length - 1];
|
|
879
|
+
if (!segmentIntersectsObstacles(lastPoint.x, lastPoint.z, point.x, point.z, obstacles)) {
|
|
880
|
+
smoothed.push(point);
|
|
881
|
+
} else {
|
|
882
|
+
// 如果穿过障碍物,使用原始路径点
|
|
883
|
+
if (smoothed[smoothed.length - 1] !== p1) {
|
|
884
|
+
smoothed.push(p1);
|
|
885
|
+
}
|
|
886
|
+
break;
|
|
887
|
+
}
|
|
888
|
+
} else {
|
|
889
|
+
smoothed.push(point);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// 添加终点
|
|
895
|
+
smoothed.push(path[path.length - 1]);
|
|
896
|
+
|
|
897
|
+
return smoothed;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* Catmull-Rom样条曲线插值
|
|
902
|
+
* @param {Object} p0 - 控制点0
|
|
903
|
+
* @param {Object} p1 - 起点
|
|
904
|
+
* @param {Object} p2 - 终点
|
|
905
|
+
* @param {Object} p3 - 控制点3
|
|
906
|
+
* @param {number} t - 插值参数(0-1)
|
|
907
|
+
* @param {number} tension - 张力参数
|
|
908
|
+
* @returns {Object} 插值点 {x, z}
|
|
909
|
+
*/
|
|
910
|
+
function catmullRom(p0, p1, p2, p3, t, tension) {
|
|
911
|
+
const t2 = t * t;
|
|
912
|
+
const t3 = t2 * t;
|
|
913
|
+
|
|
914
|
+
const v0 = (p2.x - p0.x) * tension;
|
|
915
|
+
const v1 = (p3.x - p1.x) * tension;
|
|
916
|
+
const w0 = (p2.z - p0.z) * tension;
|
|
917
|
+
const w1 = (p3.z - p1.z) * tension;
|
|
918
|
+
|
|
919
|
+
const x = (2 * p1.x - 2 * p2.x + v0 + v1) * t3 +
|
|
920
|
+
(-3 * p1.x + 3 * p2.x - 2 * v0 - v1) * t2 +
|
|
921
|
+
v0 * t + p1.x;
|
|
922
|
+
|
|
923
|
+
const z = (2 * p1.z - 2 * p2.z + w0 + w1) * t3 +
|
|
924
|
+
(-3 * p1.z + 3 * p2.z - 2 * w0 - w1) * t2 +
|
|
925
|
+
w0 * t + p1.z;
|
|
926
|
+
|
|
927
|
+
return { x, z };
|
|
928
|
+
}
|
|
929
|
+
|
|
679
930
|
|
|
680
931
|
|
|
681
932
|
/**
|
|
@@ -723,8 +974,24 @@
|
|
|
723
974
|
return [start, end];
|
|
724
975
|
}
|
|
725
976
|
|
|
726
|
-
// 使用A*算法规划路径(gridSize: 0.
|
|
727
|
-
|
|
977
|
+
// 使用A*算法规划路径(gridSize: 0.2米 = 20cm)
|
|
978
|
+
const rawPath = aStarPathfinding(start, end, obstacles, 0.2);
|
|
979
|
+
|
|
980
|
+
// 如果路径只有2个点(直线),直接返回
|
|
981
|
+
if (rawPath.length <= 2) {
|
|
982
|
+
return rawPath;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// 第一步:简化路径,移除不必要的中间点
|
|
986
|
+
const simplifiedPath = simplifyPath(rawPath, obstacles);
|
|
987
|
+
|
|
988
|
+
// 第二步:平滑路径(可通过options.smoothPath控制是否启用,默认启用)
|
|
989
|
+
const enableSmoothing = options.smoothPath !== false;
|
|
990
|
+
if (enableSmoothing && simplifiedPath.length > 2) {
|
|
991
|
+
return smoothPath(simplifiedPath, obstacles, 0.5, 8);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
return simplifiedPath;
|
|
728
995
|
};
|
|
729
996
|
|
|
730
997
|
/**
|