UNPKG

@esengine/pathfinding

Version:

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

1,311 lines (1,304 loc) 40.8 kB
import { __name, __publicField } from "./chunk-T626JPC7.js"; // src/avoidance/ILocalAvoidance.ts var DEFAULT_ORCA_CONFIG = { defaultTimeHorizon: 2, defaultTimeHorizonObst: 1, timeStep: 1 / 60, epsilon: 1e-5, yAxisDown: false }; var DEFAULT_AGENT_PARAMS = { radius: 0.5, maxSpeed: 5, neighborDist: 15, maxNeighbors: 10, timeHorizon: 2, timeHorizonObst: 1 }; // src/avoidance/LinearProgram.ts import { Vector2 } from "@esengine/ecs-framework-math"; var EPSILON = 1e-5; var { dot, det, lengthSq } = Vector2; function linearProgram1(lines, lineNo, radius, optVelocity, directionOpt, result) { const line = lines[lineNo]; const dotProduct = dot(line.point, line.direction); const discriminant = dotProduct * dotProduct + radius * radius - lengthSq(line.point); if (discriminant < 0) { return false; } const sqrtDiscriminant = Math.sqrt(discriminant); let tLeft = -dotProduct - sqrtDiscriminant; let tRight = -dotProduct + sqrtDiscriminant; for (let i = 0; i < lineNo; i++) { const constraint = lines[i]; const denominator = det(line.direction, constraint.direction); const numerator = det(constraint.direction, { x: line.point.x - constraint.point.x, y: line.point.y - constraint.point.y }); if (Math.abs(denominator) <= EPSILON) { if (numerator < 0) { return false; } continue; } const t2 = numerator / denominator; if (denominator >= 0) { tRight = Math.min(tRight, t2); } else { tLeft = Math.max(tLeft, t2); } if (tLeft > tRight) { return false; } } let t; if (directionOpt) { if (dot(optVelocity, line.direction) > 0) { t = tRight; } else { t = tLeft; } } else { t = dot(line.direction, { x: optVelocity.x - line.point.x, y: optVelocity.y - line.point.y }); if (t < tLeft) { t = tLeft; } else if (t > tRight) { t = tRight; } } result.x = line.point.x + t * line.direction.x; result.y = line.point.y + t * line.direction.y; return true; } __name(linearProgram1, "linearProgram1"); function linearProgram2(lines, radius, optVelocity, directionOpt, result) { if (directionOpt) { result.x = optVelocity.x * radius; result.y = optVelocity.y * radius; } else if (lengthSq(optVelocity) > radius * radius) { const len2 = Math.sqrt(lengthSq(optVelocity)); result.x = optVelocity.x / len2 * radius; result.y = optVelocity.y / len2 * radius; } else { result.x = optVelocity.x; result.y = optVelocity.y; } for (let i = 0; i < lines.length; i++) { const line = lines[i]; const detVal = det(line.direction, { x: line.point.x - result.x, y: line.point.y - result.y }); if (detVal > 0) { const tempResult = result.clone(); if (!linearProgram1(lines, i, radius, optVelocity, directionOpt, result)) { result.copy(tempResult); return i; } } } return lines.length; } __name(linearProgram2, "linearProgram2"); function linearProgram3(lines, numObstLines, beginLine, radius, result) { let distance = 0; for (let i = beginLine; i < lines.length; i++) { const line = lines[i]; if (det(line.direction, { x: line.point.x - result.x, y: line.point.y - result.y }) > distance) { const projLines = []; for (let j = 0; j < numObstLines; j++) { projLines.push(lines[j]); } for (let j = numObstLines; j < i; j++) { const line1 = lines[j]; const line2 = lines[i]; let newLine; const determinant = det(line1.direction, line2.direction); if (Math.abs(determinant) <= EPSILON) { if (dot(line1.direction, line2.direction) > 0) { continue; } newLine = { point: { x: 0.5 * (line1.point.x + line2.point.x), y: 0.5 * (line1.point.y + line2.point.y) }, direction: { x: 0, y: 0 } }; } else { const diff = { x: line1.point.x - line2.point.x, y: line1.point.y - line2.point.y }; const t = det(line2.direction, diff) / determinant; newLine = { point: { x: line1.point.x + t * line1.direction.x, y: line1.point.y + t * line1.direction.y }, direction: { x: 0, y: 0 } }; } const dirDiff = { x: line1.direction.x - line2.direction.x, y: line1.direction.y - line2.direction.y }; const dirLen = Math.sqrt(lengthSq(dirDiff)); if (dirLen > EPSILON) { newLine.direction.x = dirDiff.x / dirLen; newLine.direction.y = dirDiff.y / dirLen; } projLines.push(newLine); } const tempResult = result.clone(); const optVelocity = { x: -lines[i].direction.y, y: lines[i].direction.x }; if (linearProgram2(projLines, radius, optVelocity, true, result) < projLines.length) { result.copy(tempResult); } if (!verifyObstacleConstraints(lines, numObstLines, result)) { result.copy(tempResult); } distance = det(lines[i].direction, { x: lines[i].point.x - result.x, y: lines[i].point.y - result.y }); } } } __name(linearProgram3, "linearProgram3"); function verifyObstacleConstraints(lines, numObstLines, velocity) { for (let i = 0; i < numObstLines; i++) { const line = lines[i]; const detVal = det(line.direction, { x: line.point.x - velocity.x, y: line.point.y - velocity.y }); if (detVal > EPSILON) { return false; } } return true; } __name(verifyObstacleConstraints, "verifyObstacleConstraints"); function solveORCALinearProgram(lines, numObstLines, maxSpeed, preferredVelocity) { const result = new Vector2(); const lineFail = linearProgram2(lines, maxSpeed, preferredVelocity, false, result); let feasible = lineFail >= lines.length; let violatedConstraints = 0; if (!feasible) { linearProgram3(lines, numObstLines, lineFail, maxSpeed, result); violatedConstraints = lines.length - lineFail; } if (numObstLines > 0 && !verifyObstacleConstraints(lines, numObstLines, result)) { feasible = false; violatedConstraints = Math.max(violatedConstraints, 1); } return { velocity: result, feasible, violatedConstraints }; } __name(solveORCALinearProgram, "solveORCALinearProgram"); // src/avoidance/ObstacleBuilder.ts var EPSILON2 = 1e-5; function leftOf(p1, p2, p3) { return (p1.x - p3.x) * (p2.y - p1.y) - (p1.y - p3.y) * (p2.x - p1.x); } __name(leftOf, "leftOf"); function createObstacleVertices(vertices, startId = 0) { const n = vertices.length; if (n < 2) { return []; } const obstacleVertices = []; for (let i = 0; i < n; i++) { obstacleVertices.push({ point: { x: vertices[i].x, y: vertices[i].y }, direction: { x: 0, y: 0 }, next: null, previous: null, isConvex: false, id: startId + i }); } for (let i = 0; i < n; i++) { const curr = obstacleVertices[i]; const next = obstacleVertices[(i + 1) % n]; const prev = obstacleVertices[(i + n - 1) % n]; curr.next = next; curr.previous = prev; const dx = next.point.x - curr.point.x; const dy = next.point.y - curr.point.y; const edgeLen = Math.sqrt(dx * dx + dy * dy); if (edgeLen > EPSILON2) { curr.direction = { x: dx / edgeLen, y: dy / edgeLen }; } else { curr.direction = { x: 1, y: 0 }; } } for (let i = 0; i < n; i++) { const curr = obstacleVertices[i]; const prev = curr.previous; const next = curr.next; curr.isConvex = leftOf(prev.point, curr.point, next.point) >= 0; } return obstacleVertices; } __name(createObstacleVertices, "createObstacleVertices"); function buildObstacleVertices(obstacles, options = {}) { const { yAxisDown = false } = options; const allVertices = []; let nextId = 0; for (const obstacle of obstacles) { const ccwVertices = ensureCCW([ ...obstacle.vertices ], yAxisDown); const vertices = createObstacleVertices(ccwVertices, nextId); allVertices.push(...vertices); nextId += vertices.length; } return allVertices; } __name(buildObstacleVertices, "buildObstacleVertices"); function ensureCCW(vertices, yAxisDown = false) { if (vertices.length < 3) { return vertices; } let signedArea = 0; for (let i = 0; i < vertices.length; i++) { const curr = vertices[i]; const next = vertices[(i + 1) % vertices.length]; signedArea += curr.x * next.y - next.x * curr.y; } signedArea *= 0.5; const isCCW = yAxisDown ? signedArea < 0 : signedArea > 0; if (isCCW) { return vertices; } return [ ...vertices ].reverse(); } __name(ensureCCW, "ensureCCW"); // src/avoidance/ORCASolver.ts import { Vector2 as Vector22 } from "@esengine/ecs-framework-math"; var EPSILON3 = 1e-5; var { det: det2, dot: dot2, lengthSq: lengthSq2, len } = Vector22; function normalize(v) { const length = len(v); if (length < EPSILON3) { return { x: 0, y: 0 }; } return { x: v.x / length, y: v.y / length }; } __name(normalize, "normalize"); var _ORCASolver = class _ORCASolver { constructor(config = {}) { __publicField(this, "config"); this.config = { ...DEFAULT_ORCA_CONFIG, ...config }; } /** * @zh 计算代理的新速度 * @en Compute new velocity for agent * * @param agent - @zh 当前代理 @en Current agent * @param neighbors - @zh 邻近代理列表 @en List of neighboring agents * @param obstacles - @zh 障碍物列表 @en List of obstacles * @param deltaTime - @zh 时间步长 @en Time step * @returns @zh 计算得到的新速度 @en Computed new velocity */ computeNewVelocity(agent, neighbors, obstacles, deltaTime) { const result = this.computeNewVelocityWithResult(agent, neighbors, obstacles, deltaTime); return result.velocity; } /** * @zh 计算代理的新速度(带完整结果) * @en Compute new velocity for agent (with full result) * * @param agent - @zh 当前代理 @en Current agent * @param neighbors - @zh 邻近代理列表 @en List of neighboring agents * @param obstacles - @zh 障碍物列表 @en List of obstacles * @param deltaTime - @zh 时间步长 @en Time step * @returns @zh 完整求解结果 @en Full solve result */ computeNewVelocityWithResult(agent, neighbors, obstacles, deltaTime) { const orcaLines = []; const obstacleVertices = buildObstacleVertices(obstacles, { yAxisDown: this.config.yAxisDown }); const numObstLines = this.createObstacleORCALines(agent, obstacleVertices, orcaLines); this.createAgentORCALines(agent, neighbors, deltaTime, orcaLines); const result = solveORCALinearProgram(orcaLines, numObstLines, agent.maxSpeed, agent.preferredVelocity); return { ...result, numLines: orcaLines.length }; } /** * @zh 创建代理间的 ORCA 约束线 * @en Create ORCA constraint lines for agent-agent avoidance */ createAgentORCALines(agent, neighbors, deltaTime, orcaLines) { const invTimeHorizon = 1 / agent.timeHorizon; for (const other of neighbors) { if (other.id === agent.id) continue; const relativePosition = { x: other.position.x - agent.position.x, y: other.position.y - agent.position.y }; const relativeVelocity = { x: agent.velocity.x - other.velocity.x, y: agent.velocity.y - other.velocity.y }; const distSq = lengthSq2(relativePosition); const combinedRadius = agent.radius + other.radius; const combinedRadiusSq = combinedRadius * combinedRadius; const line = { point: { x: 0, y: 0 }, direction: { x: 0, y: 0 } }; let u; if (distSq > combinedRadiusSq) { const w = { x: relativeVelocity.x - invTimeHorizon * relativePosition.x, y: relativeVelocity.y - invTimeHorizon * relativePosition.y }; const wLengthSq = lengthSq2(w); const dotProduct1 = dot2(w, relativePosition); if (dotProduct1 < 0 && dotProduct1 * dotProduct1 > combinedRadiusSq * wLengthSq) { const wLength = Math.sqrt(wLengthSq); const unitW = normalize(w); line.direction = { x: unitW.y, y: -unitW.x }; u = { x: (combinedRadius * invTimeHorizon - wLength) * unitW.x, y: (combinedRadius * invTimeHorizon - wLength) * unitW.y }; } else { const leg = Math.sqrt(distSq - combinedRadiusSq); if (det2(relativePosition, w) > 0) { line.direction = { x: (relativePosition.x * leg - relativePosition.y * combinedRadius) / distSq, y: (relativePosition.x * combinedRadius + relativePosition.y * leg) / distSq }; } else { line.direction = { x: -(relativePosition.x * leg + relativePosition.y * combinedRadius) / distSq, y: -(-relativePosition.x * combinedRadius + relativePosition.y * leg) / distSq }; } const dotProduct2 = dot2(relativeVelocity, line.direction); u = { x: dotProduct2 * line.direction.x - relativeVelocity.x, y: dotProduct2 * line.direction.y - relativeVelocity.y }; } } else { const invTimeStep = 1 / deltaTime; const w = { x: relativeVelocity.x - invTimeStep * relativePosition.x, y: relativeVelocity.y - invTimeStep * relativePosition.y }; const wLength = len(w); const unitW = wLength > EPSILON3 ? { x: w.x / wLength, y: w.y / wLength } : { x: 1, y: 0 }; line.direction = { x: unitW.y, y: -unitW.x }; u = { x: (combinedRadius * invTimeStep - wLength) * unitW.x, y: (combinedRadius * invTimeStep - wLength) * unitW.y }; } line.point = { x: agent.velocity.x + 0.5 * u.x, y: agent.velocity.y + 0.5 * u.y }; orcaLines.push(line); } } /** * @zh 创建障碍物的 ORCA 约束线 * @en Create ORCA constraint lines for obstacle avoidance */ createObstacleORCALines(agent, obstacleVertices, orcaLines) { const invTimeHorizonObst = 1 / agent.timeHorizonObst; const radiusSq = agent.radius * agent.radius; let numObstLines = 0; for (const obstacle1 of obstacleVertices) { const obstacle2 = obstacle1.next; const relativePosition1 = { x: obstacle1.point.x - agent.position.x, y: obstacle1.point.y - agent.position.y }; const relativePosition2 = { x: obstacle2.point.x - agent.position.x, y: obstacle2.point.y - agent.position.y }; const obstacleVector = { x: obstacle2.point.x - obstacle1.point.x, y: obstacle2.point.y - obstacle1.point.y }; const signedDistToEdge = det2(obstacleVector, relativePosition1); if (signedDistToEdge < -EPSILON3) { continue; } let alreadyCovered = false; for (const existingLine of orcaLines) { const scaledRelPos1 = { x: invTimeHorizonObst * relativePosition1.x - existingLine.point.x, y: invTimeHorizonObst * relativePosition1.y - existingLine.point.y }; const scaledRelPos2 = { x: invTimeHorizonObst * relativePosition2.x - existingLine.point.x, y: invTimeHorizonObst * relativePosition2.y - existingLine.point.y }; if (det2(scaledRelPos1, existingLine.direction) - invTimeHorizonObst * agent.radius >= -EPSILON3 && det2(scaledRelPos2, existingLine.direction) - invTimeHorizonObst * agent.radius >= -EPSILON3) { alreadyCovered = true; break; } } if (alreadyCovered) { continue; } const distSq1 = lengthSq2(relativePosition1); const distSq2 = lengthSq2(relativePosition2); const obstacleVectorSq = lengthSq2(obstacleVector); const s = obstacleVectorSq > EPSILON3 ? -dot2(relativePosition1, obstacleVector) / obstacleVectorSq : 0; const distSqLineToEdge = lengthSq2({ x: -relativePosition1.x - s * obstacleVector.x, y: -relativePosition1.y - s * obstacleVector.y }); const line = { point: { x: 0, y: 0 }, direction: { x: 0, y: 0 } }; if (s < 0 && distSq1 <= radiusSq) { if (obstacle1.isConvex) { line.point = { x: 0, y: 0 }; line.direction = normalize({ x: -relativePosition1.y, y: relativePosition1.x }); orcaLines.push(line); numObstLines++; } continue; } if (s > 1 && distSq2 <= radiusSq) { if (obstacle2.isConvex && det2(relativePosition2, obstacle2.direction) >= 0) { line.point = { x: 0, y: 0 }; line.direction = normalize({ x: -relativePosition2.y, y: relativePosition2.x }); orcaLines.push(line); numObstLines++; } continue; } if (s >= 0 && s <= 1 && distSqLineToEdge <= radiusSq) { line.point = { x: 0, y: 0 }; line.direction = { x: -obstacle1.direction.x, y: -obstacle1.direction.y }; orcaLines.push(line); numObstLines++; continue; } let obs1 = obstacle1; let obs2 = obstacle2; let leftLegDirection; let rightLegDirection; if (s < 0 && distSqLineToEdge <= radiusSq) { if (!obstacle1.isConvex) continue; obs2 = obstacle1; const leg1 = Math.sqrt(Math.max(0, distSq1 - radiusSq)); leftLegDirection = { x: (relativePosition1.x * leg1 - relativePosition1.y * agent.radius) / distSq1, y: (relativePosition1.x * agent.radius + relativePosition1.y * leg1) / distSq1 }; rightLegDirection = { x: (relativePosition1.x * leg1 + relativePosition1.y * agent.radius) / distSq1, y: (-relativePosition1.x * agent.radius + relativePosition1.y * leg1) / distSq1 }; } else if (s > 1 && distSqLineToEdge <= radiusSq) { if (!obstacle2.isConvex) continue; obs1 = obstacle2; const leg2 = Math.sqrt(Math.max(0, distSq2 - radiusSq)); leftLegDirection = { x: (relativePosition2.x * leg2 - relativePosition2.y * agent.radius) / distSq2, y: (relativePosition2.x * agent.radius + relativePosition2.y * leg2) / distSq2 }; rightLegDirection = { x: (relativePosition2.x * leg2 + relativePosition2.y * agent.radius) / distSq2, y: (-relativePosition2.x * agent.radius + relativePosition2.y * leg2) / distSq2 }; } else { if (obstacle1.isConvex) { const leg1 = Math.sqrt(Math.max(0, distSq1 - radiusSq)); leftLegDirection = { x: (relativePosition1.x * leg1 - relativePosition1.y * agent.radius) / distSq1, y: (relativePosition1.x * agent.radius + relativePosition1.y * leg1) / distSq1 }; } else { leftLegDirection = { x: -obstacle1.direction.x, y: -obstacle1.direction.y }; } if (obstacle2.isConvex) { const leg2 = Math.sqrt(Math.max(0, distSq2 - radiusSq)); rightLegDirection = { x: (relativePosition2.x * leg2 + relativePosition2.y * agent.radius) / distSq2, y: (-relativePosition2.x * agent.radius + relativePosition2.y * leg2) / distSq2 }; } else { rightLegDirection = { x: obstacle1.direction.x, y: obstacle1.direction.y }; } } const leftNeighbor = obs1.previous; let isLeftLegForeign = false; let isRightLegForeign = false; if (obs1.isConvex) { const negLeftNeighborDir = { x: -leftNeighbor.direction.x, y: -leftNeighbor.direction.y }; if (det2(leftLegDirection, negLeftNeighborDir) >= 0) { leftLegDirection = negLeftNeighborDir; isLeftLegForeign = true; } } if (obs2.isConvex) { if (det2(rightLegDirection, obs2.direction) <= 0) { rightLegDirection = { x: obs2.direction.x, y: obs2.direction.y }; isRightLegForeign = true; } } const leftCutoff = { x: invTimeHorizonObst * (obs1.point.x - agent.position.x), y: invTimeHorizonObst * (obs1.point.y - agent.position.y) }; const rightCutoff = { x: invTimeHorizonObst * (obs2.point.x - agent.position.x), y: invTimeHorizonObst * (obs2.point.y - agent.position.y) }; const cutoffVector = { x: rightCutoff.x - leftCutoff.x, y: rightCutoff.y - leftCutoff.y }; const sameVertex = obs1 === obs2; const cutoffVectorSq = lengthSq2(cutoffVector); const t = sameVertex ? 0.5 : cutoffVectorSq > EPSILON3 ? dot2({ x: agent.velocity.x - leftCutoff.x, y: agent.velocity.y - leftCutoff.y }, cutoffVector) / cutoffVectorSq : 0.5; const tLeft = dot2({ x: agent.velocity.x - leftCutoff.x, y: agent.velocity.y - leftCutoff.y }, leftLegDirection); const tRight = dot2({ x: agent.velocity.x - rightCutoff.x, y: agent.velocity.y - rightCutoff.y }, rightLegDirection); if (t < 0 && tLeft < 0 || sameVertex && tLeft < 0 && tRight < 0) { const unitW = normalize({ x: agent.velocity.x - leftCutoff.x, y: agent.velocity.y - leftCutoff.y }); line.direction = { x: unitW.y, y: -unitW.x }; line.point = { x: leftCutoff.x + agent.radius * invTimeHorizonObst * unitW.x, y: leftCutoff.y + agent.radius * invTimeHorizonObst * unitW.y }; orcaLines.push(line); numObstLines++; continue; } if (t > 1 && tRight < 0) { const unitW = normalize({ x: agent.velocity.x - rightCutoff.x, y: agent.velocity.y - rightCutoff.y }); line.direction = { x: unitW.y, y: -unitW.x }; line.point = { x: rightCutoff.x + agent.radius * invTimeHorizonObst * unitW.x, y: rightCutoff.y + agent.radius * invTimeHorizonObst * unitW.y }; orcaLines.push(line); numObstLines++; continue; } const distSqCutoff = t < 0 || t > 1 || sameVertex ? Infinity : lengthSq2({ x: agent.velocity.x - (leftCutoff.x + t * cutoffVector.x), y: agent.velocity.y - (leftCutoff.y + t * cutoffVector.y) }); const distSqLeft = tLeft < 0 ? Infinity : lengthSq2({ x: agent.velocity.x - (leftCutoff.x + tLeft * leftLegDirection.x), y: agent.velocity.y - (leftCutoff.y + tLeft * leftLegDirection.y) }); const distSqRight = tRight < 0 ? Infinity : lengthSq2({ x: agent.velocity.x - (rightCutoff.x + tRight * rightLegDirection.x), y: agent.velocity.y - (rightCutoff.y + tRight * rightLegDirection.y) }); if (distSqCutoff <= distSqLeft && distSqCutoff <= distSqRight) { line.direction = { x: -obs1.direction.x, y: -obs1.direction.y }; line.point = { x: leftCutoff.x + agent.radius * invTimeHorizonObst * -line.direction.y, y: leftCutoff.y + agent.radius * invTimeHorizonObst * line.direction.x }; orcaLines.push(line); numObstLines++; continue; } if (distSqLeft <= distSqRight) { if (isLeftLegForeign) { continue; } line.direction = { x: leftLegDirection.x, y: leftLegDirection.y }; line.point = { x: leftCutoff.x + agent.radius * invTimeHorizonObst * -line.direction.y, y: leftCutoff.y + agent.radius * invTimeHorizonObst * line.direction.x }; orcaLines.push(line); numObstLines++; continue; } if (isRightLegForeign) { continue; } line.direction = { x: -rightLegDirection.x, y: -rightLegDirection.y }; line.point = { x: rightCutoff.x + agent.radius * invTimeHorizonObst * -line.direction.y, y: rightCutoff.y + agent.radius * invTimeHorizonObst * line.direction.x }; orcaLines.push(line); numObstLines++; } return numObstLines; } }; __name(_ORCASolver, "ORCASolver"); var ORCASolver = _ORCASolver; function createORCASolver(config) { return new ORCASolver(config); } __name(createORCASolver, "createORCASolver"); // src/avoidance/KDTree.ts var _KDTree = class _KDTree { constructor() { __publicField(this, "agents", []); __publicField(this, "agentIndices", []); __publicField(this, "nodes", []); /** * @zh 最大叶节点大小 * @en Maximum leaf size */ __publicField(this, "maxLeafSize", 10); } /** * @zh 构建 KD-Tree * @en Build KD-Tree */ build(agents) { this.agents = agents; this.agentIndices = []; this.nodes = []; if (agents.length === 0) { return; } for (let i = 0; i < agents.length; i++) { this.agentIndices.push(i); } this.buildRecursive(0, agents.length, 0); } /** * @zh 递归构建 KD-Tree * @en Recursively build KD-Tree */ buildRecursive(begin, end, depth) { const nodeIndex = this.nodes.length; const node = { agentIndex: -1, splitValue: 0, left: -1, right: -1, begin, end, minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }; this.nodes.push(node); for (let i = begin; i < end; i++) { const agent = this.agents[this.agentIndices[i]]; node.minX = Math.min(node.minX, agent.position.x); node.minY = Math.min(node.minY, agent.position.y); node.maxX = Math.max(node.maxX, agent.position.x); node.maxY = Math.max(node.maxY, agent.position.y); } const count = end - begin; if (count <= this.maxLeafSize) { return nodeIndex; } const splitDim = depth % 2; if (splitDim === 0) { this.sortByX(begin, end); } else { this.sortByY(begin, end); } const mid = Math.floor((begin + end) / 2); const midAgent = this.agents[this.agentIndices[mid]]; node.splitValue = splitDim === 0 ? midAgent.position.x : midAgent.position.y; node.left = this.buildRecursive(begin, mid, depth + 1); node.right = this.buildRecursive(mid, end, depth + 1); return nodeIndex; } /** * @zh 按 X 坐标排序 * @en Sort by X coordinate */ sortByX(begin, end) { const indices = this.agentIndices; const agents = this.agents; for (let i = begin + 1; i < end; i++) { const key = indices[i]; const keyX = agents[key].position.x; let j = i - 1; while (j >= begin && agents[indices[j]].position.x > keyX) { indices[j + 1] = indices[j]; j--; } indices[j + 1] = key; } } /** * @zh 按 Y 坐标排序 * @en Sort by Y coordinate */ sortByY(begin, end) { const indices = this.agentIndices; const agents = this.agents; for (let i = begin + 1; i < end; i++) { const key = indices[i]; const keyY = agents[key].position.y; let j = i - 1; while (j >= begin && agents[indices[j]].position.y > keyY) { indices[j + 1] = indices[j]; j--; } indices[j + 1] = key; } } /** * @zh 查询邻居 * @en Query neighbors */ queryNeighbors(position, radius, maxResults, excludeId) { const results = []; const radiusSq = radius * radius; if (this.nodes.length === 0) { return results; } this.queryRecursive(0, position, radiusSq, maxResults, excludeId, results); results.sort((a, b) => a.distanceSq - b.distanceSq); if (results.length > maxResults) { results.length = maxResults; } return results; } /** * @zh 递归查询 * @en Recursive query */ queryRecursive(nodeIndex, position, radiusSq, maxResults, excludeId, results) { const node = this.nodes[nodeIndex]; if (!node) return; const closestX = Math.max(node.minX, Math.min(position.x, node.maxX)); const closestY = Math.max(node.minY, Math.min(position.y, node.maxY)); const dx = position.x - closestX; const dy = position.y - closestY; const distSqToBBox = dx * dx + dy * dy; if (distSqToBBox > radiusSq) { return; } if (node.left === -1 && node.right === -1) { for (let i = node.begin; i < node.end; i++) { const agentIndex = this.agentIndices[i]; const agent = this.agents[agentIndex]; if (excludeId !== void 0 && agent.id === excludeId) { continue; } const adx = position.x - agent.position.x; const ady = position.y - agent.position.y; const distSq = adx * adx + ady * ady; if (distSq < radiusSq) { results.push({ agent, distanceSq: distSq }); } } return; } if (node.left !== -1) { this.queryRecursive(node.left, position, radiusSq, maxResults, excludeId, results); } if (node.right !== -1) { this.queryRecursive(node.right, position, radiusSq, maxResults, excludeId, results); } } /** * @zh 清空索引 * @en Clear the index */ clear() { this.agents = []; this.agentIndices = []; this.nodes = []; } /** * @zh 获取代理数量 * @en Get agent count */ get agentCount() { return this.agents.length; } }; __name(_KDTree, "KDTree"); var KDTree = _KDTree; function createKDTree() { return new KDTree(); } __name(createKDTree, "createKDTree"); // src/avoidance/CollisionResolver.ts var EPSILON4 = 1e-5; var EMPTY_COLLISION = { collided: false, penetration: 0, normal: { x: 0, y: 0 }, closestPoint: { x: 0, y: 0 } }; function closestPointOnSegment(point, segStart, segEnd) { const dx = segEnd.x - segStart.x; const dy = segEnd.y - segStart.y; const lengthSq3 = dx * dx + dy * dy; if (lengthSq3 < EPSILON4) { return { x: segStart.x, y: segStart.y }; } const t = Math.max(0, Math.min(1, ((point.x - segStart.x) * dx + (point.y - segStart.y) * dy) / lengthSq3)); return { x: segStart.x + t * dx, y: segStart.y + t * dy }; } __name(closestPointOnSegment, "closestPointOnSegment"); function isPointInPolygon(point, 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 > point.y !== yj > point.y && point.x < (xj - xi) * (point.y - yi) / (yj - yi) + xi) { inside = !inside; } } return inside; } __name(isPointInPolygon, "isPointInPolygon"); function closestPointOnPolygon(point, vertices) { let minDistSq = Infinity; let closestPt = { x: 0, y: 0 }; let closestEdge = 0; for (let i = 0; i < vertices.length; i++) { const j = (i + 1) % vertices.length; const closest = closestPointOnSegment(point, vertices[i], vertices[j]); const dx = point.x - closest.x; const dy = point.y - closest.y; const distSq = dx * dx + dy * dy; if (distSq < minDistSq) { minDistSq = distSq; closestPt = closest; closestEdge = i; } } return { point: closestPt, distanceSq: minDistSq, edgeIndex: closestEdge }; } __name(closestPointOnPolygon, "closestPointOnPolygon"); var DEFAULT_COLLISION_CONFIG = { responseFactor: 1, safetyMargin: 0.01 }; var _CollisionResolver = class _CollisionResolver { constructor(config = {}) { __publicField(this, "config"); this.config = { ...DEFAULT_COLLISION_CONFIG, ...config }; } /** * @zh 检测圆与单个障碍物的碰撞 * @en Detect collision between circle and single obstacle * * @param position - @zh 圆心位置 @en Circle center position * @param radius - @zh 圆半径 @en Circle radius * @param obstacle - @zh 障碍物 @en Obstacle * @returns @zh 碰撞结果 @en Collision result */ detectCollision(position, radius, obstacle) { const vertices = obstacle.vertices; if (vertices.length < 3) { return EMPTY_COLLISION; } const isInside = isPointInPolygon(position, vertices); const closest = closestPointOnPolygon(position, vertices); const distance = Math.sqrt(closest.distanceSq); let penetration; let normalX; let normalY; if (isInside) { penetration = radius + distance; const dx = closest.point.x - position.x; const dy = closest.point.y - position.y; const len2 = Math.sqrt(dx * dx + dy * dy); if (len2 > EPSILON4) { normalX = dx / len2; normalY = dy / len2; } else { normalX = 1; normalY = 0; } } else if (distance < radius) { penetration = radius - distance; const dx = position.x - closest.point.x; const dy = position.y - closest.point.y; const len2 = Math.sqrt(dx * dx + dy * dy); if (len2 > EPSILON4) { normalX = dx / len2; normalY = dy / len2; } else { normalX = 1; normalY = 0; } } else { return EMPTY_COLLISION; } return { collided: true, penetration, normal: { x: normalX, y: normalY }, closestPoint: closest.point }; } /** * @zh 检测圆与所有障碍物的碰撞 * @en Detect collision between circle and all obstacles * * @param position - @zh 圆心位置 @en Circle center position * @param radius - @zh 圆半径 @en Circle radius * @param obstacles - @zh 障碍物列表 @en List of obstacles * @returns @zh 最严重的碰撞结果 @en Most severe collision result */ detectCollisions(position, radius, obstacles) { let worstCollision = EMPTY_COLLISION; let maxPenetration = 0; for (const obstacle of obstacles) { const collision = this.detectCollision(position, radius, obstacle); if (collision.collided && collision.penetration > maxPenetration) { maxPenetration = collision.penetration; worstCollision = collision; } } return worstCollision; } /** * @zh 解决碰撞,返回修正后的位置 * @en Resolve collision, return corrected position * * @param position - @zh 当前位置 @en Current position * @param radius - @zh 半径 @en Radius * @param obstacles - @zh 障碍物列表 @en List of obstacles * @returns @zh 修正后的位置 @en Corrected position */ resolveCollision(position, radius, obstacles) { const result = { x: position.x, y: position.y }; const maxIterations = 4; for (let iter = 0; iter < maxIterations; iter++) { const collision = this.detectCollisions(result, radius, obstacles); if (!collision.collided) { break; } const pushDistance = (collision.penetration + this.config.safetyMargin) * this.config.responseFactor; result.x += collision.normal.x * pushDistance; result.y += collision.normal.y * pushDistance; } return result; } /** * @zh 验证速度是否会导致碰撞,返回安全速度 * @en Validate velocity won't cause collision, return safe velocity * * @param position - @zh 当前位置 @en Current position * @param velocity - @zh 目标速度 @en Target velocity * @param radius - @zh 半径 @en Radius * @param obstacles - @zh 障碍物列表 @en List of obstacles * @param deltaTime - @zh 时间步长 @en Time step * @returns @zh 安全速度 @en Safe velocity */ validateVelocity(position, velocity, radius, obstacles, deltaTime) { const speed = Math.sqrt(velocity.x * velocity.x + velocity.y * velocity.y); if (speed < EPSILON4) { return velocity; } const newPos = { x: position.x + velocity.x * deltaTime, y: position.y + velocity.y * deltaTime }; const collision = this.detectCollisions(newPos, radius, obstacles); if (!collision.collided) { return velocity; } const dotProduct = velocity.x * collision.normal.x + velocity.y * collision.normal.y; if (dotProduct >= 0) { return velocity; } const slideVelocity = { x: velocity.x - dotProduct * collision.normal.x, y: velocity.y - dotProduct * collision.normal.y }; const slideSpeed = Math.sqrt(slideVelocity.x * slideVelocity.x + slideVelocity.y * slideVelocity.y); if (slideSpeed < speed * 0.1) { const perpDir1 = { x: -collision.normal.y, y: collision.normal.x }; const perpDir2 = { x: collision.normal.y, y: -collision.normal.x }; const dot1 = velocity.x * perpDir1.x + velocity.y * perpDir1.y; const dot22 = velocity.x * perpDir2.x + velocity.y * perpDir2.y; const chosenDir = dot1 >= dot22 ? perpDir1 : perpDir2; return { x: chosenDir.x * speed, y: chosenDir.y * speed }; } return slideVelocity; } /** * @zh 检测两个代理之间的碰撞 * @en Detect collision between two agents * * @param posA - @zh 代理 A 位置 @en Agent A position * @param radiusA - @zh 代理 A 半径 @en Agent A radius * @param posB - @zh 代理 B 位置 @en Agent B position * @param radiusB - @zh 代理 B 半径 @en Agent B radius * @returns @zh 碰撞结果 @en Collision result */ detectAgentCollision(posA, radiusA, posB, radiusB) { const dx = posB.x - posA.x; const dy = posB.y - posA.y; const distSq = dx * dx + dy * dy; const combinedRadius = radiusA + radiusB; if (distSq >= combinedRadius * combinedRadius) { return EMPTY_COLLISION; } const distance = Math.sqrt(distSq); const penetration = combinedRadius - distance; let normalX, normalY; if (distance > EPSILON4) { normalX = -dx / distance; normalY = -dy / distance; } else { normalX = 1; normalY = 0; } return { collided: true, penetration, normal: { x: normalX, y: normalY }, closestPoint: { x: posA.x + normalX * radiusA, y: posA.y + normalY * radiusA } }; } /** * @zh 解决代理之间的碰撞 * @en Resolve collision between agents * * @param posA - @zh 代理 A 位置 @en Agent A position * @param radiusA - @zh 代理 A 半径 @en Agent A radius * @param posB - @zh 代理 B 位置 @en Agent B position * @param radiusB - @zh 代理 B 半径 @en Agent B radius * @returns @zh 修正后的位置 [A, B] @en Corrected positions [A, B] */ resolveAgentCollision(posA, radiusA, posB, radiusB) { const collision = this.detectAgentCollision(posA, radiusA, posB, radiusB); if (!collision.collided) { return [ posA, posB ]; } const halfPush = (collision.penetration + this.config.safetyMargin) * 0.5 * this.config.responseFactor; return [ { x: posA.x + collision.normal.x * halfPush, y: posA.y + collision.normal.y * halfPush }, { x: posB.x - collision.normal.x * halfPush, y: posB.y - collision.normal.y * halfPush } ]; } }; __name(_CollisionResolver, "CollisionResolver"); var CollisionResolver = _CollisionResolver; function createCollisionResolver(config) { return new CollisionResolver(config); } __name(createCollisionResolver, "createCollisionResolver"); export { DEFAULT_ORCA_CONFIG, DEFAULT_AGENT_PARAMS, linearProgram2, linearProgram3, solveORCALinearProgram, createObstacleVertices, buildObstacleVertices, ensureCCW, ORCASolver, createORCASolver, KDTree, createKDTree, EMPTY_COLLISION, DEFAULT_COLLISION_CONFIG, CollisionResolver, createCollisionResolver }; //# sourceMappingURL=chunk-3VEX32JO.js.map