use glam::Vec2;
use polyanya::{Mesh as ExternalMesh, Trimesh};
use std::collections::{BTreeMap, BTreeSet};
use crate::{Navmesh, Point2};
const EPSILON: f64 = 1e-9;
const KEY_SCALE: f64 = 1_000_000_000.0;
#[derive(Debug, Clone, Copy, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum PolyanyaMeshAdapterError {
#[error("navmesh cell {cell_index} degenerates during mesh adaptation")]
DegenerateCell {
cell_index: usize,
},
#[error("{0}")]
ExternalMesh(
#[from]
#[source]
polyanya::MeshError,
),
}
#[derive(Debug)]
pub struct PolyanyaMeshAdapter {
vertices: Vec<Vec2>,
triangles: Vec<[usize; 3]>,
triangle_to_cell: Vec<usize>,
}
impl PolyanyaMeshAdapter {
pub fn from_navmesh(navmesh: &Navmesh) -> Result<Self, PolyanyaMeshAdapterError> {
let mut vertices = Vec::new();
let mut triangles = Vec::new();
let mut triangle_to_cell = Vec::new();
let mut shared_vertex_indices = BTreeMap::new();
let shared_portal_endpoints = navmesh
.portals()
.iter()
.flat_map(|portal| [point_key(portal.start), point_key(portal.end)])
.collect::<BTreeSet<_>>();
for (cell_index, cell) in navmesh.cells().iter().enumerate() {
let ring = build_cell_boundary_ring(
navmesh,
cell_index,
&normalized_ccw(cell.vertices()),
&shared_portal_endpoints,
&mut vertices,
&mut shared_vertex_indices,
);
if ring.len() < 3 {
return Err(PolyanyaMeshAdapterError::DegenerateCell { cell_index });
}
let centroid_index = push_vertex(&mut vertices, polygon_centroid(cell.vertices()));
let new_triangles = triangulate_convex_ring(&vertices, centroid_index, &ring);
if new_triangles.is_empty() {
return Err(PolyanyaMeshAdapterError::DegenerateCell { cell_index });
}
for _ in 0..new_triangles.len() {
triangle_to_cell.push(cell_index);
}
triangles.extend(new_triangles);
}
Ok(Self {
vertices,
triangles,
triangle_to_cell,
})
}
pub fn into_external_mesh(self) -> Result<ExternalMesh, PolyanyaMeshAdapterError> {
Trimesh {
vertices: self.vertices,
triangles: self.triangles,
}
.try_into()
.map_err(Into::into)
}
#[must_use]
pub fn triangle_to_cell(&self) -> &[usize] {
&self.triangle_to_cell
}
}
fn build_cell_boundary_ring(
navmesh: &Navmesh,
cell_index: usize,
vertices: &[Point2],
shared_portal_endpoints: &BTreeSet<PointKey>,
global_vertices: &mut Vec<Vec2>,
shared_vertex_indices: &mut BTreeMap<PointKey, usize>,
) -> Vec<usize> {
let mut ring = Vec::new();
let mut local_vertex_indices = BTreeMap::new();
for (start, end) in polygon_edges(vertices) {
let mut edge_points = vec![(0.0, start), (1.0, end)];
for portal in navmesh.portals_from(cell_index) {
for endpoint in [portal.start, portal.end] {
if point_on_segment(endpoint, start, end) {
edge_points.push((segment_parameter(endpoint, start, end), endpoint));
}
}
}
edge_points.sort_by(|left, right| left.0.total_cmp(&right.0));
edge_points.dedup_by(|left, right| points_equal(left.1, right.1));
for (position, (_, point)) in edge_points.iter().enumerate() {
if position + 1 == edge_points.len() {
continue;
}
let key = point_key(*point);
let index = if shared_portal_endpoints.contains(&key) {
*shared_vertex_indices
.entry(key)
.or_insert_with(|| push_vertex(global_vertices, *point))
} else {
*local_vertex_indices
.entry(key)
.or_insert_with(|| push_vertex(global_vertices, *point))
};
if ring.last().copied() != Some(index) {
ring.push(index);
}
}
}
if ring.first() == ring.last() {
ring.pop();
}
ring
}
fn triangulate_convex_ring(
vertices: &[Vec2],
centroid_index: usize,
ring: &[usize],
) -> Vec<[usize; 3]> {
let mut triangles = Vec::with_capacity(ring.len());
for (left, right) in ring
.iter()
.copied()
.zip(ring.iter().copied().cycle().skip(1))
.take(ring.len())
{
let area = triangle_area(vertices[centroid_index], vertices[left], vertices[right]);
if area.abs() <= EPSILON {
continue;
}
if area > 0.0 {
triangles.push([centroid_index, left, right]);
} else {
triangles.push([centroid_index, right, left]);
}
}
triangles
}
fn normalized_ccw(vertices: &[Point2]) -> Vec<Point2> {
if signed_area(vertices) >= 0.0 {
vertices.to_vec()
} else {
let mut reversed = vertices.to_vec();
reversed.reverse();
reversed
}
}
fn polygon_centroid(vertices: &[Point2]) -> Point2 {
let (x_sum, y_sum) = vertices.iter().fold((0.0, 0.0), |(x_sum, y_sum), vertex| {
(x_sum + vertex.x, y_sum + vertex.y)
});
let count = vertices.len() as f64;
Point2::new(x_sum / count, y_sum / count)
}
fn polygon_edges(vertices: &[Point2]) -> impl Iterator<Item = (Point2, Point2)> + '_ {
vertices
.iter()
.copied()
.zip(vertices.iter().copied().cycle().skip(1))
.take(vertices.len())
}
fn push_vertex(vertices: &mut Vec<Vec2>, point: Point2) -> usize {
let index = vertices.len();
vertices.push(to_external_vec2(point));
index
}
fn segment_parameter(point: Point2, start: Point2, end: Point2) -> f64 {
let dx = end.x - start.x;
let dy = end.y - start.y;
if dx.abs() >= dy.abs() && dx.abs() > EPSILON {
((point.x - start.x) / dx).clamp(0.0, 1.0)
} else if dy.abs() > EPSILON {
((point.y - start.y) / dy).clamp(0.0, 1.0)
} else {
0.0
}
}
fn signed_area(vertices: &[Point2]) -> f64 {
polygon_edges(vertices)
.map(|(left, right)| (left.x * right.y) - (right.x * left.y))
.sum::<f64>()
/ 2.0
}
fn triangle_area(a: Vec2, b: Vec2, c: Vec2) -> f64 {
f64::from(((b.x - a.x) * (c.y - a.y)) - ((b.y - a.y) * (c.x - a.x)))
}
fn point_on_segment(point: Point2, start: Point2, end: Point2) -> bool {
let cross =
((point.y - start.y) * (end.x - start.x)) - ((point.x - start.x) * (end.y - start.y));
if cross.abs() > EPSILON {
return false;
}
let dot = ((point.x - start.x) * (end.x - start.x)) + ((point.y - start.y) * (end.y - start.y));
if dot < -EPSILON {
return false;
}
let length_sq =
((end.x - start.x) * (end.x - start.x)) + ((end.y - start.y) * (end.y - start.y));
dot <= length_sq + EPSILON
}
pub fn points_equal(left: Point2, right: Point2) -> bool {
(left.x - right.x).abs() <= EPSILON && (left.y - right.y).abs() <= EPSILON
}
fn point_key(point: Point2) -> PointKey {
PointKey {
x: (point.x * KEY_SCALE).round() as i64,
y: (point.y * KEY_SCALE).round() as i64,
}
}
pub fn to_external_vec2(point: Point2) -> Vec2 {
Vec2::new(point.x as f32, point.y as f32)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct PointKey {
x: i64,
y: i64,
}