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 let (min, max) = self.aabb_vec();
310 let scale = (max[0] - min[0]).max(max[1] - min[1]).max(1.0);
311 let area_eps = 1e-9 * scale * scale;
312 if geom_polygon::signed_area(&self.exterior).abs() < area_eps {
313 return Err(Error::InvalidGeometry(format!(
314 "Polygon '{}' is degenerate (zero area / collinear vertices)",
315 self.id
316 )));
317 }
318
319 if !crate::polygon_ops::is_simple_polygon(&self.exterior) {
323 return Err(Error::InvalidGeometry(format!(
324 "Polygon '{}' is self-intersecting (edges cross)",
325 self.id
326 )));
327 }
328
329 Ok(())
330 }
331
332 fn rotation_constraint(&self) -> &RotationConstraint<f64> {
333 &self.rotation_constraint
334 }
335
336 fn allow_mirror(&self) -> bool {
337 self.allow_flip
338 }
339
340 fn priority(&self) -> i32 {
341 self.priority
342 }
343}
344
345impl Geometry2DExt for Geometry2D {
346 fn aabb_2d(&self) -> AABB2D<f64> {
347 let (min, max) = self.aabb_vec();
348 AABB2D::new(min[0], min[1], max[0], max[1])
349 }
350
351 fn outer_ring(&self) -> &[(f64, f64)] {
352 &self.exterior
353 }
354
355 fn holes(&self) -> &[Vec<(f64, f64)>] {
356 &self.holes
357 }
358
359 fn is_convex(&self) -> bool {
360 if let Some(is_convex) = self.cached_is_convex {
361 is_convex
362 } else {
363 self.calculate_is_convex()
364 }
365 }
366
367 fn convex_hull(&self) -> Vec<(f64, f64)> {
368 if let Some(ref hull) = self.cached_convex_hull {
369 hull.clone()
370 } else {
371 self.calculate_convex_hull()
372 }
373 }
374
375 fn perimeter(&self) -> f64 {
376 if let Some(perim) = self.cached_perimeter {
377 perim
378 } else {
379 self.calculate_perimeter()
380 }
381 }
382}
383
384impl Geometry2D {
385 pub fn aabb_at_rotation(&self, rotation: f64) -> ([f64; 2], [f64; 2]) {
389 if rotation.abs() < 1e-10 {
390 return self.aabb();
391 }
392
393 let cos_r = rotation.cos();
394 let sin_r = rotation.sin();
395
396 let mut min_x = f64::INFINITY;
397 let mut min_y = f64::INFINITY;
398 let mut max_x = f64::NEG_INFINITY;
399 let mut max_y = f64::NEG_INFINITY;
400
401 for &(x, y) in &self.exterior {
402 let rx = x * cos_r - y * sin_r;
403 let ry = x * sin_r + y * cos_r;
404 min_x = min_x.min(rx);
405 min_y = min_y.min(ry);
406 max_x = max_x.max(rx);
407 max_y = max_y.max(ry);
408 }
409
410 ([min_x, min_y], [max_x, max_y])
411 }
412
413 pub fn aabb_at_rotation_mirrored(&self, rotation: f64, mirror: bool) -> ([f64; 2], [f64; 2]) {
428 if !mirror {
429 return self.aabb_at_rotation(rotation);
430 }
431
432 let cos_r = rotation.cos();
433 let sin_r = rotation.sin();
434
435 let mut min_x = f64::INFINITY;
436 let mut min_y = f64::INFINITY;
437 let mut max_x = f64::NEG_INFINITY;
438 let mut max_y = f64::NEG_INFINITY;
439
440 for &(x, y) in &self.exterior {
441 let mx = -x; let rx = mx * cos_r - y * sin_r;
443 let ry = mx * sin_r + y * cos_r;
444 min_x = min_x.min(rx);
445 min_y = min_y.min(ry);
446 max_x = max_x.max(rx);
447 max_y = max_y.max(ry);
448 }
449
450 ([min_x, min_y], [max_x, max_y])
451 }
452
453 pub fn dimensions_at_rotation(&self, rotation: f64) -> (f64, f64) {
455 let (min, max) = self.aabb_at_rotation(rotation);
456 (max[0] - min[0], max[1] - min[1])
457 }
458
459 pub fn transformed_exterior(&self, x: f64, y: f64, rotation: f64) -> Vec<(f64, f64)> {
467 let cos_r = rotation.cos();
468 let sin_r = rotation.sin();
469 self.exterior
470 .iter()
471 .map(|&(vx, vy)| {
472 let rx = vx * cos_r - vy * sin_r;
473 let ry = vx * sin_r + vy * cos_r;
474 (x + rx, y + ry)
475 })
476 .collect()
477 }
478}
479
480#[cfg(test)]
481mod tests {
482 use super::*;
483 use approx::assert_relative_eq;
484
485 #[test]
486 fn test_rectangle_area() {
487 let rect = Geometry2D::rectangle("R1", 10.0, 5.0);
488 assert_relative_eq!(rect.measure(), 50.0, epsilon = 0.001);
489 }
490
491 #[test]
492 fn test_polygon_with_hole() {
493 let poly = Geometry2D::new("P1")
494 .with_polygon(vec![(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)])
495 .with_hole(vec![(25.0, 25.0), (75.0, 25.0), (75.0, 75.0), (25.0, 75.0)]);
496
497 assert_relative_eq!(poly.measure(), 7500.0, epsilon = 0.001);
499 }
500
501 #[test]
502 fn test_aabb() {
503 let poly = Geometry2D::new("P1").with_polygon(vec![
504 (10.0, 20.0),
505 (50.0, 20.0),
506 (50.0, 80.0),
507 (10.0, 80.0),
508 ]);
509
510 let aabb = poly.aabb_2d();
511 assert_relative_eq!(aabb.min_x, 10.0);
512 assert_relative_eq!(aabb.min_y, 20.0);
513 assert_relative_eq!(aabb.max_x, 50.0);
514 assert_relative_eq!(aabb.max_y, 80.0);
515 }
516
517 #[test]
518 fn test_aabb_at_rotation_mirrored_delegates_when_not_mirrored() {
519 let poly = Geometry2D::new("P1").with_polygon(vec![
520 (10.0, 20.0),
521 (50.0, 20.0),
522 (50.0, 80.0),
523 (10.0, 80.0),
524 ]);
525 assert_eq!(
526 poly.aabb_at_rotation_mirrored(0.3, false),
527 poly.aabb_at_rotation(0.3)
528 );
529 }
530
531 #[test]
532 fn test_aabb_at_rotation_mirrored_reflects_asymmetric_extents() {
533 let poly = Geometry2D::new("P1").with_polygon(vec![
539 (10.0, 20.0),
540 (50.0, 20.0),
541 (50.0, 80.0),
542 (10.0, 80.0),
543 ]);
544 let (min, max) = poly.aabb_at_rotation_mirrored(0.0, true);
545 assert_relative_eq!(min[0], -50.0);
546 assert_relative_eq!(max[0], -10.0);
547 assert_relative_eq!(min[1], 20.0);
549 assert_relative_eq!(max[1], 80.0);
550 let (unmirrored_min, unmirrored_max) = poly.aabb_at_rotation(0.0);
552 assert_relative_eq!(max[0] - min[0], unmirrored_max[0] - unmirrored_min[0]);
553 assert_relative_eq!(max[1] - min[1], unmirrored_max[1] - unmirrored_min[1]);
554 }
555
556 #[test]
557 fn test_rectangle_is_convex() {
558 let rect = Geometry2D::rectangle("R1", 10.0, 10.0);
559 assert!(rect.is_convex());
560 }
561
562 #[test]
563 fn test_l_shape_is_not_convex() {
564 let l = Geometry2D::l_shape("L1", 20.0, 20.0, 10.0, 10.0);
565 assert!(!l.is_convex());
566 }
567
568 #[test]
569 fn test_convex_hull() {
570 let l = Geometry2D::l_shape("L1", 20.0, 20.0, 10.0, 10.0);
571 let hull = l.convex_hull();
572 assert!(hull.len() >= 4);
574 }
575
576 #[test]
577 fn test_centroid() {
578 let rect = Geometry2D::rectangle("R1", 10.0, 10.0);
579 let centroid = rect.centroid();
580 assert_relative_eq!(centroid[0], 5.0, epsilon = 0.001);
581 assert_relative_eq!(centroid[1], 5.0, epsilon = 0.001);
582 }
583
584 #[test]
585 fn test_perimeter() {
586 let rect = Geometry2D::rectangle("R1", 10.0, 5.0);
587 assert_relative_eq!(rect.perimeter(), 30.0, epsilon = 0.001);
588 }
589
590 #[test]
591 fn test_validation() {
592 let valid = Geometry2D::rectangle("R1", 10.0, 10.0);
593 assert!(valid.validate().is_ok());
594
595 let invalid = Geometry2D::new("P1").with_polygon(vec![(0.0, 0.0), (1.0, 0.0)]);
596 assert!(invalid.validate().is_err());
597 }
598
599 #[test]
600 fn test_circle() {
601 let circle = Geometry2D::circle("C1", 10.0, 32);
602 let area = circle.measure();
603 let expected = std::f64::consts::PI * 10.0 * 10.0;
604 assert_relative_eq!(area, expected, epsilon = 5.0);
606 }
607}