1use u_nesting_core::geom::polygon as geom_polygon;
4use u_nesting_core::geometry::{Geometry, Geometry2DExt, GeometryId, RotationConstraint};
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 Geometry2D {
15 id: GeometryId,
17
18 exterior: Vec<(f64, f64)>,
20
21 holes: Vec<Vec<(f64, f64)>>,
23
24 quantity: usize,
26
27 rotation_constraint: RotationConstraint<f64>,
29
30 allow_flip: bool,
32
33 priority: i32,
35
36 #[cfg_attr(feature = "serde", serde(skip))]
38 cached_area: Option<f64>,
39
40 #[cfg_attr(feature = "serde", serde(skip))]
42 cached_convex_hull: Option<Vec<(f64, f64)>>,
43
44 #[cfg_attr(feature = "serde", serde(skip))]
46 cached_perimeter: Option<f64>,
47
48 #[cfg_attr(feature = "serde", serde(skip))]
50 cached_is_convex: Option<bool>,
51}
52
53impl Geometry2D {
54 pub fn new(id: impl Into<GeometryId>) -> Self {
56 Self {
57 id: id.into(),
58 exterior: Vec::new(),
59 holes: Vec::new(),
60 quantity: 1,
61 rotation_constraint: RotationConstraint::None,
62 allow_flip: false,
63 priority: 0,
64 cached_area: None,
65 cached_convex_hull: None,
66 cached_perimeter: None,
67 cached_is_convex: None,
68 }
69 }
70
71 pub fn with_polygon(mut self, vertices: Vec<(f64, f64)>) -> Self {
73 self.exterior = vertices;
74 self.clear_cache();
75 self
76 }
77
78 pub fn with_hole(mut self, vertices: Vec<(f64, f64)>) -> Self {
80 self.holes.push(vertices);
81 self.clear_cache();
82 self
83 }
84
85 pub fn with_quantity(mut self, n: usize) -> Self {
87 self.quantity = n;
88 self
89 }
90
91 pub fn with_rotations_deg(mut self, angles: Vec<f64>) -> Self {
93 let radians: Vec<f64> = angles.into_iter().map(|a| a.to_radians()).collect();
94 self.rotation_constraint = RotationConstraint::Discrete(radians);
95 self
96 }
97
98 pub fn with_rotations(mut self, angles: Vec<f64>) -> Self {
100 self.rotation_constraint = RotationConstraint::Discrete(angles);
101 self
102 }
103
104 pub fn with_rotation_constraint(mut self, constraint: RotationConstraint<f64>) -> Self {
106 self.rotation_constraint = constraint;
107 self
108 }
109
110 pub fn with_flip(mut self, allow: bool) -> Self {
112 self.allow_flip = allow;
113 self
114 }
115
116 pub fn with_priority(mut self, priority: i32) -> Self {
118 self.priority = priority;
119 self
120 }
121
122 pub fn rectangle(id: impl Into<GeometryId>, width: f64, height: f64) -> Self {
124 Self::new(id).with_polygon(vec![
125 (0.0, 0.0),
126 (width, 0.0),
127 (width, height),
128 (0.0, height),
129 ])
130 }
131
132 pub fn circle(id: impl Into<GeometryId>, radius: f64, n: usize) -> Self {
134 let n = n.max(8);
135 let step = std::f64::consts::TAU / n as f64;
136 let vertices: Vec<(f64, f64)> = (0..n)
137 .map(|i| {
138 let angle = i as f64 * step;
139 (radius * angle.cos() + radius, radius * angle.sin() + radius)
140 })
141 .collect();
142 Self::new(id).with_polygon(vertices)
143 }
144
145 pub fn l_shape(
147 id: impl Into<GeometryId>,
148 width: f64,
149 height: f64,
150 notch_width: f64,
151 notch_height: f64,
152 ) -> Self {
153 Self::new(id).with_polygon(vec![
154 (0.0, 0.0),
155 (width, 0.0),
156 (width, notch_height),
157 (notch_width, notch_height),
158 (notch_width, height),
159 (0.0, height),
160 ])
161 }
162
163 pub fn exterior(&self) -> &[(f64, f64)] {
165 &self.exterior
166 }
167
168 pub fn rotations(&self) -> Vec<f64> {
170 self.rotation_constraint.angles()
171 }
172
173 pub fn allow_flip(&self) -> bool {
175 self.allow_flip
176 }
177
178 fn clear_cache(&mut self) {
180 self.cached_area = None;
181 self.cached_convex_hull = None;
182 self.cached_perimeter = None;
183 self.cached_is_convex = None;
184 }
185
186 fn calculate_area(&self) -> f64 {
188 let hole_refs: Vec<&[(f64, f64)]> = self.holes.iter().map(|h| h.as_slice()).collect();
189 geom_polygon::area_with_holes(&self.exterior, &hole_refs)
190 }
191
192 fn calculate_perimeter(&self) -> f64 {
194 let mut perim = geom_polygon::perimeter(&self.exterior);
195 for hole in &self.holes {
196 perim += geom_polygon::perimeter(hole);
197 }
198 perim
199 }
200
201 fn calculate_convex_hull(&self) -> Vec<(f64, f64)> {
203 geom_polygon::convex_hull(&self.exterior)
204 }
205
206 fn calculate_is_convex(&self) -> bool {
208 if self.exterior.len() < 3 || !self.holes.is_empty() {
209 return false;
210 }
211
212 let n = self.exterior.len();
215 let mut sign = 0i32;
216
217 for i in 0..n {
218 let (x1, y1) = self.exterior[i];
219 let (x2, y2) = self.exterior[(i + 1) % n];
220 let (x3, y3) = self.exterior[(i + 2) % n];
221
222 let cross = (x2 - x1) * (y3 - y2) - (y2 - y1) * (x3 - x2);
223
224 if cross.abs() > 1e-10 {
225 let current_sign = if cross > 0.0 { 1 } else { -1 };
226 if sign == 0 {
227 sign = current_sign;
228 } else if sign != current_sign {
229 return false;
230 }
231 }
232 }
233
234 true
235 }
236}
237
238impl Geometry for Geometry2D {
239 type Scalar = f64;
240
241 fn id(&self) -> &GeometryId {
242 &self.id
243 }
244
245 fn quantity(&self) -> usize {
246 self.quantity
247 }
248
249 fn measure(&self) -> f64 {
250 if let Some(area) = self.cached_area {
251 area
252 } else {
253 self.calculate_area()
254 }
255 }
256
257 fn aabb(&self) -> ([f64; 2], [f64; 2]) {
258 let (min, max) = self.aabb_vec();
259 ([min[0], min[1]], [max[0], max[1]])
260 }
261
262 fn aabb_vec(&self) -> (Vec<f64>, Vec<f64>) {
263 if self.exterior.is_empty() {
264 return (vec![0.0, 0.0], vec![0.0, 0.0]);
265 }
266
267 let mut min_x = f64::MAX;
268 let mut min_y = f64::MAX;
269 let mut max_x = f64::MIN;
270 let mut max_y = f64::MIN;
271
272 for &(x, y) in &self.exterior {
273 min_x = min_x.min(x);
274 min_y = min_y.min(y);
275 max_x = max_x.max(x);
276 max_y = max_y.max(y);
277 }
278
279 (vec![min_x, min_y], vec![max_x, max_y])
280 }
281
282 fn centroid(&self) -> Vec<f64> {
283 let hole_refs: Vec<&[(f64, f64)]> = self.holes.iter().map(|h| h.as_slice()).collect();
284 if let Some((cx, cy)) = geom_polygon::centroid_with_holes(&self.exterior, &hole_refs) {
285 vec![cx, cy]
286 } else {
287 vec![0.0, 0.0]
288 }
289 }
290
291 fn validate(&self) -> Result<()> {
292 if self.exterior.len() < 3 {
293 return Err(Error::InvalidGeometry(format!(
294 "Polygon '{}' must have at least 3 vertices",
295 self.id
296 )));
297 }
298
299 if self.quantity == 0 {
300 return Err(Error::InvalidGeometry(format!(
301 "Quantity for '{}' must be at least 1",
302 self.id
303 )));
304 }
305
306 if self.allow_flip {
310 return Err(Error::InvalidGeometry(format!(
311 "Polygon '{}': allow_flip is not supported (mirroring is unimplemented); \
312 set allow_flip to false",
313 self.id
314 )));
315 }
316
317 let (min, max) = self.aabb_vec();
321 let scale = (max[0] - min[0]).max(max[1] - min[1]).max(1.0);
322 let area_eps = 1e-9 * scale * scale;
323 if geom_polygon::signed_area(&self.exterior).abs() < area_eps {
324 return Err(Error::InvalidGeometry(format!(
325 "Polygon '{}' is degenerate (zero area / collinear vertices)",
326 self.id
327 )));
328 }
329
330 if !crate::polygon_ops::is_simple_polygon(&self.exterior) {
334 return Err(Error::InvalidGeometry(format!(
335 "Polygon '{}' is self-intersecting (edges cross)",
336 self.id
337 )));
338 }
339
340 Ok(())
341 }
342
343 fn rotation_constraint(&self) -> &RotationConstraint<f64> {
344 &self.rotation_constraint
345 }
346
347 fn allow_mirror(&self) -> bool {
348 self.allow_flip
349 }
350
351 fn priority(&self) -> i32 {
352 self.priority
353 }
354}
355
356impl Geometry2DExt for Geometry2D {
357 fn aabb_2d(&self) -> AABB2D<f64> {
358 let (min, max) = self.aabb_vec();
359 AABB2D::new(min[0], min[1], max[0], max[1])
360 }
361
362 fn outer_ring(&self) -> &[(f64, f64)] {
363 &self.exterior
364 }
365
366 fn holes(&self) -> &[Vec<(f64, f64)>] {
367 &self.holes
368 }
369
370 fn is_convex(&self) -> bool {
371 if let Some(is_convex) = self.cached_is_convex {
372 is_convex
373 } else {
374 self.calculate_is_convex()
375 }
376 }
377
378 fn convex_hull(&self) -> Vec<(f64, f64)> {
379 if let Some(ref hull) = self.cached_convex_hull {
380 hull.clone()
381 } else {
382 self.calculate_convex_hull()
383 }
384 }
385
386 fn perimeter(&self) -> f64 {
387 if let Some(perim) = self.cached_perimeter {
388 perim
389 } else {
390 self.calculate_perimeter()
391 }
392 }
393}
394
395impl Geometry2D {
396 pub fn aabb_at_rotation(&self, rotation: f64) -> ([f64; 2], [f64; 2]) {
400 if rotation.abs() < 1e-10 {
401 return self.aabb();
402 }
403
404 let cos_r = rotation.cos();
405 let sin_r = rotation.sin();
406
407 let mut min_x = f64::INFINITY;
408 let mut min_y = f64::INFINITY;
409 let mut max_x = f64::NEG_INFINITY;
410 let mut max_y = f64::NEG_INFINITY;
411
412 for &(x, y) in &self.exterior {
413 let rx = x * cos_r - y * sin_r;
414 let ry = x * sin_r + y * cos_r;
415 min_x = min_x.min(rx);
416 min_y = min_y.min(ry);
417 max_x = max_x.max(rx);
418 max_y = max_y.max(ry);
419 }
420
421 ([min_x, min_y], [max_x, max_y])
422 }
423
424 pub fn dimensions_at_rotation(&self, rotation: f64) -> (f64, f64) {
426 let (min, max) = self.aabb_at_rotation(rotation);
427 (max[0] - min[0], max[1] - min[1])
428 }
429
430 pub fn transformed_exterior(&self, x: f64, y: f64, rotation: f64) -> Vec<(f64, f64)> {
438 let cos_r = rotation.cos();
439 let sin_r = rotation.sin();
440 self.exterior
441 .iter()
442 .map(|&(vx, vy)| {
443 let rx = vx * cos_r - vy * sin_r;
444 let ry = vx * sin_r + vy * cos_r;
445 (x + rx, y + ry)
446 })
447 .collect()
448 }
449}
450
451#[cfg(test)]
452mod tests {
453 use super::*;
454 use approx::assert_relative_eq;
455
456 #[test]
457 fn test_rectangle_area() {
458 let rect = Geometry2D::rectangle("R1", 10.0, 5.0);
459 assert_relative_eq!(rect.measure(), 50.0, epsilon = 0.001);
460 }
461
462 #[test]
463 fn test_polygon_with_hole() {
464 let poly = Geometry2D::new("P1")
465 .with_polygon(vec![(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)])
466 .with_hole(vec![(25.0, 25.0), (75.0, 25.0), (75.0, 75.0), (25.0, 75.0)]);
467
468 assert_relative_eq!(poly.measure(), 7500.0, epsilon = 0.001);
470 }
471
472 #[test]
473 fn test_aabb() {
474 let poly = Geometry2D::new("P1").with_polygon(vec![
475 (10.0, 20.0),
476 (50.0, 20.0),
477 (50.0, 80.0),
478 (10.0, 80.0),
479 ]);
480
481 let aabb = poly.aabb_2d();
482 assert_relative_eq!(aabb.min_x, 10.0);
483 assert_relative_eq!(aabb.min_y, 20.0);
484 assert_relative_eq!(aabb.max_x, 50.0);
485 assert_relative_eq!(aabb.max_y, 80.0);
486 }
487
488 #[test]
489 fn test_rectangle_is_convex() {
490 let rect = Geometry2D::rectangle("R1", 10.0, 10.0);
491 assert!(rect.is_convex());
492 }
493
494 #[test]
495 fn test_l_shape_is_not_convex() {
496 let l = Geometry2D::l_shape("L1", 20.0, 20.0, 10.0, 10.0);
497 assert!(!l.is_convex());
498 }
499
500 #[test]
501 fn test_convex_hull() {
502 let l = Geometry2D::l_shape("L1", 20.0, 20.0, 10.0, 10.0);
503 let hull = l.convex_hull();
504 assert!(hull.len() >= 4);
506 }
507
508 #[test]
509 fn test_centroid() {
510 let rect = Geometry2D::rectangle("R1", 10.0, 10.0);
511 let centroid = rect.centroid();
512 assert_relative_eq!(centroid[0], 5.0, epsilon = 0.001);
513 assert_relative_eq!(centroid[1], 5.0, epsilon = 0.001);
514 }
515
516 #[test]
517 fn test_perimeter() {
518 let rect = Geometry2D::rectangle("R1", 10.0, 5.0);
519 assert_relative_eq!(rect.perimeter(), 30.0, epsilon = 0.001);
520 }
521
522 #[test]
523 fn test_validation() {
524 let valid = Geometry2D::rectangle("R1", 10.0, 10.0);
525 assert!(valid.validate().is_ok());
526
527 let invalid = Geometry2D::new("P1").with_polygon(vec![(0.0, 0.0), (1.0, 0.0)]);
528 assert!(invalid.validate().is_err());
529 }
530
531 #[test]
532 fn test_circle() {
533 let circle = Geometry2D::circle("C1", 10.0, 32);
534 let area = circle.measure();
535 let expected = std::f64::consts::PI * 10.0 * 10.0;
536 assert_relative_eq!(area, expected, epsilon = 5.0);
538 }
539}