Rust code for gridless pathfinding

This commit is contained in:
Manuel Vögele
2022-01-31 21:48:20 +01:00
parent dc85609a75
commit 08f41ed2ff
8 changed files with 463 additions and 3 deletions
+2 -2
View File
@@ -13,12 +13,12 @@ import {recalculate} from "./socket.js";
import {SpeedProvider} from "./speed_provider.js" import {SpeedProvider} from "./speed_provider.js"
import {setSnapParameterOnOptions} from "./util.js"; import {setSnapParameterOnOptions} from "./util.js";
import * as GridlessPathfinding from "../wasm/gridless_pathfinding.js" import initGridlessPathfinding, * as GridlessPathfinding from "../wasm/gridless_pathfinding.js"
CONFIG.debug.dragRuler = false; CONFIG.debug.dragRuler = false;
export let debugGraphics = undefined; export let debugGraphics = undefined;
GridlessPathfinding.init(); initGridlessPathfinding();
Hooks.once("init", () => { Hooks.once("init", () => {
registerSettings() registerSettings()
+17
View File
@@ -29,6 +29,17 @@ name = "gridless-pathfinding"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"console_error_panic_hook", "console_error_panic_hook",
"js-sys",
"rustc-hash",
"wasm-bindgen",
]
[[package]]
name = "js-sys"
version = "0.3.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04"
dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
@@ -65,6 +76,12 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.86" version = "1.0.86"
+2
View File
@@ -14,4 +14,6 @@ lto = true
[dependencies] [dependencies]
console_error_panic_hook = "0.1.7" console_error_panic_hook = "0.1.7"
js-sys = "0.3.56"
rustc-hash = "1.1.0"
wasm-bindgen = "0.2.79" wasm-bindgen = "0.2.79"
+51
View File
@@ -0,0 +1,51 @@
use std::hash::{Hash, Hasher};
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
pub type JsPoint;
#[wasm_bindgen(method, getter)]
fn x(this: &JsPoint) -> f64;
#[wasm_bindgen(method, getter)]
fn y(this: &JsPoint) -> f64;
}
#[wasm_bindgen]
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Point {
pub x: f64,
pub y: f64,
}
impl Point {
pub fn new(x: f64, y: f64) -> Self {
Self { x, y }
}
pub fn distance_to(&self, to: Point) -> f64 {
(self.y - to.y).hypot(self.x - to.x)
}
pub fn is_same_as(&self, other: &Self) -> bool {
let e = 0.000001;
(self.x - other.x).abs() < e && (self.y - other.y).abs() < e
}
}
impl Eq for Point {}
impl Hash for Point {
fn hash<H: Hasher>(&self, hasher: &mut H) {
self.x.to_bits().hash(hasher);
self.y.to_bits().hash(hasher);
}
}
impl From<&JsPoint> for Point {
fn from(point: &JsPoint) -> Self {
Self::new(point.x(), point.y())
}
}
+118
View File
@@ -0,0 +1,118 @@
use js_sys::Array;
use wasm_bindgen::prelude::*;
use crate::{
geometry::Point,
pathfinder::{DiscoveredNodePtr, Pathfinder},
};
#[allow(unused)]
macro_rules! log {
( $( $t:tt )* ) => {
log(&format!( $( $t )* ));
};
}
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console, js_name=warn)]
pub fn log(s: &str);
}
#[wasm_bindgen(
inline_js = "export function collidesWithWall(p1, p2) { return canvas.walls.checkCollision(new Ray(p1, p2)); }"
)]
extern "C" {
#[wasm_bindgen(js_name=collidesWithWall)]
pub fn collides_with_wall(p1: Point, p2: Point) -> bool;
}
#[wasm_bindgen]
extern "C" {
pub type JsWall;
pub type JsWallData;
#[wasm_bindgen(method, getter)]
fn data(this: &JsWall) -> JsWallData;
#[wasm_bindgen(method, getter)]
fn c(this: &JsWallData) -> Vec<f64>;
}
#[derive(Debug, Clone, Copy)]
pub struct Wall {
pub p1: Point,
pub p2: Point,
}
impl Wall {
pub fn new(p1: Point, p2: Point) -> Self {
Self { p1, p2 }
}
}
impl Wall {
fn from_js(wall: &JsWall) -> Self {
let data = wall.data();
let mut c = data.c();
c.iter_mut().for_each(|val| *val = val.round());
Self::new(Point::new(c[0], c[1]), Point::new(c[2], c[3]))
}
}
#[allow(dead_code)]
#[wasm_bindgen(js_name=buildCache)]
pub fn initialize(js_walls: Vec<JsValue>) -> Pathfinder {
let mut walls = Vec::with_capacity(js_walls.len());
for wall in js_walls {
let wall = JsWall::from(wall);
walls.push(Wall::from_js(&wall));
}
Pathfinder::initialize(walls)
}
#[allow(dead_code)]
#[wasm_bindgen]
pub fn free(pathfinder: Pathfinder) {
drop(pathfinder);
}
#[allow(dead_code)]
#[wasm_bindgen(js_name=findPath)]
pub fn find_path(pathfinder: &mut Pathfinder, from: Point, to: Point) -> Option<Array> {
if let Some(first_node) = pathfinder.find_path(from, to) {
Some(first_node.iter_path().map(JsValue::from).collect())
} else {
None
}
}
trait IteratePath {
fn iter_path(&self) -> PathIterator;
}
impl IteratePath for DiscoveredNodePtr {
fn iter_path(&self) -> PathIterator {
PathIterator {
current_node: Some(self.clone()),
}
}
}
struct PathIterator {
current_node: Option<DiscoveredNodePtr>,
}
impl Iterator for PathIterator {
type Item = Point;
fn next(&mut self) -> Option<Self::Item> {
if let Some(node) = self.current_node.clone() {
let point = node.borrow().node.borrow().point;
self.current_node = node.borrow().previous.clone();
Some(point)
} else {
None
}
}
}
+6 -1
View File
@@ -1,6 +1,11 @@
mod geometry;
mod js_api;
mod pathfinder;
mod ptr_indexed_hash_set;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
#[wasm_bindgen(start)] #[wasm_bindgen(start)]
pub fn main() { pub fn main() {
std::panic::set_hook(Box::new(console_error_panic_hook::hook)) std::panic::set_hook(Box::new(console_error_panic_hook::hook));
} }
+200
View File
@@ -0,0 +1,200 @@
use std::{cell::RefCell, rc::Rc};
use wasm_bindgen::prelude::*;
use rustc_hash::FxHashMap;
use crate::{
geometry::Point,
js_api::{collides_with_wall, Wall},
ptr_indexed_hash_set::PtrIndexedHashSet,
};
pub struct Edge {
target: NodePtr,
cost: f64,
}
pub struct Node {
pub point: Point,
edges: Option<Vec<Edge>>,
}
impl Node {
pub fn new(point: Point) -> Self {
Self { point, edges: None }
}
}
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(Vec<NodePtr>);
impl NodeStorage {
fn new() -> Self {
Self::default()
}
fn push(&mut self, node: NodePtr) {
self.0.push(node);
}
fn initialize_edges(&self, node: &NodePtr) {
if node.borrow().edges.is_some() {
return;
}
let point = node.borrow().point;
let mut edges = Vec::new();
for neighbor in &self.0 {
if Rc::ptr_eq(neighbor, &node) {
continue;
}
let neighbor_point = neighbor.borrow().point;
if !collides_with_wall(point, neighbor_point) {
let cost = point.distance_to(neighbor_point);
edges.push(Edge {
target: neighbor.clone(),
cost,
});
}
}
node.borrow_mut().edges = Some(edges);
}
}
#[wasm_bindgen]
pub struct Pathfinder {
#[wasm_bindgen(skip)]
pub nodes: NodeStorage,
}
impl Pathfinder {
pub fn initialize<I>(walls: I) -> Self
where
I: IntoIterator<Item = Wall>,
{
let mut endpoints = FxHashMap::<Point, Vec<f64>>::default();
for wall in walls {
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);
let p2_angle = (y_diff + 180.0).rem_euclid(360.0);
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);
}
}
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.len() > 0);
for i in 1..angles.len() {
let angle1 = angles[i - 1];
let angle2 = angles[i - 1];
let angle_between = (angle2 - angle1) / 2.0 + angle1;
nodes.push(calc_pathfinding_node(point, angle_between));
}
let angle1 = angles.last().unwrap();
let angle2 = angles.first().unwrap() + 360.0;
let angle_between = (angle2 - angle1) / 2.0 + angle1;
let angle_between = angle_between.rem_euclid(360.0);
nodes.push(calc_pathfinding_node(point, angle_between));
}
// TODO Eliminating nodes close to each other may improve performance
Self { nodes }
}
pub fn find_path(&mut self, from: Point, to: Point) -> Option<DiscoveredNodePtr> {
let mut nodes = self.nodes.clone();
nodes.push(NodePtr::from(Node::new(from)));
let nodes = nodes;
let to_node = NodePtr::from(Node::new(to));
nodes.initialize_edges(&to_node);
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.len() > 0 {
// 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().edges.as_ref().unwrap() {
let neighbor = &edge.target;
if previous_nodes.contains(neighbor) {
continue;
}
nodes.initialize_edges(neighbor);
let cost = current_node.borrow().cost + edge.cost;
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) -> NodePtr {
let diatance_from_walls = 10.0;
let x = p.x + angle.cos() * diatance_from_walls;
let y = p.y + angle.sin() * diatance_from_walls;
NodePtr::from(Node::new(Point { x, y }))
}
+67
View File
@@ -0,0 +1,67 @@
use rustc_hash::FxHashSet;
use std::collections::hash_set;
use std::hash::{Hash, Hasher};
use std::rc::Rc;
pub struct PtrIndexedHashSet<T>(FxHashSet<PtrIndexedRc<T>>);
impl<T> PtrIndexedHashSet<T> {
pub fn new() -> Self {
PtrIndexedHashSet(FxHashSet::default())
}
pub fn insert(&mut self, value: Rc<T>) -> bool {
self.0.insert(PtrIndexedRc(value))
}
pub fn remove(&mut self, value: &Rc<T>) -> bool {
self.0.remove(&PtrIndexedRc(Rc::clone(value)))
}
pub fn contains(&mut self, value: &Rc<T>) -> bool {
self.0.contains(&PtrIndexedRc(Rc::clone(value)))
}
}
impl<T: std::fmt::Debug> std::fmt::Debug for PtrIndexedHashSet<T> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_set().entries(self.0.iter().map(|e| &e.0)).finish()
}
}
struct PtrIndexedRc<T>(Rc<T>);
impl<T> Hash for PtrIndexedRc<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
Rc::as_ptr(&self.0).hash(state)
}
}
impl<T> PartialEq for PtrIndexedRc<T> {
fn eq(&self, other: &Self) -> bool {
Rc::ptr_eq(&self.0, &other.0)
}
}
impl<T> Eq for PtrIndexedRc<T> {}
pub struct PtrIndexedHashSetIterator<'a, T>(hash_set::Iter<'a, PtrIndexedRc<T>>);
impl<'a, T> Iterator for PtrIndexedHashSetIterator<'a, T> {
type Item = &'a Rc<T>;
fn next(&mut self) -> Option<Self::Item> {
match self.0.next() {
Some(item) => Some(&item.0),
None => None,
}
}
}
impl<'a, T> IntoIterator for &'a PtrIndexedHashSet<T> {
type Item = &'a Rc<T>;
type IntoIter = PtrIndexedHashSetIterator<'a, T>;
fn into_iter(self) -> Self::IntoIter {
PtrIndexedHashSetIterator::<T>((&self.0).into_iter())
}
}