diff --git a/Cargo.toml b/Cargo.toml index 8e2280a..7129e25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ glam = { version = "0.24", features = ["approx"] } smallvec = { version = "1.9", features = ["union", "const_generics"] } bvh2d = { version = "0.3", git = "https://github.com/mockersf/bvh2d" } serde = { version = "1.0", features = ["derive"], optional = true } +spade = "2.2" [dev-dependencies] criterion = "0.5" @@ -51,3 +52,7 @@ harness = false [[bench]] name = "baking" harness = false + +[[bench]] +name = "triangulation" +harness = false diff --git a/benches/triangulation.rs b/benches/triangulation.rs new file mode 100644 index 0000000..728e89d --- /dev/null +++ b/benches/triangulation.rs @@ -0,0 +1,141 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use glam::vec2; +use polyanya::{Mesh, Triangulation}; + +fn baking(c: &mut Criterion) { + c.bench_function(&"triangulation".to_string(), |b| { + b.iter(|| { + // Equivalent to the arena mesh + let mut triangulation = Triangulation::from_outer_edges(vec![ + vec2(1., 3.), + vec2(2., 3.), + vec2(2., 2.), + vec2(3., 2.), + vec2(3., 1.), + vec2(15., 1.), + vec2(15., 3.), + vec2(18., 3.), + vec2(18., 2.), + vec2(19., 2.), + vec2(19., 1.), + vec2(20., 1.), + vec2(20., 2.), + vec2(23., 2.), + vec2(23., 1.), + vec2(26., 1.), + vec2(26., 3.), + vec2(29., 3.), + vec2(29., 2.), + vec2(30., 2.), + vec2(30., 1.), + vec2(31., 1.), + vec2(31., 3.), + vec2(34., 3.), + vec2(34., 2.), + vec2(35., 2.), + vec2(35., 1.), + vec2(47., 1.), + vec2(47., 3.), + vec2(48., 3.), + vec2(48., 15.), + vec2(47., 15.), + vec2(47., 18.), + vec2(48., 18.), + vec2(48., 31.), + vec2(47., 31.), + vec2(47., 35.), + vec2(48., 35.), + vec2(48., 47.), + vec2(47., 47.), + vec2(47., 48.), + vec2(35., 48.), + vec2(35., 47.), + vec2(31., 47.), + vec2(31., 48.), + vec2(30., 48.), + vec2(30., 47.), + vec2(29., 47.), + vec2(29., 46.), + vec2(26., 46.), + vec2(26., 48.), + vec2(24., 48.), + vec2(24., 47.), + vec2(23., 47.), + vec2(23., 46.), + vec2(20., 46.), + vec2(20., 48.), + vec2(19., 48.), + vec2(19., 47.), + vec2(15., 47.), + vec2(15., 48.), + vec2(3., 48.), + vec2(3., 47.), + vec2(1., 47.), + vec2(1., 35.), + vec2(2., 35.), + vec2(2., 34.), + vec2(3., 34.), + vec2(3., 31.), + vec2(1., 31.), + vec2(1., 30.), + vec2(3., 30.), + vec2(3., 27.), + vec2(2., 27.), + vec2(2., 26.), + vec2(1., 26.), + vec2(1., 23.), + vec2(2., 23.), + vec2(2., 18.), + vec2(3., 18.), + vec2(3., 15.), + vec2(1., 15.), + ]); + + triangulation.add_obstacle(vec![ + vec2(15., 15.), + vec2(19., 15.), + vec2(19., 18.), + vec2(18., 18.), + vec2(18., 19.), + vec2(15., 19.), + ]); + triangulation.add_obstacle(vec![ + vec2(31., 15.), + vec2(35., 15.), + vec2(35., 18.), + vec2(34., 18.), + vec2(34., 19.), + vec2(31., 19.), + ]); + triangulation.add_obstacle(vec![ + vec2(15., 31.), + vec2(19., 31.), + vec2(19., 34.), + vec2(18., 34.), + vec2(18., 35.), + vec2(15., 35.), + ]); + triangulation.add_obstacle(vec![ + vec2(31., 31.), + vec2(35., 31.), + vec2(35., 34.), + vec2(34., 34.), + vec2(34., 35.), + vec2(31., 35.), + ]); + triangulation.add_obstacle(vec![ + vec2(23., 10.), + vec2(23., 8.), + vec2(24., 8.), + vec2(24., 7.), + vec2(26., 7.), + vec2(26., 10.), + ]); + let mesh: Mesh = triangulation.into(); + black_box(mesh); + }) + }); +} + +criterion_group!(benches, baking); +criterion_main!(benches); diff --git a/src/input/mod.rs b/src/input/mod.rs index 915de70..ee904b1 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -1,2 +1,3 @@ pub mod polyanya_file; +pub mod triangulation; pub mod trimesh; diff --git a/src/input/triangulation.rs b/src/input/triangulation.rs new file mode 100644 index 0000000..6026add --- /dev/null +++ b/src/input/triangulation.rs @@ -0,0 +1,165 @@ +use std::collections::VecDeque; + +use glam::{vec2, Vec2}; +use hashbrown::HashMap; +use spade::{ConstrainedDelaunayTriangulation, Point2, Triangulation as SpadeTriangulation}; + +use crate::{ + helpers::{line_intersect_segment, Vec2Helper}, + Mesh, Polygon, Vertex, +}; + +/// An helper to create a [`Mesh`] from a list of edges and obstacle, using a constrained Delaunay triangulation. +#[derive(Debug)] +pub struct Triangulation { + edges: Vec, + obstacles: Vec>, +} + +impl Triangulation { + /// Create a new triangulation from a the list of points on its outer edges. + pub fn from_outer_edges(edges: Vec) -> Triangulation { + Self { + edges, + obstacles: Default::default(), + } + } + + /// Add an obstacle delimited by the list of points on its edges. + pub fn add_obstacle(&mut self, edges: Vec) { + self.obstacles.push(edges); + } + + #[inline] + fn add_constraint_edges( + cdt: &mut ConstrainedDelaunayTriangulation>, + edges: Vec, + ) -> (Vec<(Vec2, Vec2)>, (Vec2, Vec2)) { + let mut edge_iter = edges.iter().peekable(); + let mut vertex_pairs = Vec::new(); + let mut aabb_min = edges[0]; + let mut aabb_max = edges[0]; + loop { + let from = edge_iter.next().unwrap(); + let next = edge_iter.peek(); + + aabb_min = aabb_min.min(*from); + aabb_max = aabb_max.max(*from); + + if let Some(next) = next { + cdt.add_constraint_edge( + Point2 { + x: from.x, + y: from.y, + }, + Point2 { + x: next.x, + y: next.y, + }, + ) + .unwrap(); + vertex_pairs.push((*from, **next)); + } else { + cdt.add_constraint_edge( + Point2 { + x: from.x, + y: from.y, + }, + Point2 { + x: edges[0].x, + y: edges[0].y, + }, + ) + .unwrap(); + vertex_pairs.push((*from, edges[0])); + break; + } + } + + (vertex_pairs, (aabb_min, aabb_max)) + } +} + +impl From for Mesh { + fn from(value: Triangulation) -> Self { + let mut cdt = ConstrainedDelaunayTriangulation::>::new(); + let (outer_edges, aabb_outer) = Triangulation::add_constraint_edges(&mut cdt, value.edges); + + let mut obstacles = vec![]; + for obstacle in value.obstacles { + obstacles.push(Triangulation::add_constraint_edges(&mut cdt, obstacle)); + } + + let in_polygon = |point: Vec2, edges: &Vec<(Vec2, Vec2)>, aabb: (Vec2, Vec2)| { + if point.x < aabb.0.x || point.x > aabb.1.x || point.y < aabb.0.y || point.y > aabb.1.y + { + return false; + } + let mut parallel = 0; + let intersect = edges + .iter() + .filter(|edge| { + let start = point; + let far = point + vec2(0.0, aabb.1.y + 1.0); + if edge.0.on_segment((start, far)) && edge.1.on_segment((start, far)) { + parallel += 1; + } + line_intersect_segment((start, far), **edge) + .map(|i| i.y > start.y) + .unwrap_or(false) + }) + .count(); + (intersect - parallel) % 2 == 1 + }; + + let mut face_to_polygon = HashMap::new(); + let polygons = cdt + .inner_faces() + .filter_map(|face| { + let centre = face.center(); + let centre = Vec2::new(centre.x, centre.y); + (in_polygon(centre, &outer_edges, aabb_outer) + && obstacles + .iter() + .map(|(edges, aabb)| !in_polygon(centre, edges, *aabb)) + .all(|b| b)) + .then(|| { + face_to_polygon.insert(face.index(), face_to_polygon.len() as isize); + Polygon::new( + face.vertices() + .iter() + .map(|vhandle| vhandle.index() as u32) + .collect(), + false, + ) + }) + }) + .collect::>(); + + let vertices = cdt + .vertices() + .map(|point| { + let mut point_polygons = point + .out_edges() + .map(|out_edge| { + face_to_polygon + .get(&out_edge.face().index()) + .cloned() + .unwrap_or(-1) + }) + .collect::>(); + while point_polygons[0] == -1 { + point_polygons.rotate_left(1); + } + let mut point_polygons: Vec<_> = point_polygons.into(); + point_polygons.dedup(); + Vertex::new( + Vec2::new(point.position().x, point.position().y), + point_polygons, + ) + }) + .collect::>(); + + Mesh::new(vertices, polygons) + } +} diff --git a/src/lib.rs b/src/lib.rs index 0682824..e68bfe3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,6 +45,7 @@ mod primitives; #[cfg(feature = "async")] pub use async_helpers::FuturePath; pub use input::polyanya_file::PolyanyaFile; +pub use input::triangulation::Triangulation; pub use input::trimesh::Trimesh; pub use primitives::{Polygon, Vertex};