@esengine/pathfinding 13.0.0 → 13.2.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.
package/dist/index.js CHANGED
@@ -1,9 +1,7 @@
1
1
  import {
2
2
  CatmullRomSmoother,
3
3
  CombinedSmoother,
4
- DEFAULT_AGENT_PARAMS,
5
4
  DEFAULT_GRID_OPTIONS,
6
- DEFAULT_ORCA_CONFIG,
7
5
  DEFAULT_PATHFINDING_OPTIONS,
8
6
  DEFAULT_PATH_CACHE_CONFIG,
9
7
  DIRECTIONS_4,
@@ -13,9 +11,7 @@ import {
13
11
  GridNode,
14
12
  IncrementalAStarPathfinder,
15
13
  IndexedBinaryHeap,
16
- KDTree,
17
14
  LineOfSightSmoother,
18
- ORCASolver,
19
15
  ObstacleChangeManager,
20
16
  PathCache,
21
17
  PathValidator,
@@ -25,9 +21,7 @@ import {
25
21
  createCombinedSmoother,
26
22
  createGridMap,
27
23
  createIncrementalAStarPathfinder,
28
- createKDTree,
29
24
  createLineOfSightSmoother,
30
- createORCASolver,
31
25
  createObstacleChangeManager,
32
26
  createPathCache,
33
27
  createPathValidator,
@@ -35,16 +29,27 @@ import {
35
29
  euclideanDistance,
36
30
  manhattanDistance,
37
31
  octileDistance,
38
- raycastLineOfSight,
39
- solveORCALinearProgram
40
- } from "./chunk-OA7ZZQMH.js";
32
+ raycastLineOfSight
33
+ } from "./chunk-VNC2YAAL.js";
41
34
  import {
42
35
  DEFAULT_REPLANNING_CONFIG,
43
36
  EMPTY_PROGRESS,
44
- PathfindingState,
37
+ PathfindingState
38
+ } from "./chunk-YKA3PWU3.js";
39
+ import "./chunk-KEYTX37K.js";
40
+ import {
41
+ DEFAULT_AGENT_PARAMS,
42
+ DEFAULT_ORCA_CONFIG,
43
+ KDTree,
44
+ ORCASolver,
45
+ createKDTree,
46
+ createORCASolver,
47
+ solveORCALinearProgram
48
+ } from "./chunk-JTZP55BJ.js";
49
+ import {
45
50
  __name,
46
51
  __publicField
47
- } from "./chunk-GTFFYRZM.js";
52
+ } from "./chunk-T626JPC7.js";
48
53
 
49
54
  // src/core/BinaryHeap.ts
50
55
  var _BinaryHeap = class _BinaryHeap {
@@ -1404,24 +1409,255 @@ __name(createJPSPathfinder, "createJPSPathfinder");
1404
1409
 
1405
1410
  // src/core/HPAPathfinder.ts
1406
1411
  var DEFAULT_HPA_CONFIG = {
1407
- clusterSize: 10,
1408
- maxEntranceWidth: 6,
1409
- cacheInternalPaths: true
1412
+ clusterSize: 64,
1413
+ maxEntranceWidth: 16,
1414
+ cacheInternalPaths: true,
1415
+ entranceStrategy: "end",
1416
+ lazyIntraEdges: true
1410
1417
  };
1418
+ var _a3;
1419
+ var SubMap = (_a3 = class {
1420
+ constructor(parentMap, originX, originY, width, height) {
1421
+ __publicField(this, "parentMap");
1422
+ __publicField(this, "originX");
1423
+ __publicField(this, "originY");
1424
+ __publicField(this, "width");
1425
+ __publicField(this, "height");
1426
+ this.parentMap = parentMap;
1427
+ this.originX = originX;
1428
+ this.originY = originY;
1429
+ this.width = width;
1430
+ this.height = height;
1431
+ }
1432
+ /**
1433
+ * @zh 局部坐标转全局坐标
1434
+ * @en Convert local to global coordinates
1435
+ */
1436
+ localToGlobal(localX, localY) {
1437
+ return {
1438
+ x: this.originX + localX,
1439
+ y: this.originY + localY
1440
+ };
1441
+ }
1442
+ /**
1443
+ * @zh 全局坐标转局部坐标
1444
+ * @en Convert global to local coordinates
1445
+ */
1446
+ globalToLocal(globalX, globalY) {
1447
+ return {
1448
+ x: globalX - this.originX,
1449
+ y: globalY - this.originY
1450
+ };
1451
+ }
1452
+ isWalkable(x, y) {
1453
+ if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
1454
+ return false;
1455
+ }
1456
+ return this.parentMap.isWalkable(this.originX + x, this.originY + y);
1457
+ }
1458
+ getNodeAt(x, y) {
1459
+ if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
1460
+ return null;
1461
+ }
1462
+ const globalNode = this.parentMap.getNodeAt(this.originX + x, this.originY + y);
1463
+ if (!globalNode) return null;
1464
+ return {
1465
+ id: y * this.width + x,
1466
+ position: {
1467
+ x,
1468
+ y
1469
+ },
1470
+ cost: globalNode.cost,
1471
+ walkable: globalNode.walkable
1472
+ };
1473
+ }
1474
+ getNeighbors(node) {
1475
+ const neighbors = [];
1476
+ const { x, y } = node.position;
1477
+ const directions = [
1478
+ {
1479
+ dx: 0,
1480
+ dy: -1
1481
+ },
1482
+ {
1483
+ dx: 1,
1484
+ dy: -1
1485
+ },
1486
+ {
1487
+ dx: 1,
1488
+ dy: 0
1489
+ },
1490
+ {
1491
+ dx: 1,
1492
+ dy: 1
1493
+ },
1494
+ {
1495
+ dx: 0,
1496
+ dy: 1
1497
+ },
1498
+ {
1499
+ dx: -1,
1500
+ dy: 1
1501
+ },
1502
+ {
1503
+ dx: -1,
1504
+ dy: 0
1505
+ },
1506
+ {
1507
+ dx: -1,
1508
+ dy: -1
1509
+ }
1510
+ // NW
1511
+ ];
1512
+ for (const dir of directions) {
1513
+ const nx = x + dir.dx;
1514
+ const ny = y + dir.dy;
1515
+ if (nx < 0 || nx >= this.width || ny < 0 || ny >= this.height) {
1516
+ continue;
1517
+ }
1518
+ if (!this.isWalkable(nx, ny)) {
1519
+ continue;
1520
+ }
1521
+ if (dir.dx !== 0 && dir.dy !== 0) {
1522
+ if (!this.isWalkable(x + dir.dx, y) || !this.isWalkable(x, y + dir.dy)) {
1523
+ continue;
1524
+ }
1525
+ }
1526
+ const neighborNode = this.getNodeAt(nx, ny);
1527
+ if (neighborNode) {
1528
+ neighbors.push(neighborNode);
1529
+ }
1530
+ }
1531
+ return neighbors;
1532
+ }
1533
+ heuristic(a, b) {
1534
+ const dx = Math.abs(a.x - b.x);
1535
+ const dy = Math.abs(a.y - b.y);
1536
+ return dx + dy + (Math.SQRT2 - 2) * Math.min(dx, dy);
1537
+ }
1538
+ getMovementCost(from, to) {
1539
+ const dx = Math.abs(to.position.x - from.position.x);
1540
+ const dy = Math.abs(to.position.y - from.position.y);
1541
+ const baseCost = dx !== 0 && dy !== 0 ? Math.SQRT2 : 1;
1542
+ return baseCost * to.cost;
1543
+ }
1544
+ }, __name(_a3, "SubMap"), _a3);
1545
+ var _a4;
1546
+ var Cluster = (_a4 = class {
1547
+ constructor(id, originX, originY, width, height, parentMap) {
1548
+ __publicField(this, "id");
1549
+ __publicField(this, "originX");
1550
+ __publicField(this, "originY");
1551
+ __publicField(this, "width");
1552
+ __publicField(this, "height");
1553
+ __publicField(this, "subMap");
1554
+ /** @zh 集群内的抽象节点 ID 列表 @en Abstract node IDs in this cluster */
1555
+ __publicField(this, "nodeIds", []);
1556
+ /** @zh 预计算的距离缓存 @en Precomputed distance cache */
1557
+ __publicField(this, "distanceCache", /* @__PURE__ */ new Map());
1558
+ /** @zh 预计算的路径缓存 @en Precomputed path cache */
1559
+ __publicField(this, "pathCache", /* @__PURE__ */ new Map());
1560
+ this.id = id;
1561
+ this.originX = originX;
1562
+ this.originY = originY;
1563
+ this.width = width;
1564
+ this.height = height;
1565
+ this.subMap = new SubMap(parentMap, originX, originY, width, height);
1566
+ }
1567
+ /**
1568
+ * @zh 检查点是否在集群内
1569
+ * @en Check if point is in cluster
1570
+ */
1571
+ containsPoint(x, y) {
1572
+ return x >= this.originX && x < this.originX + this.width && y >= this.originY && y < this.originY + this.height;
1573
+ }
1574
+ /**
1575
+ * @zh 添加节点 ID
1576
+ * @en Add node ID
1577
+ */
1578
+ addNodeId(nodeId) {
1579
+ if (!this.nodeIds.includes(nodeId)) {
1580
+ this.nodeIds.push(nodeId);
1581
+ }
1582
+ }
1583
+ /**
1584
+ * @zh 移除节点 ID
1585
+ * @en Remove node ID
1586
+ */
1587
+ removeNodeId(nodeId) {
1588
+ const idx = this.nodeIds.indexOf(nodeId);
1589
+ if (idx !== -1) {
1590
+ this.nodeIds.splice(idx, 1);
1591
+ }
1592
+ }
1593
+ /**
1594
+ * @zh 生成缓存键
1595
+ * @en Generate cache key
1596
+ */
1597
+ getCacheKey(fromId, toId) {
1598
+ return `${fromId}->${toId}`;
1599
+ }
1600
+ /**
1601
+ * @zh 设置缓存
1602
+ * @en Set cache
1603
+ */
1604
+ setCache(fromId, toId, cost, path) {
1605
+ const key = this.getCacheKey(fromId, toId);
1606
+ this.distanceCache.set(key, cost);
1607
+ this.pathCache.set(key, path);
1608
+ }
1609
+ /**
1610
+ * @zh 获取缓存的距离
1611
+ * @en Get cached distance
1612
+ */
1613
+ getCachedDistance(fromId, toId) {
1614
+ return this.distanceCache.get(this.getCacheKey(fromId, toId));
1615
+ }
1616
+ /**
1617
+ * @zh 获取缓存的路径
1618
+ * @en Get cached path
1619
+ */
1620
+ getCachedPath(fromId, toId) {
1621
+ return this.pathCache.get(this.getCacheKey(fromId, toId));
1622
+ }
1623
+ /**
1624
+ * @zh 清除缓存
1625
+ * @en Clear cache
1626
+ */
1627
+ clearCache() {
1628
+ this.distanceCache.clear();
1629
+ this.pathCache.clear();
1630
+ }
1631
+ /**
1632
+ * @zh 获取缓存大小
1633
+ * @en Get cache size
1634
+ */
1635
+ getCacheSize() {
1636
+ return this.distanceCache.size;
1637
+ }
1638
+ }, __name(_a4, "Cluster"), _a4);
1411
1639
  var _HPAPathfinder = class _HPAPathfinder {
1412
1640
  constructor(map, config) {
1413
1641
  __publicField(this, "map");
1414
1642
  __publicField(this, "config");
1415
- __publicField(this, "width");
1416
- __publicField(this, "height");
1643
+ __publicField(this, "mapWidth");
1644
+ __publicField(this, "mapHeight");
1645
+ // 集群管理
1417
1646
  __publicField(this, "clusters", []);
1418
- __publicField(this, "entrances", []);
1419
- __publicField(this, "abstractNodes", /* @__PURE__ */ new Map());
1420
1647
  __publicField(this, "clusterGrid", []);
1421
- __publicField(this, "nextEntranceId", 0);
1648
+ __publicField(this, "clustersX", 0);
1649
+ __publicField(this, "clustersY", 0);
1650
+ // 抽象图
1651
+ __publicField(this, "abstractNodes", /* @__PURE__ */ new Map());
1652
+ __publicField(this, "nodesByCluster", /* @__PURE__ */ new Map());
1422
1653
  __publicField(this, "nextNodeId", 0);
1423
- __publicField(this, "internalPathCache", /* @__PURE__ */ new Map());
1654
+ // 入口统计
1655
+ __publicField(this, "entranceCount", 0);
1656
+ // 内部寻路器
1424
1657
  __publicField(this, "localPathfinder");
1658
+ // 完整路径缓存
1659
+ __publicField(this, "pathCache");
1660
+ __publicField(this, "mapVersion", 0);
1425
1661
  __publicField(this, "preprocessed", false);
1426
1662
  this.map = map;
1427
1663
  this.config = {
@@ -1429,28 +1665,26 @@ var _HPAPathfinder = class _HPAPathfinder {
1429
1665
  ...config
1430
1666
  };
1431
1667
  const bounds = this.getMapBounds();
1432
- this.width = bounds.width;
1433
- this.height = bounds.height;
1668
+ this.mapWidth = bounds.width;
1669
+ this.mapHeight = bounds.height;
1434
1670
  this.localPathfinder = new AStarPathfinder(map);
1671
+ this.pathCache = new PathCache({
1672
+ maxEntries: 1e3,
1673
+ ttlMs: 0
1674
+ });
1435
1675
  }
1676
+ // =========================================================================
1677
+ // 公共 API | Public API
1678
+ // =========================================================================
1436
1679
  /**
1437
1680
  * @zh 预处理地图(构建抽象图)
1438
1681
  * @en Preprocess map (build abstract graph)
1439
- *
1440
- * @zh 在地图变化后需要重新调用
1441
- * @en Need to call again after map changes
1442
1682
  */
1443
1683
  preprocess() {
1444
- this.clusters = [];
1445
- this.entrances = [];
1446
- this.abstractNodes.clear();
1447
- this.clusterGrid = [];
1448
- this.internalPathCache.clear();
1449
- this.nextEntranceId = 0;
1450
- this.nextNodeId = 0;
1684
+ this.clear();
1451
1685
  this.buildClusters();
1452
- this.findEntrances();
1453
- this.buildAbstractGraph();
1686
+ this.buildEntrances();
1687
+ this.buildIntraEdges();
1454
1688
  this.preprocessed = true;
1455
1689
  }
1456
1690
  /**
@@ -1481,23 +1715,33 @@ var _HPAPathfinder = class _HPAPathfinder {
1481
1715
  nodesSearched: 1
1482
1716
  };
1483
1717
  }
1718
+ const cached = this.pathCache.get(startX, startY, endX, endY, this.mapVersion);
1719
+ if (cached) {
1720
+ return cached;
1721
+ }
1484
1722
  const startCluster = this.getClusterAt(startX, startY);
1485
1723
  const endCluster = this.getClusterAt(endX, endY);
1486
1724
  if (!startCluster || !endCluster) {
1487
1725
  return EMPTY_PATH_RESULT;
1488
1726
  }
1727
+ let result;
1489
1728
  if (startCluster.id === endCluster.id) {
1490
- return this.findLocalPath(startX, startY, endX, endY, opts);
1491
- }
1492
- const startNodes = this.insertTemporaryNode(startX, startY, startCluster);
1493
- const endNodes = this.insertTemporaryNode(endX, endY, endCluster);
1494
- const abstractPath = this.searchAbstractGraph(startNodes, endNodes, opts);
1495
- this.removeTemporaryNodes(startNodes);
1496
- this.removeTemporaryNodes(endNodes);
1497
- if (!abstractPath || abstractPath.length === 0) {
1498
- return EMPTY_PATH_RESULT;
1729
+ result = this.findLocalPath(startX, startY, endX, endY, opts);
1730
+ } else {
1731
+ const startTemp = this.insertTempNode(startX, startY, startCluster);
1732
+ const endTemp = this.insertTempNode(endX, endY, endCluster);
1733
+ const abstractPath = this.abstractSearch(startTemp, endTemp, opts);
1734
+ this.removeTempNode(startTemp, startCluster);
1735
+ this.removeTempNode(endTemp, endCluster);
1736
+ if (!abstractPath || abstractPath.length === 0) {
1737
+ return EMPTY_PATH_RESULT;
1738
+ }
1739
+ result = this.refinePath(abstractPath, startX, startY, endX, endY, opts);
1740
+ }
1741
+ if (result.found) {
1742
+ this.pathCache.set(startX, startY, endX, endY, result, this.mapVersion);
1499
1743
  }
1500
- return this.refinePath(abstractPath, startX, startY, endX, endY, opts);
1744
+ return result;
1501
1745
  }
1502
1746
  /**
1503
1747
  * @zh 清理状态
@@ -1505,10 +1749,13 @@ var _HPAPathfinder = class _HPAPathfinder {
1505
1749
  */
1506
1750
  clear() {
1507
1751
  this.clusters = [];
1508
- this.entrances = [];
1509
- this.abstractNodes.clear();
1510
1752
  this.clusterGrid = [];
1511
- this.internalPathCache.clear();
1753
+ this.abstractNodes.clear();
1754
+ this.nodesByCluster.clear();
1755
+ this.nextNodeId = 0;
1756
+ this.entranceCount = 0;
1757
+ this.pathCache.invalidateAll();
1758
+ this.mapVersion++;
1512
1759
  this.preprocessed = false;
1513
1760
  }
1514
1761
  /**
@@ -1516,19 +1763,34 @@ var _HPAPathfinder = class _HPAPathfinder {
1516
1763
  * @en Notify map region change
1517
1764
  */
1518
1765
  notifyRegionChange(minX, minY, maxX, maxY) {
1519
- this.preprocessed = false;
1520
- this.internalPathCache.clear();
1766
+ const affectedClusters = this.getAffectedClusters(minX, minY, maxX, maxY);
1767
+ for (const cluster of affectedClusters) {
1768
+ cluster.clearCache();
1769
+ for (const nodeId of cluster.nodeIds) {
1770
+ const node = this.abstractNodes.get(nodeId);
1771
+ if (node) {
1772
+ node.edges = node.edges.filter((e) => e.isInterEdge);
1773
+ }
1774
+ }
1775
+ this.buildClusterIntraEdges(cluster);
1776
+ }
1777
+ this.pathCache.invalidateRegion(minX, minY, maxX, maxY);
1778
+ this.mapVersion++;
1521
1779
  }
1522
1780
  /**
1523
1781
  * @zh 获取预处理统计信息
1524
1782
  * @en Get preprocessing statistics
1525
1783
  */
1526
1784
  getStats() {
1785
+ let cacheSize = 0;
1786
+ for (const cluster of this.clusters) {
1787
+ cacheSize += cluster.getCacheSize();
1788
+ }
1527
1789
  return {
1528
1790
  clusters: this.clusters.length,
1529
- entrances: this.entrances.length,
1791
+ entrances: this.entranceCount,
1530
1792
  abstractNodes: this.abstractNodes.size,
1531
- cacheSize: this.internalPathCache.size
1793
+ cacheSize
1532
1794
  };
1533
1795
  }
1534
1796
  // =========================================================================
@@ -1547,354 +1809,657 @@ var _HPAPathfinder = class _HPAPathfinder {
1547
1809
  height: 1e3
1548
1810
  };
1549
1811
  }
1812
+ /**
1813
+ * @zh 构建集群
1814
+ * @en Build clusters
1815
+ */
1550
1816
  buildClusters() {
1551
1817
  const clusterSize = this.config.clusterSize;
1552
- const clustersX = Math.ceil(this.width / clusterSize);
1553
- const clustersY = Math.ceil(this.height / clusterSize);
1818
+ this.clustersX = Math.ceil(this.mapWidth / clusterSize);
1819
+ this.clustersY = Math.ceil(this.mapHeight / clusterSize);
1554
1820
  this.clusterGrid = [];
1555
- for (let x = 0; x < clustersX; x++) {
1556
- this.clusterGrid[x] = [];
1821
+ for (let cx = 0; cx < this.clustersX; cx++) {
1822
+ this.clusterGrid[cx] = [];
1823
+ for (let cy = 0; cy < this.clustersY; cy++) {
1824
+ this.clusterGrid[cx][cy] = null;
1825
+ }
1557
1826
  }
1558
1827
  let clusterId = 0;
1559
- for (let cy = 0; cy < clustersY; cy++) {
1560
- for (let cx = 0; cx < clustersX; cx++) {
1561
- const cluster = {
1562
- id: clusterId++,
1563
- x: cx * clusterSize,
1564
- y: cy * clusterSize,
1565
- width: Math.min(clusterSize, this.width - cx * clusterSize),
1566
- height: Math.min(clusterSize, this.height - cy * clusterSize),
1567
- entrances: []
1568
- };
1828
+ for (let cy = 0; cy < this.clustersY; cy++) {
1829
+ for (let cx = 0; cx < this.clustersX; cx++) {
1830
+ const originX = cx * clusterSize;
1831
+ const originY = cy * clusterSize;
1832
+ const width = Math.min(clusterSize, this.mapWidth - originX);
1833
+ const height = Math.min(clusterSize, this.mapHeight - originY);
1834
+ const cluster = new Cluster(clusterId, originX, originY, width, height, this.map);
1569
1835
  this.clusters.push(cluster);
1570
- this.clusterGrid[cx][cy] = cluster;
1836
+ this.clusterGrid[cx][cy] = clusterId;
1837
+ this.nodesByCluster.set(clusterId, []);
1838
+ clusterId++;
1571
1839
  }
1572
1840
  }
1573
1841
  }
1574
- findEntrances() {
1842
+ /**
1843
+ * @zh 检测入口并创建抽象节点
1844
+ * @en Detect entrances and create abstract nodes
1845
+ */
1846
+ buildEntrances() {
1575
1847
  const clusterSize = this.config.clusterSize;
1576
- const clustersX = Math.ceil(this.width / clusterSize);
1577
- const clustersY = Math.ceil(this.height / clusterSize);
1578
- for (let cy = 0; cy < clustersY; cy++) {
1579
- for (let cx = 0; cx < clustersX; cx++) {
1580
- const cluster = this.clusterGrid[cx][cy];
1581
- if (!cluster) continue;
1582
- if (cx < clustersX - 1) {
1583
- const rightCluster = this.clusterGrid[cx + 1]?.[cy];
1584
- if (rightCluster) {
1585
- this.findEntrancesBetween(cluster, rightCluster, "horizontal");
1848
+ for (let cy = 0; cy < this.clustersY; cy++) {
1849
+ for (let cx = 0; cx < this.clustersX; cx++) {
1850
+ const clusterId = this.clusterGrid[cx][cy];
1851
+ if (clusterId === null) continue;
1852
+ const cluster1 = this.clusters[clusterId];
1853
+ if (cx < this.clustersX - 1) {
1854
+ const cluster2Id = this.clusterGrid[cx + 1][cy];
1855
+ if (cluster2Id !== null) {
1856
+ const cluster2 = this.clusters[cluster2Id];
1857
+ this.detectAndCreateEntrances(cluster1, cluster2, "vertical");
1586
1858
  }
1587
1859
  }
1588
- if (cy < clustersY - 1) {
1589
- const bottomCluster = this.clusterGrid[cx]?.[cy + 1];
1590
- if (bottomCluster) {
1591
- this.findEntrancesBetween(cluster, bottomCluster, "vertical");
1860
+ if (cy < this.clustersY - 1) {
1861
+ const cluster2Id = this.clusterGrid[cx][cy + 1];
1862
+ if (cluster2Id !== null) {
1863
+ const cluster2 = this.clusters[cluster2Id];
1864
+ this.detectAndCreateEntrances(cluster1, cluster2, "horizontal");
1592
1865
  }
1593
1866
  }
1594
1867
  }
1595
1868
  }
1596
1869
  }
1597
- findEntrancesBetween(cluster1, cluster2, direction) {
1598
- const maxWidth = this.config.maxEntranceWidth;
1599
- let entranceStart = null;
1600
- let entranceLength = 0;
1601
- if (direction === "horizontal") {
1602
- const x1 = cluster1.x + cluster1.width - 1;
1603
- const x2 = cluster2.x;
1604
- const startY = Math.max(cluster1.y, cluster2.y);
1605
- const endY = Math.min(cluster1.y + cluster1.height, cluster2.y + cluster2.height);
1870
+ /**
1871
+ * @zh 检测并创建两个相邻集群之间的入口
1872
+ * @en Detect and create entrances between two adjacent clusters
1873
+ */
1874
+ detectAndCreateEntrances(cluster1, cluster2, boundaryDirection) {
1875
+ const spans = this.detectEntranceSpans(cluster1, cluster2, boundaryDirection);
1876
+ for (const span of spans) {
1877
+ this.createEntranceNodes(cluster1, cluster2, span, boundaryDirection);
1878
+ }
1879
+ }
1880
+ /**
1881
+ * @zh 检测边界上的连续可通行区间
1882
+ * @en Detect continuous walkable spans on boundary
1883
+ */
1884
+ detectEntranceSpans(cluster1, cluster2, boundaryDirection) {
1885
+ const spans = [];
1886
+ if (boundaryDirection === "vertical") {
1887
+ const x1 = cluster1.originX + cluster1.width - 1;
1888
+ const x2 = cluster2.originX;
1889
+ const startY = Math.max(cluster1.originY, cluster2.originY);
1890
+ const endY = Math.min(cluster1.originY + cluster1.height, cluster2.originY + cluster2.height);
1891
+ let spanStart = null;
1606
1892
  for (let y = startY; y < endY; y++) {
1607
1893
  const walkable1 = this.map.isWalkable(x1, y);
1608
1894
  const walkable2 = this.map.isWalkable(x2, y);
1609
1895
  if (walkable1 && walkable2) {
1610
- if (entranceStart === null) {
1611
- entranceStart = y;
1612
- entranceLength = 1;
1613
- } else {
1614
- entranceLength++;
1896
+ if (spanStart === null) {
1897
+ spanStart = y;
1615
1898
  }
1616
- if (entranceLength >= maxWidth || y === endY - 1) {
1617
- this.createEntrance(cluster1, cluster2, x1, x2, entranceStart, entranceStart + entranceLength - 1, "horizontal");
1618
- entranceStart = null;
1619
- entranceLength = 0;
1899
+ } else {
1900
+ if (spanStart !== null) {
1901
+ spans.push({
1902
+ start: spanStart,
1903
+ end: y - 1
1904
+ });
1905
+ spanStart = null;
1620
1906
  }
1621
- } else if (entranceStart !== null) {
1622
- this.createEntrance(cluster1, cluster2, x1, x2, entranceStart, entranceStart + entranceLength - 1, "horizontal");
1623
- entranceStart = null;
1624
- entranceLength = 0;
1625
1907
  }
1626
1908
  }
1909
+ if (spanStart !== null) {
1910
+ spans.push({
1911
+ start: spanStart,
1912
+ end: endY - 1
1913
+ });
1914
+ }
1627
1915
  } else {
1628
- const y1 = cluster1.y + cluster1.height - 1;
1629
- const y2 = cluster2.y;
1630
- const startX = Math.max(cluster1.x, cluster2.x);
1631
- const endX = Math.min(cluster1.x + cluster1.width, cluster2.x + cluster2.width);
1916
+ const y1 = cluster1.originY + cluster1.height - 1;
1917
+ const y2 = cluster2.originY;
1918
+ const startX = Math.max(cluster1.originX, cluster2.originX);
1919
+ const endX = Math.min(cluster1.originX + cluster1.width, cluster2.originX + cluster2.width);
1920
+ let spanStart = null;
1632
1921
  for (let x = startX; x < endX; x++) {
1633
1922
  const walkable1 = this.map.isWalkable(x, y1);
1634
1923
  const walkable2 = this.map.isWalkable(x, y2);
1635
1924
  if (walkable1 && walkable2) {
1636
- if (entranceStart === null) {
1637
- entranceStart = x;
1638
- entranceLength = 1;
1639
- } else {
1640
- entranceLength++;
1925
+ if (spanStart === null) {
1926
+ spanStart = x;
1641
1927
  }
1642
- if (entranceLength >= maxWidth || x === endX - 1) {
1643
- this.createEntrance(cluster1, cluster2, entranceStart, entranceStart + entranceLength - 1, y1, y2, "vertical");
1644
- entranceStart = null;
1645
- entranceLength = 0;
1928
+ } else {
1929
+ if (spanStart !== null) {
1930
+ spans.push({
1931
+ start: spanStart,
1932
+ end: x - 1
1933
+ });
1934
+ spanStart = null;
1646
1935
  }
1647
- } else if (entranceStart !== null) {
1648
- this.createEntrance(cluster1, cluster2, entranceStart, entranceStart + entranceLength - 1, y1, y2, "vertical");
1649
- entranceStart = null;
1650
- entranceLength = 0;
1651
1936
  }
1652
1937
  }
1938
+ if (spanStart !== null) {
1939
+ spans.push({
1940
+ start: spanStart,
1941
+ end: endX - 1
1942
+ });
1943
+ }
1653
1944
  }
1945
+ return spans;
1654
1946
  }
1655
- createEntrance(cluster1, cluster2, coord1Start, coord1End, coord2Start, coord2End, direction) {
1656
- let point1;
1657
- let point2;
1658
- let center;
1659
- if (direction === "horizontal") {
1660
- const midY = Math.floor((coord1Start + coord1End) / 2);
1661
- point1 = {
1662
- x: coord1Start,
1663
- y: midY
1664
- };
1665
- point2 = {
1666
- x: coord2Start,
1667
- y: midY
1668
- };
1669
- center = {
1670
- x: coord1Start,
1671
- y: midY
1672
- };
1947
+ /**
1948
+ * @zh 为入口区间创建抽象节点
1949
+ * @en Create abstract nodes for entrance span
1950
+ */
1951
+ createEntranceNodes(cluster1, cluster2, span, boundaryDirection) {
1952
+ const spanLength = span.end - span.start + 1;
1953
+ const maxWidth = this.config.maxEntranceWidth;
1954
+ const strategy = this.config.entranceStrategy;
1955
+ const positions = [];
1956
+ if (spanLength <= maxWidth) {
1957
+ positions.push(Math.floor((span.start + span.end) / 2));
1673
1958
  } else {
1674
- const midX = Math.floor((coord1Start + coord1End) / 2);
1675
- point1 = {
1676
- x: midX,
1677
- y: coord2Start
1678
- };
1679
- point2 = {
1680
- x: midX,
1681
- y: coord2End
1682
- };
1683
- center = {
1684
- x: midX,
1685
- y: coord2Start
1686
- };
1959
+ const numNodes = Math.ceil(spanLength / maxWidth);
1960
+ const spacing = spanLength / numNodes;
1961
+ for (let i = 0; i < numNodes; i++) {
1962
+ const pos = Math.floor(span.start + spacing * (i + 0.5));
1963
+ positions.push(Math.min(pos, span.end));
1964
+ }
1965
+ if (strategy === "end") {
1966
+ if (!positions.includes(span.start)) {
1967
+ positions.unshift(span.start);
1968
+ }
1969
+ if (!positions.includes(span.end)) {
1970
+ positions.push(span.end);
1971
+ }
1972
+ }
1687
1973
  }
1688
- const entrance = {
1689
- id: this.nextEntranceId++,
1690
- cluster1Id: cluster1.id,
1691
- cluster2Id: cluster2.id,
1692
- point1,
1693
- point2,
1694
- center
1695
- };
1696
- this.entrances.push(entrance);
1697
- cluster1.entrances.push(entrance);
1698
- cluster2.entrances.push(entrance);
1699
- }
1700
- buildAbstractGraph() {
1701
- for (const entrance of this.entrances) {
1702
- const node1 = this.createAbstractNode(entrance.point1, entrance.cluster1Id, entrance.id);
1703
- const node2 = this.createAbstractNode(entrance.point2, entrance.cluster2Id, entrance.id);
1974
+ for (const pos of positions) {
1975
+ let p1, p2;
1976
+ if (boundaryDirection === "vertical") {
1977
+ p1 = {
1978
+ x: cluster1.originX + cluster1.width - 1,
1979
+ y: pos
1980
+ };
1981
+ p2 = {
1982
+ x: cluster2.originX,
1983
+ y: pos
1984
+ };
1985
+ } else {
1986
+ p1 = {
1987
+ x: pos,
1988
+ y: cluster1.originY + cluster1.height - 1
1989
+ };
1990
+ p2 = {
1991
+ x: pos,
1992
+ y: cluster2.originY
1993
+ };
1994
+ }
1995
+ const node1 = this.createAbstractNode(p1, cluster1);
1996
+ const node2 = this.createAbstractNode(p2, cluster2);
1997
+ const interCost = 1;
1704
1998
  node1.edges.push({
1705
1999
  targetNodeId: node2.id,
1706
- cost: 1,
1707
- isInterEdge: true
2000
+ cost: interCost,
2001
+ isInterEdge: true,
2002
+ innerPath: null
1708
2003
  });
1709
2004
  node2.edges.push({
1710
2005
  targetNodeId: node1.id,
1711
- cost: 1,
1712
- isInterEdge: true
2006
+ cost: interCost,
2007
+ isInterEdge: true,
2008
+ innerPath: null
1713
2009
  });
1714
- }
1715
- for (const cluster of this.clusters) {
1716
- this.connectIntraClusterNodes(cluster);
2010
+ this.entranceCount++;
1717
2011
  }
1718
2012
  }
1719
- createAbstractNode(position, clusterId, entranceId) {
2013
+ /**
2014
+ * @zh 创建抽象节点
2015
+ * @en Create abstract node
2016
+ */
2017
+ createAbstractNode(position, cluster) {
2018
+ const concreteId = position.y * this.mapWidth + position.x;
2019
+ for (const nodeId of cluster.nodeIds) {
2020
+ const existing = this.abstractNodes.get(nodeId);
2021
+ if (existing && existing.concreteNodeId === concreteId) {
2022
+ return existing;
2023
+ }
2024
+ }
1720
2025
  const node = {
1721
2026
  id: this.nextNodeId++,
1722
- position,
1723
- clusterId,
1724
- entranceId,
2027
+ position: {
2028
+ x: position.x,
2029
+ y: position.y
2030
+ },
2031
+ clusterId: cluster.id,
2032
+ concreteNodeId: concreteId,
1725
2033
  edges: []
1726
2034
  };
1727
2035
  this.abstractNodes.set(node.id, node);
2036
+ cluster.addNodeId(node.id);
2037
+ const clusterNodes = this.nodesByCluster.get(cluster.id);
2038
+ if (clusterNodes) {
2039
+ clusterNodes.push(node.id);
2040
+ }
1728
2041
  return node;
1729
2042
  }
1730
- connectIntraClusterNodes(cluster) {
1731
- const nodesInCluster = [];
1732
- for (const node of this.abstractNodes.values()) {
1733
- if (node.clusterId === cluster.id) {
1734
- nodesInCluster.push(node);
1735
- }
2043
+ /**
2044
+ * @zh 构建所有集群的 intra-edges
2045
+ * @en Build intra-edges for all clusters
2046
+ */
2047
+ buildIntraEdges() {
2048
+ for (const cluster of this.clusters) {
2049
+ this.buildClusterIntraEdges(cluster);
1736
2050
  }
1737
- for (let i = 0; i < nodesInCluster.length; i++) {
1738
- for (let j = i + 1; j < nodesInCluster.length; j++) {
1739
- const node1 = nodesInCluster[i];
1740
- const node2 = nodesInCluster[j];
1741
- const cost = this.heuristic(node1.position, node2.position);
2051
+ }
2052
+ /**
2053
+ * @zh 构建单个集群的 intra-edges
2054
+ * @en Build intra-edges for single cluster
2055
+ */
2056
+ buildClusterIntraEdges(cluster) {
2057
+ const nodeIds = cluster.nodeIds;
2058
+ if (nodeIds.length < 2) return;
2059
+ if (this.config.lazyIntraEdges) {
2060
+ this.buildLazyIntraEdges(cluster);
2061
+ } else {
2062
+ this.buildEagerIntraEdges(cluster);
2063
+ }
2064
+ }
2065
+ /**
2066
+ * @zh 延迟构建 intra-edges(只用启发式距离)
2067
+ * @en Build lazy intra-edges (using heuristic distance only)
2068
+ */
2069
+ buildLazyIntraEdges(cluster) {
2070
+ const nodeIds = cluster.nodeIds;
2071
+ for (let i = 0; i < nodeIds.length; i++) {
2072
+ for (let j = i + 1; j < nodeIds.length; j++) {
2073
+ const node1 = this.abstractNodes.get(nodeIds[i]);
2074
+ const node2 = this.abstractNodes.get(nodeIds[j]);
2075
+ const heuristicCost = this.heuristic(node1.position, node2.position);
1742
2076
  node1.edges.push({
1743
2077
  targetNodeId: node2.id,
1744
- cost,
1745
- isInterEdge: false
2078
+ cost: heuristicCost,
2079
+ isInterEdge: false,
2080
+ innerPath: null
2081
+ // 标记为未计算
1746
2082
  });
1747
2083
  node2.edges.push({
1748
2084
  targetNodeId: node1.id,
1749
- cost,
1750
- isInterEdge: false
2085
+ cost: heuristicCost,
2086
+ isInterEdge: false,
2087
+ innerPath: null
1751
2088
  });
1752
2089
  }
1753
2090
  }
1754
2091
  }
2092
+ /**
2093
+ * @zh 立即构建 intra-edges(计算真实路径)
2094
+ * @en Build eager intra-edges (compute actual paths)
2095
+ */
2096
+ buildEagerIntraEdges(cluster) {
2097
+ const nodeIds = cluster.nodeIds;
2098
+ const subPathfinder = new AStarPathfinder(cluster.subMap);
2099
+ for (let i = 0; i < nodeIds.length; i++) {
2100
+ for (let j = i + 1; j < nodeIds.length; j++) {
2101
+ const node1 = this.abstractNodes.get(nodeIds[i]);
2102
+ const node2 = this.abstractNodes.get(nodeIds[j]);
2103
+ const local1 = cluster.subMap.globalToLocal(node1.position.x, node1.position.y);
2104
+ const local2 = cluster.subMap.globalToLocal(node2.position.x, node2.position.y);
2105
+ const result = subPathfinder.findPath(local1.x, local1.y, local2.x, local2.y);
2106
+ if (result.found && result.path.length > 0) {
2107
+ const globalPath = result.path.map((p) => {
2108
+ const global = cluster.subMap.localToGlobal(p.x, p.y);
2109
+ return global.y * this.mapWidth + global.x;
2110
+ });
2111
+ if (this.config.cacheInternalPaths) {
2112
+ cluster.setCache(node1.id, node2.id, result.cost, globalPath);
2113
+ cluster.setCache(node2.id, node1.id, result.cost, [
2114
+ ...globalPath
2115
+ ].reverse());
2116
+ }
2117
+ node1.edges.push({
2118
+ targetNodeId: node2.id,
2119
+ cost: result.cost,
2120
+ isInterEdge: false,
2121
+ innerPath: this.config.cacheInternalPaths ? globalPath : null
2122
+ });
2123
+ node2.edges.push({
2124
+ targetNodeId: node1.id,
2125
+ cost: result.cost,
2126
+ isInterEdge: false,
2127
+ innerPath: this.config.cacheInternalPaths ? [
2128
+ ...globalPath
2129
+ ].reverse() : null
2130
+ });
2131
+ }
2132
+ }
2133
+ }
2134
+ }
2135
+ /**
2136
+ * @zh 按需计算 intra-edge 的真实路径
2137
+ * @en Compute actual path for intra-edge on demand
2138
+ */
2139
+ computeIntraEdgePath(fromNode, toNode, edge) {
2140
+ const cluster = this.clusters[fromNode.clusterId];
2141
+ if (!cluster) return null;
2142
+ const cachedPath = cluster.getCachedPath(fromNode.id, toNode.id);
2143
+ const cachedCost = cluster.getCachedDistance(fromNode.id, toNode.id);
2144
+ if (cachedPath && cachedCost !== void 0) {
2145
+ edge.cost = cachedCost;
2146
+ edge.innerPath = cachedPath;
2147
+ return {
2148
+ cost: cachedCost,
2149
+ path: cachedPath
2150
+ };
2151
+ }
2152
+ const subPathfinder = new AStarPathfinder(cluster.subMap);
2153
+ const local1 = cluster.subMap.globalToLocal(fromNode.position.x, fromNode.position.y);
2154
+ const local2 = cluster.subMap.globalToLocal(toNode.position.x, toNode.position.y);
2155
+ const result = subPathfinder.findPath(local1.x, local1.y, local2.x, local2.y);
2156
+ if (result.found && result.path.length > 0) {
2157
+ const globalPath = result.path.map((p) => {
2158
+ const global = cluster.subMap.localToGlobal(p.x, p.y);
2159
+ return global.y * this.mapWidth + global.x;
2160
+ });
2161
+ if (this.config.cacheInternalPaths) {
2162
+ cluster.setCache(fromNode.id, toNode.id, result.cost, globalPath);
2163
+ cluster.setCache(toNode.id, fromNode.id, result.cost, [
2164
+ ...globalPath
2165
+ ].reverse());
2166
+ }
2167
+ edge.cost = result.cost;
2168
+ edge.innerPath = globalPath;
2169
+ const reverseEdge = toNode.edges.find((e) => e.targetNodeId === fromNode.id);
2170
+ if (reverseEdge) {
2171
+ reverseEdge.cost = result.cost;
2172
+ reverseEdge.innerPath = [
2173
+ ...globalPath
2174
+ ].reverse();
2175
+ }
2176
+ return {
2177
+ cost: result.cost,
2178
+ path: globalPath
2179
+ };
2180
+ }
2181
+ return null;
2182
+ }
1755
2183
  // =========================================================================
1756
2184
  // 搜索方法 | Search Methods
1757
2185
  // =========================================================================
2186
+ /**
2187
+ * @zh 获取指定位置的集群
2188
+ * @en Get cluster at position
2189
+ */
1758
2190
  getClusterAt(x, y) {
2191
+ const cx = Math.floor(x / this.config.clusterSize);
2192
+ const cy = Math.floor(y / this.config.clusterSize);
2193
+ if (cx < 0 || cx >= this.clustersX || cy < 0 || cy >= this.clustersY) {
2194
+ return null;
2195
+ }
2196
+ const clusterId = this.clusterGrid[cx]?.[cy];
2197
+ if (clusterId === null || clusterId === void 0) {
2198
+ return null;
2199
+ }
2200
+ return this.clusters[clusterId] || null;
2201
+ }
2202
+ /**
2203
+ * @zh 获取受影响的集群
2204
+ * @en Get affected clusters
2205
+ */
2206
+ getAffectedClusters(minX, minY, maxX, maxY) {
2207
+ const affected = [];
1759
2208
  const clusterSize = this.config.clusterSize;
1760
- const cx = Math.floor(x / clusterSize);
1761
- const cy = Math.floor(y / clusterSize);
1762
- return this.clusterGrid[cx]?.[cy] ?? null;
1763
- }
1764
- insertTemporaryNode(x, y, cluster) {
1765
- const tempNodes = [];
1766
- const tempNode = this.createAbstractNode({
1767
- x,
1768
- y
1769
- }, cluster.id, -1);
1770
- tempNodes.push(tempNode);
1771
- for (const node of this.abstractNodes.values()) {
1772
- if (node.clusterId === cluster.id && node.id !== tempNode.id) {
1773
- const cost = this.heuristic({
1774
- x,
1775
- y
1776
- }, node.position);
2209
+ const minCX = Math.floor(minX / clusterSize);
2210
+ const maxCX = Math.floor(maxX / clusterSize);
2211
+ const minCY = Math.floor(minY / clusterSize);
2212
+ const maxCY = Math.floor(maxY / clusterSize);
2213
+ for (let cy = minCY; cy <= maxCY; cy++) {
2214
+ for (let cx = minCX; cx <= maxCX; cx++) {
2215
+ if (cx >= 0 && cx < this.clustersX && cy >= 0 && cy < this.clustersY) {
2216
+ const clusterId = this.clusterGrid[cx]?.[cy];
2217
+ if (clusterId !== null && clusterId !== void 0) {
2218
+ affected.push(this.clusters[clusterId]);
2219
+ }
2220
+ }
2221
+ }
2222
+ }
2223
+ return affected;
2224
+ }
2225
+ /**
2226
+ * @zh 插入临时节点
2227
+ * @en Insert temporary node
2228
+ */
2229
+ insertTempNode(x, y, cluster) {
2230
+ const concreteId = y * this.mapWidth + x;
2231
+ for (const nodeId of cluster.nodeIds) {
2232
+ const existing = this.abstractNodes.get(nodeId);
2233
+ if (existing && existing.concreteNodeId === concreteId) {
2234
+ return existing;
2235
+ }
2236
+ }
2237
+ const tempNode = {
2238
+ id: this.nextNodeId++,
2239
+ position: {
2240
+ x,
2241
+ y
2242
+ },
2243
+ clusterId: cluster.id,
2244
+ concreteNodeId: concreteId,
2245
+ edges: []
2246
+ };
2247
+ this.abstractNodes.set(tempNode.id, tempNode);
2248
+ cluster.addNodeId(tempNode.id);
2249
+ const subPathfinder = new AStarPathfinder(cluster.subMap);
2250
+ const localPos = cluster.subMap.globalToLocal(x, y);
2251
+ for (const existingNodeId of cluster.nodeIds) {
2252
+ if (existingNodeId === tempNode.id) continue;
2253
+ const existingNode = this.abstractNodes.get(existingNodeId);
2254
+ if (!existingNode) continue;
2255
+ const targetLocalPos = cluster.subMap.globalToLocal(existingNode.position.x, existingNode.position.y);
2256
+ const result = subPathfinder.findPath(localPos.x, localPos.y, targetLocalPos.x, targetLocalPos.y);
2257
+ if (result.found && result.path.length > 0) {
2258
+ const globalPath = result.path.map((p) => {
2259
+ const global = cluster.subMap.localToGlobal(p.x, p.y);
2260
+ return global.y * this.mapWidth + global.x;
2261
+ });
1777
2262
  tempNode.edges.push({
1778
- targetNodeId: node.id,
1779
- cost,
1780
- isInterEdge: false
2263
+ targetNodeId: existingNode.id,
2264
+ cost: result.cost,
2265
+ isInterEdge: false,
2266
+ innerPath: globalPath
1781
2267
  });
1782
- node.edges.push({
2268
+ existingNode.edges.push({
1783
2269
  targetNodeId: tempNode.id,
1784
- cost,
1785
- isInterEdge: false
2270
+ cost: result.cost,
2271
+ isInterEdge: false,
2272
+ innerPath: [
2273
+ ...globalPath
2274
+ ].reverse()
1786
2275
  });
1787
2276
  }
1788
2277
  }
1789
- return tempNodes;
2278
+ return tempNode;
1790
2279
  }
1791
- removeTemporaryNodes(nodes) {
1792
- for (const node of nodes) {
1793
- for (const edge of node.edges) {
1794
- const targetNode = this.abstractNodes.get(edge.targetNodeId);
1795
- if (targetNode) {
1796
- targetNode.edges = targetNode.edges.filter((e) => e.targetNodeId !== node.id);
1797
- }
2280
+ /**
2281
+ * @zh 移除临时节点
2282
+ * @en Remove temporary node
2283
+ */
2284
+ removeTempNode(node, cluster) {
2285
+ for (const existingNodeId of cluster.nodeIds) {
2286
+ if (existingNodeId === node.id) continue;
2287
+ const existingNode = this.abstractNodes.get(existingNodeId);
2288
+ if (existingNode) {
2289
+ existingNode.edges = existingNode.edges.filter((e) => e.targetNodeId !== node.id);
1798
2290
  }
1799
- this.abstractNodes.delete(node.id);
1800
2291
  }
2292
+ cluster.removeNodeId(node.id);
2293
+ this.abstractNodes.delete(node.id);
1801
2294
  }
1802
- searchAbstractGraph(startNodes, endNodes, opts) {
1803
- if (startNodes.length === 0 || endNodes.length === 0) {
1804
- return null;
1805
- }
1806
- const endNodeIds = new Set(endNodes.map((n) => n.id));
1807
- const openList = new BinaryHeap((a, b) => a.f - b.f);
1808
- const closedSet = /* @__PURE__ */ new Set();
1809
- for (const startNode of startNodes) {
1810
- const h = this.heuristic(startNode.position, endNodes[0].position);
1811
- openList.push({
1812
- abstractNode: startNode,
1813
- g: 0,
1814
- h: h * opts.heuristicWeight,
1815
- f: h * opts.heuristicWeight,
1816
- parent: null
1817
- });
1818
- }
2295
+ /**
2296
+ * @zh 在抽象图上进行 A* 搜索
2297
+ * @en Perform A* search on abstract graph
2298
+ */
2299
+ abstractSearch(startNode, endNode, opts) {
2300
+ const openList = new IndexedBinaryHeap((a, b) => a.f - b.f);
2301
+ const nodeMap = /* @__PURE__ */ new Map();
2302
+ const endPosition = endNode.position;
2303
+ const h = this.heuristic(startNode.position, endPosition) * opts.heuristicWeight;
2304
+ const startSearchNode = {
2305
+ node: startNode,
2306
+ g: 0,
2307
+ h,
2308
+ f: h,
2309
+ parent: null,
2310
+ closed: false,
2311
+ opened: true,
2312
+ heapIndex: -1
2313
+ };
2314
+ openList.push(startSearchNode);
2315
+ nodeMap.set(startNode.id, startSearchNode);
1819
2316
  let nodesSearched = 0;
1820
2317
  while (!openList.isEmpty && nodesSearched < opts.maxNodes) {
1821
2318
  const current = openList.pop();
2319
+ current.closed = true;
1822
2320
  nodesSearched++;
1823
- if (endNodeIds.has(current.abstractNode.id)) {
1824
- return this.reconstructAbstractPath(current);
1825
- }
1826
- if (closedSet.has(current.abstractNode.id)) {
1827
- continue;
2321
+ if (current.node.id === endNode.id) {
2322
+ return this.reconstructPath(current);
1828
2323
  }
1829
- closedSet.add(current.abstractNode.id);
1830
- for (const edge of current.abstractNode.edges) {
1831
- if (closedSet.has(edge.targetNodeId)) {
1832
- continue;
2324
+ for (const edge of current.node.edges) {
2325
+ let neighbor = nodeMap.get(edge.targetNodeId);
2326
+ if (!neighbor) {
2327
+ const neighborNode = this.abstractNodes.get(edge.targetNodeId);
2328
+ if (!neighborNode) continue;
2329
+ const nh = this.heuristic(neighborNode.position, endPosition) * opts.heuristicWeight;
2330
+ neighbor = {
2331
+ node: neighborNode,
2332
+ g: Infinity,
2333
+ h: nh,
2334
+ f: Infinity,
2335
+ parent: null,
2336
+ closed: false,
2337
+ opened: false,
2338
+ heapIndex: -1
2339
+ };
2340
+ nodeMap.set(edge.targetNodeId, neighbor);
1833
2341
  }
1834
- const neighbor = this.abstractNodes.get(edge.targetNodeId);
1835
- if (!neighbor) continue;
2342
+ if (neighbor.closed) continue;
1836
2343
  const tentativeG = current.g + edge.cost;
1837
- const h = this.heuristic(neighbor.position, endNodes[0].position) * opts.heuristicWeight;
1838
- openList.push({
1839
- abstractNode: neighbor,
1840
- g: tentativeG,
1841
- h,
1842
- f: tentativeG + h,
1843
- parent: current
1844
- });
2344
+ if (!neighbor.opened) {
2345
+ neighbor.g = tentativeG;
2346
+ neighbor.f = tentativeG + neighbor.h;
2347
+ neighbor.parent = current;
2348
+ neighbor.opened = true;
2349
+ openList.push(neighbor);
2350
+ } else if (tentativeG < neighbor.g) {
2351
+ neighbor.g = tentativeG;
2352
+ neighbor.f = tentativeG + neighbor.h;
2353
+ neighbor.parent = current;
2354
+ openList.update(neighbor);
2355
+ }
1845
2356
  }
1846
2357
  }
1847
2358
  return null;
1848
2359
  }
1849
- reconstructAbstractPath(endNode) {
2360
+ /**
2361
+ * @zh 重建抽象路径
2362
+ * @en Reconstruct abstract path
2363
+ */
2364
+ reconstructPath(endNode) {
1850
2365
  const path = [];
1851
2366
  let current = endNode;
1852
2367
  while (current) {
1853
- path.unshift(current.abstractNode);
2368
+ path.unshift(current.node);
1854
2369
  current = current.parent;
1855
2370
  }
1856
2371
  return path;
1857
2372
  }
2373
+ /**
2374
+ * @zh 细化抽象路径为具体路径
2375
+ * @en Refine abstract path to concrete path
2376
+ */
1858
2377
  refinePath(abstractPath, startX, startY, endX, endY, opts) {
2378
+ if (abstractPath.length === 0) {
2379
+ return EMPTY_PATH_RESULT;
2380
+ }
1859
2381
  const fullPath = [];
1860
2382
  let totalCost = 0;
1861
2383
  let nodesSearched = abstractPath.length;
1862
- let currentX = startX;
1863
- let currentY = startY;
1864
- for (let i = 0; i < abstractPath.length; i++) {
1865
- const node = abstractPath[i];
1866
- const targetX = i === abstractPath.length - 1 ? endX : node.position.x;
1867
- const targetY = i === abstractPath.length - 1 ? endY : node.position.y;
1868
- if (currentX !== targetX || currentY !== targetY) {
1869
- const segment = this.localPathfinder.findPath(currentX, currentY, targetX, targetY, opts);
1870
- if (!segment.found) {
1871
- if (fullPath.length > 0) {
1872
- return {
1873
- found: true,
1874
- path: fullPath,
1875
- cost: totalCost,
1876
- nodesSearched
1877
- };
2384
+ for (let i = 0; i < abstractPath.length - 1; i++) {
2385
+ const fromNode = abstractPath[i];
2386
+ const toNode = abstractPath[i + 1];
2387
+ const edge = fromNode.edges.find((e) => e.targetNodeId === toNode.id);
2388
+ if (!edge) {
2389
+ const segResult = this.findLocalPath(fromNode.position.x, fromNode.position.y, toNode.position.x, toNode.position.y, opts);
2390
+ if (segResult.found) {
2391
+ this.appendPath(fullPath, segResult.path);
2392
+ totalCost += segResult.cost;
2393
+ nodesSearched += segResult.nodesSearched;
2394
+ }
2395
+ } else if (edge.isInterEdge) {
2396
+ if (fullPath.length === 0 || fullPath[fullPath.length - 1].x !== fromNode.position.x || fullPath[fullPath.length - 1].y !== fromNode.position.y) {
2397
+ fullPath.push({
2398
+ x: fromNode.position.x,
2399
+ y: fromNode.position.y
2400
+ });
2401
+ }
2402
+ fullPath.push({
2403
+ x: toNode.position.x,
2404
+ y: toNode.position.y
2405
+ });
2406
+ totalCost += edge.cost;
2407
+ } else if (edge.innerPath && edge.innerPath.length > 0) {
2408
+ const concretePath = edge.innerPath.map((id) => ({
2409
+ x: id % this.mapWidth,
2410
+ y: Math.floor(id / this.mapWidth)
2411
+ }));
2412
+ this.appendPath(fullPath, concretePath);
2413
+ totalCost += edge.cost;
2414
+ } else {
2415
+ const computed = this.computeIntraEdgePath(fromNode, toNode, edge);
2416
+ if (computed && computed.path.length > 0) {
2417
+ const concretePath = computed.path.map((id) => ({
2418
+ x: id % this.mapWidth,
2419
+ y: Math.floor(id / this.mapWidth)
2420
+ }));
2421
+ this.appendPath(fullPath, concretePath);
2422
+ totalCost += computed.cost;
2423
+ } else {
2424
+ const segResult = this.findLocalPath(fromNode.position.x, fromNode.position.y, toNode.position.x, toNode.position.y, opts);
2425
+ if (segResult.found) {
2426
+ this.appendPath(fullPath, segResult.path);
2427
+ totalCost += segResult.cost;
2428
+ nodesSearched += segResult.nodesSearched;
1878
2429
  }
1879
- return EMPTY_PATH_RESULT;
1880
2430
  }
1881
- for (let j = fullPath.length === 0 ? 0 : 1; j < segment.path.length; j++) {
1882
- fullPath.push(segment.path[j]);
2431
+ }
2432
+ }
2433
+ if (fullPath.length > 0 && (fullPath[0].x !== startX || fullPath[0].y !== startY)) {
2434
+ const firstPoint = fullPath[0];
2435
+ if (Math.abs(firstPoint.x - startX) <= 1 && Math.abs(firstPoint.y - startY) <= 1) {
2436
+ fullPath.unshift({
2437
+ x: startX,
2438
+ y: startY
2439
+ });
2440
+ } else {
2441
+ const segResult = this.findLocalPath(startX, startY, firstPoint.x, firstPoint.y, opts);
2442
+ if (segResult.found) {
2443
+ fullPath.splice(0, 0, ...segResult.path.slice(0, -1));
2444
+ totalCost += segResult.cost;
1883
2445
  }
1884
- totalCost += segment.cost;
1885
- nodesSearched += segment.nodesSearched;
1886
2446
  }
1887
- currentX = targetX;
1888
- currentY = targetY;
1889
2447
  }
1890
- if (currentX !== endX || currentY !== endY) {
1891
- const finalSegment = this.localPathfinder.findPath(currentX, currentY, endX, endY, opts);
1892
- if (finalSegment.found) {
1893
- for (let j = 1; j < finalSegment.path.length; j++) {
1894
- fullPath.push(finalSegment.path[j]);
2448
+ if (fullPath.length > 0) {
2449
+ const lastPoint = fullPath[fullPath.length - 1];
2450
+ if (lastPoint.x !== endX || lastPoint.y !== endY) {
2451
+ if (Math.abs(lastPoint.x - endX) <= 1 && Math.abs(lastPoint.y - endY) <= 1) {
2452
+ fullPath.push({
2453
+ x: endX,
2454
+ y: endY
2455
+ });
2456
+ } else {
2457
+ const segResult = this.findLocalPath(lastPoint.x, lastPoint.y, endX, endY, opts);
2458
+ if (segResult.found) {
2459
+ fullPath.push(...segResult.path.slice(1));
2460
+ totalCost += segResult.cost;
2461
+ }
1895
2462
  }
1896
- totalCost += finalSegment.cost;
1897
- nodesSearched += finalSegment.nodesSearched;
1898
2463
  }
1899
2464
  }
1900
2465
  return {
@@ -1904,39 +2469,37 @@ var _HPAPathfinder = class _HPAPathfinder {
1904
2469
  nodesSearched
1905
2470
  };
1906
2471
  }
1907
- // =========================================================================
1908
- // 辅助方法 | Helper Methods
1909
- // =========================================================================
1910
- findLocalPath(startX, startY, endX, endY, opts) {
1911
- return this.localPathfinder.findPath(startX, startY, endX, endY, opts);
1912
- }
1913
- findInternalPath(startX, startY, endX, endY, cluster) {
1914
- const cacheKey = `${cluster.id}:${startX},${startY}->${endX},${endY}`;
1915
- if (this.config.cacheInternalPaths) {
1916
- const cached = this.internalPathCache.get(cacheKey);
1917
- if (cached) {
1918
- return cached;
2472
+ /**
2473
+ * @zh 追加路径(避免重复点)
2474
+ * @en Append path (avoid duplicate points)
2475
+ */
2476
+ appendPath(fullPath, segment) {
2477
+ if (segment.length === 0) return;
2478
+ let startIdx = 0;
2479
+ if (fullPath.length > 0) {
2480
+ const last = fullPath[fullPath.length - 1];
2481
+ if (last.x === segment[0].x && last.y === segment[0].y) {
2482
+ startIdx = 1;
1919
2483
  }
1920
2484
  }
1921
- const result = this.localPathfinder.findPath(startX, startY, endX, endY);
1922
- if (result.found && this.config.cacheInternalPaths) {
1923
- this.internalPathCache.set(cacheKey, [
1924
- ...result.path
1925
- ]);
2485
+ for (let i = startIdx; i < segment.length; i++) {
2486
+ fullPath.push({
2487
+ x: segment[i].x,
2488
+ y: segment[i].y
2489
+ });
1926
2490
  }
1927
- return result.found ? [
1928
- ...result.path
1929
- ] : null;
1930
2491
  }
1931
- calculatePathCost(path) {
1932
- let cost = 0;
1933
- for (let i = 1; i < path.length; i++) {
1934
- const dx = Math.abs(path[i].x - path[i - 1].x);
1935
- const dy = Math.abs(path[i].y - path[i - 1].y);
1936
- cost += dx !== 0 && dy !== 0 ? Math.SQRT2 : 1;
1937
- }
1938
- return cost;
2492
+ /**
2493
+ * @zh 局部寻路
2494
+ * @en Local pathfinding
2495
+ */
2496
+ findLocalPath(startX, startY, endX, endY, opts) {
2497
+ return this.localPathfinder.findPath(startX, startY, endX, endY, opts);
1939
2498
  }
2499
+ /**
2500
+ * @zh 启发式函数(Octile 距离)
2501
+ * @en Heuristic function (Octile distance)
2502
+ */
1940
2503
  heuristic(a, b) {
1941
2504
  const dx = Math.abs(a.x - b.x);
1942
2505
  const dy = Math.abs(a.y - b.y);
@@ -1951,8 +2514,8 @@ function createHPAPathfinder(map, config) {
1951
2514
  __name(createHPAPathfinder, "createHPAPathfinder");
1952
2515
 
1953
2516
  // src/navmesh/NavMesh.ts
1954
- var _a3;
1955
- var NavMeshNode = (_a3 = class {
2517
+ var _a5;
2518
+ var NavMeshNode = (_a5 = class {
1956
2519
  constructor(polygon) {
1957
2520
  __publicField(this, "id");
1958
2521
  __publicField(this, "position");
@@ -1965,7 +2528,7 @@ var NavMeshNode = (_a3 = class {
1965
2528
  this.walkable = true;
1966
2529
  this.polygon = polygon;
1967
2530
  }
1968
- }, __name(_a3, "NavMeshNode"), _a3);
2531
+ }, __name(_a5, "NavMeshNode"), _a5);
1969
2532
  var _NavMesh = class _NavMesh {
1970
2533
  constructor() {
1971
2534
  __publicField(this, "polygons", /* @__PURE__ */ new Map());