@esengine/pathfinding 12.0.0 → 12.1.0

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.
@@ -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