use u_nesting_core::geom::polygon as geom_polygon;
use u_nesting_core::geometry::{Geometry, Geometry2DExt, GeometryId, RotationConstraint};
use u_nesting_core::transform::AABB2D;
use u_nesting_core::{Error, Result};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Geometry2D {
id: GeometryId,
exterior: Vec<(f64, f64)>,
holes: Vec<Vec<(f64, f64)>>,
quantity: usize,
rotation_constraint: RotationConstraint<f64>,
allow_flip: bool,
priority: i32,
#[cfg_attr(feature = "serde", serde(skip))]
cached_area: Option<f64>,
#[cfg_attr(feature = "serde", serde(skip))]
cached_convex_hull: Option<Vec<(f64, f64)>>,
#[cfg_attr(feature = "serde", serde(skip))]
cached_perimeter: Option<f64>,
#[cfg_attr(feature = "serde", serde(skip))]
cached_is_convex: Option<bool>,
}
impl Geometry2D {
pub fn new(id: impl Into<GeometryId>) -> Self {
Self {
id: id.into(),
exterior: Vec::new(),
holes: Vec::new(),
quantity: 1,
rotation_constraint: RotationConstraint::None,
allow_flip: false,
priority: 0,
cached_area: None,
cached_convex_hull: None,
cached_perimeter: None,
cached_is_convex: None,
}
}
pub fn with_polygon(mut self, vertices: Vec<(f64, f64)>) -> Self {
self.exterior = vertices;
self.clear_cache();
self
}
pub fn with_hole(mut self, vertices: Vec<(f64, f64)>) -> Self {
self.holes.push(vertices);
self.clear_cache();
self
}
pub fn with_quantity(mut self, n: usize) -> Self {
self.quantity = n;
self
}
pub fn with_rotations_deg(mut self, angles: Vec<f64>) -> Self {
let radians: Vec<f64> = angles.into_iter().map(|a| a.to_radians()).collect();
self.rotation_constraint = RotationConstraint::Discrete(radians);
self
}
pub fn with_rotations(mut self, angles: Vec<f64>) -> Self {
self.rotation_constraint = RotationConstraint::Discrete(angles);
self
}
pub fn with_rotation_constraint(mut self, constraint: RotationConstraint<f64>) -> Self {
self.rotation_constraint = constraint;
self
}
pub fn with_flip(mut self, allow: bool) -> Self {
self.allow_flip = allow;
self
}
pub fn with_priority(mut self, priority: i32) -> Self {
self.priority = priority;
self
}
pub fn rectangle(id: impl Into<GeometryId>, width: f64, height: f64) -> Self {
Self::new(id).with_polygon(vec![
(0.0, 0.0),
(width, 0.0),
(width, height),
(0.0, height),
])
}
pub fn circle(id: impl Into<GeometryId>, radius: f64, n: usize) -> Self {
let n = n.max(8);
let step = std::f64::consts::TAU / n as f64;
let vertices: Vec<(f64, f64)> = (0..n)
.map(|i| {
let angle = i as f64 * step;
(radius * angle.cos() + radius, radius * angle.sin() + radius)
})
.collect();
Self::new(id).with_polygon(vertices)
}
pub fn l_shape(
id: impl Into<GeometryId>,
width: f64,
height: f64,
notch_width: f64,
notch_height: f64,
) -> Self {
Self::new(id).with_polygon(vec![
(0.0, 0.0),
(width, 0.0),
(width, notch_height),
(notch_width, notch_height),
(notch_width, height),
(0.0, height),
])
}
pub fn exterior(&self) -> &[(f64, f64)] {
&self.exterior
}
pub fn rotations(&self) -> Vec<f64> {
self.rotation_constraint.angles()
}
pub fn allow_flip(&self) -> bool {
self.allow_flip
}
fn clear_cache(&mut self) {
self.cached_area = None;
self.cached_convex_hull = None;
self.cached_perimeter = None;
self.cached_is_convex = None;
}
fn calculate_area(&self) -> f64 {
let hole_refs: Vec<&[(f64, f64)]> = self.holes.iter().map(|h| h.as_slice()).collect();
geom_polygon::area_with_holes(&self.exterior, &hole_refs)
}
fn calculate_perimeter(&self) -> f64 {
let mut perim = geom_polygon::perimeter(&self.exterior);
for hole in &self.holes {
perim += geom_polygon::perimeter(hole);
}
perim
}
fn calculate_convex_hull(&self) -> Vec<(f64, f64)> {
geom_polygon::convex_hull(&self.exterior)
}
fn calculate_is_convex(&self) -> bool {
if self.exterior.len() < 3 || !self.holes.is_empty() {
return false;
}
let n = self.exterior.len();
let mut sign = 0i32;
for i in 0..n {
let (x1, y1) = self.exterior[i];
let (x2, y2) = self.exterior[(i + 1) % n];
let (x3, y3) = self.exterior[(i + 2) % n];
let cross = (x2 - x1) * (y3 - y2) - (y2 - y1) * (x3 - x2);
if cross.abs() > 1e-10 {
let current_sign = if cross > 0.0 { 1 } else { -1 };
if sign == 0 {
sign = current_sign;
} else if sign != current_sign {
return false;
}
}
}
true
}
}
impl Geometry for Geometry2D {
type Scalar = f64;
fn id(&self) -> &GeometryId {
&self.id
}
fn quantity(&self) -> usize {
self.quantity
}
fn measure(&self) -> f64 {
if let Some(area) = self.cached_area {
area
} else {
self.calculate_area()
}
}
fn aabb(&self) -> ([f64; 2], [f64; 2]) {
let (min, max) = self.aabb_vec();
([min[0], min[1]], [max[0], max[1]])
}
fn aabb_vec(&self) -> (Vec<f64>, Vec<f64>) {
if self.exterior.is_empty() {
return (vec![0.0, 0.0], vec![0.0, 0.0]);
}
let mut min_x = f64::MAX;
let mut min_y = f64::MAX;
let mut max_x = f64::MIN;
let mut max_y = f64::MIN;
for &(x, y) in &self.exterior {
min_x = min_x.min(x);
min_y = min_y.min(y);
max_x = max_x.max(x);
max_y = max_y.max(y);
}
(vec![min_x, min_y], vec![max_x, max_y])
}
fn centroid(&self) -> Vec<f64> {
let hole_refs: Vec<&[(f64, f64)]> = self.holes.iter().map(|h| h.as_slice()).collect();
if let Some((cx, cy)) = geom_polygon::centroid_with_holes(&self.exterior, &hole_refs) {
vec![cx, cy]
} else {
vec![0.0, 0.0]
}
}
fn validate(&self) -> Result<()> {
if self.exterior.len() < 3 {
return Err(Error::InvalidGeometry(format!(
"Polygon '{}' must have at least 3 vertices",
self.id
)));
}
if self.quantity == 0 {
return Err(Error::InvalidGeometry(format!(
"Quantity for '{}' must be at least 1",
self.id
)));
}
Ok(())
}
fn rotation_constraint(&self) -> &RotationConstraint<f64> {
&self.rotation_constraint
}
fn allow_mirror(&self) -> bool {
self.allow_flip
}
fn priority(&self) -> i32 {
self.priority
}
}
impl Geometry2DExt for Geometry2D {
fn aabb_2d(&self) -> AABB2D<f64> {
let (min, max) = self.aabb_vec();
AABB2D::new(min[0], min[1], max[0], max[1])
}
fn outer_ring(&self) -> &[(f64, f64)] {
&self.exterior
}
fn holes(&self) -> &[Vec<(f64, f64)>] {
&self.holes
}
fn is_convex(&self) -> bool {
if let Some(is_convex) = self.cached_is_convex {
is_convex
} else {
self.calculate_is_convex()
}
}
fn convex_hull(&self) -> Vec<(f64, f64)> {
if let Some(ref hull) = self.cached_convex_hull {
hull.clone()
} else {
self.calculate_convex_hull()
}
}
fn perimeter(&self) -> f64 {
if let Some(perim) = self.cached_perimeter {
perim
} else {
self.calculate_perimeter()
}
}
}
impl Geometry2D {
pub fn aabb_at_rotation(&self, rotation: f64) -> ([f64; 2], [f64; 2]) {
if rotation.abs() < 1e-10 {
return self.aabb();
}
let cos_r = rotation.cos();
let sin_r = rotation.sin();
let mut min_x = f64::INFINITY;
let mut min_y = f64::INFINITY;
let mut max_x = f64::NEG_INFINITY;
let mut max_y = f64::NEG_INFINITY;
for &(x, y) in &self.exterior {
let rx = x * cos_r - y * sin_r;
let ry = x * sin_r + y * cos_r;
min_x = min_x.min(rx);
min_y = min_y.min(ry);
max_x = max_x.max(rx);
max_y = max_y.max(ry);
}
([min_x, min_y], [max_x, max_y])
}
pub fn dimensions_at_rotation(&self, rotation: f64) -> (f64, f64) {
let (min, max) = self.aabb_at_rotation(rotation);
(max[0] - min[0], max[1] - min[1])
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_rectangle_area() {
let rect = Geometry2D::rectangle("R1", 10.0, 5.0);
assert_relative_eq!(rect.measure(), 50.0, epsilon = 0.001);
}
#[test]
fn test_polygon_with_hole() {
let poly = Geometry2D::new("P1")
.with_polygon(vec![(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)])
.with_hole(vec![(25.0, 25.0), (75.0, 25.0), (75.0, 75.0), (25.0, 75.0)]);
assert_relative_eq!(poly.measure(), 7500.0, epsilon = 0.001);
}
#[test]
fn test_aabb() {
let poly = Geometry2D::new("P1").with_polygon(vec![
(10.0, 20.0),
(50.0, 20.0),
(50.0, 80.0),
(10.0, 80.0),
]);
let aabb = poly.aabb_2d();
assert_relative_eq!(aabb.min_x, 10.0);
assert_relative_eq!(aabb.min_y, 20.0);
assert_relative_eq!(aabb.max_x, 50.0);
assert_relative_eq!(aabb.max_y, 80.0);
}
#[test]
fn test_rectangle_is_convex() {
let rect = Geometry2D::rectangle("R1", 10.0, 10.0);
assert!(rect.is_convex());
}
#[test]
fn test_l_shape_is_not_convex() {
let l = Geometry2D::l_shape("L1", 20.0, 20.0, 10.0, 10.0);
assert!(!l.is_convex());
}
#[test]
fn test_convex_hull() {
let l = Geometry2D::l_shape("L1", 20.0, 20.0, 10.0, 10.0);
let hull = l.convex_hull();
assert!(hull.len() >= 4);
}
#[test]
fn test_centroid() {
let rect = Geometry2D::rectangle("R1", 10.0, 10.0);
let centroid = rect.centroid();
assert_relative_eq!(centroid[0], 5.0, epsilon = 0.001);
assert_relative_eq!(centroid[1], 5.0, epsilon = 0.001);
}
#[test]
fn test_perimeter() {
let rect = Geometry2D::rectangle("R1", 10.0, 5.0);
assert_relative_eq!(rect.perimeter(), 30.0, epsilon = 0.001);
}
#[test]
fn test_validation() {
let valid = Geometry2D::rectangle("R1", 10.0, 10.0);
assert!(valid.validate().is_ok());
let invalid = Geometry2D::new("P1").with_polygon(vec![(0.0, 0.0), (1.0, 0.0)]);
assert!(invalid.validate().is_err());
}
#[test]
fn test_circle() {
let circle = Geometry2D::circle("C1", 10.0, 32);
let area = circle.measure();
let expected = std::f64::consts::PI * 10.0 * 10.0;
assert_relative_eq!(area, expected, epsilon = 5.0);
}
}