1#![allow(clippy::needless_range_loop, clippy::type_complexity)]
2#![allow(dead_code)]
28#![allow(clippy::too_many_arguments)]
29
30use std::io::{self, Write};
31
32#[derive(Debug, Clone, Default)]
38pub struct PointCloudPoint {
39 pub position: [f64; 3],
41 pub normal: Option<[f64; 3]>,
43 pub color: Option<[u8; 3]>,
45 pub intensity: Option<f64>,
47 pub classification: Option<u8>,
49}
50
51#[derive(Debug, Clone)]
53pub struct PointCloud {
54 pub positions: Vec<[f64; 3]>,
56 pub normals: Option<Vec<[f64; 3]>>,
58 pub colors: Option<Vec<[u8; 3]>>,
60 pub intensities: Option<Vec<f64>>,
62 pub classifications: Option<Vec<u8>>,
64}
65
66impl PointCloud {
67 pub fn new() -> Self {
69 Self {
70 positions: Vec::new(),
71 normals: None,
72 colors: None,
73 intensities: None,
74 classifications: None,
75 }
76 }
77
78 pub fn from_positions(positions: Vec<[f64; 3]>) -> Self {
80 Self {
81 positions,
82 normals: None,
83 colors: None,
84 intensities: None,
85 classifications: None,
86 }
87 }
88
89 pub fn len(&self) -> usize {
91 self.positions.len()
92 }
93
94 pub fn is_empty(&self) -> bool {
96 self.positions.is_empty()
97 }
98
99 pub fn has_normals(&self) -> bool {
101 self.normals.is_some()
102 }
103
104 pub fn has_colors(&self) -> bool {
106 self.colors.is_some()
107 }
108
109 pub fn has_intensities(&self) -> bool {
111 self.intensities.is_some()
112 }
113
114 pub fn add_point(&mut self, pos: [f64; 3]) {
116 self.positions.push(pos);
117 if let Some(ref mut n) = self.normals {
118 n.push([0.0; 3]);
119 }
120 if let Some(ref mut c) = self.colors {
121 c.push([255, 255, 255]);
122 }
123 if let Some(ref mut i) = self.intensities {
124 i.push(0.0);
125 }
126 if let Some(ref mut cl) = self.classifications {
127 cl.push(0);
128 }
129 }
130
131 pub fn add_full_point(&mut self, point: PointCloudPoint) {
133 self.positions.push(point.position);
134 if let Some(ref mut n) = self.normals {
135 n.push(point.normal.unwrap_or([0.0; 3]));
136 } else if let Some(n) = point.normal {
137 let mut norms = vec![[0.0_f64; 3]; self.positions.len() - 1];
138 norms.push(n);
139 self.normals = Some(norms);
140 }
141 if let Some(ref mut c) = self.colors {
142 c.push(point.color.unwrap_or([255, 255, 255]));
143 } else if let Some(c) = point.color {
144 let mut cols = vec![[255_u8; 3]; self.positions.len() - 1];
145 cols.push(c);
146 self.colors = Some(cols);
147 }
148 if let Some(ref mut i) = self.intensities {
149 i.push(point.intensity.unwrap_or(0.0));
150 } else if let Some(intensity) = point.intensity {
151 let mut ints = vec![0.0_f64; self.positions.len() - 1];
152 ints.push(intensity);
153 self.intensities = Some(ints);
154 }
155 if let Some(ref mut cl) = self.classifications {
156 cl.push(point.classification.unwrap_or(0));
157 }
158 }
159
160 pub fn get_point(&self, idx: usize) -> Option<PointCloudPoint> {
162 if idx >= self.positions.len() {
163 return None;
164 }
165 Some(PointCloudPoint {
166 position: self.positions[idx],
167 normal: self.normals.as_ref().map(|n| n[idx]),
168 color: self.colors.as_ref().map(|c| c[idx]),
169 intensity: self.intensities.as_ref().map(|i| i[idx]),
170 classification: self.classifications.as_ref().map(|c| c[idx]),
171 })
172 }
173
174 pub fn reserve(&mut self, n: usize) {
176 self.positions.reserve(n);
177 if let Some(ref mut normals) = self.normals {
178 normals.reserve(n);
179 }
180 if let Some(ref mut colors) = self.colors {
181 colors.reserve(n);
182 }
183 if let Some(ref mut ints) = self.intensities {
184 ints.reserve(n);
185 }
186 }
187}
188
189impl Default for PointCloud {
190 fn default() -> Self {
191 Self::new()
192 }
193}
194
195#[derive(Debug, Clone, Copy)]
201pub struct BoundingBox {
202 pub min: [f64; 3],
204 pub max: [f64; 3],
206}
207
208impl BoundingBox {
209 pub fn from_point_cloud(cloud: &PointCloud) -> Option<Self> {
211 if cloud.is_empty() {
212 return None;
213 }
214 let mut min = [f64::MAX; 3];
215 let mut max = [f64::MIN; 3];
216 for p in &cloud.positions {
217 for d in 0..3 {
218 if p[d] < min[d] {
219 min[d] = p[d];
220 }
221 if p[d] > max[d] {
222 max[d] = p[d];
223 }
224 }
225 }
226 Some(Self { min, max })
227 }
228
229 pub fn center(&self) -> [f64; 3] {
231 [
232 0.5 * (self.min[0] + self.max[0]),
233 0.5 * (self.min[1] + self.max[1]),
234 0.5 * (self.min[2] + self.max[2]),
235 ]
236 }
237
238 pub fn extents(&self) -> [f64; 3] {
240 [
241 0.5 * (self.max[0] - self.min[0]),
242 0.5 * (self.max[1] - self.min[1]),
243 0.5 * (self.max[2] - self.min[2]),
244 ]
245 }
246
247 pub fn diagonal(&self) -> f64 {
249 let dx = self.max[0] - self.min[0];
250 let dy = self.max[1] - self.min[1];
251 let dz = self.max[2] - self.min[2];
252 (dx * dx + dy * dy + dz * dz).sqrt()
253 }
254
255 pub fn volume(&self) -> f64 {
257 let dx = self.max[0] - self.min[0];
258 let dy = self.max[1] - self.min[1];
259 let dz = self.max[2] - self.min[2];
260 dx * dy * dz
261 }
262
263 pub fn contains(&self, p: [f64; 3]) -> bool {
265 p[0] >= self.min[0]
266 && p[0] <= self.max[0]
267 && p[1] >= self.min[1]
268 && p[1] <= self.max[1]
269 && p[2] >= self.min[2]
270 && p[2] <= self.max[2]
271 }
272
273 pub fn merge(&self, other: &BoundingBox) -> BoundingBox {
275 BoundingBox {
276 min: [
277 self.min[0].min(other.min[0]),
278 self.min[1].min(other.min[1]),
279 self.min[2].min(other.min[2]),
280 ],
281 max: [
282 self.max[0].max(other.max[0]),
283 self.max[1].max(other.max[1]),
284 self.max[2].max(other.max[2]),
285 ],
286 }
287 }
288}
289
290#[derive(Debug)]
296enum KdNode {
297 Leaf {
299 indices: Vec<usize>,
301 },
302 Split {
304 axis: usize,
306 split_value: f64,
308 left: Box<KdNode>,
310 right: Box<KdNode>,
312 },
313}
314
315#[derive(Debug)]
317pub struct KdTree {
318 root: Option<KdNode>,
320 positions: Vec<[f64; 3]>,
322}
323
324const KD_LEAF_SIZE: usize = 16;
326
327impl KdTree {
328 pub fn build(cloud: &PointCloud) -> Self {
330 let positions = cloud.positions.clone();
331 let indices: Vec<usize> = (0..positions.len()).collect();
332 let root = if indices.is_empty() {
333 None
334 } else {
335 Some(Self::build_recursive(&positions, indices, 0))
336 };
337 Self { root, positions }
338 }
339
340 pub fn from_positions(positions: &[[f64; 3]]) -> Self {
342 let pos = positions.to_vec();
343 let indices: Vec<usize> = (0..pos.len()).collect();
344 let root = if indices.is_empty() {
345 None
346 } else {
347 Some(Self::build_recursive(&pos, indices, 0))
348 };
349 Self {
350 root,
351 positions: pos,
352 }
353 }
354
355 fn build_recursive(positions: &[[f64; 3]], mut indices: Vec<usize>, depth: usize) -> KdNode {
356 if indices.len() <= KD_LEAF_SIZE {
357 return KdNode::Leaf { indices };
358 }
359
360 let axis = depth % 3;
361 indices.sort_by(|&a, &b| {
362 positions[a][axis]
363 .partial_cmp(&positions[b][axis])
364 .unwrap_or(std::cmp::Ordering::Equal)
365 });
366
367 let mid = indices.len() / 2;
368 let split_value = positions[indices[mid]][axis];
369
370 let left_indices = indices[..mid].to_vec();
371 let right_indices = indices[mid..].to_vec();
372
373 KdNode::Split {
374 axis,
375 split_value,
376 left: Box::new(Self::build_recursive(positions, left_indices, depth + 1)),
377 right: Box::new(Self::build_recursive(positions, right_indices, depth + 1)),
378 }
379 }
380
381 pub fn nearest(&self, query: [f64; 3]) -> Option<(usize, f64)> {
385 let root = self.root.as_ref()?;
386 let mut best_idx = 0;
387 let mut best_dist_sq = f64::MAX;
388 Self::nearest_recursive(
389 root,
390 &self.positions,
391 query,
392 &mut best_idx,
393 &mut best_dist_sq,
394 );
395 Some((best_idx, best_dist_sq))
396 }
397
398 fn nearest_recursive(
399 node: &KdNode,
400 positions: &[[f64; 3]],
401 query: [f64; 3],
402 best_idx: &mut usize,
403 best_dist_sq: &mut f64,
404 ) {
405 match node {
406 KdNode::Leaf { indices } => {
407 for &i in indices {
408 let d = dist_sq(positions[i], query);
409 if d < *best_dist_sq {
410 *best_dist_sq = d;
411 *best_idx = i;
412 }
413 }
414 }
415 KdNode::Split {
416 axis,
417 split_value,
418 left,
419 right,
420 } => {
421 let diff = query[*axis] - split_value;
422 let (first, second) = if diff <= 0.0 {
423 (left.as_ref(), right.as_ref())
424 } else {
425 (right.as_ref(), left.as_ref())
426 };
427 Self::nearest_recursive(first, positions, query, best_idx, best_dist_sq);
428 if diff * diff < *best_dist_sq {
430 Self::nearest_recursive(second, positions, query, best_idx, best_dist_sq);
431 }
432 }
433 }
434 }
435
436 pub fn k_nearest(&self, query: [f64; 3], k: usize) -> Vec<(usize, f64)> {
440 if self.root.is_none() || k == 0 {
441 return Vec::new();
442 }
443 let mut results: Vec<(usize, f64)> = Vec::with_capacity(k + 1);
444 Self::knn_recursive(
445 self.root.as_ref().expect("value should be Some"),
446 &self.positions,
447 query,
448 k,
449 &mut results,
450 );
451 results.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
452 results.truncate(k);
453 results
454 }
455
456 fn knn_recursive(
457 node: &KdNode,
458 positions: &[[f64; 3]],
459 query: [f64; 3],
460 k: usize,
461 results: &mut Vec<(usize, f64)>,
462 ) {
463 match node {
464 KdNode::Leaf { indices } => {
465 for &i in indices {
466 let d = dist_sq(positions[i], query);
467 if results.len() < k {
468 results.push((i, d));
469 results.sort_by(|a, b| {
470 a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)
471 });
472 } else if d < results[k - 1].1 {
473 results[k - 1] = (i, d);
474 results.sort_by(|a, b| {
475 a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)
476 });
477 }
478 }
479 }
480 KdNode::Split {
481 axis,
482 split_value,
483 left,
484 right,
485 } => {
486 let diff = query[*axis] - split_value;
487 let (first, second) = if diff <= 0.0 {
488 (left.as_ref(), right.as_ref())
489 } else {
490 (right.as_ref(), left.as_ref())
491 };
492 Self::knn_recursive(first, positions, query, k, results);
493 let worst_dist = if results.len() < k {
494 f64::MAX
495 } else {
496 results[results.len() - 1].1
497 };
498 if diff * diff < worst_dist {
499 Self::knn_recursive(second, positions, query, k, results);
500 }
501 }
502 }
503 }
504
505 pub fn radius_search(&self, query: [f64; 3], radius_sq: f64) -> Vec<(usize, f64)> {
507 let mut results = Vec::new();
508 if let Some(ref root) = self.root {
509 Self::radius_recursive(root, &self.positions, query, radius_sq, &mut results);
510 }
511 results.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
512 results
513 }
514
515 fn radius_recursive(
516 node: &KdNode,
517 positions: &[[f64; 3]],
518 query: [f64; 3],
519 radius_sq: f64,
520 results: &mut Vec<(usize, f64)>,
521 ) {
522 match node {
523 KdNode::Leaf { indices } => {
524 for &i in indices {
525 let d = dist_sq(positions[i], query);
526 if d <= radius_sq {
527 results.push((i, d));
528 }
529 }
530 }
531 KdNode::Split {
532 axis,
533 split_value,
534 left,
535 right,
536 } => {
537 let diff = query[*axis] - split_value;
538 let (first, second) = if diff <= 0.0 {
539 (left.as_ref(), right.as_ref())
540 } else {
541 (right.as_ref(), left.as_ref())
542 };
543 Self::radius_recursive(first, positions, query, radius_sq, results);
544 if diff * diff <= radius_sq {
545 Self::radius_recursive(second, positions, query, radius_sq, results);
546 }
547 }
548 }
549 }
550
551 pub fn len(&self) -> usize {
553 self.positions.len()
554 }
555
556 pub fn is_empty(&self) -> bool {
558 self.positions.is_empty()
559 }
560}
561
562fn dist_sq(a: [f64; 3], b: [f64; 3]) -> f64 {
564 let dx = a[0] - b[0];
565 let dy = a[1] - b[1];
566 let dz = a[2] - b[2];
567 dx * dx + dy * dy + dz * dz
568}
569
570fn dist(a: [f64; 3], b: [f64; 3]) -> f64 {
572 dist_sq(a, b).sqrt()
573}
574
575fn normalize3(v: [f64; 3]) -> [f64; 3] {
577 let len = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
578 if len < 1e-15 {
579 [0.0; 3]
580 } else {
581 [v[0] / len, v[1] / len, v[2] / len]
582 }
583}
584
585fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
587 [
588 a[1] * b[2] - a[2] * b[1],
589 a[2] * b[0] - a[0] * b[2],
590 a[0] * b[1] - a[1] * b[0],
591 ]
592}
593
594pub fn voxel_grid_downsample(cloud: &PointCloud, voxel_size: f64) -> PointCloud {
603 if cloud.is_empty() || voxel_size <= 0.0 {
604 return cloud.clone();
605 }
606
607 let inv_size = 1.0 / voxel_size;
608 let mut voxels: std::collections::HashMap<(i64, i64, i64), (Vec<usize>, [f64; 3], usize)> =
609 std::collections::HashMap::new();
610
611 for (i, p) in cloud.positions.iter().enumerate() {
612 let vx = (p[0] * inv_size).floor() as i64;
613 let vy = (p[1] * inv_size).floor() as i64;
614 let vz = (p[2] * inv_size).floor() as i64;
615 let key = (vx, vy, vz);
616 let entry = voxels
617 .entry(key)
618 .or_insert_with(|| (Vec::new(), [0.0; 3], 0));
619 entry.0.push(i);
620 entry.1[0] += p[0];
621 entry.1[1] += p[1];
622 entry.1[2] += p[2];
623 entry.2 += 1;
624 }
625
626 let mut result = PointCloud::new();
627 let has_normals = cloud.has_normals();
628 let has_colors = cloud.has_colors();
629 let has_intensities = cloud.has_intensities();
630
631 if has_normals {
632 result.normals = Some(Vec::new());
633 }
634 if has_colors {
635 result.colors = Some(Vec::new());
636 }
637 if has_intensities {
638 result.intensities = Some(Vec::new());
639 }
640
641 for (indices, sum, count) in voxels.values() {
642 let n = *count as f64;
643 let centroid = [sum[0] / n, sum[1] / n, sum[2] / n];
644 result.positions.push(centroid);
645
646 if has_normals {
647 let normals = cloud.normals.as_ref().expect("value should be Some");
648 let mut avg_n = [0.0; 3];
649 for &i in indices {
650 avg_n[0] += normals[i][0];
651 avg_n[1] += normals[i][1];
652 avg_n[2] += normals[i][2];
653 }
654 result
655 .normals
656 .as_mut()
657 .expect("value should be Some")
658 .push(normalize3(avg_n));
659 }
660
661 if has_colors {
662 let colors = cloud.colors.as_ref().expect("value should be Some");
663 let mut avg_c = [0.0_f64; 3];
664 for &i in indices {
665 avg_c[0] += colors[i][0] as f64;
666 avg_c[1] += colors[i][1] as f64;
667 avg_c[2] += colors[i][2] as f64;
668 }
669 result.colors.as_mut().expect("value should be Some").push([
670 (avg_c[0] / n).round().clamp(0.0, 255.0) as u8,
671 (avg_c[1] / n).round().clamp(0.0, 255.0) as u8,
672 (avg_c[2] / n).round().clamp(0.0, 255.0) as u8,
673 ]);
674 }
675
676 if has_intensities {
677 let ints = cloud.intensities.as_ref().expect("value should be Some");
678 let avg_i: f64 = indices.iter().map(|&i| ints[i]).sum::<f64>() / n;
679 result
680 .intensities
681 .as_mut()
682 .expect("value should be Some")
683 .push(avg_i);
684 }
685 }
686
687 result
688}
689
690pub fn statistical_outlier_removal(
700 cloud: &PointCloud,
701 k: usize,
702 std_dev_multiplier: f64,
703) -> PointCloud {
704 if cloud.len() <= k {
705 return cloud.clone();
706 }
707
708 let tree = KdTree::build(cloud);
709 let mut mean_dists = Vec::with_capacity(cloud.len());
710
711 for (i, pos) in cloud.positions.iter().enumerate() {
712 let neighbors = tree.k_nearest(*pos, k + 1);
713 let sum: f64 = neighbors
714 .iter()
715 .filter(|(idx, _)| *idx != i)
716 .take(k)
717 .map(|(_, d_sq)| d_sq.sqrt())
718 .sum();
719 mean_dists.push(sum / k as f64);
720 }
721
722 let global_mean: f64 = mean_dists.iter().sum::<f64>() / mean_dists.len() as f64;
723 let variance: f64 = mean_dists
724 .iter()
725 .map(|d| (d - global_mean).powi(2))
726 .sum::<f64>()
727 / mean_dists.len() as f64;
728 let std_dev = variance.sqrt();
729 let threshold = global_mean + std_dev_multiplier * std_dev;
730
731 let mut result = PointCloud::new();
732 if cloud.has_normals() {
733 result.normals = Some(Vec::new());
734 }
735 if cloud.has_colors() {
736 result.colors = Some(Vec::new());
737 }
738 if cloud.has_intensities() {
739 result.intensities = Some(Vec::new());
740 }
741
742 for (i, &md) in mean_dists.iter().enumerate() {
743 if md <= threshold {
744 result.positions.push(cloud.positions[i]);
745 if let Some(ref normals) = cloud.normals {
746 result
747 .normals
748 .as_mut()
749 .expect("value should be Some")
750 .push(normals[i]);
751 }
752 if let Some(ref colors) = cloud.colors {
753 result
754 .colors
755 .as_mut()
756 .expect("value should be Some")
757 .push(colors[i]);
758 }
759 if let Some(ref ints) = cloud.intensities {
760 result
761 .intensities
762 .as_mut()
763 .expect("value should be Some")
764 .push(ints[i]);
765 }
766 }
767 }
768
769 result
770}
771
772pub fn estimate_normals(cloud: &PointCloud, k: usize) -> Vec<[f64; 3]> {
783 if cloud.is_empty() {
784 return Vec::new();
785 }
786
787 let tree = KdTree::build(cloud);
788 let mut normals = Vec::with_capacity(cloud.len());
789
790 let centroid = compute_centroid(cloud);
792
793 for (i, pos) in cloud.positions.iter().enumerate() {
794 let neighbors = tree.k_nearest(*pos, k + 1);
795 let neighbor_positions: Vec<[f64; 3]> = neighbors
796 .iter()
797 .filter(|(idx, _)| *idx != i)
798 .take(k)
799 .map(|(idx, _)| cloud.positions[*idx])
800 .collect();
801
802 if neighbor_positions.len() < 3 {
803 normals.push([0.0, 0.0, 1.0]);
804 continue;
805 }
806
807 let n_neigh = neighbor_positions.len() as f64;
809 let mut mean = [0.0; 3];
810 for np in &neighbor_positions {
811 mean[0] += np[0];
812 mean[1] += np[1];
813 mean[2] += np[2];
814 }
815 mean[0] /= n_neigh;
816 mean[1] /= n_neigh;
817 mean[2] /= n_neigh;
818
819 let mut cov = [[0.0_f64; 3]; 3];
821 for np in &neighbor_positions {
822 let d = [np[0] - mean[0], np[1] - mean[1], np[2] - mean[2]];
823 for r in 0..3 {
824 for c in 0..3 {
825 cov[r][c] += d[r] * d[c];
826 }
827 }
828 }
829 for r in 0..3 {
830 for c in 0..3 {
831 cov[r][c] /= n_neigh;
832 }
833 }
834
835 let normal = smallest_eigenvector_3x3(cov);
837
838 let to_centroid = [
840 centroid[0] - pos[0],
841 centroid[1] - pos[1],
842 centroid[2] - pos[2],
843 ];
844 let dot_val =
845 normal[0] * to_centroid[0] + normal[1] * to_centroid[1] + normal[2] * to_centroid[2];
846 if dot_val > 0.0 {
847 normals.push([-normal[0], -normal[1], -normal[2]]);
848 } else {
849 normals.push(normal);
850 }
851 }
852
853 normals
854}
855
856pub fn compute_centroid(cloud: &PointCloud) -> [f64; 3] {
858 if cloud.is_empty() {
859 return [0.0; 3];
860 }
861 let n = cloud.len() as f64;
862 let mut c = [0.0; 3];
863 for p in &cloud.positions {
864 c[0] += p[0];
865 c[1] += p[1];
866 c[2] += p[2];
867 }
868 [c[0] / n, c[1] / n, c[2] / n]
869}
870
871fn smallest_eigenvector_3x3(m: [[f64; 3]; 3]) -> [f64; 3] {
874 let mut v = [1.0, 0.0, 0.0];
876 for _ in 0..20 {
877 let mv = mat_vec_3x3(m, v);
878 let len = (mv[0] * mv[0] + mv[1] * mv[1] + mv[2] * mv[2]).sqrt();
879 if len < 1e-15 {
880 return v;
881 }
882 v = [mv[0] / len, mv[1] / len, mv[2] / len];
883 }
884 let max_eval =
885 v[0] * mat_vec_3x3(m, v)[0] + v[1] * mat_vec_3x3(m, v)[1] + v[2] * mat_vec_3x3(m, v)[2];
886
887 let shift = max_eval + 1.0; let b = [
890 [shift - m[0][0], -m[0][1], -m[0][2]],
891 [-m[1][0], shift - m[1][1], -m[1][2]],
892 [-m[2][0], -m[2][1], shift - m[2][2]],
893 ];
894
895 let mut w = [0.577, 0.577, 0.577]; for _ in 0..30 {
898 let bw = mat_vec_3x3(b, w);
899 let len = (bw[0] * bw[0] + bw[1] * bw[1] + bw[2] * bw[2]).sqrt();
900 if len < 1e-15 {
901 return w;
902 }
903 w = [bw[0] / len, bw[1] / len, bw[2] / len];
904 }
905 w
906}
907
908fn mat_vec_3x3(m: [[f64; 3]; 3], v: [f64; 3]) -> [f64; 3] {
910 [
911 m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2],
912 m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2],
913 m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2],
914 ]
915}
916
917pub fn merge_point_clouds(a: &PointCloud, b: &PointCloud) -> PointCloud {
923 let mut result = PointCloud::new();
924 result.positions = a
925 .positions
926 .iter()
927 .chain(b.positions.iter())
928 .copied()
929 .collect();
930
931 if a.has_normals() || b.has_normals() {
932 let mut normals = Vec::with_capacity(a.len() + b.len());
933 if let Some(ref na) = a.normals {
934 normals.extend_from_slice(na);
935 } else {
936 normals.extend(std::iter::repeat_n([0.0_f64; 3], a.len()));
937 }
938 if let Some(ref nb) = b.normals {
939 normals.extend_from_slice(nb);
940 } else {
941 normals.extend(std::iter::repeat_n([0.0_f64; 3], b.len()));
942 }
943 result.normals = Some(normals);
944 }
945
946 if a.has_colors() || b.has_colors() {
947 let mut colors = Vec::with_capacity(a.len() + b.len());
948 if let Some(ref ca) = a.colors {
949 colors.extend_from_slice(ca);
950 } else {
951 colors.extend(std::iter::repeat_n([255_u8; 3], a.len()));
952 }
953 if let Some(ref cb) = b.colors {
954 colors.extend_from_slice(cb);
955 } else {
956 colors.extend(std::iter::repeat_n([255_u8; 3], b.len()));
957 }
958 result.colors = Some(colors);
959 }
960
961 if a.has_intensities() || b.has_intensities() {
962 let mut ints = Vec::with_capacity(a.len() + b.len());
963 if let Some(ref ia) = a.intensities {
964 ints.extend_from_slice(ia);
965 } else {
966 ints.extend(std::iter::repeat_n(0.0_f64, a.len()));
967 }
968 if let Some(ref ib) = b.intensities {
969 ints.extend_from_slice(ib);
970 } else {
971 ints.extend(std::iter::repeat_n(0.0_f64, b.len()));
972 }
973 result.intensities = Some(ints);
974 }
975
976 result
977}
978
979pub fn transform_point_cloud(
987 cloud: &PointCloud,
988 rotation: [[f64; 3]; 3],
989 translation: [f64; 3],
990) -> PointCloud {
991 let mut result = cloud.clone();
992 for pos in &mut result.positions {
993 let rotated = mat_vec_3x3(rotation, *pos);
994 *pos = [
995 rotated[0] + translation[0],
996 rotated[1] + translation[1],
997 rotated[2] + translation[2],
998 ];
999 }
1000 if let Some(ref mut normals) = result.normals {
1001 for n in normals.iter_mut() {
1002 *n = normalize3(mat_vec_3x3(rotation, *n));
1003 }
1004 }
1005 result
1006}
1007
1008pub fn scale_point_cloud(cloud: &PointCloud, scale: f64) -> PointCloud {
1010 let mut result = cloud.clone();
1011 for pos in &mut result.positions {
1012 pos[0] *= scale;
1013 pos[1] *= scale;
1014 pos[2] *= scale;
1015 }
1016 result
1017}
1018
1019pub fn translate_point_cloud(cloud: &PointCloud, offset: [f64; 3]) -> PointCloud {
1021 let mut result = cloud.clone();
1022 for pos in &mut result.positions {
1023 pos[0] += offset[0];
1024 pos[1] += offset[1];
1025 pos[2] += offset[2];
1026 }
1027 result
1028}
1029
1030pub fn center_point_cloud(cloud: &PointCloud) -> PointCloud {
1032 let c = compute_centroid(cloud);
1033 translate_point_cloud(cloud, [-c[0], -c[1], -c[2]])
1034}
1035
1036#[derive(Debug, Clone, Copy, PartialEq)]
1042pub enum PlyEncoding {
1043 Ascii,
1045 BinaryLittleEndian,
1047}
1048
1049pub fn write_ply_ascii(cloud: &PointCloud, writer: &mut dyn Write) -> io::Result<()> {
1051 writeln!(writer, "ply")?;
1052 writeln!(writer, "format ascii 1.0")?;
1053 writeln!(writer, "element vertex {}", cloud.len())?;
1054 writeln!(writer, "property float x")?;
1055 writeln!(writer, "property float y")?;
1056 writeln!(writer, "property float z")?;
1057 if cloud.has_normals() {
1058 writeln!(writer, "property float nx")?;
1059 writeln!(writer, "property float ny")?;
1060 writeln!(writer, "property float nz")?;
1061 }
1062 if cloud.has_colors() {
1063 writeln!(writer, "property uchar red")?;
1064 writeln!(writer, "property uchar green")?;
1065 writeln!(writer, "property uchar blue")?;
1066 }
1067 if cloud.has_intensities() {
1068 writeln!(writer, "property float intensity")?;
1069 }
1070 writeln!(writer, "end_header")?;
1071
1072 for i in 0..cloud.len() {
1073 let p = cloud.positions[i];
1074 write!(writer, "{:.6} {:.6} {:.6}", p[0], p[1], p[2])?;
1075 if let Some(ref normals) = cloud.normals {
1076 let n = normals[i];
1077 write!(writer, " {:.6} {:.6} {:.6}", n[0], n[1], n[2])?;
1078 }
1079 if let Some(ref colors) = cloud.colors {
1080 let c = colors[i];
1081 write!(writer, " {} {} {}", c[0], c[1], c[2])?;
1082 }
1083 if let Some(ref ints) = cloud.intensities {
1084 write!(writer, " {:.6}", ints[i])?;
1085 }
1086 writeln!(writer)?;
1087 }
1088 Ok(())
1089}
1090
1091pub fn write_ply_binary_le(cloud: &PointCloud) -> Vec<u8> {
1093 let mut buf = Vec::new();
1094
1095 let header = format!(
1096 "ply\nformat binary_little_endian 1.0\nelement vertex {}\nproperty float x\nproperty float y\nproperty float z\nend_header\n",
1097 cloud.len()
1098 );
1099 buf.extend_from_slice(header.as_bytes());
1100
1101 for p in &cloud.positions {
1102 buf.extend_from_slice(&(p[0] as f32).to_le_bytes());
1103 buf.extend_from_slice(&(p[1] as f32).to_le_bytes());
1104 buf.extend_from_slice(&(p[2] as f32).to_le_bytes());
1105 }
1106 buf
1107}
1108
1109pub fn parse_ply_ascii(data: &str) -> Option<PointCloud> {
1111 let mut lines = data.lines();
1112 let first = lines.next()?;
1113 if first.trim() != "ply" {
1114 return None;
1115 }
1116
1117 let mut vertex_count = 0usize;
1118 let mut has_normals = false;
1119 let mut has_colors = false;
1120 let mut _has_intensity = false;
1121 let mut in_header = true;
1122
1123 while in_header {
1124 let line = lines.next()?;
1125 let line = line.trim();
1126 if line.starts_with("element vertex") {
1127 vertex_count = line.split_whitespace().last()?.parse().ok()?;
1128 } else if line.starts_with("property") {
1129 if line.contains(" nx") || line.contains(" ny") || line.contains(" nz") {
1130 has_normals = true;
1131 }
1132 if line.contains(" red") || line.contains(" green") || line.contains(" blue") {
1133 has_colors = true;
1134 }
1135 if line.contains(" intensity") {
1136 _has_intensity = true;
1137 }
1138 } else if line == "end_header" {
1139 in_header = false;
1140 }
1141 }
1142
1143 let mut cloud = PointCloud::new();
1144 cloud.positions.reserve(vertex_count);
1145 if has_normals {
1146 cloud.normals = Some(Vec::with_capacity(vertex_count));
1147 }
1148 if has_colors {
1149 cloud.colors = Some(Vec::with_capacity(vertex_count));
1150 }
1151
1152 for _ in 0..vertex_count {
1153 let line = lines.next()?;
1154 let vals: Vec<&str> = line.split_whitespace().collect();
1155 if vals.len() < 3 {
1156 continue;
1157 }
1158 let x: f64 = vals[0].parse().ok()?;
1159 let y: f64 = vals[1].parse().ok()?;
1160 let z: f64 = vals[2].parse().ok()?;
1161 cloud.positions.push([x, y, z]);
1162
1163 let mut idx = 3;
1164 if has_normals && vals.len() >= idx + 3 {
1165 let nx: f64 = vals[idx].parse().ok()?;
1166 let ny: f64 = vals[idx + 1].parse().ok()?;
1167 let nz: f64 = vals[idx + 2].parse().ok()?;
1168 cloud
1169 .normals
1170 .as_mut()
1171 .expect("value should be Some")
1172 .push([nx, ny, nz]);
1173 idx += 3;
1174 }
1175 if has_colors && vals.len() >= idx + 3 {
1176 let r: u8 = vals[idx].parse().ok()?;
1177 let g: u8 = vals[idx + 1].parse().ok()?;
1178 let b: u8 = vals[idx + 2].parse().ok()?;
1179 cloud
1180 .colors
1181 .as_mut()
1182 .expect("value should be Some")
1183 .push([r, g, b]);
1184 }
1185 }
1186
1187 Some(cloud)
1188}
1189
1190pub fn write_pcd_ascii(cloud: &PointCloud, writer: &mut dyn Write) -> io::Result<()> {
1196 writeln!(writer, "# .PCD v0.7 - Point Cloud Data")?;
1197 writeln!(writer, "VERSION 0.7")?;
1198 writeln!(writer, "FIELDS x y z")?;
1199 writeln!(writer, "SIZE 4 4 4")?;
1200 writeln!(writer, "TYPE F F F")?;
1201 writeln!(writer, "COUNT 1 1 1")?;
1202 writeln!(writer, "WIDTH {}", cloud.len())?;
1203 writeln!(writer, "HEIGHT 1")?;
1204 writeln!(writer, "VIEWPOINT 0 0 0 1 0 0 0")?;
1205 writeln!(writer, "POINTS {}", cloud.len())?;
1206 writeln!(writer, "DATA ascii")?;
1207
1208 for p in &cloud.positions {
1209 writeln!(writer, "{:.6} {:.6} {:.6}", p[0], p[1], p[2])?;
1210 }
1211 Ok(())
1212}
1213
1214pub fn parse_pcd_ascii(data: &str) -> Option<PointCloud> {
1216 let mut lines = data.lines();
1217 let mut num_points = 0usize;
1218 let mut data_started = false;
1219
1220 let mut cloud = PointCloud::new();
1221
1222 for line in &mut lines {
1223 let line = line.trim();
1224 if line.starts_with("POINTS") {
1225 num_points = line.split_whitespace().last()?.parse().ok()?;
1226 cloud.positions.reserve(num_points);
1227 } else if line.starts_with("DATA") {
1228 data_started = true;
1229 break;
1230 }
1231 }
1232
1233 if !data_started {
1234 return None;
1235 }
1236
1237 for _ in 0..num_points {
1238 let line = lines.next()?;
1239 let vals: Vec<&str> = line.split_whitespace().collect();
1240 if vals.len() < 3 {
1241 continue;
1242 }
1243 let x: f64 = vals[0].parse().ok()?;
1244 let y: f64 = vals[1].parse().ok()?;
1245 let z: f64 = vals[2].parse().ok()?;
1246 cloud.positions.push([x, y, z]);
1247 }
1248
1249 Some(cloud)
1250}
1251
1252pub fn write_xyz(cloud: &PointCloud, writer: &mut dyn Write) -> io::Result<()> {
1258 for i in 0..cloud.len() {
1259 let p = cloud.positions[i];
1260 write!(writer, "{:.6} {:.6} {:.6}", p[0], p[1], p[2])?;
1261 if let Some(ref normals) = cloud.normals {
1262 let n = normals[i];
1263 write!(writer, " {:.6} {:.6} {:.6}", n[0], n[1], n[2])?;
1264 }
1265 if let Some(ref colors) = cloud.colors {
1266 let c = colors[i];
1267 write!(writer, " {} {} {}", c[0], c[1], c[2])?;
1268 }
1269 writeln!(writer)?;
1270 }
1271 Ok(())
1272}
1273
1274pub fn parse_xyz(data: &str) -> PointCloud {
1276 let mut cloud = PointCloud::new();
1277 let mut first_line_fields = 0;
1278
1279 for (line_idx, line) in data.lines().enumerate() {
1280 let line = line.trim();
1281 if line.is_empty() || line.starts_with('#') {
1282 continue;
1283 }
1284 let vals: Vec<&str> = line.split_whitespace().collect();
1285 if vals.len() < 3 {
1286 continue;
1287 }
1288
1289 if line_idx == 0 || first_line_fields == 0 {
1290 first_line_fields = vals.len();
1291 if first_line_fields >= 6 {
1292 cloud.normals = Some(Vec::new());
1293 }
1294 if first_line_fields >= 9 {
1295 cloud.colors = Some(Vec::new());
1296 }
1297 }
1298
1299 let x: f64 = vals[0].parse().unwrap_or(0.0);
1300 let y: f64 = vals[1].parse().unwrap_or(0.0);
1301 let z: f64 = vals[2].parse().unwrap_or(0.0);
1302 cloud.positions.push([x, y, z]);
1303
1304 if vals.len() >= 6
1305 && let Some(ref mut normals) = cloud.normals
1306 {
1307 let nx: f64 = vals[3].parse().unwrap_or(0.0);
1308 let ny: f64 = vals[4].parse().unwrap_or(0.0);
1309 let nz: f64 = vals[5].parse().unwrap_or(0.0);
1310 normals.push([nx, ny, nz]);
1311 }
1312
1313 if vals.len() >= 9
1314 && let Some(ref mut colors) = cloud.colors
1315 {
1316 let r: u8 = vals[6].parse().unwrap_or(0);
1317 let g: u8 = vals[7].parse().unwrap_or(0);
1318 let b: u8 = vals[8].parse().unwrap_or(0);
1319 colors.push([r, g, b]);
1320 }
1321 }
1322 cloud
1323}
1324
1325#[derive(Debug, Clone)]
1331pub struct LasHeader {
1332 pub file_signature: String,
1334 pub file_source_id: u16,
1336 pub version_major: u8,
1338 pub version_minor: u8,
1340 pub point_format: u8,
1342 pub point_record_length: u16,
1344 pub num_points: u64,
1346 pub scale: [f64; 3],
1348 pub offset: [f64; 3],
1350 pub min_bounds: [f64; 3],
1352 pub max_bounds: [f64; 3],
1354}
1355
1356impl LasHeader {
1357 pub fn mock(num_points: u64) -> Self {
1359 Self {
1360 file_signature: "LASF".to_string(),
1361 file_source_id: 0,
1362 version_major: 1,
1363 version_minor: 4,
1364 point_format: 0,
1365 point_record_length: 20,
1366 num_points,
1367 scale: [0.001, 0.001, 0.001],
1368 offset: [0.0, 0.0, 0.0],
1369 min_bounds: [0.0, 0.0, 0.0],
1370 max_bounds: [100.0, 100.0, 50.0],
1371 }
1372 }
1373
1374 pub fn from_bytes(data: &[u8]) -> Option<Self> {
1376 if data.len() < 227 {
1377 return None;
1378 }
1379 let sig = std::str::from_utf8(&data[0..4]).ok()?;
1380 if sig != "LASF" {
1381 return None;
1382 }
1383 let file_source_id = u16::from_le_bytes([data[4], data[5]]);
1384 let version_major = data[24];
1385 let version_minor = data[25];
1386 let point_format = data[104];
1387 let point_record_length = u16::from_le_bytes([data[105], data[106]]);
1388
1389 let num_points_legacy =
1391 u32::from_le_bytes([data[107], data[108], data[109], data[110]]) as u64;
1392
1393 let parse_f64 = |offset: usize| -> f64 {
1394 if offset + 8 > data.len() {
1395 return 0.0;
1396 }
1397 let bytes: [u8; 8] = data[offset..offset + 8].try_into().unwrap_or([0; 8]);
1398 f64::from_le_bytes(bytes)
1399 };
1400
1401 let scale = [parse_f64(131), parse_f64(139), parse_f64(147)];
1402 let offset = [parse_f64(155), parse_f64(163), parse_f64(171)];
1403 let max_x = parse_f64(179);
1404 let min_x = parse_f64(187);
1405 let max_y = parse_f64(195);
1406 let min_y = parse_f64(203);
1407 let max_z = parse_f64(211);
1408 let min_z = parse_f64(219);
1409
1410 Some(Self {
1411 file_signature: sig.to_string(),
1412 file_source_id,
1413 version_major,
1414 version_minor,
1415 point_format,
1416 point_record_length,
1417 num_points: num_points_legacy,
1418 scale,
1419 offset,
1420 min_bounds: [min_x, min_y, min_z],
1421 max_bounds: [max_x, max_y, max_z],
1422 })
1423 }
1424
1425 pub fn bounding_volume(&self) -> f64 {
1427 let dx = self.max_bounds[0] - self.min_bounds[0];
1428 let dy = self.max_bounds[1] - self.min_bounds[1];
1429 let dz = self.max_bounds[2] - self.min_bounds[2];
1430 dx.abs() * dy.abs() * dz.abs()
1431 }
1432}
1433
1434#[derive(Debug, Clone)]
1440pub struct E57ScanHeader {
1441 pub name: String,
1443 pub num_points: u64,
1445 pub has_color: bool,
1447 pub has_intensity: bool,
1449 pub scanner_position: [f64; 3],
1451 pub scanner_orientation: [f64; 4],
1453}
1454
1455impl E57ScanHeader {
1456 pub fn mock(name: &str, num_points: u64) -> Self {
1458 Self {
1459 name: name.to_string(),
1460 num_points,
1461 has_color: true,
1462 has_intensity: true,
1463 scanner_position: [0.0; 3],
1464 scanner_orientation: [0.0, 0.0, 0.0, 1.0],
1465 }
1466 }
1467}
1468
1469#[derive(Debug, Clone)]
1471pub struct E57File {
1472 pub version_major: u32,
1474 pub version_minor: u32,
1476 pub scans: Vec<E57ScanHeader>,
1478}
1479
1480impl E57File {
1481 pub fn mock() -> Self {
1483 Self {
1484 version_major: 1,
1485 version_minor: 0,
1486 scans: vec![E57ScanHeader::mock("scan_001", 1000)],
1487 }
1488 }
1489
1490 pub fn total_points(&self) -> u64 {
1492 self.scans.iter().map(|s| s.num_points).sum()
1493 }
1494
1495 pub fn num_scans(&self) -> usize {
1497 self.scans.len()
1498 }
1499}
1500
1501#[derive(Debug, Clone, Copy)]
1507pub struct Triangle {
1508 pub indices: [usize; 3],
1510}
1511
1512pub fn ball_pivoting_mesh(cloud: &PointCloud, ball_radius: f64) -> Vec<Triangle> {
1519 if cloud.len() < 3 {
1520 return Vec::new();
1521 }
1522
1523 let tree = KdTree::build(cloud);
1524 let mut triangles = Vec::new();
1525 let mut used = vec![false; cloud.len()];
1526 let search_radius_sq = (2.0 * ball_radius) * (2.0 * ball_radius);
1527
1528 let neighbors = tree.k_nearest(cloud.positions[0], 20.min(cloud.len()));
1530 let mut found_seed = false;
1531 let mut seed = [0usize; 3];
1532
1533 'outer: for i in 0..neighbors.len() {
1534 for j in (i + 1)..neighbors.len() {
1535 for k in (j + 1)..neighbors.len() {
1536 let a = neighbors[i].0;
1537 let b = neighbors[j].0;
1538 let c = neighbors[k].0;
1539 let ab = dist(cloud.positions[a], cloud.positions[b]);
1540 let bc = dist(cloud.positions[b], cloud.positions[c]);
1541 let ca = dist(cloud.positions[c], cloud.positions[a]);
1542 if ab < 2.0 * ball_radius && bc < 2.0 * ball_radius && ca < 2.0 * ball_radius {
1543 seed = [a, b, c];
1544 found_seed = true;
1545 break 'outer;
1546 }
1547 }
1548 }
1549 }
1550
1551 if !found_seed {
1552 return triangles;
1553 }
1554
1555 triangles.push(Triangle { indices: seed });
1556 used[seed[0]] = true;
1557 used[seed[1]] = true;
1558 used[seed[2]] = true;
1559
1560 let max_triangles = cloud.len().min(5000);
1563 let mut edge_queue: Vec<(usize, usize)> =
1564 vec![(seed[0], seed[1]), (seed[1], seed[2]), (seed[2], seed[0])];
1565
1566 while let Some((a, b)) = edge_queue.pop() {
1567 if triangles.len() >= max_triangles {
1568 break;
1569 }
1570 let mid = [
1571 0.5 * (cloud.positions[a][0] + cloud.positions[b][0]),
1572 0.5 * (cloud.positions[a][1] + cloud.positions[b][1]),
1573 0.5 * (cloud.positions[a][2] + cloud.positions[b][2]),
1574 ];
1575 let nearby = tree.radius_search(mid, search_radius_sq);
1576 for (c, _) in nearby {
1577 if c == a || c == b || used[c] {
1578 continue;
1579 }
1580 let ac = dist(cloud.positions[a], cloud.positions[c]);
1581 let bc = dist(cloud.positions[b], cloud.positions[c]);
1582 if ac < 2.0 * ball_radius && bc < 2.0 * ball_radius {
1583 triangles.push(Triangle { indices: [a, b, c] });
1584 used[c] = true;
1585 edge_queue.push((a, c));
1586 edge_queue.push((c, b));
1587 break;
1588 }
1589 }
1590 }
1591
1592 triangles
1593}
1594
1595#[cfg(test)]
1600mod tests {
1601 use super::*;
1602
1603 fn make_cube_cloud(n_per_face: usize) -> PointCloud {
1604 let mut cloud = PointCloud::new();
1605 let step = 1.0 / n_per_face as f64;
1606 for i in 0..n_per_face {
1608 for j in 0..n_per_face {
1609 let u = i as f64 * step;
1610 let v = j as f64 * step;
1611 cloud.positions.push([u, v, 0.0]); cloud.positions.push([u, v, 1.0]); cloud.positions.push([0.0, u, v]); cloud.positions.push([1.0, u, v]); cloud.positions.push([u, 0.0, v]); cloud.positions.push([u, 1.0, v]); }
1618 }
1619 cloud
1620 }
1621
1622 fn make_simple_cloud() -> PointCloud {
1623 PointCloud::from_positions(vec![
1624 [0.0, 0.0, 0.0],
1625 [1.0, 0.0, 0.0],
1626 [0.0, 1.0, 0.0],
1627 [0.0, 0.0, 1.0],
1628 [1.0, 1.0, 0.0],
1629 [1.0, 0.0, 1.0],
1630 [0.0, 1.0, 1.0],
1631 [1.0, 1.0, 1.0],
1632 ])
1633 }
1634
1635 #[test]
1636 fn test_point_cloud_new() {
1637 let cloud = PointCloud::new();
1638 assert!(cloud.is_empty());
1639 assert_eq!(cloud.len(), 0);
1640 }
1641
1642 #[test]
1643 fn test_point_cloud_from_positions() {
1644 let cloud = make_simple_cloud();
1645 assert_eq!(cloud.len(), 8);
1646 assert!(!cloud.is_empty());
1647 assert!(!cloud.has_normals());
1648 assert!(!cloud.has_colors());
1649 }
1650
1651 #[test]
1652 fn test_point_cloud_add_point() {
1653 let mut cloud = PointCloud::new();
1654 cloud.add_point([1.0, 2.0, 3.0]);
1655 assert_eq!(cloud.len(), 1);
1656 assert!((cloud.positions[0][0] - 1.0).abs() < 1e-10);
1657 }
1658
1659 #[test]
1660 fn test_point_cloud_get_point() {
1661 let cloud = make_simple_cloud();
1662 let pt = cloud.get_point(0).unwrap();
1663 assert!((pt.position[0]).abs() < 1e-10);
1664 assert!(cloud.get_point(100).is_none());
1665 }
1666
1667 #[test]
1668 fn test_bounding_box_basic() {
1669 let cloud = make_simple_cloud();
1670 let bb = BoundingBox::from_point_cloud(&cloud).unwrap();
1671 assert!((bb.min[0]).abs() < 1e-10);
1672 assert!((bb.max[0] - 1.0).abs() < 1e-10);
1673 assert!((bb.max[2] - 1.0).abs() < 1e-10);
1674 }
1675
1676 #[test]
1677 fn test_bounding_box_center() {
1678 let cloud = make_simple_cloud();
1679 let bb = BoundingBox::from_point_cloud(&cloud).unwrap();
1680 let c = bb.center();
1681 assert!((c[0] - 0.5).abs() < 1e-10);
1682 assert!((c[1] - 0.5).abs() < 1e-10);
1683 assert!((c[2] - 0.5).abs() < 1e-10);
1684 }
1685
1686 #[test]
1687 fn test_bounding_box_volume() {
1688 let cloud = make_simple_cloud();
1689 let bb = BoundingBox::from_point_cloud(&cloud).unwrap();
1690 assert!((bb.volume() - 1.0).abs() < 1e-10);
1691 }
1692
1693 #[test]
1694 fn test_bounding_box_diagonal() {
1695 let cloud = make_simple_cloud();
1696 let bb = BoundingBox::from_point_cloud(&cloud).unwrap();
1697 let expected = (3.0_f64).sqrt();
1698 assert!((bb.diagonal() - expected).abs() < 1e-10);
1699 }
1700
1701 #[test]
1702 fn test_bounding_box_contains() {
1703 let cloud = make_simple_cloud();
1704 let bb = BoundingBox::from_point_cloud(&cloud).unwrap();
1705 assert!(bb.contains([0.5, 0.5, 0.5]));
1706 assert!(!bb.contains([2.0, 0.0, 0.0]));
1707 }
1708
1709 #[test]
1710 fn test_bounding_box_merge() {
1711 let bb1 = BoundingBox {
1712 min: [0.0, 0.0, 0.0],
1713 max: [1.0, 1.0, 1.0],
1714 };
1715 let bb2 = BoundingBox {
1716 min: [-1.0, -1.0, -1.0],
1717 max: [0.5, 0.5, 0.5],
1718 };
1719 let merged = bb1.merge(&bb2);
1720 assert!((merged.min[0] + 1.0).abs() < 1e-10);
1721 assert!((merged.max[0] - 1.0).abs() < 1e-10);
1722 }
1723
1724 #[test]
1725 fn test_kdtree_nearest() {
1726 let cloud = make_simple_cloud();
1727 let tree = KdTree::build(&cloud);
1728 let (idx, d_sq) = tree.nearest([0.1, 0.1, 0.1]).unwrap();
1729 assert_eq!(idx, 0); assert!(d_sq < 0.1);
1731 }
1732
1733 #[test]
1734 fn test_kdtree_k_nearest() {
1735 let cloud = make_simple_cloud();
1736 let tree = KdTree::build(&cloud);
1737 let results = tree.k_nearest([0.0, 0.0, 0.0], 3);
1738 assert_eq!(results.len(), 3);
1739 assert_eq!(results[0].0, 0);
1741 assert!(results[0].1 < 1e-10);
1742 }
1743
1744 #[test]
1745 fn test_kdtree_radius_search() {
1746 let cloud = make_simple_cloud();
1747 let tree = KdTree::build(&cloud);
1748 let results = tree.radius_search([0.0, 0.0, 0.0], 1.01); assert!(!results.is_empty());
1751 assert!(results.len() <= 4);
1752 }
1753
1754 #[test]
1755 fn test_kdtree_empty() {
1756 let cloud = PointCloud::new();
1757 let tree = KdTree::build(&cloud);
1758 assert!(tree.is_empty());
1759 assert!(tree.nearest([0.0, 0.0, 0.0]).is_none());
1760 }
1761
1762 #[test]
1763 fn test_voxel_grid_downsample() {
1764 let cloud = make_cube_cloud(10); assert!(cloud.len() > 100);
1766 let downsampled = voxel_grid_downsample(&cloud, 0.5);
1767 assert!(downsampled.len() < cloud.len());
1768 assert!(!downsampled.is_empty());
1769 }
1770
1771 #[test]
1772 fn test_voxel_grid_preserves_bounds() {
1773 let cloud = make_simple_cloud();
1774 let bb_orig = BoundingBox::from_point_cloud(&cloud).unwrap();
1775 let downsampled = voxel_grid_downsample(&cloud, 0.5);
1776 let bb_down = BoundingBox::from_point_cloud(&downsampled).unwrap();
1777 assert!(bb_down.min[0] >= bb_orig.min[0] - 0.5);
1779 assert!(bb_down.max[0] <= bb_orig.max[0] + 0.5);
1780 }
1781
1782 #[test]
1783 fn test_statistical_outlier_removal() {
1784 let mut cloud = make_simple_cloud();
1785 cloud.positions.push([100.0, 100.0, 100.0]);
1787 let filtered = statistical_outlier_removal(&cloud, 3, 1.0);
1788 assert!(filtered.len() <= cloud.len());
1790 for p in &filtered.positions {
1792 assert!(p[0] < 50.0, "Outlier not removed: {p:?}");
1793 }
1794 }
1795
1796 #[test]
1797 fn test_estimate_normals() {
1798 let mut cloud = PointCloud::new();
1800 for i in 0..10 {
1801 for j in 0..10 {
1802 cloud.positions.push([i as f64, j as f64, 0.0]);
1803 }
1804 }
1805 let normals = estimate_normals(&cloud, 6);
1806 assert_eq!(normals.len(), 100);
1807 let mut z_aligned = 0;
1809 for n in &normals {
1810 if n[2].abs() > 0.5 {
1811 z_aligned += 1;
1812 }
1813 }
1814 assert!(z_aligned > 50, "Only {z_aligned}/100 normals z-aligned");
1815 }
1816
1817 #[test]
1818 fn test_compute_centroid() {
1819 let cloud = make_simple_cloud();
1820 let c = compute_centroid(&cloud);
1821 assert!((c[0] - 0.5).abs() < 1e-10);
1822 assert!((c[1] - 0.5).abs() < 1e-10);
1823 assert!((c[2] - 0.5).abs() < 1e-10);
1824 }
1825
1826 #[test]
1827 fn test_merge_point_clouds() {
1828 let a = PointCloud::from_positions(vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]]);
1829 let b = PointCloud::from_positions(vec![[2.0, 0.0, 0.0], [3.0, 0.0, 0.0]]);
1830 let merged = merge_point_clouds(&a, &b);
1831 assert_eq!(merged.len(), 4);
1832 assert!((merged.positions[2][0] - 2.0).abs() < 1e-10);
1833 }
1834
1835 #[test]
1836 fn test_transform_point_cloud() {
1837 let cloud = PointCloud::from_positions(vec![[1.0, 0.0, 0.0]]);
1838 let rot = [[0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]];
1840 let trans = [0.0, 0.0, 0.0];
1841 let result = transform_point_cloud(&cloud, rot, trans);
1842 assert!((result.positions[0][0]).abs() < 1e-10);
1843 assert!((result.positions[0][1] - 1.0).abs() < 1e-10);
1844 }
1845
1846 #[test]
1847 fn test_scale_point_cloud() {
1848 let cloud = PointCloud::from_positions(vec![[1.0, 2.0, 3.0]]);
1849 let scaled = scale_point_cloud(&cloud, 2.0);
1850 assert!((scaled.positions[0][0] - 2.0).abs() < 1e-10);
1851 assert!((scaled.positions[0][1] - 4.0).abs() < 1e-10);
1852 }
1853
1854 #[test]
1855 fn test_translate_point_cloud() {
1856 let cloud = PointCloud::from_positions(vec![[1.0, 2.0, 3.0]]);
1857 let translated = translate_point_cloud(&cloud, [10.0, 20.0, 30.0]);
1858 assert!((translated.positions[0][0] - 11.0).abs() < 1e-10);
1859 }
1860
1861 #[test]
1862 fn test_center_point_cloud() {
1863 let cloud = PointCloud::from_positions(vec![[2.0, 4.0, 6.0], [4.0, 6.0, 8.0]]);
1864 let centered = center_point_cloud(&cloud);
1865 let c = compute_centroid(¢ered);
1866 assert!(c[0].abs() < 1e-10);
1867 assert!(c[1].abs() < 1e-10);
1868 assert!(c[2].abs() < 1e-10);
1869 }
1870
1871 #[test]
1872 fn test_write_parse_ply_ascii() {
1873 let mut cloud = PointCloud::from_positions(vec![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]);
1874 cloud.normals = Some(vec![[0.0, 0.0, 1.0], [0.0, 1.0, 0.0]]);
1875
1876 let mut buf = Vec::new();
1877 write_ply_ascii(&cloud, &mut buf).unwrap();
1878 let text = String::from_utf8(buf).unwrap();
1879
1880 let parsed = parse_ply_ascii(&text).unwrap();
1881 assert_eq!(parsed.len(), 2);
1882 assert!((parsed.positions[0][0] - 1.0).abs() < 0.001);
1883 assert!(parsed.has_normals());
1884 }
1885
1886 #[test]
1887 fn test_write_ply_binary_le() {
1888 let cloud = PointCloud::from_positions(vec![[1.0, 2.0, 3.0]]);
1889 let buf = write_ply_binary_le(&cloud);
1890 assert!(buf.starts_with(b"ply"));
1892 assert!(buf.len() > 12); }
1894
1895 #[test]
1896 fn test_write_parse_pcd_ascii() {
1897 let cloud =
1898 PointCloud::from_positions(vec![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]);
1899 let mut buf = Vec::new();
1900 write_pcd_ascii(&cloud, &mut buf).unwrap();
1901 let text = String::from_utf8(buf).unwrap();
1902
1903 let parsed = parse_pcd_ascii(&text).unwrap();
1904 assert_eq!(parsed.len(), 3);
1905 assert!((parsed.positions[1][0] - 4.0).abs() < 0.001);
1906 }
1907
1908 #[test]
1909 fn test_write_parse_xyz() {
1910 let mut cloud = PointCloud::from_positions(vec![[1.0, 2.0, 3.0]]);
1911 cloud.normals = Some(vec![[0.0, 0.0, 1.0]]);
1912 cloud.colors = Some(vec![[255, 128, 0]]);
1913
1914 let mut buf = Vec::new();
1915 write_xyz(&cloud, &mut buf).unwrap();
1916 let text = String::from_utf8(buf).unwrap();
1917
1918 let parsed = parse_xyz(&text);
1919 assert_eq!(parsed.len(), 1);
1920 assert!((parsed.positions[0][0] - 1.0).abs() < 0.001);
1921 }
1922
1923 #[test]
1924 fn test_parse_xyz_basic() {
1925 let data = "1.0 2.0 3.0\n4.0 5.0 6.0\n";
1926 let cloud = parse_xyz(data);
1927 assert_eq!(cloud.len(), 2);
1928 assert!(!cloud.has_normals());
1929 }
1930
1931 #[test]
1932 fn test_las_header_mock() {
1933 let header = LasHeader::mock(50000);
1934 assert_eq!(header.num_points, 50000);
1935 assert_eq!(header.file_signature, "LASF");
1936 assert!(header.bounding_volume() > 0.0);
1937 }
1938
1939 #[test]
1940 fn test_e57_structure() {
1941 let e57 = E57File::mock();
1942 assert_eq!(e57.num_scans(), 1);
1943 assert_eq!(e57.total_points(), 1000);
1944 }
1945
1946 #[test]
1947 fn test_ball_pivoting_mesh() {
1948 let mut cloud = PointCloud::new();
1950 for i in 0..5 {
1951 for j in 0..5 {
1952 cloud.positions.push([i as f64 * 0.5, j as f64 * 0.5, 0.0]);
1953 }
1954 }
1955 let triangles = ball_pivoting_mesh(&cloud, 0.6);
1956 assert!(!triangles.is_empty(), "Should produce some triangles");
1957 for tri in &triangles {
1958 assert!(tri.indices[0] < cloud.len());
1959 assert!(tri.indices[1] < cloud.len());
1960 assert!(tri.indices[2] < cloud.len());
1961 }
1962 }
1963
1964 #[test]
1965 fn test_dist_sq_and_dist() {
1966 let a = [1.0, 0.0, 0.0];
1967 let b = [4.0, 0.0, 0.0];
1968 assert!((dist_sq(a, b) - 9.0).abs() < 1e-10);
1969 assert!((dist(a, b) - 3.0).abs() < 1e-10);
1970 }
1971
1972 #[test]
1973 fn test_normalize3() {
1974 let v = [3.0, 4.0, 0.0];
1975 let n = normalize3(v);
1976 assert!((n[0] - 0.6).abs() < 1e-10);
1977 assert!((n[1] - 0.8).abs() < 1e-10);
1978
1979 let z = normalize3([0.0, 0.0, 0.0]);
1980 assert!(z[0].abs() < 1e-10);
1981 }
1982
1983 #[test]
1984 fn test_cross3() {
1985 let a = [1.0, 0.0, 0.0];
1986 let b = [0.0, 1.0, 0.0];
1987 let c = cross3(a, b);
1988 assert!((c[2] - 1.0).abs() < 1e-10);
1989 }
1990
1991 #[test]
1992 fn test_kdtree_large() {
1993 let mut cloud = PointCloud::new();
1994 for i in 0..100 {
1995 cloud
1996 .positions
1997 .push([(i % 10) as f64, (i / 10) as f64, 0.0]);
1998 }
1999 let tree = KdTree::build(&cloud);
2000 assert_eq!(tree.len(), 100);
2001
2002 let (idx, _d) = tree.nearest([4.5, 4.5, 0.0]).unwrap();
2003 let p = cloud.positions[idx];
2005 assert!((p[0] - 4.5).abs() <= 1.0);
2006 assert!((p[1] - 4.5).abs() <= 1.0);
2007 }
2008
2009 #[test]
2010 fn test_point_cloud_reserve() {
2011 let mut cloud = PointCloud::new();
2012 cloud.reserve(100);
2013 assert!(cloud.is_empty());
2014 cloud.add_point([1.0, 2.0, 3.0]);
2015 assert_eq!(cloud.len(), 1);
2016 }
2017
2018 #[test]
2019 fn test_bounding_box_empty() {
2020 let cloud = PointCloud::new();
2021 assert!(BoundingBox::from_point_cloud(&cloud).is_none());
2022 }
2023}