335 lines
8.2 KiB
Rust
335 lines
8.2 KiB
Rust
use std::{cell::RefCell, f64::consts::PI, rc::Rc};
|
|
|
|
use wasm_bindgen::prelude::*;
|
|
|
|
use rustc_hash::FxHashMap;
|
|
|
|
use crate::{
|
|
geometry::{LineSegment, Point},
|
|
js_api::{Wall, WallSenseType},
|
|
ptr_indexed_hash_set::PtrIndexedHashSet,
|
|
};
|
|
|
|
pub struct Edge {
|
|
target: NodePtr,
|
|
cost: f64,
|
|
}
|
|
|
|
pub struct Node {
|
|
pub point: Point,
|
|
edges: Option<Vec<Edge>>,
|
|
final_edge: Option<Option<Edge>>,
|
|
}
|
|
|
|
impl Node {
|
|
pub fn new(point: Point) -> Self {
|
|
Self {
|
|
point,
|
|
edges: None,
|
|
final_edge: None,
|
|
}
|
|
}
|
|
|
|
fn iter_edges(
|
|
&self,
|
|
) -> std::iter::Chain<std::slice::Iter<'_, Edge>, std::option::Iter<'_, Edge>> {
|
|
self.edges
|
|
.as_ref()
|
|
.unwrap()
|
|
.iter()
|
|
.chain(self.final_edge.as_ref().unwrap().iter())
|
|
}
|
|
}
|
|
|
|
type NodePtr = Rc<RefCell<Node>>;
|
|
|
|
impl From<Node> for NodePtr {
|
|
fn from(node: Node) -> Self {
|
|
Rc::new(RefCell::new(node))
|
|
}
|
|
}
|
|
|
|
pub struct DiscoveredNode {
|
|
pub node: NodePtr,
|
|
cost: f64,
|
|
estimated: f64,
|
|
pub previous: Option<DiscoveredNodePtr>,
|
|
}
|
|
|
|
pub type DiscoveredNodePtr = Rc<RefCell<DiscoveredNode>>;
|
|
|
|
impl From<DiscoveredNode> for DiscoveredNodePtr {
|
|
fn from(node: DiscoveredNode) -> Self {
|
|
Rc::new(RefCell::new(node))
|
|
}
|
|
}
|
|
|
|
#[derive(Default, Clone)]
|
|
pub struct NodeStorage {
|
|
regular_nodes: Vec<NodePtr>,
|
|
final_node: Option<NodePtr>,
|
|
}
|
|
|
|
pub type NodeStorageIterator<'a> = std::iter::Chain<
|
|
std::slice::Iter<'a, Rc<RefCell<Node>>>,
|
|
std::option::Iter<'a, Rc<RefCell<Node>>>,
|
|
>;
|
|
|
|
impl NodeStorage {
|
|
fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
fn push(&mut self, node: NodePtr) {
|
|
self.regular_nodes.push(node);
|
|
}
|
|
|
|
fn initialize_edges(&mut self, node: &NodePtr, walls: &[LineSegment]) {
|
|
if node.borrow().final_edge.is_none() {
|
|
let final_edge = self
|
|
.final_node
|
|
.as_ref()
|
|
.filter(|neighbor| {
|
|
!self.collides_with_wall(
|
|
&LineSegment::new(node.borrow().point, neighbor.borrow().point),
|
|
walls,
|
|
)
|
|
})
|
|
.map(|neighbor| Edge {
|
|
target: neighbor.clone(),
|
|
cost: node.borrow().point.distance_to(neighbor.borrow().point),
|
|
});
|
|
node.borrow_mut().final_edge = Some(final_edge);
|
|
}
|
|
|
|
if node.borrow().edges.is_some() {
|
|
return;
|
|
}
|
|
|
|
let point = node.borrow().point;
|
|
let mut edges = Vec::new();
|
|
for neighbor in &self.regular_nodes {
|
|
if Rc::ptr_eq(neighbor, node) {
|
|
continue;
|
|
}
|
|
let neighbor_point = neighbor.borrow().point;
|
|
if !self.collides_with_wall(&LineSegment::new(point, neighbor_point), walls) {
|
|
let cost = point.distance_to(neighbor_point);
|
|
edges.push(Edge {
|
|
target: neighbor.clone(),
|
|
cost,
|
|
});
|
|
}
|
|
}
|
|
node.borrow_mut().edges = Some(edges);
|
|
}
|
|
|
|
fn collides_with_wall(&self, line: &LineSegment, walls: &[LineSegment]) -> bool {
|
|
walls.iter().any(|wall| line.intersection(wall).is_some())
|
|
}
|
|
|
|
pub fn cleanup_final_edges(&mut self) {
|
|
for node in &self.regular_nodes {
|
|
node.borrow_mut().final_edge = None;
|
|
}
|
|
}
|
|
|
|
pub fn iter(&self) -> NodeStorageIterator {
|
|
self.regular_nodes.iter().chain(self.final_node.iter())
|
|
}
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub struct Pathfinder {
|
|
#[wasm_bindgen(skip)]
|
|
pub nodes: NodeStorage,
|
|
#[wasm_bindgen(skip)]
|
|
pub walls: Vec<LineSegment>,
|
|
}
|
|
|
|
impl Pathfinder {
|
|
pub fn initialize<I>(walls: I, token_size: f64) -> Self
|
|
where
|
|
I: IntoIterator<Item = Wall>,
|
|
{
|
|
let distance_from_walls = token_size / 2.0;
|
|
let mut endpoints = FxHashMap::<Point, Vec<f64>>::default();
|
|
let mut line_segments = Vec::new();
|
|
for wall in walls {
|
|
if wall.move_type == WallSenseType::NONE {
|
|
continue;
|
|
}
|
|
if wall.is_door() && wall.is_open() {
|
|
continue;
|
|
}
|
|
let x_diff = wall.p2.x - wall.p1.x;
|
|
let y_diff = wall.p2.y - wall.p1.y;
|
|
let p1_angle = y_diff.atan2(x_diff).rem_euclid(2.0 * PI);
|
|
let p2_angle = (p1_angle + PI).rem_euclid(2.0 * PI);
|
|
for (point, angle) in [(wall.p1, p1_angle), (wall.p2, p2_angle)] {
|
|
let angles = endpoints.entry(point).or_insert_with(Vec::new);
|
|
angles.push(angle);
|
|
}
|
|
line_segments.push(LineSegment::new(wall.p1, wall.p2));
|
|
}
|
|
endpoints
|
|
.values_mut()
|
|
.for_each(|angles| angles.sort_by(|a, b| a.partial_cmp(b).unwrap()));
|
|
let mut nodes = NodeStorage::new();
|
|
for (point, angles) in endpoints {
|
|
assert!(!angles.is_empty());
|
|
for i in 1..angles.len() {
|
|
let angle1 = angles[i - 1];
|
|
let angle2 = angles[i];
|
|
if angle1 == angle2 {
|
|
continue;
|
|
}
|
|
let angle_diff = angle2 - angle1;
|
|
if angle_diff <= PI {
|
|
continue;
|
|
}
|
|
{
|
|
let angle_between = angle_diff / 2.0 + angle1;
|
|
let pathfinding_node = calc_pathfinding_node(
|
|
point,
|
|
angle_between,
|
|
distance_from_walls,
|
|
&mut line_segments,
|
|
);
|
|
if angle_diff > 1.5 * PI {
|
|
nodes.push(pathfinding_node);
|
|
}
|
|
}
|
|
nodes.push(calc_pathfinding_node(
|
|
point,
|
|
angle1 + 0.5 * PI,
|
|
distance_from_walls,
|
|
&mut line_segments,
|
|
));
|
|
nodes.push(calc_pathfinding_node(
|
|
point,
|
|
angle2 - 0.5 * PI,
|
|
distance_from_walls,
|
|
&mut line_segments,
|
|
));
|
|
}
|
|
let angle1 = angles.last().unwrap();
|
|
let angle2 = angles.first().unwrap() + 2.0 * PI;
|
|
let angle_diff = angle2 - angle1;
|
|
if angle_diff <= PI {
|
|
continue;
|
|
}
|
|
if angle_diff > 1.5 * PI {
|
|
let angle_between = angle_diff / 2.0 + angle1;
|
|
nodes.push(calc_pathfinding_node(
|
|
point,
|
|
angle_between,
|
|
distance_from_walls,
|
|
&mut line_segments,
|
|
));
|
|
}
|
|
nodes.push(calc_pathfinding_node(
|
|
point,
|
|
angle1 + 0.5 * PI,
|
|
distance_from_walls,
|
|
&mut line_segments,
|
|
));
|
|
nodes.push(calc_pathfinding_node(
|
|
point,
|
|
angle2 - 0.5 * PI,
|
|
distance_from_walls,
|
|
&mut line_segments,
|
|
));
|
|
}
|
|
// TODO Eliminating nodes close to each other may improve performance
|
|
Self {
|
|
nodes,
|
|
walls: line_segments,
|
|
}
|
|
}
|
|
|
|
pub fn find_path(&mut self, from: Point, to: Point) -> Option<DiscoveredNodePtr> {
|
|
self.nodes.cleanup_final_edges();
|
|
let mut nodes = self.nodes.clone();
|
|
nodes.final_node = Some(NodePtr::from(Node::new(from)));
|
|
let to_node = NodePtr::from(Node::new(to));
|
|
nodes.initialize_edges(&to_node, &self.walls);
|
|
let to = DiscoveredNode {
|
|
node: to_node,
|
|
cost: 0.0,
|
|
estimated: to.distance_to(from),
|
|
previous: None,
|
|
};
|
|
// TODO Use a sorted set for next_nodes for better performance
|
|
let mut next_nodes = vec![DiscoveredNodePtr::from(to)];
|
|
let mut previous_nodes = PtrIndexedHashSet::new();
|
|
while !next_nodes.is_empty() {
|
|
// Sort by estimated cost, high to low
|
|
// TODO Maybe tere's a faster way to do this than re-sorting every iteration?
|
|
next_nodes.sort_by(|a, b| {
|
|
b.borrow()
|
|
.estimated
|
|
.partial_cmp(&a.borrow().estimated)
|
|
.unwrap()
|
|
});
|
|
|
|
// Get node with cheapest estimate
|
|
let current_node = next_nodes.pop().unwrap();
|
|
if current_node.borrow().node.borrow().point.x == from.x
|
|
&& current_node.borrow().node.borrow().point.y == from.y
|
|
{
|
|
return Some(current_node);
|
|
}
|
|
previous_nodes.insert(current_node.borrow().node.clone());
|
|
for edge in current_node.borrow().node.borrow().iter_edges() {
|
|
let neighbor = &edge.target;
|
|
if previous_nodes.contains(neighbor) {
|
|
continue;
|
|
}
|
|
nodes.initialize_edges(neighbor, &self.walls);
|
|
// Add a flat 0.00001 cost per node to discurage creation of unnecessary waypoints
|
|
let cost = current_node.borrow().cost + edge.cost + 0.00001;
|
|
let discovered_neighbor = DiscoveredNode {
|
|
node: neighbor.clone(),
|
|
cost,
|
|
estimated: cost + neighbor.borrow().point.distance_to(from),
|
|
previous: Some(current_node.clone()),
|
|
};
|
|
let neighbor_entry = next_nodes
|
|
.iter()
|
|
.find(|node| Rc::ptr_eq(&node.borrow().node, neighbor));
|
|
if let Some(entry) = neighbor_entry {
|
|
// If the neighbor is cheaper to reach via the current route than through previously discovered routes, replace it
|
|
if entry.borrow().cost > cost {
|
|
*entry.borrow_mut() = discovered_neighbor;
|
|
}
|
|
} else {
|
|
next_nodes.push(discovered_neighbor.into());
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
}
|
|
|
|
fn calc_pathfinding_node(
|
|
p: Point,
|
|
angle: f64,
|
|
distance_from_walls: f64,
|
|
line_segments: &mut Vec<LineSegment>,
|
|
) -> NodePtr {
|
|
let offset_x = angle.cos() * distance_from_walls;
|
|
let offset_y = angle.sin() * distance_from_walls;
|
|
line_segments.push(LineSegment::new(
|
|
p,
|
|
Point {
|
|
x: p.x + offset_x * 0.99,
|
|
y: p.y + offset_y * 0.99,
|
|
},
|
|
));
|
|
NodePtr::from(Node::new(Point {
|
|
x: p.x + offset_x,
|
|
y: p.y + offset_y,
|
|
}))
|
|
}
|