1use geo::{Area, Centroid, ConvexHull, Coord, LineString, Polygon as GeoPolygon};
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 pub fn to_geo_polygon(&self) -> GeoPolygon<f64> {
180 let exterior = LineString::from(
181 self.exterior
182 .iter()
183 .map(|&(x, y)| Coord { x, y })
184 .collect::<Vec<_>>(),
185 );
186
187 let holes: Vec<LineString<f64>> = self
188 .holes
189 .iter()
190 .map(|hole| {
191 LineString::from(
192 hole.iter()
193 .map(|&(x, y)| Coord { x, y })
194 .collect::<Vec<_>>(),
195 )
196 })
197 .collect();
198
199 GeoPolygon::new(exterior, holes)
200 }
201
202 fn clear_cache(&mut self) {
204 self.cached_area = None;
205 self.cached_convex_hull = None;
206 self.cached_perimeter = None;
207 self.cached_is_convex = None;
208 }
209
210 fn calculate_area(&self) -> f64 {
212 self.to_geo_polygon().unsigned_area()
213 }
214
215 fn calculate_perimeter(&self) -> f64 {
217 use geo::{Euclidean, Length};
218 let poly = self.to_geo_polygon();
219 let mut perim = poly.exterior().length::<Euclidean>();
220 for hole in poly.interiors() {
221 perim += hole.length::<Euclidean>();
222 }
223 perim
224 }
225
226 fn calculate_convex_hull(&self) -> Vec<(f64, f64)> {
228 let poly = self.to_geo_polygon();
229 let hull = poly.convex_hull();
230 hull.exterior().points().map(|p| (p.x(), p.y())).collect()
231 }
232
233 fn calculate_is_convex(&self) -> bool {
235 if self.exterior.len() < 3 || !self.holes.is_empty() {
236 return false;
237 }
238
239 let n = self.exterior.len();
242 let mut sign = 0i32;
243
244 for i in 0..n {
245 let (x1, y1) = self.exterior[i];
246 let (x2, y2) = self.exterior[(i + 1) % n];
247 let (x3, y3) = self.exterior[(i + 2) % n];
248
249 let cross = (x2 - x1) * (y3 - y2) - (y2 - y1) * (x3 - x2);
250
251 if cross.abs() > 1e-10 {
252 let current_sign = if cross > 0.0 { 1 } else { -1 };
253 if sign == 0 {
254 sign = current_sign;
255 } else if sign != current_sign {
256 return false;
257 }
258 }
259 }
260
261 true
262 }
263}
264
265impl Geometry for Geometry2D {
266 type Scalar = f64;
267
268 fn id(&self) -> &GeometryId {
269 &self.id
270 }
271
272 fn quantity(&self) -> usize {
273 self.quantity
274 }
275
276 fn measure(&self) -> f64 {
277 if let Some(area) = self.cached_area {
278 area
279 } else {
280 self.calculate_area()
281 }
282 }
283
284 fn aabb(&self) -> ([f64; 2], [f64; 2]) {
285 let (min, max) = self.aabb_vec();
286 ([min[0], min[1]], [max[0], max[1]])
287 }
288
289 fn aabb_vec(&self) -> (Vec<f64>, Vec<f64>) {
290 if self.exterior.is_empty() {
291 return (vec![0.0, 0.0], vec![0.0, 0.0]);
292 }
293
294 let mut min_x = f64::MAX;
295 let mut min_y = f64::MAX;
296 let mut max_x = f64::MIN;
297 let mut max_y = f64::MIN;
298
299 for &(x, y) in &self.exterior {
300 min_x = min_x.min(x);
301 min_y = min_y.min(y);
302 max_x = max_x.max(x);
303 max_y = max_y.max(y);
304 }
305
306 (vec![min_x, min_y], vec![max_x, max_y])
307 }
308
309 fn centroid(&self) -> Vec<f64> {
310 if let Some(c) = self.to_geo_polygon().centroid() {
311 vec![c.x(), c.y()]
312 } else {
313 vec![0.0, 0.0]
314 }
315 }
316
317 fn validate(&self) -> Result<()> {
318 if self.exterior.len() < 3 {
319 return Err(Error::InvalidGeometry(format!(
320 "Polygon '{}' must have at least 3 vertices",
321 self.id
322 )));
323 }
324
325 if self.quantity == 0 {
326 return Err(Error::InvalidGeometry(format!(
327 "Quantity for '{}' must be at least 1",
328 self.id
329 )));
330 }
331
332 Ok(())
335 }
336
337 fn rotation_constraint(&self) -> &RotationConstraint<f64> {
338 &self.rotation_constraint
339 }
340
341 fn allow_mirror(&self) -> bool {
342 self.allow_flip
343 }
344
345 fn priority(&self) -> i32 {
346 self.priority
347 }
348}
349
350impl Geometry2DExt for Geometry2D {
351 fn aabb_2d(&self) -> AABB2D<f64> {
352 let (min, max) = self.aabb_vec();
353 AABB2D::new(min[0], min[1], max[0], max[1])
354 }
355
356 fn outer_ring(&self) -> &[(f64, f64)] {
357 &self.exterior
358 }
359
360 fn holes(&self) -> &[Vec<(f64, f64)>] {
361 &self.holes
362 }
363
364 fn is_convex(&self) -> bool {
365 if let Some(is_convex) = self.cached_is_convex {
366 is_convex
367 } else {
368 self.calculate_is_convex()
369 }
370 }
371
372 fn convex_hull(&self) -> Vec<(f64, f64)> {
373 if let Some(ref hull) = self.cached_convex_hull {
374 hull.clone()
375 } else {
376 self.calculate_convex_hull()
377 }
378 }
379
380 fn perimeter(&self) -> f64 {
381 if let Some(perim) = self.cached_perimeter {
382 perim
383 } else {
384 self.calculate_perimeter()
385 }
386 }
387}
388
389impl Geometry2D {
390 pub fn aabb_at_rotation(&self, rotation: f64) -> ([f64; 2], [f64; 2]) {
394 if rotation.abs() < 1e-10 {
395 return self.aabb();
396 }
397
398 let cos_r = rotation.cos();
399 let sin_r = rotation.sin();
400
401 let mut min_x = f64::INFINITY;
402 let mut min_y = f64::INFINITY;
403 let mut max_x = f64::NEG_INFINITY;
404 let mut max_y = f64::NEG_INFINITY;
405
406 for &(x, y) in &self.exterior {
407 let rx = x * cos_r - y * sin_r;
408 let ry = x * sin_r + y * cos_r;
409 min_x = min_x.min(rx);
410 min_y = min_y.min(ry);
411 max_x = max_x.max(rx);
412 max_y = max_y.max(ry);
413 }
414
415 ([min_x, min_y], [max_x, max_y])
416 }
417
418 pub fn dimensions_at_rotation(&self, rotation: f64) -> (f64, f64) {
420 let (min, max) = self.aabb_at_rotation(rotation);
421 (max[0] - min[0], max[1] - min[1])
422 }
423}
424
425#[cfg(test)]
426mod tests {
427 use super::*;
428 use approx::assert_relative_eq;
429
430 #[test]
431 fn test_rectangle_area() {
432 let rect = Geometry2D::rectangle("R1", 10.0, 5.0);
433 assert_relative_eq!(rect.measure(), 50.0, epsilon = 0.001);
434 }
435
436 #[test]
437 fn test_polygon_with_hole() {
438 let poly = Geometry2D::new("P1")
439 .with_polygon(vec![(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)])
440 .with_hole(vec![(25.0, 25.0), (75.0, 25.0), (75.0, 75.0), (25.0, 75.0)]);
441
442 assert_relative_eq!(poly.measure(), 7500.0, epsilon = 0.001);
444 }
445
446 #[test]
447 fn test_aabb() {
448 let poly = Geometry2D::new("P1").with_polygon(vec![
449 (10.0, 20.0),
450 (50.0, 20.0),
451 (50.0, 80.0),
452 (10.0, 80.0),
453 ]);
454
455 let aabb = poly.aabb_2d();
456 assert_relative_eq!(aabb.min_x, 10.0);
457 assert_relative_eq!(aabb.min_y, 20.0);
458 assert_relative_eq!(aabb.max_x, 50.0);
459 assert_relative_eq!(aabb.max_y, 80.0);
460 }
461
462 #[test]
463 fn test_rectangle_is_convex() {
464 let rect = Geometry2D::rectangle("R1", 10.0, 10.0);
465 assert!(rect.is_convex());
466 }
467
468 #[test]
469 fn test_l_shape_is_not_convex() {
470 let l = Geometry2D::l_shape("L1", 20.0, 20.0, 10.0, 10.0);
471 assert!(!l.is_convex());
472 }
473
474 #[test]
475 fn test_convex_hull() {
476 let l = Geometry2D::l_shape("L1", 20.0, 20.0, 10.0, 10.0);
477 let hull = l.convex_hull();
478 assert!(hull.len() >= 4);
480 }
481
482 #[test]
483 fn test_centroid() {
484 let rect = Geometry2D::rectangle("R1", 10.0, 10.0);
485 let centroid = rect.centroid();
486 assert_relative_eq!(centroid[0], 5.0, epsilon = 0.001);
487 assert_relative_eq!(centroid[1], 5.0, epsilon = 0.001);
488 }
489
490 #[test]
491 fn test_perimeter() {
492 let rect = Geometry2D::rectangle("R1", 10.0, 5.0);
493 assert_relative_eq!(rect.perimeter(), 30.0, epsilon = 0.001);
494 }
495
496 #[test]
497 fn test_validation() {
498 let valid = Geometry2D::rectangle("R1", 10.0, 10.0);
499 assert!(valid.validate().is_ok());
500
501 let invalid = Geometry2D::new("P1").with_polygon(vec![(0.0, 0.0), (1.0, 0.0)]);
502 assert!(invalid.validate().is_err());
503 }
504
505 #[test]
506 fn test_circle() {
507 let circle = Geometry2D::circle("C1", 10.0, 32);
508 let area = circle.measure();
509 let expected = std::f64::consts::PI * 10.0 * 10.0;
510 assert_relative_eq!(area, expected, epsilon = 5.0);
512 }
513}