1use crate::error::{SpatialError, SpatialResult};
26use scirs2_core::ndarray::{Array2, ArrayView2};
27
28#[derive(Debug, Clone)]
30pub struct AABB {
31 pub min: [f64; 2],
33 pub max: [f64; 2],
35}
36
37impl AABB {
38 pub fn width(&self) -> f64 {
40 self.max[0] - self.min[0]
41 }
42
43 pub fn height(&self) -> f64 {
45 self.max[1] - self.min[1]
46 }
47
48 pub fn area(&self) -> f64 {
50 self.width() * self.height()
51 }
52
53 pub fn perimeter(&self) -> f64 {
55 2.0 * (self.width() + self.height())
56 }
57
58 pub fn center(&self) -> [f64; 2] {
60 [
61 (self.min[0] + self.max[0]) / 2.0,
62 (self.min[1] + self.max[1]) / 2.0,
63 ]
64 }
65
66 pub fn contains(&self, point: &[f64; 2]) -> bool {
68 point[0] >= self.min[0]
69 && point[0] <= self.max[0]
70 && point[1] >= self.min[1]
71 && point[1] <= self.max[1]
72 }
73
74 pub fn intersects(&self, other: &AABB) -> bool {
76 self.min[0] <= other.max[0]
77 && self.max[0] >= other.min[0]
78 && self.min[1] <= other.max[1]
79 && self.max[1] >= other.min[1]
80 }
81}
82
83#[derive(Debug, Clone)]
85pub struct OrientedBoundingRect {
86 pub center: [f64; 2],
88 pub axis_u: [f64; 2],
90 pub axis_v: [f64; 2],
92 pub half_width: f64,
94 pub half_height: f64,
96 pub angle: f64,
98 pub corners: [[f64; 2]; 4],
100}
101
102impl OrientedBoundingRect {
103 pub fn area(&self) -> f64 {
105 4.0 * self.half_width * self.half_height
106 }
107
108 pub fn perimeter(&self) -> f64 {
110 4.0 * (self.half_width + self.half_height)
111 }
112
113 pub fn contains(&self, point: &[f64; 2]) -> bool {
115 let dx = point[0] - self.center[0];
117 let dy = point[1] - self.center[1];
118
119 let proj_u = dx * self.axis_u[0] + dy * self.axis_u[1];
120 let proj_v = dx * self.axis_v[0] + dy * self.axis_v[1];
121
122 proj_u.abs() <= self.half_width && proj_v.abs() <= self.half_height
123 }
124}
125
126pub fn axis_aligned_bounding_box(points: &ArrayView2<'_, f64>) -> SpatialResult<AABB> {
148 if points.nrows() == 0 {
149 return Err(SpatialError::ValueError(
150 "Cannot compute bounding box of empty point set".to_string(),
151 ));
152 }
153 if points.ncols() != 2 {
154 return Err(SpatialError::DimensionError(
155 "Points must be 2D for bounding box computation".to_string(),
156 ));
157 }
158
159 let mut min_x = f64::INFINITY;
160 let mut min_y = f64::INFINITY;
161 let mut max_x = f64::NEG_INFINITY;
162 let mut max_y = f64::NEG_INFINITY;
163
164 for i in 0..points.nrows() {
165 let x = points[[i, 0]];
166 let y = points[[i, 1]];
167 if x < min_x {
168 min_x = x;
169 }
170 if y < min_y {
171 min_y = y;
172 }
173 if x > max_x {
174 max_x = x;
175 }
176 if y > max_y {
177 max_y = y;
178 }
179 }
180
181 Ok(AABB {
182 min: [min_x, min_y],
183 max: [max_x, max_y],
184 })
185}
186
187pub fn minimum_bounding_rectangle(
230 points: &ArrayView2<'_, f64>,
231) -> SpatialResult<OrientedBoundingRect> {
232 if points.nrows() < 3 {
233 return Err(SpatialError::ValueError(
234 "Need at least 3 points for minimum bounding rectangle".to_string(),
235 ));
236 }
237 if points.ncols() != 2 {
238 return Err(SpatialError::DimensionError(
239 "Points must be 2D for bounding rectangle computation".to_string(),
240 ));
241 }
242
243 let hull = compute_convex_hull_ccw(points)?;
245 let n = hull.len();
246
247 if n < 3 {
248 return Err(SpatialError::ComputationError(
249 "Convex hull has fewer than 3 vertices (degenerate case)".to_string(),
250 ));
251 }
252
253 let mut best_area = f64::INFINITY;
254 let mut best_rect: Option<OrientedBoundingRect> = None;
255
256 for i in 0..n {
258 let j = (i + 1) % n;
259
260 let edge_dx = hull[j][0] - hull[i][0];
262 let edge_dy = hull[j][1] - hull[i][1];
263 let edge_len = (edge_dx * edge_dx + edge_dy * edge_dy).sqrt();
264
265 if edge_len < 1e-15 {
266 continue;
267 }
268
269 let ux = edge_dx / edge_len;
271 let uy = edge_dy / edge_len;
272 let vx = -uy;
273 let vy = ux;
274
275 let mut min_u = f64::INFINITY;
277 let mut max_u = f64::NEG_INFINITY;
278 let mut min_v = f64::INFINITY;
279 let mut max_v = f64::NEG_INFINITY;
280
281 for pt in &hull {
282 let dx = pt[0] - hull[i][0];
283 let dy = pt[1] - hull[i][1];
284
285 let proj_u = dx * ux + dy * uy;
286 let proj_v = dx * vx + dy * vy;
287
288 if proj_u < min_u {
289 min_u = proj_u;
290 }
291 if proj_u > max_u {
292 max_u = proj_u;
293 }
294 if proj_v < min_v {
295 min_v = proj_v;
296 }
297 if proj_v > max_v {
298 max_v = proj_v;
299 }
300 }
301
302 let width = max_u - min_u;
303 let height = max_v - min_v;
304 let area = width * height;
305
306 if area < best_area {
307 best_area = area;
308
309 let center_u = (min_u + max_u) / 2.0;
311 let center_v = (min_v + max_v) / 2.0;
312
313 let center_x = hull[i][0] + center_u * ux + center_v * vx;
314 let center_y = hull[i][1] + center_u * uy + center_v * vy;
315
316 let hw = width / 2.0;
318 let hh = height / 2.0;
319
320 let corners = [
321 [center_x - hw * ux - hh * vx, center_y - hw * uy - hh * vy],
322 [center_x + hw * ux - hh * vx, center_y + hw * uy - hh * vy],
323 [center_x + hw * ux + hh * vx, center_y + hw * uy + hh * vy],
324 [center_x - hw * ux + hh * vx, center_y - hw * uy + hh * vy],
325 ];
326
327 let angle = uy.atan2(ux);
328
329 best_rect = Some(OrientedBoundingRect {
330 center: [center_x, center_y],
331 axis_u: [ux, uy],
332 axis_v: [vx, vy],
333 half_width: hw,
334 half_height: hh,
335 angle,
336 corners,
337 });
338 }
339 }
340
341 best_rect.ok_or_else(|| {
342 SpatialError::ComputationError("Failed to compute minimum bounding rectangle".to_string())
343 })
344}
345
346pub fn polygon_perimeter(vertices: &ArrayView2<'_, f64>) -> f64 {
367 let n = vertices.nrows();
368 if n < 2 {
369 return 0.0;
370 }
371
372 let mut perimeter = 0.0;
373 for i in 0..n {
374 let j = (i + 1) % n;
375 let dx = vertices[[j, 0]] - vertices[[i, 0]];
376 let dy = vertices[[j, 1]] - vertices[[i, 1]];
377 perimeter += (dx * dx + dy * dy).sqrt();
378 }
379
380 perimeter
381}
382
383pub fn signed_polygon_area(vertices: &ArrayView2<'_, f64>) -> f64 {
395 let n = vertices.nrows();
396 if n < 3 {
397 return 0.0;
398 }
399
400 let mut area = 0.0;
401 for i in 0..n {
402 let j = (i + 1) % n;
403 area += vertices[[i, 0]] * vertices[[j, 1]] - vertices[[j, 0]] * vertices[[i, 1]];
404 }
405
406 area / 2.0
407}
408
409pub fn convex_hull_area_perimeter(points: &ArrayView2<'_, f64>) -> SpatialResult<(f64, f64)> {
433 if points.nrows() < 3 {
434 return Err(SpatialError::ValueError(
435 "Need at least 3 points".to_string(),
436 ));
437 }
438
439 let hull = compute_convex_hull_ccw(points)?;
440 let n = hull.len();
441
442 let mut area = 0.0;
444 for i in 0..n {
445 let j = (i + 1) % n;
446 area += hull[i][0] * hull[j][1] - hull[j][0] * hull[i][1];
447 }
448 area = area.abs() / 2.0;
449
450 let mut perimeter = 0.0;
452 for i in 0..n {
453 let j = (i + 1) % n;
454 let dx = hull[j][0] - hull[i][0];
455 let dy = hull[j][1] - hull[i][1];
456 perimeter += (dx * dx + dy * dy).sqrt();
457 }
458
459 Ok((area, perimeter))
460}
461
462pub fn convex_hull_area(points: &ArrayView2<'_, f64>) -> SpatialResult<f64> {
472 let (area, _) = convex_hull_area_perimeter(points)?;
473 Ok(area)
474}
475
476pub fn convex_hull_perimeter(points: &ArrayView2<'_, f64>) -> SpatialResult<f64> {
486 let (_, perim) = convex_hull_area_perimeter(points)?;
487 Ok(perim)
488}
489
490pub fn is_convex(vertices: &ArrayView2<'_, f64>) -> bool {
500 let n = vertices.nrows();
501 if n < 3 {
502 return false;
503 }
504
505 let mut sign = 0i32;
506
507 for i in 0..n {
508 let j = (i + 1) % n;
509 let k = (i + 2) % n;
510
511 let dx1 = vertices[[j, 0]] - vertices[[i, 0]];
512 let dy1 = vertices[[j, 1]] - vertices[[i, 1]];
513 let dx2 = vertices[[k, 0]] - vertices[[j, 0]];
514 let dy2 = vertices[[k, 1]] - vertices[[j, 1]];
515
516 let cross = dx1 * dy2 - dy1 * dx2;
517
518 if cross.abs() > 1e-10 {
519 let current_sign = if cross > 0.0 { 1 } else { -1 };
520 if sign == 0 {
521 sign = current_sign;
522 } else if sign != current_sign {
523 return false;
524 }
525 }
526 }
527
528 true
529}
530
531pub fn point_set_diameter(
543 points: &ArrayView2<'_, f64>,
544) -> SpatialResult<(f64, [f64; 2], [f64; 2])> {
545 if points.nrows() < 2 {
546 return Err(SpatialError::ValueError(
547 "Need at least 2 points to compute diameter".to_string(),
548 ));
549 }
550
551 let hull = compute_convex_hull_ccw(points)?;
552 let n = hull.len();
553
554 if n < 2 {
555 return Err(SpatialError::ComputationError(
556 "Convex hull has fewer than 2 vertices".to_string(),
557 ));
558 }
559
560 let mut max_dist = 0.0;
562 let mut best_a = hull[0];
563 let mut best_b = hull[1];
564
565 let mut j = 1;
567
568 for i in 0..n {
569 let i_next = (i + 1) % n;
570
571 let edge_x = hull[i_next][0] - hull[i][0];
573 let edge_y = hull[i_next][1] - hull[i][1];
574
575 loop {
577 let j_next = (j + 1) % n;
578 let cross_curr =
579 edge_x * (hull[j_next][1] - hull[j][1]) - edge_y * (hull[j_next][0] - hull[j][0]);
580
581 if cross_curr > 0.0 {
582 j = j_next;
583 } else {
584 break;
585 }
586 }
587
588 let dx = hull[j][0] - hull[i][0];
590 let dy = hull[j][1] - hull[i][1];
591 let dist = (dx * dx + dy * dy).sqrt();
592
593 if dist > max_dist {
594 max_dist = dist;
595 best_a = hull[i];
596 best_b = hull[j];
597 }
598 }
599
600 Ok((max_dist, best_a, best_b))
601}
602
603pub fn point_set_width(points: &ArrayView2<'_, f64>) -> SpatialResult<f64> {
615 if points.nrows() < 3 {
616 return Err(SpatialError::ValueError(
617 "Need at least 3 points to compute width".to_string(),
618 ));
619 }
620
621 let hull = compute_convex_hull_ccw(points)?;
622 let n = hull.len();
623
624 let mut min_width = f64::INFINITY;
625
626 for i in 0..n {
628 let j = (i + 1) % n;
629
630 let edge_dx = hull[j][0] - hull[i][0];
631 let edge_dy = hull[j][1] - hull[i][1];
632 let edge_len = (edge_dx * edge_dx + edge_dy * edge_dy).sqrt();
633
634 if edge_len < 1e-15 {
635 continue;
636 }
637
638 let nx = -edge_dy / edge_len;
640 let ny = edge_dx / edge_len;
641
642 let mut min_proj = f64::INFINITY;
644 let mut max_proj = f64::NEG_INFINITY;
645
646 for pt in &hull {
647 let proj = pt[0] * nx + pt[1] * ny;
648 if proj < min_proj {
649 min_proj = proj;
650 }
651 if proj > max_proj {
652 max_proj = proj;
653 }
654 }
655
656 let width = max_proj - min_proj;
657 if width < min_width {
658 min_width = width;
659 }
660 }
661
662 Ok(min_width)
663}
664
665fn compute_convex_hull_ccw(points: &ArrayView2<'_, f64>) -> SpatialResult<Vec<[f64; 2]>> {
667 let n = points.nrows();
668
669 if n < 3 {
670 let mut hull = Vec::with_capacity(n);
671 for i in 0..n {
672 hull.push([points[[i, 0]], points[[i, 1]]]);
673 }
674 return Ok(hull);
675 }
676
677 let mut lowest = 0;
679 for i in 1..n {
680 if points[[i, 1]] < points[[lowest, 1]]
681 || (points[[i, 1]] == points[[lowest, 1]] && points[[i, 0]] < points[[lowest, 0]])
682 {
683 lowest = i;
684 }
685 }
686
687 let pivot_x = points[[lowest, 0]];
688 let pivot_y = points[[lowest, 1]];
689
690 let mut indexed: Vec<(usize, f64)> = (0..n)
692 .map(|i| {
693 let dx = points[[i, 0]] - pivot_x;
694 let dy = points[[i, 1]] - pivot_y;
695 let angle = dy.atan2(dx);
696 (i, angle)
697 })
698 .collect();
699
700 indexed.sort_by(|a, b| {
701 let angle_cmp = a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal);
702 if angle_cmp == std::cmp::Ordering::Equal {
703 let dist_a =
704 (points[[a.0, 0]] - pivot_x).powi(2) + (points[[a.0, 1]] - pivot_y).powi(2);
705 let dist_b =
706 (points[[b.0, 0]] - pivot_x).powi(2) + (points[[b.0, 1]] - pivot_y).powi(2);
707 dist_a
708 .partial_cmp(&dist_b)
709 .unwrap_or(std::cmp::Ordering::Equal)
710 } else {
711 angle_cmp
712 }
713 });
714
715 let mut hull_indices = vec![lowest];
717
718 for &(idx, _) in &indexed {
719 if idx == lowest {
720 continue;
721 }
722
723 while hull_indices.len() >= 2 {
724 let top = hull_indices.len() - 1;
725 let a = hull_indices[top - 1];
726 let b = hull_indices[top];
727 let c = idx;
728
729 let cross = (points[[b, 0]] - points[[a, 0]]) * (points[[c, 1]] - points[[a, 1]])
730 - (points[[b, 1]] - points[[a, 1]]) * (points[[c, 0]] - points[[a, 0]]);
731
732 if cross > 0.0 {
733 break;
734 }
735 hull_indices.pop();
736 }
737
738 hull_indices.push(idx);
739 }
740
741 let hull: Vec<[f64; 2]> = hull_indices
742 .iter()
743 .map(|&i| [points[[i, 0]], points[[i, 1]]])
744 .collect();
745
746 Ok(hull)
747}
748
749#[cfg(test)]
750mod tests {
751 use super::*;
752 use scirs2_core::ndarray::array;
753
754 #[test]
755 fn test_aabb_basic() {
756 let points = array![[0.0, 0.0], [2.0, 3.0], [1.0, 1.0], [-1.0, 2.0]];
757 let aabb = axis_aligned_bounding_box(&points.view()).expect("Operation failed");
758
759 assert!((aabb.min[0] - (-1.0)).abs() < 1e-10);
760 assert!((aabb.min[1] - 0.0).abs() < 1e-10);
761 assert!((aabb.max[0] - 2.0).abs() < 1e-10);
762 assert!((aabb.max[1] - 3.0).abs() < 1e-10);
763 assert!((aabb.area() - 9.0).abs() < 1e-10);
764 assert!((aabb.perimeter() - 12.0).abs() < 1e-10);
765 }
766
767 #[test]
768 fn test_aabb_contains() {
769 let points = array![[0.0, 0.0], [2.0, 2.0]];
770 let aabb = axis_aligned_bounding_box(&points.view()).expect("Operation failed");
771
772 assert!(aabb.contains(&[1.0, 1.0]));
773 assert!(aabb.contains(&[0.0, 0.0]));
774 assert!(aabb.contains(&[2.0, 2.0]));
775 assert!(!aabb.contains(&[3.0, 1.0]));
776 assert!(!aabb.contains(&[-1.0, 1.0]));
777 }
778
779 #[test]
780 fn test_aabb_intersects() {
781 let p1 = array![[0.0, 0.0], [2.0, 2.0]];
782 let p2 = array![[1.0, 1.0], [3.0, 3.0]];
783 let p3 = array![[5.0, 5.0], [6.0, 6.0]];
784
785 let aabb1 = axis_aligned_bounding_box(&p1.view()).expect("Operation failed");
786 let aabb2 = axis_aligned_bounding_box(&p2.view()).expect("Operation failed");
787 let aabb3 = axis_aligned_bounding_box(&p3.view()).expect("Operation failed");
788
789 assert!(aabb1.intersects(&aabb2));
790 assert!(!aabb1.intersects(&aabb3));
791 }
792
793 #[test]
794 fn test_aabb_center() {
795 let points = array![[0.0, 0.0], [4.0, 6.0]];
796 let aabb = axis_aligned_bounding_box(&points.view()).expect("Operation failed");
797 let center = aabb.center();
798 assert!((center[0] - 2.0).abs() < 1e-10);
799 assert!((center[1] - 3.0).abs() < 1e-10);
800 }
801
802 #[test]
803 fn test_aabb_empty_input() {
804 let points = Array2::<f64>::zeros((0, 2));
805 let result = axis_aligned_bounding_box(&points.view());
806 assert!(result.is_err());
807 }
808
809 #[test]
810 fn test_polygon_perimeter_square() {
811 let square = array![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
812 let perim = polygon_perimeter(&square.view());
813 assert!((perim - 4.0).abs() < 1e-10);
814 }
815
816 #[test]
817 fn test_polygon_perimeter_triangle() {
818 let triangle = array![[0.0, 0.0], [3.0, 0.0], [0.0, 4.0]];
819 let perim = polygon_perimeter(&triangle.view());
820 assert!((perim - 12.0).abs() < 1e-10);
822 }
823
824 #[test]
825 fn test_signed_area() {
826 let ccw = array![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
828 let area = signed_polygon_area(&ccw.view());
829 assert!((area - 1.0).abs() < 1e-10);
830
831 let cw = array![[0.0, 0.0], [0.0, 1.0], [1.0, 1.0], [1.0, 0.0]];
833 let area = signed_polygon_area(&cw.view());
834 assert!((area - (-1.0)).abs() < 1e-10);
835 }
836
837 #[test]
838 fn test_convex_hull_area_perimeter_square() {
839 let points = array![
840 [0.0, 0.0],
841 [1.0, 0.0],
842 [1.0, 1.0],
843 [0.0, 1.0],
844 [0.5, 0.5] ];
846
847 let (area, perim) = convex_hull_area_perimeter(&points.view()).expect("Operation failed");
848 assert!((area - 1.0).abs() < 1e-10);
849 assert!((perim - 4.0).abs() < 1e-10);
850 }
851
852 #[test]
853 fn test_convex_hull_area_function() {
854 let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]];
855 let area = convex_hull_area(&points.view()).expect("Operation failed");
856 assert!((area - 0.5).abs() < 1e-10);
857 }
858
859 #[test]
860 fn test_convex_hull_perimeter_function() {
861 let points = array![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
862 let perim = convex_hull_perimeter(&points.view()).expect("Operation failed");
863 assert!((perim - 4.0).abs() < 1e-10);
864 }
865
866 #[test]
867 fn test_minimum_bounding_rectangle_square() {
868 let points = array![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
869 let mbr = minimum_bounding_rectangle(&points.view()).expect("Operation failed");
870
871 assert!((mbr.area() - 1.0).abs() < 0.1);
873 }
874
875 #[test]
876 fn test_minimum_bounding_rectangle_triangle() {
877 let points = array![[0.0, 0.0], [2.0, 0.0], [1.0, 1.0]];
878 let mbr = minimum_bounding_rectangle(&points.view()).expect("Operation failed");
879
880 let aabb = axis_aligned_bounding_box(&points.view()).expect("Operation failed");
882 assert!(mbr.area() <= aabb.area() + 1e-6);
883 }
884
885 #[test]
886 fn test_minimum_bounding_rectangle_contains_all_points() {
887 let points = array![[0.0, 0.0], [3.0, 1.0], [2.0, 4.0], [-1.0, 3.0], [1.0, 2.0]];
888
889 let mbr = minimum_bounding_rectangle(&points.view()).expect("Operation failed");
890
891 for i in 0..points.nrows() {
893 let pt = [points[[i, 0]], points[[i, 1]]];
894 let dx = pt[0] - mbr.center[0];
896 let dy = pt[1] - mbr.center[1];
897 let proj_u = dx * mbr.axis_u[0] + dy * mbr.axis_u[1];
898 let proj_v = dx * mbr.axis_v[0] + dy * mbr.axis_v[1];
899 assert!(
900 proj_u.abs() <= mbr.half_width + 1e-6,
901 "Point {} outside MBR along u: {} > {}",
902 i,
903 proj_u.abs(),
904 mbr.half_width
905 );
906 assert!(
907 proj_v.abs() <= mbr.half_height + 1e-6,
908 "Point {} outside MBR along v: {} > {}",
909 i,
910 proj_v.abs(),
911 mbr.half_height
912 );
913 }
914 }
915
916 #[test]
917 fn test_is_convex() {
918 let convex = array![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
919 assert!(is_convex(&convex.view()));
920
921 let concave = array![
922 [0.0, 0.0],
923 [2.0, 0.0],
924 [2.0, 2.0],
925 [1.0, 0.5], [0.0, 2.0]
927 ];
928 assert!(!is_convex(&concave.view()));
929 }
930
931 #[test]
932 fn test_point_set_diameter() {
933 let points = array![[0.0, 0.0], [3.0, 4.0], [1.0, 1.0]];
934 let (diameter, _, _) = point_set_diameter(&points.view()).expect("Operation failed");
935 assert!((diameter - 5.0).abs() < 1e-10);
936 }
937
938 #[test]
939 fn test_point_set_diameter_square() {
940 let points = array![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
941 let (diameter, _, _) = point_set_diameter(&points.view()).expect("Operation failed");
942 assert!((diameter - std::f64::consts::SQRT_2).abs() < 1e-10);
944 }
945
946 #[test]
947 fn test_point_set_width() {
948 let points = array![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
949 let width = point_set_width(&points.view()).expect("Operation failed");
950 assert!((width - 1.0).abs() < 1e-10);
951 }
952
953 #[test]
954 fn test_oriented_bounding_rect_contains() {
955 let points = array![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
956 let mbr = minimum_bounding_rectangle(&points.view()).expect("Operation failed");
957
958 assert!(mbr.contains(&[0.5, 0.5]));
959 assert!(!mbr.contains(&[5.0, 5.0]));
960 }
961}