diff --git a/Cargo.toml b/Cargo.toml index 9a7cd81..7078ff2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "earcut" -version = "0.3.6" +version = "0.4.0" edition = "2021" description = "A Rust port of the Earcut polygon triangulation library" authors = ["Taku Fukada ", "MIERUNE Inc. "] @@ -9,7 +9,7 @@ repository = "https://github.com/MIERUNE/earcut-rs" categories = ["graphics", "science", "no-std"] [dependencies] -num-traits = "0.2" +num-traits = { version = "0.2", no-default-features = true } [dev-dependencies] serde_json = { version = "1.0", features = ["float_roundtrip"] } diff --git a/README.md b/README.md index de3ee12..59e7958 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,32 @@ A Rust port of the [mapbox/earcut](https://github.com/mapbox/earcut) polygon triangulation library, implemented from scratch with some reference to [donbright/earcutr](https://github.com/donbright/earcutr). - Based on the latest earcut 2.2.4 release. -- Designed to avoid unnecessary memory allocations. You can reuse the internal buffer and the output index vector. -- (Experimental) An additional module, `utils3d`, can rotate polygons from 3D to 2D space before triangulation. +- Designed to avoid unnecessary memory allocations. You can reuse the internal buffer and the output index vector for multiple triangulations. +- (Experimental) An additional module, `utils3d`, can rotate 3D coplanar polygons into the 2D plane before triangulation. - License: ISC

+ + +## Benchmarks + +on Macbook Pro (M1 Pro) + +| Polygon | earcut.hpp | earcut-rs (0.4.0) | earcutr (0.4.3) | +|----------------|-------------:|--------------:|-------------:| +| bad_hole | 3.574 µs/i | 4.630 µs/i | 4.415 µs/i | +| building | 397 ns/i | 197 ns/i | 604 ns/i | +| degenerate | 142 ns/i | 46 ns/i | 206 ns/i | +| dude | 5.061 µs/i | 6.516 µs/i | 8.096 µs/i | +| empty_square | 195 ns/i | 88 ns/i | 331 ns/i | +| water | 459.6 µs/i | 623.5 µs/i | 801.3 µs/i | +| water2 | 334.1 µs/i | 409.5 µs/i | 450.3 µs/i | +| water3 | 13.12 µs/i | 19.12 µs/i | 23.46 µs/i | +| water3b | 1.340 µs/i | 1.368 µs/i | 2.165 µs/i | +| water4 | 81.48 µs/i | 122.0 µs/i | 154.1 µs/i | +| water_huge | 6.906 ms/i | 11.01 ms/i | 10.90 ms/i | +| water_huge2 | 15.38 ms/i | 23.40 ms/i | 22.35 ms/i | + +(Earcutr is not besed on the latest earcut.) diff --git a/benches/benchmark.rs b/benches/benchmark.rs index c4188dc..443560d 100644 --- a/benches/benchmark.rs +++ b/benches/benchmark.rs @@ -4,7 +4,7 @@ use criterion::{criterion_group, criterion_main, Criterion}; use earcut::Earcut; -fn load_fixture(name: &str) -> (Vec, Vec) { +fn load_fixture(name: &str) -> (Vec<[f64; 2]>, Vec) { // load JSON type Coords = Vec>; let s = fs::read_to_string("./tests/fixtures/".to_string() + name + ".json").unwrap(); @@ -12,12 +12,7 @@ fn load_fixture(name: &str) -> (Vec, Vec) { // prepare input let num_holes = expected.len(); - let data = expected - .clone() - .into_iter() - .flatten() - .flatten() - .collect::>(); + let data = expected.clone().into_iter().flatten().collect::>(); let hole_indices: Vec<_> = expected .iter() .map(|x| x.len()) @@ -109,7 +104,6 @@ fn bench(c: &mut Criterion) { let (data, hole_indices) = load_fixture("water-huge"); b.iter(|| { earcut.earcut(data.iter().copied(), &hole_indices, &mut triangles); - assert_eq!(triangles.len(), 5177 * 3) }) }); diff --git a/examples/example.rs b/examples/example.rs index a2aac7e..d4e9164 100644 --- a/examples/example.rs +++ b/examples/example.rs @@ -10,12 +10,7 @@ fn load_fixture(name: &str, num_triangles: usize, expected_deviation: f64) { // prepare input let num_holes = expected.len(); - let vertices = expected - .clone() - .into_iter() - .flatten() - .flatten() - .collect::>(); + let vertices = expected.clone().into_iter().flatten().collect::>(); let hole_indices: Vec<_> = expected .into_iter() .map(|x| x.len() as u32) diff --git a/src/lib.rs b/src/lib.rs index 66b1296..b60ad40 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ extern crate alloc; pub mod utils3d; use alloc::vec::Vec; +use core::num::NonZeroU32; use core::ptr; use num_traits::float::Float; @@ -42,22 +43,36 @@ impl Index for usize { macro_rules! node { ($self:ident.$nodes:ident, $index:expr) => { - unsafe { $self.$nodes.get_unchecked($index as usize) } + unsafe { + debug_assert!($index.get() < $self.$nodes.len() as u32); + $self.$nodes.get_unchecked($index.get() as usize) + } }; ($nodes:ident, $index:expr) => { - unsafe { $nodes.get_unchecked($index as usize) } + unsafe { + debug_assert!($index.get() < $nodes.len() as u32); + $nodes.get_unchecked($index.get() as usize) + } }; } macro_rules! node_mut { ($self:ident.$nodes:ident, $index:expr) => { - unsafe { $self.$nodes.get_unchecked_mut($index as usize) } + unsafe { + debug_assert!($index.get() < $self.$nodes.len() as u32); + $self.$nodes.get_unchecked_mut($index.get() as usize) + } }; ($nodes:ident, $index:expr) => { - unsafe { $nodes.get_unchecked_mut($index as usize) } + unsafe { + debug_assert!($index.get() < $nodes.len() as u32); + $nodes.get_unchecked_mut($index.get() as usize) + } }; } +type NodeIndex = NonZeroU32; + struct Node { /// vertex index in coordinates array i: u32, @@ -68,13 +83,13 @@ struct Node { /// vertex coordinates y y: T, /// previous vertex nodes in a polygon ring - prev_i: u32, + prev_i: NodeIndex, /// next vertex nodes in a polygon ring - next_i: u32, + next_i: NodeIndex, /// previous nodes in z-order - prev_z_i: Option, + prev_z_i: Option, /// next nodes in z-order - next_z_i: Option, + next_z_i: Option, /// indicates whether this is a steiner point steiner: bool, } @@ -85,8 +100,8 @@ impl Node { i, x, y, - prev_i: 0, - next_i: 0, + prev_i: unsafe { NodeIndex::new_unchecked(1) }, + next_i: unsafe { NodeIndex::new_unchecked(1) }, z: 0, prev_z_i: None, next_z_i: None, @@ -97,9 +112,9 @@ impl Node { /// Instance of the earcut algorithm. pub struct Earcut { - data: Vec, + data: Vec<[T; 2]>, nodes: Vec>, - queue: Vec, + queue: Vec, } impl Default for Earcut { @@ -123,6 +138,7 @@ impl Earcut { fn reset(&mut self, capacity: usize) { self.nodes.clear(); self.nodes.reserve(capacity); + self.nodes.push(Node::new(0, T::infinity(), T::infinity())); // dummy node } /// Performs the earcut triangulation on a polygon. @@ -130,24 +146,32 @@ impl Earcut { /// The API is similar to the original JavaScript implementation, except you can provide a vector for the output indices. pub fn earcut( &mut self, - data: impl IntoIterator, + data: impl IntoIterator, hole_indices: &[N], triangles_out: &mut Vec, ) { - // initialize self.data.clear(); self.data.extend(data); triangles_out.clear(); - self.reset(self.data.len() / 2 * 3 / 2); + if self.data.len() < 3 { + return; + } + self.earcut_impl(hole_indices, triangles_out); + } + + pub fn earcut_impl(&mut self, hole_indices: &[N], triangles_out: &mut Vec) { + triangles_out.reserve(self.data.len() + 1); + self.reset(self.data.len() / 2 * 3); + let has_holes = !hole_indices.is_empty(); let outer_len: usize = if has_holes { - hole_indices[0].into_usize() * 2 + hole_indices[0].into_usize() } else { self.data.len() }; // create nodes - let Some(mut outer_node_i) = self.linked_list(0, outer_len as u32, true) else { + let Some(mut outer_node_i) = self.linked_list(0, outer_len, true) else { return; }; let outer_node = node!(self.nodes, outer_node_i); @@ -163,24 +187,20 @@ impl Earcut { let mut inv_size = T::zero(); // if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox - if self.data.len() > 80 * 2 { - let max_x = self.data[2..outer_len] + if self.data.len() > 80 { + let [max_x, max_y] = + self.data[1..outer_len] + .iter() + .fold(self.data[0], |[ax, ay], b| { + let (bx, by) = (b[0], b[1]); + [T::max(ax, bx), T::max(ay, by)] + }); + [min_x, min_y] = self.data[1..outer_len] .iter() - .step_by(2) - .fold(self.data[0], |a, b| T::max(a, *b)); - min_x = self.data[2..outer_len] - .iter() - .step_by(2) - .fold(self.data[0], |a, b| T::min(a, *b)); - let max_y = self.data[2 + 1..outer_len] - .iter() - .step_by(2) - .fold(self.data[1], |a, b| T::max(a, *b)); - min_y = self.data[2 + 1..outer_len] - .iter() - .step_by(2) - .fold(self.data[1], |a, b| T::min(a, *b)); - + .fold(self.data[0], |[ax, ay], b| { + let (bx, by) = (b[0], b[1]); + [T::min(ax, bx), T::min(ay, by)] + }); // minX, minY and invSize are later used to transform coords into integers for z-order calculation inv_size = (max_x - min_x).max(max_y - min_y); if inv_size != T::zero() { @@ -200,29 +220,24 @@ impl Earcut { } /// create a circular doubly linked list from polygon points in the specified winding order - fn linked_list(&mut self, start: u32, end: u32, clockwise: bool) -> Option { - let mut last_i: Option = None; - if start >= end { - return None; - } + fn linked_list(&mut self, start: usize, end: usize, clockwise: bool) -> Option { + let mut last_i: Option = None; + let iter = self.data[start..end].iter().enumerate(); - let iter = self.data[start as usize..end as usize] - .chunks_exact(2) - .enumerate(); if clockwise == (signed_area(&self.data, start, end) > T::zero()) { - for (i, v) in iter { - let idx = start + i as u32 * 2; - last_i = Some(insert_node(&mut self.nodes, idx, v[0], v[1], last_i)); + for (i, &[x, y]) in iter { + let idx = start + i; + last_i = Some(insert_node(&mut self.nodes, idx as u32, x, y, last_i)); } } else { - for (i, v) in iter.rev() { - let idx = start + i as u32 * 2; - last_i = Some(insert_node(&mut self.nodes, idx, v[0], v[1], last_i)); + for (i, &[x, y]) in iter.rev() { + let idx = start + i; + last_i = Some(insert_node(&mut self.nodes, idx as u32, x, y, last_i)); } }; if let Some(li) = last_i { - let last = &node!(self.nodes, li); + let last = node!(self.nodes, li); if equals(last, node!(self.nodes, last.next_i)) { let (_, next_i) = remove_node(&mut self.nodes, li); last_i = Some(next_i); @@ -233,16 +248,20 @@ impl Earcut { } /// link every hole into the outer loop, producing a single-ring polygon without holes - fn eliminate_holes(&mut self, hole_indices: &[N], mut outer_node_i: u32) -> u32 { + fn eliminate_holes( + &mut self, + hole_indices: &[N], + mut outer_node_i: NodeIndex, + ) -> NodeIndex { self.queue.clear(); for (i, hi) in hole_indices.iter().enumerate() { - let start = (*hi).into_usize() * 2; + let start = (*hi).into_usize(); let end = if i < hole_indices.len() - 1 { - hole_indices[i + 1].into_usize() * 2 + hole_indices[i + 1].into_usize() } else { self.data.len() }; - if let Some(list_i) = self.linked_list(start as u32, end as u32, false) { + if let Some(list_i) = self.linked_list(start, end, false) { let list = &mut node_mut!(self.nodes, list_i); if list_i == list.next_i { list.steiner = true; @@ -278,7 +297,7 @@ enum Pass { #[allow(clippy::too_many_arguments)] fn earcut_linked( nodes: &mut Vec>, - ear_i: u32, + ear_i: NodeIndex, triangles: &mut Vec, min_x: T, min_y: T, @@ -295,10 +314,13 @@ fn earcut_linked( let mut stop_i = ear_i; // iterate through ears, slicing them one by one - while node!(nodes, ear_i).prev_i != node!(nodes, ear_i).next_i { + loop { let ear = node!(nodes, ear_i); - let prev_i = ear.prev_i; - let next_i = ear.next_i; + if ear.prev_i == ear.next_i { + break; + } + let pi = ear.prev_i; + let ni = ear.next_i; let is_ear = if inv_size != T::zero() { is_ear_hashed(nodes, ear_i, min_x, min_y, inv_size) @@ -306,23 +328,24 @@ fn earcut_linked( is_ear(nodes, ear_i) }; if is_ear { + let next = node!(nodes, ni); + let next_i = next.i; + let next_next_i = next.next_i; + // cut off the triangle - triangles.extend([ - N::from_usize(node!(nodes, prev_i).i as usize / 2), - N::from_usize(ear.i as usize / 2), - N::from_usize(node!(nodes, next_i).i as usize / 2), - ]); + triangles.push(N::from_usize(node!(nodes, pi).i as usize)); + triangles.push(N::from_usize(ear.i as usize)); + triangles.push(N::from_usize(next_i as usize)); remove_node(nodes, ear_i); // skipping the next vertex leads to less sliver triangles - let next_next_i = node!(nodes, next_i).next_i; (ear_i, stop_i) = (next_next_i, next_next_i); continue; } - ear_i = next_i; + ear_i = ni; // if we looped through the whole remaining polygon and can't find any more ears if ear_i == stop_i { @@ -345,7 +368,7 @@ fn earcut_linked( } /// check whether a polygon node forms a valid ear with adjacent nodes -fn is_ear(nodes: &[Node], ear_i: u32) -> bool { +fn is_ear(nodes: &[Node], ear_i: NodeIndex) -> bool { let b = node!(nodes, ear_i); let a = node!(nodes, b.prev_i); let c = node!(nodes, b.next_i); @@ -378,7 +401,13 @@ fn is_ear(nodes: &[Node], ear_i: u32) -> bool { true } -fn is_ear_hashed(nodes: &[Node], ear_i: u32, min_x: T, min_y: T, inv_size: T) -> bool { +fn is_ear_hashed( + nodes: &[Node], + ear_i: NodeIndex, + min_x: T, + min_y: T, + inv_size: T, +) -> bool { let b = node!(nodes, ear_i); let a = node!(nodes, b.prev_i); let c = node!(nodes, b.next_i); @@ -399,11 +428,9 @@ fn is_ear_hashed(nodes: &[Node], ear_i: u32, min_x: T, min_y: T, in let max_z = z_order(x1, y1, min_x, min_y, inv_size); let ear = node!(nodes, ear_i); - let mut o_p = ear.prev_z_i.map(|i| node!(nodes, i as usize)); + let mut o_p = ear.prev_z_i.map(|i| node!(nodes, i)); let mut o_n = ear.next_z_i.map(|i| node!(nodes, i)); - // NOTE: Disabled this part because it rather degrades performance. - // // look for points inside the triangle in both directions loop { let Some(p) = o_p else { break }; @@ -470,9 +497,9 @@ fn is_ear_hashed(nodes: &[Node], ear_i: u32, min_x: T, min_y: T, in /// go through all polygon nodes and cure small local self-intersections fn cure_local_intersections( nodes: &mut [Node], - mut start_i: u32, + mut start_i: NodeIndex, triangles: &mut Vec, -) -> u32 { +) -> NodeIndex { let mut p_i = start_i; loop { let p = node!(nodes, p_i); @@ -488,9 +515,9 @@ fn cure_local_intersections( && locally_inside(nodes, b, a) { triangles.extend([ - N::from_usize(a.i as usize / 2), - N::from_usize(p.i as usize / 2), - N::from_usize(b.i as usize / 2), + N::from_usize(a.i as usize), + N::from_usize(p.i as usize), + N::from_usize(b.i as usize), ]); remove_node(nodes, p_i); @@ -509,7 +536,7 @@ fn cure_local_intersections( /// try splitting polygon into two and triangulate them independently fn split_earcut( nodes: &mut Vec>, - start_i: u32, + start_i: NodeIndex, triangles: &mut Vec, min_x: T, min_y: T, @@ -517,16 +544,15 @@ fn split_earcut( ) { // look for a valid diagonal that divides the polygon into two let mut ai = start_i; + let mut a = node!(nodes, ai); loop { - let a = node!(nodes, ai); - let a_i = a.i; - let a_prev_i = a.prev_i; - let a_next_i = a.next_i; - let mut bi = (node!(nodes, a_next_i)).next_i; + let a_next = node!(nodes, a.next_i); + let a_prev = node!(nodes, a.prev_i); + let mut bi = a_next.next_i; - while bi != a_prev_i { + while bi != a.prev_i { let b = node!(nodes, bi); - if a_i != b.i && is_valid_diagonal(nodes, a, b) { + if a.i != b.i && is_valid_diagonal(nodes, a, b, a_next, a_prev) { // split the polygon in two by the diagonal let mut ci = split_polygon(nodes, ai, bi); @@ -544,100 +570,115 @@ fn split_earcut( bi = b.next_i; } - ai = node!(nodes, ai).next_i; + ai = a.next_i; if ai == start_i { return; } + a = a_next; } } /// interlink polygon nodes in z-order -fn index_curve(nodes: &mut [Node], start_i: u32, min_x: T, min_y: T, inv_size: T) { +fn index_curve( + nodes: &mut [Node], + start_i: NodeIndex, + min_x: T, + min_y: T, + inv_size: T, +) { let mut p_i = start_i; + let mut p = node_mut!(nodes, p_i); loop { - let p = node_mut!(nodes, p_i); if p.z == 0 { p.z = z_order(p.x, p.y, min_x, min_y, inv_size); } p.prev_z_i = Some(p.prev_i); p.next_z_i = Some(p.next_i); p_i = p.next_i; + p = node_mut!(nodes, p_i); if p_i == start_i { break; } } - let p_prev_z_i = node!(nodes, p_i).prev_z_i.unwrap(); + let p_prev_z_i = p.prev_z_i.take().unwrap(); node_mut!(nodes, p_prev_z_i).next_z_i = None; - node_mut!(nodes, p_i).prev_z_i = None; sort_linked(nodes, p_i); } /// Simon Tatham's linked list merge sort algorithm /// http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html -fn sort_linked(nodes: &mut [Node], list_i: u32) { +fn sort_linked(nodes: &mut [Node], list_i: NodeIndex) { let mut in_size: usize = 1; let mut list_i = Some(list_i); loop { let mut p_i = list_i; list_i = None; - let mut tail_i: Option = None; + let mut tail_i: Option = None; let mut num_merges = 0; - while let Some(pp) = p_i { + while let Some(p_i_s) = p_i { num_merges += 1; - let mut q_i = node!(nodes, pp).next_z_i; + let mut q_i = node!(nodes, p_i_s).next_z_i; let mut p_size: u32 = 1; for _ in 1..in_size { if let Some(i) = q_i { p_size += 1; q_i = node!(nodes, i).next_z_i; } else { - q_i = None; break; } } let mut q_size = in_size; - while p_size > 0 || (q_size > 0 && q_i.is_some()) { - let (e_i, e) = if p_size == 0 { - q_size -= 1; - let e_i = q_i.unwrap(); - let e = node_mut!(nodes, e_i); - q_i = e.next_z_i; - (e_i, e) - } else if q_size == 0 { - p_size -= 1; - let e_i = p_i.unwrap(); - let e = node_mut!(nodes, e_i); - p_i = e.next_z_i; - (e_i, e) - } else { - let p_i_s = p_i.unwrap(); - if let Some(q_i_s) = q_i { - if node!(nodes, p_i_s).z <= node!(nodes, q_i_s).z { + loop { + let e_i = if p_size > 0 { + let Some(p_i_s) = p_i else { break }; + if q_size > 0 { + if let Some(q_i_s) = q_i { + if node!(nodes, p_i_s).z <= node!(nodes, q_i_s).z { + p_size -= 1; + let e = node_mut!(nodes, p_i_s); + e.prev_z_i = tail_i; + p_i = e.next_z_i; + p_i_s + } else { + q_size -= 1; + let e = node_mut!(nodes, q_i_s); + e.prev_z_i = tail_i; + q_i = e.next_z_i; + q_i_s + } + } else { p_size -= 1; let e = node_mut!(nodes, p_i_s); + e.prev_z_i = tail_i; p_i = e.next_z_i; - (p_i_s, e) - } else { - q_size -= 1; - let e = node_mut!(nodes, q_i_s); - q_i = e.next_z_i; - (q_i_s, e) + p_i_s } } else { p_size -= 1; let e = node_mut!(nodes, p_i_s); + e.prev_z_i = tail_i; p_i = e.next_z_i; - (p_i_s, e) + p_i_s } + } else if q_size > 0 { + if let Some(q_i_s) = q_i { + q_size -= 1; + let e = node_mut!(nodes, q_i_s); + e.prev_z_i = tail_i; + q_i = e.next_z_i; + q_i_s + } else { + break; + } + } else { + break; }; - e.prev_z_i = tail_i; - if let Some(tail_i) = tail_i { node_mut!(nodes, tail_i).next_z_i = Some(e_i); } else { @@ -658,10 +699,10 @@ fn sort_linked(nodes: &mut [Node], list_i: u32) { } /// find the leftmost node of a polygon ring -fn get_leftmost(nodes: &[Node], start_i: u32) -> u32 { +fn get_leftmost(nodes: &[Node], start_i: NodeIndex) -> NodeIndex { let mut p_i = start_i; - let mut leftmost_i = start_i; let mut p = node!(nodes, p_i); + let mut leftmost_i = start_i; let mut leftmost = p; loop { @@ -677,9 +718,13 @@ fn get_leftmost(nodes: &[Node], start_i: u32) -> u32 { } /// check if a diagonal between two polygon nodes is valid (lies in polygon interior) -fn is_valid_diagonal(nodes: &[Node], a: &Node, b: &Node) -> bool { - let a_next = node!(nodes, a.next_i); - let a_prev = node!(nodes, a.prev_i); +fn is_valid_diagonal( + nodes: &[Node], + a: &Node, + b: &Node, + a_next: &Node, + a_prev: &Node, +) -> bool { let b_next = node!(nodes, b.next_i); let b_prev = node!(nodes, b.prev_i); // dones't intersect other edges @@ -701,10 +746,10 @@ fn intersects(p1: &Node, q1: &Node, p2: &Node, q2: &Node) let o3 = sign(area(p2, q2, p1)); let o4 = sign(area(p2, q2, q1)); (o1 != o2 && o3 != o4) // general case - || (o1 == 0 && on_segment(p1, p2, q1)) // p1, q1 and p2 are collinear and p2 lies on p1q1 - || (o2 == 0 && on_segment(p1, q2, q1)) // p1, q1 and q2 are collinear and q2 lies on p1q1 || (o3 == 0 && on_segment(p2, p1, q2)) // p2, q2 and p1 are collinear and p1 lies on p2q2 || (o4 == 0 && on_segment(p2, q1, q2)) // p2, q2 and q1 are collinear and q1 lies on p2q2 + || (o2 == 0 && on_segment(p1, q2, q1)) // p1, q1 and q2 are collinear and q2 lies on p1q1 + || (o1 == 0 && on_segment(p1, p2, q1)) // p1, q1 and p2 are collinear and p2 lies on p1q1 } /// check if a polygon diagonal intersects any polygon segments @@ -743,7 +788,11 @@ fn middle_inside(nodes: &[Node], a: &Node, b: &Node) -> bool } /// find a bridge between vertices that connects hole with an outer ring and and link it -fn eliminate_hole(nodes: &mut Vec>, hole_i: u32, outer_node_i: u32) -> u32 { +fn eliminate_hole( + nodes: &mut Vec>, + hole_i: NodeIndex, + outer_node_i: NodeIndex, +) -> NodeIndex { let Some(bridge_i) = find_hole_bridge(nodes, node!(nodes, hole_i), outer_node_i) else { return outer_node_i; }; @@ -767,33 +816,37 @@ fn locally_inside(nodes: &[Node], a: &Node, b: &Node) -> bool } } -/// dimavid Eberly's algorithm for finding a bridge between hole and outer polygon -fn find_hole_bridge(nodes: &[Node], hole: &Node, outer_node_i: u32) -> Option { +/// David Eberly's algorithm for finding a bridge between hole and outer polygon +fn find_hole_bridge( + nodes: &[Node], + hole: &Node, + outer_node_i: NodeIndex, +) -> Option { let mut p_i = outer_node_i; let mut qx = T::neg_infinity(); - let mut m_i: Option = None; + let mut m_i: Option = None; // find a segment intersected by a ray from the hole's leftmost point to the left; // segment's endpoint with lesser x will be potential connection point + let mut p = node!(nodes, p_i); loop { - let p = node!(nodes, p_i); - let p_next_i = p.next_i; - let p_next = node!(nodes, p_next_i); + let p_next = node!(nodes, p.next_i); if hole.y <= p.y && hole.y >= p_next.y && p_next.y != p.y { let x = p.x + (hole.y - p.y) * (p_next.x - p.x) / (p_next.y - p.y); if x <= hole.x && x > qx { qx = x; - m_i = Some(if p.x < p_next.x { p_i } else { p_next_i }); + m_i = Some(if p.x < p_next.x { p_i } else { p.next_i }); if x == hole.x { // hole touches outer segment; pick leftmost endpoint return m_i; } } } - p_i = p_next_i; + p_i = p.next_i; if p_i == outer_node_i { break; } + p = p_next; } let mut m_i = m_i?; @@ -827,7 +880,7 @@ fn find_hole_bridge(nodes: &[Node], hole: &Node, outer_node_i: u if locally_inside(nodes, p, hole) && (tan < tan_min || (tan == tan_min - && (p.x > m.x || p.x == m.x && sector_contains_sector(nodes, m, p)))) + && (p.x > m.x || (p.x == m.x && sector_contains_sector(nodes, m, p))))) { (m_i, m) = (p_i, p); tan_min = tan; @@ -849,33 +902,41 @@ fn sector_contains_sector(nodes: &[Node], m: &Node, p: &Node) } /// eliminate colinear or duplicate points -fn filter_points(nodes: &mut [Node], start_i: u32, end_i: Option) -> u32 { +fn filter_points( + nodes: &mut [Node], + start_i: NodeIndex, + end_i: Option, +) -> NodeIndex { let mut end_i = end_i.unwrap_or(start_i); let mut p_i = start_i; + let mut p = node!(nodes, p_i); loop { - let p = node!(nodes, p_i); let p_next = node!(nodes, p.next_i); - if !p.steiner && (equals(p, p_next) || area(node!(nodes, p.prev_i), p, p_next).is_zero()) { + if !p.steiner && (equals(p, p_next) || area(node!(nodes, p.prev_i), p, p_next) == T::zero()) + { let (prev_i, next_i) = remove_node(nodes, p_i); (p_i, end_i) = (prev_i, prev_i); if p_i == next_i { return end_i; } + p = node!(nodes, p_i); } else { p_i = p.next_i; if p_i == end_i { return end_i; } + p = p_next; }; } } /// link two polygon vertices with a bridge; if the vertices belong to the same ring, it splits polygon into two; /// if one belongs to the outer ring and another to a hole, it merges it into a single ring -fn split_polygon(nodes: &mut Vec>, a_i: u32, b_i: u32) -> u32 { - let a2_i = nodes.len() as u32; - let b2_i = a2_i + 1; +fn split_polygon(nodes: &mut Vec>, a_i: NodeIndex, b_i: NodeIndex) -> NodeIndex { + debug_assert!(!nodes.is_empty()); + let a2_i = unsafe { NodeIndex::new_unchecked(nodes.len() as u32) }; + let b2_i = unsafe { NodeIndex::new_unchecked(nodes.len() as u32 + 1) }; let a = node_mut!(nodes, a_i); let mut a2 = Node::new(a.i, a.x, a.y); @@ -899,15 +960,20 @@ fn split_polygon(nodes: &mut Vec>, a_i: u32, b_i: u32) -> u32 } /// create a node and optionally link it with previous one (in a circular doubly linked list) -fn insert_node(nodes: &mut Vec>, i: u32, x: T, y: T, last: Option) -> u32 { +fn insert_node( + nodes: &mut Vec>, + i: u32, + x: T, + y: T, + last: Option, +) -> NodeIndex { let mut p = Node::new(i, x, y); - let p_i = nodes.len() as u32; + let p_i = unsafe { NodeIndex::new_unchecked(nodes.len() as u32) }; match last { Some(last_i) => { let last = node_mut!(nodes, last_i); let last_next_i = last.next_i; - last.next_i = p_i; - p.next_i = last_next_i; + (p.next_i, last.next_i) = (last_next_i, p_i); p.prev_i = last_i; node_mut!(nodes, last_next_i).prev_i = p_i; } @@ -919,7 +985,7 @@ fn insert_node(nodes: &mut Vec>, i: u32, x: T, y: T, last: Opt p_i } -fn remove_node(nodes: &mut [Node], p_i: u32) -> (u32, u32) { +fn remove_node(nodes: &mut [Node], p_i: NodeIndex) -> (NodeIndex, NodeIndex) { let p = node!(nodes, p_i); let p_next_i = p.next_i; let p_prev_i = p.prev_i; @@ -941,41 +1007,47 @@ fn remove_node(nodes: &mut [Node], p_i: u32) -> (u32, u32) { /// Returns a percentage difference between the polygon area and its triangulation area; /// used to verify correctness of triangulation pub fn deviation( - data: impl IntoIterator, + data: impl IntoIterator, hole_indices: &[N], triangles: &[N], ) -> T { - let data = data.into_iter().collect::>(); - + let data = data.into_iter().collect::>(); let has_holes = !hole_indices.is_empty(); let outer_len = match has_holes { - true => hole_indices[0].into_usize() * 2, + true => hole_indices[0].into_usize(), false => data.len(), - } as u32; - let mut polygon_area = signed_area(&data, 0, outer_len).abs(); - if has_holes { - for i in 0..hole_indices.len() { - let start = hole_indices[i].into_usize() as u32 * 2; - let end = if i < hole_indices.len() - 1 { - hole_indices[i + 1].into_usize() * 2 - } else { - data.len() - } as u32; - polygon_area = polygon_area - signed_area(&data, start, end).abs(); + }; + let polygon_area = if data.len() < 3 { + T::zero() + } else { + let mut polygon_area = signed_area(&data, 0, outer_len).abs(); + if has_holes { + for i in 0..hole_indices.len() { + let start = hole_indices[i].into_usize(); + let end = if i < hole_indices.len() - 1 { + hole_indices[i + 1].into_usize() + } else { + data.len() + }; + if end - start >= 3 { + polygon_area = polygon_area - signed_area(&data, start, end).abs(); + } + } } - } + polygon_area + }; let mut triangles_area = T::zero(); for [a, b, c] in triangles .chunks_exact(3) .map(|idxs| [idxs[0], idxs[1], idxs[2]]) { - let a = a.into_usize() * 2; - let b = b.into_usize() * 2; - let c = c.into_usize() * 2; + let a = a.into_usize(); + let b = b.into_usize(); + let c = c.into_usize(); triangles_area = triangles_area - + ((data[a] - data[c]) * (data[b + 1] - data[a + 1]) - - (data[a] - data[b]) * (data[c + 1] - data[a + 1])) + + ((data[a][0] - data[c][0]) * (data[b][1] - data[a][1]) + - (data[a][0] - data[b][0]) * (data[c][1] - data[a][1])) .abs(); } if polygon_area == T::zero() && triangles_area == T::zero() { @@ -986,17 +1058,10 @@ pub fn deviation( } /// check if a point lies within a convex triangle -fn signed_area(data: &[T], start: u32, end: u32) -> T { - if start == end { - return T::zero(); - } - let j = if end > 2 { end - 2 } else { 0 }; - let mut bx = data[j as usize]; - let mut by = data[(j + 1) as usize]; - +fn signed_area(data: &[[T; 2]], start: usize, end: usize) -> T { + let [mut bx, mut by] = data[end - 1]; let mut sum = T::zero(); - for a in data[start as usize..end as usize].chunks_exact(2) { - let (ax, ay) = (a[0], a[1]); + for &[ax, ay] in &data[start..end] { sum = sum + (bx - ax) * (ay + by); (bx, by) = (ax, ay); } @@ -1006,17 +1071,14 @@ fn signed_area(data: &[T], start: u32, end: u32) -> T { /// z-order of a point given coords and inverse of the longer side of data bbox fn z_order(x: T, y: T, min_x: T, min_y: T, inv_size: T) -> i32 { // coords are transformed into non-negative 15-bit integer range - let mut x = ((x - min_x) * inv_size).to_i32().unwrap(); - let mut y = ((y - min_y) * inv_size).to_i32().unwrap(); - x = (x | (x << 8)) & 0x00FF00FF; - x = (x | (x << 4)) & 0x0F0F0F0F; - x = (x | (x << 2)) & 0x33333333; - x = (x | (x << 1)) & 0x55555555; - y = (y | (y << 8)) & 0x00FF00FF; - y = (y | (y << 4)) & 0x0F0F0F0F; - y = (y | (y << 2)) & 0x33333333; - y = (y | (y << 1)) & 0x55555555; - x | (y << 1) + let x = ((x - min_x) * inv_size).to_u32().unwrap(); + let y = ((y - min_y) * inv_size).to_u32().unwrap(); + let mut xy = (x as i64) << 32 | y as i64; + xy = (xy | (xy << 8)) & 0x00FF00FF00FF00FF; + xy = (xy | (xy << 4)) & 0x0F0F0F0F0F0F0F0F; + xy = (xy | (xy << 2)) & 0x3333333333333333; + xy = (xy | (xy << 1)) & 0x5555555555555555; + (xy >> 32 | xy << 1) as i32 } #[allow(clippy::too_many_arguments)] @@ -1041,6 +1103,6 @@ fn on_segment(p: &Node, q: &Node, r: &Node) -> bool { q.x <= p.x.max(r.x) && q.x >= p.x.min(r.x) && q.y <= p.y.max(r.y) && q.y >= p.y.max(r.y) } -fn sign(v: T) -> i8 { - (v > T::zero()) as i8 - (v < T::zero()) as i8 +fn sign(v: T) -> i32 { + (v > T::zero()) as i32 - (v < T::zero()) as i32 } diff --git a/tests/fixture.rs b/tests/fixture.rs index 6453aea..a8cf35b 100644 --- a/tests/fixture.rs +++ b/tests/fixture.rs @@ -10,7 +10,7 @@ fn test_fixture(name: &str, num_triangles: usize, expected_deviation: f64) { // prepare input let num_holes = expected.len(); - let data: Vec = expected.clone().into_iter().flatten().flatten().collect(); + let data: Vec<[f64; 2]> = expected.clone().into_iter().flatten().collect(); let hole_indices: Vec<_> = expected .into_iter() .map(|x| x.len() as u32) diff --git a/tests/simple.rs b/tests/simple.rs index 91f4eb6..50cbbf8 100644 --- a/tests/simple.rs +++ b/tests/simple.rs @@ -6,10 +6,10 @@ fn test_empty() { let data: [[f64; 2]; 0] = []; let hole_indices: &[u32] = &[]; let mut triangles = vec![]; - earcut.earcut(data.iter().flatten().copied(), hole_indices, &mut triangles); + earcut.earcut(data.iter().copied(), hole_indices, &mut triangles); assert_eq!(triangles.len(), 0); assert_eq!( - deviation(data.iter().flatten().copied(), hole_indices, &triangles), + deviation(data.iter().copied(), hole_indices, &triangles), 0.0 ); } @@ -20,10 +20,10 @@ fn test_invalid_point() { let data = [[100.0, 200.0]]; let hole_indices: &[u32] = &[]; let mut triangles = vec![]; - earcut.earcut(data.iter().flatten().copied(), hole_indices, &mut triangles); + earcut.earcut(data.iter().copied(), hole_indices, &mut triangles); assert_eq!(triangles.len(), 0); assert_eq!( - deviation(data.iter().flatten().copied(), hole_indices, &triangles), + deviation(data.iter().copied(), hole_indices, &triangles), 0.0 ); } @@ -34,10 +34,10 @@ fn test_invalid_line() { let data = [[0.0, 0.0], [100.0, 200.0]]; let hole_indices: &[u32] = &[]; let mut triangles = vec![]; - earcut.earcut(data.iter().flatten().copied(), hole_indices, &mut triangles); + earcut.earcut(data.iter().copied(), hole_indices, &mut triangles); assert_eq!(triangles.len(), 0); assert_eq!( - deviation(data.iter().flatten().copied(), hole_indices, &triangles), + deviation(data.iter().copied(), hole_indices, &triangles), 0.0 ); } @@ -48,10 +48,10 @@ fn test_invalid_empty_hole() { let data = [[0.0, 0.0], [100.0, 0.0], [100.0, 100.0]]; let hole_indices: &[u32] = &[3]; let mut triangles = vec![]; - earcut.earcut(data.iter().flatten().copied(), hole_indices, &mut triangles); + earcut.earcut(data.iter().copied(), hole_indices, &mut triangles); assert_eq!(triangles.len(), 3); assert_eq!( - deviation(data.iter().flatten().copied(), hole_indices, &triangles), + deviation(data.iter().copied(), hole_indices, &triangles), 0.0 ); } @@ -62,10 +62,10 @@ fn test_steiner_point_hole() { let data = [[0.0, 0.0], [100.0, 0.0], [100.0, 100.0], [50.0, 30.0]]; let hole_indices: &[u32] = &[3]; let mut triangles = vec![]; - earcut.earcut(data.iter().flatten().copied(), hole_indices, &mut triangles); + earcut.earcut(data.iter().copied(), hole_indices, &mut triangles); assert_eq!(triangles.len(), 3 * 3); assert_eq!( - deviation(data.iter().flatten().copied(), hole_indices, &triangles), + deviation(data.iter().copied(), hole_indices, &triangles), 0.0 ); } @@ -76,10 +76,10 @@ fn test_steiner_line_hole() { let data = [[0., 0.], [100., 0.], [100., 100.], [50., 30.], [60., 30.]]; let hole_indices: &[u32] = &[3]; let mut triangles = vec![]; - earcut.earcut(data.iter().flatten().copied(), hole_indices, &mut triangles); + earcut.earcut(data.iter().copied(), hole_indices, &mut triangles); assert_eq!(triangles.len(), 5 * 3); assert_eq!( - deviation(data.iter().flatten().copied(), hole_indices, &triangles), + deviation(data.iter().copied(), hole_indices, &triangles), 0.0 ); } @@ -90,10 +90,10 @@ fn test_square() { let data = [[0.0, 0.0], [100.0, 0.0], [100.0, 100.0], [0.0, 100.0]]; let hole_indices: &[u32] = &[]; let mut triangles = vec![]; - earcut.earcut(data.iter().flatten().copied(), hole_indices, &mut triangles); + earcut.earcut(data.iter().copied(), hole_indices, &mut triangles); assert_eq!(triangles, vec![2, 3, 0, 0, 1, 2]); assert_eq!( - deviation(data.iter().flatten().copied(), hole_indices, &triangles), + deviation(data.iter().copied(), hole_indices, &triangles), 0.0 ); } @@ -104,10 +104,10 @@ fn test_square_u16() { let data = [[0.0, 0.0], [100.0, 0.0], [100.0, 100.0], [0.0, 100.0]]; let hole_indices: &[u16] = &[]; let mut triangles = vec![]; - earcut.earcut(data.iter().flatten().copied(), hole_indices, &mut triangles); + earcut.earcut(data.iter().copied(), hole_indices, &mut triangles); assert_eq!(triangles, vec![2, 3, 0, 0, 1, 2]); assert_eq!( - deviation(data.iter().flatten().copied(), hole_indices, &triangles), + deviation(data.iter().copied(), hole_indices, &triangles), 0.0 ); } @@ -118,10 +118,10 @@ fn test_square_usize() { let data = [[0.0, 0.0], [100.0, 0.0], [100.0, 100.0], [0.0, 100.0]]; let hole_indices: &[usize] = &[]; let mut triangles = vec![]; - earcut.earcut(data.iter().flatten().copied(), hole_indices, &mut triangles); + earcut.earcut(data.iter().copied(), hole_indices, &mut triangles); assert_eq!(triangles, vec![2, 3, 0, 0, 1, 2]); assert_eq!( - deviation(data.iter().flatten().copied(), hole_indices, &triangles), + deviation(data.iter().copied(), hole_indices, &triangles), 0.0 ); } @@ -139,17 +139,13 @@ fn test_map_3d_to_2d() { let hole_indices: &[usize] = &[]; let mut triangles = vec![]; earcut.earcut( - data.iter().flat_map(|v| [v[0], v[1]]), + data.iter().map(|v| [v[0], v[1]]), hole_indices, &mut triangles, ); assert_eq!(triangles, vec![2, 3, 0, 0, 1, 2]); assert_eq!( - deviation( - data.iter().flat_map(|v| [v[0], v[1]]), - hole_indices, - &triangles - ), + deviation(data.iter().map(|v| [v[0], v[1]]), hole_indices, &triangles), 0.0 ); } @@ -169,13 +165,13 @@ fn test_square_with_square_hole() { ]; let hole_indices: &[u32] = &[4]; let mut triangles = vec![]; - earcut.earcut(data.iter().flatten().copied(), hole_indices, &mut triangles); + earcut.earcut(data.iter().copied(), hole_indices, &mut triangles); assert_eq!( triangles, vec![0, 4, 7, 5, 4, 0, 3, 0, 7, 5, 0, 1, 2, 3, 7, 6, 5, 1, 2, 7, 6, 6, 1, 2] ); assert_eq!( - deviation(data.iter().flatten().copied(), hole_indices, &triangles), + deviation(data.iter().copied(), hole_indices, &triangles), 0.0 ); }