@esengine/pathfinding 12.0.0 → 12.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/IIncrementalPathfinding-3qs7e_pO.d.ts +450 -0
- package/dist/chunk-GTFFYRZM.js +36 -0
- package/dist/chunk-GTFFYRZM.js.map +1 -0
- package/dist/chunk-TPT7Q3E3.js +1648 -0
- package/dist/chunk-TPT7Q3E3.js.map +1 -0
- package/dist/ecs.d.ts +503 -0
- package/dist/ecs.js +1033 -0
- package/dist/ecs.js.map +1 -0
- package/dist/index.d.ts +886 -192
- package/dist/index.js +1650 -1066
- package/dist/index.js.map +1 -1
- package/dist/nodes.d.ts +143 -0
- package/dist/nodes.js +1174 -0
- package/dist/nodes.js.map +1 -0
- package/package.json +21 -5
|
@@ -0,0 +1,1648 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EMPTY_PROGRESS,
|
|
3
|
+
PathfindingState,
|
|
4
|
+
__name,
|
|
5
|
+
__publicField
|
|
6
|
+
} from "./chunk-GTFFYRZM.js";
|
|
7
|
+
|
|
8
|
+
// src/core/IPathfinding.ts
|
|
9
|
+
function createPoint(x, y) {
|
|
10
|
+
return {
|
|
11
|
+
x,
|
|
12
|
+
y
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
__name(createPoint, "createPoint");
|
|
16
|
+
var EMPTY_PATH_RESULT = {
|
|
17
|
+
found: false,
|
|
18
|
+
path: [],
|
|
19
|
+
cost: 0,
|
|
20
|
+
nodesSearched: 0
|
|
21
|
+
};
|
|
22
|
+
function manhattanDistance(a, b) {
|
|
23
|
+
return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
|
|
24
|
+
}
|
|
25
|
+
__name(manhattanDistance, "manhattanDistance");
|
|
26
|
+
function euclideanDistance(a, b) {
|
|
27
|
+
const dx = a.x - b.x;
|
|
28
|
+
const dy = a.y - b.y;
|
|
29
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
30
|
+
}
|
|
31
|
+
__name(euclideanDistance, "euclideanDistance");
|
|
32
|
+
function chebyshevDistance(a, b) {
|
|
33
|
+
return Math.max(Math.abs(a.x - b.x), Math.abs(a.y - b.y));
|
|
34
|
+
}
|
|
35
|
+
__name(chebyshevDistance, "chebyshevDistance");
|
|
36
|
+
function octileDistance(a, b) {
|
|
37
|
+
const dx = Math.abs(a.x - b.x);
|
|
38
|
+
const dy = Math.abs(a.y - b.y);
|
|
39
|
+
const D = 1;
|
|
40
|
+
const D2 = Math.SQRT2;
|
|
41
|
+
return D * (dx + dy) + (D2 - 2 * D) * Math.min(dx, dy);
|
|
42
|
+
}
|
|
43
|
+
__name(octileDistance, "octileDistance");
|
|
44
|
+
var DEFAULT_PATHFINDING_OPTIONS = {
|
|
45
|
+
maxNodes: 1e4,
|
|
46
|
+
heuristicWeight: 1,
|
|
47
|
+
allowDiagonal: true,
|
|
48
|
+
avoidCorners: true
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// src/core/IndexedBinaryHeap.ts
|
|
52
|
+
var _IndexedBinaryHeap = class _IndexedBinaryHeap {
|
|
53
|
+
/**
|
|
54
|
+
* @zh 创建带索引追踪的二叉堆
|
|
55
|
+
* @en Create indexed binary heap
|
|
56
|
+
*
|
|
57
|
+
* @param compare - @zh 比较函数,返回负数表示 a < b @en Compare function, returns negative if a < b
|
|
58
|
+
*/
|
|
59
|
+
constructor(compare) {
|
|
60
|
+
__publicField(this, "heap", []);
|
|
61
|
+
__publicField(this, "compare");
|
|
62
|
+
this.compare = compare;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* @zh 堆大小
|
|
66
|
+
* @en Heap size
|
|
67
|
+
*/
|
|
68
|
+
get size() {
|
|
69
|
+
return this.heap.length;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* @zh 是否为空
|
|
73
|
+
* @en Is empty
|
|
74
|
+
*/
|
|
75
|
+
get isEmpty() {
|
|
76
|
+
return this.heap.length === 0;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* @zh 插入元素
|
|
80
|
+
* @en Push element
|
|
81
|
+
*/
|
|
82
|
+
push(item) {
|
|
83
|
+
item.heapIndex = this.heap.length;
|
|
84
|
+
this.heap.push(item);
|
|
85
|
+
this.bubbleUp(this.heap.length - 1);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* @zh 弹出最小元素
|
|
89
|
+
* @en Pop minimum element
|
|
90
|
+
*/
|
|
91
|
+
pop() {
|
|
92
|
+
if (this.heap.length === 0) {
|
|
93
|
+
return void 0;
|
|
94
|
+
}
|
|
95
|
+
const result = this.heap[0];
|
|
96
|
+
result.heapIndex = -1;
|
|
97
|
+
const last = this.heap.pop();
|
|
98
|
+
if (this.heap.length > 0) {
|
|
99
|
+
last.heapIndex = 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
|
|
115
|
+
*/
|
|
116
|
+
update(item) {
|
|
117
|
+
const index = item.heapIndex;
|
|
118
|
+
if (index >= 0 && index < this.heap.length && this.heap[index] === item) {
|
|
119
|
+
this.bubbleUp(index);
|
|
120
|
+
this.sinkDown(item.heapIndex);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* @zh 检查是否包含元素
|
|
125
|
+
* @en Check if contains element
|
|
126
|
+
*/
|
|
127
|
+
contains(item) {
|
|
128
|
+
const index = item.heapIndex;
|
|
129
|
+
return index >= 0 && index < this.heap.length && this.heap[index] === item;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* @zh 从堆中移除指定元素
|
|
133
|
+
* @en Remove specific element from heap
|
|
134
|
+
*/
|
|
135
|
+
remove(item) {
|
|
136
|
+
const index = item.heapIndex;
|
|
137
|
+
if (index < 0 || index >= this.heap.length || this.heap[index] !== item) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
item.heapIndex = -1;
|
|
141
|
+
if (index === this.heap.length - 1) {
|
|
142
|
+
this.heap.pop();
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
const last = this.heap.pop();
|
|
146
|
+
last.heapIndex = index;
|
|
147
|
+
this.heap[index] = last;
|
|
148
|
+
this.bubbleUp(index);
|
|
149
|
+
this.sinkDown(last.heapIndex);
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* @zh 清空堆
|
|
154
|
+
* @en Clear heap
|
|
155
|
+
*/
|
|
156
|
+
clear() {
|
|
157
|
+
for (const item of this.heap) {
|
|
158
|
+
item.heapIndex = -1;
|
|
159
|
+
}
|
|
160
|
+
this.heap.length = 0;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* @zh 上浮操作
|
|
164
|
+
* @en Bubble up operation
|
|
165
|
+
*/
|
|
166
|
+
bubbleUp(index) {
|
|
167
|
+
const item = this.heap[index];
|
|
168
|
+
while (index > 0) {
|
|
169
|
+
const parentIndex = index - 1 >> 1;
|
|
170
|
+
const parent = this.heap[parentIndex];
|
|
171
|
+
if (this.compare(item, parent) >= 0) {
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
parent.heapIndex = index;
|
|
175
|
+
this.heap[index] = parent;
|
|
176
|
+
index = parentIndex;
|
|
177
|
+
}
|
|
178
|
+
item.heapIndex = index;
|
|
179
|
+
this.heap[index] = item;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* @zh 下沉操作
|
|
183
|
+
* @en Sink down operation
|
|
184
|
+
*/
|
|
185
|
+
sinkDown(index) {
|
|
186
|
+
const length = this.heap.length;
|
|
187
|
+
const item = this.heap[index];
|
|
188
|
+
const halfLength = length >> 1;
|
|
189
|
+
while (index < halfLength) {
|
|
190
|
+
const leftIndex = (index << 1) + 1;
|
|
191
|
+
const rightIndex = leftIndex + 1;
|
|
192
|
+
let smallest = index;
|
|
193
|
+
let smallestItem = item;
|
|
194
|
+
const left = this.heap[leftIndex];
|
|
195
|
+
if (this.compare(left, smallestItem) < 0) {
|
|
196
|
+
smallest = leftIndex;
|
|
197
|
+
smallestItem = left;
|
|
198
|
+
}
|
|
199
|
+
if (rightIndex < length) {
|
|
200
|
+
const right = this.heap[rightIndex];
|
|
201
|
+
if (this.compare(right, smallestItem) < 0) {
|
|
202
|
+
smallest = rightIndex;
|
|
203
|
+
smallestItem = right;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (smallest === index) {
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
smallestItem.heapIndex = index;
|
|
210
|
+
this.heap[index] = smallestItem;
|
|
211
|
+
index = smallest;
|
|
212
|
+
}
|
|
213
|
+
item.heapIndex = index;
|
|
214
|
+
this.heap[index] = item;
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
__name(_IndexedBinaryHeap, "IndexedBinaryHeap");
|
|
218
|
+
var IndexedBinaryHeap = _IndexedBinaryHeap;
|
|
219
|
+
|
|
220
|
+
// src/core/PathCache.ts
|
|
221
|
+
var DEFAULT_PATH_CACHE_CONFIG = {
|
|
222
|
+
maxEntries: 1e3,
|
|
223
|
+
ttlMs: 5e3,
|
|
224
|
+
enableApproximateMatch: false,
|
|
225
|
+
approximateRange: 2
|
|
226
|
+
};
|
|
227
|
+
var _PathCache = class _PathCache {
|
|
228
|
+
constructor(config = {}) {
|
|
229
|
+
__publicField(this, "config");
|
|
230
|
+
__publicField(this, "cache");
|
|
231
|
+
__publicField(this, "accessOrder");
|
|
232
|
+
this.config = {
|
|
233
|
+
...DEFAULT_PATH_CACHE_CONFIG,
|
|
234
|
+
...config
|
|
235
|
+
};
|
|
236
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
237
|
+
this.accessOrder = [];
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* @zh 获取缓存的路径
|
|
241
|
+
* @en Get cached path
|
|
242
|
+
*
|
|
243
|
+
* @param startX - @zh 起点 X 坐标 @en Start X coordinate
|
|
244
|
+
* @param startY - @zh 起点 Y 坐标 @en Start Y coordinate
|
|
245
|
+
* @param endX - @zh 终点 X 坐标 @en End X coordinate
|
|
246
|
+
* @param endY - @zh 终点 Y 坐标 @en End Y coordinate
|
|
247
|
+
* @param mapVersion - @zh 地图版本号 @en Map version number
|
|
248
|
+
* @returns @zh 缓存的路径结果或 null @en Cached path result or null
|
|
249
|
+
*/
|
|
250
|
+
get(startX, startY, endX, endY, mapVersion) {
|
|
251
|
+
const key = this.generateKey(startX, startY, endX, endY);
|
|
252
|
+
const entry = this.cache.get(key);
|
|
253
|
+
if (!entry) {
|
|
254
|
+
if (this.config.enableApproximateMatch) {
|
|
255
|
+
return this.getApproximate(startX, startY, endX, endY, mapVersion);
|
|
256
|
+
}
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
if (!this.isValid(entry, mapVersion)) {
|
|
260
|
+
this.cache.delete(key);
|
|
261
|
+
this.removeFromAccessOrder(key);
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
this.updateAccessOrder(key);
|
|
265
|
+
return entry.result;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* @zh 设置缓存路径
|
|
269
|
+
* @en Set cached path
|
|
270
|
+
*
|
|
271
|
+
* @param startX - @zh 起点 X 坐标 @en Start X coordinate
|
|
272
|
+
* @param startY - @zh 起点 Y 坐标 @en Start Y coordinate
|
|
273
|
+
* @param endX - @zh 终点 X 坐标 @en End X coordinate
|
|
274
|
+
* @param endY - @zh 终点 Y 坐标 @en End Y coordinate
|
|
275
|
+
* @param result - @zh 路径结果 @en Path result
|
|
276
|
+
* @param mapVersion - @zh 地图版本号 @en Map version number
|
|
277
|
+
*/
|
|
278
|
+
set(startX, startY, endX, endY, result, mapVersion) {
|
|
279
|
+
if (this.cache.size >= this.config.maxEntries) {
|
|
280
|
+
this.evictLRU();
|
|
281
|
+
}
|
|
282
|
+
const key = this.generateKey(startX, startY, endX, endY);
|
|
283
|
+
const entry = {
|
|
284
|
+
result,
|
|
285
|
+
timestamp: Date.now(),
|
|
286
|
+
mapVersion
|
|
287
|
+
};
|
|
288
|
+
this.cache.set(key, entry);
|
|
289
|
+
this.updateAccessOrder(key);
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* @zh 使所有缓存失效
|
|
293
|
+
* @en Invalidate all cache
|
|
294
|
+
*/
|
|
295
|
+
invalidateAll() {
|
|
296
|
+
this.cache.clear();
|
|
297
|
+
this.accessOrder.length = 0;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* @zh 使指定区域的缓存失效
|
|
301
|
+
* @en Invalidate cache for specified region
|
|
302
|
+
*
|
|
303
|
+
* @param minX - @zh 最小 X 坐标 @en Minimum X coordinate
|
|
304
|
+
* @param minY - @zh 最小 Y 坐标 @en Minimum Y coordinate
|
|
305
|
+
* @param maxX - @zh 最大 X 坐标 @en Maximum X coordinate
|
|
306
|
+
* @param maxY - @zh 最大 Y 坐标 @en Maximum Y coordinate
|
|
307
|
+
*/
|
|
308
|
+
invalidateRegion(minX, minY, maxX, maxY) {
|
|
309
|
+
const keysToDelete = [];
|
|
310
|
+
for (const [key, entry] of this.cache) {
|
|
311
|
+
const path = entry.result.path;
|
|
312
|
+
if (path.length === 0) continue;
|
|
313
|
+
for (const point of path) {
|
|
314
|
+
if (point.x >= minX && point.x <= maxX && point.y >= minY && point.y <= maxY) {
|
|
315
|
+
keysToDelete.push(key);
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
for (const key of keysToDelete) {
|
|
321
|
+
this.cache.delete(key);
|
|
322
|
+
this.removeFromAccessOrder(key);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* @zh 获取缓存统计信息
|
|
327
|
+
* @en Get cache statistics
|
|
328
|
+
*/
|
|
329
|
+
getStats() {
|
|
330
|
+
return {
|
|
331
|
+
size: this.cache.size,
|
|
332
|
+
maxSize: this.config.maxEntries
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* @zh 清理过期条目
|
|
337
|
+
* @en Clean up expired entries
|
|
338
|
+
*/
|
|
339
|
+
cleanup() {
|
|
340
|
+
if (this.config.ttlMs === 0) return;
|
|
341
|
+
const now = Date.now();
|
|
342
|
+
const keysToDelete = [];
|
|
343
|
+
for (const [key, entry] of this.cache) {
|
|
344
|
+
if (now - entry.timestamp > this.config.ttlMs) {
|
|
345
|
+
keysToDelete.push(key);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
for (const key of keysToDelete) {
|
|
349
|
+
this.cache.delete(key);
|
|
350
|
+
this.removeFromAccessOrder(key);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
// =========================================================================
|
|
354
|
+
// 私有方法 | Private Methods
|
|
355
|
+
// =========================================================================
|
|
356
|
+
generateKey(startX, startY, endX, endY) {
|
|
357
|
+
return `${startX},${startY}->${endX},${endY}`;
|
|
358
|
+
}
|
|
359
|
+
isValid(entry, mapVersion) {
|
|
360
|
+
if (entry.mapVersion !== mapVersion) {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
if (this.config.ttlMs > 0) {
|
|
364
|
+
const age = Date.now() - entry.timestamp;
|
|
365
|
+
if (age > this.config.ttlMs) {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
371
|
+
getApproximate(startX, startY, endX, endY, mapVersion) {
|
|
372
|
+
const range = this.config.approximateRange;
|
|
373
|
+
for (let sx = startX - range; sx <= startX + range; sx++) {
|
|
374
|
+
for (let sy = startY - range; sy <= startY + range; sy++) {
|
|
375
|
+
for (let ex = endX - range; ex <= endX + range; ex++) {
|
|
376
|
+
for (let ey = endY - range; ey <= endY + range; ey++) {
|
|
377
|
+
const key = this.generateKey(sx, sy, ex, ey);
|
|
378
|
+
const entry = this.cache.get(key);
|
|
379
|
+
if (entry && this.isValid(entry, mapVersion)) {
|
|
380
|
+
this.updateAccessOrder(key);
|
|
381
|
+
return this.adjustPathForApproximate(entry.result, startX, startY, endX, endY);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
adjustPathForApproximate(result, newStartX, newStartY, newEndX, newEndY) {
|
|
390
|
+
if (result.path.length === 0) {
|
|
391
|
+
return result;
|
|
392
|
+
}
|
|
393
|
+
const newPath = [];
|
|
394
|
+
const oldStart = result.path[0];
|
|
395
|
+
const oldEnd = result.path[result.path.length - 1];
|
|
396
|
+
if (newStartX !== oldStart.x || newStartY !== oldStart.y) {
|
|
397
|
+
newPath.push({
|
|
398
|
+
x: newStartX,
|
|
399
|
+
y: newStartY
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
newPath.push(...result.path);
|
|
403
|
+
if (newEndX !== oldEnd.x || newEndY !== oldEnd.y) {
|
|
404
|
+
newPath.push({
|
|
405
|
+
x: newEndX,
|
|
406
|
+
y: newEndY
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
return {
|
|
410
|
+
...result,
|
|
411
|
+
path: newPath
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
updateAccessOrder(key) {
|
|
415
|
+
this.removeFromAccessOrder(key);
|
|
416
|
+
this.accessOrder.push(key);
|
|
417
|
+
}
|
|
418
|
+
removeFromAccessOrder(key) {
|
|
419
|
+
const index = this.accessOrder.indexOf(key);
|
|
420
|
+
if (index !== -1) {
|
|
421
|
+
this.accessOrder.splice(index, 1);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
evictLRU() {
|
|
425
|
+
const lruKey = this.accessOrder.shift();
|
|
426
|
+
if (lruKey) {
|
|
427
|
+
this.cache.delete(lruKey);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
__name(_PathCache, "PathCache");
|
|
432
|
+
var PathCache = _PathCache;
|
|
433
|
+
function createPathCache(config) {
|
|
434
|
+
return new PathCache(config);
|
|
435
|
+
}
|
|
436
|
+
__name(createPathCache, "createPathCache");
|
|
437
|
+
|
|
438
|
+
// src/core/IncrementalAStarPathfinder.ts
|
|
439
|
+
var _IncrementalAStarPathfinder = class _IncrementalAStarPathfinder {
|
|
440
|
+
/**
|
|
441
|
+
* @zh 创建增量 A* 寻路器
|
|
442
|
+
* @en Create incremental A* pathfinder
|
|
443
|
+
*
|
|
444
|
+
* @param map - @zh 寻路地图实例 @en Pathfinding map instance
|
|
445
|
+
* @param config - @zh 配置选项 @en Configuration options
|
|
446
|
+
*/
|
|
447
|
+
constructor(map, config) {
|
|
448
|
+
__publicField(this, "map");
|
|
449
|
+
__publicField(this, "sessions", /* @__PURE__ */ new Map());
|
|
450
|
+
__publicField(this, "nextRequestId", 0);
|
|
451
|
+
__publicField(this, "affectedRegions", []);
|
|
452
|
+
__publicField(this, "maxRegionAge", 5e3);
|
|
453
|
+
__publicField(this, "cache");
|
|
454
|
+
__publicField(this, "enableCache");
|
|
455
|
+
__publicField(this, "mapVersion", 0);
|
|
456
|
+
__publicField(this, "cacheHits", 0);
|
|
457
|
+
__publicField(this, "cacheMisses", 0);
|
|
458
|
+
this.map = map;
|
|
459
|
+
this.enableCache = config?.enableCache ?? false;
|
|
460
|
+
this.cache = this.enableCache ? new PathCache(config?.cacheConfig) : null;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* @zh 请求寻路(非阻塞)
|
|
464
|
+
* @en Request pathfinding (non-blocking)
|
|
465
|
+
*/
|
|
466
|
+
requestPath(startX, startY, endX, endY, options) {
|
|
467
|
+
const id = this.nextRequestId++;
|
|
468
|
+
const priority = options?.priority ?? 50;
|
|
469
|
+
const opts = {
|
|
470
|
+
...DEFAULT_PATHFINDING_OPTIONS,
|
|
471
|
+
...options
|
|
472
|
+
};
|
|
473
|
+
const request = {
|
|
474
|
+
id,
|
|
475
|
+
startX,
|
|
476
|
+
startY,
|
|
477
|
+
endX,
|
|
478
|
+
endY,
|
|
479
|
+
options: opts,
|
|
480
|
+
priority,
|
|
481
|
+
createdAt: Date.now()
|
|
482
|
+
};
|
|
483
|
+
if (this.cache) {
|
|
484
|
+
const cached = this.cache.get(startX, startY, endX, endY, this.mapVersion);
|
|
485
|
+
if (cached) {
|
|
486
|
+
this.cacheHits++;
|
|
487
|
+
const session2 = {
|
|
488
|
+
request,
|
|
489
|
+
state: cached.found ? PathfindingState.Completed : PathfindingState.Failed,
|
|
490
|
+
options: opts,
|
|
491
|
+
openList: new IndexedBinaryHeap((a, b) => a.f - b.f),
|
|
492
|
+
nodeCache: /* @__PURE__ */ new Map(),
|
|
493
|
+
startNode: this.map.getNodeAt(startX, startY),
|
|
494
|
+
endNode: this.map.getNodeAt(endX, endY),
|
|
495
|
+
endPosition: {
|
|
496
|
+
x: endX,
|
|
497
|
+
y: endY
|
|
498
|
+
},
|
|
499
|
+
nodesSearched: cached.nodesSearched,
|
|
500
|
+
framesUsed: 0,
|
|
501
|
+
initialDistance: 0,
|
|
502
|
+
result: {
|
|
503
|
+
requestId: id,
|
|
504
|
+
found: cached.found,
|
|
505
|
+
path: [
|
|
506
|
+
...cached.path
|
|
507
|
+
],
|
|
508
|
+
cost: cached.cost,
|
|
509
|
+
nodesSearched: cached.nodesSearched,
|
|
510
|
+
framesUsed: 0,
|
|
511
|
+
isPartial: false
|
|
512
|
+
},
|
|
513
|
+
affectedByChange: false
|
|
514
|
+
};
|
|
515
|
+
this.sessions.set(id, session2);
|
|
516
|
+
return request;
|
|
517
|
+
}
|
|
518
|
+
this.cacheMisses++;
|
|
519
|
+
}
|
|
520
|
+
const startNode = this.map.getNodeAt(startX, startY);
|
|
521
|
+
const endNode = this.map.getNodeAt(endX, endY);
|
|
522
|
+
if (!startNode || !endNode || !startNode.walkable || !endNode.walkable) {
|
|
523
|
+
const session2 = {
|
|
524
|
+
request,
|
|
525
|
+
state: PathfindingState.Failed,
|
|
526
|
+
options: opts,
|
|
527
|
+
openList: new IndexedBinaryHeap((a, b) => a.f - b.f),
|
|
528
|
+
nodeCache: /* @__PURE__ */ new Map(),
|
|
529
|
+
startNode,
|
|
530
|
+
endNode,
|
|
531
|
+
endPosition: endNode?.position ?? {
|
|
532
|
+
x: endX,
|
|
533
|
+
y: endY
|
|
534
|
+
},
|
|
535
|
+
nodesSearched: 0,
|
|
536
|
+
framesUsed: 0,
|
|
537
|
+
initialDistance: 0,
|
|
538
|
+
result: this.createEmptyResult(id),
|
|
539
|
+
affectedByChange: false
|
|
540
|
+
};
|
|
541
|
+
this.sessions.set(id, session2);
|
|
542
|
+
return request;
|
|
543
|
+
}
|
|
544
|
+
if (startNode.id === endNode.id) {
|
|
545
|
+
const session2 = {
|
|
546
|
+
request,
|
|
547
|
+
state: PathfindingState.Completed,
|
|
548
|
+
options: opts,
|
|
549
|
+
openList: new IndexedBinaryHeap((a, b) => a.f - b.f),
|
|
550
|
+
nodeCache: /* @__PURE__ */ new Map(),
|
|
551
|
+
startNode,
|
|
552
|
+
endNode,
|
|
553
|
+
endPosition: endNode.position,
|
|
554
|
+
nodesSearched: 1,
|
|
555
|
+
framesUsed: 0,
|
|
556
|
+
initialDistance: 0,
|
|
557
|
+
result: {
|
|
558
|
+
requestId: id,
|
|
559
|
+
found: true,
|
|
560
|
+
path: [
|
|
561
|
+
startNode.position
|
|
562
|
+
],
|
|
563
|
+
cost: 0,
|
|
564
|
+
nodesSearched: 1,
|
|
565
|
+
framesUsed: 0,
|
|
566
|
+
isPartial: false
|
|
567
|
+
},
|
|
568
|
+
affectedByChange: false
|
|
569
|
+
};
|
|
570
|
+
this.sessions.set(id, session2);
|
|
571
|
+
return request;
|
|
572
|
+
}
|
|
573
|
+
const initialDistance = this.map.heuristic(startNode.position, endNode.position);
|
|
574
|
+
const openList = new IndexedBinaryHeap((a, b) => a.f - b.f);
|
|
575
|
+
const nodeCache = /* @__PURE__ */ new Map();
|
|
576
|
+
const startAStarNode = {
|
|
577
|
+
node: startNode,
|
|
578
|
+
g: 0,
|
|
579
|
+
h: initialDistance * opts.heuristicWeight,
|
|
580
|
+
f: initialDistance * opts.heuristicWeight,
|
|
581
|
+
parent: null,
|
|
582
|
+
closed: false,
|
|
583
|
+
opened: true,
|
|
584
|
+
heapIndex: -1
|
|
585
|
+
};
|
|
586
|
+
nodeCache.set(startNode.id, startAStarNode);
|
|
587
|
+
openList.push(startAStarNode);
|
|
588
|
+
const session = {
|
|
589
|
+
request,
|
|
590
|
+
state: PathfindingState.InProgress,
|
|
591
|
+
options: opts,
|
|
592
|
+
openList,
|
|
593
|
+
nodeCache,
|
|
594
|
+
startNode,
|
|
595
|
+
endNode,
|
|
596
|
+
endPosition: endNode.position,
|
|
597
|
+
nodesSearched: 0,
|
|
598
|
+
framesUsed: 0,
|
|
599
|
+
initialDistance,
|
|
600
|
+
result: null,
|
|
601
|
+
affectedByChange: false
|
|
602
|
+
};
|
|
603
|
+
this.sessions.set(id, session);
|
|
604
|
+
return request;
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* @zh 执行一步搜索
|
|
608
|
+
* @en Execute one step of search
|
|
609
|
+
*/
|
|
610
|
+
step(requestId, maxIterations) {
|
|
611
|
+
const session = this.sessions.get(requestId);
|
|
612
|
+
if (!session) {
|
|
613
|
+
return EMPTY_PROGRESS;
|
|
614
|
+
}
|
|
615
|
+
if (session.state !== PathfindingState.InProgress) {
|
|
616
|
+
return this.createProgress(session);
|
|
617
|
+
}
|
|
618
|
+
session.framesUsed++;
|
|
619
|
+
let iterations = 0;
|
|
620
|
+
while (!session.openList.isEmpty && iterations < maxIterations) {
|
|
621
|
+
const current = session.openList.pop();
|
|
622
|
+
current.closed = true;
|
|
623
|
+
session.nodesSearched++;
|
|
624
|
+
iterations++;
|
|
625
|
+
if (current.node.id === session.endNode.id) {
|
|
626
|
+
session.state = PathfindingState.Completed;
|
|
627
|
+
session.result = this.buildResult(session, current);
|
|
628
|
+
if (this.cache && session.result.found) {
|
|
629
|
+
const req = session.request;
|
|
630
|
+
this.cache.set(req.startX, req.startY, req.endX, req.endY, {
|
|
631
|
+
found: true,
|
|
632
|
+
path: session.result.path,
|
|
633
|
+
cost: session.result.cost,
|
|
634
|
+
nodesSearched: session.result.nodesSearched
|
|
635
|
+
}, this.mapVersion);
|
|
636
|
+
}
|
|
637
|
+
return this.createProgress(session);
|
|
638
|
+
}
|
|
639
|
+
this.expandNeighbors(session, current);
|
|
640
|
+
if (session.nodesSearched >= session.options.maxNodes) {
|
|
641
|
+
session.state = PathfindingState.Failed;
|
|
642
|
+
session.result = this.createEmptyResult(requestId);
|
|
643
|
+
return this.createProgress(session);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
if (session.openList.isEmpty && session.state === PathfindingState.InProgress) {
|
|
647
|
+
session.state = PathfindingState.Failed;
|
|
648
|
+
session.result = this.createEmptyResult(requestId);
|
|
649
|
+
}
|
|
650
|
+
return this.createProgress(session);
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* @zh 暂停寻路
|
|
654
|
+
* @en Pause pathfinding
|
|
655
|
+
*/
|
|
656
|
+
pause(requestId) {
|
|
657
|
+
const session = this.sessions.get(requestId);
|
|
658
|
+
if (session && session.state === PathfindingState.InProgress) {
|
|
659
|
+
session.state = PathfindingState.Paused;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* @zh 恢复寻路
|
|
664
|
+
* @en Resume pathfinding
|
|
665
|
+
*/
|
|
666
|
+
resume(requestId) {
|
|
667
|
+
const session = this.sessions.get(requestId);
|
|
668
|
+
if (session && session.state === PathfindingState.Paused) {
|
|
669
|
+
session.state = PathfindingState.InProgress;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* @zh 取消寻路
|
|
674
|
+
* @en Cancel pathfinding
|
|
675
|
+
*/
|
|
676
|
+
cancel(requestId) {
|
|
677
|
+
const session = this.sessions.get(requestId);
|
|
678
|
+
if (session && (session.state === PathfindingState.InProgress || session.state === PathfindingState.Paused)) {
|
|
679
|
+
session.state = PathfindingState.Cancelled;
|
|
680
|
+
session.result = this.createEmptyResult(requestId);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* @zh 获取寻路结果
|
|
685
|
+
* @en Get pathfinding result
|
|
686
|
+
*/
|
|
687
|
+
getResult(requestId) {
|
|
688
|
+
const session = this.sessions.get(requestId);
|
|
689
|
+
return session?.result ?? null;
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* @zh 获取当前进度
|
|
693
|
+
* @en Get current progress
|
|
694
|
+
*/
|
|
695
|
+
getProgress(requestId) {
|
|
696
|
+
const session = this.sessions.get(requestId);
|
|
697
|
+
return session ? this.createProgress(session) : null;
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* @zh 清理已完成的请求
|
|
701
|
+
* @en Clean up completed request
|
|
702
|
+
*/
|
|
703
|
+
cleanup(requestId) {
|
|
704
|
+
const session = this.sessions.get(requestId);
|
|
705
|
+
if (session) {
|
|
706
|
+
session.openList.clear();
|
|
707
|
+
session.nodeCache.clear();
|
|
708
|
+
this.sessions.delete(requestId);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* @zh 通知障碍物变化
|
|
713
|
+
* @en Notify obstacle change
|
|
714
|
+
*/
|
|
715
|
+
notifyObstacleChange(minX, minY, maxX, maxY) {
|
|
716
|
+
this.mapVersion++;
|
|
717
|
+
if (this.cache) {
|
|
718
|
+
this.cache.invalidateRegion(minX, minY, maxX, maxY);
|
|
719
|
+
}
|
|
720
|
+
const region = {
|
|
721
|
+
minX,
|
|
722
|
+
minY,
|
|
723
|
+
maxX,
|
|
724
|
+
maxY,
|
|
725
|
+
timestamp: Date.now()
|
|
726
|
+
};
|
|
727
|
+
this.affectedRegions.push(region);
|
|
728
|
+
for (const session of this.sessions.values()) {
|
|
729
|
+
if (session.state === PathfindingState.InProgress || session.state === PathfindingState.Paused) {
|
|
730
|
+
if (this.sessionAffectedByRegion(session, region)) {
|
|
731
|
+
session.affectedByChange = true;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
this.cleanupOldRegions();
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* @zh 清理所有请求
|
|
739
|
+
* @en Clear all requests
|
|
740
|
+
*/
|
|
741
|
+
clear() {
|
|
742
|
+
for (const session of this.sessions.values()) {
|
|
743
|
+
session.openList.clear();
|
|
744
|
+
session.nodeCache.clear();
|
|
745
|
+
}
|
|
746
|
+
this.sessions.clear();
|
|
747
|
+
this.affectedRegions.length = 0;
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* @zh 清空路径缓存
|
|
751
|
+
* @en Clear path cache
|
|
752
|
+
*/
|
|
753
|
+
clearCache() {
|
|
754
|
+
if (this.cache) {
|
|
755
|
+
this.cache.invalidateAll();
|
|
756
|
+
this.cacheHits = 0;
|
|
757
|
+
this.cacheMisses = 0;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* @zh 获取缓存统计信息
|
|
762
|
+
* @en Get cache statistics
|
|
763
|
+
*/
|
|
764
|
+
getCacheStats() {
|
|
765
|
+
if (!this.cache) {
|
|
766
|
+
return {
|
|
767
|
+
enabled: false,
|
|
768
|
+
hits: 0,
|
|
769
|
+
misses: 0,
|
|
770
|
+
hitRate: 0,
|
|
771
|
+
size: 0
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
const total = this.cacheHits + this.cacheMisses;
|
|
775
|
+
const hitRate = total > 0 ? this.cacheHits / total : 0;
|
|
776
|
+
return {
|
|
777
|
+
enabled: true,
|
|
778
|
+
hits: this.cacheHits,
|
|
779
|
+
misses: this.cacheMisses,
|
|
780
|
+
hitRate,
|
|
781
|
+
size: this.cache.getStats().size
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* @zh 检查会话是否被障碍物变化影响
|
|
786
|
+
* @en Check if session is affected by obstacle change
|
|
787
|
+
*/
|
|
788
|
+
isAffectedByChange(requestId) {
|
|
789
|
+
const session = this.sessions.get(requestId);
|
|
790
|
+
return session?.affectedByChange ?? false;
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* @zh 清除会话的变化标记
|
|
794
|
+
* @en Clear session's change flag
|
|
795
|
+
*/
|
|
796
|
+
clearChangeFlag(requestId) {
|
|
797
|
+
const session = this.sessions.get(requestId);
|
|
798
|
+
if (session) {
|
|
799
|
+
session.affectedByChange = false;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
// =========================================================================
|
|
803
|
+
// 私有方法 | Private Methods
|
|
804
|
+
// =========================================================================
|
|
805
|
+
/**
|
|
806
|
+
* @zh 展开邻居节点
|
|
807
|
+
* @en Expand neighbor nodes
|
|
808
|
+
*/
|
|
809
|
+
expandNeighbors(session, current) {
|
|
810
|
+
const neighbors = this.map.getNeighbors(current.node);
|
|
811
|
+
for (const neighborNode of neighbors) {
|
|
812
|
+
if (!neighborNode.walkable) {
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
let neighbor = session.nodeCache.get(neighborNode.id);
|
|
816
|
+
if (!neighbor) {
|
|
817
|
+
neighbor = {
|
|
818
|
+
node: neighborNode,
|
|
819
|
+
g: Infinity,
|
|
820
|
+
h: 0,
|
|
821
|
+
f: Infinity,
|
|
822
|
+
parent: null,
|
|
823
|
+
closed: false,
|
|
824
|
+
opened: false,
|
|
825
|
+
heapIndex: -1
|
|
826
|
+
};
|
|
827
|
+
session.nodeCache.set(neighborNode.id, neighbor);
|
|
828
|
+
}
|
|
829
|
+
if (neighbor.closed) {
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
const movementCost = this.map.getMovementCost(current.node, neighborNode);
|
|
833
|
+
const tentativeG = current.g + movementCost;
|
|
834
|
+
if (!neighbor.opened) {
|
|
835
|
+
neighbor.g = tentativeG;
|
|
836
|
+
neighbor.h = this.map.heuristic(neighborNode.position, session.endPosition) * session.options.heuristicWeight;
|
|
837
|
+
neighbor.f = neighbor.g + neighbor.h;
|
|
838
|
+
neighbor.parent = current;
|
|
839
|
+
neighbor.opened = true;
|
|
840
|
+
session.openList.push(neighbor);
|
|
841
|
+
} else if (tentativeG < neighbor.g) {
|
|
842
|
+
neighbor.g = tentativeG;
|
|
843
|
+
neighbor.f = neighbor.g + neighbor.h;
|
|
844
|
+
neighbor.parent = current;
|
|
845
|
+
session.openList.update(neighbor);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* @zh 创建进度对象
|
|
851
|
+
* @en Create progress object
|
|
852
|
+
*/
|
|
853
|
+
createProgress(session) {
|
|
854
|
+
let estimatedProgress = 0;
|
|
855
|
+
if (session.state === PathfindingState.Completed) {
|
|
856
|
+
estimatedProgress = 1;
|
|
857
|
+
} else if (session.state === PathfindingState.InProgress && session.initialDistance > 0) {
|
|
858
|
+
const bestNode = session.openList.peek();
|
|
859
|
+
if (bestNode) {
|
|
860
|
+
const currentDistance = bestNode.h / session.options.heuristicWeight;
|
|
861
|
+
estimatedProgress = Math.max(0, Math.min(1, 1 - currentDistance / session.initialDistance));
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
return {
|
|
865
|
+
state: session.state,
|
|
866
|
+
nodesSearched: session.nodesSearched,
|
|
867
|
+
openListSize: session.openList.size,
|
|
868
|
+
estimatedProgress
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* @zh 构建路径结果
|
|
873
|
+
* @en Build path result
|
|
874
|
+
*/
|
|
875
|
+
buildResult(session, endNode) {
|
|
876
|
+
const path = [];
|
|
877
|
+
let current = endNode;
|
|
878
|
+
while (current) {
|
|
879
|
+
path.push(current.node.position);
|
|
880
|
+
current = current.parent;
|
|
881
|
+
}
|
|
882
|
+
path.reverse();
|
|
883
|
+
return {
|
|
884
|
+
requestId: session.request.id,
|
|
885
|
+
found: true,
|
|
886
|
+
path,
|
|
887
|
+
cost: endNode.g,
|
|
888
|
+
nodesSearched: session.nodesSearched,
|
|
889
|
+
framesUsed: session.framesUsed,
|
|
890
|
+
isPartial: false
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* @zh 创建空结果
|
|
895
|
+
* @en Create empty result
|
|
896
|
+
*/
|
|
897
|
+
createEmptyResult(requestId) {
|
|
898
|
+
return {
|
|
899
|
+
requestId,
|
|
900
|
+
found: false,
|
|
901
|
+
path: [],
|
|
902
|
+
cost: 0,
|
|
903
|
+
nodesSearched: 0,
|
|
904
|
+
framesUsed: 0,
|
|
905
|
+
isPartial: false
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* @zh 检查会话是否被区域影响
|
|
910
|
+
* @en Check if session is affected by region
|
|
911
|
+
*/
|
|
912
|
+
sessionAffectedByRegion(session, region) {
|
|
913
|
+
for (const astarNode of session.nodeCache.values()) {
|
|
914
|
+
if (astarNode.opened || astarNode.closed) {
|
|
915
|
+
const pos = astarNode.node.position;
|
|
916
|
+
if (pos.x >= region.minX && pos.x <= region.maxX && pos.y >= region.minY && pos.y <= region.maxY) {
|
|
917
|
+
return true;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
const start = session.request;
|
|
922
|
+
const end = session.endPosition;
|
|
923
|
+
if (start.startX >= region.minX && start.startX <= region.maxX && start.startY >= region.minY && start.startY <= region.maxY || end.x >= region.minX && end.x <= region.maxX && end.y >= region.minY && end.y <= region.maxY) {
|
|
924
|
+
return true;
|
|
925
|
+
}
|
|
926
|
+
return false;
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* @zh 清理过期的变化区域
|
|
930
|
+
* @en Clean up expired change regions
|
|
931
|
+
*/
|
|
932
|
+
cleanupOldRegions() {
|
|
933
|
+
const now = Date.now();
|
|
934
|
+
let i = 0;
|
|
935
|
+
while (i < this.affectedRegions.length) {
|
|
936
|
+
if (now - this.affectedRegions[i].timestamp > this.maxRegionAge) {
|
|
937
|
+
this.affectedRegions.splice(i, 1);
|
|
938
|
+
} else {
|
|
939
|
+
i++;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
};
|
|
944
|
+
__name(_IncrementalAStarPathfinder, "IncrementalAStarPathfinder");
|
|
945
|
+
var IncrementalAStarPathfinder = _IncrementalAStarPathfinder;
|
|
946
|
+
function createIncrementalAStarPathfinder(map) {
|
|
947
|
+
return new IncrementalAStarPathfinder(map);
|
|
948
|
+
}
|
|
949
|
+
__name(createIncrementalAStarPathfinder, "createIncrementalAStarPathfinder");
|
|
950
|
+
|
|
951
|
+
// src/core/PathValidator.ts
|
|
952
|
+
var _PathValidator = class _PathValidator {
|
|
953
|
+
/**
|
|
954
|
+
* @zh 验证路径段的有效性
|
|
955
|
+
* @en Validate path segment validity
|
|
956
|
+
*
|
|
957
|
+
* @param path - @zh 要验证的路径 @en Path to validate
|
|
958
|
+
* @param fromIndex - @zh 起始索引 @en Start index
|
|
959
|
+
* @param toIndex - @zh 结束索引 @en End index
|
|
960
|
+
* @param map - @zh 地图实例 @en Map instance
|
|
961
|
+
* @returns @zh 验证结果 @en Validation result
|
|
962
|
+
*/
|
|
963
|
+
validatePath(path, fromIndex, toIndex, map) {
|
|
964
|
+
const end = Math.min(toIndex, path.length);
|
|
965
|
+
for (let i = fromIndex; i < end; i++) {
|
|
966
|
+
const point = path[i];
|
|
967
|
+
const x = Math.floor(point.x);
|
|
968
|
+
const y = Math.floor(point.y);
|
|
969
|
+
if (!map.isWalkable(x, y)) {
|
|
970
|
+
return {
|
|
971
|
+
valid: false,
|
|
972
|
+
invalidIndex: i
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
if (i > fromIndex) {
|
|
976
|
+
const prev = path[i - 1];
|
|
977
|
+
if (!this.checkLineOfSight(prev.x, prev.y, point.x, point.y, map)) {
|
|
978
|
+
return {
|
|
979
|
+
valid: false,
|
|
980
|
+
invalidIndex: i
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
return {
|
|
986
|
+
valid: true,
|
|
987
|
+
invalidIndex: -1
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* @zh 检查两点之间的视线(使用 Bresenham 算法)
|
|
992
|
+
* @en Check line of sight between two points (using Bresenham algorithm)
|
|
993
|
+
*
|
|
994
|
+
* @param x1 - @zh 起点 X @en Start X
|
|
995
|
+
* @param y1 - @zh 起点 Y @en Start Y
|
|
996
|
+
* @param x2 - @zh 终点 X @en End X
|
|
997
|
+
* @param y2 - @zh 终点 Y @en End Y
|
|
998
|
+
* @param map - @zh 地图实例 @en Map instance
|
|
999
|
+
* @returns @zh 是否有视线 @en Whether there is line of sight
|
|
1000
|
+
*/
|
|
1001
|
+
checkLineOfSight(x1, y1, x2, y2, map) {
|
|
1002
|
+
const ix1 = Math.floor(x1);
|
|
1003
|
+
const iy1 = Math.floor(y1);
|
|
1004
|
+
const ix2 = Math.floor(x2);
|
|
1005
|
+
const iy2 = Math.floor(y2);
|
|
1006
|
+
let dx = Math.abs(ix2 - ix1);
|
|
1007
|
+
let dy = Math.abs(iy2 - iy1);
|
|
1008
|
+
let x = ix1;
|
|
1009
|
+
let y = iy1;
|
|
1010
|
+
const sx = ix1 < ix2 ? 1 : -1;
|
|
1011
|
+
const sy = iy1 < iy2 ? 1 : -1;
|
|
1012
|
+
if (dx > dy) {
|
|
1013
|
+
let err = dx / 2;
|
|
1014
|
+
while (x !== ix2) {
|
|
1015
|
+
if (!map.isWalkable(x, y)) {
|
|
1016
|
+
return false;
|
|
1017
|
+
}
|
|
1018
|
+
err -= dy;
|
|
1019
|
+
if (err < 0) {
|
|
1020
|
+
y += sy;
|
|
1021
|
+
err += dx;
|
|
1022
|
+
}
|
|
1023
|
+
x += sx;
|
|
1024
|
+
}
|
|
1025
|
+
} else {
|
|
1026
|
+
let err = dy / 2;
|
|
1027
|
+
while (y !== iy2) {
|
|
1028
|
+
if (!map.isWalkable(x, y)) {
|
|
1029
|
+
return false;
|
|
1030
|
+
}
|
|
1031
|
+
err -= dx;
|
|
1032
|
+
if (err < 0) {
|
|
1033
|
+
x += sx;
|
|
1034
|
+
err += dy;
|
|
1035
|
+
}
|
|
1036
|
+
y += sy;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
return map.isWalkable(ix2, iy2);
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
1042
|
+
__name(_PathValidator, "PathValidator");
|
|
1043
|
+
var PathValidator = _PathValidator;
|
|
1044
|
+
var _ObstacleChangeManager = class _ObstacleChangeManager {
|
|
1045
|
+
constructor() {
|
|
1046
|
+
__publicField(this, "changes", /* @__PURE__ */ new Map());
|
|
1047
|
+
__publicField(this, "epoch", 0);
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* @zh 记录障碍物变化
|
|
1051
|
+
* @en Record obstacle change
|
|
1052
|
+
*
|
|
1053
|
+
* @param x - @zh X 坐标 @en X coordinate
|
|
1054
|
+
* @param y - @zh Y 坐标 @en Y coordinate
|
|
1055
|
+
* @param wasWalkable - @zh 变化前是否可通行 @en Was walkable before change
|
|
1056
|
+
*/
|
|
1057
|
+
recordChange(x, y, wasWalkable) {
|
|
1058
|
+
const key = `${x},${y}`;
|
|
1059
|
+
this.changes.set(key, {
|
|
1060
|
+
x,
|
|
1061
|
+
y,
|
|
1062
|
+
wasWalkable,
|
|
1063
|
+
timestamp: Date.now()
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* @zh 获取影响区域
|
|
1068
|
+
* @en Get affected region
|
|
1069
|
+
*
|
|
1070
|
+
* @returns @zh 影响区域或 null(如果没有变化)@en Affected region or null if no changes
|
|
1071
|
+
*/
|
|
1072
|
+
getAffectedRegion() {
|
|
1073
|
+
if (this.changes.size === 0) {
|
|
1074
|
+
return null;
|
|
1075
|
+
}
|
|
1076
|
+
let minX = Infinity;
|
|
1077
|
+
let minY = Infinity;
|
|
1078
|
+
let maxX = -Infinity;
|
|
1079
|
+
let maxY = -Infinity;
|
|
1080
|
+
for (const change of this.changes.values()) {
|
|
1081
|
+
minX = Math.min(minX, change.x);
|
|
1082
|
+
minY = Math.min(minY, change.y);
|
|
1083
|
+
maxX = Math.max(maxX, change.x);
|
|
1084
|
+
maxY = Math.max(maxY, change.y);
|
|
1085
|
+
}
|
|
1086
|
+
return {
|
|
1087
|
+
minX,
|
|
1088
|
+
minY,
|
|
1089
|
+
maxX,
|
|
1090
|
+
maxY
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* @zh 获取所有变化
|
|
1095
|
+
* @en Get all changes
|
|
1096
|
+
*
|
|
1097
|
+
* @returns @zh 变化列表 @en List of changes
|
|
1098
|
+
*/
|
|
1099
|
+
getChanges() {
|
|
1100
|
+
return Array.from(this.changes.values());
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* @zh 检查是否有变化
|
|
1104
|
+
* @en Check if there are changes
|
|
1105
|
+
*
|
|
1106
|
+
* @returns @zh 是否有变化 @en Whether there are changes
|
|
1107
|
+
*/
|
|
1108
|
+
hasChanges() {
|
|
1109
|
+
return this.changes.size > 0;
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* @zh 获取当前 epoch
|
|
1113
|
+
* @en Get current epoch
|
|
1114
|
+
*
|
|
1115
|
+
* @returns @zh 当前 epoch @en Current epoch
|
|
1116
|
+
*/
|
|
1117
|
+
getEpoch() {
|
|
1118
|
+
return this.epoch;
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* @zh 清空变化记录并推进 epoch
|
|
1122
|
+
* @en Clear changes and advance epoch
|
|
1123
|
+
*/
|
|
1124
|
+
flush() {
|
|
1125
|
+
this.changes.clear();
|
|
1126
|
+
this.epoch++;
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* @zh 清空所有状态
|
|
1130
|
+
* @en Clear all state
|
|
1131
|
+
*/
|
|
1132
|
+
clear() {
|
|
1133
|
+
this.changes.clear();
|
|
1134
|
+
this.epoch = 0;
|
|
1135
|
+
}
|
|
1136
|
+
};
|
|
1137
|
+
__name(_ObstacleChangeManager, "ObstacleChangeManager");
|
|
1138
|
+
var ObstacleChangeManager = _ObstacleChangeManager;
|
|
1139
|
+
function createPathValidator() {
|
|
1140
|
+
return new PathValidator();
|
|
1141
|
+
}
|
|
1142
|
+
__name(createPathValidator, "createPathValidator");
|
|
1143
|
+
function createObstacleChangeManager() {
|
|
1144
|
+
return new ObstacleChangeManager();
|
|
1145
|
+
}
|
|
1146
|
+
__name(createObstacleChangeManager, "createObstacleChangeManager");
|
|
1147
|
+
|
|
1148
|
+
// src/grid/GridMap.ts
|
|
1149
|
+
var _GridNode = class _GridNode {
|
|
1150
|
+
constructor(x, y, width, walkable = true, cost = 1) {
|
|
1151
|
+
__publicField(this, "id");
|
|
1152
|
+
__publicField(this, "position");
|
|
1153
|
+
__publicField(this, "x");
|
|
1154
|
+
__publicField(this, "y");
|
|
1155
|
+
__publicField(this, "cost");
|
|
1156
|
+
__publicField(this, "walkable");
|
|
1157
|
+
this.x = x;
|
|
1158
|
+
this.y = y;
|
|
1159
|
+
this.id = y * width + x;
|
|
1160
|
+
this.position = createPoint(x, y);
|
|
1161
|
+
this.walkable = walkable;
|
|
1162
|
+
this.cost = cost;
|
|
1163
|
+
}
|
|
1164
|
+
};
|
|
1165
|
+
__name(_GridNode, "GridNode");
|
|
1166
|
+
var GridNode = _GridNode;
|
|
1167
|
+
var DIRECTIONS_4 = [
|
|
1168
|
+
{
|
|
1169
|
+
dx: 0,
|
|
1170
|
+
dy: -1
|
|
1171
|
+
},
|
|
1172
|
+
{
|
|
1173
|
+
dx: 1,
|
|
1174
|
+
dy: 0
|
|
1175
|
+
},
|
|
1176
|
+
{
|
|
1177
|
+
dx: 0,
|
|
1178
|
+
dy: 1
|
|
1179
|
+
},
|
|
1180
|
+
{
|
|
1181
|
+
dx: -1,
|
|
1182
|
+
dy: 0
|
|
1183
|
+
}
|
|
1184
|
+
// Left
|
|
1185
|
+
];
|
|
1186
|
+
var DIRECTIONS_8 = [
|
|
1187
|
+
{
|
|
1188
|
+
dx: 0,
|
|
1189
|
+
dy: -1
|
|
1190
|
+
},
|
|
1191
|
+
{
|
|
1192
|
+
dx: 1,
|
|
1193
|
+
dy: -1
|
|
1194
|
+
},
|
|
1195
|
+
{
|
|
1196
|
+
dx: 1,
|
|
1197
|
+
dy: 0
|
|
1198
|
+
},
|
|
1199
|
+
{
|
|
1200
|
+
dx: 1,
|
|
1201
|
+
dy: 1
|
|
1202
|
+
},
|
|
1203
|
+
{
|
|
1204
|
+
dx: 0,
|
|
1205
|
+
dy: 1
|
|
1206
|
+
},
|
|
1207
|
+
{
|
|
1208
|
+
dx: -1,
|
|
1209
|
+
dy: 1
|
|
1210
|
+
},
|
|
1211
|
+
{
|
|
1212
|
+
dx: -1,
|
|
1213
|
+
dy: 0
|
|
1214
|
+
},
|
|
1215
|
+
{
|
|
1216
|
+
dx: -1,
|
|
1217
|
+
dy: -1
|
|
1218
|
+
}
|
|
1219
|
+
// Up-Left
|
|
1220
|
+
];
|
|
1221
|
+
var DEFAULT_GRID_OPTIONS = {
|
|
1222
|
+
allowDiagonal: true,
|
|
1223
|
+
diagonalCost: Math.SQRT2,
|
|
1224
|
+
avoidCorners: true,
|
|
1225
|
+
heuristic: octileDistance
|
|
1226
|
+
};
|
|
1227
|
+
var _GridMap = class _GridMap {
|
|
1228
|
+
constructor(width, height, options) {
|
|
1229
|
+
__publicField(this, "width");
|
|
1230
|
+
__publicField(this, "height");
|
|
1231
|
+
__publicField(this, "nodes");
|
|
1232
|
+
__publicField(this, "options");
|
|
1233
|
+
this.width = width;
|
|
1234
|
+
this.height = height;
|
|
1235
|
+
this.options = {
|
|
1236
|
+
...DEFAULT_GRID_OPTIONS,
|
|
1237
|
+
...options
|
|
1238
|
+
};
|
|
1239
|
+
this.nodes = this.createNodes();
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* @zh 创建网格节点
|
|
1243
|
+
* @en Create grid nodes
|
|
1244
|
+
*/
|
|
1245
|
+
createNodes() {
|
|
1246
|
+
const nodes = [];
|
|
1247
|
+
for (let y = 0; y < this.height; y++) {
|
|
1248
|
+
nodes[y] = [];
|
|
1249
|
+
for (let x = 0; x < this.width; x++) {
|
|
1250
|
+
nodes[y][x] = new GridNode(x, y, this.width, true, 1);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
return nodes;
|
|
1254
|
+
}
|
|
1255
|
+
/**
|
|
1256
|
+
* @zh 获取指定位置的节点
|
|
1257
|
+
* @en Get node at position
|
|
1258
|
+
*/
|
|
1259
|
+
getNodeAt(x, y) {
|
|
1260
|
+
if (!this.isInBounds(x, y)) {
|
|
1261
|
+
return null;
|
|
1262
|
+
}
|
|
1263
|
+
return this.nodes[y][x];
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1266
|
+
* @zh 检查坐标是否在边界内
|
|
1267
|
+
* @en Check if coordinates are within bounds
|
|
1268
|
+
*/
|
|
1269
|
+
isInBounds(x, y) {
|
|
1270
|
+
return x >= 0 && x < this.width && y >= 0 && y < this.height;
|
|
1271
|
+
}
|
|
1272
|
+
/**
|
|
1273
|
+
* @zh 检查位置是否可通行
|
|
1274
|
+
* @en Check if position is walkable
|
|
1275
|
+
*/
|
|
1276
|
+
isWalkable(x, y) {
|
|
1277
|
+
const node = this.getNodeAt(x, y);
|
|
1278
|
+
return node !== null && node.walkable;
|
|
1279
|
+
}
|
|
1280
|
+
/**
|
|
1281
|
+
* @zh 设置位置是否可通行
|
|
1282
|
+
* @en Set position walkability
|
|
1283
|
+
*/
|
|
1284
|
+
setWalkable(x, y, walkable) {
|
|
1285
|
+
const node = this.getNodeAt(x, y);
|
|
1286
|
+
if (node) {
|
|
1287
|
+
node.walkable = walkable;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* @zh 设置位置的移动代价
|
|
1292
|
+
* @en Set movement cost at position
|
|
1293
|
+
*/
|
|
1294
|
+
setCost(x, y, cost) {
|
|
1295
|
+
const node = this.getNodeAt(x, y);
|
|
1296
|
+
if (node) {
|
|
1297
|
+
node.cost = cost;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
/**
|
|
1301
|
+
* @zh 获取节点的邻居
|
|
1302
|
+
* @en Get neighbors of a node
|
|
1303
|
+
*/
|
|
1304
|
+
getNeighbors(node) {
|
|
1305
|
+
const neighbors = [];
|
|
1306
|
+
const { x, y } = node.position;
|
|
1307
|
+
const directions = this.options.allowDiagonal ? DIRECTIONS_8 : DIRECTIONS_4;
|
|
1308
|
+
for (let i = 0; i < directions.length; i++) {
|
|
1309
|
+
const dir = directions[i];
|
|
1310
|
+
const nx = x + dir.dx;
|
|
1311
|
+
const ny = y + dir.dy;
|
|
1312
|
+
if (nx < 0 || nx >= this.width || ny < 0 || ny >= this.height) {
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
const neighbor = this.nodes[ny][nx];
|
|
1316
|
+
if (!neighbor.walkable) {
|
|
1317
|
+
continue;
|
|
1318
|
+
}
|
|
1319
|
+
if (this.options.avoidCorners && dir.dx !== 0 && dir.dy !== 0) {
|
|
1320
|
+
const hNode = this.nodes[y][x + dir.dx];
|
|
1321
|
+
const vNode = this.nodes[y + dir.dy][x];
|
|
1322
|
+
if (!hNode.walkable || !vNode.walkable) {
|
|
1323
|
+
continue;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
neighbors.push(neighbor);
|
|
1327
|
+
}
|
|
1328
|
+
return neighbors;
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* @zh 遍历节点的邻居(零分配)
|
|
1332
|
+
* @en Iterate over neighbors (zero allocation)
|
|
1333
|
+
*/
|
|
1334
|
+
forEachNeighbor(node, callback) {
|
|
1335
|
+
const { x, y } = node.position;
|
|
1336
|
+
const directions = this.options.allowDiagonal ? DIRECTIONS_8 : DIRECTIONS_4;
|
|
1337
|
+
for (let i = 0; i < directions.length; i++) {
|
|
1338
|
+
const dir = directions[i];
|
|
1339
|
+
const nx = x + dir.dx;
|
|
1340
|
+
const ny = y + dir.dy;
|
|
1341
|
+
if (nx < 0 || nx >= this.width || ny < 0 || ny >= this.height) {
|
|
1342
|
+
continue;
|
|
1343
|
+
}
|
|
1344
|
+
const neighbor = this.nodes[ny][nx];
|
|
1345
|
+
if (!neighbor.walkable) {
|
|
1346
|
+
continue;
|
|
1347
|
+
}
|
|
1348
|
+
if (this.options.avoidCorners && dir.dx !== 0 && dir.dy !== 0) {
|
|
1349
|
+
const hNode = this.nodes[y][x + dir.dx];
|
|
1350
|
+
const vNode = this.nodes[y + dir.dy][x];
|
|
1351
|
+
if (!hNode.walkable || !vNode.walkable) {
|
|
1352
|
+
continue;
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
if (callback(neighbor) === false) {
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* @zh 计算启发式距离
|
|
1362
|
+
* @en Calculate heuristic distance
|
|
1363
|
+
*/
|
|
1364
|
+
heuristic(a, b) {
|
|
1365
|
+
return this.options.heuristic(a, b);
|
|
1366
|
+
}
|
|
1367
|
+
/**
|
|
1368
|
+
* @zh 计算移动代价
|
|
1369
|
+
* @en Calculate movement cost
|
|
1370
|
+
*/
|
|
1371
|
+
getMovementCost(from, to) {
|
|
1372
|
+
const dx = Math.abs(from.position.x - to.position.x);
|
|
1373
|
+
const dy = Math.abs(from.position.y - to.position.y);
|
|
1374
|
+
if (dx !== 0 && dy !== 0) {
|
|
1375
|
+
return to.cost * this.options.diagonalCost;
|
|
1376
|
+
}
|
|
1377
|
+
return to.cost;
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* @zh 从二维数组加载地图
|
|
1381
|
+
* @en Load map from 2D array
|
|
1382
|
+
*
|
|
1383
|
+
* @param data - @zh 0=可通行,非0=不可通行 @en 0=walkable, non-0=blocked
|
|
1384
|
+
*/
|
|
1385
|
+
loadFromArray(data) {
|
|
1386
|
+
for (let y = 0; y < Math.min(data.length, this.height); y++) {
|
|
1387
|
+
for (let x = 0; x < Math.min(data[y].length, this.width); x++) {
|
|
1388
|
+
this.nodes[y][x].walkable = data[y][x] === 0;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* @zh 从字符串加载地图
|
|
1394
|
+
* @en Load map from string
|
|
1395
|
+
*
|
|
1396
|
+
* @param str - @zh 地图字符串,'.'=可通行,'#'=障碍 @en Map string, '.'=walkable, '#'=blocked
|
|
1397
|
+
*/
|
|
1398
|
+
loadFromString(str) {
|
|
1399
|
+
const lines = str.trim().split("\n");
|
|
1400
|
+
for (let y = 0; y < Math.min(lines.length, this.height); y++) {
|
|
1401
|
+
const line = lines[y];
|
|
1402
|
+
for (let x = 0; x < Math.min(line.length, this.width); x++) {
|
|
1403
|
+
this.nodes[y][x].walkable = line[x] !== "#";
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* @zh 导出为字符串
|
|
1409
|
+
* @en Export to string
|
|
1410
|
+
*/
|
|
1411
|
+
toString() {
|
|
1412
|
+
let result = "";
|
|
1413
|
+
for (let y = 0; y < this.height; y++) {
|
|
1414
|
+
for (let x = 0; x < this.width; x++) {
|
|
1415
|
+
result += this.nodes[y][x].walkable ? "." : "#";
|
|
1416
|
+
}
|
|
1417
|
+
result += "\n";
|
|
1418
|
+
}
|
|
1419
|
+
return result;
|
|
1420
|
+
}
|
|
1421
|
+
/**
|
|
1422
|
+
* @zh 重置所有节点为可通行
|
|
1423
|
+
* @en Reset all nodes to walkable
|
|
1424
|
+
*/
|
|
1425
|
+
reset() {
|
|
1426
|
+
for (let y = 0; y < this.height; y++) {
|
|
1427
|
+
for (let x = 0; x < this.width; x++) {
|
|
1428
|
+
this.nodes[y][x].walkable = true;
|
|
1429
|
+
this.nodes[y][x].cost = 1;
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
/**
|
|
1434
|
+
* @zh 设置矩形区域的通行性
|
|
1435
|
+
* @en Set walkability for a rectangle region
|
|
1436
|
+
*/
|
|
1437
|
+
setRectWalkable(x, y, width, height, walkable) {
|
|
1438
|
+
for (let dy = 0; dy < height; dy++) {
|
|
1439
|
+
for (let dx = 0; dx < width; dx++) {
|
|
1440
|
+
this.setWalkable(x + dx, y + dy, walkable);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
};
|
|
1445
|
+
__name(_GridMap, "GridMap");
|
|
1446
|
+
var GridMap = _GridMap;
|
|
1447
|
+
function createGridMap(width, height, options) {
|
|
1448
|
+
return new GridMap(width, height, options);
|
|
1449
|
+
}
|
|
1450
|
+
__name(createGridMap, "createGridMap");
|
|
1451
|
+
|
|
1452
|
+
// src/smoothing/PathSmoother.ts
|
|
1453
|
+
function bresenhamLineOfSight(x1, y1, x2, y2, map) {
|
|
1454
|
+
let ix1 = Math.floor(x1);
|
|
1455
|
+
let iy1 = Math.floor(y1);
|
|
1456
|
+
const ix2 = Math.floor(x2);
|
|
1457
|
+
const iy2 = Math.floor(y2);
|
|
1458
|
+
const dx = Math.abs(ix2 - ix1);
|
|
1459
|
+
const dy = Math.abs(iy2 - iy1);
|
|
1460
|
+
const sx = ix1 < ix2 ? 1 : -1;
|
|
1461
|
+
const sy = iy1 < iy2 ? 1 : -1;
|
|
1462
|
+
let err = dx - dy;
|
|
1463
|
+
while (true) {
|
|
1464
|
+
if (!map.isWalkable(ix1, iy1)) {
|
|
1465
|
+
return false;
|
|
1466
|
+
}
|
|
1467
|
+
if (ix1 === ix2 && iy1 === iy2) {
|
|
1468
|
+
break;
|
|
1469
|
+
}
|
|
1470
|
+
const e2 = 2 * err;
|
|
1471
|
+
if (e2 > -dy) {
|
|
1472
|
+
err -= dy;
|
|
1473
|
+
ix1 += sx;
|
|
1474
|
+
}
|
|
1475
|
+
if (e2 < dx) {
|
|
1476
|
+
err += dx;
|
|
1477
|
+
iy1 += sy;
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
return true;
|
|
1481
|
+
}
|
|
1482
|
+
__name(bresenhamLineOfSight, "bresenhamLineOfSight");
|
|
1483
|
+
function raycastLineOfSight(x1, y1, x2, y2, map, stepSize = 0.5) {
|
|
1484
|
+
const dx = x2 - x1;
|
|
1485
|
+
const dy = y2 - y1;
|
|
1486
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
1487
|
+
if (distance === 0) {
|
|
1488
|
+
return map.isWalkable(Math.floor(x1), Math.floor(y1));
|
|
1489
|
+
}
|
|
1490
|
+
const steps = Math.ceil(distance / stepSize);
|
|
1491
|
+
const stepX = dx / steps;
|
|
1492
|
+
const stepY = dy / steps;
|
|
1493
|
+
let x = x1;
|
|
1494
|
+
let y = y1;
|
|
1495
|
+
for (let i = 0; i <= steps; i++) {
|
|
1496
|
+
if (!map.isWalkable(Math.floor(x), Math.floor(y))) {
|
|
1497
|
+
return false;
|
|
1498
|
+
}
|
|
1499
|
+
x += stepX;
|
|
1500
|
+
y += stepY;
|
|
1501
|
+
}
|
|
1502
|
+
return true;
|
|
1503
|
+
}
|
|
1504
|
+
__name(raycastLineOfSight, "raycastLineOfSight");
|
|
1505
|
+
var _LineOfSightSmoother = class _LineOfSightSmoother {
|
|
1506
|
+
constructor(lineOfSight = bresenhamLineOfSight) {
|
|
1507
|
+
__publicField(this, "lineOfSight");
|
|
1508
|
+
this.lineOfSight = lineOfSight;
|
|
1509
|
+
}
|
|
1510
|
+
smooth(path, map) {
|
|
1511
|
+
if (path.length <= 2) {
|
|
1512
|
+
return [
|
|
1513
|
+
...path
|
|
1514
|
+
];
|
|
1515
|
+
}
|
|
1516
|
+
const result = [
|
|
1517
|
+
path[0]
|
|
1518
|
+
];
|
|
1519
|
+
let current = 0;
|
|
1520
|
+
while (current < path.length - 1) {
|
|
1521
|
+
let furthest = current + 1;
|
|
1522
|
+
for (let i = path.length - 1; i > current + 1; i--) {
|
|
1523
|
+
if (this.lineOfSight(path[current].x, path[current].y, path[i].x, path[i].y, map)) {
|
|
1524
|
+
furthest = i;
|
|
1525
|
+
break;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
result.push(path[furthest]);
|
|
1529
|
+
current = furthest;
|
|
1530
|
+
}
|
|
1531
|
+
return result;
|
|
1532
|
+
}
|
|
1533
|
+
};
|
|
1534
|
+
__name(_LineOfSightSmoother, "LineOfSightSmoother");
|
|
1535
|
+
var LineOfSightSmoother = _LineOfSightSmoother;
|
|
1536
|
+
var _CatmullRomSmoother = class _CatmullRomSmoother {
|
|
1537
|
+
/**
|
|
1538
|
+
* @param segments - @zh 每段之间的插值点数 @en Number of interpolation points per segment
|
|
1539
|
+
* @param tension - @zh 张力 (0-1) @en Tension (0-1)
|
|
1540
|
+
*/
|
|
1541
|
+
constructor(segments = 5, tension = 0.5) {
|
|
1542
|
+
__publicField(this, "segments");
|
|
1543
|
+
__publicField(this, "tension");
|
|
1544
|
+
this.segments = segments;
|
|
1545
|
+
this.tension = tension;
|
|
1546
|
+
}
|
|
1547
|
+
smooth(path, _map) {
|
|
1548
|
+
if (path.length <= 2) {
|
|
1549
|
+
return [
|
|
1550
|
+
...path
|
|
1551
|
+
];
|
|
1552
|
+
}
|
|
1553
|
+
const result = [];
|
|
1554
|
+
const points = [
|
|
1555
|
+
path[0],
|
|
1556
|
+
...path,
|
|
1557
|
+
path[path.length - 1]
|
|
1558
|
+
];
|
|
1559
|
+
for (let i = 1; i < points.length - 2; i++) {
|
|
1560
|
+
const p0 = points[i - 1];
|
|
1561
|
+
const p1 = points[i];
|
|
1562
|
+
const p2 = points[i + 1];
|
|
1563
|
+
const p3 = points[i + 2];
|
|
1564
|
+
for (let j = 0; j < this.segments; j++) {
|
|
1565
|
+
const t = j / this.segments;
|
|
1566
|
+
const point = this.interpolate(p0, p1, p2, p3, t);
|
|
1567
|
+
result.push(point);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
result.push(path[path.length - 1]);
|
|
1571
|
+
return result;
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* @zh Catmull-Rom 插值
|
|
1575
|
+
* @en Catmull-Rom interpolation
|
|
1576
|
+
*/
|
|
1577
|
+
interpolate(p0, p1, p2, p3, t) {
|
|
1578
|
+
const t2 = t * t;
|
|
1579
|
+
const t3 = t2 * t;
|
|
1580
|
+
const tension = this.tension;
|
|
1581
|
+
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);
|
|
1582
|
+
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);
|
|
1583
|
+
return createPoint(x, y);
|
|
1584
|
+
}
|
|
1585
|
+
};
|
|
1586
|
+
__name(_CatmullRomSmoother, "CatmullRomSmoother");
|
|
1587
|
+
var CatmullRomSmoother = _CatmullRomSmoother;
|
|
1588
|
+
var _CombinedSmoother = class _CombinedSmoother {
|
|
1589
|
+
constructor(curveSegments = 5, tension = 0.5) {
|
|
1590
|
+
__publicField(this, "simplifier");
|
|
1591
|
+
__publicField(this, "curveSmoother");
|
|
1592
|
+
this.simplifier = new LineOfSightSmoother();
|
|
1593
|
+
this.curveSmoother = new CatmullRomSmoother(curveSegments, tension);
|
|
1594
|
+
}
|
|
1595
|
+
smooth(path, map) {
|
|
1596
|
+
const simplified = this.simplifier.smooth(path, map);
|
|
1597
|
+
return this.curveSmoother.smooth(simplified, map);
|
|
1598
|
+
}
|
|
1599
|
+
};
|
|
1600
|
+
__name(_CombinedSmoother, "CombinedSmoother");
|
|
1601
|
+
var CombinedSmoother = _CombinedSmoother;
|
|
1602
|
+
function createLineOfSightSmoother(lineOfSight) {
|
|
1603
|
+
return new LineOfSightSmoother(lineOfSight);
|
|
1604
|
+
}
|
|
1605
|
+
__name(createLineOfSightSmoother, "createLineOfSightSmoother");
|
|
1606
|
+
function createCatmullRomSmoother(segments, tension) {
|
|
1607
|
+
return new CatmullRomSmoother(segments, tension);
|
|
1608
|
+
}
|
|
1609
|
+
__name(createCatmullRomSmoother, "createCatmullRomSmoother");
|
|
1610
|
+
function createCombinedSmoother(curveSegments, tension) {
|
|
1611
|
+
return new CombinedSmoother(curveSegments, tension);
|
|
1612
|
+
}
|
|
1613
|
+
__name(createCombinedSmoother, "createCombinedSmoother");
|
|
1614
|
+
|
|
1615
|
+
export {
|
|
1616
|
+
createPoint,
|
|
1617
|
+
EMPTY_PATH_RESULT,
|
|
1618
|
+
manhattanDistance,
|
|
1619
|
+
euclideanDistance,
|
|
1620
|
+
chebyshevDistance,
|
|
1621
|
+
octileDistance,
|
|
1622
|
+
DEFAULT_PATHFINDING_OPTIONS,
|
|
1623
|
+
IndexedBinaryHeap,
|
|
1624
|
+
DEFAULT_PATH_CACHE_CONFIG,
|
|
1625
|
+
PathCache,
|
|
1626
|
+
createPathCache,
|
|
1627
|
+
IncrementalAStarPathfinder,
|
|
1628
|
+
createIncrementalAStarPathfinder,
|
|
1629
|
+
PathValidator,
|
|
1630
|
+
ObstacleChangeManager,
|
|
1631
|
+
createPathValidator,
|
|
1632
|
+
createObstacleChangeManager,
|
|
1633
|
+
GridNode,
|
|
1634
|
+
DIRECTIONS_4,
|
|
1635
|
+
DIRECTIONS_8,
|
|
1636
|
+
DEFAULT_GRID_OPTIONS,
|
|
1637
|
+
GridMap,
|
|
1638
|
+
createGridMap,
|
|
1639
|
+
bresenhamLineOfSight,
|
|
1640
|
+
raycastLineOfSight,
|
|
1641
|
+
LineOfSightSmoother,
|
|
1642
|
+
CatmullRomSmoother,
|
|
1643
|
+
CombinedSmoother,
|
|
1644
|
+
createLineOfSightSmoother,
|
|
1645
|
+
createCatmullRomSmoother,
|
|
1646
|
+
createCombinedSmoother
|
|
1647
|
+
};
|
|
1648
|
+
//# sourceMappingURL=chunk-TPT7Q3E3.js.map
|