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 holes(&self) -> &[Vec<(f64, f64)>] {
170 &self.holes
171 }
172
173 pub fn rotations(&self) -> Vec<f64> {
175 self.rotation_constraint.angles()
176 }
177
178 pub fn allow_flip(&self) -> bool {
180 self.allow_flip
181 }
182
183 fn clear_cache(&mut self) {
185 self.cached_area = None;
186 self.cached_convex_hull = None;
187 self.cached_perimeter = None;
188 self.cached_is_convex = None;
189 }
190
191 fn calculate_area(&self) -> f64 {
193 let hole_refs: Vec<&[(f64, f64)]> = self.holes.iter().map(|h| h.as_slice()).collect();
194 geom_polygon::area_with_holes(&self.exterior, &hole_refs)
195 }
196
197 fn calculate_perimeter(&self) -> f64 {
199 let mut perim = geom_polygon::perimeter(&self.exterior);
200 for hole in &self.holes {
201 perim += geom_polygon::perimeter(hole);
202 }
203 perim
204 }
205
206 fn calculate_convex_hull(&self) -> Vec<(f64, f64)> {
208 geom_polygon::convex_hull(&self.exterior)
209 }
210
211 fn calculate_is_convex(&self) -> bool {
213 if self.exterior.len() < 3 || !self.holes.is_empty() {
214 return false;
215 }
216
217 let n = self.exterior.len();
220 let mut sign = 0i32;
221
222 for i in 0..n {
223 let (x1, y1) = self.exterior[i];
224 let (x2, y2) = self.exterior[(i + 1) % n];
225 let (x3, y3) = self.exterior[(i + 2) % n];
226
227 let cross = (x2 - x1) * (y3 - y2) - (y2 - y1) * (x3 - x2);
228
229 if cross.abs() > 1e-10 {
230 let current_sign = if cross > 0.0 { 1 } else { -1 };
231 if sign == 0 {
232 sign = current_sign;
233 } else if sign != current_sign {
234 return false;
235 }
236 }
237 }
238
239 true
240 }
241}
242
243#[cfg(feature = "serde")]
244impl Geometry2D {
245 pub fn from_request(
253 index: usize,
254 request: u_nesting_core::api_types::Geometry2DRequest,
255 ) -> Result<Self> {
256 let points = |ring: Vec<[f64; 2]>| ring.into_iter().map(|[x, y]| (x, y)).collect();
257 let quantity = u_nesting_core::check_quantity(
258 &format!("geometries[{index}].quantity"),
259 &request.id,
260 request.quantity,
261 )?;
262 let mut geom = Self::new(request.id)
263 .with_polygon(points(request.polygon))
264 .with_quantity(quantity)
265 .with_flip(request.allow_flip);
266 if let Some(rotations) = request.rotations {
267 geom = geom.with_rotations_deg(rotations);
268 }
269 for hole in request.holes.into_iter().flatten() {
270 geom = geom.with_hole(points(hole));
271 }
272 Ok(geom)
273 }
274
275 pub fn from_requests(
282 requests: Vec<u_nesting_core::api_types::Geometry2DRequest>,
283 ) -> Result<Vec<Self>> {
284 requests
285 .into_iter()
286 .enumerate()
287 .map(|(index, request)| Self::from_request(index, request))
288 .collect()
289 }
290}
291
292impl Geometry for Geometry2D {
293 type Scalar = f64;
294
295 fn id(&self) -> &GeometryId {
296 &self.id
297 }
298
299 fn quantity(&self) -> usize {
300 self.quantity
301 }
302
303 fn measure(&self) -> f64 {
304 if let Some(area) = self.cached_area {
305 area
306 } else {
307 self.calculate_area()
308 }
309 }
310
311 fn aabb(&self) -> ([f64; 2], [f64; 2]) {
312 let (min, max) = self.aabb_vec();
313 ([min[0], min[1]], [max[0], max[1]])
314 }
315
316 fn aabb_vec(&self) -> (Vec<f64>, Vec<f64>) {
317 if self.exterior.is_empty() {
318 return (vec![0.0, 0.0], vec![0.0, 0.0]);
319 }
320
321 let mut min_x = f64::MAX;
322 let mut min_y = f64::MAX;
323 let mut max_x = f64::MIN;
324 let mut max_y = f64::MIN;
325
326 for &(x, y) in &self.exterior {
327 min_x = min_x.min(x);
328 min_y = min_y.min(y);
329 max_x = max_x.max(x);
330 max_y = max_y.max(y);
331 }
332
333 (vec![min_x, min_y], vec![max_x, max_y])
334 }
335
336 fn centroid(&self) -> Vec<f64> {
337 let hole_refs: Vec<&[(f64, f64)]> = self.holes.iter().map(|h| h.as_slice()).collect();
338 if let Some((cx, cy)) = geom_polygon::centroid_with_holes(&self.exterior, &hole_refs) {
339 vec![cx, cy]
340 } else {
341 vec![0.0, 0.0]
342 }
343 }
344
345 fn validate(&self) -> Result<()> {
346 if self.exterior.len() < 3 {
347 return Err(Error::invalid_geometry(
348 Some(&self.id),
349 format!("Polygon '{}' must have at least 3 vertices", self.id),
350 ));
351 }
352
353 u_nesting_core::check_quantity("quantity", &self.id, self.quantity as i128)?;
354
355 if let RotationConstraint::Discrete(angles) = &self.rotation_constraint {
358 if angles.is_empty() {
359 return Err(Error::invalid_geometry(
360 Some(&self.id),
361 format!(
362 "Rotations for '{}' are empty; list at least one angle, \
363 or leave rotations out for a fixed orientation",
364 self.id
365 ),
366 ));
367 }
368 if let Some(a) = angles.iter().find(|a| !a.is_finite()) {
369 return Err(Error::invalid_geometry(
370 Some(&self.id),
371 format!(
372 "Rotations for '{}' contain a non-finite angle ({a})",
373 self.id
374 ),
375 ));
376 }
377 }
378
379 let (min, max) = self.aabb_vec();
383 let scale = (max[0] - min[0]).max(max[1] - min[1]).max(1.0);
384 let area_eps = 1e-9 * scale * scale;
385 if geom_polygon::signed_area(&self.exterior).abs() < area_eps {
386 return Err(Error::invalid_geometry(
387 Some(&self.id),
388 format!(
389 "Polygon '{}' is degenerate (zero area / collinear vertices)",
390 self.id
391 ),
392 ));
393 }
394
395 if !crate::polygon_ops::is_simple_polygon(&self.exterior) {
399 return Err(Error::invalid_geometry(
400 Some(&self.id),
401 format!("Polygon '{}' is self-intersecting (edges cross)", self.id),
402 ));
403 }
404
405 Ok(())
406 }
407
408 fn rotation_constraint(&self) -> &RotationConstraint<f64> {
409 &self.rotation_constraint
410 }
411
412 fn allow_mirror(&self) -> bool {
413 self.allow_flip
414 }
415
416 fn priority(&self) -> i32 {
417 self.priority
418 }
419}
420
421impl Geometry2DExt for Geometry2D {
422 fn aabb_2d(&self) -> AABB2D<f64> {
423 let (min, max) = self.aabb_vec();
424 AABB2D::new(min[0], min[1], max[0], max[1])
425 }
426
427 fn outer_ring(&self) -> &[(f64, f64)] {
428 &self.exterior
429 }
430
431 fn holes(&self) -> &[Vec<(f64, f64)>] {
432 &self.holes
433 }
434
435 fn is_convex(&self) -> bool {
436 if let Some(is_convex) = self.cached_is_convex {
437 is_convex
438 } else {
439 self.calculate_is_convex()
440 }
441 }
442
443 fn convex_hull(&self) -> Vec<(f64, f64)> {
444 if let Some(ref hull) = self.cached_convex_hull {
445 hull.clone()
446 } else {
447 self.calculate_convex_hull()
448 }
449 }
450
451 fn perimeter(&self) -> f64 {
452 if let Some(perim) = self.cached_perimeter {
453 perim
454 } else {
455 self.calculate_perimeter()
456 }
457 }
458}
459
460impl Geometry2D {
461 pub fn aabb_at_rotation(&self, rotation: f64) -> ([f64; 2], [f64; 2]) {
465 if rotation.abs() < 1e-10 {
466 return self.aabb();
467 }
468
469 let cos_r = rotation.cos();
470 let sin_r = rotation.sin();
471
472 let mut min_x = f64::INFINITY;
473 let mut min_y = f64::INFINITY;
474 let mut max_x = f64::NEG_INFINITY;
475 let mut max_y = f64::NEG_INFINITY;
476
477 for &(x, y) in &self.exterior {
478 let rx = x * cos_r - y * sin_r;
479 let ry = x * sin_r + y * cos_r;
480 min_x = min_x.min(rx);
481 min_y = min_y.min(ry);
482 max_x = max_x.max(rx);
483 max_y = max_y.max(ry);
484 }
485
486 ([min_x, min_y], [max_x, max_y])
487 }
488
489 pub fn aabb_at_rotation_mirrored(&self, rotation: f64, mirror: bool) -> ([f64; 2], [f64; 2]) {
504 if !mirror {
505 return self.aabb_at_rotation(rotation);
506 }
507
508 let cos_r = rotation.cos();
509 let sin_r = rotation.sin();
510
511 let mut min_x = f64::INFINITY;
512 let mut min_y = f64::INFINITY;
513 let mut max_x = f64::NEG_INFINITY;
514 let mut max_y = f64::NEG_INFINITY;
515
516 for &(x, y) in &self.exterior {
517 let mx = -x; let rx = mx * cos_r - y * sin_r;
519 let ry = mx * sin_r + y * cos_r;
520 min_x = min_x.min(rx);
521 min_y = min_y.min(ry);
522 max_x = max_x.max(rx);
523 max_y = max_y.max(ry);
524 }
525
526 ([min_x, min_y], [max_x, max_y])
527 }
528
529 pub fn dimensions_at_rotation(&self, rotation: f64) -> (f64, f64) {
531 let (min, max) = self.aabb_at_rotation(rotation);
532 (max[0] - min[0], max[1] - min[1])
533 }
534
535 pub fn transformed_exterior(&self, x: f64, y: f64, rotation: f64) -> Vec<(f64, f64)> {
543 let cos_r = rotation.cos();
544 let sin_r = rotation.sin();
545 self.exterior
546 .iter()
547 .map(|&(vx, vy)| {
548 let rx = vx * cos_r - vy * sin_r;
549 let ry = vx * sin_r + vy * cos_r;
550 (x + rx, y + ry)
551 })
552 .collect()
553 }
554}
555
556#[cfg(test)]
557mod tests {
558 use super::*;
559 use approx::assert_relative_eq;
560
561 #[test]
562 fn test_rectangle_area() {
563 let rect = Geometry2D::rectangle("R1", 10.0, 5.0);
564 assert_relative_eq!(rect.measure(), 50.0, epsilon = 0.001);
565 }
566
567 #[test]
568 fn test_polygon_with_hole() {
569 let poly = Geometry2D::new("P1")
570 .with_polygon(vec![(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)])
571 .with_hole(vec![(25.0, 25.0), (75.0, 25.0), (75.0, 75.0), (25.0, 75.0)]);
572
573 assert_relative_eq!(poly.measure(), 7500.0, epsilon = 0.001);
575 }
576
577 #[test]
578 fn test_aabb() {
579 let poly = Geometry2D::new("P1").with_polygon(vec![
580 (10.0, 20.0),
581 (50.0, 20.0),
582 (50.0, 80.0),
583 (10.0, 80.0),
584 ]);
585
586 let aabb = poly.aabb_2d();
587 assert_relative_eq!(aabb.min_x, 10.0);
588 assert_relative_eq!(aabb.min_y, 20.0);
589 assert_relative_eq!(aabb.max_x, 50.0);
590 assert_relative_eq!(aabb.max_y, 80.0);
591 }
592
593 #[test]
594 fn test_aabb_at_rotation_mirrored_delegates_when_not_mirrored() {
595 let poly = Geometry2D::new("P1").with_polygon(vec![
596 (10.0, 20.0),
597 (50.0, 20.0),
598 (50.0, 80.0),
599 (10.0, 80.0),
600 ]);
601 assert_eq!(
602 poly.aabb_at_rotation_mirrored(0.3, false),
603 poly.aabb_at_rotation(0.3)
604 );
605 }
606
607 #[test]
608 fn test_aabb_at_rotation_mirrored_reflects_asymmetric_extents() {
609 let poly = Geometry2D::new("P1").with_polygon(vec![
615 (10.0, 20.0),
616 (50.0, 20.0),
617 (50.0, 80.0),
618 (10.0, 80.0),
619 ]);
620 let (min, max) = poly.aabb_at_rotation_mirrored(0.0, true);
621 assert_relative_eq!(min[0], -50.0);
622 assert_relative_eq!(max[0], -10.0);
623 assert_relative_eq!(min[1], 20.0);
625 assert_relative_eq!(max[1], 80.0);
626 let (unmirrored_min, unmirrored_max) = poly.aabb_at_rotation(0.0);
628 assert_relative_eq!(max[0] - min[0], unmirrored_max[0] - unmirrored_min[0]);
629 assert_relative_eq!(max[1] - min[1], unmirrored_max[1] - unmirrored_min[1]);
630 }
631
632 #[test]
633 fn test_rectangle_is_convex() {
634 let rect = Geometry2D::rectangle("R1", 10.0, 10.0);
635 assert!(rect.is_convex());
636 }
637
638 #[test]
639 fn test_l_shape_is_not_convex() {
640 let l = Geometry2D::l_shape("L1", 20.0, 20.0, 10.0, 10.0);
641 assert!(!l.is_convex());
642 }
643
644 #[test]
645 fn test_convex_hull() {
646 let l = Geometry2D::l_shape("L1", 20.0, 20.0, 10.0, 10.0);
647 let hull = l.convex_hull();
648 assert!(hull.len() >= 4);
650 }
651
652 #[test]
653 fn test_centroid() {
654 let rect = Geometry2D::rectangle("R1", 10.0, 10.0);
655 let centroid = rect.centroid();
656 assert_relative_eq!(centroid[0], 5.0, epsilon = 0.001);
657 assert_relative_eq!(centroid[1], 5.0, epsilon = 0.001);
658 }
659
660 #[test]
661 fn test_perimeter() {
662 let rect = Geometry2D::rectangle("R1", 10.0, 5.0);
663 assert_relative_eq!(rect.perimeter(), 30.0, epsilon = 0.001);
664 }
665
666 #[test]
667 fn test_validation() {
668 let valid = Geometry2D::rectangle("R1", 10.0, 10.0);
669 assert!(valid.validate().is_ok());
670
671 let invalid = Geometry2D::new("P1").with_polygon(vec![(0.0, 0.0), (1.0, 0.0)]);
672 assert!(invalid.validate().is_err());
673 }
674
675 #[test]
676 fn test_circle() {
677 let circle = Geometry2D::circle("C1", 10.0, 32);
678 let area = circle.measure();
679 let expected = std::f64::consts::PI * 10.0 * 10.0;
680 assert_relative_eq!(area, expected, epsilon = 5.0);
682 }
683}