1use std::collections::HashMap;
27use u_nesting_core::geom::nalgebra_types::{NaPoint3 as Point3, NaVector3 as Vector3};
28use u_nesting_core::timing::Timer;
29
30#[cfg(feature = "serde")]
31use serde::{Deserialize, Serialize};
32
33#[derive(Debug, Clone, Copy, PartialEq, Default)]
35#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
36pub enum StabilityConstraint {
37 #[default]
39 None,
40
41 FullBase,
44
45 PartialBase {
48 min_ratio: f64,
50 },
51
52 CogPolygon,
55
56 StaticEquilibrium {
60 force_tolerance: f64,
62 moment_tolerance: f64,
64 },
65}
66
67impl StabilityConstraint {
68 pub fn partial_base(min_ratio: f64) -> Self {
70 Self::PartialBase {
71 min_ratio: min_ratio.clamp(0.0, 1.0),
72 }
73 }
74
75 pub fn static_equilibrium() -> Self {
77 Self::StaticEquilibrium {
78 force_tolerance: 1e-6,
79 moment_tolerance: 1e-6,
80 }
81 }
82
83 pub fn is_enabled(&self) -> bool {
85 !matches!(self, Self::None)
86 }
87}
88
89#[derive(Debug, Clone)]
91pub struct PlacedBox {
92 pub id: String,
94 pub instance: usize,
96 pub position: Point3<f64>,
98 pub dimensions: Vector3<f64>,
100 pub mass: Option<f64>,
102 pub cog_offset: Option<Vector3<f64>>,
104}
105
106impl PlacedBox {
107 pub fn new(
109 id: impl Into<String>,
110 instance: usize,
111 position: Point3<f64>,
112 dimensions: Vector3<f64>,
113 ) -> Self {
114 Self {
115 id: id.into(),
116 instance,
117 position,
118 dimensions,
119 mass: None,
120 cog_offset: None,
121 }
122 }
123
124 pub fn with_mass(mut self, mass: f64) -> Self {
126 self.mass = Some(mass);
127 self
128 }
129
130 pub fn with_cog_offset(mut self, offset: Vector3<f64>) -> Self {
132 self.cog_offset = Some(offset);
133 self
134 }
135
136 pub fn center(&self) -> Point3<f64> {
138 Point3::new(
139 self.position.x + self.dimensions.x / 2.0,
140 self.position.y + self.dimensions.y / 2.0,
141 self.position.z + self.dimensions.z / 2.0,
142 )
143 }
144
145 pub fn center_of_gravity(&self) -> Point3<f64> {
147 let center = self.center();
148 if let Some(offset) = self.cog_offset {
149 Point3::new(
150 center.x + offset.x,
151 center.y + offset.y,
152 center.z + offset.z,
153 )
154 } else {
155 center
156 }
157 }
158
159 pub fn bottom_face(&self) -> (Point3<f64>, Point3<f64>) {
161 (
162 Point3::new(self.position.x, self.position.y, self.position.z),
163 Point3::new(
164 self.position.x + self.dimensions.x,
165 self.position.y + self.dimensions.y,
166 self.position.z,
167 ),
168 )
169 }
170
171 pub fn top_face(&self) -> (Point3<f64>, Point3<f64>) {
173 let top_z = self.position.z + self.dimensions.z;
174 (
175 Point3::new(self.position.x, self.position.y, top_z),
176 Point3::new(
177 self.position.x + self.dimensions.x,
178 self.position.y + self.dimensions.y,
179 top_z,
180 ),
181 )
182 }
183
184 pub fn volume(&self) -> f64 {
186 self.dimensions.x * self.dimensions.y * self.dimensions.z
187 }
188
189 pub fn base_area(&self) -> f64 {
191 self.dimensions.x * self.dimensions.y
192 }
193}
194
195#[derive(Debug, Clone)]
197#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
198pub struct StabilityResult {
199 pub id: String,
201 pub instance: usize,
203 pub is_stable: bool,
205 pub support_ratio: f64,
207 pub supported_by: Vec<(String, usize)>,
209 pub cog_within_support: bool,
211 pub force_imbalance: f64,
213 pub moment_imbalance: f64,
215 pub stability_score: f64,
217}
218
219impl StabilityResult {
220 pub fn new(id: impl Into<String>, instance: usize) -> Self {
222 Self {
223 id: id.into(),
224 instance,
225 is_stable: true,
226 support_ratio: 1.0,
227 supported_by: Vec::new(),
228 cog_within_support: true,
229 force_imbalance: 0.0,
230 moment_imbalance: 0.0,
231 stability_score: 1.0,
232 }
233 }
234
235 pub fn unstable(mut self) -> Self {
237 self.is_stable = false;
238 self.stability_score = 0.0;
239 self
240 }
241}
242
243#[derive(Debug, Clone)]
245#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
246pub struct StabilityReport {
247 pub results: Vec<StabilityResult>,
249 pub stable_count: usize,
251 pub unstable_count: usize,
253 pub overall_stability: f64,
255 pub min_support_ratio: f64,
257 pub avg_support_ratio: f64,
259 pub unstable_weight: f64,
261 pub analysis_time_ms: u64,
263}
264
265impl StabilityReport {
266 pub fn new() -> Self {
268 Self {
269 results: Vec::new(),
270 stable_count: 0,
271 unstable_count: 0,
272 overall_stability: 1.0,
273 min_support_ratio: 1.0,
274 avg_support_ratio: 1.0,
275 unstable_weight: 0.0,
276 analysis_time_ms: 0,
277 }
278 }
279
280 pub fn is_all_stable(&self) -> bool {
282 self.unstable_count == 0
283 }
284
285 pub fn unstable_boxes(&self) -> Vec<&StabilityResult> {
287 self.results.iter().filter(|r| !r.is_stable).collect()
288 }
289}
290
291impl Default for StabilityReport {
292 fn default() -> Self {
293 Self::new()
294 }
295}
296
297pub struct StabilityAnalyzer {
299 constraint: StabilityConstraint,
300 contact_tolerance: f64,
302 gravity: Vector3<f64>,
304}
305
306impl StabilityAnalyzer {
307 pub fn new(constraint: StabilityConstraint) -> Self {
309 Self {
310 constraint,
311 contact_tolerance: 1e-6,
312 gravity: Vector3::new(0.0, 0.0, -9.81),
313 }
314 }
315
316 pub fn with_contact_tolerance(mut self, tolerance: f64) -> Self {
318 self.contact_tolerance = tolerance;
319 self
320 }
321
322 pub fn with_gravity(mut self, gravity: Vector3<f64>) -> Self {
324 self.gravity = gravity;
325 self
326 }
327
328 pub fn analyze(&self, boxes: &[PlacedBox], floor_z: f64) -> StabilityReport {
330 let start = Timer::now();
331 let mut report = StabilityReport::new();
332
333 if boxes.is_empty() || !self.constraint.is_enabled() {
334 report.analysis_time_ms = start.elapsed_ms();
335 return report;
336 }
337
338 let boxes_by_top_z = self.build_top_z_index(boxes);
340
341 for placed_box in boxes {
343 let result = self.analyze_box(placed_box, boxes, floor_z, &boxes_by_top_z);
344 report.results.push(result);
345 }
346
347 self.compute_summary(&mut report, boxes);
349 report.analysis_time_ms = start.elapsed_ms();
350
351 report
352 }
353
354 pub fn is_stable(&self, placed_box: &PlacedBox, supports: &[PlacedBox], floor_z: f64) -> bool {
356 let all_boxes: Vec<_> = std::iter::once(placed_box.clone())
357 .chain(supports.iter().cloned())
358 .collect();
359
360 let boxes_by_top_z = self.build_top_z_index(&all_boxes);
361 let result = self.analyze_box(placed_box, &all_boxes, floor_z, &boxes_by_top_z);
362 result.is_stable
363 }
364
365 fn analyze_box(
367 &self,
368 placed_box: &PlacedBox,
369 all_boxes: &[PlacedBox],
370 floor_z: f64,
371 boxes_by_top_z: &HashMap<i64, Vec<usize>>,
372 ) -> StabilityResult {
373 let mut result = StabilityResult::new(&placed_box.id, placed_box.instance);
374
375 let (support_ratio, supporters) =
377 self.compute_support(placed_box, all_boxes, floor_z, boxes_by_top_z);
378
379 result.support_ratio = support_ratio;
380 result.supported_by = supporters;
381
382 result.is_stable = match &self.constraint {
384 StabilityConstraint::None => true,
385 StabilityConstraint::FullBase => support_ratio >= 1.0 - self.contact_tolerance,
386 StabilityConstraint::PartialBase { min_ratio } => support_ratio >= *min_ratio,
387 StabilityConstraint::CogPolygon => {
388 let cog_ok = self.check_cog_support(placed_box, all_boxes, floor_z, boxes_by_top_z);
389 result.cog_within_support = cog_ok;
390 cog_ok
391 }
392 StabilityConstraint::StaticEquilibrium {
393 force_tolerance,
394 moment_tolerance,
395 } => {
396 let (force_imb, moment_imb) =
397 self.check_equilibrium(placed_box, all_boxes, floor_z, boxes_by_top_z);
398 result.force_imbalance = force_imb;
399 result.moment_imbalance = moment_imb;
400 force_imb <= *force_tolerance && moment_imb <= *moment_tolerance
401 }
402 };
403
404 result.stability_score = self.compute_stability_score(&result);
406
407 result
408 }
409
410 fn compute_support(
412 &self,
413 placed_box: &PlacedBox,
414 all_boxes: &[PlacedBox],
415 floor_z: f64,
416 boxes_by_top_z: &HashMap<i64, Vec<usize>>,
417 ) -> (f64, Vec<(String, usize)>) {
418 let bottom_z = placed_box.position.z;
419 let base_area = placed_box.base_area();
420 let (bottom_min, bottom_max) = placed_box.bottom_face();
421
422 if (bottom_z - floor_z).abs() <= self.contact_tolerance {
424 return (1.0, vec![("floor".to_string(), 0)]);
425 }
426
427 let target_z_key = (bottom_z * 1000.0).round() as i64;
429 let mut total_support_area = 0.0;
430 let mut supporters = Vec::new();
431
432 for dz in -1i64..=1 {
434 if let Some(box_indices) = boxes_by_top_z.get(&(target_z_key + dz)) {
435 for &idx in box_indices {
436 let support_box = &all_boxes[idx];
437
438 if support_box.id == placed_box.id
440 && support_box.instance == placed_box.instance
441 {
442 continue;
443 }
444
445 let (top_min, top_max) = support_box.top_face();
446
447 if (top_min.z - bottom_z).abs() > self.contact_tolerance {
449 continue;
450 }
451
452 let overlap = self.compute_face_overlap(
454 (bottom_min.x, bottom_min.y, bottom_max.x, bottom_max.y),
455 (top_min.x, top_min.y, top_max.x, top_max.y),
456 );
457
458 if overlap > 0.0 {
459 total_support_area += overlap;
460 supporters.push((support_box.id.clone(), support_box.instance));
461 }
462 }
463 }
464 }
465
466 let support_ratio = (total_support_area / base_area).min(1.0);
467 (support_ratio, supporters)
468 }
469
470 fn check_cog_support(
472 &self,
473 placed_box: &PlacedBox,
474 all_boxes: &[PlacedBox],
475 floor_z: f64,
476 boxes_by_top_z: &HashMap<i64, Vec<usize>>,
477 ) -> bool {
478 let cog = placed_box.center_of_gravity();
479 let bottom_z = placed_box.position.z;
480
481 let cog_xy = (cog.x, cog.y);
483
484 if (bottom_z - floor_z).abs() <= self.contact_tolerance {
486 let (min, max) = placed_box.bottom_face();
487 return cog_xy.0 >= min.x
488 && cog_xy.0 <= max.x
489 && cog_xy.1 >= min.y
490 && cog_xy.1 <= max.y;
491 }
492
493 let mut support_regions: Vec<(f64, f64, f64, f64)> = Vec::new();
495 let target_z_key = (bottom_z * 1000.0).round() as i64;
496
497 for dz in -1i64..=1 {
498 if let Some(box_indices) = boxes_by_top_z.get(&(target_z_key + dz)) {
499 for &idx in box_indices {
500 let support_box = &all_boxes[idx];
501 if support_box.id == placed_box.id
502 && support_box.instance == placed_box.instance
503 {
504 continue;
505 }
506
507 let (top_min, top_max) = support_box.top_face();
508 if (top_min.z - bottom_z).abs() <= self.contact_tolerance {
509 let (bottom_min, bottom_max) = placed_box.bottom_face();
510 let overlap = self.compute_face_overlap_coords(
511 (bottom_min.x, bottom_min.y, bottom_max.x, bottom_max.y),
512 (top_min.x, top_min.y, top_max.x, top_max.y),
513 );
514 if let Some(region) = overlap {
515 support_regions.push(region);
516 }
517 }
518 }
519 }
520 }
521
522 for (min_x, min_y, max_x, max_y) in support_regions {
524 if cog_xy.0 >= min_x && cog_xy.0 <= max_x && cog_xy.1 >= min_y && cog_xy.1 <= max_y {
525 return true;
526 }
527 }
528
529 false
533 }
534
535 fn check_equilibrium(
537 &self,
538 placed_box: &PlacedBox,
539 all_boxes: &[PlacedBox],
540 floor_z: f64,
541 boxes_by_top_z: &HashMap<i64, Vec<usize>>,
542 ) -> (f64, f64) {
543 let mass = placed_box.mass.unwrap_or(1.0);
547 let _cog = placed_box.center_of_gravity();
549
550 let gravity_force = Vector3::new(0.0, 0.0, -mass * 9.81);
552
553 let (support_ratio, _supporters) =
555 self.compute_support(placed_box, all_boxes, floor_z, boxes_by_top_z);
556
557 if support_ratio < self.contact_tolerance {
558 return (gravity_force.norm(), f64::MAX);
560 }
561
562 let reaction_force = -gravity_force * support_ratio;
564 let net_force = gravity_force + reaction_force;
565 let force_imbalance = net_force.norm();
566
567 let moment_imbalance = if support_ratio >= 0.5 {
570 0.0 } else {
572 mass * 9.81 * (1.0 - support_ratio) * placed_box.dimensions.z / 2.0
574 };
575
576 (force_imbalance, moment_imbalance)
577 }
578
579 fn compute_face_overlap(
581 &self,
582 face1: (f64, f64, f64, f64),
583 face2: (f64, f64, f64, f64),
584 ) -> f64 {
585 let (x1_min, y1_min, x1_max, y1_max) = face1;
586 let (x2_min, y2_min, x2_max, y2_max) = face2;
587
588 let x_overlap = (x1_max.min(x2_max) - x1_min.max(x2_min)).max(0.0);
589 let y_overlap = (y1_max.min(y2_max) - y1_min.max(y2_min)).max(0.0);
590
591 x_overlap * y_overlap
592 }
593
594 fn compute_face_overlap_coords(
596 &self,
597 face1: (f64, f64, f64, f64),
598 face2: (f64, f64, f64, f64),
599 ) -> Option<(f64, f64, f64, f64)> {
600 let (x1_min, y1_min, x1_max, y1_max) = face1;
601 let (x2_min, y2_min, x2_max, y2_max) = face2;
602
603 let x_min = x1_min.max(x2_min);
604 let y_min = y1_min.max(y2_min);
605 let x_max = x1_max.min(x2_max);
606 let y_max = y1_max.min(y2_max);
607
608 if x_max > x_min && y_max > y_min {
609 Some((x_min, y_min, x_max, y_max))
610 } else {
611 None
612 }
613 }
614
615 fn build_top_z_index(&self, boxes: &[PlacedBox]) -> HashMap<i64, Vec<usize>> {
617 let mut index: HashMap<i64, Vec<usize>> = HashMap::new();
618 for (i, b) in boxes.iter().enumerate() {
619 let top_z = b.position.z + b.dimensions.z;
620 let key = (top_z * 1000.0).round() as i64;
621 index.entry(key).or_default().push(i);
622 }
623 index
624 }
625
626 fn compute_stability_score(&self, result: &StabilityResult) -> f64 {
628 if !result.is_stable {
629 return 0.0;
630 }
631
632 let support_score = result.support_ratio;
634 let cog_score = if result.cog_within_support { 1.0 } else { 0.5 };
635
636 (0.7 * support_score + 0.3 * cog_score).clamp(0.0, 1.0)
638 }
639
640 fn compute_summary(&self, report: &mut StabilityReport, boxes: &[PlacedBox]) {
642 let total = report.results.len();
643 if total == 0 {
644 return;
645 }
646
647 report.stable_count = report.results.iter().filter(|r| r.is_stable).count();
648 report.unstable_count = total - report.stable_count;
649 report.overall_stability = report.stable_count as f64 / total as f64;
650
651 report.min_support_ratio = report
652 .results
653 .iter()
654 .map(|r| r.support_ratio)
655 .fold(f64::MAX, f64::min);
656
657 report.avg_support_ratio =
658 report.results.iter().map(|r| r.support_ratio).sum::<f64>() / total as f64;
659
660 for result in &report.results {
662 if !result.is_stable {
663 if let Some(b) = boxes
665 .iter()
666 .find(|b| b.id == result.id && b.instance == result.instance)
667 {
668 report.unstable_weight += b.mass.unwrap_or(0.0);
669 }
670 }
671 }
672 }
673}
674
675impl Default for StabilityAnalyzer {
676 fn default() -> Self {
677 Self::new(StabilityConstraint::default())
678 }
679}
680
681#[cfg(test)]
682mod tests {
683 use super::*;
684
685 #[test]
686 fn test_stability_constraint_default() {
687 let constraint = StabilityConstraint::default();
688 assert!(!constraint.is_enabled());
689 }
690
691 #[test]
692 fn test_stability_constraint_partial_base() {
693 let constraint = StabilityConstraint::partial_base(0.75);
694 assert!(constraint.is_enabled());
695 if let StabilityConstraint::PartialBase { min_ratio } = constraint {
696 assert!((min_ratio - 0.75).abs() < 0.001);
697 } else {
698 panic!("Expected PartialBase");
699 }
700 }
701
702 #[test]
703 fn test_placed_box_center() {
704 let b = PlacedBox::new(
705 "B1",
706 0,
707 Point3::new(10.0, 20.0, 30.0),
708 Vector3::new(100.0, 50.0, 40.0),
709 );
710
711 let center = b.center();
712 assert!((center.x - 60.0).abs() < 0.001);
713 assert!((center.y - 45.0).abs() < 0.001);
714 assert!((center.z - 50.0).abs() < 0.001);
715 }
716
717 #[test]
718 fn test_placed_box_with_cog_offset() {
719 let b = PlacedBox::new(
720 "B1",
721 0,
722 Point3::new(0.0, 0.0, 0.0),
723 Vector3::new(100.0, 100.0, 100.0),
724 )
725 .with_cog_offset(Vector3::new(10.0, 0.0, -5.0));
726
727 let cog = b.center_of_gravity();
728 assert!((cog.x - 60.0).abs() < 0.001); assert!((cog.y - 50.0).abs() < 0.001);
730 assert!((cog.z - 45.0).abs() < 0.001); }
732
733 #[test]
734 fn test_box_on_floor_is_stable() {
735 let analyzer = StabilityAnalyzer::new(StabilityConstraint::FullBase);
736
737 let boxes = vec![PlacedBox::new(
738 "B1",
739 0,
740 Point3::new(0.0, 0.0, 0.0),
741 Vector3::new(100.0, 100.0, 50.0),
742 )];
743
744 let report = analyzer.analyze(&boxes, 0.0);
745
746 assert!(report.is_all_stable());
747 assert_eq!(report.stable_count, 1);
748 assert!((report.results[0].support_ratio - 1.0).abs() < 0.001);
749 }
750
751 #[test]
752 fn test_stacked_boxes_full_support() {
753 let analyzer = StabilityAnalyzer::new(StabilityConstraint::FullBase);
754
755 let boxes = vec![
756 PlacedBox::new(
757 "B1",
758 0,
759 Point3::new(0.0, 0.0, 0.0),
760 Vector3::new(100.0, 100.0, 50.0),
761 ),
762 PlacedBox::new(
763 "B2",
764 0,
765 Point3::new(0.0, 0.0, 50.0),
766 Vector3::new(100.0, 100.0, 50.0),
767 ),
768 ];
769
770 let report = analyzer.analyze(&boxes, 0.0);
771
772 assert!(report.is_all_stable());
773 assert_eq!(report.stable_count, 2);
774 }
775
776 #[test]
777 fn test_stacked_box_partial_support() {
778 let analyzer = StabilityAnalyzer::new(StabilityConstraint::partial_base(0.5));
779
780 let boxes = vec![
781 PlacedBox::new(
783 "B1",
784 0,
785 Point3::new(0.0, 0.0, 0.0),
786 Vector3::new(100.0, 100.0, 50.0),
787 ),
788 PlacedBox::new(
790 "B2",
791 0,
792 Point3::new(50.0, 0.0, 50.0),
793 Vector3::new(100.0, 100.0, 50.0),
794 ),
795 ];
796
797 let report = analyzer.analyze(&boxes, 0.0);
798
799 assert!(report.results[0].is_stable); assert!(report.results[1].is_stable); }
803
804 #[test]
805 fn test_unsupported_box_is_unstable() {
806 let analyzer = StabilityAnalyzer::new(StabilityConstraint::FullBase);
807
808 let boxes = vec![
809 PlacedBox::new(
811 "B1",
812 0,
813 Point3::new(0.0, 0.0, 0.0),
814 Vector3::new(50.0, 50.0, 50.0),
815 ),
816 PlacedBox::new(
818 "B2",
819 0,
820 Point3::new(100.0, 100.0, 50.0),
821 Vector3::new(50.0, 50.0, 50.0),
822 ),
823 ];
824
825 let report = analyzer.analyze(&boxes, 0.0);
826
827 assert!(!report.is_all_stable());
828 assert_eq!(report.unstable_count, 1);
829 assert!(!report.results[1].is_stable);
830 }
831
832 #[test]
833 fn test_cog_stability_check() {
834 let analyzer = StabilityAnalyzer::new(StabilityConstraint::CogPolygon);
835
836 let boxes = vec![PlacedBox::new(
837 "B1",
838 0,
839 Point3::new(0.0, 0.0, 0.0),
840 Vector3::new(100.0, 100.0, 50.0),
841 )];
842
843 let report = analyzer.analyze(&boxes, 0.0);
844
845 assert!(report.results[0].cog_within_support);
846 assert!(report.results[0].is_stable);
847 }
848
849 #[test]
850 fn test_equilibrium_check() {
851 let analyzer = StabilityAnalyzer::new(StabilityConstraint::static_equilibrium());
852
853 let boxes = vec![PlacedBox::new(
854 "B1",
855 0,
856 Point3::new(0.0, 0.0, 0.0),
857 Vector3::new(100.0, 100.0, 50.0),
858 )
859 .with_mass(10.0)];
860
861 let report = analyzer.analyze(&boxes, 0.0);
862
863 assert!(report.results[0].is_stable);
864 assert!(report.results[0].force_imbalance < 1.0);
865 }
866
867 #[test]
868 fn test_stability_report_summary() {
869 let analyzer = StabilityAnalyzer::new(StabilityConstraint::partial_base(0.8));
870
871 let boxes = vec![
872 PlacedBox::new(
873 "B1",
874 0,
875 Point3::new(0.0, 0.0, 0.0),
876 Vector3::new(100.0, 100.0, 50.0),
877 )
878 .with_mass(5.0),
879 PlacedBox::new(
880 "B2",
881 0,
882 Point3::new(50.0, 50.0, 50.0),
883 Vector3::new(100.0, 100.0, 50.0),
884 )
885 .with_mass(3.0),
886 ];
887
888 let report = analyzer.analyze(&boxes, 0.0);
889
890 assert_eq!(report.results.len(), 2);
891 assert!(report.analysis_time_ms < 1000); }
893
894 #[test]
895 fn test_no_constraint_always_stable() {
896 let analyzer = StabilityAnalyzer::new(StabilityConstraint::None);
897
898 let boxes = vec![
899 PlacedBox::new(
901 "B1",
902 0,
903 Point3::new(0.0, 0.0, 100.0), Vector3::new(50.0, 50.0, 50.0),
905 ),
906 ];
907
908 let report = analyzer.analyze(&boxes, 0.0);
909
910 assert!(report.results.is_empty() || report.is_all_stable());
912 }
913
914 #[test]
915 fn test_face_overlap_computation() {
916 let analyzer = StabilityAnalyzer::default();
917
918 let area1 = analyzer.compute_face_overlap((0.0, 0.0, 10.0, 10.0), (0.0, 0.0, 10.0, 10.0));
920 assert!((area1 - 100.0).abs() < 0.001);
921
922 let area2 = analyzer.compute_face_overlap((0.0, 0.0, 10.0, 10.0), (5.0, 5.0, 15.0, 15.0));
924 assert!((area2 - 25.0).abs() < 0.001); let area3 = analyzer.compute_face_overlap((0.0, 0.0, 10.0, 10.0), (20.0, 20.0, 30.0, 30.0));
928 assert!(area3 < 0.001);
929 }
930}