223 lines
7.1 KiB
JavaScript
223 lines
7.1 KiB
JavaScript
import Entity from './entity.js'
|
|
import PriorityQueue from './priority-queue.js'
|
|
import SATX from './satx.js'
|
|
|
|
export default class Pathfind {
|
|
static precision = 0.01
|
|
static multiplier = 1000000 // (1 / this.precision) * 10^expected_digit_count / 10
|
|
|
|
static key2(a, b) {
|
|
return `${a},${b}`
|
|
}
|
|
|
|
// Fowler-Noll-Vo hash prime and offset basis for small keyspaces
|
|
static floatKey4(a, b, c, d) {
|
|
const prime = 16777619
|
|
let result = 2166136261
|
|
result ^= Math.floor(a * Pathfind.multiplier)
|
|
result *= prime
|
|
result ^= Math.floor(b * Pathfind.multiplier)
|
|
result *= prime
|
|
result ^= Math.floor(c * Pathfind.multiplier)
|
|
result *= prime
|
|
result ^= Math.floor(d * Pathfind.multiplier)
|
|
result *= prime
|
|
return result
|
|
}
|
|
|
|
static uniqueWaypoints(waypoints) {
|
|
const included = new Set()
|
|
const uniqueWaypoints = []
|
|
for (const waypoint of waypoints) {
|
|
const key = Pathfind.key2(waypoint[0], waypoint[1])
|
|
if (!included.has(key)) {
|
|
included.add(key)
|
|
uniqueWaypoints.push(waypoint)
|
|
}
|
|
}
|
|
|
|
return uniqueWaypoints
|
|
}
|
|
|
|
static shortestPath(graph, start, goal) {
|
|
const queue = new PriorityQueue((a, b) => a[1] < b[1])
|
|
const visited = new Map()
|
|
|
|
queue.push([[start], 0])
|
|
|
|
while (!queue.isEmpty()) {
|
|
const [path, cost] = queue.pop()
|
|
const waypoint = path.at(-1)
|
|
|
|
if (Math.abs(waypoint[0] - goal[0]) < Pathfind.precision && Math.abs(waypoint[1] - goal[1]) < Pathfind.precision) {
|
|
path.shift()
|
|
return path
|
|
}
|
|
|
|
const waypointKey = Pathfind.key2(waypoint[0], waypoint[1])
|
|
if (!visited.has(waypointKey) || visited.get(waypointKey) > cost) {
|
|
visited.set(waypointKey, cost)
|
|
|
|
for (let i = 0; i < graph.length; i += 5) {
|
|
if (Math.abs(waypoint[0] - graph[i]) > Pathfind.precision || Math.abs(waypoint[1] - graph[i + 1]) > Pathfind.precision) {
|
|
continue // waypoint and graph.from aren't the same (so graph.to isn't a neighbor)
|
|
}
|
|
|
|
const nextKey = Pathfind.key2(graph[i + 2], graph[i + 3])
|
|
if (!visited.has(nextKey) || visited.get(nextKey) > cost + graph[i + 4]) {
|
|
const next = new Float32Array(2)
|
|
next[0] = graph[i + 2]
|
|
next[1] = graph[i + 3]
|
|
queue.push([[...path, next], cost + graph[i + 4]])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return []
|
|
}
|
|
|
|
static buildGraph(waypoints, bboxes, obstacles, radius) {
|
|
const filteredWaypoints = []
|
|
const checked = new Set()
|
|
|
|
if (radius > 0) {
|
|
for (const waypoint of waypoints) {
|
|
const bbox = Entity.bbox(waypoint[0], waypoint[1], radius)
|
|
const bboxCheckedObstacles = []
|
|
for (let i = 0; i < bboxes.length; i += 5) {
|
|
if (bbox[0] <= bboxes[i + 2]) { continue }
|
|
if (bbox[1] <= bboxes[i + 3]) { continue }
|
|
if (bbox[2] >= bboxes[i]) { continue }
|
|
if (bbox[3] >= bboxes[i + 1]) { continue }
|
|
|
|
bboxCheckedObstacles.push(obstacles[bboxes[i + 4]])
|
|
}
|
|
|
|
if (bboxCheckedObstacles.length > 0) {
|
|
const collider = Entity.collider(waypoint[0], waypoint[1], radius)
|
|
const colliding = bboxCheckedObstacles.flat().some((it) => SATX.collideObject(collider, it))
|
|
if (colliding) {
|
|
continue
|
|
}
|
|
}
|
|
|
|
filteredWaypoints.push(waypoint)
|
|
}
|
|
}
|
|
|
|
const mergedWaypoints = new Float32Array(filteredWaypoints.length * 2)
|
|
let mergedWaypointsIndex = 0
|
|
for (const waypoint of filteredWaypoints) {
|
|
mergedWaypoints[mergedWaypointsIndex] = waypoint[0]
|
|
mergedWaypoints[mergedWaypointsIndex + 1] = waypoint[1]
|
|
mergedWaypointsIndex += 2
|
|
}
|
|
|
|
const nodes = []
|
|
for (let i = 0; i < mergedWaypoints.length; i += 2) {
|
|
for (let j = 0; j < mergedWaypoints.length; j += 2) {
|
|
if (i == j) {
|
|
continue
|
|
}
|
|
|
|
if (Math.abs(mergedWaypoints[i] - mergedWaypoints[j]) < Pathfind.precision && Math.abs(mergedWaypoints[i + 1] - mergedWaypoints[j + 1]) < Pathfind.precision) {
|
|
continue
|
|
}
|
|
|
|
const key = Pathfind.floatKey4(mergedWaypoints[i], mergedWaypoints[i + 1], mergedWaypoints[j], mergedWaypoints[j + 1])
|
|
if (checked.has(key)) {
|
|
continue
|
|
}
|
|
|
|
checked.add(key)
|
|
checked.add(Pathfind.floatKey4(mergedWaypoints[j], mergedWaypoints[j + 1], mergedWaypoints[i], mergedWaypoints[i + 1]))
|
|
|
|
const bbox = Entity.tunnelBbox(mergedWaypoints[i], mergedWaypoints[i + 1], mergedWaypoints[j], mergedWaypoints[j + 1], radius)
|
|
|
|
const bboxCheckedObstacles = []
|
|
for (let i = 0; i < bboxes.length; i += 5) {
|
|
if (bbox[0] <= bboxes[i + 2]) { continue }
|
|
if (bbox[1] <= bboxes[i + 3]) { continue }
|
|
if (bbox[2] >= bboxes[i]) { continue }
|
|
if (bbox[3] >= bboxes[i + 1]) { continue }
|
|
|
|
bboxCheckedObstacles.push(obstacles[bboxes[i + 4]])
|
|
}
|
|
|
|
if (bboxCheckedObstacles.length > 0) {
|
|
const tunnel = Entity.tunnelCollider(mergedWaypoints[i], mergedWaypoints[i + 1], mergedWaypoints[j], mergedWaypoints[j + 1], radius)
|
|
const colliding = bboxCheckedObstacles.some((it) => it.some((c) => SATX.collideObject(tunnel, c)))
|
|
if (colliding) {
|
|
continue
|
|
}
|
|
}
|
|
|
|
const node = new Float32Array(5)
|
|
node[0] = mergedWaypoints[i]
|
|
node[1] = mergedWaypoints[i + 1]
|
|
node[2] = mergedWaypoints[j]
|
|
node[3] = mergedWaypoints[j + 1]
|
|
node[4] = Math.hypot(mergedWaypoints[j] - mergedWaypoints[i], mergedWaypoints[j + 1] - mergedWaypoints[i + 1])
|
|
nodes.push(node)
|
|
|
|
const reverseNode = new Float32Array(5)
|
|
reverseNode[0] = mergedWaypoints[j]
|
|
reverseNode[1] = mergedWaypoints[j + 1]
|
|
reverseNode[2] = mergedWaypoints[i]
|
|
reverseNode[3] = mergedWaypoints[i + 1]
|
|
reverseNode[4] = node[4] // distance is the same, copying is less expensive
|
|
nodes.push(reverseNode)
|
|
}
|
|
}
|
|
|
|
const graph = new Float32Array(nodes.length * 5)
|
|
let graphIndex = 0
|
|
for (const node of nodes) {
|
|
graph[graphIndex] = node[0]
|
|
graph[graphIndex + 1] = node[1]
|
|
graph[graphIndex + 2] = node[2]
|
|
graph[graphIndex + 3] = node[3]
|
|
graph[graphIndex + 4] = node[4]
|
|
graphIndex += 5
|
|
}
|
|
|
|
// const niceGraph = []
|
|
// for (let i = 0; i < graph.length / 5; i += 5) {
|
|
// niceGraph.push({
|
|
// from: [graph[i], graph[i + 1]],
|
|
// to: [graph[i + 2], graph[i + 3]],
|
|
// distance: graph[i + 4],
|
|
// })
|
|
// }
|
|
// console.log(niceGraph)
|
|
return graph
|
|
}
|
|
|
|
static formatFloat32Array(array, columns = 2, text = false) {
|
|
const formatted = []
|
|
let columnWidth = 0
|
|
for (let i = 0; i < array.length; i += columns) {
|
|
const row = []
|
|
for (let j = i; j < i + columns; j++) {
|
|
if (text) {
|
|
row.push(`${array[j]}`)
|
|
if (`${array[j]}`.length > columnWidth) {
|
|
columnWidth = `${array[j]}`.length
|
|
}
|
|
}
|
|
else {
|
|
row.push(array[j])
|
|
}
|
|
}
|
|
formatted.push(row)
|
|
}
|
|
|
|
if (text) {
|
|
return formatted.map((row) => row.map((v) => v.padEnd(columnWidth, ' ')).join(' | ')).join('\n')
|
|
}
|
|
|
|
return formatted
|
|
}
|
|
}
|