1use crate::error::{SpatialError, SpatialResult};
36
37const EPSILON: f64 = 1e-10;
39
40#[derive(Debug, Clone)]
42pub struct HullFace {
43 pub vertices: [usize; 3],
45 pub normal: [f64; 3],
47 pub distance: f64,
49 active: bool,
51}
52
53impl HullFace {
54 fn signed_distance(&self, point: &[f64; 3]) -> f64 {
58 self.normal[0] * point[0] + self.normal[1] * point[1] + self.normal[2] * point[2]
59 - self.distance
60 }
61
62 fn is_visible_from(&self, point: &[f64; 3]) -> bool {
64 self.signed_distance(point) > EPSILON
65 }
66
67 pub fn area(&self, vertices: &[[f64; 3]]) -> f64 {
69 let a = vertices[self.vertices[0]];
70 let b = vertices[self.vertices[1]];
71 let c = vertices[self.vertices[2]];
72
73 let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
74 let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
75
76 let cross = [
77 ab[1] * ac[2] - ab[2] * ac[1],
78 ab[2] * ac[0] - ab[0] * ac[2],
79 ab[0] * ac[1] - ab[1] * ac[0],
80 ];
81
82 0.5 * (cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2]).sqrt()
83 }
84}
85
86#[derive(Debug, Clone, Hash, PartialEq, Eq)]
88struct HullEdge {
89 v1: usize,
90 v2: usize,
91}
92
93impl HullEdge {
94 fn new(v1: usize, v2: usize) -> Self {
95 Self { v1, v2 }
96 }
97
98 fn reversed(&self) -> Self {
100 Self {
101 v1: self.v2,
102 v2: self.v1,
103 }
104 }
105}
106
107#[derive(Debug, Clone)]
109pub struct IncrementalHull3D {
110 points: Vec<[f64; 3]>,
112 vertex_indices: Vec<usize>,
114 faces: Vec<HullFace>,
116}
117
118impl IncrementalHull3D {
119 pub fn new(points: &[[f64; 3]]) -> SpatialResult<Self> {
145 if points.len() < 4 {
146 return Err(SpatialError::ValueError(
147 "Need at least 4 points for a 3D convex hull".to_string(),
148 ));
149 }
150
151 let mut hull = IncrementalHull3D {
152 points: points.to_vec(),
153 vertex_indices: Vec::new(),
154 faces: Vec::new(),
155 };
156
157 hull.initialize_tetrahedron()?;
159
160 for i in 0..points.len() {
162 if hull.vertex_indices.contains(&i) {
163 continue;
164 }
165 hull.add_point(i)?;
166 }
167
168 hull.faces.retain(|f| f.active);
170
171 let mut used_vertices = std::collections::HashSet::new();
173 for face in &hull.faces {
174 for &v in &face.vertices {
175 used_vertices.insert(v);
176 }
177 }
178 hull.vertex_indices = used_vertices.into_iter().collect();
179 hull.vertex_indices.sort();
180
181 Ok(hull)
182 }
183
184 fn initialize_tetrahedron(&mut self) -> SpatialResult<()> {
186 let n = self.points.len();
187
188 let p0 = 0;
191 let mut p1 = None;
192
193 for i in 1..n {
194 let d = distance_3d(&self.points[p0], &self.points[i]);
195 if d > EPSILON {
196 p1 = Some(i);
197 break;
198 }
199 }
200
201 let p1 = p1.ok_or_else(|| {
202 SpatialError::ComputationError("All points are coincident".to_string())
203 })?;
204
205 let mut p2 = None;
207 for i in 0..n {
208 if i == p0 || i == p1 {
209 continue;
210 }
211 let cross = cross_product_3d(
212 &sub_3d(&self.points[p1], &self.points[p0]),
213 &sub_3d(&self.points[i], &self.points[p0]),
214 );
215 let cross_len = norm_3d(&cross);
216 if cross_len > EPSILON {
217 p2 = Some(i);
218 break;
219 }
220 }
221
222 let p2 = p2.ok_or_else(|| {
223 SpatialError::ComputationError("All points are collinear".to_string())
224 })?;
225
226 let normal = cross_product_3d(
228 &sub_3d(&self.points[p1], &self.points[p0]),
229 &sub_3d(&self.points[p2], &self.points[p0]),
230 );
231
232 let mut p3 = None;
233 for i in 0..n {
234 if i == p0 || i == p1 || i == p2 {
235 continue;
236 }
237 let diff = sub_3d(&self.points[i], &self.points[p0]);
238 let vol = dot_3d(&normal, &diff);
239 if vol.abs() > EPSILON {
240 p3 = Some(i);
241 break;
242 }
243 }
244
245 let p3 = p3
246 .ok_or_else(|| SpatialError::ComputationError("All points are coplanar".to_string()))?;
247
248 self.vertex_indices = vec![p0, p1, p2, p3];
249
250 let centroid = [
255 (self.points[p0][0] + self.points[p1][0] + self.points[p2][0] + self.points[p3][0])
256 / 4.0,
257 (self.points[p0][1] + self.points[p1][1] + self.points[p2][1] + self.points[p3][1])
258 / 4.0,
259 (self.points[p0][2] + self.points[p1][2] + self.points[p2][2] + self.points[p3][2])
260 / 4.0,
261 ];
262
263 let face_verts = [[p0, p1, p2], [p0, p1, p3], [p0, p2, p3], [p1, p2, p3]];
264
265 for verts in &face_verts {
266 let face = self.create_face(verts[0], verts[1], verts[2], ¢roid);
267 self.faces.push(face);
268 }
269
270 Ok(())
271 }
272
273 fn create_face(&self, v0: usize, v1: usize, v2: usize, interior_point: &[f64; 3]) -> HullFace {
275 let a = self.points[v0];
276 let b = self.points[v1];
277 let c = self.points[v2];
278
279 let ab = sub_3d(&b, &a);
280 let ac = sub_3d(&c, &a);
281 let mut normal = cross_product_3d(&ab, &ac);
282
283 let normal_len = norm_3d(&normal);
284 if normal_len > EPSILON {
285 normal[0] /= normal_len;
286 normal[1] /= normal_len;
287 normal[2] /= normal_len;
288 }
289
290 let distance = dot_3d(&normal, &a);
291
292 let interior_dist = dot_3d(&normal, interior_point) - distance;
294
295 if interior_dist > 0.0 {
296 normal[0] = -normal[0];
298 normal[1] = -normal[1];
299 normal[2] = -normal[2];
300 let new_distance = -distance;
301
302 HullFace {
303 vertices: [v0, v2, v1], normal,
305 distance: new_distance,
306 active: true,
307 }
308 } else {
309 HullFace {
310 vertices: [v0, v1, v2],
311 normal,
312 distance,
313 active: true,
314 }
315 }
316 }
317
318 fn add_point(&mut self, point_idx: usize) -> SpatialResult<()> {
320 let point = self.points[point_idx];
321
322 let mut visible_faces: Vec<usize> = Vec::new();
324 for (i, face) in self.faces.iter().enumerate() {
325 if face.active && face.is_visible_from(&point) {
326 visible_faces.push(i);
327 }
328 }
329
330 if visible_faces.is_empty() {
332 return Ok(());
333 }
334
335 let horizon_edges = self.find_horizon_edges(&visible_faces);
337
338 if horizon_edges.is_empty() {
339 return Ok(());
340 }
341
342 let centroid = self.compute_centroid();
344
345 for &face_idx in &visible_faces {
347 self.faces[face_idx].active = false;
348 }
349
350 for edge in &horizon_edges {
352 let face = self.create_face(edge.v1, edge.v2, point_idx, ¢roid);
353 self.faces.push(face);
354 }
355
356 Ok(())
357 }
358
359 fn find_horizon_edges(&self, visible_faces: &[usize]) -> Vec<HullEdge> {
361 let mut edge_count: std::collections::HashMap<(usize, usize), i32> =
362 std::collections::HashMap::new();
363
364 for &face_idx in visible_faces {
365 let face = &self.faces[face_idx];
366 let v = face.vertices;
367
368 let edges = [(v[0], v[1]), (v[1], v[2]), (v[2], v[0])];
370
371 for (a, b) in edges {
372 let key = if a < b { (a, b) } else { (b, a) };
373 *edge_count.entry(key).or_insert(0) += 1;
374 }
375 }
376
377 let mut horizon = Vec::new();
380 for (&(a, b), &count) in &edge_count {
381 if count == 1 {
382 for &face_idx in visible_faces {
386 let face = &self.faces[face_idx];
387 let v = face.vertices;
388 let edges = [(v[0], v[1]), (v[1], v[2]), (v[2], v[0])];
389
390 for (ea, eb) in edges {
391 let key = if ea < eb { (ea, eb) } else { (eb, ea) };
392 if key == (a, b) {
393 horizon.push(HullEdge::new(eb, ea));
397 break;
398 }
399 }
400 }
401 }
402 }
403
404 horizon
405 }
406
407 fn compute_centroid(&self) -> [f64; 3] {
409 let active_verts: std::collections::HashSet<usize> = self
410 .faces
411 .iter()
412 .filter(|f| f.active)
413 .flat_map(|f| f.vertices.iter().copied())
414 .collect();
415
416 if active_verts.is_empty() {
417 return [0.0, 0.0, 0.0];
418 }
419
420 let n = active_verts.len() as f64;
421 let mut cx = 0.0;
422 let mut cy = 0.0;
423 let mut cz = 0.0;
424
425 for &v in &active_verts {
426 cx += self.points[v][0];
427 cy += self.points[v][1];
428 cz += self.points[v][2];
429 }
430
431 [cx / n, cy / n, cz / n]
432 }
433
434 pub fn num_vertices(&self) -> usize {
436 self.vertex_indices.len()
437 }
438
439 pub fn num_faces(&self) -> usize {
441 self.faces.len()
442 }
443
444 pub fn num_edges(&self) -> usize {
448 let v = self.num_vertices();
449 let f = self.num_faces();
450 (v + f).saturating_sub(2)
451 }
452
453 pub fn get_vertices(&self) -> Vec<[f64; 3]> {
455 self.vertex_indices
456 .iter()
457 .map(|&i| self.points[i])
458 .collect()
459 }
460
461 pub fn vertex_indices(&self) -> &[usize] {
463 &self.vertex_indices
464 }
465
466 pub fn get_faces(&self) -> &[HullFace] {
468 &self.faces
469 }
470
471 pub fn get_face_indices(&self) -> Vec<[usize; 3]> {
473 self.faces.iter().map(|f| f.vertices).collect()
474 }
475
476 pub fn volume(&self) -> f64 {
481 let mut vol = 0.0;
482
483 for face in &self.faces {
484 let a = self.points[face.vertices[0]];
485 let b = self.points[face.vertices[1]];
486 let c = self.points[face.vertices[2]];
487
488 vol += a[0] * (b[1] * c[2] - b[2] * c[1])
490 + a[1] * (b[2] * c[0] - b[0] * c[2])
491 + a[2] * (b[0] * c[1] - b[1] * c[0]);
492 }
493
494 vol.abs() / 6.0
495 }
496
497 pub fn surface_area(&self) -> f64 {
499 let mut area = 0.0;
500
501 for face in &self.faces {
502 area += face.area(&self.points);
503 }
504
505 area
506 }
507
508 pub fn contains(&self, point: &[f64; 3]) -> bool {
512 for face in &self.faces {
513 if face.signed_distance(point) > EPSILON {
514 return false;
515 }
516 }
517 true
518 }
519
520 pub fn points(&self) -> &[[f64; 3]] {
522 &self.points
523 }
524}
525
526fn sub_3d(a: &[f64; 3], b: &[f64; 3]) -> [f64; 3] {
529 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
530}
531
532fn cross_product_3d(a: &[f64; 3], b: &[f64; 3]) -> [f64; 3] {
533 [
534 a[1] * b[2] - a[2] * b[1],
535 a[2] * b[0] - a[0] * b[2],
536 a[0] * b[1] - a[1] * b[0],
537 ]
538}
539
540fn dot_3d(a: &[f64; 3], b: &[f64; 3]) -> f64 {
541 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
542}
543
544fn norm_3d(a: &[f64; 3]) -> f64 {
545 (a[0] * a[0] + a[1] * a[1] + a[2] * a[2]).sqrt()
546}
547
548fn distance_3d(a: &[f64; 3], b: &[f64; 3]) -> f64 {
549 let d = sub_3d(a, b);
550 norm_3d(&d)
551}
552
553#[cfg(test)]
554mod tests {
555 use super::*;
556
557 #[test]
558 fn test_tetrahedron() {
559 let points = vec![
560 [0.0, 0.0, 0.0],
561 [1.0, 0.0, 0.0],
562 [0.0, 1.0, 0.0],
563 [0.0, 0.0, 1.0],
564 ];
565
566 let hull = IncrementalHull3D::new(&points).expect("Operation failed");
567 assert_eq!(hull.num_vertices(), 4);
568 assert_eq!(hull.num_faces(), 4);
569 assert_eq!(hull.num_edges(), 6);
571 }
572
573 #[test]
574 fn test_tetrahedron_with_interior_point() {
575 let points = vec![
576 [0.0, 0.0, 0.0],
577 [1.0, 0.0, 0.0],
578 [0.0, 1.0, 0.0],
579 [0.0, 0.0, 1.0],
580 [0.1, 0.1, 0.1], ];
582
583 let hull = IncrementalHull3D::new(&points).expect("Operation failed");
584 assert_eq!(hull.num_vertices(), 4);
585 assert_eq!(hull.num_faces(), 4);
586 }
587
588 #[test]
589 fn test_cube() {
590 let points = vec![
591 [0.0, 0.0, 0.0],
592 [1.0, 0.0, 0.0],
593 [0.0, 1.0, 0.0],
594 [1.0, 1.0, 0.0],
595 [0.0, 0.0, 1.0],
596 [1.0, 0.0, 1.0],
597 [0.0, 1.0, 1.0],
598 [1.0, 1.0, 1.0],
599 ];
600
601 let hull = IncrementalHull3D::new(&points).expect("Operation failed");
602 assert_eq!(hull.num_vertices(), 8);
603 assert_eq!(hull.num_faces(), 12);
605 }
606
607 #[test]
608 fn test_volume_tetrahedron() {
609 let points = vec![
611 [0.0, 0.0, 0.0],
612 [1.0, 0.0, 0.0],
613 [0.0, 1.0, 0.0],
614 [0.0, 0.0, 1.0],
615 ];
616
617 let hull = IncrementalHull3D::new(&points).expect("Operation failed");
618 let vol = hull.volume();
619 assert!((vol - 1.0 / 6.0).abs() < 1e-10);
621 }
622
623 #[test]
624 fn test_volume_cube() {
625 let points = vec![
626 [0.0, 0.0, 0.0],
627 [1.0, 0.0, 0.0],
628 [0.0, 1.0, 0.0],
629 [1.0, 1.0, 0.0],
630 [0.0, 0.0, 1.0],
631 [1.0, 0.0, 1.0],
632 [0.0, 1.0, 1.0],
633 [1.0, 1.0, 1.0],
634 ];
635
636 let hull = IncrementalHull3D::new(&points).expect("Operation failed");
637 let vol = hull.volume();
638 assert!((vol - 1.0).abs() < 1e-10);
639 }
640
641 #[test]
642 fn test_surface_area_cube() {
643 let points = vec![
644 [0.0, 0.0, 0.0],
645 [1.0, 0.0, 0.0],
646 [0.0, 1.0, 0.0],
647 [1.0, 1.0, 0.0],
648 [0.0, 0.0, 1.0],
649 [1.0, 0.0, 1.0],
650 [0.0, 1.0, 1.0],
651 [1.0, 1.0, 1.0],
652 ];
653
654 let hull = IncrementalHull3D::new(&points).expect("Operation failed");
655 let sa = hull.surface_area();
656 assert!((sa - 6.0).abs() < 1e-10);
658 }
659
660 #[test]
661 fn test_contains() {
662 let points = vec![
663 [0.0, 0.0, 0.0],
664 [1.0, 0.0, 0.0],
665 [0.0, 1.0, 0.0],
666 [0.0, 0.0, 1.0],
667 ];
668
669 let hull = IncrementalHull3D::new(&points).expect("Operation failed");
670
671 assert!(hull.contains(&[0.1, 0.1, 0.1]));
673 assert!(hull.contains(&[0.1, 0.1, 0.1]));
675 assert!(!hull.contains(&[2.0, 2.0, 2.0]));
677 }
678
679 #[test]
680 fn test_too_few_points() {
681 let points = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
682 let result = IncrementalHull3D::new(&points);
683 assert!(result.is_err());
684 }
685
686 #[test]
687 fn test_coplanar_points() {
688 let points = vec![
689 [0.0, 0.0, 0.0],
690 [1.0, 0.0, 0.0],
691 [0.0, 1.0, 0.0],
692 [1.0, 1.0, 0.0],
693 ];
694 let result = IncrementalHull3D::new(&points);
695 assert!(result.is_err());
696 }
697
698 #[test]
699 fn test_get_vertices() {
700 let points = vec![
701 [0.0, 0.0, 0.0],
702 [1.0, 0.0, 0.0],
703 [0.0, 1.0, 0.0],
704 [0.0, 0.0, 1.0],
705 ];
706
707 let hull = IncrementalHull3D::new(&points).expect("Operation failed");
708 let verts = hull.get_vertices();
709 assert_eq!(verts.len(), 4);
710 }
711
712 #[test]
713 fn test_get_face_indices() {
714 let points = vec![
715 [0.0, 0.0, 0.0],
716 [1.0, 0.0, 0.0],
717 [0.0, 1.0, 0.0],
718 [0.0, 0.0, 1.0],
719 ];
720
721 let hull = IncrementalHull3D::new(&points).expect("Operation failed");
722 let face_indices = hull.get_face_indices();
723 assert_eq!(face_indices.len(), 4);
724
725 for face in &face_indices {
727 assert_ne!(face[0], face[1]);
728 assert_ne!(face[1], face[2]);
729 assert_ne!(face[0], face[2]);
730 }
731 }
732
733 #[test]
734 fn test_many_points() {
735 let points = vec![
738 [1.0, 0.0, 0.0],
739 [-1.0, 0.0, 0.0],
740 [0.0, 1.0, 0.0],
741 [0.0, -1.0, 0.0],
742 [0.0, 0.0, 1.0],
743 [0.0, 0.0, -1.0],
744 [0.0, 0.0, 0.0],
745 ];
746
747 let hull = IncrementalHull3D::new(&points).expect("Operation failed");
748 assert_eq!(hull.num_vertices(), 6); assert!(hull.contains(&[0.0, 0.0, 0.0])); }
751
752 #[test]
753 fn test_surface_area_positive() {
754 let points = vec![
755 [0.0, 0.0, 0.0],
756 [1.0, 0.0, 0.0],
757 [0.0, 1.0, 0.0],
758 [0.0, 0.0, 1.0],
759 ];
760
761 let hull = IncrementalHull3D::new(&points).expect("Operation failed");
762 assert!(hull.surface_area() > 0.0);
763 }
764
765 #[test]
766 fn test_volume_positive() {
767 let points = vec![
768 [0.0, 0.0, 0.0],
769 [1.0, 0.0, 0.0],
770 [0.0, 1.0, 0.0],
771 [0.0, 0.0, 1.0],
772 ];
773
774 let hull = IncrementalHull3D::new(&points).expect("Operation failed");
775 assert!(hull.volume() > 0.0);
776 }
777}