use super::{
is_ccw, is_collinear, orientation, GeometryError, GeometryResult, Point2D, GEOMETRY_EPSILON,
};
pub fn graham_scan(points: &[Point2D]) -> GeometryResult<Vec<Point2D>> {
if points.len() < 3 {
return Err(GeometryError::InsufficientPoints {
needed: 3,
got: points.len(),
});
}
let mut pivot_idx = 0;
for (i, p) in points.iter().enumerate().skip(1) {
if p.y < points[pivot_idx].y || (p.y == points[pivot_idx].y && p.x < points[pivot_idx].x) {
pivot_idx = i;
}
}
let pivot = points[pivot_idx];
let mut indexed_points: Vec<(usize, Point2D)> = points
.iter()
.enumerate()
.filter(|&(i, _)| i != pivot_idx)
.map(|(i, &p)| (i, p))
.collect();
indexed_points.sort_by(|a, b| {
let angle_a = (a.1.y - pivot.y).atan2(a.1.x - pivot.x);
let angle_b = (b.1.y - pivot.y).atan2(b.1.x - pivot.x);
match angle_a.partial_cmp(&angle_b) {
Some(std::cmp::Ordering::Equal) => {
let dist_a = pivot.distance_squared(&a.1);
let dist_b = pivot.distance_squared(&b.1);
dist_a
.partial_cmp(&dist_b)
.unwrap_or(std::cmp::Ordering::Equal)
}
Some(ord) => ord,
None => std::cmp::Ordering::Equal,
}
});
let mut filtered: Vec<Point2D> = Vec::with_capacity(indexed_points.len());
for i in 0..indexed_points.len() {
if i + 1 < indexed_points.len() {
let curr = indexed_points[i].1;
let next = indexed_points[i + 1].1;
if is_collinear(&pivot, &curr, &next) {
let dist_curr = pivot.distance_squared(&curr);
let dist_next = pivot.distance_squared(&next);
if dist_next >= dist_curr {
continue;
}
}
}
filtered.push(indexed_points[i].1);
}
if filtered.len() < 2 {
let mut result = vec![pivot];
if !filtered.is_empty() {
result.push(filtered[filtered.len() - 1]);
}
return Ok(result);
}
let mut stack: Vec<Point2D> = Vec::with_capacity(filtered.len() + 1);
stack.push(pivot);
stack.push(filtered[0]);
for &point in filtered.iter().skip(1) {
while stack.len() > 1 {
let top = stack[stack.len() - 1];
let second = stack[stack.len() - 2];
if orientation(&second, &top, &point) <= GEOMETRY_EPSILON {
stack.pop();
} else {
break;
}
}
stack.push(point);
}
if stack.len() < 3 {
return Err(GeometryError::DegenerateGeometry(
"All points are collinear".to_string(),
));
}
Ok(stack)
}
pub fn gift_wrapping(points: &[Point2D]) -> GeometryResult<Vec<Point2D>> {
let n = points.len();
if n < 3 {
return Err(GeometryError::InsufficientPoints { needed: 3, got: n });
}
let mut leftmost = 0;
for i in 1..n {
if points[i].x < points[leftmost].x
|| (points[i].x == points[leftmost].x && points[i].y < points[leftmost].y)
{
leftmost = i;
}
}
let mut hull: Vec<Point2D> = Vec::new();
let mut current = leftmost;
let max_iterations = n + 1;
for _ in 0..max_iterations {
hull.push(points[current]);
let mut next = 0;
for i in 1..n {
if next == current {
next = i;
continue;
}
let orient = orientation(&points[current], &points[next], &points[i]);
if orient > GEOMETRY_EPSILON {
next = i;
} else if orient.abs() <= GEOMETRY_EPSILON {
let dist_next = points[current].distance_squared(&points[next]);
let dist_i = points[current].distance_squared(&points[i]);
if dist_i > dist_next {
next = i;
}
}
}
current = next;
if current == leftmost {
break;
}
}
if hull.len() < 3 {
return Err(GeometryError::DegenerateGeometry(
"All points are collinear".to_string(),
));
}
Ok(hull)
}
pub fn convex_hull_area(hull: &[Point2D]) -> f64 {
if hull.len() < 3 {
return 0.0;
}
let n = hull.len();
let mut area = 0.0;
for i in 0..n {
let j = (i + 1) % n;
area += hull[i].x * hull[j].y;
area -= hull[j].x * hull[i].y;
}
(area / 2.0).abs()
}
pub fn convex_hull_perimeter(hull: &[Point2D]) -> f64 {
if hull.len() < 2 {
return 0.0;
}
let n = hull.len();
let mut perimeter = 0.0;
for i in 0..n {
let j = (i + 1) % n;
perimeter += hull[i].distance(&hull[j]);
}
perimeter
}
pub fn point_in_convex_hull(point: &Point2D, hull: &[Point2D]) -> bool {
if hull.len() < 3 {
return false;
}
let n = hull.len();
let mut all_positive = true;
let mut all_negative = true;
for i in 0..n {
let j = (i + 1) % n;
let cross = orientation(&hull[i], &hull[j], point);
if cross < -GEOMETRY_EPSILON {
all_positive = false;
}
if cross > GEOMETRY_EPSILON {
all_negative = false;
}
}
all_positive || all_negative
}
pub fn convex_hull_merge(hull_a: &[Point2D], hull_b: &[Point2D]) -> GeometryResult<Vec<Point2D>> {
if hull_a.is_empty() && hull_b.is_empty() {
return Err(GeometryError::InsufficientPoints { needed: 3, got: 0 });
}
if hull_a.is_empty() {
return Ok(hull_b.to_vec());
}
if hull_b.is_empty() {
return Ok(hull_a.to_vec());
}
let mut all_points: Vec<Point2D> = Vec::with_capacity(hull_a.len() + hull_b.len());
all_points.extend_from_slice(hull_a);
all_points.extend_from_slice(hull_b);
graham_scan(&all_points)
}
pub fn monotone_chain(points: &[Point2D]) -> GeometryResult<Vec<Point2D>> {
let n = points.len();
if n < 3 {
return Err(GeometryError::InsufficientPoints { needed: 3, got: n });
}
let mut sorted: Vec<Point2D> = points.to_vec();
sorted.sort_by(|a, b| {
a.x.partial_cmp(&b.x)
.unwrap_or(std::cmp::Ordering::Equal)
.then(a.y.partial_cmp(&b.y).unwrap_or(std::cmp::Ordering::Equal))
});
sorted.dedup_by(|a, b| a.distance(b) < GEOMETRY_EPSILON);
let m = sorted.len();
if m < 3 {
return Err(GeometryError::DegenerateGeometry(
"Too few unique points for a convex hull".to_string(),
));
}
let mut lower: Vec<Point2D> = Vec::with_capacity(m);
for &p in &sorted {
while lower.len() >= 2 {
let len = lower.len();
if orientation(&lower[len - 2], &lower[len - 1], &p) <= GEOMETRY_EPSILON {
lower.pop();
} else {
break;
}
}
lower.push(p);
}
let mut upper: Vec<Point2D> = Vec::with_capacity(m);
for &p in sorted.iter().rev() {
while upper.len() >= 2 {
let len = upper.len();
if orientation(&upper[len - 2], &upper[len - 1], &p) <= GEOMETRY_EPSILON {
upper.pop();
} else {
break;
}
}
upper.push(p);
}
lower.pop();
upper.pop();
lower.extend(upper);
if lower.len() < 3 {
return Err(GeometryError::DegenerateGeometry(
"All points are collinear".to_string(),
));
}
Ok(lower)
}
fn _upper_tangent(hull_a: &[Point2D], hull_b: &[Point2D]) -> (usize, usize) {
let na = hull_a.len();
let nb = hull_b.len();
let mut ia = 0;
for i in 1..na {
if hull_a[i].x > hull_a[ia].x {
ia = i;
}
}
let mut ib = 0;
for i in 1..nb {
if hull_b[i].x < hull_b[ib].x {
ib = i;
}
}
let mut done = false;
while !done {
done = true;
if na > 1 {
loop {
let next = (ia + 1) % na;
if orientation(&hull_b[ib], &hull_a[ia], &hull_a[next]) > 0.0 {
ia = next;
done = false;
} else {
break;
}
}
}
if nb > 1 {
loop {
let prev = if ib == 0 { nb - 1 } else { ib - 1 };
if orientation(&hull_a[ia], &hull_b[ib], &hull_b[prev]) < 0.0 {
ib = prev;
done = false;
} else {
break;
}
}
}
}
(ia, ib)
}
#[cfg(test)]
mod tests {
use super::*;
fn square_points() -> Vec<Point2D> {
vec![
Point2D::new(0.0, 0.0),
Point2D::new(1.0, 0.0),
Point2D::new(1.0, 1.0),
Point2D::new(0.0, 1.0),
]
}
fn square_with_interior() -> Vec<Point2D> {
vec![
Point2D::new(0.0, 0.0),
Point2D::new(1.0, 0.0),
Point2D::new(1.0, 1.0),
Point2D::new(0.0, 1.0),
Point2D::new(0.5, 0.5), Point2D::new(0.3, 0.3), Point2D::new(0.7, 0.2), ]
}
#[test]
fn test_graham_scan_square() {
let points = square_points();
let hull = graham_scan(&points).expect("graham scan should succeed");
assert_eq!(hull.len(), 4);
assert!((convex_hull_area(&hull) - 1.0).abs() < 1e-10);
}
#[test]
fn test_graham_scan_with_interior() {
let points = square_with_interior();
let hull = graham_scan(&points).expect("graham scan should succeed");
assert_eq!(hull.len(), 4);
assert!((convex_hull_area(&hull) - 1.0).abs() < 1e-10);
}
#[test]
fn test_gift_wrapping_square() {
let points = square_points();
let hull = gift_wrapping(&points).expect("gift wrapping should succeed");
assert_eq!(hull.len(), 4);
assert!((convex_hull_area(&hull) - 1.0).abs() < 1e-10);
}
#[test]
fn test_gift_wrapping_with_interior() {
let points = square_with_interior();
let hull = gift_wrapping(&points).expect("gift wrapping should succeed");
assert_eq!(hull.len(), 4);
assert!((convex_hull_area(&hull) - 1.0).abs() < 1e-10);
}
#[test]
fn test_convex_hull_area_triangle() {
let hull = vec![
Point2D::new(0.0, 0.0),
Point2D::new(4.0, 0.0),
Point2D::new(0.0, 3.0),
];
assert!((convex_hull_area(&hull) - 6.0).abs() < 1e-10);
}
#[test]
fn test_convex_hull_perimeter_square() {
let hull = vec![
Point2D::new(0.0, 0.0),
Point2D::new(1.0, 0.0),
Point2D::new(1.0, 1.0),
Point2D::new(0.0, 1.0),
];
assert!((convex_hull_perimeter(&hull) - 4.0).abs() < 1e-10);
}
#[test]
fn test_point_in_convex_hull_inside() {
let hull = vec![
Point2D::new(0.0, 0.0),
Point2D::new(4.0, 0.0),
Point2D::new(4.0, 4.0),
Point2D::new(0.0, 4.0),
];
assert!(point_in_convex_hull(&Point2D::new(2.0, 2.0), &hull));
assert!(point_in_convex_hull(&Point2D::new(0.5, 0.5), &hull));
}
#[test]
fn test_point_in_convex_hull_outside() {
let hull = vec![
Point2D::new(0.0, 0.0),
Point2D::new(4.0, 0.0),
Point2D::new(4.0, 4.0),
Point2D::new(0.0, 4.0),
];
assert!(!point_in_convex_hull(&Point2D::new(5.0, 5.0), &hull));
assert!(!point_in_convex_hull(&Point2D::new(-1.0, 2.0), &hull));
}
#[test]
fn test_point_in_convex_hull_on_boundary() {
let hull = vec![
Point2D::new(0.0, 0.0),
Point2D::new(4.0, 0.0),
Point2D::new(4.0, 4.0),
Point2D::new(0.0, 4.0),
];
assert!(point_in_convex_hull(&Point2D::new(2.0, 0.0), &hull));
assert!(point_in_convex_hull(&Point2D::new(0.0, 2.0), &hull));
}
#[test]
fn test_convex_hull_merge() {
let hull_a = vec![
Point2D::new(0.0, 0.0),
Point2D::new(2.0, 0.0),
Point2D::new(2.0, 2.0),
Point2D::new(0.0, 2.0),
];
let hull_b = vec![
Point2D::new(3.0, 0.0),
Point2D::new(5.0, 0.0),
Point2D::new(5.0, 2.0),
Point2D::new(3.0, 2.0),
];
let merged = convex_hull_merge(&hull_a, &hull_b).expect("hull merge should succeed");
let merged_area = convex_hull_area(&merged);
assert!(merged_area >= convex_hull_area(&hull_a) - GEOMETRY_EPSILON);
assert!(merged_area >= convex_hull_area(&hull_b) - GEOMETRY_EPSILON);
}
#[test]
fn test_insufficient_points() {
let points = vec![Point2D::new(0.0, 0.0), Point2D::new(1.0, 1.0)];
assert!(graham_scan(&points).is_err());
assert!(gift_wrapping(&points).is_err());
}
#[test]
fn test_triangle_hull() {
let points = vec![
Point2D::new(0.0, 0.0),
Point2D::new(1.0, 0.0),
Point2D::new(0.5, 1.0),
];
let hull_gs = graham_scan(&points).expect("graham scan should succeed");
assert_eq!(hull_gs.len(), 3);
assert!((convex_hull_area(&hull_gs) - 0.5).abs() < 1e-10);
let hull_gw = gift_wrapping(&points).expect("gift wrapping should succeed");
assert_eq!(hull_gw.len(), 3);
assert!((convex_hull_area(&hull_gw) - 0.5).abs() < 1e-10);
}
#[test]
fn test_many_points_on_circle() {
let n = 20;
let mut points: Vec<Point2D> = Vec::with_capacity(n);
for i in 0..n {
let angle = 2.0 * std::f64::consts::PI * (i as f64) / (n as f64);
points.push(Point2D::new(angle.cos(), angle.sin()));
}
let hull = graham_scan(&points).expect("graham scan should succeed");
assert_eq!(hull.len(), n);
let area = convex_hull_area(&hull);
assert!((area - std::f64::consts::PI).abs() < 0.1);
}
#[test]
fn test_monotone_chain_square() {
let points = square_points();
let hull = monotone_chain(&points).expect("monotone chain should succeed");
assert_eq!(hull.len(), 4);
assert!((convex_hull_area(&hull) - 1.0).abs() < 1e-10);
}
#[test]
fn test_monotone_chain_with_interior() {
let points = square_with_interior();
let hull = monotone_chain(&points).expect("monotone chain should succeed");
assert_eq!(hull.len(), 4);
assert!((convex_hull_area(&hull) - 1.0).abs() < 1e-10);
}
#[test]
fn test_monotone_chain_triangle() {
let points = vec![
Point2D::new(0.0, 0.0),
Point2D::new(1.0, 0.0),
Point2D::new(0.5, 1.0),
];
let hull = monotone_chain(&points).expect("monotone chain should succeed");
assert_eq!(hull.len(), 3);
assert!((convex_hull_area(&hull) - 0.5).abs() < 1e-10);
}
#[test]
fn test_hull_consistency() {
let points = vec![
Point2D::new(0.0, 0.0),
Point2D::new(3.0, 0.0),
Point2D::new(3.0, 4.0),
Point2D::new(1.5, 5.0),
Point2D::new(0.0, 4.0),
Point2D::new(1.0, 2.0), Point2D::new(2.0, 1.0), ];
let hull_gs = graham_scan(&points).expect("graham scan should succeed");
let hull_gw = gift_wrapping(&points).expect("gift wrapping should succeed");
let area_gs = convex_hull_area(&hull_gs);
let area_gw = convex_hull_area(&hull_gw);
assert!(
(area_gs - area_gw).abs() < 1e-8,
"Graham scan area ({}) and gift wrapping area ({}) should match",
area_gs,
area_gw
);
}
}