use oxiui_core::paint::FillRule;
pub fn triangulate(contours: &[Vec<[f32; 2]>], fill_rule: FillRule) -> Vec<[[f32; 2]; 3]> {
let prepared: Vec<Vec<[f32; 2]>> = contours.iter().filter_map(|c| prepare_contour(c)).collect();
if prepared.is_empty() {
return Vec::new();
}
if prepared.len() == 1 {
return ear_clip_simple(&prepared[0]);
}
let n = prepared.len();
let areas: Vec<f64> = prepared.iter().map(|c| signed_area(c)).collect();
let parents: Vec<Option<usize>> = compute_parents(&prepared);
let depths: Vec<usize> = compute_depths(&parents, n);
let solid_flags: Vec<bool> = (0..n)
.map(|i| is_solid(i, &depths, &areas, fill_rule, &parents))
.collect();
let regions = collect_regions(n, &solid_flags, &parents, &depths);
let mut out: Vec<[[f32; 2]; 3]> = Vec::new();
for (outer_idx, hole_indices) in ®ions {
let outer = &prepared[*outer_idx];
let holes: Vec<&Vec<[f32; 2]>> = hole_indices.iter().map(|&hi| &prepared[hi]).collect();
let poly = bridge_holes(outer, &holes);
let tris = ear_clip_simple(&poly);
out.extend_from_slice(&tris);
}
out
}
fn prepare_contour(pts: &[[f32; 2]]) -> Option<Vec<[f32; 2]>> {
if pts.is_empty() {
return None;
}
let mut v: Vec<[f32; 2]> = pts.to_vec();
if v.len() >= 2 {
let first = v[0];
let last = *v.last()?;
let dx = first[0] - last[0];
let dy = first[1] - last[1];
if dx * dx + dy * dy < 1e-8 {
v.pop();
}
}
if v.len() < 3 {
return None;
}
let area = signed_area(&v);
if area.abs() < 1e-4 {
return None;
}
Some(v)
}
fn signed_area(pts: &[[f32; 2]]) -> f64 {
let n = pts.len();
if n < 3 {
return 0.0;
}
let mut sum = 0.0f64;
for i in 0..n {
let j = (i + 1) % n;
sum += (pts[i][0] as f64) * (pts[j][1] as f64);
sum -= (pts[j][0] as f64) * (pts[i][1] as f64);
}
sum * 0.5
}
fn compute_parents(contours: &[Vec<[f32; 2]>]) -> Vec<Option<usize>> {
let n = contours.len();
let areas: Vec<f64> = contours.iter().map(|c| signed_area(c).abs()).collect();
let mut parents: Vec<Option<usize>> = vec![None; n];
for i in 0..n {
let probe = contours[i][0];
let mut best_area = f64::MAX;
let mut best_idx: Option<usize> = None;
for j in 0..n {
if j == i {
continue;
}
if point_in_polygon(probe, &contours[j]) {
let a = areas[j];
if a < best_area {
best_area = a;
best_idx = Some(j);
}
}
}
parents[i] = best_idx;
}
parents
}
fn compute_depths(parents: &[Option<usize>], n: usize) -> Vec<usize> {
let mut depths = vec![usize::MAX; n];
for i in 0..n {
if depths[i] == usize::MAX {
fill_depth(i, parents, &mut depths);
}
}
depths
}
fn fill_depth(start: usize, parents: &[Option<usize>], depths: &mut [usize]) {
let mut chain: Vec<usize> = Vec::new();
let mut cur = start;
loop {
if depths[cur] != usize::MAX {
let base = depths[cur];
for (k, &idx) in chain.iter().rev().enumerate() {
depths[idx] = base + k + 1;
}
return;
}
chain.push(cur);
match parents[cur] {
None => {
let len = chain.len();
for (k, &idx) in chain.iter().enumerate() {
depths[idx] = len - 1 - k;
}
return;
}
Some(p) => {
cur = p;
}
}
}
}
fn is_solid(
i: usize,
depths: &[usize],
areas: &[f64],
fill_rule: FillRule,
parents: &[Option<usize>],
) -> bool {
let depth = depths[i];
match fill_rule {
FillRule::EvenOdd => {
depth.is_multiple_of(2)
}
FillRule::NonZero => {
let winding = winding_sum(i, areas, parents);
winding != 0
}
}
}
fn winding_sum(i: usize, areas: &[f64], parents: &[Option<usize>]) -> i32 {
let mut sum = 0i32;
let mut cur = i;
loop {
let sign = if areas[cur] >= 0.0 { 1i32 } else { -1i32 };
sum += sign;
match parents[cur] {
None => break,
Some(p) => cur = p,
}
}
sum
}
fn collect_regions(
n: usize,
solid_flags: &[bool],
parents: &[Option<usize>],
depths: &[usize],
) -> Vec<(usize, Vec<usize>)> {
let mut regions: Vec<(usize, Vec<usize>)> = Vec::new();
for i in 0..n {
if !solid_flags[i] {
continue;
}
let is_outer = match parents[i] {
None => true,
Some(p) => !solid_flags[p],
};
if !is_outer {
continue;
}
let my_depth = depths[i];
let holes: Vec<usize> = (0..n)
.filter(|&j| !solid_flags[j] && parents[j] == Some(i) && depths[j] == my_depth + 1)
.collect();
regions.push((i, holes));
}
if regions.is_empty() {
for i in 0..n {
regions.push((i, Vec::new()));
}
}
regions
}
fn bridge_holes(outer: &[[f32; 2]], holes: &[&Vec<[f32; 2]>]) -> Vec<[f32; 2]> {
if holes.is_empty() {
return outer.to_vec();
}
let mut poly = ensure_ccw(outer.to_vec());
for &hole in holes {
let hole_cw = ensure_cw(hole.to_vec());
poly = splice_one_hole(&poly, &hole_cw);
}
poly
}
fn splice_one_hole(outer: &[[f32; 2]], hole: &[[f32; 2]]) -> Vec<[f32; 2]> {
let m_idx = max_x_vertex(hole);
let m = hole[m_idx];
let bridge_result = find_bridge_vertex(m, outer);
let v_idx = match bridge_result {
Some(v) => v,
None => {
closest_vertex(m, outer)
}
};
let on = outer.len();
let hn = hole.len();
let mut result = Vec::with_capacity(on + hn + 2);
for &p in &outer[..=v_idx] {
result.push(p);
}
for k in 0..=hn {
result.push(hole[(m_idx + k) % hn]);
}
result.push(outer[v_idx]);
for &p in &outer[v_idx + 1..] {
result.push(p);
}
result
}
fn find_bridge_vertex(m: [f32; 2], outer: &[[f32; 2]]) -> Option<usize> {
let n = outer.len();
let mx = m[0];
let my = m[1];
let mut nearest_x = f32::MAX;
let mut nearest_edge_a: usize = n;
for i in 0..n {
let j = (i + 1) % n;
let ax = outer[i][0];
let ay = outer[i][1];
let bx = outer[j][0];
let by = outer[j][1];
let (min_y, max_y) = if ay <= by { (ay, by) } else { (by, ay) };
if my < min_y || my >= max_y {
continue; }
let t = (my - ay) / (by - ay);
let ix = ax + t * (bx - ax);
if ix > mx && ix < nearest_x {
nearest_x = ix;
nearest_edge_a = i;
}
}
if nearest_edge_a == n {
return None; }
let ea = nearest_edge_a;
let eb = (ea + 1) % n;
let va_x = outer[ea][0];
let vb_x = outer[eb][0];
let best_edge_v = if va_x >= vb_x { ea } else { eb };
let hit = [nearest_x, my];
let v_pt = outer[best_edge_v];
let mut best_v = best_edge_v;
let mut best_x = v_pt[0];
for (k, &p) in outer.iter().enumerate() {
if k == ea || k == eb {
continue;
}
if p[0] > mx && p[0] < best_x {
if point_in_triangle(p, m, hit, v_pt) {
best_v = k;
best_x = p[0];
}
}
}
Some(best_v)
}
fn max_x_vertex(pts: &[[f32; 2]]) -> usize {
let mut best = 0;
let mut best_x = pts[0][0];
for (i, &p) in pts.iter().enumerate().skip(1) {
if p[0] > best_x {
best_x = p[0];
best = i;
}
}
best
}
fn closest_vertex(m: [f32; 2], pts: &[[f32; 2]]) -> usize {
let mut best = 0;
let mut best_d = f32::MAX;
for (i, &p) in pts.iter().enumerate() {
let dx = p[0] - m[0];
let dy = p[1] - m[1];
let d = dx * dx + dy * dy;
if d < best_d {
best_d = d;
best = i;
}
}
best
}
fn ensure_ccw(mut pts: Vec<[f32; 2]>) -> Vec<[f32; 2]> {
if signed_area(&pts) < 0.0 {
pts.reverse();
}
pts
}
fn ensure_cw(mut pts: Vec<[f32; 2]>) -> Vec<[f32; 2]> {
if signed_area(&pts) > 0.0 {
pts.reverse();
}
pts
}
pub fn ear_clip_simple(pts: &[[f32; 2]]) -> Vec<[[f32; 2]; 3]> {
if pts.len() < 3 {
return Vec::new();
}
let verts = ensure_ccw(pts.to_vec());
let n = verts.len();
if n == 3 {
return vec![[verts[0], verts[1], verts[2]]];
}
let mut prev: Vec<usize> = (0..n).map(|i| if i == 0 { n - 1 } else { i - 1 }).collect();
let mut next: Vec<usize> = (0..n).map(|i| if i == n - 1 { 0 } else { i + 1 }).collect();
let mut alive: Vec<bool> = vec![true; n];
let mut remaining = n;
let mut is_reflex: Vec<bool> = (0..n)
.map(|i| {
let p = prev[i];
let nx = next[i];
cross_2d(verts[p], verts[i], verts[nx]) <= 0.0
})
.collect();
let mut out: Vec<[[f32; 2]; 3]> = Vec::with_capacity(n - 2);
let cap = n * 2;
let mut iters = 0usize;
let mut cur = 0usize;
while remaining > 3 {
iters += 1;
if iters > cap {
fan_remaining(&verts, &alive, &mut out);
return out;
}
if !alive[cur] {
cur = next_alive(cur, &alive, &next, n);
}
let p = prev[cur];
let nx = next[cur];
if is_ear(cur, p, nx, &verts, &is_reflex) {
out.push([verts[p], verts[cur], verts[nx]]);
alive[cur] = false;
next[p] = nx;
prev[nx] = p;
remaining -= 1;
let pp = prev[p];
let nnx = next[nx];
is_reflex[p] = cross_2d(verts[pp], verts[p], verts[nx]) <= 0.0;
is_reflex[nx] = cross_2d(verts[p], verts[nx], verts[nnx]) <= 0.0;
cur = nx;
iters = 0; } else {
cur = next_alive(cur, &alive, &next, n);
}
}
let (a, b, c) = last_three(&alive, &next, n);
out.push([verts[a], verts[b], verts[c]]);
out
}
fn is_ear(cur: usize, p: usize, nx: usize, verts: &[[f32; 2]], is_reflex: &[bool]) -> bool {
let pv = verts[p];
let cv = verts[cur];
let nv = verts[nx];
if cross_2d(pv, cv, nv) <= 0.0 {
return false;
}
for (k, &reflex) in is_reflex.iter().enumerate() {
if !reflex {
continue;
}
if k == p || k == cur || k == nx {
continue;
}
if point_in_triangle_strict(verts[k], pv, cv, nv) {
return false;
}
}
true
}
fn next_alive(start: usize, alive: &[bool], next: &[usize], n: usize) -> usize {
let mut cur = next[start];
let mut limit = n;
while !alive[cur] && limit > 0 {
cur = next[cur];
limit -= 1;
}
cur
}
fn last_three(alive: &[bool], next: &[usize], n: usize) -> (usize, usize, usize) {
let a = alive.iter().position(|&b| b).unwrap_or(0);
let b = next[a];
let c = next[b];
let _ = n;
(a, b, c)
}
fn fan_remaining(verts: &[[f32; 2]], alive: &[bool], out: &mut Vec<[[f32; 2]; 3]>) {
let alive_verts: Vec<[f32; 2]> = alive
.iter()
.enumerate()
.filter_map(|(i, &a)| if a { Some(verts[i]) } else { None })
.collect();
if alive_verts.len() < 3 {
return;
}
let p0 = alive_verts[0];
for i in 1..alive_verts.len() - 1 {
out.push([p0, alive_verts[i], alive_verts[i + 1]]);
}
}
#[inline]
fn cross_2d(a: [f32; 2], b: [f32; 2], c: [f32; 2]) -> f32 {
(b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
}
fn point_in_triangle_strict(p: [f32; 2], a: [f32; 2], b: [f32; 2], c: [f32; 2]) -> bool {
const EPS: f32 = 1e-5;
let d1 = cross_2d(a, b, p);
let d2 = cross_2d(b, c, p);
let d3 = cross_2d(c, a, p);
let all_non_neg = d1 >= -EPS && d2 >= -EPS && d3 >= -EPS;
let all_non_pos = d1 <= EPS && d2 <= EPS && d3 <= EPS;
all_non_neg || all_non_pos
}
fn point_in_triangle(p: [f32; 2], a: [f32; 2], b: [f32; 2], c: [f32; 2]) -> bool {
let d1 = cross_2d(a, b, p);
let d2 = cross_2d(b, c, p);
let d3 = cross_2d(c, a, p);
let has_neg = d1 < 0.0 || d2 < 0.0 || d3 < 0.0;
let has_pos = d1 > 0.0 || d2 > 0.0 || d3 > 0.0;
!(has_neg && has_pos)
}
fn point_in_polygon(pt: [f32; 2], polygon: &[[f32; 2]]) -> bool {
let n = polygon.len();
let mut inside = false;
let (px, py) = (pt[0], pt[1]);
let mut j = n - 1;
for i in 0..n {
let (xi, yi) = (polygon[i][0], polygon[i][1]);
let (xj, yj) = (polygon[j][0], polygon[j][1]);
let crosses_y = (yi > py) != (yj > py);
if crosses_y {
let x_intersect = (xj - xi) * (py - yi) / (yj - yi) + xi;
if px < x_intersect {
inside = !inside;
}
}
j = i;
}
inside
}
#[cfg(test)]
mod tests {
use super::*;
fn tri_area(t: &[[f32; 2]; 3]) -> f64 {
let a = t[0];
let b = t[1];
let c = t[2];
let area = (b[0] as f64 - a[0] as f64) * (c[1] as f64 - a[1] as f64)
- (c[0] as f64 - a[0] as f64) * (b[1] as f64 - a[1] as f64);
(area * 0.5).abs()
}
fn total_area(tris: &[[[f32; 2]; 3]]) -> f64 {
tris.iter().map(tri_area).sum()
}
#[test]
fn convex_square_area_correct() {
let sq = vec![[0.0f32, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
let tris = triangulate(&[sq], FillRule::NonZero);
assert_eq!(tris.len(), 2, "square → 2 triangles");
let area = total_area(&tris);
assert!((area - 1.0).abs() < 1e-3, "area should be ~1.0, got {area}");
}
#[test]
fn concave_notch_area_correct() {
let pts = vec![
[0.0f32, 0.0],
[10.0, 0.0],
[10.0, 10.0],
[5.0, 5.0], [0.0, 10.0],
[0.0, 0.0],
];
let tris = triangulate(&[pts], FillRule::NonZero);
assert!(!tris.is_empty(), "should produce triangles");
let area = total_area(&tris);
assert!(
area > 30.0 && area < 100.0,
"area should be in (30,100), got {area}"
);
}
#[test]
fn donut_hole_correct_area() {
let outer = vec![[0.0f32, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0]];
let inner = vec![[4.0f32, 4.0], [4.0, 6.0], [6.0, 6.0], [6.0, 4.0]];
let tris = triangulate(&[outer, inner], FillRule::NonZero);
assert!(!tris.is_empty());
let area = total_area(&tris);
assert!(
(area - 96.0).abs() < 2.0,
"donut area should be ~96.0, got {area}"
);
}
#[test]
fn evenodd_vs_nonzero_differ() {
let outer = vec![[0.0f32, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0]];
let inner = vec![[3.0f32, 3.0], [7.0, 3.0], [7.0, 7.0], [3.0, 7.0]];
let tris_nz = triangulate(&[outer.clone(), inner.clone()], FillRule::NonZero);
let tris_eo = triangulate(&[outer, inner], FillRule::EvenOdd);
let area_nz = total_area(&tris_nz);
let area_eo = total_area(&tris_eo);
assert!(
area_nz > area_eo,
"NonZero should fill more than EvenOdd: nz={area_nz}, eo={area_eo}"
);
}
#[test]
fn degenerate_too_few_points_is_empty() {
let pts = vec![[0.0f32, 0.0], [1.0, 0.0]];
let tris = triangulate(&[pts], FillRule::NonZero);
assert!(tris.is_empty());
}
#[test]
fn degenerate_collinear_is_empty() {
let pts = vec![[0.0f32, 0.0], [1.0, 0.0], [2.0, 0.0]];
let tris = triangulate(&[pts], FillRule::NonZero);
assert!(tris.is_empty());
}
#[test]
fn empty_contours_is_empty() {
let tris = triangulate(&[], FillRule::NonZero);
assert!(tris.is_empty());
}
#[test]
fn triangle_gives_one_triangle() {
let pts = vec![[0.0f32, 0.0], [1.0, 0.0], [0.5, 1.0]];
let tris = triangulate(&[pts], FillRule::NonZero);
assert_eq!(tris.len(), 1);
}
}