use std::collections::HashMap;
use axiolid_contracts::{GeomError, GeomResult};
use axiolid_core::Point2;
use super::orient;
pub(crate) const MAX_HALVINGS: u32 = 60;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct Steiner {
pub(crate) seam: [u32; 2],
pub(crate) toward: u32,
pub(crate) halvings: u32,
}
impl Steiner {
pub(crate) fn place<T: Copy>(&self, at: impl Fn(u32) -> T, midpoint: impl Fn(T, T) -> T) -> T {
let m = midpoint(at(self.seam[0]), at(self.seam[1]));
let mut x = at(self.toward);
for _ in 0..self.halvings {
x = midpoint(m, x);
}
x
}
}
pub(crate) fn midpoint2(a: Point2, b: Point2) -> Point2 {
Point2::new((a.x + b.x) * 0.5, (a.y + b.y) * 0.5)
}
pub(crate) fn seal(
points: &mut Vec<Point2>,
triangles: &mut Vec<[u32; 3]>,
seams: &[[u32; 2]],
) -> GeomResult<Vec<Steiner>> {
let before = triangles.clone();
let mut added = Vec::new();
let mut owner: HashMap<(u32, u32), usize> = HashMap::with_capacity(3 * triangles.len());
for (index, t) in triangles.iter().enumerate() {
for k in 0..3 {
owner.insert((t[k], t[(k + 1) % 3]), index);
}
}
for &[p, q] in seams {
let (Some(&one), Some(&other)) = (owner.get(&(p, q)), owner.get(&(q, p))) else {
continue;
};
let c = third(triangles[one], p, q);
let d = third(triangles[other], q, p);
let at = |v: u32| points[v as usize];
let replace: Vec<[u32; 3]> =
if orient(at(c), at(p), at(d)) > 0 && orient(at(d), at(q), at(c)) > 0 {
vec![[c, p, d], [d, q, c]]
} else {
let Some(steiner) = place(points, p, q, c, d) else {
return Err(GeomError::Degenerate(format!(
"profile seam from ({}, {}) to ({}, {}) is forced to be a triangle edge: \
no point inside its two triangles can take it out",
at(p).x,
at(p).y,
at(q).x,
at(q).y
)));
};
let s = u32::try_from(points.len()).map_err(|_| {
GeomError::InvalidInput(
"profile Steiner points exceed what a u32 index addresses".to_owned(),
)
})?;
let position = steiner.place(|v| points[v as usize], midpoint2);
points.push(position);
added.push(steiner);
vec![[p, d, s], [d, q, s], [q, c, s], [c, p, s]]
};
for index in [one, other] {
let t = triangles[index];
for k in 0..3 {
owner.remove(&(t[k], t[(k + 1) % 3]));
}
}
let slots = [one, other];
for (k, t) in replace.into_iter().enumerate() {
let index = if k < 2 {
triangles[slots[k]] = t;
slots[k]
} else {
triangles.push(t);
triangles.len() - 1
};
for j in 0..3 {
owner.insert((t[j], t[(j + 1) % 3]), index);
}
}
}
certify_same_cover(points, &before, triangles, added.len(), seams)?;
Ok(added)
}
fn third(t: [u32; 3], a: u32, b: u32) -> u32 {
t.into_iter().find(|&v| v != a && v != b).unwrap_or(t[0])
}
fn place(points: &[Point2], p: u32, q: u32, c: u32, d: u32) -> Option<Steiner> {
let at = |v: u32| points[v as usize];
let fits = |steiner: &Steiner| {
let s = steiner.place(at, midpoint2);
[(p, d), (d, q), (q, c), (c, p)]
.into_iter()
.all(|(a, b)| orient(at(a), at(b), s) > 0)
&& orient(at(p), at(q), s) != 0
};
for halvings in 1..=MAX_HALVINGS {
for toward in [c, d] {
let first = Steiner {
seam: [p, q],
toward,
halvings,
};
if fits(&first) {
let deeper = Steiner {
halvings: halvings + 1,
..first
};
return Some(if fits(&deeper) { deeper } else { first });
}
}
}
None
}
pub(crate) fn certify_same_cover(
points: &[Point2],
before: &[[u32; 3]],
after: &[[u32; 3]],
steiner: usize,
seams: &[[u32; 2]],
) -> GeomResult<()> {
let fail = |why: String| {
Err(GeomError::Degenerate(format!(
"keyhole triangulation failed its certificate: {why}"
)))
};
if after.len() != before.len() + 2 * steiner {
return fail(format!(
"{} triangles, the cover it replaces has {} and {steiner} Steiner points add {}",
after.len(),
before.len(),
2 * steiner
));
}
let net = |triangles: &[[u32; 3]]| {
let mut net: HashMap<(u32, u32), i64> = HashMap::with_capacity(3 * triangles.len());
for t in triangles {
for k in 0..3 {
let (a, b) = (t[k], t[(k + 1) % 3]);
let (key, sign) = if a < b { ((a, b), 1) } else { ((b, a), -1) };
*net.entry(key).or_insert(0) += sign;
}
}
net.retain(|_, n| *n != 0);
net
};
let mut directed: HashMap<(u32, u32), u32> = HashMap::with_capacity(3 * after.len());
for t in after {
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 k in 0..3 {
let edge = (t[k], t[(k + 1) % 3]);
let uses = directed.entry(edge).or_insert(0);
*uses += 1;
if *uses > 1 {
return fail(format!("edge {}->{} is used twice", edge.0, edge.1));
}
if seams
.iter()
.any(|s| (s[0], s[1]) == edge || (s[1], s[0]) == edge)
{
return fail(format!("edge {}->{} lies on a seam", edge.0, edge.1));
}
}
}
if net(before) != net(after) {
return fail("its boundary differs from the cover it replaces".to_owned());
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn p(x: f64, y: f64) -> Point2 {
Point2::new(x, y)
}
fn reflex() -> (Vec<Point2>, Vec<[u32; 3]>) {
let points = vec![p(0.0, 0.0), p(1.0, 1.0), p(4.0, 1.0), p(1.0, 4.0)];
(points, vec![[0, 1, 3], [1, 0, 2]])
}
#[test]
fn a_convex_quadrilateral_flips_without_a_point() {
let mut points = vec![p(0.0, 0.0), p(2.0, 2.0), p(2.0, 0.0), p(0.0, 2.0)];
let mut triangles = vec![[0, 1, 3], [1, 0, 2]];
let added = seal(&mut points, &mut triangles, &[[0, 1]]).expect("flips");
assert!(added.is_empty());
assert_eq!(points.len(), 4);
assert_eq!(triangles, vec![[3, 0, 2], [2, 1, 3]]);
}
#[test]
fn a_reflex_quadrilateral_takes_a_steiner_point_off_the_seam() {
let (mut points, mut triangles) = reflex();
let added = seal(&mut points, &mut triangles, &[[0, 1]]).expect("a point fits");
assert_eq!(added.len(), 1);
assert_eq!(triangles.len(), 4);
let s = points[4];
assert_ne!(orient(points[0], points[1], s), 0);
assert_eq!(
added[0].place(|v| points[v as usize], midpoint2),
s,
"the recipe places the point"
);
}
#[test]
fn the_certificate_names_each_defect() {
let (mut points, before) = reflex();
points.push(p(5.0, 5.0));
let seams = [[0, 1]];
let reason = |after: &[[u32; 3]], steiner: usize| match certify_same_cover(
&points, &before, after, steiner, &seams,
) {
Err(GeomError::Degenerate(message)) => message,
other => panic!("expected a refusal, got {other:?}"),
};
assert!(reason(&before, 0).contains("lies on a seam"));
assert!(reason(&before, 1).contains("Steiner points add 2"));
assert!(reason(&[[0, 3, 1], [1, 2, 0]], 0).contains("not strictly counter-clockwise"));
assert!(reason(&[[0, 2, 4], [0, 4, 3]], 0).contains("boundary differs"));
assert!(reason(&[[0, 2, 4], [0, 2, 3]], 0).contains("used twice"));
}
}