use smallvec::SmallVec;
pub(crate) type Point = [f64; 2];
const CLIPPED_INLINE: usize = 8;
type ClippedPolygon = SmallVec<[Point; CLIPPED_INLINE]>;
#[derive(Clone, Copy)]
enum CellEdge {
Left { x_min: f64 },
Right { x_max: f64 },
Bottom { y_min: f64 },
Top { y_max: f64 },
}
impl CellEdge {
fn is_inside(self, point: Point) -> bool {
match self {
CellEdge::Left { x_min } => point[0] >= x_min,
CellEdge::Right { x_max } => point[0] <= x_max,
CellEdge::Bottom { y_min } => point[1] >= y_min,
CellEdge::Top { y_max } => point[1] <= y_max,
}
}
fn intersect(self, a: Point, b: Point) -> Point {
match self {
CellEdge::Left { x_min: x_target } | CellEdge::Right { x_max: x_target } => {
let denom = b[0] - a[0];
let t = (x_target - a[0]) / denom;
[x_target, a[1] + t * (b[1] - a[1])]
}
CellEdge::Bottom { y_min: y_target } | CellEdge::Top { y_max: y_target } => {
let denom = b[1] - a[1];
let t = (y_target - a[1]) / denom;
[a[0] + t * (b[0] - a[0]), y_target]
}
}
}
}
pub(crate) fn clip_quad_against_unit_cell(
quad: &[Point; 4],
cell_origin: (i32, i32),
) -> SmallVec<[Point; 8]> {
let (column, row) = cell_origin;
let x_min = column as f64;
let x_max = x_min + 1.0;
let y_min = row as f64;
let y_max = y_min + 1.0;
let edges = [
CellEdge::Left { x_min },
CellEdge::Right { x_max },
CellEdge::Bottom { y_min },
CellEdge::Top { y_max },
];
let mut input: ClippedPolygon = SmallVec::new();
input.extend_from_slice(quad);
for edge in edges {
if input.is_empty() {
return input;
}
let mut output: ClippedPolygon = SmallVec::new();
let vertex_count = input.len();
for i in 0..vertex_count {
let current = input[i];
let previous = input[(i + vertex_count - 1) % vertex_count];
let current_inside = edge.is_inside(current);
let previous_inside = edge.is_inside(previous);
match (previous_inside, current_inside) {
(true, true) => {
output.push(current);
}
(true, false) => {
output.push(edge.intersect(previous, current));
}
(false, true) => {
output.push(edge.intersect(previous, current));
output.push(current);
}
(false, false) => {}
}
}
input = output;
}
input
}
pub(crate) fn signed_area(polygon: &[Point]) -> f64 {
if polygon.len() < 3 {
return 0.0;
}
let mut accumulator = 0.0;
let vertex_count = polygon.len();
for i in 0..vertex_count {
let current = polygon[i];
let next = polygon[(i + 1) % vertex_count];
accumulator += current[0] * next[1] - next[0] * current[1];
}
accumulator * 0.5
}
#[cfg(test)]
mod tests {
use super::*;
const TOL: f64 = 1e-12;
fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() <= tol * a.abs().max(b.abs()).max(1.0)
}
#[test]
fn signed_area_counter_clockwise_unit_square_is_one() {
let square: [Point; 4] = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
assert!(approx_eq(signed_area(&square), 1.0, TOL));
}
#[test]
fn signed_area_clockwise_unit_square_is_minus_one() {
let square: [Point; 4] = [[0.0, 0.0], [0.0, 1.0], [1.0, 1.0], [1.0, 0.0]];
assert!(approx_eq(signed_area(&square), -1.0, TOL));
}
#[test]
fn signed_area_degenerate_collinear_is_zero() {
let triangle: [Point; 3] = [[0.0, 0.0], [1.0, 0.0], [2.0, 0.0]];
assert!(approx_eq(signed_area(&triangle), 0.0, TOL));
}
#[test]
fn signed_area_empty_or_short_is_zero() {
let empty: [Point; 0] = [];
assert!(approx_eq(signed_area(&empty), 0.0, TOL));
let two: [Point; 2] = [[0.0, 0.0], [1.0, 1.0]];
assert!(approx_eq(signed_area(&two), 0.0, TOL));
}
#[test]
fn signed_area_known_triangle() {
let triangle: [Point; 3] = [[0.0, 0.0], [4.0, 0.0], [0.0, 3.0]];
assert!(approx_eq(signed_area(&triangle), 6.0, TOL));
}
#[test]
fn clip_subject_completely_inside_cell_returns_subject() {
let quad: [Point; 4] = [
[0.1, 0.1],
[0.7, 0.2],
[0.8, 0.9],
[0.2, 0.8],
];
let clipped = clip_quad_against_unit_cell(&quad, (0, 0));
assert_eq!(clipped.len(), 4);
assert!(approx_eq(signed_area(&clipped).abs(), signed_area(&quad).abs(), TOL));
}
#[test]
fn clip_subject_completely_disjoint_returns_empty() {
let quad: [Point; 4] = [
[2.0, 2.0],
[3.0, 2.0],
[3.0, 3.0],
[2.0, 3.0],
];
let clipped = clip_quad_against_unit_cell(&quad, (0, 0));
assert!(clipped.is_empty());
assert!(approx_eq(signed_area(&clipped).abs(), 0.0, TOL));
}
#[test]
fn clip_axis_aligned_partial_overlap() {
let quad: [Point; 4] = [
[0.5, 0.5],
[1.5, 0.5],
[1.5, 1.5],
[0.5, 1.5],
];
let clipped = clip_quad_against_unit_cell(&quad, (0, 0));
let area = signed_area(&clipped).abs();
assert!(approx_eq(area, 0.25, TOL));
}
#[test]
fn clip_cell_entirely_inside_subject_returns_cell() {
let quad: [Point; 4] = [
[-5.0, -5.0],
[5.0, -5.0],
[5.0, 5.0],
[-5.0, 5.0],
];
let clipped = clip_quad_against_unit_cell(&quad, (0, 0));
let area = signed_area(&clipped).abs();
assert!(approx_eq(area, 1.0, TOL));
}
#[test]
fn clip_one_corner_inside() {
let quad: [Point; 4] = [
[-0.5, -0.5],
[0.5, -0.5],
[0.5, 0.5],
[-0.5, 0.5],
];
let clipped = clip_quad_against_unit_cell(&quad, (0, 0));
let area = signed_area(&clipped).abs();
assert!(approx_eq(area, 0.25, TOL));
}
#[test]
fn clip_rotated_quad_through_cell() {
let quad: [Point; 4] = [
[0.5, 0.0],
[1.0, 0.5],
[0.5, 1.0],
[0.0, 0.5],
];
let clipped = clip_quad_against_unit_cell(&quad, (0, 0));
let area = signed_area(&clipped).abs();
assert!(approx_eq(area, 0.5, TOL));
}
#[test]
fn clip_degenerate_quad_has_zero_area() {
let quad: [Point; 4] = [
[0.0, 0.0],
[0.4, 0.0],
[0.8, 0.0],
[0.2, 0.0],
];
let clipped = clip_quad_against_unit_cell(&quad, (0, 0));
let area = signed_area(&clipped).abs();
assert!(approx_eq(area, 0.0, TOL));
}
#[test]
fn clip_against_offset_cell() {
let quad: [Point; 4] = [
[3.5, 5.5],
[4.5, 5.5],
[4.5, 6.5],
[3.5, 6.5],
];
let clipped = clip_quad_against_unit_cell(&quad, (3, 5));
let area = signed_area(&clipped).abs();
assert!(approx_eq(area, 0.25, TOL));
}
#[test]
fn clip_clockwise_subject_returns_same_magnitude() {
let quad: [Point; 4] = [
[0.5, 0.5],
[0.5, 1.5],
[1.5, 1.5],
[1.5, 0.5],
];
let clipped = clip_quad_against_unit_cell(&quad, (0, 0));
let area = signed_area(&clipped).abs();
assert!(approx_eq(area, 0.25, TOL));
}
}