1use nalgebra::{Point3, Vector3};
27use std::collections::HashMap;
28
29#[cfg(feature = "serde")]
30use serde::{Deserialize, Serialize};
31
32#[derive(Debug, Clone, PartialEq, Default)]
34#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
35pub enum StabilityConstraint {
36 #[default]
38 None,
39
40 FullBase,
43
44 PartialBase {
47 min_ratio: f64,
49 },
50
51 CogPolygon,
54
55 StaticEquilibrium {
59 force_tolerance: f64,
61 moment_tolerance: f64,
63 },
64}
65
66impl StabilityConstraint {
67 pub fn partial_base(min_ratio: f64) -> Self {
69 Self::PartialBase {
70 min_ratio: min_ratio.clamp(0.0, 1.0),
71 }
72 }
73
74 pub fn static_equilibrium() -> Self {
76 Self::StaticEquilibrium {
77 force_tolerance: 1e-6,
78 moment_tolerance: 1e-6,
79 }
80 }
81
82 pub fn is_enabled(&self) -> bool {
84 !matches!(self, Self::None)
85 }
86}
87
88#[derive(Debug, Clone)]
90pub struct PlacedBox {
91 pub id: String,
93 pub instance: usize,
95 pub position: Point3<f64>,
97 pub dimensions: Vector3<f64>,
99 pub mass: Option<f64>,
101 pub cog_offset: Option<Vector3<f64>>,
103}
104
105impl PlacedBox {
106 pub fn new(
108 id: impl Into<String>,
109 instance: usize,
110 position: Point3<f64>,
111 dimensions: Vector3<f64>,
112 ) -> Self {
113 Self {
114 id: id.into(),
115 instance,
116 position,
117 dimensions,
118 mass: None,
119 cog_offset: None,
120 }
121 }
122
123 pub fn with_mass(mut self, mass: f64) -> Self {
125 self.mass = Some(mass);
126 self
127 }
128
129 pub fn with_cog_offset(mut self, offset: Vector3<f64>) -> Self {
131 self.cog_offset = Some(offset);
132 self
133 }
134
135 pub fn center(&self) -> Point3<f64> {
137 Point3::new(
138 self.position.x + self.dimensions.x / 2.0,
139 self.position.y + self.dimensions.y / 2.0,
140 self.position.z + self.dimensions.z / 2.0,
141 )
142 }
143
144 pub fn center_of_gravity(&self) -> Point3<f64> {
146 let center = self.center();
147 if let Some(offset) = self.cog_offset {
148 Point3::new(
149 center.x + offset.x,
150 center.y + offset.y,
151 center.z + offset.z,
152 )
153 } else {
154 center
155 }
156 }
157
158 pub fn bottom_face(&self) -> (Point3<f64>, Point3<f64>) {
160 (
161 Point3::new(self.position.x, self.position.y, self.position.z),
162 Point3::new(
163 self.position.x + self.dimensions.x,
164 self.position.y + self.dimensions.y,
165 self.position.z,
166 ),
167 )
168 }
169
170 pub fn top_face(&self) -> (Point3<f64>, Point3<f64>) {
172 let top_z = self.position.z + self.dimensions.z;
173 (
174 Point3::new(self.position.x, self.position.y, top_z),
175 Point3::new(
176 self.position.x + self.dimensions.x,
177 self.position.y + self.dimensions.y,
178 top_z,
179 ),
180 )
181 }
182
183 pub fn volume(&self) -> f64 {
185 self.dimensions.x * self.dimensions.y * self.dimensions.z
186 }
187
188 pub fn base_area(&self) -> f64 {
190 self.dimensions.x * self.dimensions.y
191 }
192}
193
194#[derive(Debug, Clone)]
196#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
197pub struct StabilityResult {
198 pub id: String,
200 pub instance: usize,
202 pub is_stable: bool,
204 pub support_ratio: f64,
206 pub supported_by: Vec<(String, usize)>,
208 pub cog_within_support: bool,
210 pub force_imbalance: f64,
212 pub moment_imbalance: f64,
214 pub stability_score: f64,
216}
217
218impl StabilityResult {
219 pub fn new(id: impl Into<String>, instance: usize) -> Self {
221 Self {
222 id: id.into(),
223 instance,
224 is_stable: true,
225 support_ratio: 1.0,
226 supported_by: Vec::new(),
227 cog_within_support: true,
228 force_imbalance: 0.0,
229 moment_imbalance: 0.0,
230 stability_score: 1.0,
231 }
232 }
233
234 pub fn unstable(mut self) -> Self {
236 self.is_stable = false;
237 self.stability_score = 0.0;
238 self
239 }
240}
241
242#[derive(Debug, Clone)]
244#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
245pub struct StabilityReport {
246 pub results: Vec<StabilityResult>,
248 pub stable_count: usize,
250 pub unstable_count: usize,
252 pub overall_stability: f64,
254 pub min_support_ratio: f64,
256 pub avg_support_ratio: f64,
258 pub unstable_weight: f64,
260 pub analysis_time_ms: u64,
262}
263
264impl StabilityReport {
265 pub fn new() -> Self {
267 Self {
268 results: Vec::new(),
269 stable_count: 0,
270 unstable_count: 0,
271 overall_stability: 1.0,
272 min_support_ratio: 1.0,
273 avg_support_ratio: 1.0,
274 unstable_weight: 0.0,
275 analysis_time_ms: 0,
276 }
277 }
278
279 pub fn is_all_stable(&self) -> bool {
281 self.unstable_count == 0
282 }
283
284 pub fn unstable_boxes(&self) -> Vec<&StabilityResult> {
286 self.results.iter().filter(|r| !r.is_stable).collect()
287 }
288}
289
290impl Default for StabilityReport {
291 fn default() -> Self {
292 Self::new()
293 }
294}
295
296pub struct StabilityAnalyzer {
298 constraint: StabilityConstraint,
299 contact_tolerance: f64,
301 gravity: Vector3<f64>,
303}
304
305impl StabilityAnalyzer {
306 pub fn new(constraint: StabilityConstraint) -> Self {
308 Self {
309 constraint,
310 contact_tolerance: 1e-6,
311 gravity: Vector3::new(0.0, 0.0, -9.81),
312 }
313 }
314
315 pub fn with_contact_tolerance(mut self, tolerance: f64) -> Self {
317 self.contact_tolerance = tolerance;
318 self
319 }
320
321 pub fn with_gravity(mut self, gravity: Vector3<f64>) -> Self {
323 self.gravity = gravity;
324 self
325 }
326
327 pub fn analyze(&self, boxes: &[PlacedBox], floor_z: f64) -> StabilityReport {
329 let start = std::time::Instant::now();
330 let mut report = StabilityReport::new();
331
332 if boxes.is_empty() || !self.constraint.is_enabled() {
333 report.analysis_time_ms = start.elapsed().as_millis() as u64;
334 return report;
335 }
336
337 let boxes_by_top_z = self.build_top_z_index(boxes);
339
340 for placed_box in boxes {
342 let result = self.analyze_box(placed_box, boxes, floor_z, &boxes_by_top_z);
343 report.results.push(result);
344 }
345
346 self.compute_summary(&mut report, boxes);
348 report.analysis_time_ms = start.elapsed().as_millis() as u64;
349
350 report
351 }
352
353 pub fn is_stable(&self, placed_box: &PlacedBox, supports: &[PlacedBox], floor_z: f64) -> bool {
355 let all_boxes: Vec<_> = std::iter::once(placed_box.clone())
356 .chain(supports.iter().cloned())
357 .collect();
358
359 let boxes_by_top_z = self.build_top_z_index(&all_boxes);
360 let result = self.analyze_box(placed_box, &all_boxes, floor_z, &boxes_by_top_z);
361 result.is_stable
362 }
363
364 fn analyze_box(
366 &self,
367 placed_box: &PlacedBox,
368 all_boxes: &[PlacedBox],
369 floor_z: f64,
370 boxes_by_top_z: &HashMap<i64, Vec<usize>>,
371 ) -> StabilityResult {
372 let mut result = StabilityResult::new(&placed_box.id, placed_box.instance);
373
374 let (support_ratio, supporters) =
376 self.compute_support(placed_box, all_boxes, floor_z, boxes_by_top_z);
377
378 result.support_ratio = support_ratio;
379 result.supported_by = supporters;
380
381 result.is_stable = match &self.constraint {
383 StabilityConstraint::None => true,
384 StabilityConstraint::FullBase => support_ratio >= 1.0 - self.contact_tolerance,
385 StabilityConstraint::PartialBase { min_ratio } => support_ratio >= *min_ratio,
386 StabilityConstraint::CogPolygon => {
387 let cog_ok = self.check_cog_support(placed_box, all_boxes, floor_z, boxes_by_top_z);
388 result.cog_within_support = cog_ok;
389 cog_ok
390 }
391 StabilityConstraint::StaticEquilibrium {
392 force_tolerance,
393 moment_tolerance,
394 } => {
395 let (force_imb, moment_imb) =
396 self.check_equilibrium(placed_box, all_boxes, floor_z, boxes_by_top_z);
397 result.force_imbalance = force_imb;
398 result.moment_imbalance = moment_imb;
399 force_imb <= *force_tolerance && moment_imb <= *moment_tolerance
400 }
401 };
402
403 result.stability_score = self.compute_stability_score(&result);
405
406 result
407 }
408
409 fn compute_support(
411 &self,
412 placed_box: &PlacedBox,
413 all_boxes: &[PlacedBox],
414 floor_z: f64,
415 boxes_by_top_z: &HashMap<i64, Vec<usize>>,
416 ) -> (f64, Vec<(String, usize)>) {
417 let bottom_z = placed_box.position.z;
418 let base_area = placed_box.base_area();
419 let (bottom_min, bottom_max) = placed_box.bottom_face();
420
421 if (bottom_z - floor_z).abs() <= self.contact_tolerance {
423 return (1.0, vec![("floor".to_string(), 0)]);
424 }
425
426 let target_z_key = (bottom_z * 1000.0).round() as i64;
428 let mut total_support_area = 0.0;
429 let mut supporters = Vec::new();
430
431 for dz in -1i64..=1 {
433 if let Some(box_indices) = boxes_by_top_z.get(&(target_z_key + dz)) {
434 for &idx in box_indices {
435 let support_box = &all_boxes[idx];
436
437 if support_box.id == placed_box.id
439 && support_box.instance == placed_box.instance
440 {
441 continue;
442 }
443
444 let (top_min, top_max) = support_box.top_face();
445
446 if (top_min.z - bottom_z).abs() > self.contact_tolerance {
448 continue;
449 }
450
451 let overlap = self.compute_face_overlap(
453 (bottom_min.x, bottom_min.y, bottom_max.x, bottom_max.y),
454 (top_min.x, top_min.y, top_max.x, top_max.y),
455 );
456
457 if overlap > 0.0 {
458 total_support_area += overlap;
459 supporters.push((support_box.id.clone(), support_box.instance));
460 }
461 }
462 }
463 }
464
465 let support_ratio = (total_support_area / base_area).min(1.0);
466 (support_ratio, supporters)
467 }
468
469 fn check_cog_support(
471 &self,
472 placed_box: &PlacedBox,
473 all_boxes: &[PlacedBox],
474 floor_z: f64,
475 boxes_by_top_z: &HashMap<i64, Vec<usize>>,
476 ) -> bool {
477 let cog = placed_box.center_of_gravity();
478 let bottom_z = placed_box.position.z;
479
480 let cog_xy = (cog.x, cog.y);
482
483 if (bottom_z - floor_z).abs() <= self.contact_tolerance {
485 let (min, max) = placed_box.bottom_face();
486 return cog_xy.0 >= min.x
487 && cog_xy.0 <= max.x
488 && cog_xy.1 >= min.y
489 && cog_xy.1 <= max.y;
490 }
491
492 let mut support_regions: Vec<(f64, f64, f64, f64)> = Vec::new();
494 let target_z_key = (bottom_z * 1000.0).round() as i64;
495
496 for dz in -1i64..=1 {
497 if let Some(box_indices) = boxes_by_top_z.get(&(target_z_key + dz)) {
498 for &idx in box_indices {
499 let support_box = &all_boxes[idx];
500 if support_box.id == placed_box.id
501 && support_box.instance == placed_box.instance
502 {
503 continue;
504 }
505
506 let (top_min, top_max) = support_box.top_face();
507 if (top_min.z - bottom_z).abs() <= self.contact_tolerance {
508 let (bottom_min, bottom_max) = placed_box.bottom_face();
509 let overlap = self.compute_face_overlap_coords(
510 (bottom_min.x, bottom_min.y, bottom_max.x, bottom_max.y),
511 (top_min.x, top_min.y, top_max.x, top_max.y),
512 );
513 if let Some(region) = overlap {
514 support_regions.push(region);
515 }
516 }
517 }
518 }
519 }
520
521 for (min_x, min_y, max_x, max_y) in support_regions {
523 if cog_xy.0 >= min_x && cog_xy.0 <= max_x && cog_xy.1 >= min_y && cog_xy.1 <= max_y {
524 return true;
525 }
526 }
527
528 false
532 }
533
534 fn check_equilibrium(
536 &self,
537 placed_box: &PlacedBox,
538 all_boxes: &[PlacedBox],
539 floor_z: f64,
540 boxes_by_top_z: &HashMap<i64, Vec<usize>>,
541 ) -> (f64, f64) {
542 let mass = placed_box.mass.unwrap_or(1.0);
546 let _cog = placed_box.center_of_gravity();
548
549 let gravity_force = Vector3::new(0.0, 0.0, -mass * 9.81);
551
552 let (support_ratio, _supporters) =
554 self.compute_support(placed_box, all_boxes, floor_z, boxes_by_top_z);
555
556 if support_ratio < self.contact_tolerance {
557 return (gravity_force.norm(), f64::MAX);
559 }
560
561 let reaction_force = -gravity_force * support_ratio;
563 let net_force = gravity_force + reaction_force;
564 let force_imbalance = net_force.norm();
565
566 let moment_imbalance = if support_ratio >= 0.5 {
569 0.0 } else {
571 mass * 9.81 * (1.0 - support_ratio) * placed_box.dimensions.z / 2.0
573 };
574
575 (force_imbalance, moment_imbalance)
576 }
577
578 fn compute_face_overlap(
580 &self,
581 face1: (f64, f64, f64, f64),
582 face2: (f64, f64, f64, f64),
583 ) -> f64 {
584 let (x1_min, y1_min, x1_max, y1_max) = face1;
585 let (x2_min, y2_min, x2_max, y2_max) = face2;
586
587 let x_overlap = (x1_max.min(x2_max) - x1_min.max(x2_min)).max(0.0);
588 let y_overlap = (y1_max.min(y2_max) - y1_min.max(y2_min)).max(0.0);
589
590 x_overlap * y_overlap
591 }
592
593 fn compute_face_overlap_coords(
595 &self,
596 face1: (f64, f64, f64, f64),
597 face2: (f64, f64, f64, f64),
598 ) -> Option<(f64, f64, f64, f64)> {
599 let (x1_min, y1_min, x1_max, y1_max) = face1;
600 let (x2_min, y2_min, x2_max, y2_max) = face2;
601
602 let x_min = x1_min.max(x2_min);
603 let y_min = y1_min.max(y2_min);
604 let x_max = x1_max.min(x2_max);
605 let y_max = y1_max.min(y2_max);
606
607 if x_max > x_min && y_max > y_min {
608 Some((x_min, y_min, x_max, y_max))
609 } else {
610 None
611 }
612 }
613
614 fn build_top_z_index(&self, boxes: &[PlacedBox]) -> HashMap<i64, Vec<usize>> {
616 let mut index: HashMap<i64, Vec<usize>> = HashMap::new();
617 for (i, b) in boxes.iter().enumerate() {
618 let top_z = b.position.z + b.dimensions.z;
619 let key = (top_z * 1000.0).round() as i64;
620 index.entry(key).or_default().push(i);
621 }
622 index
623 }
624
625 fn compute_stability_score(&self, result: &StabilityResult) -> f64 {
627 if !result.is_stable {
628 return 0.0;
629 }
630
631 let support_score = result.support_ratio;
633 let cog_score = if result.cog_within_support { 1.0 } else { 0.5 };
634
635 (0.7 * support_score + 0.3 * cog_score).clamp(0.0, 1.0)
637 }
638
639 fn compute_summary(&self, report: &mut StabilityReport, boxes: &[PlacedBox]) {
641 let total = report.results.len();
642 if total == 0 {
643 return;
644 }
645
646 report.stable_count = report.results.iter().filter(|r| r.is_stable).count();
647 report.unstable_count = total - report.stable_count;
648 report.overall_stability = report.stable_count as f64 / total as f64;
649
650 report.min_support_ratio = report
651 .results
652 .iter()
653 .map(|r| r.support_ratio)
654 .fold(f64::MAX, f64::min);
655
656 report.avg_support_ratio =
657 report.results.iter().map(|r| r.support_ratio).sum::<f64>() / total as f64;
658
659 for result in &report.results {
661 if !result.is_stable {
662 if let Some(b) = boxes
664 .iter()
665 .find(|b| b.id == result.id && b.instance == result.instance)
666 {
667 report.unstable_weight += b.mass.unwrap_or(0.0);
668 }
669 }
670 }
671 }
672}
673
674impl Default for StabilityAnalyzer {
675 fn default() -> Self {
676 Self::new(StabilityConstraint::default())
677 }
678}
679
680#[cfg(test)]
681mod tests {
682 use super::*;
683
684 #[test]
685 fn test_stability_constraint_default() {
686 let constraint = StabilityConstraint::default();
687 assert!(!constraint.is_enabled());
688 }
689
690 #[test]
691 fn test_stability_constraint_partial_base() {
692 let constraint = StabilityConstraint::partial_base(0.75);
693 assert!(constraint.is_enabled());
694 if let StabilityConstraint::PartialBase { min_ratio } = constraint {
695 assert!((min_ratio - 0.75).abs() < 0.001);
696 } else {
697 panic!("Expected PartialBase");
698 }
699 }
700
701 #[test]
702 fn test_placed_box_center() {
703 let b = PlacedBox::new(
704 "B1",
705 0,
706 Point3::new(10.0, 20.0, 30.0),
707 Vector3::new(100.0, 50.0, 40.0),
708 );
709
710 let center = b.center();
711 assert!((center.x - 60.0).abs() < 0.001);
712 assert!((center.y - 45.0).abs() < 0.001);
713 assert!((center.z - 50.0).abs() < 0.001);
714 }
715
716 #[test]
717 fn test_placed_box_with_cog_offset() {
718 let b = PlacedBox::new(
719 "B1",
720 0,
721 Point3::new(0.0, 0.0, 0.0),
722 Vector3::new(100.0, 100.0, 100.0),
723 )
724 .with_cog_offset(Vector3::new(10.0, 0.0, -5.0));
725
726 let cog = b.center_of_gravity();
727 assert!((cog.x - 60.0).abs() < 0.001); assert!((cog.y - 50.0).abs() < 0.001);
729 assert!((cog.z - 45.0).abs() < 0.001); }
731
732 #[test]
733 fn test_box_on_floor_is_stable() {
734 let analyzer = StabilityAnalyzer::new(StabilityConstraint::FullBase);
735
736 let boxes = vec![PlacedBox::new(
737 "B1",
738 0,
739 Point3::new(0.0, 0.0, 0.0),
740 Vector3::new(100.0, 100.0, 50.0),
741 )];
742
743 let report = analyzer.analyze(&boxes, 0.0);
744
745 assert!(report.is_all_stable());
746 assert_eq!(report.stable_count, 1);
747 assert!((report.results[0].support_ratio - 1.0).abs() < 0.001);
748 }
749
750 #[test]
751 fn test_stacked_boxes_full_support() {
752 let analyzer = StabilityAnalyzer::new(StabilityConstraint::FullBase);
753
754 let boxes = vec![
755 PlacedBox::new(
756 "B1",
757 0,
758 Point3::new(0.0, 0.0, 0.0),
759 Vector3::new(100.0, 100.0, 50.0),
760 ),
761 PlacedBox::new(
762 "B2",
763 0,
764 Point3::new(0.0, 0.0, 50.0),
765 Vector3::new(100.0, 100.0, 50.0),
766 ),
767 ];
768
769 let report = analyzer.analyze(&boxes, 0.0);
770
771 assert!(report.is_all_stable());
772 assert_eq!(report.stable_count, 2);
773 }
774
775 #[test]
776 fn test_stacked_box_partial_support() {
777 let analyzer = StabilityAnalyzer::new(StabilityConstraint::partial_base(0.5));
778
779 let boxes = vec![
780 PlacedBox::new(
782 "B1",
783 0,
784 Point3::new(0.0, 0.0, 0.0),
785 Vector3::new(100.0, 100.0, 50.0),
786 ),
787 PlacedBox::new(
789 "B2",
790 0,
791 Point3::new(50.0, 0.0, 50.0),
792 Vector3::new(100.0, 100.0, 50.0),
793 ),
794 ];
795
796 let report = analyzer.analyze(&boxes, 0.0);
797
798 assert!(report.results[0].is_stable); assert!(report.results[1].is_stable); }
802
803 #[test]
804 fn test_unsupported_box_is_unstable() {
805 let analyzer = StabilityAnalyzer::new(StabilityConstraint::FullBase);
806
807 let boxes = vec![
808 PlacedBox::new(
810 "B1",
811 0,
812 Point3::new(0.0, 0.0, 0.0),
813 Vector3::new(50.0, 50.0, 50.0),
814 ),
815 PlacedBox::new(
817 "B2",
818 0,
819 Point3::new(100.0, 100.0, 50.0),
820 Vector3::new(50.0, 50.0, 50.0),
821 ),
822 ];
823
824 let report = analyzer.analyze(&boxes, 0.0);
825
826 assert!(!report.is_all_stable());
827 assert_eq!(report.unstable_count, 1);
828 assert!(!report.results[1].is_stable);
829 }
830
831 #[test]
832 fn test_cog_stability_check() {
833 let analyzer = StabilityAnalyzer::new(StabilityConstraint::CogPolygon);
834
835 let boxes = vec![PlacedBox::new(
836 "B1",
837 0,
838 Point3::new(0.0, 0.0, 0.0),
839 Vector3::new(100.0, 100.0, 50.0),
840 )];
841
842 let report = analyzer.analyze(&boxes, 0.0);
843
844 assert!(report.results[0].cog_within_support);
845 assert!(report.results[0].is_stable);
846 }
847
848 #[test]
849 fn test_equilibrium_check() {
850 let analyzer = StabilityAnalyzer::new(StabilityConstraint::static_equilibrium());
851
852 let boxes = vec![PlacedBox::new(
853 "B1",
854 0,
855 Point3::new(0.0, 0.0, 0.0),
856 Vector3::new(100.0, 100.0, 50.0),
857 )
858 .with_mass(10.0)];
859
860 let report = analyzer.analyze(&boxes, 0.0);
861
862 assert!(report.results[0].is_stable);
863 assert!(report.results[0].force_imbalance < 1.0);
864 }
865
866 #[test]
867 fn test_stability_report_summary() {
868 let analyzer = StabilityAnalyzer::new(StabilityConstraint::partial_base(0.8));
869
870 let boxes = vec![
871 PlacedBox::new(
872 "B1",
873 0,
874 Point3::new(0.0, 0.0, 0.0),
875 Vector3::new(100.0, 100.0, 50.0),
876 )
877 .with_mass(5.0),
878 PlacedBox::new(
879 "B2",
880 0,
881 Point3::new(50.0, 50.0, 50.0),
882 Vector3::new(100.0, 100.0, 50.0),
883 )
884 .with_mass(3.0),
885 ];
886
887 let report = analyzer.analyze(&boxes, 0.0);
888
889 assert_eq!(report.results.len(), 2);
890 assert!(report.analysis_time_ms < 1000); }
892
893 #[test]
894 fn test_no_constraint_always_stable() {
895 let analyzer = StabilityAnalyzer::new(StabilityConstraint::None);
896
897 let boxes = vec![
898 PlacedBox::new(
900 "B1",
901 0,
902 Point3::new(0.0, 0.0, 100.0), Vector3::new(50.0, 50.0, 50.0),
904 ),
905 ];
906
907 let report = analyzer.analyze(&boxes, 0.0);
908
909 assert!(report.results.is_empty() || report.is_all_stable());
911 }
912
913 #[test]
914 fn test_face_overlap_computation() {
915 let analyzer = StabilityAnalyzer::default();
916
917 let area1 = analyzer.compute_face_overlap((0.0, 0.0, 10.0, 10.0), (0.0, 0.0, 10.0, 10.0));
919 assert!((area1 - 100.0).abs() < 0.001);
920
921 let area2 = analyzer.compute_face_overlap((0.0, 0.0, 10.0, 10.0), (5.0, 5.0, 15.0, 15.0));
923 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));
927 assert!(area3 < 0.001);
928 }
929}