UNPKG

@esengine/pathfinding

Version:

寻路系统 | Pathfinding System - A*, Grid, NavMesh

1,826 lines (1,819 loc) 45 kB
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