use alloc::vec::Vec;
use crate::{Point, Polygon, Window};
#[must_use]
pub fn clip_polygon(polygon: &Polygon, window: Window) -> Polygon {
let clipped = &polygon.vertices;
let clipped = clip_left(clipped, window.x_min);
let clipped = clip_right(&clipped, window.x_max);
let clipped = clip_bottom(&clipped, window.y_min);
clip_top(&clipped, window.y_max).into()
}
fn clip_left(clipped: &[Point], x_min: f64) -> Vec<Point> {
clip_edge(
clipped,
|p| p.x >= x_min,
|p1, p2| {
let t = (x_min - p1.x) / (p2.x - p1.x);
Point::new(x_min, p1.y + t * (p2.y - p1.y))
},
)
}
fn clip_right(clipped: &[Point], x_max: f64) -> Vec<Point> {
clip_edge(
clipped,
|p| p.x <= x_max,
|p1, p2| {
let t = (x_max - p1.x) / (p2.x - p1.x);
Point::new(x_max, p1.y + t * (p2.y - p1.y))
},
)
}
fn clip_bottom(clipped: &[Point], y_min: f64) -> Vec<Point> {
clip_edge(
clipped,
|p| p.y >= y_min,
|p1, p2| {
let t = (y_min - p1.y) / (p2.y - p1.y);
Point::new(p1.x + t * (p2.x - p1.x), y_min)
},
)
}
fn clip_top(clipped: &[Point], y_max: f64) -> Vec<Point> {
clip_edge(
clipped,
|p| p.y <= y_max,
|p1, p2| {
let t = (y_max - p1.y) / (p2.y - p1.y);
Point::new(p1.x + t * (p2.x - p1.x), y_max)
},
)
}
fn clip_edge<F, I>(vertices: &[Point], is_inside: F, get_intersection: I) -> Vec<Point>
where
F: Fn(Point) -> bool,
I: Fn(Point, Point) -> Point,
{
let Some(&last) = vertices.last() else {
return Vec::new();
};
let mut result = Vec::with_capacity(vertices.len());
let mut previous = last;
for ¤t in vertices {
let previous_inside = is_inside(previous);
let current_inside = is_inside(current);
match (previous_inside, current_inside) {
(true, true) => result.push(current),
(true, false) => result.push(get_intersection(previous, current)),
(false, true) => {
result.push(get_intersection(previous, current));
result.push(current);
}
(false, false) => {}
}
previous = current;
}
result
}
#[cfg(test)]
mod tests {
use alloc::vec::Vec;
use rstest::rstest;
use super::*;
const WINDOW: Window = Window::new(-1.0, 1.0, -1.0, 1.0);
fn poly(points: &[(f64, f64)]) -> Polygon {
let vertices: Vec<Point> = points.iter().map(|&(x, y)| Point::new(x, y)).collect();
vertices.into()
}
#[rstest]
#[case::inside(
&[(-0.5, -0.5), (-0.5, 0.5), (0.5, 0.5), (0.5, -0.5)],
&[(-0.5, -0.5), (-0.5, 0.5), (0.5, 0.5), (0.5, -0.5)]
)]
#[case::outside_left(
&[(-3.0, -0.5), (-3.0, 0.5), (-2.0, 0.5), (-2.0, -0.5)],
&[]
)]
#[case::outside_right(
&[(2.0, -0.5), (2.0, 0.5), (3.0, 0.5), (3.0, -0.5)],
&[]
)]
#[case::outside_top(
&[(-0.5, 2.0), (-0.5, 3.0), (0.5, 3.0), (0.5, 2.0)],
&[]
)]
#[case::outside_bottom(
&[(-0.5, -3.0), (-0.5, -2.0), (0.5, -2.0), (0.5, -3.0)],
&[]
)]
#[case::outside_top_right(
&[(2.0, 2.0), (2.0, 3.0), (3.0, 3.0), (3.0, 2.0)],
&[]
)]
#[case::inside_triangle(
&[(-0.5, -0.5), (0.0, 0.5), (0.5, -0.5)],
&[(-0.5, -0.5), (0.0, 0.5), (0.5, -0.5)]
)]
#[case::empty(&[], &[])]
fn no_clipping(#[case] input: &[(f64, f64)], #[case] expected: &[(f64, f64)]) {
assert_eq!(clip_polygon(&poly(input), WINDOW), poly(expected));
}
#[rstest]
#[case::top(
&[(-0.5, -0.5), (-0.5, 1.5), (0.5, 1.5), (0.5, -0.5)],
&[(-0.5, -0.5), (-0.5, 1.0), (0.5, 1.0), (0.5, -0.5)]
)]
#[case::bottom(
&[(-0.5, 0.5), (-0.5, -1.5), (0.5, -1.5), (0.5, 0.5)],
&[(-0.5, 0.5), (-0.5, -1.0), (0.5, -1.0), (0.5, 0.5)]
)]
#[case::right(
&[(-0.5, -0.5), (1.5, -0.5), (1.5, 0.5), (-0.5, 0.5)],
&[(-0.5, -0.5), (1.0, -0.5), (1.0, 0.5), (-0.5, 0.5)]
)]
#[case::left(
&[(0.5, -0.5), (-1.5, -0.5), (-1.5, 0.5), (0.5, 0.5)],
&[(0.5, -0.5), (-1.0, -0.5), (-1.0, 0.5), (0.5, 0.5)]
)]
fn one_edge(#[case] input: &[(f64, f64)], #[case] expected: &[(f64, f64)]) {
assert_eq!(clip_polygon(&poly(input), WINDOW), poly(expected));
}
#[rstest]
#[case::top_bottom(
&[(-0.5, -1.5), (-0.5, 1.5), (0.5, 1.5), (0.5, -1.5)],
&[(-0.5, -1.0), (-0.5, 1.0), (0.5, 1.0), (0.5, -1.0)]
)]
#[case::left_right(
&[(-1.5, -0.5), (1.5, -0.5), (1.5, 0.5), (-1.5, 0.5)],
&[(-1.0, -0.5), (1.0, -0.5), (1.0, 0.5), (-1.0, 0.5)]
)]
#[case::top_right(
&[(0.5, 0.5), (0.5, 1.5), (1.5, 1.5), (1.5, 0.5)],
&[(1.0, 1.0), (1.0, 0.5), (0.5, 0.5), (0.5, 1.0)]
)]
#[case::top_left(
&[(-0.5, 0.5), (-1.5, 0.5), (-1.5, 1.5), (-0.5, 1.5)],
&[(-0.5, 1.0), (-0.5, 0.5), (-1.0, 0.5), (-1.0, 1.0)]
)]
#[case::bottom_right(
&[(0.5, -0.5), (0.5, -1.5), (1.5, -1.5), (1.5, -0.5)],
&[(1.0, -1.0), (1.0, -0.5), (0.5, -0.5), (0.5, -1.0)]
)]
#[case::bottom_left(
&[(-0.5, -0.5), (-0.5, -1.5), (-1.5, -1.5), (-1.5, -0.5)],
&[(-1.0, -1.0), (-1.0, -0.5), (-0.5, -0.5), (-0.5, -1.0)]
)]
fn two_edges(#[case] input: &[(f64, f64)], #[case] expected: &[(f64, f64)]) {
assert_eq!(clip_polygon(&poly(input), WINDOW), poly(expected));
}
#[rstest]
#[case::top_bottom_left(
&[(-1.5, -1.5), (-1.5, 1.5), (0.5, 1.5), (0.5, -1.5)],
&[(-1.0, -1.0), (-1.0, 1.0), (0.5, 1.0), (0.5, -1.0)]
)]
#[case::top_bottom_right(
&[(1.5, -1.5), (1.5, 1.5), (-0.5, 1.5), (-0.5, -1.5)],
&[(1.0, -1.0), (1.0, 1.0), (-0.5, 1.0), (-0.5, -1.0)]
)]
#[case::top_left_right(
&[(-1.5, 1.5), (1.5, 1.5), (1.5, -0.5), (-1.5, -0.5)],
&[(-1.0, 1.0), (1.0, 1.0), (1.0, -0.5), (-1.0, -0.5)]
)]
#[case::bottom_left_right(
&[(-1.5, -1.5), (1.5, -1.5), (1.5, 0.5), (-1.5, 0.5)],
&[(-1.0, -1.0), (1.0, -1.0), (1.0, 0.5), (-1.0, 0.5)]
)]
fn three_edges(#[case] input: &[(f64, f64)], #[case] expected: &[(f64, f64)]) {
assert_eq!(clip_polygon(&poly(input), WINDOW), poly(expected));
}
#[rstest]
#[case::all(
&[(-2.0, -2.0), (-2.0, 2.0), (2.0, 2.0), (2.0, -2.0)],
&[(1.0, 1.0), (1.0, -1.0), (-1.0, -1.0), (-1.0, 1.0)]
)]
fn all_edges(#[case] input: &[(f64, f64)], #[case] expected: &[(f64, f64)]) {
assert_eq!(clip_polygon(&poly(input), WINDOW), poly(expected));
}
#[test]
fn concave_u_joins_disconnected_regions() {
let input = poly(&[
(-0.75, 0.75),
(-0.75, -2.0),
(0.75, -2.0),
(0.75, 0.75),
(0.25, 0.75),
(0.25, -1.5),
(-0.25, -1.5),
(-0.25, 0.75),
]);
let expected = poly(&[
(-0.75, 0.75),
(-0.75, -1.0),
(0.75, -1.0),
(0.75, 0.75),
(0.25, 0.75),
(0.25, -1.0),
(-0.25, -1.0),
(-0.25, 0.75),
]);
assert_eq!(clip_polygon(&input, WINDOW), expected);
}
#[rstest]
#[case::triangle_to_quadrilateral(
&[(0.0, 0.0), (-0.5, 2.0), (0.5, 0.0)],
&[(0.0, 0.0), (-0.25, 1.0), (0.0, 1.0), (0.5, 0.0)]
)]
#[case::quadrilateral_to_pentagon(
&[(-0.75, 0.5), (0.0, 1.5), (0.75, 0.5), (0.0, -0.5)],
&[(-0.75, 0.5), (-0.375, 1.0), (0.375, 1.0), (0.75, 0.5), (0.0, -0.5)]
)]
#[case::quadrilateral_to_hexagon(
&[(-0.5, 0.0), (0.0, 2.0), (0.5, 0.0), (0.0, -2.0)],
&[(-0.25, -1.0), (-0.5, 0.0), (-0.25, 1.0), (0.25, 1.0), (0.5, 0.0), (0.25, -1.0)]
)]
fn diagonal_edges(#[case] input: &[(f64, f64)], #[case] expected: &[(f64, f64)]) {
assert_eq!(clip_polygon(&poly(input), WINDOW), poly(expected));
}
}