use starsight_layer_1::primitives::{Point, Rect};
#[derive(Debug, Clone)]
pub enum MarkExtent {
Bbox(Rect),
Segments(Vec<(Point, Point)>),
Rects(Vec<Rect>),
Polygons(Vec<Vec<Point>>),
}
impl MarkExtent {
#[must_use]
pub fn intersects(&self, candidate: &Rect) -> bool {
match self {
Self::Bbox(b) => b.intersection(candidate).is_some(),
Self::Segments(segs) => segs
.iter()
.any(|(a, b)| segment_intersects_rect(*a, *b, candidate)),
Self::Rects(rs) => rs.iter().any(|r| r.intersection(candidate).is_some()),
Self::Polygons(ps) => ps.iter().any(|p| polygon_intersects_rect(p, candidate)),
}
}
#[must_use]
pub fn overlap_area(&self, candidate: &Rect) -> f32 {
match self {
Self::Bbox(b) => rect_clip_area(b, candidate),
Self::Segments(segs) => {
segs.iter()
.map(|(a, b)| segment_clip_length(*a, *b, candidate))
.sum::<f32>()
* 2.0
}
Self::Rects(rs) => rs.iter().map(|r| rect_clip_area(r, candidate)).sum(),
Self::Polygons(ps) => ps.iter().map(|p| polygon_clip_area(p, candidate)).sum(),
}
}
}
fn rect_clip_area(r: &Rect, c: &Rect) -> f32 {
r.intersection(c)
.map_or(0.0, |clip| clip.width() * clip.height())
}
fn segment_intersects_rect(start: Point, end: Point, rect: &Rect) -> bool {
liang_barsky(start, end, rect).is_some()
}
fn segment_clip_length(start: Point, end: Point, rect: &Rect) -> f32 {
if let Some((t_min, t_max)) = liang_barsky(start, end, rect) {
let dx = (end.x - start.x) * (t_max - t_min);
let dy = (end.y - start.y) * (t_max - t_min);
(dx * dx + dy * dy).sqrt()
} else {
0.0
}
}
fn liang_barsky(start: Point, end: Point, rect: &Rect) -> Option<(f32, f32)> {
let dx = end.x - start.x;
let dy = end.y - start.y;
let denom = [-dx, dx, -dy, dy];
let dist = [
start.x - rect.left,
rect.right - start.x,
start.y - rect.top,
rect.bottom - start.y,
];
let mut t_min = 0.0_f32;
let mut t_max = 1.0_f32;
for i in 0..4 {
if denom[i].abs() < f32::EPSILON {
if dist[i] < 0.0 {
return None;
}
} else {
let t = dist[i] / denom[i];
if denom[i] < 0.0 {
if t > t_max {
return None;
}
if t > t_min {
t_min = t;
}
} else {
if t < t_min {
return None;
}
if t < t_max {
t_max = t;
}
}
}
}
Some((t_min, t_max))
}
fn sutherland_hodgman(poly: &[Point], rect: &Rect) -> Vec<Point> {
if poly.is_empty() {
return Vec::new();
}
let mut out: Vec<Point> = poly.to_vec();
let edges: [(Edge, f32); 4] = [
(Edge::Left, rect.left),
(Edge::Right, rect.right),
(Edge::Top, rect.top),
(Edge::Bottom, rect.bottom),
];
for (edge, threshold) in edges {
if out.is_empty() {
return out;
}
let input = std::mem::take(&mut out);
let n = input.len();
for i in 0..n {
let curr = input[i];
let next = input[(i + 1) % n];
let curr_in = inside(curr, edge, threshold);
let next_in = inside(next, edge, threshold);
if curr_in {
out.push(curr);
if !next_in {
out.push(intersect_edge(curr, next, edge, threshold));
}
} else if next_in {
out.push(intersect_edge(curr, next, edge, threshold));
}
}
}
out
}
#[derive(Clone, Copy)]
enum Edge {
Left,
Right,
Top,
Bottom,
}
fn inside(p: Point, edge: Edge, t: f32) -> bool {
match edge {
Edge::Left => p.x >= t,
Edge::Right => p.x <= t,
Edge::Top => p.y >= t,
Edge::Bottom => p.y <= t,
}
}
fn intersect_edge(a: Point, b: Point, edge: Edge, t: f32) -> Point {
match edge {
Edge::Left | Edge::Right => {
let dx = b.x - a.x;
if dx.abs() < f32::EPSILON {
return Point::new(t, a.y);
}
let frac = (t - a.x) / dx;
Point::new(t, a.y + (b.y - a.y) * frac)
}
Edge::Top | Edge::Bottom => {
let dy = b.y - a.y;
if dy.abs() < f32::EPSILON {
return Point::new(a.x, t);
}
let frac = (t - a.y) / dy;
Point::new(a.x + (b.x - a.x) * frac, t)
}
}
}
fn polygon_intersects_rect(poly: &[Point], rect: &Rect) -> bool {
if poly.is_empty() {
return false;
}
let mut min_x = f32::INFINITY;
let mut max_x = f32::NEG_INFINITY;
let mut min_y = f32::INFINITY;
let mut max_y = f32::NEG_INFINITY;
for p in poly {
if p.x < min_x {
min_x = p.x;
}
if p.x > max_x {
max_x = p.x;
}
if p.y < min_y {
min_y = p.y;
}
if p.y > max_y {
max_y = p.y;
}
}
if max_x < rect.left || min_x > rect.right || max_y < rect.top || min_y > rect.bottom {
return false;
}
!sutherland_hodgman(poly, rect).is_empty()
}
fn polygon_clip_area(poly: &[Point], rect: &Rect) -> f32 {
let clipped = sutherland_hodgman(poly, rect);
polygon_area(&clipped)
}
fn polygon_area(poly: &[Point]) -> f32 {
if poly.len() < 3 {
return 0.0;
}
let n = poly.len();
let mut acc = 0.0_f32;
for i in 0..n {
let a = poly[i];
let b = poly[(i + 1) % n];
acc += a.x * b.y - b.x * a.y;
}
(acc * 0.5).abs()
}
#[cfg(test)]
mod tests {
use super::*;
fn r(left: f32, top: f32, right: f32, bottom: f32) -> Rect {
Rect::new(left, top, right, bottom)
}
fn p(x: f32, y: f32) -> Point {
Point::new(x, y)
}
#[test]
fn bbox_intersects_overlapping() {
let e = MarkExtent::Bbox(r(0.0, 0.0, 10.0, 10.0));
assert!(e.intersects(&r(5.0, 5.0, 15.0, 15.0)));
}
#[test]
fn bbox_does_not_intersect_disjoint() {
let e = MarkExtent::Bbox(r(0.0, 0.0, 10.0, 10.0));
assert!(!e.intersects(&r(20.0, 20.0, 30.0, 30.0)));
}
#[test]
fn bbox_overlap_area_full_containment() {
let e = MarkExtent::Bbox(r(0.0, 0.0, 10.0, 10.0));
let candidate = r(2.0, 2.0, 8.0, 8.0);
assert!((e.overlap_area(&candidate) - 36.0).abs() < 1e-3);
}
#[test]
fn segments_diagonal_misses_off_corners() {
let segs = MarkExtent::Segments(vec![(p(0.0, 10.0), p(10.0, 0.0))]);
assert!(!segs.intersects(&r(-1.0, -1.0, 1.0, 1.0)));
assert!(!segs.intersects(&r(9.0, 9.0, 11.0, 11.0)));
assert!(segs.intersects(&r(4.0, 4.0, 6.0, 6.0)));
}
#[test]
fn segments_overlap_area_clipped_length_times_two() {
let segs = MarkExtent::Segments(vec![(p(0.0, 5.0), p(10.0, 5.0))]);
let candidate = r(3.0, 0.0, 7.0, 10.0);
assert!((segs.overlap_area(&candidate) - 8.0).abs() < 1e-3);
}
#[test]
fn rects_intersection_any() {
let e = MarkExtent::Rects(vec![r(0.0, 0.0, 5.0, 5.0), r(20.0, 0.0, 25.0, 5.0)]);
assert!(e.intersects(&r(22.0, 2.0, 23.0, 3.0)));
assert!(!e.intersects(&r(10.0, 10.0, 15.0, 15.0)));
}
#[test]
fn polygon_triangle_clip_area() {
let triangle = vec![p(0.0, 0.0), p(10.0, 0.0), p(0.0, 10.0)];
let e = MarkExtent::Polygons(vec![triangle]);
assert!((e.overlap_area(&r(0.0, 0.0, 10.0, 10.0)) - 50.0).abs() < 1e-3);
assert!((e.overlap_area(&r(20.0, 20.0, 30.0, 30.0))).abs() < 1e-3);
}
#[test]
fn polygon_intersects_uses_aabb_reject() {
let triangle = vec![p(0.0, 0.0), p(10.0, 0.0), p(0.0, 10.0)];
let e = MarkExtent::Polygons(vec![triangle]);
assert!(!e.intersects(&r(20.0, 20.0, 30.0, 30.0)));
}
}