use log::trace;
use nalgebra::{Matrix3, Point2, Vector2, Vector3};
use crate::ToPos2;
use crate::geometry::transform::GerberTransform;
#[derive(Debug, Clone, PartialEq, PartialOrd)]
pub struct BoundingBox {
pub min: Point2<f64>,
pub max: Point2<f64>,
}
impl BoundingBox {
pub fn transform_vertices(&self, transform: &GerberTransform) -> Vec<Point2<f64>> {
self.vertices()
.into_iter()
.map(|v| transform.apply_to_position(v))
.collect::<Vec<_>>()
}
pub fn expand(&mut self, other: &BoundingBox) {
self.min.x = self.min.x.min(other.min.x);
self.min.y = self.min.y.min(other.min.y);
self.max.x = self.max.x.max(other.max.x);
self.max.y = self.max.y.max(other.max.y);
}
}
impl Default for BoundingBox {
fn default() -> Self {
Self {
min: Point2::new(f64::MAX, f64::MAX),
max: Point2::new(f64::MIN, f64::MIN),
}
}
}
impl BoundingBox {
pub fn is_empty(&self) -> bool {
self.eq(&BoundingBox::default())
}
pub fn width(&self) -> f64 {
self.max.x - self.min.x
}
pub fn height(&self) -> f64 {
self.max.y - self.min.y
}
pub fn apply_transform_matrix(&self, matrix: &Matrix3<f64>) -> Self {
let transformed_bbox_vertices: Vec<_> = self
.vertices()
.into_iter()
.map(|v| {
let point_vec = Vector3::new(v.x, v.y, 1.0);
let transformed = matrix * point_vec;
Point2::new(transformed.x, transformed.y)
})
.collect();
let result = BoundingBox::from_points(&transformed_bbox_vertices);
trace!(
"Applying transform matrix to bbox. matrix {:?}: before: {:?}, after: {:?}",
matrix, self, result
);
result
}
pub fn apply_transform(&self, transform: &GerberTransform) -> Self {
let transformed_bbox_vertices: Vec<_> = self
.vertices()
.into_iter()
.map(|v| transform.apply_to_position(v))
.collect();
let result = BoundingBox::from_points(&transformed_bbox_vertices);
trace!(
"Applying transform to bbox. transform {:?}: before: {:?}, after: {:?}",
transform, self, result
);
result
}
pub fn apply_mirroring(&self, mirror_x: bool, mirror_y: bool, offset: Vector2<f64>) -> Self {
let mut vertices = self.vertices();
for position in &mut vertices {
if mirror_x {
position.x = offset.x - (position.x - offset.x);
}
if mirror_y {
position.y = offset.y - (position.y - offset.y);
}
}
Self::from_points(&vertices)
}
pub fn apply_rotation(&self, radians: f64, offset: Vector2<f64>) -> Self {
let (sin_theta, cos_theta) = radians.sin_cos();
let mut corners = self.vertices();
for pt in &mut corners {
let x = pt.x - offset.x;
let y = pt.y - offset.y;
let rotated_x = x * cos_theta - y * sin_theta;
let rotated_y = x * sin_theta + y * cos_theta;
pt.x = rotated_x + offset.x;
pt.y = rotated_y + offset.y;
}
Self::from_points(&corners)
}
pub fn center(&self) -> Point2<f64> {
Point2::new(self.min.x + self.max.x, self.min.y + self.max.y) / 2.0
}
pub fn vertices(&self) -> Vec<Point2<f64>> {
vec![
Point2::new(self.min.x, self.min.y),
Point2::new(self.max.x, self.min.y),
Point2::new(self.max.x, self.max.y),
Point2::new(self.min.x, self.max.y),
]
}
pub fn from_points(points: &[Point2<f64>]) -> Self {
let mut min = Point2::new(f64::MAX, f64::MAX);
let mut max = Point2::new(f64::MIN, f64::MIN);
for position in points {
min.x = min.x.min(position.x);
min.y = min.y.min(position.y);
max.x = max.x.max(position.x);
max.y = max.y.max(position.y);
}
Self {
min,
max,
}
}
}
#[cfg(feature = "egui")]
impl From<BoundingBox> for egui::Rect {
fn from(value: BoundingBox) -> Self {
Self {
min: value.min.to_pos2(),
max: value.max.to_pos2(),
}
}
}
#[cfg(test)]
mod bbox_tests {
use nalgebra::{Point2, Vector2};
use rstest::rstest;
use crate::geometry::bounding_box::BoundingBox;
#[rstest]
#[case(BoundingBox::default(), true)]
#[case(BoundingBox { min: Point2::new(0.0, 0.0), max: Point2::new(0.0, 0.0) }, false)]
#[case(BoundingBox { min: Point2::new(-10.0, -10.0), max: Point2::new(10.0, 10.0) }, false)]
pub fn test_is_empty(#[case] input: BoundingBox, #[case] expected: bool) {
assert_eq!(input.is_empty(), expected);
}
#[test]
pub fn test_apply_rotation_90_degrees_zero_offset() {
let bbox = BoundingBox {
min: Point2::new(1.0, 2.0),
max: Point2::new(3.0, 4.0),
};
let rotated = bbox.apply_rotation(std::f64::consts::FRAC_PI_2, Vector2::new(0.0, 0.0));
assert!((rotated.min.x - -4.0).abs() < 1e-6);
assert!((rotated.max.x - -2.0).abs() < 1e-6);
assert!((rotated.min.y - 1.0).abs() < 1e-6);
assert!((rotated.max.y - 3.0).abs() < 1e-6);
}
#[rstest]
#[case((0.0, 0.0), (10.0, 10.0), (5.0, 5.0))] #[case((10.0, 10.0), (10.0, 10.0), (15.0, 15.0))] #[case((0.0, 0.0), (5.0, 10.0), (2.5, 5.0))] #[case((0.0, 0.0), (10.0, 5.0), (5.0, 2.5))] #[case((10.0, 10.0), (5.0, 10.0), (12.5, 15.0))] #[case((10.0, 10.0), (10.0, 5.0), (15.0, 12.5))] fn test_geometric_center(#[case] origin: (f64, f64), #[case] size: (f64, f64), #[case] expected: (f64, f64)) {
let bbox = BoundingBox {
min: Point2::new(origin.0, origin.1),
max: Point2::new(origin.0 + size.0, origin.1 + size.1),
};
let center = bbox.center();
let epsilon = 1e-9;
assert!(
(center.x - expected.0).abs() < epsilon,
"X mismatch: expected {}, got {}",
expected.0,
center.x
);
assert!(
(center.y - expected.1).abs() < epsilon,
"Y mismatch: expected {}, got {}",
expected.1,
center.y
);
}
}