@esengine/pathfinding 13.2.0 → 13.3.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/{KDTree-2rs2EXvm.d.ts → CollisionResolver-CSgWsegP.d.ts} +122 -86
- package/dist/FlowController-BztOzQsW.d.ts +2781 -0
- package/dist/KDTree-BRpn7O8K.d.ts +216 -0
- package/dist/avoidance.d.ts +26 -4
- package/dist/avoidance.js +10 -2
- package/dist/{chunk-JTZP55BJ.js → chunk-3VEX32JO.js} +385 -9
- package/dist/chunk-3VEX32JO.js.map +1 -0
- package/dist/chunk-H5EFZBBT.js +1 -0
- package/dist/chunk-ZYGBA7VK.js +3831 -0
- package/dist/chunk-ZYGBA7VK.js.map +1 -0
- package/dist/ecs.d.ts +440 -647
- package/dist/ecs.js +1020 -1399
- package/dist/ecs.js.map +1 -1
- package/dist/index.d.ts +158 -711
- package/dist/index.js +1353 -1739
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/dist/IIncrementalPathfinding-3qs7e_pO.d.ts +0 -450
- package/dist/LinearProgram-DyD3pI6v.d.ts +0 -56
- package/dist/chunk-JTZP55BJ.js.map +0 -1
- package/dist/chunk-KEYTX37K.js +0 -1
- package/dist/chunk-VNC2YAAL.js +0 -1650
- package/dist/chunk-VNC2YAAL.js.map +0 -1
- /package/dist/{chunk-KEYTX37K.js.map → chunk-H5EFZBBT.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,326 +1,71 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
AStarPathfinder,
|
|
3
|
+
BinaryHeap,
|
|
4
|
+
CollisionResolverAdapter,
|
|
5
|
+
DEFAULT_FLOW_CONTROLLER_CONFIG,
|
|
6
|
+
DEFAULT_HPA_CONFIG,
|
|
7
|
+
DEFAULT_ORCA_PARAMS,
|
|
5
8
|
DEFAULT_PATHFINDING_OPTIONS,
|
|
6
9
|
DEFAULT_PATH_CACHE_CONFIG,
|
|
7
|
-
|
|
8
|
-
DIRECTIONS_8,
|
|
10
|
+
EMPTY_COLLISION_RESULT,
|
|
9
11
|
EMPTY_PATH_RESULT,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
EMPTY_PLAN_RESULT,
|
|
13
|
+
FlowController,
|
|
14
|
+
GridPathfinderAdapter,
|
|
15
|
+
HPAPathfinder,
|
|
12
16
|
IncrementalAStarPathfinder,
|
|
17
|
+
IncrementalGridPathPlannerAdapter,
|
|
13
18
|
IndexedBinaryHeap,
|
|
14
|
-
|
|
15
|
-
|
|
19
|
+
JPSPathfinder,
|
|
20
|
+
NavMeshPathPlannerAdapter,
|
|
21
|
+
ORCALocalAvoidanceAdapter,
|
|
22
|
+
PassPermission,
|
|
16
23
|
PathCache,
|
|
17
|
-
|
|
18
|
-
bresenhamLineOfSight,
|
|
24
|
+
PathPlanState,
|
|
19
25
|
chebyshevDistance,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
createAStarPathfinder,
|
|
27
|
+
createAStarPlanner,
|
|
28
|
+
createDefaultCollisionResolver,
|
|
29
|
+
createFlowController,
|
|
30
|
+
createHPAPathfinder,
|
|
31
|
+
createHPAPlanner,
|
|
23
32
|
createIncrementalAStarPathfinder,
|
|
24
|
-
|
|
25
|
-
|
|
33
|
+
createIncrementalAStarPlanner,
|
|
34
|
+
createJPSPathfinder,
|
|
35
|
+
createJPSPlanner,
|
|
36
|
+
createNavMeshPathPlanner,
|
|
37
|
+
createORCAAvoidance,
|
|
26
38
|
createPathCache,
|
|
27
|
-
createPathValidator,
|
|
28
39
|
createPoint,
|
|
29
40
|
euclideanDistance,
|
|
41
|
+
isIncrementalPlanner,
|
|
30
42
|
manhattanDistance,
|
|
31
|
-
octileDistance
|
|
32
|
-
|
|
33
|
-
} from "./chunk-VNC2YAAL.js";
|
|
43
|
+
octileDistance
|
|
44
|
+
} from "./chunk-ZYGBA7VK.js";
|
|
34
45
|
import {
|
|
35
46
|
DEFAULT_REPLANNING_CONFIG,
|
|
36
47
|
EMPTY_PROGRESS,
|
|
37
48
|
PathfindingState
|
|
38
49
|
} from "./chunk-YKA3PWU3.js";
|
|
39
|
-
import "./chunk-
|
|
50
|
+
import "./chunk-H5EFZBBT.js";
|
|
40
51
|
import {
|
|
52
|
+
CollisionResolver,
|
|
41
53
|
DEFAULT_AGENT_PARAMS,
|
|
54
|
+
DEFAULT_COLLISION_CONFIG,
|
|
42
55
|
DEFAULT_ORCA_CONFIG,
|
|
56
|
+
EMPTY_COLLISION,
|
|
43
57
|
KDTree,
|
|
44
58
|
ORCASolver,
|
|
59
|
+
createCollisionResolver,
|
|
45
60
|
createKDTree,
|
|
46
61
|
createORCASolver,
|
|
47
62
|
solveORCALinearProgram
|
|
48
|
-
} from "./chunk-
|
|
63
|
+
} from "./chunk-3VEX32JO.js";
|
|
49
64
|
import {
|
|
50
65
|
__name,
|
|
51
66
|
__publicField
|
|
52
67
|
} from "./chunk-T626JPC7.js";
|
|
53
68
|
|
|
54
|
-
// src/core/BinaryHeap.ts
|
|
55
|
-
var _BinaryHeap = class _BinaryHeap {
|
|
56
|
-
/**
|
|
57
|
-
* @zh 创建二叉堆
|
|
58
|
-
* @en Create binary heap
|
|
59
|
-
*
|
|
60
|
-
* @param compare - @zh 比较函数,返回负数表示 a < b @en Compare function, returns negative if a < b
|
|
61
|
-
*/
|
|
62
|
-
constructor(compare) {
|
|
63
|
-
__publicField(this, "heap", []);
|
|
64
|
-
__publicField(this, "compare");
|
|
65
|
-
this.compare = compare;
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* @zh 堆大小
|
|
69
|
-
* @en Heap size
|
|
70
|
-
*/
|
|
71
|
-
get size() {
|
|
72
|
-
return this.heap.length;
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* @zh 是否为空
|
|
76
|
-
* @en Is empty
|
|
77
|
-
*/
|
|
78
|
-
get isEmpty() {
|
|
79
|
-
return this.heap.length === 0;
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* @zh 插入元素
|
|
83
|
-
* @en Push element
|
|
84
|
-
*/
|
|
85
|
-
push(item) {
|
|
86
|
-
this.heap.push(item);
|
|
87
|
-
this.bubbleUp(this.heap.length - 1);
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* @zh 弹出最小元素
|
|
91
|
-
* @en Pop minimum element
|
|
92
|
-
*/
|
|
93
|
-
pop() {
|
|
94
|
-
if (this.heap.length === 0) {
|
|
95
|
-
return void 0;
|
|
96
|
-
}
|
|
97
|
-
const result = this.heap[0];
|
|
98
|
-
const last = this.heap.pop();
|
|
99
|
-
if (this.heap.length > 0) {
|
|
100
|
-
this.heap[0] = last;
|
|
101
|
-
this.sinkDown(0);
|
|
102
|
-
}
|
|
103
|
-
return result;
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* @zh 查看最小元素(不移除)
|
|
107
|
-
* @en Peek minimum element (without removing)
|
|
108
|
-
*/
|
|
109
|
-
peek() {
|
|
110
|
-
return this.heap[0];
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* @zh 更新元素(重新排序)
|
|
114
|
-
* @en Update element (re-sort)
|
|
115
|
-
*/
|
|
116
|
-
update(item) {
|
|
117
|
-
const index = this.heap.indexOf(item);
|
|
118
|
-
if (index !== -1) {
|
|
119
|
-
this.bubbleUp(index);
|
|
120
|
-
this.sinkDown(index);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* @zh 检查是否包含元素
|
|
125
|
-
* @en Check if contains element
|
|
126
|
-
*/
|
|
127
|
-
contains(item) {
|
|
128
|
-
return this.heap.indexOf(item) !== -1;
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* @zh 清空堆
|
|
132
|
-
* @en Clear heap
|
|
133
|
-
*/
|
|
134
|
-
clear() {
|
|
135
|
-
this.heap.length = 0;
|
|
136
|
-
}
|
|
137
|
-
/**
|
|
138
|
-
* @zh 上浮操作
|
|
139
|
-
* @en Bubble up operation
|
|
140
|
-
*/
|
|
141
|
-
bubbleUp(index) {
|
|
142
|
-
const item = this.heap[index];
|
|
143
|
-
while (index > 0) {
|
|
144
|
-
const parentIndex = Math.floor((index - 1) / 2);
|
|
145
|
-
const parent = this.heap[parentIndex];
|
|
146
|
-
if (this.compare(item, parent) >= 0) {
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
|
-
this.heap[index] = parent;
|
|
150
|
-
index = parentIndex;
|
|
151
|
-
}
|
|
152
|
-
this.heap[index] = item;
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* @zh 下沉操作
|
|
156
|
-
* @en Sink down operation
|
|
157
|
-
*/
|
|
158
|
-
sinkDown(index) {
|
|
159
|
-
const length = this.heap.length;
|
|
160
|
-
const item = this.heap[index];
|
|
161
|
-
while (true) {
|
|
162
|
-
const leftIndex = 2 * index + 1;
|
|
163
|
-
const rightIndex = 2 * index + 2;
|
|
164
|
-
let smallest = index;
|
|
165
|
-
if (leftIndex < length && this.compare(this.heap[leftIndex], this.heap[smallest]) < 0) {
|
|
166
|
-
smallest = leftIndex;
|
|
167
|
-
}
|
|
168
|
-
if (rightIndex < length && this.compare(this.heap[rightIndex], this.heap[smallest]) < 0) {
|
|
169
|
-
smallest = rightIndex;
|
|
170
|
-
}
|
|
171
|
-
if (smallest === index) {
|
|
172
|
-
break;
|
|
173
|
-
}
|
|
174
|
-
this.heap[index] = this.heap[smallest];
|
|
175
|
-
this.heap[smallest] = item;
|
|
176
|
-
index = smallest;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
};
|
|
180
|
-
__name(_BinaryHeap, "BinaryHeap");
|
|
181
|
-
var BinaryHeap = _BinaryHeap;
|
|
182
|
-
|
|
183
|
-
// src/core/AStarPathfinder.ts
|
|
184
|
-
var _AStarPathfinder = class _AStarPathfinder {
|
|
185
|
-
constructor(map) {
|
|
186
|
-
__publicField(this, "map");
|
|
187
|
-
__publicField(this, "nodeCache", /* @__PURE__ */ new Map());
|
|
188
|
-
__publicField(this, "openList");
|
|
189
|
-
this.map = map;
|
|
190
|
-
this.openList = new IndexedBinaryHeap((a, b) => a.f - b.f);
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* @zh 查找路径
|
|
194
|
-
* @en Find path
|
|
195
|
-
*/
|
|
196
|
-
findPath(startX, startY, endX, endY, options) {
|
|
197
|
-
const opts = {
|
|
198
|
-
...DEFAULT_PATHFINDING_OPTIONS,
|
|
199
|
-
...options
|
|
200
|
-
};
|
|
201
|
-
this.clear();
|
|
202
|
-
const startNode = this.map.getNodeAt(startX, startY);
|
|
203
|
-
const endNode = this.map.getNodeAt(endX, endY);
|
|
204
|
-
if (!startNode || !endNode) {
|
|
205
|
-
return EMPTY_PATH_RESULT;
|
|
206
|
-
}
|
|
207
|
-
if (!startNode.walkable || !endNode.walkable) {
|
|
208
|
-
return EMPTY_PATH_RESULT;
|
|
209
|
-
}
|
|
210
|
-
if (startNode.id === endNode.id) {
|
|
211
|
-
return {
|
|
212
|
-
found: true,
|
|
213
|
-
path: [
|
|
214
|
-
startNode.position
|
|
215
|
-
],
|
|
216
|
-
cost: 0,
|
|
217
|
-
nodesSearched: 1
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
const start = this.getOrCreateAStarNode(startNode);
|
|
221
|
-
start.g = 0;
|
|
222
|
-
start.h = this.map.heuristic(startNode.position, endNode.position) * opts.heuristicWeight;
|
|
223
|
-
start.f = start.h;
|
|
224
|
-
start.opened = true;
|
|
225
|
-
this.openList.push(start);
|
|
226
|
-
let nodesSearched = 0;
|
|
227
|
-
const endPosition = endNode.position;
|
|
228
|
-
while (!this.openList.isEmpty && nodesSearched < opts.maxNodes) {
|
|
229
|
-
const current = this.openList.pop();
|
|
230
|
-
current.closed = true;
|
|
231
|
-
nodesSearched++;
|
|
232
|
-
if (current.node.id === endNode.id) {
|
|
233
|
-
return this.buildPath(current, nodesSearched);
|
|
234
|
-
}
|
|
235
|
-
const neighbors = this.map.getNeighbors(current.node);
|
|
236
|
-
for (const neighborNode of neighbors) {
|
|
237
|
-
if (!neighborNode.walkable) {
|
|
238
|
-
continue;
|
|
239
|
-
}
|
|
240
|
-
const neighbor = this.getOrCreateAStarNode(neighborNode);
|
|
241
|
-
if (neighbor.closed) {
|
|
242
|
-
continue;
|
|
243
|
-
}
|
|
244
|
-
const movementCost = this.map.getMovementCost(current.node, neighborNode);
|
|
245
|
-
const tentativeG = current.g + movementCost;
|
|
246
|
-
if (!neighbor.opened) {
|
|
247
|
-
neighbor.g = tentativeG;
|
|
248
|
-
neighbor.h = this.map.heuristic(neighborNode.position, endPosition) * opts.heuristicWeight;
|
|
249
|
-
neighbor.f = neighbor.g + neighbor.h;
|
|
250
|
-
neighbor.parent = current;
|
|
251
|
-
neighbor.opened = true;
|
|
252
|
-
this.openList.push(neighbor);
|
|
253
|
-
} else if (tentativeG < neighbor.g) {
|
|
254
|
-
neighbor.g = tentativeG;
|
|
255
|
-
neighbor.f = neighbor.g + neighbor.h;
|
|
256
|
-
neighbor.parent = current;
|
|
257
|
-
this.openList.update(neighbor);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
return {
|
|
262
|
-
found: false,
|
|
263
|
-
path: [],
|
|
264
|
-
cost: 0,
|
|
265
|
-
nodesSearched
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
/**
|
|
269
|
-
* @zh 清理状态
|
|
270
|
-
* @en Clear state
|
|
271
|
-
*/
|
|
272
|
-
clear() {
|
|
273
|
-
this.nodeCache.clear();
|
|
274
|
-
this.openList.clear();
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* @zh 获取或创建 A* 节点
|
|
278
|
-
* @en Get or create A* node
|
|
279
|
-
*/
|
|
280
|
-
getOrCreateAStarNode(node) {
|
|
281
|
-
let astarNode = this.nodeCache.get(node.id);
|
|
282
|
-
if (!astarNode) {
|
|
283
|
-
astarNode = {
|
|
284
|
-
node,
|
|
285
|
-
g: Infinity,
|
|
286
|
-
h: 0,
|
|
287
|
-
f: Infinity,
|
|
288
|
-
parent: null,
|
|
289
|
-
closed: false,
|
|
290
|
-
opened: false,
|
|
291
|
-
heapIndex: -1
|
|
292
|
-
};
|
|
293
|
-
this.nodeCache.set(node.id, astarNode);
|
|
294
|
-
}
|
|
295
|
-
return astarNode;
|
|
296
|
-
}
|
|
297
|
-
/**
|
|
298
|
-
* @zh 构建路径结果
|
|
299
|
-
* @en Build path result
|
|
300
|
-
*/
|
|
301
|
-
buildPath(endNode, nodesSearched) {
|
|
302
|
-
const path = [];
|
|
303
|
-
let current = endNode;
|
|
304
|
-
while (current) {
|
|
305
|
-
path.push(current.node.position);
|
|
306
|
-
current = current.parent;
|
|
307
|
-
}
|
|
308
|
-
path.reverse();
|
|
309
|
-
return {
|
|
310
|
-
found: true,
|
|
311
|
-
path,
|
|
312
|
-
cost: endNode.g,
|
|
313
|
-
nodesSearched
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
};
|
|
317
|
-
__name(_AStarPathfinder, "AStarPathfinder");
|
|
318
|
-
var AStarPathfinder = _AStarPathfinder;
|
|
319
|
-
function createAStarPathfinder(map) {
|
|
320
|
-
return new AStarPathfinder(map);
|
|
321
|
-
}
|
|
322
|
-
__name(createAStarPathfinder, "createAStarPathfinder");
|
|
323
|
-
|
|
324
69
|
// src/core/GridPathfinder.ts
|
|
325
70
|
var CLOSED_FLAG = 1;
|
|
326
71
|
var OPENED_FLAG = 2;
|
|
@@ -967,1555 +712,524 @@ function createGridPathfinder(map, config) {
|
|
|
967
712
|
}
|
|
968
713
|
__name(createGridPathfinder, "createGridPathfinder");
|
|
969
714
|
|
|
970
|
-
// src/core/
|
|
971
|
-
var
|
|
972
|
-
constructor(map) {
|
|
973
|
-
__publicField(this, "map");
|
|
974
|
-
__publicField(this, "width");
|
|
975
|
-
__publicField(this, "height");
|
|
976
|
-
__publicField(this, "openList");
|
|
977
|
-
__publicField(this, "nodeGrid");
|
|
978
|
-
this.map = map;
|
|
979
|
-
const bounds = this.getMapBounds();
|
|
980
|
-
this.width = bounds.width;
|
|
981
|
-
this.height = bounds.height;
|
|
982
|
-
this.openList = new BinaryHeap((a, b) => a.f - b.f);
|
|
983
|
-
this.nodeGrid = [];
|
|
984
|
-
}
|
|
715
|
+
// src/core/PathValidator.ts
|
|
716
|
+
var _PathValidator = class _PathValidator {
|
|
985
717
|
/**
|
|
986
|
-
* @zh
|
|
987
|
-
* @en
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
x: startX,
|
|
1003
|
-
y: startY
|
|
1004
|
-
}
|
|
1005
|
-
],
|
|
1006
|
-
cost: 0,
|
|
1007
|
-
nodesSearched: 1
|
|
1008
|
-
};
|
|
1009
|
-
}
|
|
1010
|
-
this.initGrid();
|
|
1011
|
-
this.openList.clear();
|
|
1012
|
-
const startNode = this.getOrCreateNode(startX, startY);
|
|
1013
|
-
startNode.g = 0;
|
|
1014
|
-
startNode.h = this.heuristic(startX, startY, endX, endY) * opts.heuristicWeight;
|
|
1015
|
-
startNode.f = startNode.h;
|
|
1016
|
-
this.openList.push(startNode);
|
|
1017
|
-
let nodesSearched = 0;
|
|
1018
|
-
while (!this.openList.isEmpty && nodesSearched < opts.maxNodes) {
|
|
1019
|
-
const current = this.openList.pop();
|
|
1020
|
-
current.closed = true;
|
|
1021
|
-
nodesSearched++;
|
|
1022
|
-
if (current.x === endX && current.y === endY) {
|
|
718
|
+
* @zh 验证路径段的有效性
|
|
719
|
+
* @en Validate path segment validity
|
|
720
|
+
*
|
|
721
|
+
* @param path - @zh 要验证的路径 @en Path to validate
|
|
722
|
+
* @param fromIndex - @zh 起始索引 @en Start index
|
|
723
|
+
* @param toIndex - @zh 结束索引 @en End index
|
|
724
|
+
* @param map - @zh 地图实例 @en Map instance
|
|
725
|
+
* @returns @zh 验证结果 @en Validation result
|
|
726
|
+
*/
|
|
727
|
+
validatePath(path, fromIndex, toIndex, map) {
|
|
728
|
+
const end = Math.min(toIndex, path.length);
|
|
729
|
+
for (let i = fromIndex; i < end; i++) {
|
|
730
|
+
const point = path[i];
|
|
731
|
+
const x = Math.floor(point.x);
|
|
732
|
+
const y = Math.floor(point.y);
|
|
733
|
+
if (!map.isWalkable(x, y)) {
|
|
1023
734
|
return {
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
cost: current.g,
|
|
1027
|
-
nodesSearched
|
|
735
|
+
valid: false,
|
|
736
|
+
invalidIndex: i
|
|
1028
737
|
};
|
|
1029
738
|
}
|
|
1030
|
-
|
|
739
|
+
if (i > fromIndex) {
|
|
740
|
+
const prev = path[i - 1];
|
|
741
|
+
if (!this.checkLineOfSight(prev.x, prev.y, point.x, point.y, map)) {
|
|
742
|
+
return {
|
|
743
|
+
valid: false,
|
|
744
|
+
invalidIndex: i
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
}
|
|
1031
748
|
}
|
|
1032
749
|
return {
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
cost: 0,
|
|
1036
|
-
nodesSearched
|
|
750
|
+
valid: true,
|
|
751
|
+
invalidIndex: -1
|
|
1037
752
|
};
|
|
1038
753
|
}
|
|
1039
754
|
/**
|
|
1040
|
-
* @zh
|
|
1041
|
-
* @en
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
755
|
+
* @zh 检查两点之间的视线(使用 Bresenham 算法)
|
|
756
|
+
* @en Check line of sight between two points (using Bresenham algorithm)
|
|
757
|
+
*
|
|
758
|
+
* @param x1 - @zh 起点 X @en Start X
|
|
759
|
+
* @param y1 - @zh 起点 Y @en Start Y
|
|
760
|
+
* @param x2 - @zh 终点 X @en End X
|
|
761
|
+
* @param y2 - @zh 终点 Y @en End Y
|
|
762
|
+
* @param map - @zh 地图实例 @en Map instance
|
|
763
|
+
* @returns @zh 是否有视线 @en Whether there is line of sight
|
|
764
|
+
*/
|
|
765
|
+
checkLineOfSight(x1, y1, x2, y2, map) {
|
|
766
|
+
const ix1 = Math.floor(x1);
|
|
767
|
+
const iy1 = Math.floor(y1);
|
|
768
|
+
const ix2 = Math.floor(x2);
|
|
769
|
+
const iy2 = Math.floor(y2);
|
|
770
|
+
let dx = Math.abs(ix2 - ix1);
|
|
771
|
+
let dy = Math.abs(iy2 - iy1);
|
|
772
|
+
let x = ix1;
|
|
773
|
+
let y = iy1;
|
|
774
|
+
const sx = ix1 < ix2 ? 1 : -1;
|
|
775
|
+
const sy = iy1 < iy2 ? 1 : -1;
|
|
776
|
+
if (dx > dy) {
|
|
777
|
+
let err = dx / 2;
|
|
778
|
+
while (x !== ix2) {
|
|
779
|
+
if (!map.isWalkable(x, y)) {
|
|
780
|
+
return false;
|
|
781
|
+
}
|
|
782
|
+
err -= dy;
|
|
783
|
+
if (err < 0) {
|
|
784
|
+
y += sy;
|
|
785
|
+
err += dx;
|
|
786
|
+
}
|
|
787
|
+
x += sx;
|
|
788
|
+
}
|
|
789
|
+
} else {
|
|
790
|
+
let err = dy / 2;
|
|
791
|
+
while (y !== iy2) {
|
|
792
|
+
if (!map.isWalkable(x, y)) {
|
|
793
|
+
return false;
|
|
794
|
+
}
|
|
795
|
+
err -= dx;
|
|
796
|
+
if (err < 0) {
|
|
797
|
+
x += sx;
|
|
798
|
+
err += dy;
|
|
799
|
+
}
|
|
800
|
+
y += sy;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
return map.isWalkable(ix2, iy2);
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
__name(_PathValidator, "PathValidator");
|
|
807
|
+
var PathValidator = _PathValidator;
|
|
808
|
+
var _ObstacleChangeManager = class _ObstacleChangeManager {
|
|
809
|
+
constructor() {
|
|
810
|
+
__publicField(this, "changes", /* @__PURE__ */ new Map());
|
|
811
|
+
__publicField(this, "epoch", 0);
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* @zh 记录障碍物变化
|
|
815
|
+
* @en Record obstacle change
|
|
816
|
+
*
|
|
817
|
+
* @param x - @zh X 坐标 @en X coordinate
|
|
818
|
+
* @param y - @zh Y 坐标 @en Y coordinate
|
|
819
|
+
* @param wasWalkable - @zh 变化前是否可通行 @en Was walkable before change
|
|
820
|
+
*/
|
|
821
|
+
recordChange(x, y, wasWalkable) {
|
|
822
|
+
const key = `${x},${y}`;
|
|
823
|
+
this.changes.set(key, {
|
|
824
|
+
x,
|
|
825
|
+
y,
|
|
826
|
+
wasWalkable,
|
|
827
|
+
timestamp: Date.now()
|
|
828
|
+
});
|
|
1046
829
|
}
|
|
1047
|
-
// =========================================================================
|
|
1048
|
-
// 私有方法 | Private Methods
|
|
1049
|
-
// =========================================================================
|
|
1050
830
|
/**
|
|
1051
|
-
* @zh
|
|
1052
|
-
* @en Get
|
|
831
|
+
* @zh 获取影响区域
|
|
832
|
+
* @en Get affected region
|
|
833
|
+
*
|
|
834
|
+
* @returns @zh 影响区域或 null(如果没有变化)@en Affected region or null if no changes
|
|
1053
835
|
*/
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
836
|
+
getAffectedRegion() {
|
|
837
|
+
if (this.changes.size === 0) {
|
|
838
|
+
return null;
|
|
839
|
+
}
|
|
840
|
+
let minX = Infinity;
|
|
841
|
+
let minY = Infinity;
|
|
842
|
+
let maxX = -Infinity;
|
|
843
|
+
let maxY = -Infinity;
|
|
844
|
+
for (const change of this.changes.values()) {
|
|
845
|
+
minX = Math.min(minX, change.x);
|
|
846
|
+
minY = Math.min(minY, change.y);
|
|
847
|
+
maxX = Math.max(maxX, change.x);
|
|
848
|
+
maxY = Math.max(maxY, change.y);
|
|
1061
849
|
}
|
|
1062
850
|
return {
|
|
1063
|
-
|
|
1064
|
-
|
|
851
|
+
minX,
|
|
852
|
+
minY,
|
|
853
|
+
maxX,
|
|
854
|
+
maxY
|
|
1065
855
|
};
|
|
1066
856
|
}
|
|
1067
857
|
/**
|
|
1068
|
-
* @zh
|
|
1069
|
-
* @en
|
|
858
|
+
* @zh 获取所有变化
|
|
859
|
+
* @en Get all changes
|
|
860
|
+
*
|
|
861
|
+
* @returns @zh 变化列表 @en List of changes
|
|
1070
862
|
*/
|
|
1071
|
-
|
|
1072
|
-
this.
|
|
1073
|
-
for (let i = 0; i < this.width; i++) {
|
|
1074
|
-
this.nodeGrid[i] = [];
|
|
1075
|
-
}
|
|
863
|
+
getChanges() {
|
|
864
|
+
return Array.from(this.changes.values());
|
|
1076
865
|
}
|
|
1077
866
|
/**
|
|
1078
|
-
* @zh
|
|
1079
|
-
* @en
|
|
867
|
+
* @zh 检查是否有变化
|
|
868
|
+
* @en Check if there are changes
|
|
869
|
+
*
|
|
870
|
+
* @returns @zh 是否有变化 @en Whether there are changes
|
|
1080
871
|
*/
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
872
|
+
hasChanges() {
|
|
873
|
+
return this.changes.size > 0;
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* @zh 获取当前 epoch
|
|
877
|
+
* @en Get current epoch
|
|
878
|
+
*
|
|
879
|
+
* @returns @zh 当前 epoch @en Current epoch
|
|
880
|
+
*/
|
|
881
|
+
getEpoch() {
|
|
882
|
+
return this.epoch;
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* @zh 清空变化记录并推进 epoch
|
|
886
|
+
* @en Clear changes and advance epoch
|
|
887
|
+
*/
|
|
888
|
+
flush() {
|
|
889
|
+
this.changes.clear();
|
|
890
|
+
this.epoch++;
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* @zh 清空所有状态
|
|
894
|
+
* @en Clear all state
|
|
895
|
+
*/
|
|
896
|
+
clear() {
|
|
897
|
+
this.changes.clear();
|
|
898
|
+
this.epoch = 0;
|
|
899
|
+
}
|
|
900
|
+
};
|
|
901
|
+
__name(_ObstacleChangeManager, "ObstacleChangeManager");
|
|
902
|
+
var ObstacleChangeManager = _ObstacleChangeManager;
|
|
903
|
+
function createPathValidator() {
|
|
904
|
+
return new PathValidator();
|
|
905
|
+
}
|
|
906
|
+
__name(createPathValidator, "createPathValidator");
|
|
907
|
+
function createObstacleChangeManager() {
|
|
908
|
+
return new ObstacleChangeManager();
|
|
909
|
+
}
|
|
910
|
+
__name(createObstacleChangeManager, "createObstacleChangeManager");
|
|
911
|
+
|
|
912
|
+
// src/grid/GridMap.ts
|
|
913
|
+
var _GridNode = class _GridNode {
|
|
914
|
+
constructor(x, y, width, walkable = true, cost = 1) {
|
|
915
|
+
__publicField(this, "id");
|
|
916
|
+
__publicField(this, "position");
|
|
917
|
+
__publicField(this, "x");
|
|
918
|
+
__publicField(this, "y");
|
|
919
|
+
__publicField(this, "cost");
|
|
920
|
+
__publicField(this, "walkable");
|
|
921
|
+
this.x = x;
|
|
922
|
+
this.y = y;
|
|
923
|
+
this.id = y * width + x;
|
|
924
|
+
this.position = createPoint(x, y);
|
|
925
|
+
this.walkable = walkable;
|
|
926
|
+
this.cost = cost;
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
__name(_GridNode, "GridNode");
|
|
930
|
+
var GridNode = _GridNode;
|
|
931
|
+
var DIRECTIONS_4 = [
|
|
932
|
+
{
|
|
933
|
+
dx: 0,
|
|
934
|
+
dy: -1
|
|
935
|
+
},
|
|
936
|
+
{
|
|
937
|
+
dx: 1,
|
|
938
|
+
dy: 0
|
|
939
|
+
},
|
|
940
|
+
{
|
|
941
|
+
dx: 0,
|
|
942
|
+
dy: 1
|
|
943
|
+
},
|
|
944
|
+
{
|
|
945
|
+
dx: -1,
|
|
946
|
+
dy: 0
|
|
947
|
+
}
|
|
948
|
+
// Left
|
|
949
|
+
];
|
|
950
|
+
var DIRECTIONS_8 = [
|
|
951
|
+
{
|
|
952
|
+
dx: 0,
|
|
953
|
+
dy: -1
|
|
954
|
+
},
|
|
955
|
+
{
|
|
956
|
+
dx: 1,
|
|
957
|
+
dy: -1
|
|
958
|
+
},
|
|
959
|
+
{
|
|
960
|
+
dx: 1,
|
|
961
|
+
dy: 0
|
|
962
|
+
},
|
|
963
|
+
{
|
|
964
|
+
dx: 1,
|
|
965
|
+
dy: 1
|
|
966
|
+
},
|
|
967
|
+
{
|
|
968
|
+
dx: 0,
|
|
969
|
+
dy: 1
|
|
970
|
+
},
|
|
971
|
+
{
|
|
972
|
+
dx: -1,
|
|
973
|
+
dy: 1
|
|
974
|
+
},
|
|
975
|
+
{
|
|
976
|
+
dx: -1,
|
|
977
|
+
dy: 0
|
|
978
|
+
},
|
|
979
|
+
{
|
|
980
|
+
dx: -1,
|
|
981
|
+
dy: -1
|
|
982
|
+
}
|
|
983
|
+
// Up-Left
|
|
984
|
+
];
|
|
985
|
+
var DEFAULT_GRID_OPTIONS = {
|
|
986
|
+
allowDiagonal: true,
|
|
987
|
+
diagonalCost: Math.SQRT2,
|
|
988
|
+
avoidCorners: true,
|
|
989
|
+
heuristic: octileDistance
|
|
990
|
+
};
|
|
991
|
+
var _GridMap = class _GridMap {
|
|
992
|
+
constructor(width, height, options) {
|
|
993
|
+
__publicField(this, "width");
|
|
994
|
+
__publicField(this, "height");
|
|
995
|
+
__publicField(this, "nodes");
|
|
996
|
+
__publicField(this, "options");
|
|
997
|
+
if (width <= 0 || !Number.isFinite(width) || !Number.isInteger(width)) {
|
|
998
|
+
throw new Error(`width must be a positive integer, got: ${width}`);
|
|
1100
999
|
}
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
/**
|
|
1104
|
-
* @zh 启发式函数(八方向距离)
|
|
1105
|
-
* @en Heuristic function (octile distance)
|
|
1106
|
-
*/
|
|
1107
|
-
heuristic(x1, y1, x2, y2) {
|
|
1108
|
-
const dx = Math.abs(x1 - x2);
|
|
1109
|
-
const dy = Math.abs(y1 - y2);
|
|
1110
|
-
return dx + dy + (Math.SQRT2 - 2) * Math.min(dx, dy);
|
|
1111
|
-
}
|
|
1112
|
-
/**
|
|
1113
|
-
* @zh 识别后继节点(跳跃点)
|
|
1114
|
-
* @en Identify successors (jump points)
|
|
1115
|
-
*/
|
|
1116
|
-
identifySuccessors(node, endX, endY, opts) {
|
|
1117
|
-
const neighbors = this.findNeighbors(node);
|
|
1118
|
-
for (const neighbor of neighbors) {
|
|
1119
|
-
const jumpPoint = this.jump(neighbor.x, neighbor.y, node.x, node.y, endX, endY);
|
|
1120
|
-
if (jumpPoint) {
|
|
1121
|
-
const jx = jumpPoint.x;
|
|
1122
|
-
const jy = jumpPoint.y;
|
|
1123
|
-
const jpNode = this.getOrCreateNode(jx, jy);
|
|
1124
|
-
if (jpNode.closed) continue;
|
|
1125
|
-
const dx = Math.abs(jx - node.x);
|
|
1126
|
-
const dy = Math.abs(jy - node.y);
|
|
1127
|
-
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
1128
|
-
const tentativeG = node.g + distance;
|
|
1129
|
-
if (tentativeG < jpNode.g) {
|
|
1130
|
-
jpNode.g = tentativeG;
|
|
1131
|
-
jpNode.h = this.heuristic(jx, jy, endX, endY) * opts.heuristicWeight;
|
|
1132
|
-
jpNode.f = jpNode.g + jpNode.h;
|
|
1133
|
-
jpNode.parent = node;
|
|
1134
|
-
if (!this.openList.contains(jpNode)) {
|
|
1135
|
-
this.openList.push(jpNode);
|
|
1136
|
-
} else {
|
|
1137
|
-
this.openList.update(jpNode);
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1000
|
+
if (height <= 0 || !Number.isFinite(height) || !Number.isInteger(height)) {
|
|
1001
|
+
throw new Error(`height must be a positive integer, got: ${height}`);
|
|
1141
1002
|
}
|
|
1003
|
+
this.width = width;
|
|
1004
|
+
this.height = height;
|
|
1005
|
+
this.options = {
|
|
1006
|
+
...DEFAULT_GRID_OPTIONS,
|
|
1007
|
+
...options
|
|
1008
|
+
};
|
|
1009
|
+
this.nodes = this.createNodes();
|
|
1142
1010
|
}
|
|
1143
1011
|
/**
|
|
1144
|
-
* @zh
|
|
1145
|
-
* @en
|
|
1012
|
+
* @zh 创建网格节点
|
|
1013
|
+
* @en Create grid nodes
|
|
1146
1014
|
*/
|
|
1147
|
-
|
|
1148
|
-
const
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
for (let
|
|
1152
|
-
|
|
1153
|
-
if (dx2 === 0 && dy2 === 0) continue;
|
|
1154
|
-
const nx = x + dx2;
|
|
1155
|
-
const ny = y + dy2;
|
|
1156
|
-
if (this.isWalkableAt(nx, ny)) {
|
|
1157
|
-
if (dx2 !== 0 && dy2 !== 0) {
|
|
1158
|
-
if (this.isWalkableAt(x + dx2, y) || this.isWalkableAt(x, y + dy2)) {
|
|
1159
|
-
neighbors.push({
|
|
1160
|
-
x: nx,
|
|
1161
|
-
y: ny
|
|
1162
|
-
});
|
|
1163
|
-
}
|
|
1164
|
-
} else {
|
|
1165
|
-
neighbors.push({
|
|
1166
|
-
x: nx,
|
|
1167
|
-
y: ny
|
|
1168
|
-
});
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1015
|
+
createNodes() {
|
|
1016
|
+
const nodes = [];
|
|
1017
|
+
for (let y = 0; y < this.height; y++) {
|
|
1018
|
+
nodes[y] = [];
|
|
1019
|
+
for (let x = 0; x < this.width; x++) {
|
|
1020
|
+
nodes[y][x] = new GridNode(x, y, this.width, true, 1);
|
|
1172
1021
|
}
|
|
1173
|
-
return neighbors;
|
|
1174
1022
|
}
|
|
1175
|
-
|
|
1176
|
-
const dy = Math.sign(y - parent.y);
|
|
1177
|
-
if (dx !== 0 && dy !== 0) {
|
|
1178
|
-
if (this.isWalkableAt(x, y + dy)) {
|
|
1179
|
-
neighbors.push({
|
|
1180
|
-
x,
|
|
1181
|
-
y: y + dy
|
|
1182
|
-
});
|
|
1183
|
-
}
|
|
1184
|
-
if (this.isWalkableAt(x + dx, y)) {
|
|
1185
|
-
neighbors.push({
|
|
1186
|
-
x: x + dx,
|
|
1187
|
-
y
|
|
1188
|
-
});
|
|
1189
|
-
}
|
|
1190
|
-
if (this.isWalkableAt(x, y + dy) || this.isWalkableAt(x + dx, y)) {
|
|
1191
|
-
if (this.isWalkableAt(x + dx, y + dy)) {
|
|
1192
|
-
neighbors.push({
|
|
1193
|
-
x: x + dx,
|
|
1194
|
-
y: y + dy
|
|
1195
|
-
});
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
if (!this.isWalkableAt(x - dx, y) && this.isWalkableAt(x, y + dy)) {
|
|
1199
|
-
if (this.isWalkableAt(x - dx, y + dy)) {
|
|
1200
|
-
neighbors.push({
|
|
1201
|
-
x: x - dx,
|
|
1202
|
-
y: y + dy
|
|
1203
|
-
});
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
if (!this.isWalkableAt(x, y - dy) && this.isWalkableAt(x + dx, y)) {
|
|
1207
|
-
if (this.isWalkableAt(x + dx, y - dy)) {
|
|
1208
|
-
neighbors.push({
|
|
1209
|
-
x: x + dx,
|
|
1210
|
-
y: y - dy
|
|
1211
|
-
});
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
} else if (dx !== 0) {
|
|
1215
|
-
if (this.isWalkableAt(x + dx, y)) {
|
|
1216
|
-
neighbors.push({
|
|
1217
|
-
x: x + dx,
|
|
1218
|
-
y
|
|
1219
|
-
});
|
|
1220
|
-
if (!this.isWalkableAt(x, y + 1) && this.isWalkableAt(x + dx, y + 1)) {
|
|
1221
|
-
neighbors.push({
|
|
1222
|
-
x: x + dx,
|
|
1223
|
-
y: y + 1
|
|
1224
|
-
});
|
|
1225
|
-
}
|
|
1226
|
-
if (!this.isWalkableAt(x, y - 1) && this.isWalkableAt(x + dx, y - 1)) {
|
|
1227
|
-
neighbors.push({
|
|
1228
|
-
x: x + dx,
|
|
1229
|
-
y: y - 1
|
|
1230
|
-
});
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
} else if (dy !== 0) {
|
|
1234
|
-
if (this.isWalkableAt(x, y + dy)) {
|
|
1235
|
-
neighbors.push({
|
|
1236
|
-
x,
|
|
1237
|
-
y: y + dy
|
|
1238
|
-
});
|
|
1239
|
-
if (!this.isWalkableAt(x + 1, y) && this.isWalkableAt(x + 1, y + dy)) {
|
|
1240
|
-
neighbors.push({
|
|
1241
|
-
x: x + 1,
|
|
1242
|
-
y: y + dy
|
|
1243
|
-
});
|
|
1244
|
-
}
|
|
1245
|
-
if (!this.isWalkableAt(x - 1, y) && this.isWalkableAt(x - 1, y + dy)) {
|
|
1246
|
-
neighbors.push({
|
|
1247
|
-
x: x - 1,
|
|
1248
|
-
y: y + dy
|
|
1249
|
-
});
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
return neighbors;
|
|
1023
|
+
return nodes;
|
|
1254
1024
|
}
|
|
1255
1025
|
/**
|
|
1256
|
-
* @zh
|
|
1257
|
-
* @en
|
|
1026
|
+
* @zh 获取指定位置的节点
|
|
1027
|
+
* @en Get node at position
|
|
1258
1028
|
*/
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
let x = startX;
|
|
1263
|
-
let y = startY;
|
|
1264
|
-
while (true) {
|
|
1265
|
-
if (!this.isWalkableAt(x, y)) {
|
|
1266
|
-
return null;
|
|
1267
|
-
}
|
|
1268
|
-
if (x === endX && y === endY) {
|
|
1269
|
-
return {
|
|
1270
|
-
x,
|
|
1271
|
-
y
|
|
1272
|
-
};
|
|
1273
|
-
}
|
|
1274
|
-
if (dx !== 0 && dy !== 0) {
|
|
1275
|
-
if (this.isWalkableAt(x - dx, y + dy) && !this.isWalkableAt(x - dx, y) || this.isWalkableAt(x + dx, y - dy) && !this.isWalkableAt(x, y - dy)) {
|
|
1276
|
-
return {
|
|
1277
|
-
x,
|
|
1278
|
-
y
|
|
1279
|
-
};
|
|
1280
|
-
}
|
|
1281
|
-
if (this.jumpStraight(x + dx, y, dx, 0, endX, endY) || this.jumpStraight(x, y + dy, 0, dy, endX, endY)) {
|
|
1282
|
-
return {
|
|
1283
|
-
x,
|
|
1284
|
-
y
|
|
1285
|
-
};
|
|
1286
|
-
}
|
|
1287
|
-
if (!this.isWalkableAt(x + dx, y) && !this.isWalkableAt(x, y + dy)) {
|
|
1288
|
-
return null;
|
|
1289
|
-
}
|
|
1290
|
-
} else if (dx !== 0) {
|
|
1291
|
-
if (this.isWalkableAt(x + dx, y + 1) && !this.isWalkableAt(x, y + 1) || this.isWalkableAt(x + dx, y - 1) && !this.isWalkableAt(x, y - 1)) {
|
|
1292
|
-
return {
|
|
1293
|
-
x,
|
|
1294
|
-
y
|
|
1295
|
-
};
|
|
1296
|
-
}
|
|
1297
|
-
} else if (dy !== 0) {
|
|
1298
|
-
if (this.isWalkableAt(x + 1, y + dy) && !this.isWalkableAt(x + 1, y) || this.isWalkableAt(x - 1, y + dy) && !this.isWalkableAt(x - 1, y)) {
|
|
1299
|
-
return {
|
|
1300
|
-
x,
|
|
1301
|
-
y
|
|
1302
|
-
};
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
x += dx;
|
|
1306
|
-
y += dy;
|
|
1029
|
+
getNodeAt(x, y) {
|
|
1030
|
+
if (!this.isInBounds(x, y)) {
|
|
1031
|
+
return null;
|
|
1307
1032
|
}
|
|
1033
|
+
return this.nodes[y][x];
|
|
1308
1034
|
}
|
|
1309
1035
|
/**
|
|
1310
|
-
* @zh
|
|
1311
|
-
* @en
|
|
1036
|
+
* @zh 检查坐标是否在边界内
|
|
1037
|
+
* @en Check if coordinates are within bounds
|
|
1312
1038
|
*/
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
let y = startY;
|
|
1316
|
-
while (true) {
|
|
1317
|
-
if (!this.isWalkableAt(x, y)) {
|
|
1318
|
-
return false;
|
|
1319
|
-
}
|
|
1320
|
-
if (x === endX && y === endY) {
|
|
1321
|
-
return true;
|
|
1322
|
-
}
|
|
1323
|
-
if (dx !== 0) {
|
|
1324
|
-
if (this.isWalkableAt(x + dx, y + 1) && !this.isWalkableAt(x, y + 1) || this.isWalkableAt(x + dx, y - 1) && !this.isWalkableAt(x, y - 1)) {
|
|
1325
|
-
return true;
|
|
1326
|
-
}
|
|
1327
|
-
} else if (dy !== 0) {
|
|
1328
|
-
if (this.isWalkableAt(x + 1, y + dy) && !this.isWalkableAt(x + 1, y) || this.isWalkableAt(x - 1, y + dy) && !this.isWalkableAt(x - 1, y)) {
|
|
1329
|
-
return true;
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
x += dx;
|
|
1333
|
-
y += dy;
|
|
1334
|
-
}
|
|
1039
|
+
isInBounds(x, y) {
|
|
1040
|
+
return x >= 0 && x < this.width && y >= 0 && y < this.height;
|
|
1335
1041
|
}
|
|
1336
1042
|
/**
|
|
1337
1043
|
* @zh 检查位置是否可通行
|
|
1338
1044
|
* @en Check if position is walkable
|
|
1339
1045
|
*/
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
}
|
|
1344
|
-
return this.map.isWalkable(x, y);
|
|
1046
|
+
isWalkable(x, y) {
|
|
1047
|
+
const node = this.getNodeAt(x, y);
|
|
1048
|
+
return node !== null && node.walkable;
|
|
1345
1049
|
}
|
|
1346
1050
|
/**
|
|
1347
|
-
* @zh
|
|
1348
|
-
* @en
|
|
1051
|
+
* @zh 设置位置是否可通行
|
|
1052
|
+
* @en Set position walkability
|
|
1349
1053
|
*/
|
|
1350
|
-
|
|
1351
|
-
const
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
path.unshift({
|
|
1355
|
-
x: current.x,
|
|
1356
|
-
y: current.y
|
|
1357
|
-
});
|
|
1358
|
-
current = current.parent;
|
|
1054
|
+
setWalkable(x, y, walkable) {
|
|
1055
|
+
const node = this.getNodeAt(x, y);
|
|
1056
|
+
if (node) {
|
|
1057
|
+
node.walkable = walkable;
|
|
1359
1058
|
}
|
|
1360
|
-
return this.interpolatePath(path);
|
|
1361
1059
|
}
|
|
1362
1060
|
/**
|
|
1363
|
-
* @zh
|
|
1364
|
-
* @en
|
|
1061
|
+
* @zh 设置位置的移动代价
|
|
1062
|
+
* @en Set movement cost at position
|
|
1063
|
+
*
|
|
1064
|
+
* @param x - @zh X 坐标 @en X coordinate
|
|
1065
|
+
* @param y - @zh Y 坐标 @en Y coordinate
|
|
1066
|
+
* @param cost - @zh 移动代价,必须为正数 @en Movement cost, must be positive
|
|
1067
|
+
* @throws @zh 如果 cost 不是正数则抛出错误 @en Throws if cost is not positive
|
|
1365
1068
|
*/
|
|
1366
|
-
|
|
1367
|
-
if (
|
|
1368
|
-
|
|
1069
|
+
setCost(x, y, cost) {
|
|
1070
|
+
if (cost <= 0 || !Number.isFinite(cost)) {
|
|
1071
|
+
throw new Error(`cost must be a positive finite number, got: ${cost}`);
|
|
1369
1072
|
}
|
|
1370
|
-
const
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
for (let i = 1; i < jumpPoints.length; i++) {
|
|
1374
|
-
const prev = jumpPoints[i - 1];
|
|
1375
|
-
const curr = jumpPoints[i];
|
|
1376
|
-
const dx = curr.x - prev.x;
|
|
1377
|
-
const dy = curr.y - prev.y;
|
|
1378
|
-
const steps = Math.max(Math.abs(dx), Math.abs(dy));
|
|
1379
|
-
const stepX = dx === 0 ? 0 : dx / Math.abs(dx);
|
|
1380
|
-
const stepY = dy === 0 ? 0 : dy / Math.abs(dy);
|
|
1381
|
-
let x = prev.x;
|
|
1382
|
-
let y = prev.y;
|
|
1383
|
-
for (let j = 0; j < steps; j++) {
|
|
1384
|
-
if (x !== curr.x && y !== curr.y) {
|
|
1385
|
-
x += stepX;
|
|
1386
|
-
y += stepY;
|
|
1387
|
-
} else if (x !== curr.x) {
|
|
1388
|
-
x += stepX;
|
|
1389
|
-
} else if (y !== curr.y) {
|
|
1390
|
-
y += stepY;
|
|
1391
|
-
}
|
|
1392
|
-
if (x !== prev.x || y !== prev.y) {
|
|
1393
|
-
path.push({
|
|
1394
|
-
x,
|
|
1395
|
-
y
|
|
1396
|
-
});
|
|
1397
|
-
}
|
|
1398
|
-
}
|
|
1073
|
+
const node = this.getNodeAt(x, y);
|
|
1074
|
+
if (node) {
|
|
1075
|
+
node.cost = cost;
|
|
1399
1076
|
}
|
|
1400
|
-
return path;
|
|
1401
|
-
}
|
|
1402
|
-
};
|
|
1403
|
-
__name(_JPSPathfinder, "JPSPathfinder");
|
|
1404
|
-
var JPSPathfinder = _JPSPathfinder;
|
|
1405
|
-
function createJPSPathfinder(map) {
|
|
1406
|
-
return new JPSPathfinder(map);
|
|
1407
|
-
}
|
|
1408
|
-
__name(createJPSPathfinder, "createJPSPathfinder");
|
|
1409
|
-
|
|
1410
|
-
// src/core/HPAPathfinder.ts
|
|
1411
|
-
var DEFAULT_HPA_CONFIG = {
|
|
1412
|
-
clusterSize: 64,
|
|
1413
|
-
maxEntranceWidth: 16,
|
|
1414
|
-
cacheInternalPaths: true,
|
|
1415
|
-
entranceStrategy: "end",
|
|
1416
|
-
lazyIntraEdges: true
|
|
1417
|
-
};
|
|
1418
|
-
var _a3;
|
|
1419
|
-
var SubMap = (_a3 = class {
|
|
1420
|
-
constructor(parentMap, originX, originY, width, height) {
|
|
1421
|
-
__publicField(this, "parentMap");
|
|
1422
|
-
__publicField(this, "originX");
|
|
1423
|
-
__publicField(this, "originY");
|
|
1424
|
-
__publicField(this, "width");
|
|
1425
|
-
__publicField(this, "height");
|
|
1426
|
-
this.parentMap = parentMap;
|
|
1427
|
-
this.originX = originX;
|
|
1428
|
-
this.originY = originY;
|
|
1429
|
-
this.width = width;
|
|
1430
|
-
this.height = height;
|
|
1431
1077
|
}
|
|
1432
1078
|
/**
|
|
1433
|
-
* @zh
|
|
1434
|
-
* @en
|
|
1079
|
+
* @zh 获取节点的邻居
|
|
1080
|
+
* @en Get neighbors of a node
|
|
1435
1081
|
*/
|
|
1436
|
-
localToGlobal(localX, localY) {
|
|
1437
|
-
return {
|
|
1438
|
-
x: this.originX + localX,
|
|
1439
|
-
y: this.originY + localY
|
|
1440
|
-
};
|
|
1441
|
-
}
|
|
1442
|
-
/**
|
|
1443
|
-
* @zh 全局坐标转局部坐标
|
|
1444
|
-
* @en Convert global to local coordinates
|
|
1445
|
-
*/
|
|
1446
|
-
globalToLocal(globalX, globalY) {
|
|
1447
|
-
return {
|
|
1448
|
-
x: globalX - this.originX,
|
|
1449
|
-
y: globalY - this.originY
|
|
1450
|
-
};
|
|
1451
|
-
}
|
|
1452
|
-
isWalkable(x, y) {
|
|
1453
|
-
if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
|
|
1454
|
-
return false;
|
|
1455
|
-
}
|
|
1456
|
-
return this.parentMap.isWalkable(this.originX + x, this.originY + y);
|
|
1457
|
-
}
|
|
1458
|
-
getNodeAt(x, y) {
|
|
1459
|
-
if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
|
|
1460
|
-
return null;
|
|
1461
|
-
}
|
|
1462
|
-
const globalNode = this.parentMap.getNodeAt(this.originX + x, this.originY + y);
|
|
1463
|
-
if (!globalNode) return null;
|
|
1464
|
-
return {
|
|
1465
|
-
id: y * this.width + x,
|
|
1466
|
-
position: {
|
|
1467
|
-
x,
|
|
1468
|
-
y
|
|
1469
|
-
},
|
|
1470
|
-
cost: globalNode.cost,
|
|
1471
|
-
walkable: globalNode.walkable
|
|
1472
|
-
};
|
|
1473
|
-
}
|
|
1474
1082
|
getNeighbors(node) {
|
|
1475
1083
|
const neighbors = [];
|
|
1476
1084
|
const { x, y } = node.position;
|
|
1477
|
-
const directions =
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
dy: -1
|
|
1481
|
-
},
|
|
1482
|
-
{
|
|
1483
|
-
dx: 1,
|
|
1484
|
-
dy: -1
|
|
1485
|
-
},
|
|
1486
|
-
{
|
|
1487
|
-
dx: 1,
|
|
1488
|
-
dy: 0
|
|
1489
|
-
},
|
|
1490
|
-
{
|
|
1491
|
-
dx: 1,
|
|
1492
|
-
dy: 1
|
|
1493
|
-
},
|
|
1494
|
-
{
|
|
1495
|
-
dx: 0,
|
|
1496
|
-
dy: 1
|
|
1497
|
-
},
|
|
1498
|
-
{
|
|
1499
|
-
dx: -1,
|
|
1500
|
-
dy: 1
|
|
1501
|
-
},
|
|
1502
|
-
{
|
|
1503
|
-
dx: -1,
|
|
1504
|
-
dy: 0
|
|
1505
|
-
},
|
|
1506
|
-
{
|
|
1507
|
-
dx: -1,
|
|
1508
|
-
dy: -1
|
|
1509
|
-
}
|
|
1510
|
-
// NW
|
|
1511
|
-
];
|
|
1512
|
-
for (const dir of directions) {
|
|
1085
|
+
const directions = this.options.allowDiagonal ? DIRECTIONS_8 : DIRECTIONS_4;
|
|
1086
|
+
for (let i = 0; i < directions.length; i++) {
|
|
1087
|
+
const dir = directions[i];
|
|
1513
1088
|
const nx = x + dir.dx;
|
|
1514
1089
|
const ny = y + dir.dy;
|
|
1515
1090
|
if (nx < 0 || nx >= this.width || ny < 0 || ny >= this.height) {
|
|
1516
1091
|
continue;
|
|
1517
1092
|
}
|
|
1518
|
-
|
|
1093
|
+
const neighbor = this.nodes[ny][nx];
|
|
1094
|
+
if (!neighbor.walkable) {
|
|
1519
1095
|
continue;
|
|
1520
1096
|
}
|
|
1521
|
-
if (dir.dx !== 0 && dir.dy !== 0) {
|
|
1522
|
-
|
|
1097
|
+
if (this.options.avoidCorners && dir.dx !== 0 && dir.dy !== 0) {
|
|
1098
|
+
const hNode = this.nodes[y][x + dir.dx];
|
|
1099
|
+
const vNode = this.nodes[y + dir.dy][x];
|
|
1100
|
+
if (!hNode.walkable || !vNode.walkable) {
|
|
1523
1101
|
continue;
|
|
1524
1102
|
}
|
|
1525
1103
|
}
|
|
1526
|
-
|
|
1527
|
-
if (neighborNode) {
|
|
1528
|
-
neighbors.push(neighborNode);
|
|
1529
|
-
}
|
|
1104
|
+
neighbors.push(neighbor);
|
|
1530
1105
|
}
|
|
1531
1106
|
return neighbors;
|
|
1532
1107
|
}
|
|
1533
|
-
heuristic(a, b) {
|
|
1534
|
-
const dx = Math.abs(a.x - b.x);
|
|
1535
|
-
const dy = Math.abs(a.y - b.y);
|
|
1536
|
-
return dx + dy + (Math.SQRT2 - 2) * Math.min(dx, dy);
|
|
1537
|
-
}
|
|
1538
|
-
getMovementCost(from, to) {
|
|
1539
|
-
const dx = Math.abs(to.position.x - from.position.x);
|
|
1540
|
-
const dy = Math.abs(to.position.y - from.position.y);
|
|
1541
|
-
const baseCost = dx !== 0 && dy !== 0 ? Math.SQRT2 : 1;
|
|
1542
|
-
return baseCost * to.cost;
|
|
1543
|
-
}
|
|
1544
|
-
}, __name(_a3, "SubMap"), _a3);
|
|
1545
|
-
var _a4;
|
|
1546
|
-
var Cluster = (_a4 = class {
|
|
1547
|
-
constructor(id, originX, originY, width, height, parentMap) {
|
|
1548
|
-
__publicField(this, "id");
|
|
1549
|
-
__publicField(this, "originX");
|
|
1550
|
-
__publicField(this, "originY");
|
|
1551
|
-
__publicField(this, "width");
|
|
1552
|
-
__publicField(this, "height");
|
|
1553
|
-
__publicField(this, "subMap");
|
|
1554
|
-
/** @zh 集群内的抽象节点 ID 列表 @en Abstract node IDs in this cluster */
|
|
1555
|
-
__publicField(this, "nodeIds", []);
|
|
1556
|
-
/** @zh 预计算的距离缓存 @en Precomputed distance cache */
|
|
1557
|
-
__publicField(this, "distanceCache", /* @__PURE__ */ new Map());
|
|
1558
|
-
/** @zh 预计算的路径缓存 @en Precomputed path cache */
|
|
1559
|
-
__publicField(this, "pathCache", /* @__PURE__ */ new Map());
|
|
1560
|
-
this.id = id;
|
|
1561
|
-
this.originX = originX;
|
|
1562
|
-
this.originY = originY;
|
|
1563
|
-
this.width = width;
|
|
1564
|
-
this.height = height;
|
|
1565
|
-
this.subMap = new SubMap(parentMap, originX, originY, width, height);
|
|
1566
|
-
}
|
|
1567
|
-
/**
|
|
1568
|
-
* @zh 检查点是否在集群内
|
|
1569
|
-
* @en Check if point is in cluster
|
|
1570
|
-
*/
|
|
1571
|
-
containsPoint(x, y) {
|
|
1572
|
-
return x >= this.originX && x < this.originX + this.width && y >= this.originY && y < this.originY + this.height;
|
|
1573
|
-
}
|
|
1574
|
-
/**
|
|
1575
|
-
* @zh 添加节点 ID
|
|
1576
|
-
* @en Add node ID
|
|
1577
|
-
*/
|
|
1578
|
-
addNodeId(nodeId) {
|
|
1579
|
-
if (!this.nodeIds.includes(nodeId)) {
|
|
1580
|
-
this.nodeIds.push(nodeId);
|
|
1581
|
-
}
|
|
1582
|
-
}
|
|
1583
|
-
/**
|
|
1584
|
-
* @zh 移除节点 ID
|
|
1585
|
-
* @en Remove node ID
|
|
1586
|
-
*/
|
|
1587
|
-
removeNodeId(nodeId) {
|
|
1588
|
-
const idx = this.nodeIds.indexOf(nodeId);
|
|
1589
|
-
if (idx !== -1) {
|
|
1590
|
-
this.nodeIds.splice(idx, 1);
|
|
1591
|
-
}
|
|
1592
|
-
}
|
|
1593
|
-
/**
|
|
1594
|
-
* @zh 生成缓存键
|
|
1595
|
-
* @en Generate cache key
|
|
1596
|
-
*/
|
|
1597
|
-
getCacheKey(fromId, toId) {
|
|
1598
|
-
return `${fromId}->${toId}`;
|
|
1599
|
-
}
|
|
1600
|
-
/**
|
|
1601
|
-
* @zh 设置缓存
|
|
1602
|
-
* @en Set cache
|
|
1603
|
-
*/
|
|
1604
|
-
setCache(fromId, toId, cost, path) {
|
|
1605
|
-
const key = this.getCacheKey(fromId, toId);
|
|
1606
|
-
this.distanceCache.set(key, cost);
|
|
1607
|
-
this.pathCache.set(key, path);
|
|
1608
|
-
}
|
|
1609
|
-
/**
|
|
1610
|
-
* @zh 获取缓存的距离
|
|
1611
|
-
* @en Get cached distance
|
|
1612
|
-
*/
|
|
1613
|
-
getCachedDistance(fromId, toId) {
|
|
1614
|
-
return this.distanceCache.get(this.getCacheKey(fromId, toId));
|
|
1615
|
-
}
|
|
1616
|
-
/**
|
|
1617
|
-
* @zh 获取缓存的路径
|
|
1618
|
-
* @en Get cached path
|
|
1619
|
-
*/
|
|
1620
|
-
getCachedPath(fromId, toId) {
|
|
1621
|
-
return this.pathCache.get(this.getCacheKey(fromId, toId));
|
|
1622
|
-
}
|
|
1623
|
-
/**
|
|
1624
|
-
* @zh 清除缓存
|
|
1625
|
-
* @en Clear cache
|
|
1626
|
-
*/
|
|
1627
|
-
clearCache() {
|
|
1628
|
-
this.distanceCache.clear();
|
|
1629
|
-
this.pathCache.clear();
|
|
1630
|
-
}
|
|
1631
|
-
/**
|
|
1632
|
-
* @zh 获取缓存大小
|
|
1633
|
-
* @en Get cache size
|
|
1634
|
-
*/
|
|
1635
|
-
getCacheSize() {
|
|
1636
|
-
return this.distanceCache.size;
|
|
1637
|
-
}
|
|
1638
|
-
}, __name(_a4, "Cluster"), _a4);
|
|
1639
|
-
var _HPAPathfinder = class _HPAPathfinder {
|
|
1640
|
-
constructor(map, config) {
|
|
1641
|
-
__publicField(this, "map");
|
|
1642
|
-
__publicField(this, "config");
|
|
1643
|
-
__publicField(this, "mapWidth");
|
|
1644
|
-
__publicField(this, "mapHeight");
|
|
1645
|
-
// 集群管理
|
|
1646
|
-
__publicField(this, "clusters", []);
|
|
1647
|
-
__publicField(this, "clusterGrid", []);
|
|
1648
|
-
__publicField(this, "clustersX", 0);
|
|
1649
|
-
__publicField(this, "clustersY", 0);
|
|
1650
|
-
// 抽象图
|
|
1651
|
-
__publicField(this, "abstractNodes", /* @__PURE__ */ new Map());
|
|
1652
|
-
__publicField(this, "nodesByCluster", /* @__PURE__ */ new Map());
|
|
1653
|
-
__publicField(this, "nextNodeId", 0);
|
|
1654
|
-
// 入口统计
|
|
1655
|
-
__publicField(this, "entranceCount", 0);
|
|
1656
|
-
// 内部寻路器
|
|
1657
|
-
__publicField(this, "localPathfinder");
|
|
1658
|
-
// 完整路径缓存
|
|
1659
|
-
__publicField(this, "pathCache");
|
|
1660
|
-
__publicField(this, "mapVersion", 0);
|
|
1661
|
-
__publicField(this, "preprocessed", false);
|
|
1662
|
-
this.map = map;
|
|
1663
|
-
this.config = {
|
|
1664
|
-
...DEFAULT_HPA_CONFIG,
|
|
1665
|
-
...config
|
|
1666
|
-
};
|
|
1667
|
-
const bounds = this.getMapBounds();
|
|
1668
|
-
this.mapWidth = bounds.width;
|
|
1669
|
-
this.mapHeight = bounds.height;
|
|
1670
|
-
this.localPathfinder = new AStarPathfinder(map);
|
|
1671
|
-
this.pathCache = new PathCache({
|
|
1672
|
-
maxEntries: 1e3,
|
|
1673
|
-
ttlMs: 0
|
|
1674
|
-
});
|
|
1675
|
-
}
|
|
1676
|
-
// =========================================================================
|
|
1677
|
-
// 公共 API | Public API
|
|
1678
|
-
// =========================================================================
|
|
1679
|
-
/**
|
|
1680
|
-
* @zh 预处理地图(构建抽象图)
|
|
1681
|
-
* @en Preprocess map (build abstract graph)
|
|
1682
|
-
*/
|
|
1683
|
-
preprocess() {
|
|
1684
|
-
this.clear();
|
|
1685
|
-
this.buildClusters();
|
|
1686
|
-
this.buildEntrances();
|
|
1687
|
-
this.buildIntraEdges();
|
|
1688
|
-
this.preprocessed = true;
|
|
1689
|
-
}
|
|
1690
|
-
/**
|
|
1691
|
-
* @zh 寻找路径
|
|
1692
|
-
* @en Find path
|
|
1693
|
-
*/
|
|
1694
|
-
findPath(startX, startY, endX, endY, options) {
|
|
1695
|
-
if (!this.preprocessed) {
|
|
1696
|
-
this.preprocess();
|
|
1697
|
-
}
|
|
1698
|
-
const opts = {
|
|
1699
|
-
...DEFAULT_PATHFINDING_OPTIONS,
|
|
1700
|
-
...options
|
|
1701
|
-
};
|
|
1702
|
-
if (!this.map.isWalkable(startX, startY) || !this.map.isWalkable(endX, endY)) {
|
|
1703
|
-
return EMPTY_PATH_RESULT;
|
|
1704
|
-
}
|
|
1705
|
-
if (startX === endX && startY === endY) {
|
|
1706
|
-
return {
|
|
1707
|
-
found: true,
|
|
1708
|
-
path: [
|
|
1709
|
-
{
|
|
1710
|
-
x: startX,
|
|
1711
|
-
y: startY
|
|
1712
|
-
}
|
|
1713
|
-
],
|
|
1714
|
-
cost: 0,
|
|
1715
|
-
nodesSearched: 1
|
|
1716
|
-
};
|
|
1717
|
-
}
|
|
1718
|
-
const cached = this.pathCache.get(startX, startY, endX, endY, this.mapVersion);
|
|
1719
|
-
if (cached) {
|
|
1720
|
-
return cached;
|
|
1721
|
-
}
|
|
1722
|
-
const startCluster = this.getClusterAt(startX, startY);
|
|
1723
|
-
const endCluster = this.getClusterAt(endX, endY);
|
|
1724
|
-
if (!startCluster || !endCluster) {
|
|
1725
|
-
return EMPTY_PATH_RESULT;
|
|
1726
|
-
}
|
|
1727
|
-
let result;
|
|
1728
|
-
if (startCluster.id === endCluster.id) {
|
|
1729
|
-
result = this.findLocalPath(startX, startY, endX, endY, opts);
|
|
1730
|
-
} else {
|
|
1731
|
-
const startTemp = this.insertTempNode(startX, startY, startCluster);
|
|
1732
|
-
const endTemp = this.insertTempNode(endX, endY, endCluster);
|
|
1733
|
-
const abstractPath = this.abstractSearch(startTemp, endTemp, opts);
|
|
1734
|
-
this.removeTempNode(startTemp, startCluster);
|
|
1735
|
-
this.removeTempNode(endTemp, endCluster);
|
|
1736
|
-
if (!abstractPath || abstractPath.length === 0) {
|
|
1737
|
-
return EMPTY_PATH_RESULT;
|
|
1738
|
-
}
|
|
1739
|
-
result = this.refinePath(abstractPath, startX, startY, endX, endY, opts);
|
|
1740
|
-
}
|
|
1741
|
-
if (result.found) {
|
|
1742
|
-
this.pathCache.set(startX, startY, endX, endY, result, this.mapVersion);
|
|
1743
|
-
}
|
|
1744
|
-
return result;
|
|
1745
|
-
}
|
|
1746
1108
|
/**
|
|
1747
|
-
* @zh
|
|
1748
|
-
* @en
|
|
1109
|
+
* @zh 遍历节点的邻居(零分配)
|
|
1110
|
+
* @en Iterate over neighbors (zero allocation)
|
|
1749
1111
|
*/
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
this.
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
this.preprocessed = false;
|
|
1760
|
-
}
|
|
1761
|
-
/**
|
|
1762
|
-
* @zh 通知地图区域变化
|
|
1763
|
-
* @en Notify map region change
|
|
1764
|
-
*/
|
|
1765
|
-
notifyRegionChange(minX, minY, maxX, maxY) {
|
|
1766
|
-
const affectedClusters = this.getAffectedClusters(minX, minY, maxX, maxY);
|
|
1767
|
-
for (const cluster of affectedClusters) {
|
|
1768
|
-
cluster.clearCache();
|
|
1769
|
-
for (const nodeId of cluster.nodeIds) {
|
|
1770
|
-
const node = this.abstractNodes.get(nodeId);
|
|
1771
|
-
if (node) {
|
|
1772
|
-
node.edges = node.edges.filter((e) => e.isInterEdge);
|
|
1773
|
-
}
|
|
1774
|
-
}
|
|
1775
|
-
this.buildClusterIntraEdges(cluster);
|
|
1776
|
-
}
|
|
1777
|
-
this.pathCache.invalidateRegion(minX, minY, maxX, maxY);
|
|
1778
|
-
this.mapVersion++;
|
|
1779
|
-
}
|
|
1780
|
-
/**
|
|
1781
|
-
* @zh 获取预处理统计信息
|
|
1782
|
-
* @en Get preprocessing statistics
|
|
1783
|
-
*/
|
|
1784
|
-
getStats() {
|
|
1785
|
-
let cacheSize = 0;
|
|
1786
|
-
for (const cluster of this.clusters) {
|
|
1787
|
-
cacheSize += cluster.getCacheSize();
|
|
1788
|
-
}
|
|
1789
|
-
return {
|
|
1790
|
-
clusters: this.clusters.length,
|
|
1791
|
-
entrances: this.entranceCount,
|
|
1792
|
-
abstractNodes: this.abstractNodes.size,
|
|
1793
|
-
cacheSize
|
|
1794
|
-
};
|
|
1795
|
-
}
|
|
1796
|
-
// =========================================================================
|
|
1797
|
-
// 预处理方法 | Preprocessing Methods
|
|
1798
|
-
// =========================================================================
|
|
1799
|
-
getMapBounds() {
|
|
1800
|
-
const mapAny = this.map;
|
|
1801
|
-
if (typeof mapAny.width === "number" && typeof mapAny.height === "number") {
|
|
1802
|
-
return {
|
|
1803
|
-
width: mapAny.width,
|
|
1804
|
-
height: mapAny.height
|
|
1805
|
-
};
|
|
1806
|
-
}
|
|
1807
|
-
return {
|
|
1808
|
-
width: 1e3,
|
|
1809
|
-
height: 1e3
|
|
1810
|
-
};
|
|
1811
|
-
}
|
|
1812
|
-
/**
|
|
1813
|
-
* @zh 构建集群
|
|
1814
|
-
* @en Build clusters
|
|
1815
|
-
*/
|
|
1816
|
-
buildClusters() {
|
|
1817
|
-
const clusterSize = this.config.clusterSize;
|
|
1818
|
-
this.clustersX = Math.ceil(this.mapWidth / clusterSize);
|
|
1819
|
-
this.clustersY = Math.ceil(this.mapHeight / clusterSize);
|
|
1820
|
-
this.clusterGrid = [];
|
|
1821
|
-
for (let cx = 0; cx < this.clustersX; cx++) {
|
|
1822
|
-
this.clusterGrid[cx] = [];
|
|
1823
|
-
for (let cy = 0; cy < this.clustersY; cy++) {
|
|
1824
|
-
this.clusterGrid[cx][cy] = null;
|
|
1825
|
-
}
|
|
1826
|
-
}
|
|
1827
|
-
let clusterId = 0;
|
|
1828
|
-
for (let cy = 0; cy < this.clustersY; cy++) {
|
|
1829
|
-
for (let cx = 0; cx < this.clustersX; cx++) {
|
|
1830
|
-
const originX = cx * clusterSize;
|
|
1831
|
-
const originY = cy * clusterSize;
|
|
1832
|
-
const width = Math.min(clusterSize, this.mapWidth - originX);
|
|
1833
|
-
const height = Math.min(clusterSize, this.mapHeight - originY);
|
|
1834
|
-
const cluster = new Cluster(clusterId, originX, originY, width, height, this.map);
|
|
1835
|
-
this.clusters.push(cluster);
|
|
1836
|
-
this.clusterGrid[cx][cy] = clusterId;
|
|
1837
|
-
this.nodesByCluster.set(clusterId, []);
|
|
1838
|
-
clusterId++;
|
|
1839
|
-
}
|
|
1840
|
-
}
|
|
1841
|
-
}
|
|
1842
|
-
/**
|
|
1843
|
-
* @zh 检测入口并创建抽象节点
|
|
1844
|
-
* @en Detect entrances and create abstract nodes
|
|
1845
|
-
*/
|
|
1846
|
-
buildEntrances() {
|
|
1847
|
-
const clusterSize = this.config.clusterSize;
|
|
1848
|
-
for (let cy = 0; cy < this.clustersY; cy++) {
|
|
1849
|
-
for (let cx = 0; cx < this.clustersX; cx++) {
|
|
1850
|
-
const clusterId = this.clusterGrid[cx][cy];
|
|
1851
|
-
if (clusterId === null) continue;
|
|
1852
|
-
const cluster1 = this.clusters[clusterId];
|
|
1853
|
-
if (cx < this.clustersX - 1) {
|
|
1854
|
-
const cluster2Id = this.clusterGrid[cx + 1][cy];
|
|
1855
|
-
if (cluster2Id !== null) {
|
|
1856
|
-
const cluster2 = this.clusters[cluster2Id];
|
|
1857
|
-
this.detectAndCreateEntrances(cluster1, cluster2, "vertical");
|
|
1858
|
-
}
|
|
1859
|
-
}
|
|
1860
|
-
if (cy < this.clustersY - 1) {
|
|
1861
|
-
const cluster2Id = this.clusterGrid[cx][cy + 1];
|
|
1862
|
-
if (cluster2Id !== null) {
|
|
1863
|
-
const cluster2 = this.clusters[cluster2Id];
|
|
1864
|
-
this.detectAndCreateEntrances(cluster1, cluster2, "horizontal");
|
|
1865
|
-
}
|
|
1866
|
-
}
|
|
1867
|
-
}
|
|
1868
|
-
}
|
|
1869
|
-
}
|
|
1870
|
-
/**
|
|
1871
|
-
* @zh 检测并创建两个相邻集群之间的入口
|
|
1872
|
-
* @en Detect and create entrances between two adjacent clusters
|
|
1873
|
-
*/
|
|
1874
|
-
detectAndCreateEntrances(cluster1, cluster2, boundaryDirection) {
|
|
1875
|
-
const spans = this.detectEntranceSpans(cluster1, cluster2, boundaryDirection);
|
|
1876
|
-
for (const span of spans) {
|
|
1877
|
-
this.createEntranceNodes(cluster1, cluster2, span, boundaryDirection);
|
|
1878
|
-
}
|
|
1879
|
-
}
|
|
1880
|
-
/**
|
|
1881
|
-
* @zh 检测边界上的连续可通行区间
|
|
1882
|
-
* @en Detect continuous walkable spans on boundary
|
|
1883
|
-
*/
|
|
1884
|
-
detectEntranceSpans(cluster1, cluster2, boundaryDirection) {
|
|
1885
|
-
const spans = [];
|
|
1886
|
-
if (boundaryDirection === "vertical") {
|
|
1887
|
-
const x1 = cluster1.originX + cluster1.width - 1;
|
|
1888
|
-
const x2 = cluster2.originX;
|
|
1889
|
-
const startY = Math.max(cluster1.originY, cluster2.originY);
|
|
1890
|
-
const endY = Math.min(cluster1.originY + cluster1.height, cluster2.originY + cluster2.height);
|
|
1891
|
-
let spanStart = null;
|
|
1892
|
-
for (let y = startY; y < endY; y++) {
|
|
1893
|
-
const walkable1 = this.map.isWalkable(x1, y);
|
|
1894
|
-
const walkable2 = this.map.isWalkable(x2, y);
|
|
1895
|
-
if (walkable1 && walkable2) {
|
|
1896
|
-
if (spanStart === null) {
|
|
1897
|
-
spanStart = y;
|
|
1898
|
-
}
|
|
1899
|
-
} else {
|
|
1900
|
-
if (spanStart !== null) {
|
|
1901
|
-
spans.push({
|
|
1902
|
-
start: spanStart,
|
|
1903
|
-
end: y - 1
|
|
1904
|
-
});
|
|
1905
|
-
spanStart = null;
|
|
1906
|
-
}
|
|
1907
|
-
}
|
|
1908
|
-
}
|
|
1909
|
-
if (spanStart !== null) {
|
|
1910
|
-
spans.push({
|
|
1911
|
-
start: spanStart,
|
|
1912
|
-
end: endY - 1
|
|
1913
|
-
});
|
|
1914
|
-
}
|
|
1915
|
-
} else {
|
|
1916
|
-
const y1 = cluster1.originY + cluster1.height - 1;
|
|
1917
|
-
const y2 = cluster2.originY;
|
|
1918
|
-
const startX = Math.max(cluster1.originX, cluster2.originX);
|
|
1919
|
-
const endX = Math.min(cluster1.originX + cluster1.width, cluster2.originX + cluster2.width);
|
|
1920
|
-
let spanStart = null;
|
|
1921
|
-
for (let x = startX; x < endX; x++) {
|
|
1922
|
-
const walkable1 = this.map.isWalkable(x, y1);
|
|
1923
|
-
const walkable2 = this.map.isWalkable(x, y2);
|
|
1924
|
-
if (walkable1 && walkable2) {
|
|
1925
|
-
if (spanStart === null) {
|
|
1926
|
-
spanStart = x;
|
|
1927
|
-
}
|
|
1928
|
-
} else {
|
|
1929
|
-
if (spanStart !== null) {
|
|
1930
|
-
spans.push({
|
|
1931
|
-
start: spanStart,
|
|
1932
|
-
end: x - 1
|
|
1933
|
-
});
|
|
1934
|
-
spanStart = null;
|
|
1935
|
-
}
|
|
1936
|
-
}
|
|
1937
|
-
}
|
|
1938
|
-
if (spanStart !== null) {
|
|
1939
|
-
spans.push({
|
|
1940
|
-
start: spanStart,
|
|
1941
|
-
end: endX - 1
|
|
1942
|
-
});
|
|
1112
|
+
forEachNeighbor(node, callback) {
|
|
1113
|
+
const { x, y } = node.position;
|
|
1114
|
+
const directions = this.options.allowDiagonal ? DIRECTIONS_8 : DIRECTIONS_4;
|
|
1115
|
+
for (let i = 0; i < directions.length; i++) {
|
|
1116
|
+
const dir = directions[i];
|
|
1117
|
+
const nx = x + dir.dx;
|
|
1118
|
+
const ny = y + dir.dy;
|
|
1119
|
+
if (nx < 0 || nx >= this.width || ny < 0 || ny >= this.height) {
|
|
1120
|
+
continue;
|
|
1943
1121
|
}
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
/**
|
|
1948
|
-
* @zh 为入口区间创建抽象节点
|
|
1949
|
-
* @en Create abstract nodes for entrance span
|
|
1950
|
-
*/
|
|
1951
|
-
createEntranceNodes(cluster1, cluster2, span, boundaryDirection) {
|
|
1952
|
-
const spanLength = span.end - span.start + 1;
|
|
1953
|
-
const maxWidth = this.config.maxEntranceWidth;
|
|
1954
|
-
const strategy = this.config.entranceStrategy;
|
|
1955
|
-
const positions = [];
|
|
1956
|
-
if (spanLength <= maxWidth) {
|
|
1957
|
-
positions.push(Math.floor((span.start + span.end) / 2));
|
|
1958
|
-
} else {
|
|
1959
|
-
const numNodes = Math.ceil(spanLength / maxWidth);
|
|
1960
|
-
const spacing = spanLength / numNodes;
|
|
1961
|
-
for (let i = 0; i < numNodes; i++) {
|
|
1962
|
-
const pos = Math.floor(span.start + spacing * (i + 0.5));
|
|
1963
|
-
positions.push(Math.min(pos, span.end));
|
|
1122
|
+
const neighbor = this.nodes[ny][nx];
|
|
1123
|
+
if (!neighbor.walkable) {
|
|
1124
|
+
continue;
|
|
1964
1125
|
}
|
|
1965
|
-
if (
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
positions.push(span.end);
|
|
1126
|
+
if (this.options.avoidCorners && dir.dx !== 0 && dir.dy !== 0) {
|
|
1127
|
+
const hNode = this.nodes[y][x + dir.dx];
|
|
1128
|
+
const vNode = this.nodes[y + dir.dy][x];
|
|
1129
|
+
if (!hNode.walkable || !vNode.walkable) {
|
|
1130
|
+
continue;
|
|
1971
1131
|
}
|
|
1972
1132
|
}
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
let p1, p2;
|
|
1976
|
-
if (boundaryDirection === "vertical") {
|
|
1977
|
-
p1 = {
|
|
1978
|
-
x: cluster1.originX + cluster1.width - 1,
|
|
1979
|
-
y: pos
|
|
1980
|
-
};
|
|
1981
|
-
p2 = {
|
|
1982
|
-
x: cluster2.originX,
|
|
1983
|
-
y: pos
|
|
1984
|
-
};
|
|
1985
|
-
} else {
|
|
1986
|
-
p1 = {
|
|
1987
|
-
x: pos,
|
|
1988
|
-
y: cluster1.originY + cluster1.height - 1
|
|
1989
|
-
};
|
|
1990
|
-
p2 = {
|
|
1991
|
-
x: pos,
|
|
1992
|
-
y: cluster2.originY
|
|
1993
|
-
};
|
|
1994
|
-
}
|
|
1995
|
-
const node1 = this.createAbstractNode(p1, cluster1);
|
|
1996
|
-
const node2 = this.createAbstractNode(p2, cluster2);
|
|
1997
|
-
const interCost = 1;
|
|
1998
|
-
node1.edges.push({
|
|
1999
|
-
targetNodeId: node2.id,
|
|
2000
|
-
cost: interCost,
|
|
2001
|
-
isInterEdge: true,
|
|
2002
|
-
innerPath: null
|
|
2003
|
-
});
|
|
2004
|
-
node2.edges.push({
|
|
2005
|
-
targetNodeId: node1.id,
|
|
2006
|
-
cost: interCost,
|
|
2007
|
-
isInterEdge: true,
|
|
2008
|
-
innerPath: null
|
|
2009
|
-
});
|
|
2010
|
-
this.entranceCount++;
|
|
2011
|
-
}
|
|
2012
|
-
}
|
|
2013
|
-
/**
|
|
2014
|
-
* @zh 创建抽象节点
|
|
2015
|
-
* @en Create abstract node
|
|
2016
|
-
*/
|
|
2017
|
-
createAbstractNode(position, cluster) {
|
|
2018
|
-
const concreteId = position.y * this.mapWidth + position.x;
|
|
2019
|
-
for (const nodeId of cluster.nodeIds) {
|
|
2020
|
-
const existing = this.abstractNodes.get(nodeId);
|
|
2021
|
-
if (existing && existing.concreteNodeId === concreteId) {
|
|
2022
|
-
return existing;
|
|
1133
|
+
if (callback(neighbor) === false) {
|
|
1134
|
+
return;
|
|
2023
1135
|
}
|
|
2024
1136
|
}
|
|
2025
|
-
const node = {
|
|
2026
|
-
id: this.nextNodeId++,
|
|
2027
|
-
position: {
|
|
2028
|
-
x: position.x,
|
|
2029
|
-
y: position.y
|
|
2030
|
-
},
|
|
2031
|
-
clusterId: cluster.id,
|
|
2032
|
-
concreteNodeId: concreteId,
|
|
2033
|
-
edges: []
|
|
2034
|
-
};
|
|
2035
|
-
this.abstractNodes.set(node.id, node);
|
|
2036
|
-
cluster.addNodeId(node.id);
|
|
2037
|
-
const clusterNodes = this.nodesByCluster.get(cluster.id);
|
|
2038
|
-
if (clusterNodes) {
|
|
2039
|
-
clusterNodes.push(node.id);
|
|
2040
|
-
}
|
|
2041
|
-
return node;
|
|
2042
|
-
}
|
|
2043
|
-
/**
|
|
2044
|
-
* @zh 构建所有集群的 intra-edges
|
|
2045
|
-
* @en Build intra-edges for all clusters
|
|
2046
|
-
*/
|
|
2047
|
-
buildIntraEdges() {
|
|
2048
|
-
for (const cluster of this.clusters) {
|
|
2049
|
-
this.buildClusterIntraEdges(cluster);
|
|
2050
|
-
}
|
|
2051
1137
|
}
|
|
2052
1138
|
/**
|
|
2053
|
-
* @zh
|
|
2054
|
-
* @en
|
|
1139
|
+
* @zh 计算启发式距离
|
|
1140
|
+
* @en Calculate heuristic distance
|
|
2055
1141
|
*/
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
if (nodeIds.length < 2) return;
|
|
2059
|
-
if (this.config.lazyIntraEdges) {
|
|
2060
|
-
this.buildLazyIntraEdges(cluster);
|
|
2061
|
-
} else {
|
|
2062
|
-
this.buildEagerIntraEdges(cluster);
|
|
2063
|
-
}
|
|
2064
|
-
}
|
|
2065
|
-
/**
|
|
2066
|
-
* @zh 延迟构建 intra-edges(只用启发式距离)
|
|
2067
|
-
* @en Build lazy intra-edges (using heuristic distance only)
|
|
2068
|
-
*/
|
|
2069
|
-
buildLazyIntraEdges(cluster) {
|
|
2070
|
-
const nodeIds = cluster.nodeIds;
|
|
2071
|
-
for (let i = 0; i < nodeIds.length; i++) {
|
|
2072
|
-
for (let j = i + 1; j < nodeIds.length; j++) {
|
|
2073
|
-
const node1 = this.abstractNodes.get(nodeIds[i]);
|
|
2074
|
-
const node2 = this.abstractNodes.get(nodeIds[j]);
|
|
2075
|
-
const heuristicCost = this.heuristic(node1.position, node2.position);
|
|
2076
|
-
node1.edges.push({
|
|
2077
|
-
targetNodeId: node2.id,
|
|
2078
|
-
cost: heuristicCost,
|
|
2079
|
-
isInterEdge: false,
|
|
2080
|
-
innerPath: null
|
|
2081
|
-
// 标记为未计算
|
|
2082
|
-
});
|
|
2083
|
-
node2.edges.push({
|
|
2084
|
-
targetNodeId: node1.id,
|
|
2085
|
-
cost: heuristicCost,
|
|
2086
|
-
isInterEdge: false,
|
|
2087
|
-
innerPath: null
|
|
2088
|
-
});
|
|
2089
|
-
}
|
|
2090
|
-
}
|
|
2091
|
-
}
|
|
2092
|
-
/**
|
|
2093
|
-
* @zh 立即构建 intra-edges(计算真实路径)
|
|
2094
|
-
* @en Build eager intra-edges (compute actual paths)
|
|
2095
|
-
*/
|
|
2096
|
-
buildEagerIntraEdges(cluster) {
|
|
2097
|
-
const nodeIds = cluster.nodeIds;
|
|
2098
|
-
const subPathfinder = new AStarPathfinder(cluster.subMap);
|
|
2099
|
-
for (let i = 0; i < nodeIds.length; i++) {
|
|
2100
|
-
for (let j = i + 1; j < nodeIds.length; j++) {
|
|
2101
|
-
const node1 = this.abstractNodes.get(nodeIds[i]);
|
|
2102
|
-
const node2 = this.abstractNodes.get(nodeIds[j]);
|
|
2103
|
-
const local1 = cluster.subMap.globalToLocal(node1.position.x, node1.position.y);
|
|
2104
|
-
const local2 = cluster.subMap.globalToLocal(node2.position.x, node2.position.y);
|
|
2105
|
-
const result = subPathfinder.findPath(local1.x, local1.y, local2.x, local2.y);
|
|
2106
|
-
if (result.found && result.path.length > 0) {
|
|
2107
|
-
const globalPath = result.path.map((p) => {
|
|
2108
|
-
const global = cluster.subMap.localToGlobal(p.x, p.y);
|
|
2109
|
-
return global.y * this.mapWidth + global.x;
|
|
2110
|
-
});
|
|
2111
|
-
if (this.config.cacheInternalPaths) {
|
|
2112
|
-
cluster.setCache(node1.id, node2.id, result.cost, globalPath);
|
|
2113
|
-
cluster.setCache(node2.id, node1.id, result.cost, [
|
|
2114
|
-
...globalPath
|
|
2115
|
-
].reverse());
|
|
2116
|
-
}
|
|
2117
|
-
node1.edges.push({
|
|
2118
|
-
targetNodeId: node2.id,
|
|
2119
|
-
cost: result.cost,
|
|
2120
|
-
isInterEdge: false,
|
|
2121
|
-
innerPath: this.config.cacheInternalPaths ? globalPath : null
|
|
2122
|
-
});
|
|
2123
|
-
node2.edges.push({
|
|
2124
|
-
targetNodeId: node1.id,
|
|
2125
|
-
cost: result.cost,
|
|
2126
|
-
isInterEdge: false,
|
|
2127
|
-
innerPath: this.config.cacheInternalPaths ? [
|
|
2128
|
-
...globalPath
|
|
2129
|
-
].reverse() : null
|
|
2130
|
-
});
|
|
2131
|
-
}
|
|
2132
|
-
}
|
|
2133
|
-
}
|
|
2134
|
-
}
|
|
2135
|
-
/**
|
|
2136
|
-
* @zh 按需计算 intra-edge 的真实路径
|
|
2137
|
-
* @en Compute actual path for intra-edge on demand
|
|
2138
|
-
*/
|
|
2139
|
-
computeIntraEdgePath(fromNode, toNode, edge) {
|
|
2140
|
-
const cluster = this.clusters[fromNode.clusterId];
|
|
2141
|
-
if (!cluster) return null;
|
|
2142
|
-
const cachedPath = cluster.getCachedPath(fromNode.id, toNode.id);
|
|
2143
|
-
const cachedCost = cluster.getCachedDistance(fromNode.id, toNode.id);
|
|
2144
|
-
if (cachedPath && cachedCost !== void 0) {
|
|
2145
|
-
edge.cost = cachedCost;
|
|
2146
|
-
edge.innerPath = cachedPath;
|
|
2147
|
-
return {
|
|
2148
|
-
cost: cachedCost,
|
|
2149
|
-
path: cachedPath
|
|
2150
|
-
};
|
|
2151
|
-
}
|
|
2152
|
-
const subPathfinder = new AStarPathfinder(cluster.subMap);
|
|
2153
|
-
const local1 = cluster.subMap.globalToLocal(fromNode.position.x, fromNode.position.y);
|
|
2154
|
-
const local2 = cluster.subMap.globalToLocal(toNode.position.x, toNode.position.y);
|
|
2155
|
-
const result = subPathfinder.findPath(local1.x, local1.y, local2.x, local2.y);
|
|
2156
|
-
if (result.found && result.path.length > 0) {
|
|
2157
|
-
const globalPath = result.path.map((p) => {
|
|
2158
|
-
const global = cluster.subMap.localToGlobal(p.x, p.y);
|
|
2159
|
-
return global.y * this.mapWidth + global.x;
|
|
2160
|
-
});
|
|
2161
|
-
if (this.config.cacheInternalPaths) {
|
|
2162
|
-
cluster.setCache(fromNode.id, toNode.id, result.cost, globalPath);
|
|
2163
|
-
cluster.setCache(toNode.id, fromNode.id, result.cost, [
|
|
2164
|
-
...globalPath
|
|
2165
|
-
].reverse());
|
|
2166
|
-
}
|
|
2167
|
-
edge.cost = result.cost;
|
|
2168
|
-
edge.innerPath = globalPath;
|
|
2169
|
-
const reverseEdge = toNode.edges.find((e) => e.targetNodeId === fromNode.id);
|
|
2170
|
-
if (reverseEdge) {
|
|
2171
|
-
reverseEdge.cost = result.cost;
|
|
2172
|
-
reverseEdge.innerPath = [
|
|
2173
|
-
...globalPath
|
|
2174
|
-
].reverse();
|
|
2175
|
-
}
|
|
2176
|
-
return {
|
|
2177
|
-
cost: result.cost,
|
|
2178
|
-
path: globalPath
|
|
2179
|
-
};
|
|
2180
|
-
}
|
|
2181
|
-
return null;
|
|
1142
|
+
heuristic(a, b) {
|
|
1143
|
+
return this.options.heuristic(a, b);
|
|
2182
1144
|
}
|
|
2183
|
-
// =========================================================================
|
|
2184
|
-
// 搜索方法 | Search Methods
|
|
2185
|
-
// =========================================================================
|
|
2186
1145
|
/**
|
|
2187
|
-
* @zh
|
|
2188
|
-
* @en
|
|
1146
|
+
* @zh 计算移动代价
|
|
1147
|
+
* @en Calculate movement cost
|
|
2189
1148
|
*/
|
|
2190
|
-
|
|
2191
|
-
const
|
|
2192
|
-
const
|
|
2193
|
-
if (
|
|
2194
|
-
return
|
|
2195
|
-
}
|
|
2196
|
-
const clusterId = this.clusterGrid[cx]?.[cy];
|
|
2197
|
-
if (clusterId === null || clusterId === void 0) {
|
|
2198
|
-
return null;
|
|
2199
|
-
}
|
|
2200
|
-
return this.clusters[clusterId] || null;
|
|
2201
|
-
}
|
|
2202
|
-
/**
|
|
2203
|
-
* @zh 获取受影响的集群
|
|
2204
|
-
* @en Get affected clusters
|
|
2205
|
-
*/
|
|
2206
|
-
getAffectedClusters(minX, minY, maxX, maxY) {
|
|
2207
|
-
const affected = [];
|
|
2208
|
-
const clusterSize = this.config.clusterSize;
|
|
2209
|
-
const minCX = Math.floor(minX / clusterSize);
|
|
2210
|
-
const maxCX = Math.floor(maxX / clusterSize);
|
|
2211
|
-
const minCY = Math.floor(minY / clusterSize);
|
|
2212
|
-
const maxCY = Math.floor(maxY / clusterSize);
|
|
2213
|
-
for (let cy = minCY; cy <= maxCY; cy++) {
|
|
2214
|
-
for (let cx = minCX; cx <= maxCX; cx++) {
|
|
2215
|
-
if (cx >= 0 && cx < this.clustersX && cy >= 0 && cy < this.clustersY) {
|
|
2216
|
-
const clusterId = this.clusterGrid[cx]?.[cy];
|
|
2217
|
-
if (clusterId !== null && clusterId !== void 0) {
|
|
2218
|
-
affected.push(this.clusters[clusterId]);
|
|
2219
|
-
}
|
|
2220
|
-
}
|
|
2221
|
-
}
|
|
1149
|
+
getMovementCost(from, to) {
|
|
1150
|
+
const dx = Math.abs(from.position.x - to.position.x);
|
|
1151
|
+
const dy = Math.abs(from.position.y - to.position.y);
|
|
1152
|
+
if (dx !== 0 && dy !== 0) {
|
|
1153
|
+
return to.cost * this.options.diagonalCost;
|
|
2222
1154
|
}
|
|
2223
|
-
return
|
|
1155
|
+
return to.cost;
|
|
2224
1156
|
}
|
|
2225
1157
|
/**
|
|
2226
|
-
* @zh
|
|
2227
|
-
* @en
|
|
1158
|
+
* @zh 从二维数组加载地图
|
|
1159
|
+
* @en Load map from 2D array
|
|
1160
|
+
*
|
|
1161
|
+
* @param data - @zh 0=可通行,非0=不可通行 @en 0=walkable, non-0=blocked
|
|
2228
1162
|
*/
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
if (existing && existing.concreteNodeId === concreteId) {
|
|
2234
|
-
return existing;
|
|
2235
|
-
}
|
|
2236
|
-
}
|
|
2237
|
-
const tempNode = {
|
|
2238
|
-
id: this.nextNodeId++,
|
|
2239
|
-
position: {
|
|
2240
|
-
x,
|
|
2241
|
-
y
|
|
2242
|
-
},
|
|
2243
|
-
clusterId: cluster.id,
|
|
2244
|
-
concreteNodeId: concreteId,
|
|
2245
|
-
edges: []
|
|
2246
|
-
};
|
|
2247
|
-
this.abstractNodes.set(tempNode.id, tempNode);
|
|
2248
|
-
cluster.addNodeId(tempNode.id);
|
|
2249
|
-
const subPathfinder = new AStarPathfinder(cluster.subMap);
|
|
2250
|
-
const localPos = cluster.subMap.globalToLocal(x, y);
|
|
2251
|
-
for (const existingNodeId of cluster.nodeIds) {
|
|
2252
|
-
if (existingNodeId === tempNode.id) continue;
|
|
2253
|
-
const existingNode = this.abstractNodes.get(existingNodeId);
|
|
2254
|
-
if (!existingNode) continue;
|
|
2255
|
-
const targetLocalPos = cluster.subMap.globalToLocal(existingNode.position.x, existingNode.position.y);
|
|
2256
|
-
const result = subPathfinder.findPath(localPos.x, localPos.y, targetLocalPos.x, targetLocalPos.y);
|
|
2257
|
-
if (result.found && result.path.length > 0) {
|
|
2258
|
-
const globalPath = result.path.map((p) => {
|
|
2259
|
-
const global = cluster.subMap.localToGlobal(p.x, p.y);
|
|
2260
|
-
return global.y * this.mapWidth + global.x;
|
|
2261
|
-
});
|
|
2262
|
-
tempNode.edges.push({
|
|
2263
|
-
targetNodeId: existingNode.id,
|
|
2264
|
-
cost: result.cost,
|
|
2265
|
-
isInterEdge: false,
|
|
2266
|
-
innerPath: globalPath
|
|
2267
|
-
});
|
|
2268
|
-
existingNode.edges.push({
|
|
2269
|
-
targetNodeId: tempNode.id,
|
|
2270
|
-
cost: result.cost,
|
|
2271
|
-
isInterEdge: false,
|
|
2272
|
-
innerPath: [
|
|
2273
|
-
...globalPath
|
|
2274
|
-
].reverse()
|
|
2275
|
-
});
|
|
1163
|
+
loadFromArray(data) {
|
|
1164
|
+
for (let y = 0; y < Math.min(data.length, this.height); y++) {
|
|
1165
|
+
for (let x = 0; x < Math.min(data[y].length, this.width); x++) {
|
|
1166
|
+
this.nodes[y][x].walkable = data[y][x] === 0;
|
|
2276
1167
|
}
|
|
2277
1168
|
}
|
|
2278
|
-
return tempNode;
|
|
2279
1169
|
}
|
|
2280
1170
|
/**
|
|
2281
|
-
* @zh
|
|
2282
|
-
* @en
|
|
1171
|
+
* @zh 从字符串加载地图
|
|
1172
|
+
* @en Load map from string
|
|
1173
|
+
*
|
|
1174
|
+
* @param str - @zh 地图字符串,'.'=可通行,'#'=障碍 @en Map string, '.'=walkable, '#'=blocked
|
|
2283
1175
|
*/
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
const
|
|
2288
|
-
|
|
2289
|
-
|
|
1176
|
+
loadFromString(str) {
|
|
1177
|
+
const lines = str.trim().split("\n");
|
|
1178
|
+
for (let y = 0; y < Math.min(lines.length, this.height); y++) {
|
|
1179
|
+
const line = lines[y];
|
|
1180
|
+
for (let x = 0; x < Math.min(line.length, this.width); x++) {
|
|
1181
|
+
this.nodes[y][x].walkable = line[x] !== "#";
|
|
2290
1182
|
}
|
|
2291
1183
|
}
|
|
2292
|
-
cluster.removeNodeId(node.id);
|
|
2293
|
-
this.abstractNodes.delete(node.id);
|
|
2294
1184
|
}
|
|
2295
1185
|
/**
|
|
2296
|
-
* @zh
|
|
2297
|
-
* @en
|
|
1186
|
+
* @zh 导出为字符串
|
|
1187
|
+
* @en Export to string
|
|
2298
1188
|
*/
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
const startSearchNode = {
|
|
2305
|
-
node: startNode,
|
|
2306
|
-
g: 0,
|
|
2307
|
-
h,
|
|
2308
|
-
f: h,
|
|
2309
|
-
parent: null,
|
|
2310
|
-
closed: false,
|
|
2311
|
-
opened: true,
|
|
2312
|
-
heapIndex: -1
|
|
2313
|
-
};
|
|
2314
|
-
openList.push(startSearchNode);
|
|
2315
|
-
nodeMap.set(startNode.id, startSearchNode);
|
|
2316
|
-
let nodesSearched = 0;
|
|
2317
|
-
while (!openList.isEmpty && nodesSearched < opts.maxNodes) {
|
|
2318
|
-
const current = openList.pop();
|
|
2319
|
-
current.closed = true;
|
|
2320
|
-
nodesSearched++;
|
|
2321
|
-
if (current.node.id === endNode.id) {
|
|
2322
|
-
return this.reconstructPath(current);
|
|
2323
|
-
}
|
|
2324
|
-
for (const edge of current.node.edges) {
|
|
2325
|
-
let neighbor = nodeMap.get(edge.targetNodeId);
|
|
2326
|
-
if (!neighbor) {
|
|
2327
|
-
const neighborNode = this.abstractNodes.get(edge.targetNodeId);
|
|
2328
|
-
if (!neighborNode) continue;
|
|
2329
|
-
const nh = this.heuristic(neighborNode.position, endPosition) * opts.heuristicWeight;
|
|
2330
|
-
neighbor = {
|
|
2331
|
-
node: neighborNode,
|
|
2332
|
-
g: Infinity,
|
|
2333
|
-
h: nh,
|
|
2334
|
-
f: Infinity,
|
|
2335
|
-
parent: null,
|
|
2336
|
-
closed: false,
|
|
2337
|
-
opened: false,
|
|
2338
|
-
heapIndex: -1
|
|
2339
|
-
};
|
|
2340
|
-
nodeMap.set(edge.targetNodeId, neighbor);
|
|
2341
|
-
}
|
|
2342
|
-
if (neighbor.closed) continue;
|
|
2343
|
-
const tentativeG = current.g + edge.cost;
|
|
2344
|
-
if (!neighbor.opened) {
|
|
2345
|
-
neighbor.g = tentativeG;
|
|
2346
|
-
neighbor.f = tentativeG + neighbor.h;
|
|
2347
|
-
neighbor.parent = current;
|
|
2348
|
-
neighbor.opened = true;
|
|
2349
|
-
openList.push(neighbor);
|
|
2350
|
-
} else if (tentativeG < neighbor.g) {
|
|
2351
|
-
neighbor.g = tentativeG;
|
|
2352
|
-
neighbor.f = tentativeG + neighbor.h;
|
|
2353
|
-
neighbor.parent = current;
|
|
2354
|
-
openList.update(neighbor);
|
|
2355
|
-
}
|
|
1189
|
+
toString() {
|
|
1190
|
+
let result = "";
|
|
1191
|
+
for (let y = 0; y < this.height; y++) {
|
|
1192
|
+
for (let x = 0; x < this.width; x++) {
|
|
1193
|
+
result += this.nodes[y][x].walkable ? "." : "#";
|
|
2356
1194
|
}
|
|
1195
|
+
result += "\n";
|
|
2357
1196
|
}
|
|
2358
|
-
return
|
|
2359
|
-
}
|
|
2360
|
-
/**
|
|
2361
|
-
* @zh 重建抽象路径
|
|
2362
|
-
* @en Reconstruct abstract path
|
|
2363
|
-
*/
|
|
2364
|
-
reconstructPath(endNode) {
|
|
2365
|
-
const path = [];
|
|
2366
|
-
let current = endNode;
|
|
2367
|
-
while (current) {
|
|
2368
|
-
path.unshift(current.node);
|
|
2369
|
-
current = current.parent;
|
|
2370
|
-
}
|
|
2371
|
-
return path;
|
|
1197
|
+
return result;
|
|
2372
1198
|
}
|
|
2373
1199
|
/**
|
|
2374
|
-
* @zh
|
|
2375
|
-
* @en
|
|
1200
|
+
* @zh 重置所有节点为可通行
|
|
1201
|
+
* @en Reset all nodes to walkable
|
|
2376
1202
|
*/
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
let totalCost = 0;
|
|
2383
|
-
let nodesSearched = abstractPath.length;
|
|
2384
|
-
for (let i = 0; i < abstractPath.length - 1; i++) {
|
|
2385
|
-
const fromNode = abstractPath[i];
|
|
2386
|
-
const toNode = abstractPath[i + 1];
|
|
2387
|
-
const edge = fromNode.edges.find((e) => e.targetNodeId === toNode.id);
|
|
2388
|
-
if (!edge) {
|
|
2389
|
-
const segResult = this.findLocalPath(fromNode.position.x, fromNode.position.y, toNode.position.x, toNode.position.y, opts);
|
|
2390
|
-
if (segResult.found) {
|
|
2391
|
-
this.appendPath(fullPath, segResult.path);
|
|
2392
|
-
totalCost += segResult.cost;
|
|
2393
|
-
nodesSearched += segResult.nodesSearched;
|
|
2394
|
-
}
|
|
2395
|
-
} else if (edge.isInterEdge) {
|
|
2396
|
-
if (fullPath.length === 0 || fullPath[fullPath.length - 1].x !== fromNode.position.x || fullPath[fullPath.length - 1].y !== fromNode.position.y) {
|
|
2397
|
-
fullPath.push({
|
|
2398
|
-
x: fromNode.position.x,
|
|
2399
|
-
y: fromNode.position.y
|
|
2400
|
-
});
|
|
2401
|
-
}
|
|
2402
|
-
fullPath.push({
|
|
2403
|
-
x: toNode.position.x,
|
|
2404
|
-
y: toNode.position.y
|
|
2405
|
-
});
|
|
2406
|
-
totalCost += edge.cost;
|
|
2407
|
-
} else if (edge.innerPath && edge.innerPath.length > 0) {
|
|
2408
|
-
const concretePath = edge.innerPath.map((id) => ({
|
|
2409
|
-
x: id % this.mapWidth,
|
|
2410
|
-
y: Math.floor(id / this.mapWidth)
|
|
2411
|
-
}));
|
|
2412
|
-
this.appendPath(fullPath, concretePath);
|
|
2413
|
-
totalCost += edge.cost;
|
|
2414
|
-
} else {
|
|
2415
|
-
const computed = this.computeIntraEdgePath(fromNode, toNode, edge);
|
|
2416
|
-
if (computed && computed.path.length > 0) {
|
|
2417
|
-
const concretePath = computed.path.map((id) => ({
|
|
2418
|
-
x: id % this.mapWidth,
|
|
2419
|
-
y: Math.floor(id / this.mapWidth)
|
|
2420
|
-
}));
|
|
2421
|
-
this.appendPath(fullPath, concretePath);
|
|
2422
|
-
totalCost += computed.cost;
|
|
2423
|
-
} else {
|
|
2424
|
-
const segResult = this.findLocalPath(fromNode.position.x, fromNode.position.y, toNode.position.x, toNode.position.y, opts);
|
|
2425
|
-
if (segResult.found) {
|
|
2426
|
-
this.appendPath(fullPath, segResult.path);
|
|
2427
|
-
totalCost += segResult.cost;
|
|
2428
|
-
nodesSearched += segResult.nodesSearched;
|
|
2429
|
-
}
|
|
2430
|
-
}
|
|
2431
|
-
}
|
|
2432
|
-
}
|
|
2433
|
-
if (fullPath.length > 0 && (fullPath[0].x !== startX || fullPath[0].y !== startY)) {
|
|
2434
|
-
const firstPoint = fullPath[0];
|
|
2435
|
-
if (Math.abs(firstPoint.x - startX) <= 1 && Math.abs(firstPoint.y - startY) <= 1) {
|
|
2436
|
-
fullPath.unshift({
|
|
2437
|
-
x: startX,
|
|
2438
|
-
y: startY
|
|
2439
|
-
});
|
|
2440
|
-
} else {
|
|
2441
|
-
const segResult = this.findLocalPath(startX, startY, firstPoint.x, firstPoint.y, opts);
|
|
2442
|
-
if (segResult.found) {
|
|
2443
|
-
fullPath.splice(0, 0, ...segResult.path.slice(0, -1));
|
|
2444
|
-
totalCost += segResult.cost;
|
|
2445
|
-
}
|
|
2446
|
-
}
|
|
2447
|
-
}
|
|
2448
|
-
if (fullPath.length > 0) {
|
|
2449
|
-
const lastPoint = fullPath[fullPath.length - 1];
|
|
2450
|
-
if (lastPoint.x !== endX || lastPoint.y !== endY) {
|
|
2451
|
-
if (Math.abs(lastPoint.x - endX) <= 1 && Math.abs(lastPoint.y - endY) <= 1) {
|
|
2452
|
-
fullPath.push({
|
|
2453
|
-
x: endX,
|
|
2454
|
-
y: endY
|
|
2455
|
-
});
|
|
2456
|
-
} else {
|
|
2457
|
-
const segResult = this.findLocalPath(lastPoint.x, lastPoint.y, endX, endY, opts);
|
|
2458
|
-
if (segResult.found) {
|
|
2459
|
-
fullPath.push(...segResult.path.slice(1));
|
|
2460
|
-
totalCost += segResult.cost;
|
|
2461
|
-
}
|
|
2462
|
-
}
|
|
1203
|
+
reset() {
|
|
1204
|
+
for (let y = 0; y < this.height; y++) {
|
|
1205
|
+
for (let x = 0; x < this.width; x++) {
|
|
1206
|
+
this.nodes[y][x].walkable = true;
|
|
1207
|
+
this.nodes[y][x].cost = 1;
|
|
2463
1208
|
}
|
|
2464
1209
|
}
|
|
2465
|
-
return {
|
|
2466
|
-
found: fullPath.length > 0,
|
|
2467
|
-
path: fullPath,
|
|
2468
|
-
cost: totalCost,
|
|
2469
|
-
nodesSearched
|
|
2470
|
-
};
|
|
2471
1210
|
}
|
|
2472
1211
|
/**
|
|
2473
|
-
* @zh
|
|
2474
|
-
* @en
|
|
1212
|
+
* @zh 设置矩形区域的通行性
|
|
1213
|
+
* @en Set walkability for a rectangle region
|
|
2475
1214
|
*/
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
const last = fullPath[fullPath.length - 1];
|
|
2481
|
-
if (last.x === segment[0].x && last.y === segment[0].y) {
|
|
2482
|
-
startIdx = 1;
|
|
1215
|
+
setRectWalkable(x, y, width, height, walkable) {
|
|
1216
|
+
for (let dy = 0; dy < height; dy++) {
|
|
1217
|
+
for (let dx = 0; dx < width; dx++) {
|
|
1218
|
+
this.setWalkable(x + dx, y + dy, walkable);
|
|
2483
1219
|
}
|
|
2484
1220
|
}
|
|
2485
|
-
for (let i = startIdx; i < segment.length; i++) {
|
|
2486
|
-
fullPath.push({
|
|
2487
|
-
x: segment[i].x,
|
|
2488
|
-
y: segment[i].y
|
|
2489
|
-
});
|
|
2490
|
-
}
|
|
2491
|
-
}
|
|
2492
|
-
/**
|
|
2493
|
-
* @zh 局部寻路
|
|
2494
|
-
* @en Local pathfinding
|
|
2495
|
-
*/
|
|
2496
|
-
findLocalPath(startX, startY, endX, endY, opts) {
|
|
2497
|
-
return this.localPathfinder.findPath(startX, startY, endX, endY, opts);
|
|
2498
|
-
}
|
|
2499
|
-
/**
|
|
2500
|
-
* @zh 启发式函数(Octile 距离)
|
|
2501
|
-
* @en Heuristic function (Octile distance)
|
|
2502
|
-
*/
|
|
2503
|
-
heuristic(a, b) {
|
|
2504
|
-
const dx = Math.abs(a.x - b.x);
|
|
2505
|
-
const dy = Math.abs(a.y - b.y);
|
|
2506
|
-
return dx + dy + (Math.SQRT2 - 2) * Math.min(dx, dy);
|
|
2507
1221
|
}
|
|
2508
1222
|
};
|
|
2509
|
-
__name(
|
|
2510
|
-
var
|
|
2511
|
-
function
|
|
2512
|
-
return new
|
|
1223
|
+
__name(_GridMap, "GridMap");
|
|
1224
|
+
var GridMap = _GridMap;
|
|
1225
|
+
function createGridMap(width, height, options) {
|
|
1226
|
+
return new GridMap(width, height, options);
|
|
2513
1227
|
}
|
|
2514
|
-
__name(
|
|
1228
|
+
__name(createGridMap, "createGridMap");
|
|
2515
1229
|
|
|
2516
1230
|
// src/navmesh/NavMesh.ts
|
|
2517
|
-
var
|
|
2518
|
-
var NavMeshNode = (
|
|
1231
|
+
var _a3;
|
|
1232
|
+
var NavMeshNode = (_a3 = class {
|
|
2519
1233
|
constructor(polygon) {
|
|
2520
1234
|
__publicField(this, "id");
|
|
2521
1235
|
__publicField(this, "position");
|
|
@@ -2528,12 +1242,17 @@ var NavMeshNode = (_a5 = class {
|
|
|
2528
1242
|
this.walkable = true;
|
|
2529
1243
|
this.polygon = polygon;
|
|
2530
1244
|
}
|
|
2531
|
-
}, __name(
|
|
1245
|
+
}, __name(_a3, "NavMeshNode"), _a3);
|
|
2532
1246
|
var _NavMesh = class _NavMesh {
|
|
2533
1247
|
constructor() {
|
|
2534
1248
|
__publicField(this, "polygons", /* @__PURE__ */ new Map());
|
|
2535
1249
|
__publicField(this, "nodes", /* @__PURE__ */ new Map());
|
|
2536
1250
|
__publicField(this, "nextId", 0);
|
|
1251
|
+
// @zh 动态障碍物支持
|
|
1252
|
+
// @en Dynamic obstacle support
|
|
1253
|
+
__publicField(this, "obstacles", /* @__PURE__ */ new Map());
|
|
1254
|
+
__publicField(this, "nextObstacleId", 0);
|
|
1255
|
+
__publicField(this, "disabledPolygons", /* @__PURE__ */ new Set());
|
|
2537
1256
|
}
|
|
2538
1257
|
/**
|
|
2539
1258
|
* @zh 添加导航多边形
|
|
@@ -2741,7 +1460,7 @@ var _NavMesh = class _NavMesh {
|
|
|
2741
1460
|
}
|
|
2742
1461
|
const start = createPoint(startX, startY);
|
|
2743
1462
|
const end = createPoint(endX, endY);
|
|
2744
|
-
const pointPath = this.funnelPath(start, end, polygonPath.polygons);
|
|
1463
|
+
const pointPath = this.funnelPath(start, end, polygonPath.polygons, opts.agentRadius);
|
|
2745
1464
|
return {
|
|
2746
1465
|
found: true,
|
|
2747
1466
|
path: pointPath,
|
|
@@ -2752,8 +1471,13 @@ var _NavMesh = class _NavMesh {
|
|
|
2752
1471
|
/**
|
|
2753
1472
|
* @zh 在多边形图上寻路
|
|
2754
1473
|
* @en Find path on polygon graph
|
|
1474
|
+
*
|
|
1475
|
+
* @param start - @zh 起始多边形 @en Start polygon
|
|
1476
|
+
* @param end - @zh 目标多边形 @en End polygon
|
|
1477
|
+
* @param opts - @zh 寻路选项 @en Pathfinding options
|
|
1478
|
+
* @param checkObstacles - @zh 是否检查障碍物 @en Whether to check obstacles
|
|
2755
1479
|
*/
|
|
2756
|
-
findPolygonPath(start, end, opts) {
|
|
1480
|
+
findPolygonPath(start, end, opts, checkObstacles = false) {
|
|
2757
1481
|
const openList = new BinaryHeap((a, b) => a.f - b.f);
|
|
2758
1482
|
const closed = /* @__PURE__ */ new Set();
|
|
2759
1483
|
const states = /* @__PURE__ */ new Map();
|
|
@@ -2787,6 +1511,9 @@ var _NavMesh = class _NavMesh {
|
|
|
2787
1511
|
if (closed.has(neighborId)) {
|
|
2788
1512
|
continue;
|
|
2789
1513
|
}
|
|
1514
|
+
if (checkObstacles && this.isPolygonBlocked(neighborId)) {
|
|
1515
|
+
continue;
|
|
1516
|
+
}
|
|
2790
1517
|
const neighborPolygon = this.polygons.get(neighborId);
|
|
2791
1518
|
if (!neighborPolygon) {
|
|
2792
1519
|
continue;
|
|
@@ -2817,10 +1544,15 @@ var _NavMesh = class _NavMesh {
|
|
|
2817
1544
|
};
|
|
2818
1545
|
}
|
|
2819
1546
|
/**
|
|
2820
|
-
* @zh
|
|
2821
|
-
* @en Optimize path using funnel algorithm
|
|
1547
|
+
* @zh 使用漏斗算法优化路径(支持代理半径)
|
|
1548
|
+
* @en Optimize path using funnel algorithm (supports agent radius)
|
|
1549
|
+
*
|
|
1550
|
+
* @param start - @zh 起点 @en Start point
|
|
1551
|
+
* @param end - @zh 终点 @en End point
|
|
1552
|
+
* @param polygons - @zh 多边形路径 @en Polygon path
|
|
1553
|
+
* @param agentRadius - @zh 代理半径 @en Agent radius
|
|
2822
1554
|
*/
|
|
2823
|
-
funnelPath(start, end, polygons) {
|
|
1555
|
+
funnelPath(start, end, polygons, agentRadius = 0) {
|
|
2824
1556
|
if (polygons.length <= 1) {
|
|
2825
1557
|
return [
|
|
2826
1558
|
start,
|
|
@@ -2831,7 +1563,22 @@ var _NavMesh = class _NavMesh {
|
|
|
2831
1563
|
for (let i = 0; i < polygons.length - 1; i++) {
|
|
2832
1564
|
const portal = polygons[i].portals.get(polygons[i + 1].id);
|
|
2833
1565
|
if (portal) {
|
|
2834
|
-
|
|
1566
|
+
if (agentRadius > 0) {
|
|
1567
|
+
const shrunk = this.shrinkPortal(portal.left, portal.right, agentRadius);
|
|
1568
|
+
portals.push({
|
|
1569
|
+
left: shrunk.left,
|
|
1570
|
+
right: shrunk.right,
|
|
1571
|
+
originalLeft: portal.left,
|
|
1572
|
+
originalRight: portal.right
|
|
1573
|
+
});
|
|
1574
|
+
} else {
|
|
1575
|
+
portals.push({
|
|
1576
|
+
left: portal.left,
|
|
1577
|
+
right: portal.right,
|
|
1578
|
+
originalLeft: portal.left,
|
|
1579
|
+
originalRight: portal.right
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
2835
1582
|
}
|
|
2836
1583
|
}
|
|
2837
1584
|
if (portals.length === 0) {
|
|
@@ -2844,35 +1591,50 @@ var _NavMesh = class _NavMesh {
|
|
|
2844
1591
|
start
|
|
2845
1592
|
];
|
|
2846
1593
|
let apex = start;
|
|
1594
|
+
let apexOriginal = start;
|
|
2847
1595
|
let leftIndex = 0;
|
|
2848
1596
|
let rightIndex = 0;
|
|
2849
1597
|
let left = portals[0].left;
|
|
2850
1598
|
let right = portals[0].right;
|
|
1599
|
+
let leftOriginal = portals[0].originalLeft;
|
|
1600
|
+
let rightOriginal = portals[0].originalRight;
|
|
2851
1601
|
for (let i = 1; i <= portals.length; i++) {
|
|
2852
1602
|
const nextLeft = i < portals.length ? portals[i].left : end;
|
|
2853
1603
|
const nextRight = i < portals.length ? portals[i].right : end;
|
|
2854
1604
|
if (this.triArea2(apex, right, nextRight) <= 0) {
|
|
2855
|
-
if (apex
|
|
1605
|
+
if (this.pointsEqual(apex, right) || this.triArea2(apex, left, nextRight) > 0) {
|
|
2856
1606
|
right = nextRight;
|
|
2857
1607
|
rightIndex = i;
|
|
1608
|
+
if (i < portals.length) {
|
|
1609
|
+
rightOriginal = portals[i].originalRight;
|
|
1610
|
+
}
|
|
2858
1611
|
} else {
|
|
2859
|
-
|
|
1612
|
+
const turnPoint = agentRadius > 0 ? this.offsetTurningPoint(apexOriginal, leftOriginal, left, agentRadius, "left") : left;
|
|
1613
|
+
path.push(turnPoint);
|
|
2860
1614
|
apex = left;
|
|
1615
|
+
apexOriginal = leftOriginal;
|
|
2861
1616
|
leftIndex = rightIndex = leftIndex;
|
|
2862
1617
|
left = right = apex;
|
|
1618
|
+
leftOriginal = rightOriginal = apexOriginal;
|
|
2863
1619
|
i = leftIndex;
|
|
2864
1620
|
continue;
|
|
2865
1621
|
}
|
|
2866
1622
|
}
|
|
2867
1623
|
if (this.triArea2(apex, left, nextLeft) >= 0) {
|
|
2868
|
-
if (apex
|
|
1624
|
+
if (this.pointsEqual(apex, left) || this.triArea2(apex, right, nextLeft) < 0) {
|
|
2869
1625
|
left = nextLeft;
|
|
2870
1626
|
leftIndex = i;
|
|
1627
|
+
if (i < portals.length) {
|
|
1628
|
+
leftOriginal = portals[i].originalLeft;
|
|
1629
|
+
}
|
|
2871
1630
|
} else {
|
|
2872
|
-
|
|
1631
|
+
const turnPoint = agentRadius > 0 ? this.offsetTurningPoint(apexOriginal, rightOriginal, right, agentRadius, "right") : right;
|
|
1632
|
+
path.push(turnPoint);
|
|
2873
1633
|
apex = right;
|
|
1634
|
+
apexOriginal = rightOriginal;
|
|
2874
1635
|
leftIndex = rightIndex = rightIndex;
|
|
2875
1636
|
left = right = apex;
|
|
1637
|
+
leftOriginal = rightOriginal = apexOriginal;
|
|
2876
1638
|
i = rightIndex;
|
|
2877
1639
|
continue;
|
|
2878
1640
|
}
|
|
@@ -2881,6 +1643,63 @@ var _NavMesh = class _NavMesh {
|
|
|
2881
1643
|
path.push(end);
|
|
2882
1644
|
return path;
|
|
2883
1645
|
}
|
|
1646
|
+
/**
|
|
1647
|
+
* @zh 收缩 portal(将两端点向内移动 agentRadius)
|
|
1648
|
+
* @en Shrink portal (move endpoints inward by agentRadius)
|
|
1649
|
+
*/
|
|
1650
|
+
shrinkPortal(left, right, radius) {
|
|
1651
|
+
const dx = right.x - left.x;
|
|
1652
|
+
const dy = right.y - left.y;
|
|
1653
|
+
const len = Math.sqrt(dx * dx + dy * dy);
|
|
1654
|
+
if (len <= radius * 2) {
|
|
1655
|
+
const cx = (left.x + right.x) / 2;
|
|
1656
|
+
const cy = (left.y + right.y) / 2;
|
|
1657
|
+
return {
|
|
1658
|
+
left: createPoint(cx, cy),
|
|
1659
|
+
right: createPoint(cx, cy)
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
const nx = dx / len;
|
|
1663
|
+
const ny = dy / len;
|
|
1664
|
+
return {
|
|
1665
|
+
left: createPoint(left.x + nx * radius, left.y + ny * radius),
|
|
1666
|
+
right: createPoint(right.x - nx * radius, right.y - ny * radius)
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1669
|
+
/**
|
|
1670
|
+
* @zh 偏移拐点以保持与角落的距离
|
|
1671
|
+
* @en Offset turning point to maintain distance from corner
|
|
1672
|
+
*
|
|
1673
|
+
* @param prevApex - @zh 上一个顶点 @en Previous apex
|
|
1674
|
+
* @param cornerOriginal - @zh 原始角落位置 @en Original corner position
|
|
1675
|
+
* @param cornerShrunk - @zh 收缩后的角落位置 @en Shrunk corner position
|
|
1676
|
+
* @param radius - @zh 代理半径 @en Agent radius
|
|
1677
|
+
* @param side - @zh 转向侧 ('left' 或 'right') @en Turn side ('left' or 'right')
|
|
1678
|
+
*/
|
|
1679
|
+
offsetTurningPoint(prevApex, cornerOriginal, cornerShrunk, radius, side) {
|
|
1680
|
+
const dx = cornerOriginal.x - prevApex.x;
|
|
1681
|
+
const dy = cornerOriginal.y - prevApex.y;
|
|
1682
|
+
const len = Math.sqrt(dx * dx + dy * dy);
|
|
1683
|
+
if (len < 1e-4) {
|
|
1684
|
+
return cornerShrunk;
|
|
1685
|
+
}
|
|
1686
|
+
let perpX, perpY;
|
|
1687
|
+
if (side === "left") {
|
|
1688
|
+
perpX = dy / len;
|
|
1689
|
+
perpY = -dx / len;
|
|
1690
|
+
} else {
|
|
1691
|
+
perpX = -dy / len;
|
|
1692
|
+
perpY = dx / len;
|
|
1693
|
+
}
|
|
1694
|
+
return createPoint(cornerShrunk.x + perpX * radius, cornerShrunk.y + perpY * radius);
|
|
1695
|
+
}
|
|
1696
|
+
/**
|
|
1697
|
+
* @zh 检查两点是否相等
|
|
1698
|
+
* @en Check if two points are equal
|
|
1699
|
+
*/
|
|
1700
|
+
pointsEqual(a, b) {
|
|
1701
|
+
return Math.abs(a.x - b.x) < 1e-4 && Math.abs(a.y - b.y) < 1e-4;
|
|
1702
|
+
}
|
|
2884
1703
|
/**
|
|
2885
1704
|
* @zh 计算三角形面积的两倍(用于判断点的相对位置)
|
|
2886
1705
|
* @en Calculate twice the triangle area (for point relative position)
|
|
@@ -2899,6 +1718,416 @@ var _NavMesh = class _NavMesh {
|
|
|
2899
1718
|
}
|
|
2900
1719
|
return length;
|
|
2901
1720
|
}
|
|
1721
|
+
// =========================================================================
|
|
1722
|
+
// 动态障碍物管理 | Dynamic Obstacle Management
|
|
1723
|
+
// =========================================================================
|
|
1724
|
+
/**
|
|
1725
|
+
* @zh 添加圆形障碍物
|
|
1726
|
+
* @en Add circular obstacle
|
|
1727
|
+
*
|
|
1728
|
+
* @param x - @zh 中心 X @en Center X
|
|
1729
|
+
* @param y - @zh 中心 Y @en Center Y
|
|
1730
|
+
* @param radius - @zh 半径 @en Radius
|
|
1731
|
+
* @returns @zh 障碍物 ID @en Obstacle ID
|
|
1732
|
+
*/
|
|
1733
|
+
addCircleObstacle(x, y, radius) {
|
|
1734
|
+
const id = this.nextObstacleId++;
|
|
1735
|
+
this.obstacles.set(id, {
|
|
1736
|
+
id,
|
|
1737
|
+
type: "circle",
|
|
1738
|
+
enabled: true,
|
|
1739
|
+
position: createPoint(x, y),
|
|
1740
|
+
radius
|
|
1741
|
+
});
|
|
1742
|
+
return id;
|
|
1743
|
+
}
|
|
1744
|
+
/**
|
|
1745
|
+
* @zh 添加矩形障碍物
|
|
1746
|
+
* @en Add rectangular obstacle
|
|
1747
|
+
*
|
|
1748
|
+
* @param x - @zh 中心 X @en Center X
|
|
1749
|
+
* @param y - @zh 中心 Y @en Center Y
|
|
1750
|
+
* @param halfWidth - @zh 半宽 @en Half width
|
|
1751
|
+
* @param halfHeight - @zh 半高 @en Half height
|
|
1752
|
+
* @returns @zh 障碍物 ID @en Obstacle ID
|
|
1753
|
+
*/
|
|
1754
|
+
addRectObstacle(x, y, halfWidth, halfHeight) {
|
|
1755
|
+
const id = this.nextObstacleId++;
|
|
1756
|
+
this.obstacles.set(id, {
|
|
1757
|
+
id,
|
|
1758
|
+
type: "rect",
|
|
1759
|
+
enabled: true,
|
|
1760
|
+
position: createPoint(x, y),
|
|
1761
|
+
halfWidth,
|
|
1762
|
+
halfHeight
|
|
1763
|
+
});
|
|
1764
|
+
return id;
|
|
1765
|
+
}
|
|
1766
|
+
/**
|
|
1767
|
+
* @zh 添加多边形障碍物
|
|
1768
|
+
* @en Add polygon obstacle
|
|
1769
|
+
*
|
|
1770
|
+
* @param vertices - @zh 顶点列表 @en Vertex list
|
|
1771
|
+
* @returns @zh 障碍物 ID @en Obstacle ID
|
|
1772
|
+
*/
|
|
1773
|
+
addPolygonObstacle(vertices) {
|
|
1774
|
+
const id = this.nextObstacleId++;
|
|
1775
|
+
const center = this.calculateCenter(vertices);
|
|
1776
|
+
this.obstacles.set(id, {
|
|
1777
|
+
id,
|
|
1778
|
+
type: "polygon",
|
|
1779
|
+
enabled: true,
|
|
1780
|
+
position: center,
|
|
1781
|
+
vertices
|
|
1782
|
+
});
|
|
1783
|
+
return id;
|
|
1784
|
+
}
|
|
1785
|
+
/**
|
|
1786
|
+
* @zh 移除障碍物
|
|
1787
|
+
* @en Remove obstacle
|
|
1788
|
+
*/
|
|
1789
|
+
removeObstacle(obstacleId) {
|
|
1790
|
+
return this.obstacles.delete(obstacleId);
|
|
1791
|
+
}
|
|
1792
|
+
/**
|
|
1793
|
+
* @zh 启用/禁用障碍物
|
|
1794
|
+
* @en Enable/disable obstacle
|
|
1795
|
+
*/
|
|
1796
|
+
setObstacleEnabled(obstacleId, enabled) {
|
|
1797
|
+
const obstacle = this.obstacles.get(obstacleId);
|
|
1798
|
+
if (obstacle) {
|
|
1799
|
+
obstacle.enabled = enabled;
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
/**
|
|
1803
|
+
* @zh 更新障碍物位置
|
|
1804
|
+
* @en Update obstacle position
|
|
1805
|
+
*/
|
|
1806
|
+
updateObstaclePosition(obstacleId, x, y) {
|
|
1807
|
+
const obstacle = this.obstacles.get(obstacleId);
|
|
1808
|
+
if (obstacle) {
|
|
1809
|
+
obstacle.position = createPoint(x, y);
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
/**
|
|
1813
|
+
* @zh 获取所有障碍物
|
|
1814
|
+
* @en Get all obstacles
|
|
1815
|
+
*/
|
|
1816
|
+
getObstacles() {
|
|
1817
|
+
return Array.from(this.obstacles.values());
|
|
1818
|
+
}
|
|
1819
|
+
/**
|
|
1820
|
+
* @zh 获取启用的障碍物
|
|
1821
|
+
* @en Get enabled obstacles
|
|
1822
|
+
*/
|
|
1823
|
+
getEnabledObstacles() {
|
|
1824
|
+
return Array.from(this.obstacles.values()).filter((o) => o.enabled);
|
|
1825
|
+
}
|
|
1826
|
+
/**
|
|
1827
|
+
* @zh 清除所有障碍物
|
|
1828
|
+
* @en Clear all obstacles
|
|
1829
|
+
*/
|
|
1830
|
+
clearObstacles() {
|
|
1831
|
+
this.obstacles.clear();
|
|
1832
|
+
this.nextObstacleId = 0;
|
|
1833
|
+
}
|
|
1834
|
+
// =========================================================================
|
|
1835
|
+
// 多边形禁用管理 | Polygon Disable Management
|
|
1836
|
+
// =========================================================================
|
|
1837
|
+
/**
|
|
1838
|
+
* @zh 禁用多边形
|
|
1839
|
+
* @en Disable polygon
|
|
1840
|
+
*/
|
|
1841
|
+
disablePolygon(polygonId) {
|
|
1842
|
+
this.disabledPolygons.add(polygonId);
|
|
1843
|
+
}
|
|
1844
|
+
/**
|
|
1845
|
+
* @zh 启用多边形
|
|
1846
|
+
* @en Enable polygon
|
|
1847
|
+
*/
|
|
1848
|
+
enablePolygon(polygonId) {
|
|
1849
|
+
this.disabledPolygons.delete(polygonId);
|
|
1850
|
+
}
|
|
1851
|
+
/**
|
|
1852
|
+
* @zh 检查多边形是否被禁用
|
|
1853
|
+
* @en Check if polygon is disabled
|
|
1854
|
+
*/
|
|
1855
|
+
isPolygonDisabled(polygonId) {
|
|
1856
|
+
return this.disabledPolygons.has(polygonId);
|
|
1857
|
+
}
|
|
1858
|
+
/**
|
|
1859
|
+
* @zh 禁用包含指定点的多边形
|
|
1860
|
+
* @en Disable polygon containing specified point
|
|
1861
|
+
*/
|
|
1862
|
+
disablePolygonAt(x, y) {
|
|
1863
|
+
const polygon = this.findPolygonAt(x, y);
|
|
1864
|
+
if (polygon) {
|
|
1865
|
+
this.disablePolygon(polygon.id);
|
|
1866
|
+
return polygon.id;
|
|
1867
|
+
}
|
|
1868
|
+
return null;
|
|
1869
|
+
}
|
|
1870
|
+
/**
|
|
1871
|
+
* @zh 清除所有禁用的多边形
|
|
1872
|
+
* @en Clear all disabled polygons
|
|
1873
|
+
*/
|
|
1874
|
+
clearDisabledPolygons() {
|
|
1875
|
+
this.disabledPolygons.clear();
|
|
1876
|
+
}
|
|
1877
|
+
/**
|
|
1878
|
+
* @zh 获取被禁用的多边形 ID 列表
|
|
1879
|
+
* @en Get list of disabled polygon IDs
|
|
1880
|
+
*/
|
|
1881
|
+
getDisabledPolygons() {
|
|
1882
|
+
return Array.from(this.disabledPolygons);
|
|
1883
|
+
}
|
|
1884
|
+
// =========================================================================
|
|
1885
|
+
// 障碍物碰撞检测 | Obstacle Collision Detection
|
|
1886
|
+
// =========================================================================
|
|
1887
|
+
/**
|
|
1888
|
+
* @zh 检查点是否在任何障碍物内
|
|
1889
|
+
* @en Check if point is inside any obstacle
|
|
1890
|
+
*/
|
|
1891
|
+
isPointInObstacle(x, y) {
|
|
1892
|
+
for (const obstacle of this.obstacles.values()) {
|
|
1893
|
+
if (!obstacle.enabled) continue;
|
|
1894
|
+
if (this.isPointInSingleObstacle(x, y, obstacle)) {
|
|
1895
|
+
return true;
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
return false;
|
|
1899
|
+
}
|
|
1900
|
+
/**
|
|
1901
|
+
* @zh 检查点是否在单个障碍物内
|
|
1902
|
+
* @en Check if point is inside single obstacle
|
|
1903
|
+
*/
|
|
1904
|
+
isPointInSingleObstacle(x, y, obstacle) {
|
|
1905
|
+
switch (obstacle.type) {
|
|
1906
|
+
case "circle": {
|
|
1907
|
+
const dx = x - obstacle.position.x;
|
|
1908
|
+
const dy = y - obstacle.position.y;
|
|
1909
|
+
return dx * dx + dy * dy <= (obstacle.radius ?? 0) ** 2;
|
|
1910
|
+
}
|
|
1911
|
+
case "rect": {
|
|
1912
|
+
const hw = obstacle.halfWidth ?? 0;
|
|
1913
|
+
const hh = obstacle.halfHeight ?? 0;
|
|
1914
|
+
return Math.abs(x - obstacle.position.x) <= hw && Math.abs(y - obstacle.position.y) <= hh;
|
|
1915
|
+
}
|
|
1916
|
+
case "polygon": {
|
|
1917
|
+
if (!obstacle.vertices) return false;
|
|
1918
|
+
return this.isPointInPolygon(x, y, obstacle.vertices);
|
|
1919
|
+
}
|
|
1920
|
+
default:
|
|
1921
|
+
return false;
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
/**
|
|
1925
|
+
* @zh 检查线段是否与任何障碍物相交
|
|
1926
|
+
* @en Check if line segment intersects any obstacle
|
|
1927
|
+
*/
|
|
1928
|
+
doesLineIntersectObstacle(x1, y1, x2, y2) {
|
|
1929
|
+
for (const obstacle of this.obstacles.values()) {
|
|
1930
|
+
if (!obstacle.enabled) continue;
|
|
1931
|
+
if (this.doesLineIntersectSingleObstacle(x1, y1, x2, y2, obstacle)) {
|
|
1932
|
+
return true;
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
return false;
|
|
1936
|
+
}
|
|
1937
|
+
/**
|
|
1938
|
+
* @zh 检查线段是否与单个障碍物相交
|
|
1939
|
+
* @en Check if line segment intersects single obstacle
|
|
1940
|
+
*/
|
|
1941
|
+
doesLineIntersectSingleObstacle(x1, y1, x2, y2, obstacle) {
|
|
1942
|
+
switch (obstacle.type) {
|
|
1943
|
+
case "circle": {
|
|
1944
|
+
return this.lineIntersectsCircle(x1, y1, x2, y2, obstacle.position.x, obstacle.position.y, obstacle.radius ?? 0);
|
|
1945
|
+
}
|
|
1946
|
+
case "rect": {
|
|
1947
|
+
const hw = obstacle.halfWidth ?? 0;
|
|
1948
|
+
const hh = obstacle.halfHeight ?? 0;
|
|
1949
|
+
const minX = obstacle.position.x - hw;
|
|
1950
|
+
const maxX = obstacle.position.x + hw;
|
|
1951
|
+
const minY = obstacle.position.y - hh;
|
|
1952
|
+
const maxY = obstacle.position.y + hh;
|
|
1953
|
+
return this.lineIntersectsRect(x1, y1, x2, y2, minX, minY, maxX, maxY);
|
|
1954
|
+
}
|
|
1955
|
+
case "polygon": {
|
|
1956
|
+
if (!obstacle.vertices) return false;
|
|
1957
|
+
return this.lineIntersectsPolygon(x1, y1, x2, y2, obstacle.vertices);
|
|
1958
|
+
}
|
|
1959
|
+
default:
|
|
1960
|
+
return false;
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
/**
|
|
1964
|
+
* @zh 线段与圆相交检测
|
|
1965
|
+
* @en Line segment circle intersection
|
|
1966
|
+
*/
|
|
1967
|
+
lineIntersectsCircle(x1, y1, x2, y2, cx, cy, r) {
|
|
1968
|
+
const dx = x2 - x1;
|
|
1969
|
+
const dy = y2 - y1;
|
|
1970
|
+
const fx = x1 - cx;
|
|
1971
|
+
const fy = y1 - cy;
|
|
1972
|
+
const a = dx * dx + dy * dy;
|
|
1973
|
+
const b = 2 * (fx * dx + fy * dy);
|
|
1974
|
+
const c = fx * fx + fy * fy - r * r;
|
|
1975
|
+
let discriminant = b * b - 4 * a * c;
|
|
1976
|
+
if (discriminant < 0) return false;
|
|
1977
|
+
discriminant = Math.sqrt(discriminant);
|
|
1978
|
+
const t1 = (-b - discriminant) / (2 * a);
|
|
1979
|
+
const t2 = (-b + discriminant) / (2 * a);
|
|
1980
|
+
return t1 >= 0 && t1 <= 1 || t2 >= 0 && t2 <= 1 || t1 < 0 && t2 > 1;
|
|
1981
|
+
}
|
|
1982
|
+
/**
|
|
1983
|
+
* @zh 线段与矩形相交检测
|
|
1984
|
+
* @en Line segment rectangle intersection
|
|
1985
|
+
*/
|
|
1986
|
+
lineIntersectsRect(x1, y1, x2, y2, minX, minY, maxX, maxY) {
|
|
1987
|
+
if (x1 >= minX && x1 <= maxX && y1 >= minY && y1 <= maxY || x2 >= minX && x2 <= maxX && y2 >= minY && y2 <= maxY) {
|
|
1988
|
+
return true;
|
|
1989
|
+
}
|
|
1990
|
+
return this.lineSegmentsIntersect(x1, y1, x2, y2, minX, minY, maxX, minY) || this.lineSegmentsIntersect(x1, y1, x2, y2, maxX, minY, maxX, maxY) || this.lineSegmentsIntersect(x1, y1, x2, y2, maxX, maxY, minX, maxY) || this.lineSegmentsIntersect(x1, y1, x2, y2, minX, maxY, minX, minY);
|
|
1991
|
+
}
|
|
1992
|
+
/**
|
|
1993
|
+
* @zh 线段与多边形相交检测
|
|
1994
|
+
* @en Line segment polygon intersection
|
|
1995
|
+
*/
|
|
1996
|
+
lineIntersectsPolygon(x1, y1, x2, y2, vertices) {
|
|
1997
|
+
if (this.isPointInPolygon(x1, y1, vertices) || this.isPointInPolygon(x2, y2, vertices)) {
|
|
1998
|
+
return true;
|
|
1999
|
+
}
|
|
2000
|
+
for (let i = 0; i < vertices.length; i++) {
|
|
2001
|
+
const j = (i + 1) % vertices.length;
|
|
2002
|
+
if (this.lineSegmentsIntersect(x1, y1, x2, y2, vertices[i].x, vertices[i].y, vertices[j].x, vertices[j].y)) {
|
|
2003
|
+
return true;
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
return false;
|
|
2007
|
+
}
|
|
2008
|
+
/**
|
|
2009
|
+
* @zh 两线段相交检测
|
|
2010
|
+
* @en Two line segments intersection
|
|
2011
|
+
*/
|
|
2012
|
+
lineSegmentsIntersect(x1, y1, x2, y2, x3, y3, x4, y4) {
|
|
2013
|
+
const d1 = this.direction(x3, y3, x4, y4, x1, y1);
|
|
2014
|
+
const d2 = this.direction(x3, y3, x4, y4, x2, y2);
|
|
2015
|
+
const d3 = this.direction(x1, y1, x2, y2, x3, y3);
|
|
2016
|
+
const d4 = this.direction(x1, y1, x2, y2, x4, y4);
|
|
2017
|
+
if ((d1 > 0 && d2 < 0 || d1 < 0 && d2 > 0) && (d3 > 0 && d4 < 0 || d3 < 0 && d4 > 0)) {
|
|
2018
|
+
return true;
|
|
2019
|
+
}
|
|
2020
|
+
const epsilon = 1e-4;
|
|
2021
|
+
if (Math.abs(d1) < epsilon && this.onSegment(x3, y3, x4, y4, x1, y1)) return true;
|
|
2022
|
+
if (Math.abs(d2) < epsilon && this.onSegment(x3, y3, x4, y4, x2, y2)) return true;
|
|
2023
|
+
if (Math.abs(d3) < epsilon && this.onSegment(x1, y1, x2, y2, x3, y3)) return true;
|
|
2024
|
+
if (Math.abs(d4) < epsilon && this.onSegment(x1, y1, x2, y2, x4, y4)) return true;
|
|
2025
|
+
return false;
|
|
2026
|
+
}
|
|
2027
|
+
direction(x1, y1, x2, y2, x3, y3) {
|
|
2028
|
+
return (x3 - x1) * (y2 - y1) - (y3 - y1) * (x2 - x1);
|
|
2029
|
+
}
|
|
2030
|
+
onSegment(x1, y1, x2, y2, x3, y3) {
|
|
2031
|
+
return Math.min(x1, x2) <= x3 && x3 <= Math.max(x1, x2) && Math.min(y1, y2) <= y3 && y3 <= Math.max(y1, y2);
|
|
2032
|
+
}
|
|
2033
|
+
// =========================================================================
|
|
2034
|
+
// 障碍物感知寻路 | Obstacle-Aware Pathfinding
|
|
2035
|
+
// =========================================================================
|
|
2036
|
+
/**
|
|
2037
|
+
* @zh 检查多边形是否被障碍物阻挡
|
|
2038
|
+
* @en Check if polygon is blocked by obstacle
|
|
2039
|
+
*
|
|
2040
|
+
* @zh 检查以下条件:
|
|
2041
|
+
* @en Checks the following conditions:
|
|
2042
|
+
* - @zh 多边形是否被禁用 @en Whether polygon is disabled
|
|
2043
|
+
* - @zh 多边形中心是否在障碍物内 @en Whether polygon center is inside obstacle
|
|
2044
|
+
* - @zh 多边形任意顶点是否在障碍物内 @en Whether any polygon vertex is inside obstacle
|
|
2045
|
+
* - @zh 多边形任意边是否与障碍物相交 @en Whether any polygon edge intersects obstacle
|
|
2046
|
+
*/
|
|
2047
|
+
isPolygonBlocked(polygonId) {
|
|
2048
|
+
if (this.disabledPolygons.has(polygonId)) {
|
|
2049
|
+
return true;
|
|
2050
|
+
}
|
|
2051
|
+
const polygon = this.polygons.get(polygonId);
|
|
2052
|
+
if (!polygon) return false;
|
|
2053
|
+
if (this.isPointInObstacle(polygon.center.x, polygon.center.y)) {
|
|
2054
|
+
return true;
|
|
2055
|
+
}
|
|
2056
|
+
for (const vertex of polygon.vertices) {
|
|
2057
|
+
if (this.isPointInObstacle(vertex.x, vertex.y)) {
|
|
2058
|
+
return true;
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
const vertices = polygon.vertices;
|
|
2062
|
+
for (let i = 0; i < vertices.length; i++) {
|
|
2063
|
+
const v1 = vertices[i];
|
|
2064
|
+
const v2 = vertices[(i + 1) % vertices.length];
|
|
2065
|
+
if (this.doesLineIntersectObstacle(v1.x, v1.y, v2.x, v2.y)) {
|
|
2066
|
+
return true;
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
return false;
|
|
2070
|
+
}
|
|
2071
|
+
/**
|
|
2072
|
+
* @zh 在导航网格上寻路(考虑障碍物)
|
|
2073
|
+
* @en Find path on navigation mesh (considering obstacles)
|
|
2074
|
+
*
|
|
2075
|
+
* @zh 此方法在规划阶段就考虑障碍物,自动绕过被阻挡的多边形
|
|
2076
|
+
* @en This method considers obstacles during planning, automatically avoiding blocked polygons
|
|
2077
|
+
*
|
|
2078
|
+
* @zh 与 findPath 不同,此方法会:
|
|
2079
|
+
* @en Unlike findPath, this method will:
|
|
2080
|
+
* - @zh 在 A* 搜索中跳过被障碍物阻挡的多边形
|
|
2081
|
+
* - @en Skip obstacle-blocked polygons during A* search
|
|
2082
|
+
* - @zh 验证起点和终点不在障碍物内
|
|
2083
|
+
* - @en Verify start and end points are not inside obstacles
|
|
2084
|
+
*/
|
|
2085
|
+
findPathWithObstacles(startX, startY, endX, endY, options) {
|
|
2086
|
+
const opts = {
|
|
2087
|
+
...DEFAULT_PATHFINDING_OPTIONS,
|
|
2088
|
+
...options
|
|
2089
|
+
};
|
|
2090
|
+
if (this.isPointInObstacle(startX, startY) || this.isPointInObstacle(endX, endY)) {
|
|
2091
|
+
return EMPTY_PATH_RESULT;
|
|
2092
|
+
}
|
|
2093
|
+
const startPolygon = this.findPolygonAt(startX, startY);
|
|
2094
|
+
const endPolygon = this.findPolygonAt(endX, endY);
|
|
2095
|
+
if (!startPolygon || !endPolygon) {
|
|
2096
|
+
return EMPTY_PATH_RESULT;
|
|
2097
|
+
}
|
|
2098
|
+
if (this.isPolygonBlocked(startPolygon.id) || this.isPolygonBlocked(endPolygon.id)) {
|
|
2099
|
+
return EMPTY_PATH_RESULT;
|
|
2100
|
+
}
|
|
2101
|
+
if (startPolygon.id === endPolygon.id) {
|
|
2102
|
+
const start2 = createPoint(startX, startY);
|
|
2103
|
+
const end2 = createPoint(endX, endY);
|
|
2104
|
+
if (this.doesLineIntersectObstacle(startX, startY, endX, endY)) {
|
|
2105
|
+
return EMPTY_PATH_RESULT;
|
|
2106
|
+
}
|
|
2107
|
+
return {
|
|
2108
|
+
found: true,
|
|
2109
|
+
path: [
|
|
2110
|
+
start2,
|
|
2111
|
+
end2
|
|
2112
|
+
],
|
|
2113
|
+
cost: euclideanDistance(start2, end2),
|
|
2114
|
+
nodesSearched: 1
|
|
2115
|
+
};
|
|
2116
|
+
}
|
|
2117
|
+
const polygonPath = this.findPolygonPath(startPolygon, endPolygon, opts, true);
|
|
2118
|
+
if (!polygonPath.found) {
|
|
2119
|
+
return EMPTY_PATH_RESULT;
|
|
2120
|
+
}
|
|
2121
|
+
const start = createPoint(startX, startY);
|
|
2122
|
+
const end = createPoint(endX, endY);
|
|
2123
|
+
const pointPath = this.funnelPath(start, end, polygonPath.polygons, opts.agentRadius);
|
|
2124
|
+
return {
|
|
2125
|
+
found: true,
|
|
2126
|
+
path: pointPath,
|
|
2127
|
+
cost: this.calculatePathLength(pointPath),
|
|
2128
|
+
nodesSearched: polygonPath.nodesSearched
|
|
2129
|
+
};
|
|
2130
|
+
}
|
|
2902
2131
|
/**
|
|
2903
2132
|
* @zh 清空导航网格
|
|
2904
2133
|
* @en Clear navigation mesh
|
|
@@ -2906,7 +2135,10 @@ var _NavMesh = class _NavMesh {
|
|
|
2906
2135
|
clear() {
|
|
2907
2136
|
this.polygons.clear();
|
|
2908
2137
|
this.nodes.clear();
|
|
2138
|
+
this.obstacles.clear();
|
|
2139
|
+
this.disabledPolygons.clear();
|
|
2909
2140
|
this.nextId = 0;
|
|
2141
|
+
this.nextObstacleId = 0;
|
|
2910
2142
|
}
|
|
2911
2143
|
/**
|
|
2912
2144
|
* @zh 获取所有多边形
|
|
@@ -2922,6 +2154,13 @@ var _NavMesh = class _NavMesh {
|
|
|
2922
2154
|
get polygonCount() {
|
|
2923
2155
|
return this.polygons.size;
|
|
2924
2156
|
}
|
|
2157
|
+
/**
|
|
2158
|
+
* @zh 获取障碍物数量
|
|
2159
|
+
* @en Get obstacle count
|
|
2160
|
+
*/
|
|
2161
|
+
get obstacleCount() {
|
|
2162
|
+
return this.obstacles.size;
|
|
2163
|
+
}
|
|
2925
2164
|
};
|
|
2926
2165
|
__name(_NavMesh, "NavMesh");
|
|
2927
2166
|
var NavMesh = _NavMesh;
|
|
@@ -2929,56 +2168,431 @@ function createNavMesh() {
|
|
|
2929
2168
|
return new NavMesh();
|
|
2930
2169
|
}
|
|
2931
2170
|
__name(createNavMesh, "createNavMesh");
|
|
2171
|
+
|
|
2172
|
+
// src/smoothing/PathSmoother.ts
|
|
2173
|
+
function bresenhamLineOfSight(x1, y1, x2, y2, map) {
|
|
2174
|
+
let ix1 = Math.floor(x1);
|
|
2175
|
+
let iy1 = Math.floor(y1);
|
|
2176
|
+
const ix2 = Math.floor(x2);
|
|
2177
|
+
const iy2 = Math.floor(y2);
|
|
2178
|
+
const dx = Math.abs(ix2 - ix1);
|
|
2179
|
+
const dy = Math.abs(iy2 - iy1);
|
|
2180
|
+
const sx = ix1 < ix2 ? 1 : -1;
|
|
2181
|
+
const sy = iy1 < iy2 ? 1 : -1;
|
|
2182
|
+
let err = dx - dy;
|
|
2183
|
+
while (true) {
|
|
2184
|
+
if (!map.isWalkable(ix1, iy1)) {
|
|
2185
|
+
return false;
|
|
2186
|
+
}
|
|
2187
|
+
if (ix1 === ix2 && iy1 === iy2) {
|
|
2188
|
+
break;
|
|
2189
|
+
}
|
|
2190
|
+
const e2 = 2 * err;
|
|
2191
|
+
if (e2 > -dy) {
|
|
2192
|
+
err -= dy;
|
|
2193
|
+
ix1 += sx;
|
|
2194
|
+
}
|
|
2195
|
+
if (e2 < dx) {
|
|
2196
|
+
err += dx;
|
|
2197
|
+
iy1 += sy;
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
return true;
|
|
2201
|
+
}
|
|
2202
|
+
__name(bresenhamLineOfSight, "bresenhamLineOfSight");
|
|
2203
|
+
function raycastLineOfSight(x1, y1, x2, y2, map, stepSize = 0.5) {
|
|
2204
|
+
const dx = x2 - x1;
|
|
2205
|
+
const dy = y2 - y1;
|
|
2206
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
2207
|
+
if (distance === 0) {
|
|
2208
|
+
return map.isWalkable(Math.floor(x1), Math.floor(y1));
|
|
2209
|
+
}
|
|
2210
|
+
const steps = Math.ceil(distance / stepSize);
|
|
2211
|
+
const stepX = dx / steps;
|
|
2212
|
+
const stepY = dy / steps;
|
|
2213
|
+
let x = x1;
|
|
2214
|
+
let y = y1;
|
|
2215
|
+
for (let i = 0; i <= steps; i++) {
|
|
2216
|
+
if (!map.isWalkable(Math.floor(x), Math.floor(y))) {
|
|
2217
|
+
return false;
|
|
2218
|
+
}
|
|
2219
|
+
x += stepX;
|
|
2220
|
+
y += stepY;
|
|
2221
|
+
}
|
|
2222
|
+
return true;
|
|
2223
|
+
}
|
|
2224
|
+
__name(raycastLineOfSight, "raycastLineOfSight");
|
|
2225
|
+
var _LineOfSightSmoother = class _LineOfSightSmoother {
|
|
2226
|
+
constructor(lineOfSight = bresenhamLineOfSight) {
|
|
2227
|
+
__publicField(this, "lineOfSight");
|
|
2228
|
+
this.lineOfSight = lineOfSight;
|
|
2229
|
+
}
|
|
2230
|
+
smooth(path, map) {
|
|
2231
|
+
if (path.length <= 2) {
|
|
2232
|
+
return [
|
|
2233
|
+
...path
|
|
2234
|
+
];
|
|
2235
|
+
}
|
|
2236
|
+
const result = [
|
|
2237
|
+
path[0]
|
|
2238
|
+
];
|
|
2239
|
+
let current = 0;
|
|
2240
|
+
while (current < path.length - 1) {
|
|
2241
|
+
let furthest = current + 1;
|
|
2242
|
+
for (let i = path.length - 1; i > current + 1; i--) {
|
|
2243
|
+
if (this.lineOfSight(path[current].x, path[current].y, path[i].x, path[i].y, map)) {
|
|
2244
|
+
furthest = i;
|
|
2245
|
+
break;
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
result.push(path[furthest]);
|
|
2249
|
+
current = furthest;
|
|
2250
|
+
}
|
|
2251
|
+
return result;
|
|
2252
|
+
}
|
|
2253
|
+
};
|
|
2254
|
+
__name(_LineOfSightSmoother, "LineOfSightSmoother");
|
|
2255
|
+
var LineOfSightSmoother = _LineOfSightSmoother;
|
|
2256
|
+
var _CatmullRomSmoother = class _CatmullRomSmoother {
|
|
2257
|
+
/**
|
|
2258
|
+
* @param segments - @zh 每段之间的插值点数 @en Number of interpolation points per segment
|
|
2259
|
+
* @param tension - @zh 张力 (0-1) @en Tension (0-1)
|
|
2260
|
+
*/
|
|
2261
|
+
constructor(segments = 5, tension = 0.5) {
|
|
2262
|
+
__publicField(this, "segments");
|
|
2263
|
+
__publicField(this, "tension");
|
|
2264
|
+
this.segments = segments;
|
|
2265
|
+
this.tension = tension;
|
|
2266
|
+
}
|
|
2267
|
+
smooth(path, _map) {
|
|
2268
|
+
if (path.length <= 2) {
|
|
2269
|
+
return [
|
|
2270
|
+
...path
|
|
2271
|
+
];
|
|
2272
|
+
}
|
|
2273
|
+
const result = [];
|
|
2274
|
+
const points = [
|
|
2275
|
+
path[0],
|
|
2276
|
+
...path,
|
|
2277
|
+
path[path.length - 1]
|
|
2278
|
+
];
|
|
2279
|
+
for (let i = 1; i < points.length - 2; i++) {
|
|
2280
|
+
const p0 = points[i - 1];
|
|
2281
|
+
const p1 = points[i];
|
|
2282
|
+
const p2 = points[i + 1];
|
|
2283
|
+
const p3 = points[i + 2];
|
|
2284
|
+
for (let j = 0; j < this.segments; j++) {
|
|
2285
|
+
const t = j / this.segments;
|
|
2286
|
+
const point = this.interpolate(p0, p1, p2, p3, t);
|
|
2287
|
+
result.push(point);
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
result.push(path[path.length - 1]);
|
|
2291
|
+
return result;
|
|
2292
|
+
}
|
|
2293
|
+
/**
|
|
2294
|
+
* @zh Catmull-Rom 插值
|
|
2295
|
+
* @en Catmull-Rom interpolation
|
|
2296
|
+
*/
|
|
2297
|
+
interpolate(p0, p1, p2, p3, t) {
|
|
2298
|
+
const t2 = t * t;
|
|
2299
|
+
const t3 = t2 * t;
|
|
2300
|
+
const tension = this.tension;
|
|
2301
|
+
const x = 0.5 * (2 * p1.x + (-p0.x + p2.x) * t * tension + (2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * t2 * tension + (-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * t3 * tension);
|
|
2302
|
+
const y = 0.5 * (2 * p1.y + (-p0.y + p2.y) * t * tension + (2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * t2 * tension + (-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * t3 * tension);
|
|
2303
|
+
return createPoint(x, y);
|
|
2304
|
+
}
|
|
2305
|
+
};
|
|
2306
|
+
__name(_CatmullRomSmoother, "CatmullRomSmoother");
|
|
2307
|
+
var CatmullRomSmoother = _CatmullRomSmoother;
|
|
2308
|
+
var _CombinedSmoother = class _CombinedSmoother {
|
|
2309
|
+
constructor(curveSegments = 5, tension = 0.5) {
|
|
2310
|
+
__publicField(this, "simplifier");
|
|
2311
|
+
__publicField(this, "curveSmoother");
|
|
2312
|
+
this.simplifier = new LineOfSightSmoother();
|
|
2313
|
+
this.curveSmoother = new CatmullRomSmoother(curveSegments, tension);
|
|
2314
|
+
}
|
|
2315
|
+
smooth(path, map) {
|
|
2316
|
+
const simplified = this.simplifier.smooth(path, map);
|
|
2317
|
+
return this.curveSmoother.smooth(simplified, map);
|
|
2318
|
+
}
|
|
2319
|
+
};
|
|
2320
|
+
__name(_CombinedSmoother, "CombinedSmoother");
|
|
2321
|
+
var CombinedSmoother = _CombinedSmoother;
|
|
2322
|
+
function createLineOfSightSmoother(lineOfSight) {
|
|
2323
|
+
return new LineOfSightSmoother(lineOfSight);
|
|
2324
|
+
}
|
|
2325
|
+
__name(createLineOfSightSmoother, "createLineOfSightSmoother");
|
|
2326
|
+
function createCatmullRomSmoother(segments, tension) {
|
|
2327
|
+
return new CatmullRomSmoother(segments, tension);
|
|
2328
|
+
}
|
|
2329
|
+
__name(createCatmullRomSmoother, "createCatmullRomSmoother");
|
|
2330
|
+
function createCombinedSmoother(curveSegments, tension) {
|
|
2331
|
+
return new CombinedSmoother(curveSegments, tension);
|
|
2332
|
+
}
|
|
2333
|
+
__name(createCombinedSmoother, "createCombinedSmoother");
|
|
2334
|
+
|
|
2335
|
+
// src/smoothing/RadiusAwarePathSmoother.ts
|
|
2336
|
+
var DEFAULT_CONFIG = {
|
|
2337
|
+
safetyMargin: 0.1,
|
|
2338
|
+
sampleDirections: 8,
|
|
2339
|
+
maxOffsetAttempts: 8,
|
|
2340
|
+
processCorners: true
|
|
2341
|
+
};
|
|
2342
|
+
var _RadiusAwarePathSmoother = class _RadiusAwarePathSmoother {
|
|
2343
|
+
constructor(config) {
|
|
2344
|
+
__publicField(this, "config");
|
|
2345
|
+
__publicField(this, "sampleAngles");
|
|
2346
|
+
this.config = {
|
|
2347
|
+
...DEFAULT_CONFIG,
|
|
2348
|
+
...config
|
|
2349
|
+
};
|
|
2350
|
+
this.sampleAngles = [];
|
|
2351
|
+
const step = Math.PI * 2 / this.config.sampleDirections;
|
|
2352
|
+
for (let i = 0; i < this.config.sampleDirections; i++) {
|
|
2353
|
+
this.sampleAngles.push(i * step);
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
/**
|
|
2357
|
+
* @zh 平滑路径,确保与障碍物保持安全距离
|
|
2358
|
+
* @en Smooth path, ensuring safe distance from obstacles
|
|
2359
|
+
*
|
|
2360
|
+
* @param path - @zh 原始路径 @en Original path
|
|
2361
|
+
* @param map - @zh 地图 @en Map
|
|
2362
|
+
* @returns @zh 处理后的安全路径 @en Processed safe path
|
|
2363
|
+
*/
|
|
2364
|
+
smooth(path, map) {
|
|
2365
|
+
if (path.length <= 1) {
|
|
2366
|
+
return [
|
|
2367
|
+
...path
|
|
2368
|
+
];
|
|
2369
|
+
}
|
|
2370
|
+
const result = [];
|
|
2371
|
+
const clearance = this.config.agentRadius + this.config.safetyMargin;
|
|
2372
|
+
for (let i = 0; i < path.length; i++) {
|
|
2373
|
+
const point = path[i];
|
|
2374
|
+
const isCorner = this.config.processCorners && i > 0 && i < path.length - 1;
|
|
2375
|
+
let safePoint;
|
|
2376
|
+
if (isCorner) {
|
|
2377
|
+
const prev = path[i - 1];
|
|
2378
|
+
const next = path[i + 1];
|
|
2379
|
+
safePoint = this.offsetCornerPoint(point, prev, next, clearance, map);
|
|
2380
|
+
} else {
|
|
2381
|
+
safePoint = this.offsetPointFromObstacles(point, clearance, map);
|
|
2382
|
+
}
|
|
2383
|
+
result.push(safePoint);
|
|
2384
|
+
}
|
|
2385
|
+
return result;
|
|
2386
|
+
}
|
|
2387
|
+
/**
|
|
2388
|
+
* @zh 将点从障碍物偏移
|
|
2389
|
+
* @en Offset point away from obstacles
|
|
2390
|
+
*/
|
|
2391
|
+
offsetPointFromObstacles(point, clearance, map) {
|
|
2392
|
+
const obstacleDirections = this.detectNearbyObstacles(point, clearance, map);
|
|
2393
|
+
if (obstacleDirections.length === 0) {
|
|
2394
|
+
return point;
|
|
2395
|
+
}
|
|
2396
|
+
let avgDirX = 0;
|
|
2397
|
+
let avgDirY = 0;
|
|
2398
|
+
for (const dir of obstacleDirections) {
|
|
2399
|
+
avgDirX += dir.x;
|
|
2400
|
+
avgDirY += dir.y;
|
|
2401
|
+
}
|
|
2402
|
+
const len = Math.sqrt(avgDirX * avgDirX + avgDirY * avgDirY);
|
|
2403
|
+
if (len < 1e-4) {
|
|
2404
|
+
return point;
|
|
2405
|
+
}
|
|
2406
|
+
const offsetDirX = -avgDirX / len;
|
|
2407
|
+
const offsetDirY = -avgDirY / len;
|
|
2408
|
+
for (let attempt = 1; attempt <= this.config.maxOffsetAttempts; attempt++) {
|
|
2409
|
+
const offsetDist = clearance * attempt / this.config.maxOffsetAttempts;
|
|
2410
|
+
const newX = point.x + offsetDirX * offsetDist;
|
|
2411
|
+
const newY = point.y + offsetDirY * offsetDist;
|
|
2412
|
+
if (map.isWalkable(Math.floor(newX), Math.floor(newY))) {
|
|
2413
|
+
const newObstacles = this.detectNearbyObstacles(createPoint(newX, newY), clearance, map);
|
|
2414
|
+
if (newObstacles.length === 0) {
|
|
2415
|
+
return createPoint(newX, newY);
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
return point;
|
|
2420
|
+
}
|
|
2421
|
+
/**
|
|
2422
|
+
* @zh 偏移拐点(角落)
|
|
2423
|
+
* @en Offset corner point
|
|
2424
|
+
*/
|
|
2425
|
+
offsetCornerPoint(corner, prev, next, clearance, map) {
|
|
2426
|
+
const inDirX = corner.x - prev.x;
|
|
2427
|
+
const inDirY = corner.y - prev.y;
|
|
2428
|
+
const inLen = Math.sqrt(inDirX * inDirX + inDirY * inDirY);
|
|
2429
|
+
const outDirX = next.x - corner.x;
|
|
2430
|
+
const outDirY = next.y - corner.y;
|
|
2431
|
+
const outLen = Math.sqrt(outDirX * outDirX + outDirY * outDirY);
|
|
2432
|
+
if (inLen < 1e-4 || outLen < 1e-4) {
|
|
2433
|
+
return this.offsetPointFromObstacles(corner, clearance, map);
|
|
2434
|
+
}
|
|
2435
|
+
const inNormX = inDirX / inLen;
|
|
2436
|
+
const inNormY = inDirY / inLen;
|
|
2437
|
+
const outNormX = outDirX / outLen;
|
|
2438
|
+
const outNormY = outDirY / outLen;
|
|
2439
|
+
const bisectX = inNormX - outNormX;
|
|
2440
|
+
const bisectY = inNormY - outNormY;
|
|
2441
|
+
const bisectLen = Math.sqrt(bisectX * bisectX + bisectY * bisectY);
|
|
2442
|
+
if (bisectLen < 1e-4) {
|
|
2443
|
+
return this.offsetPointFromObstacles(corner, clearance, map);
|
|
2444
|
+
}
|
|
2445
|
+
const bisectNormX = bisectX / bisectLen;
|
|
2446
|
+
const bisectNormY = bisectY / bisectLen;
|
|
2447
|
+
const dotProduct = inNormX * outNormX + inNormY * outNormY;
|
|
2448
|
+
const angle = Math.acos(Math.max(-1, Math.min(1, dotProduct)));
|
|
2449
|
+
const halfAngle = angle / 2;
|
|
2450
|
+
const sinHalfAngle = Math.sin(halfAngle);
|
|
2451
|
+
if (sinHalfAngle < 0.1) {
|
|
2452
|
+
return this.offsetPointFromObstacles(corner, clearance, map);
|
|
2453
|
+
}
|
|
2454
|
+
const offsetDist = clearance / sinHalfAngle;
|
|
2455
|
+
const maxOffset = clearance * 3;
|
|
2456
|
+
const actualOffset = Math.min(offsetDist, maxOffset);
|
|
2457
|
+
const newX = corner.x + bisectNormX * actualOffset;
|
|
2458
|
+
const newY = corner.y + bisectNormY * actualOffset;
|
|
2459
|
+
if (map.isWalkable(Math.floor(newX), Math.floor(newY))) {
|
|
2460
|
+
return createPoint(newX, newY);
|
|
2461
|
+
}
|
|
2462
|
+
const altX = corner.x - bisectNormX * actualOffset;
|
|
2463
|
+
const altY = corner.y - bisectNormY * actualOffset;
|
|
2464
|
+
if (map.isWalkable(Math.floor(altX), Math.floor(altY))) {
|
|
2465
|
+
return createPoint(altX, altY);
|
|
2466
|
+
}
|
|
2467
|
+
return this.offsetPointFromObstacles(corner, clearance, map);
|
|
2468
|
+
}
|
|
2469
|
+
/**
|
|
2470
|
+
* @zh 检测附近的障碍物方向
|
|
2471
|
+
* @en Detect nearby obstacle directions
|
|
2472
|
+
*/
|
|
2473
|
+
detectNearbyObstacles(point, clearance, map) {
|
|
2474
|
+
const obstacles = [];
|
|
2475
|
+
for (const angle of this.sampleAngles) {
|
|
2476
|
+
const dirX = Math.cos(angle);
|
|
2477
|
+
const dirY = Math.sin(angle);
|
|
2478
|
+
const sampleX = point.x + dirX * clearance;
|
|
2479
|
+
const sampleY = point.y + dirY * clearance;
|
|
2480
|
+
if (!map.isWalkable(Math.floor(sampleX), Math.floor(sampleY))) {
|
|
2481
|
+
obstacles.push(createPoint(dirX, dirY));
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
return obstacles;
|
|
2485
|
+
}
|
|
2486
|
+
};
|
|
2487
|
+
__name(_RadiusAwarePathSmoother, "RadiusAwarePathSmoother");
|
|
2488
|
+
var RadiusAwarePathSmoother = _RadiusAwarePathSmoother;
|
|
2489
|
+
var _CombinedRadiusAwareSmoother = class _CombinedRadiusAwareSmoother {
|
|
2490
|
+
constructor(baseSmoother, config) {
|
|
2491
|
+
__publicField(this, "baseSmoother");
|
|
2492
|
+
__publicField(this, "radiusAwareSmoother");
|
|
2493
|
+
this.baseSmoother = baseSmoother;
|
|
2494
|
+
this.radiusAwareSmoother = new RadiusAwarePathSmoother(config);
|
|
2495
|
+
}
|
|
2496
|
+
smooth(path, map) {
|
|
2497
|
+
const smoothed = this.baseSmoother.smooth(path, map);
|
|
2498
|
+
return this.radiusAwareSmoother.smooth(smoothed, map);
|
|
2499
|
+
}
|
|
2500
|
+
};
|
|
2501
|
+
__name(_CombinedRadiusAwareSmoother, "CombinedRadiusAwareSmoother");
|
|
2502
|
+
var CombinedRadiusAwareSmoother = _CombinedRadiusAwareSmoother;
|
|
2503
|
+
function createRadiusAwareSmoother(agentRadius, options) {
|
|
2504
|
+
return new RadiusAwarePathSmoother({
|
|
2505
|
+
agentRadius,
|
|
2506
|
+
...options
|
|
2507
|
+
});
|
|
2508
|
+
}
|
|
2509
|
+
__name(createRadiusAwareSmoother, "createRadiusAwareSmoother");
|
|
2510
|
+
function createCombinedRadiusAwareSmoother(baseSmoother, agentRadius, options) {
|
|
2511
|
+
return new CombinedRadiusAwareSmoother(baseSmoother, {
|
|
2512
|
+
agentRadius,
|
|
2513
|
+
...options
|
|
2514
|
+
});
|
|
2515
|
+
}
|
|
2516
|
+
__name(createCombinedRadiusAwareSmoother, "createCombinedRadiusAwareSmoother");
|
|
2932
2517
|
export {
|
|
2933
2518
|
AStarPathfinder,
|
|
2934
2519
|
BinaryHeap,
|
|
2935
2520
|
CatmullRomSmoother,
|
|
2521
|
+
CollisionResolver,
|
|
2522
|
+
CollisionResolverAdapter,
|
|
2523
|
+
CombinedRadiusAwareSmoother,
|
|
2936
2524
|
CombinedSmoother,
|
|
2937
2525
|
DEFAULT_AGENT_PARAMS,
|
|
2526
|
+
DEFAULT_COLLISION_CONFIG,
|
|
2527
|
+
DEFAULT_FLOW_CONTROLLER_CONFIG,
|
|
2938
2528
|
DEFAULT_GRID_OPTIONS,
|
|
2939
2529
|
DEFAULT_HPA_CONFIG,
|
|
2940
2530
|
DEFAULT_ORCA_CONFIG,
|
|
2531
|
+
DEFAULT_ORCA_PARAMS,
|
|
2941
2532
|
DEFAULT_PATHFINDING_OPTIONS,
|
|
2942
2533
|
DEFAULT_PATH_CACHE_CONFIG,
|
|
2943
2534
|
DEFAULT_REPLANNING_CONFIG,
|
|
2944
2535
|
DIRECTIONS_4,
|
|
2945
2536
|
DIRECTIONS_8,
|
|
2537
|
+
EMPTY_COLLISION,
|
|
2538
|
+
EMPTY_COLLISION_RESULT,
|
|
2946
2539
|
EMPTY_PATH_RESULT,
|
|
2540
|
+
EMPTY_PLAN_RESULT,
|
|
2947
2541
|
EMPTY_PROGRESS,
|
|
2542
|
+
FlowController,
|
|
2948
2543
|
GridMap,
|
|
2949
2544
|
GridNode,
|
|
2950
2545
|
GridPathfinder,
|
|
2546
|
+
GridPathfinderAdapter,
|
|
2951
2547
|
HPAPathfinder,
|
|
2952
2548
|
IncrementalAStarPathfinder,
|
|
2549
|
+
IncrementalGridPathPlannerAdapter,
|
|
2953
2550
|
IndexedBinaryHeap,
|
|
2954
2551
|
JPSPathfinder,
|
|
2955
2552
|
KDTree,
|
|
2956
2553
|
LineOfSightSmoother,
|
|
2957
2554
|
NavMesh,
|
|
2555
|
+
NavMeshPathPlannerAdapter,
|
|
2556
|
+
ORCALocalAvoidanceAdapter,
|
|
2958
2557
|
ORCASolver,
|
|
2959
2558
|
ObstacleChangeManager,
|
|
2559
|
+
PassPermission,
|
|
2960
2560
|
PathCache,
|
|
2561
|
+
PathPlanState,
|
|
2961
2562
|
PathValidator,
|
|
2962
2563
|
PathfindingState,
|
|
2564
|
+
RadiusAwarePathSmoother,
|
|
2963
2565
|
bresenhamLineOfSight,
|
|
2964
2566
|
chebyshevDistance,
|
|
2965
2567
|
createAStarPathfinder,
|
|
2568
|
+
createAStarPlanner,
|
|
2966
2569
|
createCatmullRomSmoother,
|
|
2570
|
+
createCollisionResolver,
|
|
2571
|
+
createCombinedRadiusAwareSmoother,
|
|
2967
2572
|
createCombinedSmoother,
|
|
2573
|
+
createDefaultCollisionResolver,
|
|
2574
|
+
createFlowController,
|
|
2968
2575
|
createGridMap,
|
|
2969
2576
|
createGridPathfinder,
|
|
2970
2577
|
createHPAPathfinder,
|
|
2578
|
+
createHPAPlanner,
|
|
2971
2579
|
createIncrementalAStarPathfinder,
|
|
2580
|
+
createIncrementalAStarPlanner,
|
|
2972
2581
|
createJPSPathfinder,
|
|
2582
|
+
createJPSPlanner,
|
|
2973
2583
|
createKDTree,
|
|
2974
2584
|
createLineOfSightSmoother,
|
|
2975
2585
|
createNavMesh,
|
|
2586
|
+
createNavMeshPathPlanner,
|
|
2587
|
+
createORCAAvoidance,
|
|
2976
2588
|
createORCASolver,
|
|
2977
2589
|
createObstacleChangeManager,
|
|
2978
2590
|
createPathCache,
|
|
2979
2591
|
createPathValidator,
|
|
2980
2592
|
createPoint,
|
|
2593
|
+
createRadiusAwareSmoother,
|
|
2981
2594
|
euclideanDistance,
|
|
2595
|
+
isIncrementalPlanner,
|
|
2982
2596
|
manhattanDistance,
|
|
2983
2597
|
octileDistance,
|
|
2984
2598
|
raycastLineOfSight,
|