use std::collections::HashMap;
use axiolid_contracts::{GeomError, GeomResult};
use axiolid_core::Point2;
use axiolid_guarantees::Sign;
use axiolid_predicates::orient2d;
use crate::profile::PinchPolicy;
mod bridge;
mod clip;
mod pinch;
mod validate;
pub(crate) fn triangulate_rings(
outer: &[Point2],
holes: &[Vec<Point2>],
policy: PinchPolicy,
) -> GeomResult<(Vec<Point2>, Vec<[u32; 3]>)> {
let mut points = Vec::with_capacity(outer.len() + holes.iter().map(Vec::len).sum::<usize>());
let mut rings = Vec::with_capacity(1 + holes.len());
for ring in core::iter::once(outer).chain(holes.iter().map(Vec::as_slice)) {
let start = points.len();
points.extend_from_slice(ring);
rings.push(start..points.len());
}
if u32::try_from(points.len()).is_err() {
return Err(GeomError::InvalidInput(format!(
"profile has {} vertices, more than a u32 index addresses",
points.len()
)));
}
validate::check_rings(&points, &rings)?;
let touches = validate::check_edges(&points, &rings, policy)?;
if let Some(canon) = pinch::canonical(&points, &touches) {
let triangles = pinch::triangulate(&points, &rings, &canon, &touches)?;
return Ok((points, triangles));
}
let loops = validate::orient_and_place(&points, &rings)?;
let mut polygon = bridge::bridge_holes(&points, &loops)?;
let triangles = clip::clip_ears(&points, &mut polygon)?;
certify(&points, &loops, &triangles, 1, loops.len() - 1)?;
Ok((points, triangles))
}
#[derive(Debug, Clone)]
struct Loop {
vertices: Vec<u32>,
}
fn orient(a: Point2, b: Point2, c: Point2) -> i8 {
match orient2d(a, b, c).sign() {
Some(Sign::Positive) => 1,
Some(Sign::Negative) => -1,
_ => 0,
}
}
fn within(a: Point2, b: Point2, c: Point2) -> bool {
a.x.min(b.x) <= c.x && c.x <= a.x.max(b.x) && a.y.min(b.y) <= c.y && c.y <= a.y.max(b.y)
}
fn segments_touch(p1: Point2, p2: Point2, q1: Point2, q2: Point2) -> bool {
let o1 = orient(p1, p2, q1);
let o2 = orient(p1, p2, q2);
let o3 = orient(q1, q2, p1);
let o4 = orient(q1, q2, p2);
if o1 * o2 < 0 && o3 * o4 < 0 {
return true;
}
(o1 == 0 && within(p1, p2, q1))
|| (o2 == 0 && within(p1, p2, q2))
|| (o3 == 0 && within(q1, q2, p1))
|| (o4 == 0 && within(q1, q2, p2))
}
fn locally_inside(a: Point2, v: Point2, b: Point2, p: Point2) -> bool {
let left_of_in = orient(a, v, p) > 0;
let left_of_out = orient(v, b, p) > 0;
if orient(a, v, b) > 0 {
left_of_in && left_of_out
} else {
left_of_in || left_of_out
}
}
fn certify(
points: &[Point2],
loops: &[Loop],
triangles: &[[u32; 3]],
outers: usize,
holes: usize,
) -> GeomResult<()> {
let fail = |why: String| {
Err(GeomError::Degenerate(format!(
"profile triangulation failed its certificate: {why}"
)))
};
let vertex_count: usize = loops.iter().map(|ring| ring.vertices.len()).sum();
let expected = (vertex_count + 2 * holes).saturating_sub(2 * outers);
if triangles.len() != expected {
let parts = if outers == 1 {
String::new()
} else {
format!(" in {outers} parts")
};
return fail(format!(
"{} triangles, a polygon with {vertex_count} vertices and {holes} holes{parts} has {expected}",
triangles.len(),
));
}
let mut directed: HashMap<(u32, u32), u32> = HashMap::with_capacity(3 * triangles.len());
for t in triangles {
let corner = |k: usize| points[t[k] as usize];
if orient(corner(0), corner(1), corner(2)) <= 0 {
return fail(format!("triangle {t:?} is not strictly counter-clockwise"));
}
for (a, b) in [(t[0], t[1]), (t[1], t[2]), (t[2], t[0])] {
*directed.entry((a, b)).or_default() += 1;
}
}
let mut boundary: HashMap<(u32, u32), ()> = HashMap::with_capacity(vertex_count);
for ring in loops {
let n = ring.vertices.len();
for k in 0..n {
let edge = (ring.vertices[k], ring.vertices[(k + 1) % n]);
if directed.get(&edge) != Some(&1) || directed.contains_key(&(edge.1, edge.0)) {
return fail(format!(
"ring edge {}->{} is not covered exactly once from inside",
edge.0, edge.1
));
}
boundary.insert(edge, ());
}
}
for (&(a, b), &count) in &directed {
if count != 1 {
return fail(format!("edge {a}->{b} is used {count} times"));
}
if !boundary.contains_key(&(a, b)) && directed.get(&(b, a)) != Some(&1) {
return fail(format!("interior edge {a}->{b} has no twin"));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn rect(x0: f64, y0: f64, x1: f64, y1: f64) -> Vec<Point2> {
vec![
Point2::new(x0, y0),
Point2::new(x1, y0),
Point2::new(x1, y1),
Point2::new(x0, y1),
]
}
fn issue_profile() -> (Vec<Point2>, Vec<Loop>) {
let mut points = rect(-2.0, -2.0, 2.0, 2.0);
for mut hole in [rect(-1.5, -0.5, -0.5, 0.5), rect(0.5, -0.5, 1.5, 0.5)] {
hole.reverse();
points.extend(hole);
}
let loops = vec![
Loop {
vertices: vec![0, 1, 2, 3],
},
Loop {
vertices: vec![4, 5, 6, 7],
},
Loop {
vertices: vec![8, 9, 10, 11],
},
];
(points, loops)
}
#[test]
fn the_certificate_refuses_earcuts_t_junction() {
let (points, loops) = issue_profile();
let earcut = [
[6, 11, 8],
[10, 7, 0],
[10, 0, 1],
[10, 1, 2],
[3, 0, 7],
[3, 7, 4],
[3, 4, 5],
[5, 6, 8],
[9, 10, 2],
[9, 2, 3],
[3, 5, 8],
[3, 8, 9],
];
assert!(reason(&points, &loops, &earcut).contains("12 triangles"));
}
fn reason(points: &[Point2], loops: &[Loop], triangles: &[[u32; 3]]) -> String {
match certify(points, loops, triangles, 1, loops.len() - 1) {
Err(GeomError::Degenerate(message)) => message,
other => panic!("expected a certificate refusal, got {other:?}"),
}
}
#[test]
fn the_certificate_names_each_defect() {
let (points, _) = issue_profile();
let square = &points[..4];
let loops = vec![Loop {
vertices: vec![0, 1, 2, 3],
}];
assert!(certify(square, &loops, &[[0, 1, 2], [0, 2, 3]], 1, 0).is_ok());
assert!(reason(square, &loops, &[[0, 2, 1], [0, 3, 2]])
.contains("not strictly counter-clockwise"));
assert!(reason(square, &loops, &[[0, 1, 2], [1, 2, 3]])
.contains("ring edge 1->2 is not covered exactly once"));
let hexagon: Vec<Point2> = (0..6)
.map(|k| {
let t = f64::from(k) * core::f64::consts::FRAC_PI_3;
Point2::new(t.cos(), t.sin())
})
.collect();
let loops = vec![Loop {
vertices: (0..6).collect(),
}];
assert!(certify(
&hexagon,
&loops,
&[[0, 1, 2], [2, 3, 4], [4, 5, 0], [0, 2, 4]],
1,
0
)
.is_ok());
assert!(reason(
&hexagon,
&loops,
&[[0, 1, 2], [2, 3, 4], [4, 5, 0], [1, 3, 5]]
)
.contains("has no twin"));
}
#[test]
fn the_issue_profile_triangulates_and_certifies() {
let (points, _) = issue_profile();
let (outer, holes) = (
points[..4].to_vec(),
vec![points[4..8].to_vec(), points[8..].to_vec()],
);
let (_, triangles) =
triangulate_rings(&outer, &holes, PinchPolicy::Refuse).expect("triangulates");
assert_eq!(triangles.len(), 14);
}
}