@esengine/pathfinding
Version:
寻路系统 | Pathfinding System - A*, Grid, NavMesh
1,855 lines (1,851 loc) • 75.4 kB
JavaScript
import {
AStarPathfinder,
BinaryHeap,
CollisionResolverAdapter,
DEFAULT_FLOW_CONTROLLER_CONFIG,
DEFAULT_HPA_CONFIG,
DEFAULT_ORCA_PARAMS,
DEFAULT_PATHFINDING_OPTIONS,
DEFAULT_PATH_CACHE_CONFIG,
EMPTY_COLLISION_RESULT,
EMPTY_PATH_RESULT,
EMPTY_PLAN_RESULT,
FlowController,
GridPathfinderAdapter,
HPAPathfinder,
IncrementalAStarPathfinder,
IncrementalGridPathPlannerAdapter,
IndexedBinaryHeap,
JPSPathfinder,
NavMeshPathPlannerAdapter,
ORCALocalAvoidanceAdapter,
PassPermission,
PathCache,
PathPlanState,
chebyshevDistance,
createAStarPathfinder,
createAStarPlanner,
createDefaultCollisionResolver,
createFlowController,
createHPAPathfinder,
createHPAPlanner,
createIncrementalAStarPathfinder,
createIncrementalAStarPlanner,
createJPSPathfinder,
createJPSPlanner,
createNavMeshPathPlanner,
createORCAAvoidance,
createPathCache,
createPoint,
euclideanDistance,
isIncrementalPlanner,
manhattanDistance,
octileDistance
} from "./chunk-ZYGBA7VK.js";
import {
DEFAULT_REPLANNING_CONFIG,
EMPTY_PROGRESS,
PathfindingState
} from "./chunk-YKA3PWU3.js";
import "./chunk-H5EFZBBT.js";
import {
CollisionResolver,
DEFAULT_AGENT_PARAMS,
DEFAULT_COLLISION_CONFIG,
DEFAULT_ORCA_CONFIG,
EMPTY_COLLISION,
KDTree,
ORCASolver,
createCollisionResolver,
createKDTree,
createORCASolver,
solveORCALinearProgram
} from "./chunk-3VEX32JO.js";
import {
__name,
__publicField
} from "./chunk-T626JPC7.js";
// src/core/GridPathfinder.ts
var CLOSED_FLAG = 1;
var OPENED_FLAG = 2;
var BACKWARD_CLOSED = 4;
var BACKWARD_OPENED = 8;
var _a;
var GridState = (_a = class {
constructor(width, height, bidirectional = false) {
__publicField(this, "size");
__publicField(this, "width");
__publicField(this, "g");
__publicField(this, "f");
__publicField(this, "flags");
__publicField(this, "parent");
__publicField(this, "heapIndex");
__publicField(this, "version");
__publicField(this, "currentVersion", 1);
// 双向搜索额外状态
__publicField(this, "gBack", null);
__publicField(this, "fBack", null);
__publicField(this, "parentBack", null);
__publicField(this, "heapIndexBack", null);
this.width = width;
this.size = width * height;
this.g = new Float32Array(this.size);
this.f = new Float32Array(this.size);
this.flags = new Uint8Array(this.size);
this.parent = new Int32Array(this.size);
this.heapIndex = new Int32Array(this.size);
this.version = new Uint32Array(this.size);
if (bidirectional) {
this.gBack = new Float32Array(this.size);
this.fBack = new Float32Array(this.size);
this.parentBack = new Int32Array(this.size);
this.heapIndexBack = new Int32Array(this.size);
}
}
reset() {
this.currentVersion++;
if (this.currentVersion > 4294967295) {
this.version.fill(0);
this.currentVersion = 1;
}
}
isInit(i) {
return this.version[i] === this.currentVersion;
}
init(i) {
if (!this.isInit(i)) {
this.g[i] = Infinity;
this.f[i] = Infinity;
this.flags[i] = 0;
this.parent[i] = -1;
this.heapIndex[i] = -1;
if (this.gBack) {
this.gBack[i] = Infinity;
this.fBack[i] = Infinity;
this.parentBack[i] = -1;
this.heapIndexBack[i] = -1;
}
this.version[i] = this.currentVersion;
}
}
// Forward
getG(i) {
return this.isInit(i) ? this.g[i] : Infinity;
}
setG(i, v) {
this.init(i);
this.g[i] = v;
}
getF(i) {
return this.isInit(i) ? this.f[i] : Infinity;
}
setF(i, v) {
this.init(i);
this.f[i] = v;
}
getParent(i) {
return this.isInit(i) ? this.parent[i] : -1;
}
setParent(i, v) {
this.init(i);
this.parent[i] = v;
}
getHeapIndex(i) {
return this.isInit(i) ? this.heapIndex[i] : -1;
}
setHeapIndex(i, v) {
this.init(i);
this.heapIndex[i] = v;
}
isClosed(i) {
return this.isInit(i) && (this.flags[i] & CLOSED_FLAG) !== 0;
}
setClosed(i) {
this.init(i);
this.flags[i] |= CLOSED_FLAG;
}
isOpened(i) {
return this.isInit(i) && (this.flags[i] & OPENED_FLAG) !== 0;
}
setOpened(i) {
this.init(i);
this.flags[i] |= OPENED_FLAG;
}
// Backward
getGBack(i) {
return this.isInit(i) ? this.gBack[i] : Infinity;
}
setGBack(i, v) {
this.init(i);
this.gBack[i] = v;
}
getFBack(i) {
return this.isInit(i) ? this.fBack[i] : Infinity;
}
setFBack(i, v) {
this.init(i);
this.fBack[i] = v;
}
getParentBack(i) {
return this.isInit(i) ? this.parentBack[i] : -1;
}
setParentBack(i, v) {
this.init(i);
this.parentBack[i] = v;
}
getHeapIndexBack(i) {
return this.isInit(i) ? this.heapIndexBack[i] : -1;
}
setHeapIndexBack(i, v) {
this.init(i);
this.heapIndexBack[i] = v;
}
isClosedBack(i) {
return this.isInit(i) && (this.flags[i] & BACKWARD_CLOSED) !== 0;
}
setClosedBack(i) {
this.init(i);
this.flags[i] |= BACKWARD_CLOSED;
}
isOpenedBack(i) {
return this.isInit(i) && (this.flags[i] & BACKWARD_OPENED) !== 0;
}
setOpenedBack(i) {
this.init(i);
this.flags[i] |= BACKWARD_OPENED;
}
}, __name(_a, "GridState"), _a);
var _a2;
var GridHeap = (_a2 = class {
constructor(state, isBack = false) {
__publicField(this, "heap", []);
__publicField(this, "state");
__publicField(this, "isBack");
this.state = state;
this.isBack = isBack;
}
get size() {
return this.heap.length;
}
get isEmpty() {
return this.heap.length === 0;
}
getF(i) {
return this.isBack ? this.state.getFBack(i) : this.state.getF(i);
}
getHeapIndex(i) {
return this.isBack ? this.state.getHeapIndexBack(i) : this.state.getHeapIndex(i);
}
setHeapIndex(i, v) {
if (this.isBack) this.state.setHeapIndexBack(i, v);
else this.state.setHeapIndex(i, v);
}
push(i) {
this.setHeapIndex(i, this.heap.length);
this.heap.push(i);
this.bubbleUp(this.heap.length - 1);
}
pop() {
if (this.heap.length === 0) return -1;
const result = this.heap[0];
this.setHeapIndex(result, -1);
const last = this.heap.pop();
if (this.heap.length > 0) {
this.heap[0] = last;
this.setHeapIndex(last, 0);
this.sinkDown(0);
}
return result;
}
update(i) {
const pos = this.getHeapIndex(i);
if (pos >= 0 && pos < this.heap.length) {
this.bubbleUp(pos);
this.sinkDown(this.getHeapIndex(i));
}
}
clear() {
this.heap.length = 0;
}
bubbleUp(pos) {
const idx = this.heap[pos];
const f = this.getF(idx);
while (pos > 0) {
const pp = pos - 1 >> 1;
const pi = this.heap[pp];
if (f >= this.getF(pi)) break;
this.heap[pos] = pi;
this.setHeapIndex(pi, pos);
pos = pp;
}
this.heap[pos] = idx;
this.setHeapIndex(idx, pos);
}
sinkDown(pos) {
const len = this.heap.length;
const idx = this.heap[pos];
const f = this.getF(idx);
const half = len >> 1;
while (pos < half) {
const left = (pos << 1) + 1;
const right = left + 1;
let smallest = pos, smallestF = f;
const lf = this.getF(this.heap[left]);
if (lf < smallestF) {
smallest = left;
smallestF = lf;
}
if (right < len) {
const rf = this.getF(this.heap[right]);
if (rf < smallestF) smallest = right;
}
if (smallest === pos) break;
const si = this.heap[smallest];
this.heap[pos] = si;
this.setHeapIndex(si, pos);
pos = smallest;
}
this.heap[pos] = idx;
this.setHeapIndex(idx, pos);
}
}, __name(_a2, "GridHeap"), _a2);
var _GridPathfinder = class _GridPathfinder {
constructor(map, config) {
__publicField(this, "map");
__publicField(this, "mode");
__publicField(this, "state");
__publicField(this, "openList");
__publicField(this, "openListBack");
this.map = map;
this.mode = config?.mode ?? "fast";
const isBidir = this.mode === "bidirectional";
this.state = new GridState(map.width, map.height, isBidir);
this.openList = new GridHeap(this.state, false);
this.openListBack = isBidir ? new GridHeap(this.state, true) : null;
}
findPath(startX, startY, endX, endY, options) {
if (this.mode === "bidirectional") {
return this.findPathBidirectional(startX, startY, endX, endY, options);
}
return this.findPathUnidirectional(startX, startY, endX, endY, options);
}
findPathUnidirectional(startX, startY, endX, endY, options) {
const opts = options ? {
...DEFAULT_PATHFINDING_OPTIONS,
...options
} : DEFAULT_PATHFINDING_OPTIONS;
const { width, height } = this.map;
this.state.reset();
this.openList.clear();
if (!this.validate(startX, startY, endX, endY)) return EMPTY_PATH_RESULT;
const startIdx = startY * width + startX;
const endIdx = endY * width + endX;
if (startIdx === endIdx) {
return {
found: true,
path: [
{
x: startX,
y: startY
}
],
cost: 0,
nodesSearched: 1
};
}
const hw = opts.heuristicWeight;
const h0 = this.map.heuristic({
x: startX,
y: startY
}, {
x: endX,
y: endY
}) * hw;
this.state.setG(startIdx, 0);
this.state.setF(startIdx, h0);
this.state.setOpened(startIdx);
this.openList.push(startIdx);
let searched = 0;
const maxNodes = opts.maxNodes;
const { allowDiagonal, avoidCorners, diagonalCost } = this.map["options"];
const nodes = this.map["nodes"];
const dx = allowDiagonal ? [
0,
1,
1,
1,
0,
-1,
-1,
-1
] : [
0,
1,
0,
-1
];
const dy = allowDiagonal ? [
-1,
-1,
0,
1,
1,
1,
0,
-1
] : [
-1,
0,
1,
0
];
const dirCount = dx.length;
while (!this.openList.isEmpty && searched < maxNodes) {
const cur = this.openList.pop();
this.state.setClosed(cur);
searched++;
if (cur === endIdx) {
return this.buildPath(startIdx, endIdx, searched);
}
const cx = cur % width, cy = cur / width | 0;
const curG = this.state.getG(cur);
for (let d = 0; d < dirCount; d++) {
const nx = cx + dx[d], ny = cy + dy[d];
if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue;
const neighbor = nodes[ny][nx];
if (!neighbor.walkable) continue;
if (avoidCorners && dx[d] !== 0 && dy[d] !== 0) {
if (!nodes[cy][cx + dx[d]].walkable || !nodes[cy + dy[d]][cx].walkable) continue;
}
const ni = ny * width + nx;
if (this.state.isClosed(ni)) continue;
const isDiag = dx[d] !== 0 && dy[d] !== 0;
const cost = isDiag ? neighbor.cost * diagonalCost : neighbor.cost;
const tentG = curG + cost;
if (!this.state.isOpened(ni)) {
const h = this.map.heuristic({
x: nx,
y: ny
}, {
x: endX,
y: endY
}) * hw;
this.state.setG(ni, tentG);
this.state.setF(ni, tentG + h);
this.state.setParent(ni, cur);
this.state.setOpened(ni);
this.openList.push(ni);
} else if (tentG < this.state.getG(ni)) {
const h = this.state.getF(ni) - this.state.getG(ni);
this.state.setG(ni, tentG);
this.state.setF(ni, tentG + h);
this.state.setParent(ni, cur);
this.openList.update(ni);
}
}
}
return {
found: false,
path: [],
cost: 0,
nodesSearched: searched
};
}
findPathBidirectional(startX, startY, endX, endY, options) {
const opts = options ? {
...DEFAULT_PATHFINDING_OPTIONS,
...options
} : DEFAULT_PATHFINDING_OPTIONS;
const { width, height } = this.map;
this.state.reset();
this.openList.clear();
this.openListBack.clear();
if (!this.validate(startX, startY, endX, endY)) return EMPTY_PATH_RESULT;
const startIdx = startY * width + startX;
const endIdx = endY * width + endX;
if (startIdx === endIdx) {
return {
found: true,
path: [
{
x: startX,
y: startY
}
],
cost: 0,
nodesSearched: 1
};
}
const hw = opts.heuristicWeight;
const startPos = {
x: startX,
y: startY
};
const endPos = {
x: endX,
y: endY
};
const hf = this.map.heuristic(startPos, endPos) * hw;
this.state.setG(startIdx, 0);
this.state.setF(startIdx, hf);
this.state.setOpened(startIdx);
this.openList.push(startIdx);
const hb = this.map.heuristic(endPos, startPos) * hw;
this.state.setGBack(endIdx, 0);
this.state.setFBack(endIdx, hb);
this.state.setOpenedBack(endIdx);
this.openListBack.push(endIdx);
let searched = 0;
const maxNodes = opts.maxNodes;
let meetIdx = -1, bestCost = Infinity;
const { allowDiagonal, avoidCorners, diagonalCost } = this.map["options"];
const nodes = this.map["nodes"];
const dx = allowDiagonal ? [
0,
1,
1,
1,
0,
-1,
-1,
-1
] : [
0,
1,
0,
-1
];
const dy = allowDiagonal ? [
-1,
-1,
0,
1,
1,
1,
0,
-1
] : [
-1,
0,
1,
0
];
const dirCount = dx.length;
while ((!this.openList.isEmpty || !this.openListBack.isEmpty) && searched < maxNodes) {
if (!this.openList.isEmpty) {
const cur = this.openList.pop();
this.state.setClosed(cur);
searched++;
const curG = this.state.getG(cur);
if (this.state.isClosedBack(cur)) {
const total = curG + this.state.getGBack(cur);
if (total < bestCost) {
bestCost = total;
meetIdx = cur;
}
}
if (meetIdx !== -1 && curG >= bestCost) break;
const cx = cur % width, cy = cur / width | 0;
for (let d = 0; d < dirCount; d++) {
const nx = cx + dx[d], ny = cy + dy[d];
if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue;
const neighbor = nodes[ny][nx];
if (!neighbor.walkable) continue;
if (avoidCorners && dx[d] !== 0 && dy[d] !== 0) {
if (!nodes[cy][cx + dx[d]].walkable || !nodes[cy + dy[d]][cx].walkable) continue;
}
const ni = ny * width + nx;
if (this.state.isClosed(ni)) continue;
const isDiag = dx[d] !== 0 && dy[d] !== 0;
const cost = isDiag ? neighbor.cost * diagonalCost : neighbor.cost;
const tentG = curG + cost;
if (!this.state.isOpened(ni)) {
const h = this.map.heuristic({
x: nx,
y: ny
}, endPos) * hw;
this.state.setG(ni, tentG);
this.state.setF(ni, tentG + h);
this.state.setParent(ni, cur);
this.state.setOpened(ni);
this.openList.push(ni);
} else if (tentG < this.state.getG(ni)) {
const h = this.state.getF(ni) - this.state.getG(ni);
this.state.setG(ni, tentG);
this.state.setF(ni, tentG + h);
this.state.setParent(ni, cur);
this.openList.update(ni);
}
}
}
if (!this.openListBack.isEmpty) {
const cur = this.openListBack.pop();
this.state.setClosedBack(cur);
searched++;
const curG = this.state.getGBack(cur);
if (this.state.isClosed(cur)) {
const total = curG + this.state.getG(cur);
if (total < bestCost) {
bestCost = total;
meetIdx = cur;
}
}
if (meetIdx !== -1 && curG >= bestCost) break;
const cx = cur % width, cy = cur / width | 0;
for (let d = 0; d < dirCount; d++) {
const nx = cx + dx[d], ny = cy + dy[d];
if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue;
const neighbor = nodes[ny][nx];
if (!neighbor.walkable) continue;
if (avoidCorners && dx[d] !== 0 && dy[d] !== 0) {
if (!nodes[cy][cx + dx[d]].walkable || !nodes[cy + dy[d]][cx].walkable) continue;
}
const ni = ny * width + nx;
if (this.state.isClosedBack(ni)) continue;
const isDiag = dx[d] !== 0 && dy[d] !== 0;
const cost = isDiag ? neighbor.cost * diagonalCost : neighbor.cost;
const tentG = curG + cost;
if (!this.state.isOpenedBack(ni)) {
const h = this.map.heuristic({
x: nx,
y: ny
}, startPos) * hw;
this.state.setGBack(ni, tentG);
this.state.setFBack(ni, tentG + h);
this.state.setParentBack(ni, cur);
this.state.setOpenedBack(ni);
this.openListBack.push(ni);
} else if (tentG < this.state.getGBack(ni)) {
const h = this.state.getFBack(ni) - this.state.getGBack(ni);
this.state.setGBack(ni, tentG);
this.state.setFBack(ni, tentG + h);
this.state.setParentBack(ni, cur);
this.openListBack.update(ni);
}
}
}
}
if (meetIdx === -1) {
return {
found: false,
path: [],
cost: 0,
nodesSearched: searched
};
}
return this.buildPathBidirectional(startIdx, endIdx, meetIdx, searched);
}
validate(startX, startY, endX, endY) {
const { width, height } = this.map;
if (startX < 0 || startX >= width || startY < 0 || startY >= height) return false;
if (endX < 0 || endX >= width || endY < 0 || endY >= height) return false;
return this.map.isWalkable(startX, startY) && this.map.isWalkable(endX, endY);
}
buildPath(startIdx, endIdx, searched) {
const w = this.state.width;
const path = [];
let cur = endIdx;
while (cur !== -1) {
path.push({
x: cur % w,
y: cur / w | 0
});
cur = cur === startIdx ? -1 : this.state.getParent(cur);
}
path.reverse();
return {
found: true,
path,
cost: this.state.getG(endIdx),
nodesSearched: searched
};
}
buildPathBidirectional(startIdx, endIdx, meetIdx, searched) {
const w = this.state.width;
const path = [];
let cur = meetIdx;
while (cur !== -1 && cur !== startIdx) {
path.push({
x: cur % w,
y: cur / w | 0
});
cur = this.state.getParent(cur);
}
path.push({
x: startIdx % w,
y: startIdx / w | 0
});
path.reverse();
cur = this.state.getParentBack(meetIdx);
while (cur !== -1 && cur !== endIdx) {
path.push({
x: cur % w,
y: cur / w | 0
});
cur = this.state.getParentBack(cur);
}
if (meetIdx !== endIdx) {
path.push({
x: endIdx % w,
y: endIdx / w | 0
});
}
const cost = this.state.getG(meetIdx) + this.state.getGBack(meetIdx);
return {
found: true,
path,
cost,
nodesSearched: searched
};
}
clear() {
this.state.reset();
this.openList.clear();
this.openListBack?.clear();
}
};
__name(_GridPathfinder, "GridPathfinder");
var GridPathfinder = _GridPathfinder;
function createGridPathfinder(map, config) {
return new GridPathfinder(map, config);
}
__name(createGridPathfinder, "createGridPathfinder");
// src/core/PathValidator.ts
var _PathValidator = class _PathValidator {
/**
* @zh 验证路径段的有效性
* @en Validate path segment validity
*
* @param path - @zh 要验证的路径 @en Path to validate
* @param fromIndex - @zh 起始索引 @en Start index
* @param toIndex - @zh 结束索引 @en End index
* @param map - @zh 地图实例 @en Map instance
* @returns @zh 验证结果 @en Validation result
*/
validatePath(path, fromIndex, toIndex, map) {
const end = Math.min(toIndex, path.length);
for (let i = fromIndex; i < end; i++) {
const point = path[i];
const x = Math.floor(point.x);
const y = Math.floor(point.y);
if (!map.isWalkable(x, y)) {
return {
valid: false,
invalidIndex: i
};
}
if (i > fromIndex) {
const prev = path[i - 1];
if (!this.checkLineOfSight(prev.x, prev.y, point.x, point.y, map)) {
return {
valid: false,
invalidIndex: i
};
}
}
}
return {
valid: true,
invalidIndex: -1
};
}
/**
* @zh 检查两点之间的视线(使用 Bresenham 算法)
* @en Check line of sight between two points (using Bresenham algorithm)
*
* @param x1 - @zh 起点 X @en Start X
* @param y1 - @zh 起点 Y @en Start Y
* @param x2 - @zh 终点 X @en End X
* @param y2 - @zh 终点 Y @en End Y
* @param map - @zh 地图实例 @en Map instance
* @returns @zh 是否有视线 @en Whether there is line of sight
*/
checkLineOfSight(x1, y1, x2, y2, map) {
const ix1 = Math.floor(x1);
const iy1 = Math.floor(y1);
const ix2 = Math.floor(x2);
const iy2 = Math.floor(y2);
let dx = Math.abs(ix2 - ix1);
let dy = Math.abs(iy2 - iy1);
let x = ix1;
let y = iy1;
const sx = ix1 < ix2 ? 1 : -1;
const sy = iy1 < iy2 ? 1 : -1;
if (dx > dy) {
let err = dx / 2;
while (x !== ix2) {
if (!map.isWalkable(x, y)) {
return false;
}
err -= dy;
if (err < 0) {
y += sy;
err += dx;
}
x += sx;
}
} else {
let err = dy / 2;
while (y !== iy2) {
if (!map.isWalkable(x, y)) {
return false;
}
err -= dx;
if (err < 0) {
x += sx;
err += dy;
}
y += sy;
}
}
return map.isWalkable(ix2, iy2);
}
};
__name(_PathValidator, "PathValidator");
var PathValidator = _PathValidator;
var _ObstacleChangeManager = class _ObstacleChangeManager {
constructor() {
__publicField(this, "changes", /* @__PURE__ */ new Map());
__publicField(this, "epoch", 0);
}
/**
* @zh 记录障碍物变化
* @en Record obstacle change
*
* @param x - @zh X 坐标 @en X coordinate
* @param y - @zh Y 坐标 @en Y coordinate
* @param wasWalkable - @zh 变化前是否可通行 @en Was walkable before change
*/
recordChange(x, y, wasWalkable) {
const key = `${x},${y}`;
this.changes.set(key, {
x,
y,
wasWalkable,
timestamp: Date.now()
});
}
/**
* @zh 获取影响区域
* @en Get affected region
*
* @returns @zh 影响区域或 null(如果没有变化)@en Affected region or null if no changes
*/
getAffectedRegion() {
if (this.changes.size === 0) {
return null;
}
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
for (const change of this.changes.values()) {
minX = Math.min(minX, change.x);
minY = Math.min(minY, change.y);
maxX = Math.max(maxX, change.x);
maxY = Math.max(maxY, change.y);
}
return {
minX,
minY,
maxX,
maxY
};
}
/**
* @zh 获取所有变化
* @en Get all changes
*
* @returns @zh 变化列表 @en List of changes
*/
getChanges() {
return Array.from(this.changes.values());
}
/**
* @zh 检查是否有变化
* @en Check if there are changes
*
* @returns @zh 是否有变化 @en Whether there are changes
*/
hasChanges() {
return this.changes.size > 0;
}
/**
* @zh 获取当前 epoch
* @en Get current epoch
*
* @returns @zh 当前 epoch @en Current epoch
*/
getEpoch() {
return this.epoch;
}
/**
* @zh 清空变化记录并推进 epoch
* @en Clear changes and advance epoch
*/
flush() {
this.changes.clear();
this.epoch++;
}
/**
* @zh 清空所有状态
* @en Clear all state
*/
clear() {
this.changes.clear();
this.epoch = 0;
}
};
__name(_ObstacleChangeManager, "ObstacleChangeManager");
var ObstacleChangeManager = _ObstacleChangeManager;
function createPathValidator() {
return new PathValidator();
}
__name(createPathValidator, "createPathValidator");
function createObstacleChangeManager() {
return new ObstacleChangeManager();
}
__name(createObstacleChangeManager, "createObstacleChangeManager");
// src/grid/GridMap.ts
var _GridNode = class _GridNode {
constructor(x, y, width, 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 = y * width + x;
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");
if (width <= 0 || !Number.isFinite(width) || !Number.isInteger(width)) {
throw new Error(`width must be a positive integer, got: ${width}`);
}
if (height <= 0 || !Number.isFinite(height) || !Number.isInteger(height)) {
throw new Error(`height must be a positive integer, got: ${height}`);
}
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, this.width, 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
*
* @param x - @zh X 坐标 @en X coordinate
* @param y - @zh Y 坐标 @en Y coordinate
* @param cost - @zh 移动代价,必须为正数 @en Movement cost, must be positive
* @throws @zh 如果 cost 不是正数则抛出错误 @en Throws if cost is not positive
*/
setCost(x, y, cost) {
if (cost <= 0 || !Number.isFinite(cost)) {
throw new Error(`cost must be a positive finite number, got: ${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 (nx < 0 || nx >= this.width || ny < 0 || ny >= this.height) {
continue;
}
const neighbor = this.nodes[ny][nx];
if (!neighbor.walkable) {
continue;
}
if (this.options.avoidCorners && dir.dx !== 0 && dir.dy !== 0) {
const hNode = this.nodes[y][x + dir.dx];
const vNode = this.nodes[y + dir.dy][x];
if (!hNode.walkable || !vNode.walkable) {
continue;
}
}
neighbors.push(neighbor);
}
return neighbors;
}
/**
* @zh 遍历节点的邻居(零分配)
* @en Iterate over neighbors (zero allocation)
*/
forEachNeighbor(node, callback) {
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 (nx < 0 || nx >= this.width || ny < 0 || ny >= this.height) {
continue;
}
const neighbor = this.nodes[ny][nx];
if (!neighbor.walkable) {
continue;
}
if (this.options.avoidCorners && dir.dx !== 0 && dir.dy !== 0) {
const hNode = this.nodes[y][x + dir.dx];
const vNode = this.nodes[y + dir.dy][x];
if (!hNode.walkable || !vNode.walkable) {
continue;
}
}
if (callback(neighbor) === false) {
return;
}
}
}
/**
* @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 _a3;
var NavMeshNode = (_a3 = 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(_a3, "NavMeshNode"), _a3);
var _NavMesh = class _NavMesh {
constructor() {
__publicField(this, "polygons", /* @__PURE__ */ new Map());
__publicField(this, "nodes", /* @__PURE__ */ new Map());
__publicField(this, "nextId", 0);
// @zh 动态障碍物支持
// @en Dynamic obstacle support
__publicField(this, "obstacles", /* @__PURE__ */ new Map());
__publicField(this, "nextObstacleId", 0);
__publicField(this, "disabledPolygons", /* @__PURE__ */ new Set());
}
/**
* @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, opts.agentRadius);
return {
found: true,
path: pointPath,
cost: this.calculatePathLength(pointPath),
nodesSearched: polygonPath.nodesSearched
};
}
/**
* @zh 在多边形图上寻路
* @en Find path on polygon graph
*
* @param start - @zh 起始多边形 @en Start polygon
* @param end - @zh 目标多边形 @en End polygon
* @param opts - @zh 寻路选项 @en Pathfinding options
* @param checkObstacles - @zh 是否检查障碍物 @en Whether to check obstacles
*/
findPolygonPath(start, end, opts, checkObstacles = false) {
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;
}
if (checkObstacles && this.isPolygonBlocked(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 (supports agent radius)
*
* @param start - @zh 起点 @en Start point
* @param end - @zh 终点 @en End point
* @param polygons - @zh 多边形路径 @en Polygon path
* @param agentRadius - @zh 代理半径 @en Agent radius
*/
funnelPath(start, end, polygons, agentRadius = 0) {
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) {
if (agentRadius > 0) {
const shrunk = this.shrinkPortal(portal.left, portal.right, agentRadius);
portals.push({
left: shrunk.left,
right: shrunk.right,
originalLeft: portal.left,
originalRight: portal.right
});
} else {
portals.push({
left: portal.left,
right: portal.right,
originalLeft: portal.left,
originalRight: portal.right
});
}
}
}
if (portals.length === 0) {
return [
start,
end
];
}
const path = [
start
];
let apex = start;
let apexOriginal = start;
let leftIndex = 0;
let rightIndex = 0;
let left = portals[0].left;
let right = portals[0].right;
let leftOriginal = portals[0].originalLeft;
let rightOriginal = portals[0].originalRight;
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 (this.pointsEqual(apex, right) || this.triArea2(apex, left, nextRight) > 0) {
right = nextRight;
rightIndex = i;
if (i < portals.length) {
rightOriginal = portals[i].originalRight;
}
} else {
const turnPoint = agentRadius > 0 ? this.offsetTurningPoint(apexOriginal, leftOriginal, left, agentRadius, "left") : left;
path.push(turnPoint);
apex = left;
apexOriginal = leftOriginal;
leftIndex = rightIndex = leftIndex;
left = right = apex;
leftOriginal = rightOriginal = apexOriginal;
i = leftIndex;
continue;
}
}
if (this.triArea2(apex, left, nextLeft) >= 0) {
if (this.pointsEqual(apex, left) || this.triArea2(apex, right, nextLeft) < 0) {
left = nextLeft;
leftIndex = i;
if (i < portals.length) {
leftOriginal = portals[i].originalLeft;
}
} else {
const turnPoint = agentRadius > 0 ? this.offsetTurningPoint(apexOriginal, rightOriginal, right, agentRadius, "right") : right;
path.push(turnPoint);
apex = right;
apexOriginal = rightOriginal;
leftIndex = rightIndex = rightIndex;
left = right = apex;
leftOriginal = rightOriginal = apexOriginal;
i = rightIndex;
continue;
}
}
}
path.push(end);
return path;
}
/**
* @zh 收缩 portal(将两端点向内移动 agentRadius)
* @en Shrink portal (move endpoints inward by agentRadius)
*/
shrinkPortal(left, right, radius) {
const dx = right.x - left.x;
const dy = right.y - left.y;
const len = Math.sqrt(dx * dx + dy * dy);
if (len <= radius * 2) {
const cx = (left.x + right.x) / 2;
const cy = (left.y + right.y) / 2;
return {
left: createPoint(cx, cy),
right: createPoint(cx, cy)
};
}
const nx = dx / len;
const ny = dy / len;
return {
left: createPoint(left.x + nx * radius, left.y + ny * radius),
right: createPoint(right.x - nx * radius, right.y - ny * radius)
};
}
/**
* @zh 偏移拐点以保持与角落的距离
* @en Offset turning point to maintain distance from corner
*
* @param prevApex - @zh 上一个顶点 @en Previous apex
* @param cornerOriginal - @zh 原始角落位置 @en Original corner position
* @param cornerShrunk - @zh 收缩后的角落位置 @en Shrunk corner position
* @param radius - @zh 代理半径 @en Agent radius
* @param side - @zh 转向侧 ('left' 或 'right') @en Turn side ('left' or 'right')
*/
offsetTurningPoint(prevApex, cornerOriginal, cornerShrunk, radius, side) {
const dx = cornerOriginal.x - prevApex.x;
const dy = cornerOriginal.y - prevApex.y;
const len = Math.sqrt(dx * dx + dy * dy);
if (len < 1e-4) {
return cornerShrunk;
}
let perpX, perpY;
if (side === "left") {
perpX = dy / len;
perpY = -dx / len;
} else {
perpX = -dy / len;
perpY = dx / len;
}
return createPoint(cornerShrunk.x + perpX * radius, cornerShrunk.y + perpY * radius);
}
/**
* @zh 检查两点是否相等
* @en Check if two points are equal
*/
pointsEqual(a, b) {
return Math.abs(a.x - b.x) < 1e-4 && Math.abs(a.y - b.y) < 1e-4;
}
/**
* @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;
}
// =========================================================================
// 动态障碍物管理 | Dynamic Obstacle Management
// =========================================================================
/**
* @zh 添加圆形障碍物
* @en Add circular obstacle
*
* @param x - @zh 中心 X @en Center X
* @param y - @zh 中心 Y @en Center Y
* @param radius - @zh 半径 @en Radius
* @returns @zh 障碍物 ID @en Obstacle ID
*/
addCircleObstacle(x, y, radius) {
const id = this.nextObstacleId++;
this.obstacles.set(id, {
id,
type: "circle",
enabled: true,
position: createPoint(x, y),
radius
});
return id;
}
/**
* @zh 添加矩形障碍物
* @en Add rectangular obstacle
*
* @param x - @zh 中心 X @en Center X
* @param y - @zh 中心 Y @en Center Y
* @param halfWidth - @zh 半宽 @en Half width
* @param halfHeight - @zh 半高 @en Half height
* @returns @zh 障碍物 ID @en Obstacle ID
*/
addRectObstacle(x, y, halfWidth, halfHeight) {
const id = this.nextObstacleId++;
this.obstacles.set(id, {
id,
type: "rect",
enabled: true,
position: createPoint(x, y),
halfWidth,
halfHeight
});
return id;
}
/**
* @zh 添加多边形障碍物
* @en Add polygon obstacle
*
* @param vertices - @zh 顶点列表 @en Vertex list
* @returns @zh 障碍物 ID @en Obstacle ID
*/
addPolygonObstacle(vertices) {
const id = this.nextObstacleId++;
const center = this.calculateCenter(vertices);
this.obstacles.set(id, {
id,
type: "polygon",
enabled: true,
position: center,
vertices
});
return id;
}
/**
* @zh 移除障碍物
* @en Remove obstacle
*/
removeObstacle(obstacleId) {
return this.obstacles.delete(obstacleId);
}
/**
* @zh 启用/禁用障碍物
* @en Enable/disable obstacle
*/
setObstacleEnabled(obstacleId, enabled) {
const obstacle = this.obstacles.get(obstacleId);
if (obstacle) {
obstacle.enabled = enabled;
}
}
/**
* @zh 更新障碍物位置
* @en Update obstacle position
*/
updateObstaclePosition(obstacleId, x, y) {
const obstacle = this.obstacles.get(obstacleId);
if (obstacle) {
obstacle.position = createPoint(x, y);
}
}
/**
* @zh 获取所有障碍物
* @en Get all obstacles
*/
getObstacles() {
return Array.from(this.obstacles.values());
}
/**
* @zh 获取启用的障碍物
* @en Get enabled obstacles
*/
getEnabledObstacles() {
return Array.from(this.obstacles.values()).filter((o) => o.enabled);
}
/**
* @zh 清除所有障碍物
* @en Clear all obstacles
*/
clearObstacles() {
this.obstacles.clear();
this.nextObstacleId = 0;
}
// =========================================================================
// 多边形禁用管理 | Polygon Disable Management
// =========================================================================
/**
* @zh 禁用多边形
* @en Disable polygon
*/
disablePolygon(polygonId) {
this.disabledPolygons.add(polygonId);
}
/**
* @zh 启用多边形
* @en Enable polygon
*/
enablePolygon(polygonId) {
this.disabledPolygons.delete(polygonId);
}
/**
* @zh 检查多边形是否被禁用
* @en Check if polygon is disabled
*/
isPolygonDisable