use nalgebra::{Point3, Vector2};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct NavTriangle {
pub vertices: [usize; 3],
pub neighbors: [Option<usize>; 3],
}
impl NavTriangle {
pub const fn new(vertices: [usize; 3], neighbors: [Option<usize>; 3]) -> Self {
Self {
vertices,
neighbors,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct NavMesh<'a> {
pub vertices: &'a [[f32; 3]],
pub triangles: &'a [NavTriangle],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NavError {
StartOutsideMesh,
EndOutsideMesh,
NoPath,
BufferOverflow,
}
#[derive(Clone, Copy)]
struct AStarNode {
triangle_idx: usize,
parent: Option<usize>,
g_cost: f32,
f_cost: f32,
}
impl<'a> NavMesh<'a> {
pub const fn new(vertices: &'a [[f32; 3]], triangles: &'a [NavTriangle]) -> Self {
Self {
vertices,
triangles,
}
}
pub fn triangle_center(&self, tri_idx: usize) -> Point3<f32> {
let tri = &self.triangles[tri_idx];
let v0 = self.vertices[tri.vertices[0]];
let v1 = self.vertices[tri.vertices[1]];
let v2 = self.vertices[tri.vertices[2]];
Point3::new(
(v0[0] + v1[0] + v2[0]) / 3.0,
(v0[1] + v1[1] + v2[1]) / 3.0,
(v0[2] + v1[2] + v2[2]) / 3.0,
)
}
pub fn find_containing_triangle(&self, point: Point3<f32>) -> Option<usize> {
let p = Vector2::new(point.x, point.z);
for (idx, tri) in self.triangles.iter().enumerate() {
let v0 = self.vertices[tri.vertices[0]];
let v1 = self.vertices[tri.vertices[1]];
let v2 = self.vertices[tri.vertices[2]];
let p0 = Vector2::new(v0[0], v0[2]);
let p1 = Vector2::new(v1[0], v1[2]);
let p2 = Vector2::new(v2[0], v2[2]);
if point_in_triangle_2d(p, p0, p1, p2) {
return Some(idx);
}
}
None
}
pub fn find_path<const MAX_NODES: usize, const MAX_PATH: usize>(
&self,
start: Point3<f32>,
end: Point3<f32>,
out_path: &mut heapless::Vec<Point3<f32>, MAX_PATH>,
) -> Result<(), NavError> {
out_path.clear();
let start_tri = self
.find_containing_triangle(start)
.ok_or(NavError::StartOutsideMesh)?;
let end_tri = self
.find_containing_triangle(end)
.ok_or(NavError::EndOutsideMesh)?;
if start_tri == end_tri {
let _ = out_path.push(start);
let _ = out_path.push(end);
return Ok(());
}
let mut nodes: [Option<AStarNode>; MAX_NODES] = [const { None }; MAX_NODES];
let mut closed_mask: [bool; MAX_NODES] = [false; MAX_NODES];
let start_center = self.triangle_center(start_tri);
let end_center = self.triangle_center(end_tri);
let h_cost = (end_center - start_center).norm();
nodes[0] = Some(AStarNode {
triangle_idx: start_tri,
parent: None,
g_cost: 0.0,
f_cost: h_cost,
});
let mut node_count = 1usize;
let mut found_end_node_idx: Option<usize> = None;
while let Some((best_idx, best_node)) = find_lowest_f_cost(&nodes, &closed_mask) {
if best_node.triangle_idx == end_tri {
found_end_node_idx = Some(best_idx);
break;
}
closed_mask[best_idx] = true;
let current_tri = &self.triangles[best_node.triangle_idx];
let current_center = self.triangle_center(best_node.triangle_idx);
for &neighbor_opt in ¤t_tri.neighbors {
let Some(neighbor_idx) = neighbor_opt else {
continue;
};
let already_closed = (0..node_count).any(|i| {
closed_mask[i] && nodes[i].is_some_and(|n| n.triangle_idx == neighbor_idx)
});
if already_closed {
continue;
}
let neighbor_center = self.triangle_center(neighbor_idx);
let step_dist = (neighbor_center - current_center).norm();
let g = best_node.g_cost + step_dist;
let h = (end_center - neighbor_center).norm();
let f = g + h;
let existing_open_idx = (0..node_count).find(|&i| {
!closed_mask[i] && nodes[i].is_some_and(|n| n.triangle_idx == neighbor_idx)
});
if let Some(open_idx) = existing_open_idx {
if let Some(ref mut existing_node) = nodes[open_idx] {
if g < existing_node.g_cost {
existing_node.g_cost = g;
existing_node.f_cost = f;
existing_node.parent = Some(best_idx);
}
}
} else if node_count < MAX_NODES {
nodes[node_count] = Some(AStarNode {
triangle_idx: neighbor_idx,
parent: Some(best_idx),
g_cost: g,
f_cost: f,
});
node_count += 1;
} else {
return Err(NavError::BufferOverflow);
}
}
}
let end_node_idx = found_end_node_idx.ok_or(NavError::NoPath)?;
let mut triangle_chain: heapless::Vec<usize, MAX_PATH> = heapless::Vec::new();
let mut curr = Some(end_node_idx);
while let Some(idx) = curr {
let node = nodes[idx].unwrap();
if triangle_chain.push(node.triangle_idx).is_err() {
break;
}
curr = node.parent;
}
let _ = out_path.push(start);
for &tri_idx in triangle_chain.iter().rev() {
let center = self.triangle_center(tri_idx);
if out_path.push(center).is_err() {
break;
}
}
let _ = out_path.push(end);
Ok(())
}
}
#[inline]
fn find_lowest_f_cost(
nodes: &[Option<AStarNode>],
closed_mask: &[bool],
) -> Option<(usize, AStarNode)> {
let mut lowest_f = f32::MAX;
let mut best = None;
for (i, node_opt) in nodes.iter().enumerate() {
if closed_mask[i] {
continue;
}
if let Some(node) = node_opt {
if node.f_cost < lowest_f {
lowest_f = node.f_cost;
best = Some((i, *node));
}
}
}
best
}
#[inline]
fn point_in_triangle_2d(
p: Vector2<f32>,
p0: Vector2<f32>,
p1: Vector2<f32>,
p2: Vector2<f32>,
) -> bool {
let d1 = sign(p, p0, p1);
let d2 = sign(p, p1, p2);
let d3 = sign(p, p2, p0);
let has_neg = (d1 < -1e-4) || (d2 < -1e-4) || (d3 < -1e-4);
let has_pos = (d1 > 1e-4) || (d2 > 1e-4) || (d3 > 1e-4);
!(has_neg && has_pos)
}
#[inline]
fn sign(p1: Vector2<f32>, p2: Vector2<f32>, p3: Vector2<f32>) -> f32 {
(p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y)
}
#[cfg(test)]
mod tests {
use super::*;
static VERTS: [[f32; 3]; 4] = [
[0.0, 0.0, 0.0],
[2.0, 0.0, 0.0],
[2.0, 0.0, 2.0],
[0.0, 0.0, 2.0],
];
static TRIS: [NavTriangle; 2] = [
NavTriangle::new([0, 1, 2], [None, Some(1), None]),
NavTriangle::new([0, 2, 3], [Some(0), None, None]),
];
#[test]
fn test_navmesh_pathfinding() {
let navmesh = NavMesh::new(&VERTS, &TRIS);
let start = Point3::new(0.5, 0.0, 0.5);
let end = Point3::new(0.5, 0.0, 1.5);
let mut path: heapless::Vec<Point3<f32>, 16> = heapless::Vec::new();
let res = navmesh.find_path::<16, 16>(start, end, &mut path);
assert!(res.is_ok());
assert!(!path.is_empty());
assert_eq!(path[0], start);
assert_eq!(path[path.len() - 1], end);
}
}