1use geo::{Contains, Coord, LineString, Point, Polygon as GeoPolygon};
4use u_nesting_core::geometry::{Boundary, Boundary2DExt};
5use u_nesting_core::transform::AABB2D;
6use u_nesting_core::{Error, Result};
7
8#[cfg(feature = "serde")]
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone)]
13#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
14pub struct Boundary2D {
15 exterior: Vec<(f64, f64)>,
17
18 holes: Vec<Vec<(f64, f64)>>,
20
21 width: Option<f64>,
23
24 height: Option<f64>,
26
27 infinite_length: bool,
29
30 #[cfg_attr(feature = "serde", serde(skip))]
32 cached_area: Option<f64>,
33}
34
35impl Boundary2D {
36 pub fn new(vertices: Vec<(f64, f64)>) -> Self {
38 Self {
39 exterior: vertices,
40 holes: Vec::new(),
41 width: None,
42 height: None,
43 infinite_length: false,
44 cached_area: None,
45 }
46 }
47
48 pub fn rectangle(width: f64, height: f64) -> Self {
50 Self {
51 exterior: vec![(0.0, 0.0), (width, 0.0), (width, height), (0.0, height)],
52 holes: Vec::new(),
53 width: Some(width),
54 height: Some(height),
55 infinite_length: false,
56 cached_area: Some(width * height),
57 }
58 }
59
60 pub fn strip(width: f64) -> Self {
62 Self {
63 exterior: vec![(0.0, 0.0), (width, 0.0), (width, f64::MAX), (0.0, f64::MAX)],
64 holes: Vec::new(),
65 width: Some(width),
66 height: None,
67 infinite_length: true,
68 cached_area: None,
69 }
70 }
71
72 pub fn with_hole(mut self, vertices: Vec<(f64, f64)>) -> Self {
74 self.holes.push(vertices);
75 self.cached_area = None;
76 self
77 }
78
79 pub fn width(&self) -> Option<f64> {
81 self.width
82 }
83
84 pub fn height(&self) -> Option<f64> {
86 self.height
87 }
88
89 pub fn is_infinite(&self) -> bool {
91 self.infinite_length
92 }
93
94 pub fn exterior(&self) -> &[(f64, f64)] {
96 &self.exterior
97 }
98
99 pub fn holes(&self) -> &[Vec<(f64, f64)>] {
101 &self.holes
102 }
103
104 pub fn to_geo_polygon(&self) -> GeoPolygon<f64> {
106 let exterior = LineString::from(
107 self.exterior
108 .iter()
109 .map(|&(x, y)| Coord { x, y })
110 .collect::<Vec<_>>(),
111 );
112
113 let holes: Vec<LineString<f64>> = self
114 .holes
115 .iter()
116 .map(|hole| {
117 LineString::from(
118 hole.iter()
119 .map(|&(x, y)| Coord { x, y })
120 .collect::<Vec<_>>(),
121 )
122 })
123 .collect();
124
125 GeoPolygon::new(exterior, holes)
126 }
127
128 fn calculate_area(&self) -> f64 {
130 if self.infinite_length {
131 return f64::INFINITY;
132 }
133 use geo::Area;
134 self.to_geo_polygon().unsigned_area()
135 }
136}
137
138impl Boundary for Boundary2D {
139 type Scalar = f64;
140
141 fn measure(&self) -> f64 {
142 if let Some(area) = self.cached_area {
143 area
144 } else {
145 self.calculate_area()
146 }
147 }
148
149 fn aabb(&self) -> ([f64; 2], [f64; 2]) {
150 let (min, max) = self.aabb_vec();
151 ([min[0], min[1]], [max[0], max[1]])
152 }
153
154 fn aabb_vec(&self) -> (Vec<f64>, Vec<f64>) {
155 if self.exterior.is_empty() {
156 return (vec![0.0, 0.0], vec![0.0, 0.0]);
157 }
158
159 let mut min_x = f64::MAX;
160 let mut min_y = f64::MAX;
161 let mut max_x = f64::MIN;
162 let mut max_y = f64::MIN;
163
164 for &(x, y) in &self.exterior {
165 if y < f64::MAX / 2.0 {
166 min_x = min_x.min(x);
168 min_y = min_y.min(y);
169 max_x = max_x.max(x);
170 max_y = max_y.max(y);
171 }
172 }
173
174 (vec![min_x, min_y], vec![max_x, max_y])
175 }
176
177 fn validate(&self) -> Result<()> {
178 if self.exterior.len() < 3 {
179 return Err(Error::InvalidBoundary(
180 "Boundary must have at least 3 vertices".into(),
181 ));
182 }
183
184 if let (Some(w), Some(h)) = (self.width, self.height) {
185 if w <= 0.0 || h <= 0.0 {
186 return Err(Error::InvalidBoundary(
187 "Width and height must be positive".into(),
188 ));
189 }
190 }
191
192 Ok(())
193 }
194
195 fn contains_point(&self, point: &[f64]) -> bool {
196 if point.len() < 2 {
197 return false;
198 }
199 let p = Point::new(point[0], point[1]);
200 let poly = self.to_geo_polygon();
201 poly.contains(&p)
202 }
203}
204
205impl Boundary2DExt for Boundary2D {
206 fn aabb_2d(&self) -> AABB2D<f64> {
207 let (min, max) = self.aabb_vec();
208 AABB2D::new(min[0], min[1], max[0], max[1])
209 }
210
211 fn vertices(&self) -> &[(f64, f64)] {
212 &self.exterior
213 }
214
215 fn contains_polygon(&self, polygon: &[(f64, f64)]) -> bool {
216 let boundary_poly = self.to_geo_polygon();
217
218 for &(x, y) in polygon {
220 let p = Point::new(x, y);
221 if !boundary_poly.contains(&p) {
222 return false;
223 }
224 }
225
226 true
229 }
230
231 fn effective_area(&self, margin: f64) -> f64 {
232 if self.infinite_length {
233 return f64::INFINITY;
234 }
235
236 if let (Some(w), Some(h)) = (self.width, self.height) {
237 let eff_w = (w - 2.0 * margin).max(0.0);
238 let eff_h = (h - 2.0 * margin).max(0.0);
239 eff_w * eff_h
240 } else {
241 use geo::{Euclidean, Length};
244 let perim = self.to_geo_polygon().exterior().length::<Euclidean>();
245 (self.calculate_area() - perim * margin).max(0.0)
246 }
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253 use approx::assert_relative_eq;
254
255 #[test]
256 fn test_rectangle_boundary() {
257 let boundary = Boundary2D::rectangle(100.0, 50.0);
258 assert_relative_eq!(boundary.measure(), 5000.0, epsilon = 0.001);
259 assert_eq!(boundary.width(), Some(100.0));
260 assert_eq!(boundary.height(), Some(50.0));
261 }
262
263 #[test]
264 fn test_strip_boundary() {
265 let boundary = Boundary2D::strip(100.0);
266 assert!(boundary.is_infinite());
267 assert_eq!(boundary.width(), Some(100.0));
268 assert_eq!(boundary.height(), None);
269 }
270
271 #[test]
272 fn test_contains_point() {
273 let boundary = Boundary2D::rectangle(100.0, 100.0);
274 assert!(boundary.contains_point(&[50.0, 50.0]));
275 assert!(!boundary.contains_point(&[150.0, 50.0]));
276 assert!(!boundary.contains_point(&[-10.0, 50.0]));
277 }
278
279 #[test]
280 fn test_effective_area() {
281 let boundary = Boundary2D::rectangle(100.0, 100.0);
282 let eff = boundary.effective_area(10.0);
283 assert_relative_eq!(eff, 6400.0, epsilon = 0.001);
285 }
286
287 #[test]
288 fn test_aabb_2d() {
289 let boundary = Boundary2D::rectangle(100.0, 50.0);
290 let aabb = boundary.aabb_2d();
291 assert_relative_eq!(aabb.min_x, 0.0);
292 assert_relative_eq!(aabb.min_y, 0.0);
293 assert_relative_eq!(aabb.max_x, 100.0);
294 assert_relative_eq!(aabb.max_y, 50.0);
295 }
296
297 #[test]
298 fn test_validation() {
299 let valid = Boundary2D::rectangle(100.0, 50.0);
300 assert!(valid.validate().is_ok());
301
302 let invalid = Boundary2D::new(vec![(0.0, 0.0), (1.0, 0.0)]);
303 assert!(invalid.validate().is_err());
304 }
305}