use hisab::Vec2;
#[must_use]
pub fn triangulate(vertices: &[Vec2]) -> Vec<(usize, usize, usize)> {
let n = vertices.len();
if n < 3 {
return Vec::new();
}
if n == 3 {
return vec![(0, 1, 2)];
}
let mut indices: Vec<usize> = (0..n).collect();
if signed_area(vertices) < 0.0 {
indices.reverse();
}
let mut triangles = Vec::with_capacity(n - 2);
ear_clip(vertices, &mut indices, &mut triangles);
triangles
}
#[must_use]
pub fn triangulate_points(vertices: &[Vec2]) -> Vec<[Vec2; 3]> {
triangulate(vertices)
.into_iter()
.map(|(a, b, c)| [vertices[a], vertices[b], vertices[c]])
.collect()
}
#[must_use]
fn signed_area(vertices: &[Vec2]) -> f32 {
let n = vertices.len();
let mut area = 0.0f32;
for i in 0..n {
let j = (i + 1) % n;
area += vertices[i].perp_dot(vertices[j]);
}
area * 0.5
}
fn ear_clip(
vertices: &[Vec2],
indices: &mut Vec<usize>,
triangles: &mut Vec<(usize, usize, usize)>,
) {
let mut remaining = indices.len();
let mut max_iter = remaining * remaining;
let mut i = 0;
while remaining > 3 && max_iter > 0 {
max_iter -= 1;
let prev = indices[wrap_idx(i as isize - 1, remaining)];
let curr = indices[i % remaining];
let next = indices[(i + 1) % remaining];
if is_ear(vertices, indices, remaining, prev, curr, next) {
triangles.push((prev, curr, next));
indices.remove(i % remaining);
remaining -= 1;
if i >= remaining {
i = 0;
}
} else {
i = (i + 1) % remaining;
}
}
if remaining == 3 {
triangles.push((indices[0], indices[1], indices[2]));
}
}
fn is_ear(
vertices: &[Vec2],
indices: &[usize],
remaining: usize,
prev: usize,
curr: usize,
next: usize,
) -> bool {
let a = vertices[prev];
let b = vertices[curr];
let c = vertices[next];
let cross = (b - a).perp_dot(c - b);
if cross <= 0.0 {
return false;
}
for &idx in &indices[..remaining] {
if idx == prev || idx == curr || idx == next {
continue;
}
if point_in_triangle(vertices[idx], a, b, c) {
return false;
}
}
true
}
#[inline]
fn point_in_triangle(p: Vec2, a: Vec2, b: Vec2, c: Vec2) -> bool {
let v0 = c - a;
let v1 = b - a;
let v2 = p - a;
let dot00 = v0.dot(v0);
let dot01 = v0.dot(v1);
let dot02 = v0.dot(v2);
let dot11 = v1.dot(v1);
let dot12 = v1.dot(v2);
let denom = dot00 * dot11 - dot01 * dot01;
if denom.abs() < f32::EPSILON {
return false; }
let inv_denom = 1.0 / denom;
let u = (dot11 * dot02 - dot01 * dot12) * inv_denom;
let v = (dot00 * dot12 - dot01 * dot02) * inv_denom;
u >= 0.0 && v >= 0.0 && (u + v) <= 1.0
}
#[inline]
fn wrap_idx(i: isize, len: usize) -> usize {
((i % len as isize) + len as isize) as usize % len
}
#[must_use]
pub fn merge_convex(vertices: &[Vec2], triangles: &[(usize, usize, usize)]) -> Vec<Vec<usize>> {
if triangles.is_empty() {
return Vec::new();
}
let mut polys: Vec<Option<Vec<usize>>> = triangles
.iter()
.map(|&(a, b, c)| Some(vec![a, b, c]))
.collect();
let mut edge_map: std::collections::HashMap<(usize, usize), Vec<usize>> =
std::collections::HashMap::new();
for (pi, poly) in polys.iter().enumerate() {
if let Some(verts) = poly {
for i in 0..verts.len() {
let j = (i + 1) % verts.len();
let key = edge_key(verts[i], verts[j]);
edge_map.entry(key).or_default().push(pi);
}
}
}
let mut merged = true;
while merged {
merged = false;
let keys: Vec<(usize, usize)> = edge_map.keys().copied().collect();
for key in keys {
let owners = match edge_map.get(&key) {
Some(o) if o.len() == 2 => [o[0], o[1]],
_ => continue,
};
let (pa, pb) = (owners[0], owners[1]);
let (poly_a, poly_b) = match (polys[pa].as_ref(), polys[pb].as_ref()) {
(Some(a), Some(b)) => (a.clone(), b.clone()),
_ => continue,
};
if let Some(merged_poly) = try_merge(vertices, &poly_a, &poly_b, key) {
remove_poly_edges(&mut edge_map, &poly_a, pa);
remove_poly_edges(&mut edge_map, &poly_b, pb);
let new_idx = pa;
polys[pa] = Some(merged_poly.clone());
polys[pb] = None;
for i in 0..merged_poly.len() {
let j = (i + 1) % merged_poly.len();
let ek = edge_key(merged_poly[i], merged_poly[j]);
edge_map.entry(ek).or_default().push(new_idx);
}
merged = true;
break; }
}
}
polys.into_iter().flatten().collect()
}
fn try_merge(
vertices: &[Vec2],
poly_a: &[usize],
poly_b: &[usize],
shared_edge: (usize, usize),
) -> Option<Vec<usize>> {
let (ea, eb) = shared_edge;
let a_len = poly_a.len();
let edge_in_a = (0..a_len).find(|&i| {
let j = (i + 1) % a_len;
edge_key(poly_a[i], poly_a[j]) == (ea, eb)
})?;
let a_end = (edge_in_a + 1) % a_len;
let b_len = poly_b.len();
let edge_in_b = (0..b_len).find(|&i| {
let j = (i + 1) % b_len;
edge_key(poly_b[i], poly_b[j]) == (ea, eb)
})?;
let mut result = Vec::with_capacity(a_len + b_len - 2);
{
let mut ai = (a_end + 1) % a_len;
loop {
result.push(poly_a[ai]);
if ai == edge_in_a {
break;
}
ai = (ai + 1) % a_len;
}
}
{
let b_end = (edge_in_b + 1) % b_len;
let mut bi = (b_end + 1) % b_len;
while bi != edge_in_b {
result.push(poly_b[bi]);
bi = (bi + 1) % b_len;
}
}
result.push(poly_a[a_end]);
if result.len() < 3 {
return None;
}
if is_convex(vertices, &result) {
Some(result)
} else {
None
}
}
fn is_convex(vertices: &[Vec2], indices: &[usize]) -> bool {
let n = indices.len();
if n < 3 {
return false;
}
for i in 0..n {
let a = vertices[indices[i]];
let b = vertices[indices[(i + 1) % n]];
let c = vertices[indices[(i + 2) % n]];
if (b - a).perp_dot(c - b) < -f32::EPSILON {
return false;
}
}
true
}
#[inline]
fn edge_key(a: usize, b: usize) -> (usize, usize) {
if a <= b { (a, b) } else { (b, a) }
}
fn remove_poly_edges(
edge_map: &mut std::collections::HashMap<(usize, usize), Vec<usize>>,
poly: &[usize],
poly_idx: usize,
) {
for i in 0..poly.len() {
let j = (i + 1) % poly.len();
let key = edge_key(poly[i], poly[j]);
if let Some(owners) = edge_map.get_mut(&key) {
owners.retain(|&x| x != poly_idx);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn triangle_passthrough() {
let verts = vec![Vec2::ZERO, Vec2::new(1.0, 0.0), Vec2::new(0.5, 1.0)];
let tris = triangulate(&verts);
assert_eq!(tris.len(), 1);
}
#[test]
fn square_two_triangles() {
let verts = vec![
Vec2::ZERO,
Vec2::new(1.0, 0.0),
Vec2::new(1.0, 1.0),
Vec2::new(0.0, 1.0),
];
let tris = triangulate(&verts);
assert_eq!(tris.len(), 2);
}
#[test]
fn pentagon() {
let verts: Vec<Vec2> = (0..5)
.map(|i| {
let angle = std::f32::consts::TAU * i as f32 / 5.0;
Vec2::new(angle.cos(), angle.sin())
})
.collect();
let tris = triangulate(&verts);
assert_eq!(tris.len(), 3); }
#[test]
fn concave_l_shape() {
let verts = vec![
Vec2::new(0.0, 0.0),
Vec2::new(2.0, 0.0),
Vec2::new(2.0, 1.0),
Vec2::new(1.0, 1.0),
Vec2::new(1.0, 2.0),
Vec2::new(0.0, 2.0),
];
let tris = triangulate(&verts);
assert_eq!(tris.len(), 4);
for (a, b, c) in &tris {
let va = verts[*a];
let vb = verts[*b];
let vc = verts[*c];
let area = (vb - va).perp_dot(vc - va);
assert!(area > 0.0, "degenerate triangle: ({a},{b},{c})");
}
}
#[test]
fn cw_polygon_handled() {
let verts = vec![
Vec2::ZERO,
Vec2::new(0.0, 1.0),
Vec2::new(1.0, 1.0),
Vec2::new(1.0, 0.0),
];
let tris = triangulate(&verts);
assert_eq!(tris.len(), 2);
}
#[test]
fn degenerate_too_few() {
assert!(triangulate(&[]).is_empty());
assert!(triangulate(&[Vec2::ZERO]).is_empty());
assert!(triangulate(&[Vec2::ZERO, Vec2::ONE]).is_empty());
}
#[test]
fn triangulate_points_returns_vertices() {
let verts = vec![
Vec2::ZERO,
Vec2::new(1.0, 0.0),
Vec2::new(1.0, 1.0),
Vec2::new(0.0, 1.0),
];
let tris = triangulate_points(&verts);
assert_eq!(tris.len(), 2);
for tri in &tris {
for v in tri {
assert!(verts.contains(v));
}
}
}
#[test]
fn all_indices_valid() {
let verts = vec![
Vec2::new(0.0, 0.0),
Vec2::new(3.0, 0.0),
Vec2::new(3.0, 1.0),
Vec2::new(2.0, 1.0),
Vec2::new(2.0, 2.0),
Vec2::new(0.0, 2.0),
];
let tris = triangulate(&verts);
for (a, b, c) in &tris {
assert!(*a < verts.len());
assert!(*b < verts.len());
assert!(*c < verts.len());
}
}
#[test]
fn large_convex_polygon() {
let verts: Vec<Vec2> = (0..20)
.map(|i| {
let angle = std::f32::consts::TAU * i as f32 / 20.0;
Vec2::new(angle.cos() * 10.0, angle.sin() * 10.0)
})
.collect();
let tris = triangulate(&verts);
assert_eq!(tris.len(), 18); }
#[test]
fn merge_square_triangles() {
let verts = vec![
Vec2::ZERO,
Vec2::new(1.0, 0.0),
Vec2::new(1.0, 1.0),
Vec2::new(0.0, 1.0),
];
let tris = triangulate(&verts);
let merged = merge_convex(&verts, &tris);
assert_eq!(merged.len(), 1);
assert_eq!(merged[0].len(), 4);
}
#[test]
fn merge_concave_stays_split() {
let verts = vec![
Vec2::new(0.0, 0.0),
Vec2::new(2.0, 0.0),
Vec2::new(2.0, 1.0),
Vec2::new(1.0, 1.0),
Vec2::new(1.0, 2.0),
Vec2::new(0.0, 2.0),
];
let tris = triangulate(&verts);
let merged = merge_convex(&verts, &tris);
assert!(merged.len() < tris.len());
assert!(!merged.is_empty());
for poly in &merged {
assert!(is_convex(&verts, poly), "non-convex merged polygon");
}
}
#[test]
fn merge_empty() {
let merged = merge_convex(&[], &[]);
assert!(merged.is_empty());
}
#[test]
fn merge_single_triangle() {
let verts = vec![Vec2::ZERO, Vec2::new(1.0, 0.0), Vec2::new(0.5, 1.0)];
let tris = vec![(0, 1, 2)];
let merged = merge_convex(&verts, &tris);
assert_eq!(merged.len(), 1);
assert_eq!(merged[0].len(), 3);
}
#[test]
fn merge_convex_polygon_fully_merges() {
let verts: Vec<Vec2> = (0..6)
.map(|i| {
let angle = std::f32::consts::TAU * i as f32 / 6.0;
Vec2::new(angle.cos(), angle.sin())
})
.collect();
let tris = triangulate(&verts);
assert_eq!(tris.len(), 4);
let merged = merge_convex(&verts, &tris);
assert!(merged.len() <= 2);
}
}