use egui::emath::{Rot2, TSTransform};
use egui::{Pos2, Rect, Vec2, vec2};
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct RotatedRect {
pub(crate) centre: Pos2,
pub(crate) half: Vec2,
pub(crate) angle: f32,
}
impl RotatedRect {
pub(crate) fn corners(&self) -> [Pos2; 4] {
let rotation = Rot2::from_angle(self.angle);
[
vec2(-self.half.x, -self.half.y),
vec2(self.half.x, -self.half.y),
vec2(self.half.x, self.half.y),
vec2(-self.half.x, self.half.y),
]
.map(|corner| self.centre + rotation * corner)
}
pub(crate) fn contains(&self, point: Pos2) -> bool {
let local = Rot2::from_angle(-self.angle) * (point - self.centre);
local.x.abs() <= self.half.x && local.y.abs() <= self.half.y
}
pub(crate) fn intersects(&self, aabb: Rect) -> bool {
let corners = self.corners();
let aabb_corners = [
aabb.left_top(),
aabb.right_top(),
aabb.right_bottom(),
aabb.left_bottom(),
];
if corners.iter().any(|corner| aabb.contains(*corner))
|| aabb_corners.iter().any(|corner| self.contains(*corner))
{
return true;
}
let edges = |points: [Pos2; 4]| (0..4).map(move |i| (points[i], points[(i + 1) % 4]));
edges(corners)
.any(|(p1, p2)| edges(aabb_corners).any(|(q1, q2)| segments_intersect(p1, p2, q1, q2)))
}
pub(crate) fn bounds(&self) -> Rect {
Rect::from_points(&self.corners())
}
pub(crate) fn transformed(&self, transform: TSTransform) -> Self {
Self {
centre: transform * self.centre,
half: self.half * transform.scaling,
angle: self.angle,
}
}
pub(crate) fn along_axis(&self, t: f32) -> Pos2 {
self.centre + Rot2::from_angle(self.angle) * vec2((2.0 * t - 1.0) * self.half.x, 0.0)
}
}
fn segments_intersect(p1: Pos2, p2: Pos2, q1: Pos2, q2: Pos2) -> bool {
let cross = |a: Vec2, b: Vec2| a.x * b.y - a.y * b.x;
let r = p2 - p1;
let s = q2 - q1;
let r_cross_s = cross(r, s);
if r_cross_s.abs() < 1e-6 {
return false;
}
let q_minus_p = q1 - p1;
let t = cross(q_minus_p, s) / r_cross_s;
let u = cross(q_minus_p, r) / r_cross_s;
(0.0..=1.0).contains(&t) && (0.0..=1.0).contains(&u)
}
#[cfg(test)]
mod tests {
use super::*;
use egui::pos2;
use std::f32::consts::FRAC_PI_4;
fn diamond() -> RotatedRect {
RotatedRect {
centre: pos2(0.0, 0.0),
half: vec2(10.0, 10.0),
angle: FRAC_PI_4,
}
}
#[test]
fn a_turned_rectangle_contains_what_it_covers() {
assert!(diamond().contains(pos2(13.0, 0.0)));
assert!(
!diamond().contains(pos2(9.0, 9.0)),
"beside the turned edge"
);
}
#[test]
fn a_box_that_only_crosses_the_edges_intersects() {
let band = Rect::from_min_max(pos2(-20.0, -1.0), pos2(20.0, 1.0));
assert!(diamond().intersects(band));
let beside = Rect::from_min_max(pos2(9.0, 9.0), pos2(20.0, 20.0));
assert!(!diamond().intersects(beside));
}
#[test]
fn its_axis_runs_from_edge_to_edge() {
let bar = RotatedRect {
centre: pos2(50.0, 0.0),
half: vec2(50.0, 2.0),
angle: 0.0,
};
assert_eq!(bar.along_axis(0.0), pos2(0.0, 0.0));
assert_eq!(bar.along_axis(1.0), pos2(100.0, 0.0));
}
#[test]
fn a_transform_carries_the_whole_rectangle() {
let screen = diamond().transformed(TSTransform::new(vec2(5.0, 5.0), 2.0));
assert_eq!(screen.centre, pos2(5.0, 5.0));
assert_eq!(screen.half, vec2(20.0, 20.0));
let corners = diamond()
.corners()
.map(|corner| corner * 2.0 + vec2(5.0, 5.0));
for (a, b) in screen.corners().iter().zip(corners) {
assert!((*a - b).length() < 1e-4);
}
}
}