@esengine/pathfinding
Version:
寻路系统 | Pathfinding System - A*, Grid, NavMesh
1,826 lines (1,819 loc) • 45 kB
JavaScript
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
// src/core/IPathfinding.ts
function createPoint(x, y) {
return {
x,
y
};
}
__name(createPoint, "createPoint");
var EMPTY_PATH_RESULT = {
found: false,
path: [],
cost: 0,
nodesSearched: 0
};
function manhattanDistance(a, b) {
return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
}
__name(manhattanDistance, "manhattanDistance");
function euclideanDistance(a, b) {
const dx = a.x - b.x;
const dy = a.y - b.y;
return Math.sqrt(dx * dx + dy * dy);
}
__name(euclideanDistance, "euclideanDistance");
function chebyshevDistance(a, b) {
return Math.max(Math.abs(a.x - b.x), Math.abs(a.y - b.y));
}
__name(chebyshevDistance, "chebyshevDistance");
function octileDistance(a, b) {
const dx = Math.abs(a.x - b.x);
const dy = Math.abs(a.y - b.y);
const D = 1;
const D2 = Math.SQRT2;
return D * (dx + dy) + (D2 - 2 * D) * Math.min(dx, dy);
}
__name(octileDistance, "octileDistance");
var DEFAULT_PATHFINDING_OPTIONS = {
maxNodes: 1e4,
heuristicWeight: 1,
allowDiagonal: true,
avoidCorners: true
};
// src/core/BinaryHeap.ts
var _BinaryHeap = class _BinaryHeap {
/**
* @zh 创建二叉堆
* @en Create binary heap
*
* @param compare - @zh 比较函数,返回负数表示 a < b @en Compare function, returns negative if a < b
*/
constructor(compare) {
__publicField(this, "heap", []);
__publicField(this, "compare");
this.compare = compare;
}
/**
* @zh 堆大小
* @en Heap size
*/
get size() {
return this.heap.length;
}
/**
* @zh 是否为空
* @en Is empty
*/
get isEmpty() {
return this.heap.length === 0;
}
/**
* @zh 插入元素
* @en Push element
*/
push(item) {
this.heap.push(item);
this.bubbleUp(this.heap.length - 1);
}
/**
* @zh 弹出最小元素
* @en Pop minimum element
*/
pop() {
if (this.heap.length === 0) {
return void 0;
}
const result = this.heap[0];
const last = this.heap.pop();
if (this.heap.length > 0) {
this.heap[0] = last;
this.sinkDown(0);
}
return result;
}
/**
* @zh 查看最小元素(不移除)
* @en Peek minimum element (without removing)
*/
peek() {
return this.heap[0];
}
/**
* @zh 更新元素(重新排序)
* @en Update element (re-sort)
*/
update(item) {
const index = this.heap.indexOf(item);
if (index !== -1) {
this.bubbleUp(index);
this.sinkDown(index);
}
}
/**
* @zh 检查是否包含元素
* @en Check if contains element
*/
contains(item) {
return this.heap.indexOf(item) !== -1;
}
/**
* @zh 清空堆
* @en Clear heap
*/
clear() {
this.heap.length = 0;
}
/**
* @zh 上浮操作
* @en Bubble up operation
*/
bubbleUp(index) {
const item = this.heap[index];
while (index > 0) {
const parentIndex = Math.floor((index - 1) / 2);
const parent = this.heap[parentIndex];
if (this.compare(item, parent) >= 0) {
break;
}
this.heap[index] = parent;
index = parentIndex;
}
this.heap[index] = item;
}
/**
* @zh 下沉操作
* @en Sink down operation
*/
sinkDown(index) {
const length = this.heap.length;
const item = this.heap[index];
while (true) {
const leftIndex = 2 * index + 1;
const rightIndex = 2 * index + 2;
let smallest = index;
if (leftIndex < length && this.compare(this.heap[leftIndex], this.heap[smallest]) < 0) {
smallest = leftIndex;
}
if (rightIndex < length && this.compare(this.heap[rightIndex], this.heap[smallest]) < 0) {
smallest = rightIndex;
}
if (smallest === index) {
break;
}
this.heap[index] = this.heap[smallest];
this.heap[smallest] = item;
index = smallest;
}
}
};
__name(_BinaryHeap, "BinaryHeap");
var BinaryHeap = _BinaryHeap;
// src/core/AStarPathfinder.ts
var _AStarPathfinder = class _AStarPathfinder {
constructor(map) {
__publicField(this, "map");
__publicField(this, "nodeCache", /* @__PURE__ */ new Map());
__publicField(this, "openList");
this.map = map;
this.openList = new BinaryHeap((a, b) => a.f - b.f);
}
/**
* @zh 查找路径
* @en Find path
*/
findPath(startX, startY, endX, endY, options) {
const opts = {
...DEFAULT_PATHFINDING_OPTIONS,
...options
};
this.clear();
const startNode = this.map.getNodeAt(startX, startY);
const endNode = this.map.getNodeAt(endX, endY);
if (!startNode || !endNode) {
return EMPTY_PATH_RESULT;
}
if (!startNode.walkable || !endNode.walkable) {
return EMPTY_PATH_RESULT;
}
if (startNode.id === endNode.id) {
return {
found: true,
path: [
startNode.position
],
cost: 0,
nodesSearched: 1
};
}
const start = this.getOrCreateAStarNode(startNode);
start.g = 0;
start.h = this.map.heuristic(startNode.position, endNode.position) * opts.heuristicWeight;
start.f = start.h;
start.opened = true;
this.openList.push(start);
let nodesSearched = 0;
const endPosition = endNode.position;
while (!this.openList.isEmpty && nodesSearched < opts.maxNodes) {
const current = this.openList.pop();
current.closed = true;
nodesSearched++;
if (current.node.id === endNode.id) {
return this.buildPath(current, nodesSearched);
}
const neighbors = this.map.getNeighbors(current.node);
for (const neighborNode of neighbors) {
if (!neighborNode.walkable) {
continue;
}
const neighbor = this.getOrCreateAStarNode(neighborNode);
if (neighbor.closed) {
continue;
}
const movementCost = this.map.getMovementCost(current.node, neighborNode);
const tentativeG = current.g + movementCost;
if (!neighbor.opened) {
neighbor.g = tentativeG;
neighbor.h = this.map.heuristic(neighborNode.position, endPosition) * opts.heuristicWeight;
neighbor.f = neighbor.g + neighbor.h;
neighbor.parent = current;
neighbor.opened = true;
this.openList.push(neighbor);
} else if (tentativeG < neighbor.g) {
neighbor.g = tentativeG;
neighbor.f = neighbor.g + neighbor.h;
neighbor.parent = current;
this.openList.update(neighbor);
}
}
}
return {
found: false,
path: [],
cost: 0,
nodesSearched
};
}
/**
* @zh 清理状态
* @en Clear state
*/
clear() {
this.nodeCache.clear();
this.openList.clear();
}
/**
* @zh 获取或创建 A* 节点
* @en Get or create A* node
*/
getOrCreateAStarNode(node) {
let astarNode = this.nodeCache.get(node.id);
if (!astarNode) {
astarNode = {
node,
g: Infinity,
h: 0,
f: Infinity,
parent: null,
closed: false,
opened: false
};
this.nodeCache.set(node.id, astarNode);
}
return astarNode;
}
/**
* @zh 构建路径结果
* @en Build path result
*/
buildPath(endNode, nodesSearched) {
const path = [];
let current = endNode;
while (current) {
path.unshift(current.node.position);
current = current.parent;
}
return {
found: true,
path,
cost: endNode.g,
nodesSearched
};
}
};
__name(_AStarPathfinder, "AStarPathfinder");
var AStarPathfinder = _AStarPathfinder;
function createAStarPathfinder(map) {
return new AStarPathfinder(map);
}
__name(createAStarPathfinder, "createAStarPathfinder");
// src/grid/GridMap.ts
var _GridNode = class _GridNode {
constructor(x, y, walkable = true, cost = 1) {
__publicField(this, "id");
__publicField(this, "position");
__publicField(this, "x");
__publicField(this, "y");
__publicField(this, "cost");
__publicField(this, "walkable");
this.x = x;
this.y = y;
this.id = `${x},${y}`;
this.position = createPoint(x, y);
this.walkable = walkable;
this.cost = cost;
}
};
__name(_GridNode, "GridNode");
var GridNode = _GridNode;
var DIRECTIONS_4 = [
{
dx: 0,
dy: -1
},
{
dx: 1,
dy: 0
},
{
dx: 0,
dy: 1
},
{
dx: -1,
dy: 0
}
// Left
];
var DIRECTIONS_8 = [
{
dx: 0,
dy: -1
},
{
dx: 1,
dy: -1
},
{
dx: 1,
dy: 0
},
{
dx: 1,
dy: 1
},
{
dx: 0,
dy: 1
},
{
dx: -1,
dy: 1
},
{
dx: -1,
dy: 0
},
{
dx: -1,
dy: -1
}
// Up-Left
];
var DEFAULT_GRID_OPTIONS = {
allowDiagonal: true,
diagonalCost: Math.SQRT2,
avoidCorners: true,
heuristic: octileDistance
};
var _GridMap = class _GridMap {
constructor(width, height, options) {
__publicField(this, "width");
__publicField(this, "height");
__publicField(this, "nodes");
__publicField(this, "options");
this.width = width;
this.height = height;
this.options = {
...DEFAULT_GRID_OPTIONS,
...options
};
this.nodes = this.createNodes();
}
/**
* @zh 创建网格节点
* @en Create grid nodes
*/
createNodes() {
const nodes = [];
for (let y = 0; y < this.height; y++) {
nodes[y] = [];
for (let x = 0; x < this.width; x++) {
nodes[y][x] = new GridNode(x, y, true, 1);
}
}
return nodes;
}
/**
* @zh 获取指定位置的节点
* @en Get node at position
*/
getNodeAt(x, y) {
if (!this.isInBounds(x, y)) {
return null;
}
return this.nodes[y][x];
}
/**
* @zh 检查坐标是否在边界内
* @en Check if coordinates are within bounds
*/
isInBounds(x, y) {
return x >= 0 && x < this.width && y >= 0 && y < this.height;
}
/**
* @zh 检查位置是否可通行
* @en Check if position is walkable
*/
isWalkable(x, y) {
const node = this.getNodeAt(x, y);
return node !== null && node.walkable;
}
/**
* @zh 设置位置是否可通行
* @en Set position walkability
*/
setWalkable(x, y, walkable) {
const node = this.getNodeAt(x, y);
if (node) {
node.walkable = walkable;
}
}
/**
* @zh 设置位置的移动代价
* @en Set movement cost at position
*/
setCost(x, y, cost) {
const node = this.getNodeAt(x, y);
if (node) {
node.cost = cost;
}
}
/**
* @zh 获取节点的邻居
* @en Get neighbors of a node
*/
getNeighbors(node) {
const neighbors = [];
const { x, y } = node.position;
const directions = this.options.allowDiagonal ? DIRECTIONS_8 : DIRECTIONS_4;
for (let i = 0; i < directions.length; i++) {
const dir = directions[i];
const nx = x + dir.dx;
const ny = y + dir.dy;
if (!this.isInBounds(nx, ny)) {
continue;
}
const neighbor = this.nodes[ny][nx];
if (!neighbor.walkable) {
continue;
}
if (this.options.avoidCorners && dir.dx !== 0 && dir.dy !== 0) {
const horizontal = this.getNodeAt(x + dir.dx, y);
const vertical = this.getNodeAt(x, y + dir.dy);
if (!horizontal?.walkable || !vertical?.walkable) {
continue;
}
}
neighbors.push(neighbor);
}
return neighbors;
}
/**
* @zh 计算启发式距离
* @en Calculate heuristic distance
*/
heuristic(a, b) {
return this.options.heuristic(a, b);
}
/**
* @zh 计算移动代价
* @en Calculate movement cost
*/
getMovementCost(from, to) {
const dx = Math.abs(from.position.x - to.position.x);
const dy = Math.abs(from.position.y - to.position.y);
if (dx !== 0 && dy !== 0) {
return to.cost * this.options.diagonalCost;
}
return to.cost;
}
/**
* @zh 从二维数组加载地图
* @en Load map from 2D array
*
* @param data - @zh 0=可通行,非0=不可通行 @en 0=walkable, non-0=blocked
*/
loadFromArray(data) {
for (let y = 0; y < Math.min(data.length, this.height); y++) {
for (let x = 0; x < Math.min(data[y].length, this.width); x++) {
this.nodes[y][x].walkable = data[y][x] === 0;
}
}
}
/**
* @zh 从字符串加载地图
* @en Load map from string
*
* @param str - @zh 地图字符串,'.'=可通行,'#'=障碍 @en Map string, '.'=walkable, '#'=blocked
*/
loadFromString(str) {
const lines = str.trim().split("\n");
for (let y = 0; y < Math.min(lines.length, this.height); y++) {
const line = lines[y];
for (let x = 0; x < Math.min(line.length, this.width); x++) {
this.nodes[y][x].walkable = line[x] !== "#";
}
}
}
/**
* @zh 导出为字符串
* @en Export to string
*/
toString() {
let result = "";
for (let y = 0; y < this.height; y++) {
for (let x = 0; x < this.width; x++) {
result += this.nodes[y][x].walkable ? "." : "#";
}
result += "\n";
}
return result;
}
/**
* @zh 重置所有节点为可通行
* @en Reset all nodes to walkable
*/
reset() {
for (let y = 0; y < this.height; y++) {
for (let x = 0; x < this.width; x++) {
this.nodes[y][x].walkable = true;
this.nodes[y][x].cost = 1;
}
}
}
/**
* @zh 设置矩形区域的通行性
* @en Set walkability for a rectangle region
*/
setRectWalkable(x, y, width, height, walkable) {
for (let dy = 0; dy < height; dy++) {
for (let dx = 0; dx < width; dx++) {
this.setWalkable(x + dx, y + dy, walkable);
}
}
}
};
__name(_GridMap, "GridMap");
var GridMap = _GridMap;
function createGridMap(width, height, options) {
return new GridMap(width, height, options);
}
__name(createGridMap, "createGridMap");
// src/navmesh/NavMesh.ts
var _a;
var NavMeshNode = (_a = class {
constructor(polygon) {
__publicField(this, "id");
__publicField(this, "position");
__publicField(this, "cost");
__publicField(this, "walkable");
__publicField(this, "polygon");
this.id = polygon.id;
this.position = polygon.center;
this.cost = 1;
this.walkable = true;
this.polygon = polygon;
}
}, __name(_a, "NavMeshNode"), _a);
var _NavMesh = class _NavMesh {
constructor() {
__publicField(this, "polygons", /* @__PURE__ */ new Map());
__publicField(this, "nodes", /* @__PURE__ */ new Map());
__publicField(this, "nextId", 0);
}
/**
* @zh 添加导航多边形
* @en Add navigation polygon
*
* @returns @zh 多边形ID @en Polygon ID
*/
addPolygon(vertices, neighbors = []) {
const id = this.nextId++;
const center = this.calculateCenter(vertices);
const polygon = {
id,
vertices,
center,
neighbors,
portals: /* @__PURE__ */ new Map()
};
this.polygons.set(id, polygon);
this.nodes.set(id, new NavMeshNode(polygon));
return id;
}
/**
* @zh 设置两个多边形之间的连接
* @en Set connection between two polygons
*/
setConnection(polyA, polyB, portal) {
const polygonA = this.polygons.get(polyA);
const polygonB = this.polygons.get(polyB);
if (!polygonA || !polygonB) {
return;
}
const neighborsA = [
...polygonA.neighbors
];
const portalsA = new Map(polygonA.portals);
if (!neighborsA.includes(polyB)) {
neighborsA.push(polyB);
}
portalsA.set(polyB, portal);
this.polygons.set(polyA, {
...polygonA,
neighbors: neighborsA,
portals: portalsA
});
const reversePortal = {
left: portal.right,
right: portal.left
};
const neighborsB = [
...polygonB.neighbors
];
const portalsB = new Map(polygonB.portals);
if (!neighborsB.includes(polyA)) {
neighborsB.push(polyA);
}
portalsB.set(polyA, reversePortal);
this.polygons.set(polyB, {
...polygonB,
neighbors: neighborsB,
portals: portalsB
});
}
/**
* @zh 自动检测并建立相邻多边形的连接
* @en Auto-detect and build connections between adjacent polygons
*/
build() {
const polygonList = Array.from(this.polygons.values());
for (let i = 0; i < polygonList.length; i++) {
for (let j = i + 1; j < polygonList.length; j++) {
const polyA = polygonList[i];
const polyB = polygonList[j];
const sharedEdge = this.findSharedEdge(polyA.vertices, polyB.vertices);
if (sharedEdge) {
this.setConnection(polyA.id, polyB.id, sharedEdge);
}
}
}
}
/**
* @zh 查找两个多边形的共享边
* @en Find shared edge between two polygons
*/
findSharedEdge(verticesA, verticesB) {
const epsilon = 1e-4;
for (let i = 0; i < verticesA.length; i++) {
const a1 = verticesA[i];
const a2 = verticesA[(i + 1) % verticesA.length];
for (let j = 0; j < verticesB.length; j++) {
const b1 = verticesB[j];
const b2 = verticesB[(j + 1) % verticesB.length];
const match1 = Math.abs(a1.x - b2.x) < epsilon && Math.abs(a1.y - b2.y) < epsilon && Math.abs(a2.x - b1.x) < epsilon && Math.abs(a2.y - b1.y) < epsilon;
const match2 = Math.abs(a1.x - b1.x) < epsilon && Math.abs(a1.y - b1.y) < epsilon && Math.abs(a2.x - b2.x) < epsilon && Math.abs(a2.y - b2.y) < epsilon;
if (match1 || match2) {
return {
left: a1,
right: a2
};
}
}
}
return null;
}
/**
* @zh 计算多边形中心
* @en Calculate polygon center
*/
calculateCenter(vertices) {
let x = 0;
let y = 0;
for (const v of vertices) {
x += v.x;
y += v.y;
}
return createPoint(x / vertices.length, y / vertices.length);
}
/**
* @zh 查找包含点的多边形
* @en Find polygon containing point
*/
findPolygonAt(x, y) {
for (const polygon of this.polygons.values()) {
if (this.isPointInPolygon(x, y, polygon.vertices)) {
return polygon;
}
}
return null;
}
/**
* @zh 检查点是否在多边形内
* @en Check if point is inside polygon
*/
isPointInPolygon(x, y, vertices) {
let inside = false;
const n = vertices.length;
for (let i = 0, j = n - 1; i < n; j = i++) {
const xi = vertices[i].x;
const yi = vertices[i].y;
const xj = vertices[j].x;
const yj = vertices[j].y;
if (yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi) {
inside = !inside;
}
}
return inside;
}
// ==========================================================================
// IPathfindingMap 接口实现 | IPathfindingMap Interface Implementation
// ==========================================================================
getNodeAt(x, y) {
const polygon = this.findPolygonAt(x, y);
return polygon ? this.nodes.get(polygon.id) ?? null : null;
}
getNeighbors(node) {
const navNode = node;
const neighbors = [];
for (const neighborId of navNode.polygon.neighbors) {
const neighbor = this.nodes.get(neighborId);
if (neighbor) {
neighbors.push(neighbor);
}
}
return neighbors;
}
heuristic(a, b) {
return euclideanDistance(a, b);
}
getMovementCost(from, to) {
return euclideanDistance(from.position, to.position);
}
isWalkable(x, y) {
return this.findPolygonAt(x, y) !== null;
}
// ==========================================================================
// 寻路 | Pathfinding
// ==========================================================================
/**
* @zh 在导航网格上寻路
* @en Find path on navigation mesh
*/
findPath(startX, startY, endX, endY, options) {
const opts = {
...DEFAULT_PATHFINDING_OPTIONS,
...options
};
const startPolygon = this.findPolygonAt(startX, startY);
const endPolygon = this.findPolygonAt(endX, endY);
if (!startPolygon || !endPolygon) {
return EMPTY_PATH_RESULT;
}
if (startPolygon.id === endPolygon.id) {
return {
found: true,
path: [
createPoint(startX, startY),
createPoint(endX, endY)
],
cost: euclideanDistance(createPoint(startX, startY), createPoint(endX, endY)),
nodesSearched: 1
};
}
const polygonPath = this.findPolygonPath(startPolygon, endPolygon, opts);
if (!polygonPath.found) {
return EMPTY_PATH_RESULT;
}
const start = createPoint(startX, startY);
const end = createPoint(endX, endY);
const pointPath = this.funnelPath(start, end, polygonPath.polygons);
return {
found: true,
path: pointPath,
cost: this.calculatePathLength(pointPath),
nodesSearched: polygonPath.nodesSearched
};
}
/**
* @zh 在多边形图上寻路
* @en Find path on polygon graph
*/
findPolygonPath(start, end, opts) {
const openList = new BinaryHeap((a, b) => a.f - b.f);
const closed = /* @__PURE__ */ new Set();
const states = /* @__PURE__ */ new Map();
const startState = {
polygon: start,
g: 0,
f: euclideanDistance(start.center, end.center) * opts.heuristicWeight,
parent: null
};
states.set(start.id, startState);
openList.push(startState);
let nodesSearched = 0;
while (!openList.isEmpty && nodesSearched < opts.maxNodes) {
const current = openList.pop();
nodesSearched++;
if (current.polygon.id === end.id) {
const path = [];
let state = current;
while (state) {
path.unshift(state.polygon);
state = state.parent;
}
return {
found: true,
polygons: path,
nodesSearched
};
}
closed.add(current.polygon.id);
for (const neighborId of current.polygon.neighbors) {
if (closed.has(neighborId)) {
continue;
}
const neighborPolygon = this.polygons.get(neighborId);
if (!neighborPolygon) {
continue;
}
const g = current.g + euclideanDistance(current.polygon.center, neighborPolygon.center);
let neighborState = states.get(neighborId);
if (!neighborState) {
neighborState = {
polygon: neighborPolygon,
g,
f: g + euclideanDistance(neighborPolygon.center, end.center) * opts.heuristicWeight,
parent: current
};
states.set(neighborId, neighborState);
openList.push(neighborState);
} else if (g < neighborState.g) {
neighborState.g = g;
neighborState.f = g + euclideanDistance(neighborPolygon.center, end.center) * opts.heuristicWeight;
neighborState.parent = current;
openList.update(neighborState);
}
}
}
return {
found: false,
polygons: [],
nodesSearched
};
}
/**
* @zh 使用漏斗算法优化路径
* @en Optimize path using funnel algorithm
*/
funnelPath(start, end, polygons) {
if (polygons.length <= 1) {
return [
start,
end
];
}
const portals = [];
for (let i = 0; i < polygons.length - 1; i++) {
const portal = polygons[i].portals.get(polygons[i + 1].id);
if (portal) {
portals.push(portal);
}
}
if (portals.length === 0) {
return [
start,
end
];
}
const path = [
start
];
let apex = start;
let leftIndex = 0;
let rightIndex = 0;
let left = portals[0].left;
let right = portals[0].right;
for (let i = 1; i <= portals.length; i++) {
const nextLeft = i < portals.length ? portals[i].left : end;
const nextRight = i < portals.length ? portals[i].right : end;
if (this.triArea2(apex, right, nextRight) <= 0) {
if (apex === right || this.triArea2(apex, left, nextRight) > 0) {
right = nextRight;
rightIndex = i;
} else {
path.push(left);
apex = left;
leftIndex = rightIndex = leftIndex;
left = right = apex;
i = leftIndex;
continue;
}
}
if (this.triArea2(apex, left, nextLeft) >= 0) {
if (apex === left || this.triArea2(apex, right, nextLeft) < 0) {
left = nextLeft;
leftIndex = i;
} else {
path.push(right);
apex = right;
leftIndex = rightIndex = rightIndex;
left = right = apex;
i = rightIndex;
continue;
}
}
}
path.push(end);
return path;
}
/**
* @zh 计算三角形面积的两倍(用于判断点的相对位置)
* @en Calculate twice the triangle area (for point relative position)
*/
triArea2(a, b, c) {
return (c.x - a.x) * (b.y - a.y) - (b.x - a.x) * (c.y - a.y);
}
/**
* @zh 计算路径总长度
* @en Calculate total path length
*/
calculatePathLength(path) {
let length = 0;
for (let i = 1; i < path.length; i++) {
length += euclideanDistance(path[i - 1], path[i]);
}
return length;
}
/**
* @zh 清空导航网格
* @en Clear navigation mesh
*/
clear() {
this.polygons.clear();
this.nodes.clear();
this.nextId = 0;
}
/**
* @zh 获取所有多边形
* @en Get all polygons
*/
getPolygons() {
return Array.from(this.polygons.values());
}
/**
* @zh 获取多边形数量
* @en Get polygon count
*/
get polygonCount() {
return this.polygons.size;
}
};
__name(_NavMesh, "NavMesh");
var NavMesh = _NavMesh;
function createNavMesh() {
return new NavMesh();
}
__name(createNavMesh, "createNavMesh");
// src/smoothing/PathSmoother.ts
function bresenhamLineOfSight(x1, y1, x2, y2, map) {
let ix1 = Math.floor(x1);
let iy1 = Math.floor(y1);
const ix2 = Math.floor(x2);
const iy2 = Math.floor(y2);
const dx = Math.abs(ix2 - ix1);
const dy = Math.abs(iy2 - iy1);
const sx = ix1 < ix2 ? 1 : -1;
const sy = iy1 < iy2 ? 1 : -1;
let err = dx - dy;
while (true) {
if (!map.isWalkable(ix1, iy1)) {
return false;
}
if (ix1 === ix2 && iy1 === iy2) {
break;
}
const e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
ix1 += sx;
}
if (e2 < dx) {
err += dx;
iy1 += sy;
}
}
return true;
}
__name(bresenhamLineOfSight, "bresenhamLineOfSight");
function raycastLineOfSight(x1, y1, x2, y2, map, stepSize = 0.5) {
const dx = x2 - x1;
const dy = y2 - y1;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance === 0) {
return map.isWalkable(Math.floor(x1), Math.floor(y1));
}
const steps = Math.ceil(distance / stepSize);
const stepX = dx / steps;
const stepY = dy / steps;
let x = x1;
let y = y1;
for (let i = 0; i <= steps; i++) {
if (!map.isWalkable(Math.floor(x), Math.floor(y))) {
return false;
}
x += stepX;
y += stepY;
}
return true;
}
__name(raycastLineOfSight, "raycastLineOfSight");
var _LineOfSightSmoother = class _LineOfSightSmoother {
constructor(lineOfSight = bresenhamLineOfSight) {
__publicField(this, "lineOfSight");
this.lineOfSight = lineOfSight;
}
smooth(path, map) {
if (path.length <= 2) {
return [
...path
];
}
const result = [
path[0]
];
let current = 0;
while (current < path.length - 1) {
let furthest = current + 1;
for (let i = path.length - 1; i > current + 1; i--) {
if (this.lineOfSight(path[current].x, path[current].y, path[i].x, path[i].y, map)) {
furthest = i;
break;
}
}
result.push(path[furthest]);
current = furthest;
}
return result;
}
};
__name(_LineOfSightSmoother, "LineOfSightSmoother");
var LineOfSightSmoother = _LineOfSightSmoother;
var _CatmullRomSmoother = class _CatmullRomSmoother {
/**
* @param segments - @zh 每段之间的插值点数 @en Number of interpolation points per segment
* @param tension - @zh 张力 (0-1) @en Tension (0-1)
*/
constructor(segments = 5, tension = 0.5) {
__publicField(this, "segments");
__publicField(this, "tension");
this.segments = segments;
this.tension = tension;
}
smooth(path, _map) {
if (path.length <= 2) {
return [
...path
];
}
const result = [];
const points = [
path[0],
...path,
path[path.length - 1]
];
for (let i = 1; i < points.length - 2; i++) {
const p0 = points[i - 1];
const p1 = points[i];
const p2 = points[i + 1];
const p3 = points[i + 2];
for (let j = 0; j < this.segments; j++) {
const t = j / this.segments;
const point = this.interpolate(p0, p1, p2, p3, t);
result.push(point);
}
}
result.push(path[path.length - 1]);
return result;
}
/**
* @zh Catmull-Rom 插值
* @en Catmull-Rom interpolation
*/
interpolate(p0, p1, p2, p3, t) {
const t2 = t * t;
const t3 = t2 * t;
const tension = this.tension;
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);
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);
return createPoint(x, y);
}
};
__name(_CatmullRomSmoother, "CatmullRomSmoother");
var CatmullRomSmoother = _CatmullRomSmoother;
var _CombinedSmoother = class _CombinedSmoother {
constructor(curveSegments = 5, tension = 0.5) {
__publicField(this, "simplifier");
__publicField(this, "curveSmoother");
this.simplifier = new LineOfSightSmoother();
this.curveSmoother = new CatmullRomSmoother(curveSegments, tension);
}
smooth(path, map) {
const simplified = this.simplifier.smooth(path, map);
return this.curveSmoother.smooth(simplified, map);
}
};
__name(_CombinedSmoother, "CombinedSmoother");
var CombinedSmoother = _CombinedSmoother;
function createLineOfSightSmoother(lineOfSight) {
return new LineOfSightSmoother(lineOfSight);
}
__name(createLineOfSightSmoother, "createLineOfSightSmoother");
function createCatmullRomSmoother(segments, tension) {
return new CatmullRomSmoother(segments, tension);
}
__name(createCatmullRomSmoother, "createCatmullRomSmoother");
function createCombinedSmoother(curveSegments, tension) {
return new CombinedSmoother(curveSegments, tension);
}
__name(createCombinedSmoother, "createCombinedSmoother");
// src/nodes/PathfindingNodes.ts
var FindPathTemplate = {
type: "FindPath",
title: "Find Path",
category: "custom",
description: "Find path from start to end / \u4ECE\u8D77\u70B9\u5230\u7EC8\u70B9\u5BFB\u8DEF",
keywords: [
"path",
"pathfinding",
"astar",
"navigate",
"route"
],
menuPath: [
"Pathfinding",
"Find Path"
],
inputs: [
{
name: "exec",
displayName: "",
type: "exec"
},
{
name: "startX",
displayName: "Start X",
type: "float"
},
{
name: "startY",
displayName: "Start Y",
type: "float"
},
{
name: "endX",
displayName: "End X",
type: "float"
},
{
name: "endY",
displayName: "End Y",
type: "float"
}
],
outputs: [
{
name: "exec",
displayName: "",
type: "exec"
},
{
name: "found",
displayName: "Found",
type: "bool"
},
{
name: "path",
displayName: "Path",
type: "array"
},
{
name: "cost",
displayName: "Cost",
type: "float"
}
],
color: "#4caf50"
};
var _FindPathExecutor = class _FindPathExecutor {
execute(node, context) {
const ctx = context;
const startX = ctx.evaluateInput(node.id, "startX", 0);
const startY = ctx.evaluateInput(node.id, "startY", 0);
const endX = ctx.evaluateInput(node.id, "endX", 0);
const endY = ctx.evaluateInput(node.id, "endY", 0);
const result = ctx.findPath(startX, startY, endX, endY);
return {
outputs: {
found: result.found,
path: result.path,
cost: result.cost
},
nextExec: "exec"
};
}
};
__name(_FindPathExecutor, "FindPathExecutor");
var FindPathExecutor = _FindPathExecutor;
var FindPathSmoothTemplate = {
type: "FindPathSmooth",
title: "Find Path (Smooth)",
category: "custom",
description: "Find path with smoothing / \u5BFB\u8DEF\u5E76\u5E73\u6ED1\u8DEF\u5F84",
keywords: [
"path",
"pathfinding",
"smooth",
"navigate"
],
menuPath: [
"Pathfinding",
"Find Path (Smooth)"
],
inputs: [
{
name: "exec",
displayName: "",
type: "exec"
},
{
name: "startX",
displayName: "Start X",
type: "float"
},
{
name: "startY",
displayName: "Start Y",
type: "float"
},
{
name: "endX",
displayName: "End X",
type: "float"
},
{
name: "endY",
displayName: "End Y",
type: "float"
}
],
outputs: [
{
name: "exec",
displayName: "",
type: "exec"
},
{
name: "found",
displayName: "Found",
type: "bool"
},
{
name: "path",
displayName: "Path",
type: "array"
},
{
name: "cost",
displayName: "Cost",
type: "float"
}
],
color: "#4caf50"
};
var _FindPathSmoothExecutor = class _FindPathSmoothExecutor {
execute(node, context) {
const ctx = context;
const startX = ctx.evaluateInput(node.id, "startX", 0);
const startY = ctx.evaluateInput(node.id, "startY", 0);
const endX = ctx.evaluateInput(node.id, "endX", 0);
const endY = ctx.evaluateInput(node.id, "endY", 0);
const result = ctx.findPathSmooth(startX, startY, endX, endY);
return {
outputs: {
found: result.found,
path: result.path,
cost: result.cost
},
nextExec: "exec"
};
}
};
__name(_FindPathSmoothExecutor, "FindPathSmoothExecutor");
var FindPathSmoothExecutor = _FindPathSmoothExecutor;
var IsWalkableTemplate = {
type: "IsWalkable",
title: "Is Walkable",
category: "custom",
description: "Check if position is walkable / \u68C0\u67E5\u4F4D\u7F6E\u662F\u5426\u53EF\u901A\u884C",
keywords: [
"walkable",
"obstacle",
"blocked",
"terrain"
],
menuPath: [
"Pathfinding",
"Is Walkable"
],
isPure: true,
inputs: [
{
name: "x",
displayName: "X",
type: "float"
},
{
name: "y",
displayName: "Y",
type: "float"
}
],
outputs: [
{
name: "walkable",
displayName: "Walkable",
type: "bool"
}
],
color: "#4caf50"
};
var _IsWalkableExecutor = class _IsWalkableExecutor {
execute(node, context) {
const ctx = context;
const x = ctx.evaluateInput(node.id, "x", 0);
const y = ctx.evaluateInput(node.id, "y", 0);
const walkable = ctx.isWalkable(x, y);
return {
outputs: {
walkable
}
};
}
};
__name(_IsWalkableExecutor, "IsWalkableExecutor");
var IsWalkableExecutor = _IsWalkableExecutor;
var GetPathLengthTemplate = {
type: "GetPathLength",
title: "Get Path Length",
category: "custom",
description: "Get the number of points in path / \u83B7\u53D6\u8DEF\u5F84\u70B9\u6570\u91CF",
keywords: [
"path",
"length",
"count",
"waypoints"
],
menuPath: [
"Pathfinding",
"Get Path Length"
],
isPure: true,
inputs: [
{
name: "path",
displayName: "Path",
type: "array"
}
],
outputs: [
{
name: "length",
displayName: "Length",
type: "int"
}
],
color: "#4caf50"
};
var _GetPathLengthExecutor = class _GetPathLengthExecutor {
execute(node, context) {
const ctx = context;
const path = ctx.evaluateInput(node.id, "path", []);
return {
outputs: {
length: path.length
}
};
}
};
__name(_GetPathLengthExecutor, "GetPathLengthExecutor");
var GetPathLengthExecutor = _GetPathLengthExecutor;
var GetPathDistanceTemplate = {
type: "GetPathDistance",
title: "Get Path Distance",
category: "custom",
description: "Get total path distance / \u83B7\u53D6\u8DEF\u5F84\u603B\u8DDD\u79BB",
keywords: [
"path",
"distance",
"length",
"travel"
],
menuPath: [
"Pathfinding",
"Get Path Distance"
],
isPure: true,
inputs: [
{
name: "path",
displayName: "Path",
type: "array"
}
],
outputs: [
{
name: "distance",
displayName: "Distance",
type: "float"
}
],
color: "#4caf50"
};
var _GetPathDistanceExecutor = class _GetPathDistanceExecutor {
execute(node, context) {
const ctx = context;
const path = ctx.evaluateInput(node.id, "path", []);
const distance = ctx.getPathDistance(path);
return {
outputs: {
distance
}
};
}
};
__name(_GetPathDistanceExecutor, "GetPathDistanceExecutor");
var GetPathDistanceExecutor = _GetPathDistanceExecutor;
var GetPathPointTemplate = {
type: "GetPathPoint",
title: "Get Path Point",
category: "custom",
description: "Get point at index in path / \u83B7\u53D6\u8DEF\u5F84\u4E2D\u6307\u5B9A\u7D22\u5F15\u7684\u70B9",
keywords: [
"path",
"point",
"waypoint",
"index"
],
menuPath: [
"Pathfinding",
"Get Path Point"
],
isPure: true,
inputs: [
{
name: "path",
displayName: "Path",
type: "array"
},
{
name: "index",
displayName: "Index",
type: "int"
}
],
outputs: [
{
name: "x",
displayName: "X",
type: "float"
},
{
name: "y",
displayName: "Y",
type: "float"
},
{
name: "valid",
displayName: "Valid",
type: "bool"
}
],
color: "#4caf50"
};
var _GetPathPointExecutor = class _GetPathPointExecutor {
execute(node, context) {
const ctx = context;
const path = ctx.evaluateInput(node.id, "path", []);
const index = ctx.evaluateInput(node.id, "index", 0);
if (index >= 0 && index < path.length) {
return {
outputs: {
x: path[index].x,
y: path[index].y,
valid: true
}
};
}
return {
outputs: {
x: 0,
y: 0,
valid: false
}
};
}
};
__name(_GetPathPointExecutor, "GetPathPointExecutor");
var GetPathPointExecutor = _GetPathPointExecutor;
var MoveAlongPathTemplate = {
type: "MoveAlongPath",
title: "Move Along Path",
category: "custom",
description: "Get position along path at progress / \u83B7\u53D6\u8DEF\u5F84\u4E0A\u6307\u5B9A\u8FDB\u5EA6\u7684\u4F4D\u7F6E",
keywords: [
"path",
"move",
"lerp",
"progress",
"interpolate"
],
menuPath: [
"Pathfinding",
"Move Along Path"
],
isPure: true,
inputs: [
{
name: "path",
displayName: "Path",
type: "array"
},
{
name: "progress",
displayName: "Progress (0-1)",
type: "float"
}
],
outputs: [
{
name: "x",
displayName: "X",
type: "float"
},
{
name: "y",
displayName: "Y",
type: "float"
}
],
color: "#4caf50"
};
var _MoveAlongPathExecutor = class _MoveAlongPathExecutor {
execute(node, context) {
const ctx = context;
const path = ctx.evaluateInput(node.id, "path", []);
let progress = ctx.evaluateInput(node.id, "progress", 0);
if (path.length === 0) {
return {
outputs: {
x: 0,
y: 0
}
};
}
if (path.length === 1) {
return {
outputs: {
x: path[0].x,
y: path[0].y
}
};
}
progress = Math.max(0, Math.min(1, progress));
let totalDistance = 0;
const segmentDistances = [];
for (let i = 1; i < path.length; i++) {
const dx = path[i].x - path[i - 1].x;
const dy = path[i].y - path[i - 1].y;
const dist = Math.sqrt(dx * dx + dy * dy);
segmentDistances.push(dist);
totalDistance += dist;
}
if (totalDistance === 0) {
return {
outputs: {
x: path[0].x,
y: path[0].y
}
};
}
const targetDistance = progress * totalDistance;
let accumulatedDistance = 0;
for (let i = 0; i < segmentDistances.length; i++) {
const segmentDist = segmentDistances[i];
if (accumulatedDistance + segmentDist >= targetDistance) {
const segmentProgress = (targetDistance - accumulatedDistance) / segmentDist;
const x = path[i].x + (path[i + 1].x - path[i].x) * segmentProgress;
const y = path[i].y + (path[i + 1].y - path[i].y) * segmentProgress;
return {
outputs: {
x,
y
}
};
}
accumulatedDistance += segmentDist;
}
const last = path[path.length - 1];
return {
outputs: {
x: last.x,
y: last.y
}
};
}
};
__name(_MoveAlongPathExecutor, "MoveAlongPathExecutor");
var MoveAlongPathExecutor = _MoveAlongPathExecutor;
var HasLineOfSightTemplate = {
type: "HasLineOfSight",
title: "Has Line of Sight",
category: "custom",
description: "Check if there is a clear line between two points / \u68C0\u67E5\u4E24\u70B9\u4E4B\u95F4\u662F\u5426\u6709\u6E05\u6670\u7684\u89C6\u7EBF",
keywords: [
"line",
"sight",
"los",
"visibility",
"raycast"
],
menuPath: [
"Pathfinding",
"Has Line of Sight"
],
isPure: true,
inputs: [
{
name: "startX",
displayName: "Start X",
type: "float"
},
{
name: "startY",
displayName: "Start Y",
type: "float"
},
{
name: "endX",
displayName: "End X",
type: "float"
},
{
name: "endY",
displayName: "End Y",
type: "float"
}
],
outputs: [
{
name: "hasLOS",
displayName: "Has LOS",
type: "bool"
}
],
color: "#4caf50"
};
var _HasLineOfSightExecutor = class _HasLineOfSightExecutor {
execute(node, context) {
const ctx = context;
const startX = ctx.evaluateInput(node.id, "startX", 0);
const startY = ctx.evaluateInput(node.id, "startY", 0);
const endX = ctx.evaluateInput(node.id, "endX", 0);
const endY = ctx.evaluateInput(node.id, "endY", 0);
const hasLOS = ctx.hasLineOfSight?.(startX, startY, endX, endY) ?? true;
return {
outputs: {
hasLOS
}
};
}
};
__name(_HasLineOfSightExecutor, "HasLineOfSightExecutor");
var HasLineOfSightExecutor = _HasLineOfSightExecutor;
var PathfindingNodeDefinitions = {
templates: [
FindPathTemplate,
FindPathSmoothTemplate,
IsWalkableTemplate,
GetPathLengthTemplate,
GetPathDistanceTemplate,
GetPathPointTemplate,
MoveAlongPathTemplate,
HasLineOfSightTemplate
],
executors: /* @__PURE__ */ new Map([
[
"FindPath",
new FindPathExecutor()
],
[
"FindPathSmooth",
new FindPathSmoothExecutor()
],
[
"IsWalkable",
new IsWalkableExecutor()
],
[
"GetPathLength",
new GetPathLengthExecutor()
],
[
"GetPathDistance",
new GetPathDistanceExecutor()
],
[
"GetPathPoint",
new GetPathPointExecutor()
],
[
"MoveAlongPath",
new MoveAlongPathExecutor()
],
[
"HasLineOfSight",
new HasLineOfSightExecutor()
]
])
};
export {
AStarPathfinder,
BinaryHeap,
CatmullRomSmoother,
CombinedSmoother,
DEFAULT_GRID_OPTIONS,
DEFAULT_PATHFINDING_OPTIONS,
DIRECTIONS_4,
DIRECTIONS_8,
EMPTY_PATH_RESULT,
FindPathExecutor,
FindPathSmoothExecutor,
FindPathSmoothTemplate,
FindPathTemplate,
GetPathDistanceExecutor,
GetPathDistanceTemplate,
GetPathLengthExecutor,
GetPathLengthTemplate,
GetPathPointExecutor,
GetPathPointTemplate,
GridMap,
GridNode,
HasLineOfSightExecutor,
HasLineOfSightTemplate,
IsWalkableExecutor,
IsWalkableTemplate,
LineOfSightSmoother,
MoveAlongPathExecutor,
MoveAlongPathTemplate,
NavMesh,
PathfindingNodeDefinitions,
bresenhamLineOfSight,
chebyshevDistance,
createAStarPathfinder,
createCatmullRomSmoother,
createCombinedSmoother,
createGridMap,
createLineOfSightSmoother,
createNavMesh,
createPoint,
euclideanDistance,
manhattanDistance,
octileDistance,
raycastLineOfSight
};
//# sourceMappingURL=index.js.map