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 Ok(())
309 }
310
311 fn rotation_constraint(&self) -> &RotationConstraint<f64> {
312 &self.rotation_constraint
313 }
314
315 fn allow_mirror(&self) -> bool {
316 self.allow_flip
317 }
318
319 fn priority(&self) -> i32 {
320 self.priority
321 }
322}
323
324impl Geometry2DExt for Geometry2D {
325 fn aabb_2d(&self) -> AABB2D<f64> {
326 let (min, max) = self.aabb_vec();
327 AABB2D::new(min[0], min[1], max[0], max[1])
328 }
329
330 fn outer_ring(&self) -> &[(f64, f64)] {
331 &self.exterior
332 }
333
334 fn holes(&self) -> &[Vec<(f64, f64)>] {
335 &self.holes
336 }
337
338 fn is_convex(&self) -> bool {
339 if let Some(is_convex) = self.cached_is_convex {
340 is_convex
341 } else {
342 self.calculate_is_convex()
343 }
344 }
345
346 fn convex_hull(&self) -> Vec<(f64, f64)> {
347 if let Some(ref hull) = self.cached_convex_hull {
348 hull.clone()
349 } else {
350 self.calculate_convex_hull()
351 }
352 }
353
354 fn perimeter(&self) -> f64 {
355 if let Some(perim) = self.cached_perimeter {
356 perim
357 } else {
358 self.calculate_perimeter()
359 }
360 }
361}
362
363impl Geometry2D {
364 pub fn aabb_at_rotation(&self, rotation: f64) -> ([f64; 2], [f64; 2]) {
368 if rotation.abs() < 1e-10 {
369 return self.aabb();
370 }
371
372 let cos_r = rotation.cos();
373 let sin_r = rotation.sin();
374
375 let mut min_x = f64::INFINITY;
376 let mut min_y = f64::INFINITY;
377 let mut max_x = f64::NEG_INFINITY;
378 let mut max_y = f64::NEG_INFINITY;
379
380 for &(x, y) in &self.exterior {
381 let rx = x * cos_r - y * sin_r;
382 let ry = x * sin_r + y * cos_r;
383 min_x = min_x.min(rx);
384 min_y = min_y.min(ry);
385 max_x = max_x.max(rx);
386 max_y = max_y.max(ry);
387 }
388
389 ([min_x, min_y], [max_x, max_y])
390 }
391
392 pub fn dimensions_at_rotation(&self, rotation: f64) -> (f64, f64) {
394 let (min, max) = self.aabb_at_rotation(rotation);
395 (max[0] - min[0], max[1] - min[1])
396 }
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402 use approx::assert_relative_eq;
403
404 #[test]
405 fn test_rectangle_area() {
406 let rect = Geometry2D::rectangle("R1", 10.0, 5.0);
407 assert_relative_eq!(rect.measure(), 50.0, epsilon = 0.001);
408 }
409
410 #[test]
411 fn test_polygon_with_hole() {
412 let poly = Geometry2D::new("P1")
413 .with_polygon(vec![(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)])
414 .with_hole(vec![(25.0, 25.0), (75.0, 25.0), (75.0, 75.0), (25.0, 75.0)]);
415
416 assert_relative_eq!(poly.measure(), 7500.0, epsilon = 0.001);
418 }
419
420 #[test]
421 fn test_aabb() {
422 let poly = Geometry2D::new("P1").with_polygon(vec![
423 (10.0, 20.0),
424 (50.0, 20.0),
425 (50.0, 80.0),
426 (10.0, 80.0),
427 ]);
428
429 let aabb = poly.aabb_2d();
430 assert_relative_eq!(aabb.min_x, 10.0);
431 assert_relative_eq!(aabb.min_y, 20.0);
432 assert_relative_eq!(aabb.max_x, 50.0);
433 assert_relative_eq!(aabb.max_y, 80.0);
434 }
435
436 #[test]
437 fn test_rectangle_is_convex() {
438 let rect = Geometry2D::rectangle("R1", 10.0, 10.0);
439 assert!(rect.is_convex());
440 }
441
442 #[test]
443 fn test_l_shape_is_not_convex() {
444 let l = Geometry2D::l_shape("L1", 20.0, 20.0, 10.0, 10.0);
445 assert!(!l.is_convex());
446 }
447
448 #[test]
449 fn test_convex_hull() {
450 let l = Geometry2D::l_shape("L1", 20.0, 20.0, 10.0, 10.0);
451 let hull = l.convex_hull();
452 assert!(hull.len() >= 4);
454 }
455
456 #[test]
457 fn test_centroid() {
458 let rect = Geometry2D::rectangle("R1", 10.0, 10.0);
459 let centroid = rect.centroid();
460 assert_relative_eq!(centroid[0], 5.0, epsilon = 0.001);
461 assert_relative_eq!(centroid[1], 5.0, epsilon = 0.001);
462 }
463
464 #[test]
465 fn test_perimeter() {
466 let rect = Geometry2D::rectangle("R1", 10.0, 5.0);
467 assert_relative_eq!(rect.perimeter(), 30.0, epsilon = 0.001);
468 }
469
470 #[test]
471 fn test_validation() {
472 let valid = Geometry2D::rectangle("R1", 10.0, 10.0);
473 assert!(valid.validate().is_ok());
474
475 let invalid = Geometry2D::new("P1").with_polygon(vec![(0.0, 0.0), (1.0, 0.0)]);
476 assert!(invalid.validate().is_err());
477 }
478
479 #[test]
480 fn test_circle() {
481 let circle = Geometry2D::circle("C1", 10.0, 32);
482 let area = circle.measure();
483 let expected = std::f64::consts::PI * 10.0 * 10.0;
484 assert_relative_eq!(area, expected, epsilon = 5.0);
486 }
487}