use std::cmp::Ordering;
use crate::vectors::Vec2;
pub type Polygon = Vec<Vec2>;
pub fn offset(mut polygon: Polygon, value: f32) -> Polygon {
polygon = sort_ccw(polygon);
let n_verts = polygon.len();
let mut offset_polygon = Vec::with_capacity(n_verts);
for i in 0..n_verts {
let prev = polygon[(i + n_verts - 1) % n_verts];
let pi = polygon[i];
let next = polygon[(i + 1) % n_verts];
let l1 = (pi - prev).normalize();
let l2 = (next - pi).normalize();
let n1 = Vec2 { x: l1.y, y: -l1.x };
let n2 = Vec2 { x: l2.y, y: -l2.x };
let bisector = (n1 + n2).normalize();
let dot = (l1 * -1.).dot(&l2).max(-1.).min(1.);
let theta = f32::acos(dot);
let offset_length = value / f32::sin(theta / 2.);
let new_pi = pi + bisector * offset_length;
offset_polygon.push(new_pi);
}
offset_polygon
}
fn sort_ccw(polygon: Polygon) -> Polygon {
if polygon.len() < 3 {
return polygon;
}
let winding = find_winding_order(&polygon);
if winding.is_ge() {
polygon
} else {
let mut out = polygon.clone();
out.reverse();
out
}
}
fn find_winding_order(polygon: &Polygon) -> Ordering {
find_signed_area(polygon).total_cmp(&0.)
}
fn find_signed_area(polygon: &Polygon) -> f32 {
let n_verts = polygon.len();
let mut area = 0.;
for i in 0..n_verts {
let current = polygon[i];
let next = polygon[(i + 1) % n_verts];
area += (current.x * next.y) - (next.x * current.y);
}
area / 2.
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vectors::Vec2;
#[test]
fn test_sort_ccw_two_points_polygon() {
let line = vec![Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 1.0, y: 0.0 }];
let sorted = sort_ccw(line.clone());
assert_eq!(sorted, line);
}
#[test]
fn test_sort_ccw_ccw_polygon() {
let ccw_polygon = vec![
Vec2 { x: 0.0, y: 0.0 },
Vec2 { x: 1.0, y: 0.0 },
Vec2 { x: 0.0, y: 1.0 },
];
let sorted = sort_ccw(ccw_polygon.clone());
assert_eq!(sorted, ccw_polygon);
}
#[test]
fn test_sort_ccw_cw_polygon() {
let cw_polygon = vec![
Vec2 { x: 0.0, y: 1.0 },
Vec2 { x: 1.0, y: 0.0 },
Vec2 { x: 0.0, y: 0.0 },
];
let sorted = sort_ccw(cw_polygon.clone());
let mut expected = cw_polygon;
expected.reverse();
assert_eq!(sorted, expected);
}
#[test]
fn test_sort_ccw_colinear_polygon() {
let colinear = vec![
Vec2 { x: 0.0, y: 0.0 },
Vec2 { x: 1.0, y: 1.0 },
Vec2 { x: 2.0, y: 2.0 },
];
let sorted = sort_ccw(colinear.clone());
assert_eq!(sorted, colinear);
}
#[test]
fn test_find_signed_area() {
let polygon = vec![
Vec2 { x: 0.0, y: 0.0 },
Vec2 { x: 1.0, y: 0.0 },
Vec2 { x: 0.0, y: 1.0 },
];
let area = find_signed_area(&polygon);
assert_eq!(area, 0.5);
}
#[test]
fn test_find_signed_area_negative() {
let polygon = vec![
Vec2 { x: 0.0, y: 1.0 },
Vec2 { x: 1.0, y: 0.0 },
Vec2 { x: 0.0, y: 0.0 },
];
let area = find_signed_area(&polygon);
assert_eq!(area, -0.5);
}
#[test]
fn test_find_signed_area_colinear() {
let polygon = vec![
Vec2 { x: 0.0, y: 0.0 },
Vec2 { x: 1.0, y: 1.0 },
Vec2 { x: 2.0, y: 2.0 },
];
let area = find_signed_area(&polygon);
assert_eq!(area, 0.0);
}
#[test]
fn test_offset_polygon() {
let polygon = vec![
Vec2 { x: 0.0, y: 0.0 },
Vec2 { x: 1.0, y: 0.0 },
Vec2 { x: 0.0, y: 1.0 },
];
let offset_value = 0.1;
let offset_polygon = offset(polygon, offset_value);
assert_eq!(offset_polygon.len(), 3);
assert!(offset_polygon[0].x < 0. && offset_polygon[0].y < 0.);
assert!(offset_polygon[1].x > 1. && offset_polygon[1].y < 0.);
assert!(offset_polygon[2].x < 0. && offset_polygon[2].y > 1.);
}
#[test]
fn test_offset_polygon_negative() {
let polygon = vec![
Vec2 { x: 0.0, y: 0.0 },
Vec2 { x: 1.0, y: 0.0 },
Vec2 { x: 0.0, y: 1.0 },
];
let offset_value = -0.1;
let offset_polygon = offset(polygon, offset_value);
assert_eq!(offset_polygon.len(), 3);
assert!(offset_polygon[0].x > 0. && offset_polygon[0].y > 0.);
assert!(offset_polygon[1].x < 1. && offset_polygon[1].y > 0.);
assert!(offset_polygon[2].x > 0. && offset_polygon[2].y < 1.);
}
}