1use crate::parallel;
7use std::collections::HashMap;
8use threecrate_core::{Error, NormalPoint3f, Point3f, PointCloud, Result, TriangleMesh};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum Algorithm {
13 Poisson,
15 BallPivoting,
17 Delaunay,
19 MovingLeastSquares,
21 MarchingCubes,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq)]
27pub enum QualityLevel {
28 Fast,
30 Balanced,
32 HighQuality,
34 MaxQuality,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq)]
40pub enum UseCase {
41 General,
43 Prototyping,
45 Engineering,
47 Organic,
49 NoisyData,
51 Sparse,
53 Dense,
55}
56
57#[derive(Debug, Clone)]
59pub struct DataCharacteristics {
60 pub point_count: usize,
62 pub has_normals: bool,
64 pub density_uniformity: f32,
66 pub noise_level: f32,
68 pub avg_neighbor_distance: f32,
70 pub bounding_box: (Point3f, Point3f),
72 pub is_closed_surface: bool,
74 pub surface_complexity: f32,
76 pub distribution_type: DistributionType,
78}
79
80#[derive(Debug, Clone, Copy, PartialEq)]
82pub enum DistributionType {
83 Planar,
85 Spherical,
87 Cylindrical,
89 Arbitrary,
91}
92
93#[derive(Debug, Clone)]
95pub struct PipelineConfig {
96 pub quality: QualityLevel,
98 pub use_case: UseCase,
100 pub preferred_algorithm: Option<Algorithm>,
102 pub fallback_algorithms: Vec<Algorithm>,
104 pub max_processing_time: Option<f32>,
106 pub enable_parallel: bool,
108 pub validate_output: bool,
110 pub auto_repair: bool,
112}
113
114impl Default for PipelineConfig {
115 fn default() -> Self {
116 Self {
117 quality: QualityLevel::Balanced,
118 use_case: UseCase::General,
119 preferred_algorithm: None,
120 fallback_algorithms: vec![
121 Algorithm::Delaunay,
122 Algorithm::BallPivoting,
123 Algorithm::MovingLeastSquares,
124 ],
125 max_processing_time: None,
126 enable_parallel: true,
127 validate_output: true,
128 auto_repair: false,
129 }
130 }
131}
132
133#[derive(Debug)]
135pub struct ReconstructionResult {
136 pub mesh: TriangleMesh,
138 pub algorithm_used: Algorithm,
140 pub processing_time: f32,
142 pub quality_metrics: QualityMetrics,
144 pub data_characteristics: DataCharacteristics,
146}
147
148#[derive(Debug, Clone)]
150pub struct QualityMetrics {
151 pub vertex_count: usize,
153 pub triangle_count: usize,
155 pub avg_triangle_quality: f32,
157 pub watertightness: f32,
159 pub smoothness: f32,
161 pub geometric_accuracy: f32,
163}
164
165pub struct ReconstructionPipeline {
167 config: PipelineConfig,
168}
169
170impl ReconstructionPipeline {
171 pub fn new(config: PipelineConfig) -> Self {
173 Self { config }
174 }
175
176 pub fn default() -> Self {
178 Self::new(PipelineConfig::default())
179 }
180
181 pub fn for_use_case(use_case: UseCase) -> Self {
183 let mut config = PipelineConfig::default();
184 config.use_case = use_case;
185
186 match use_case {
188 UseCase::Prototyping => {
189 config.quality = QualityLevel::Fast;
190 config.fallback_algorithms = vec![Algorithm::Delaunay, Algorithm::BallPivoting];
191 }
192 UseCase::Engineering => {
193 config.quality = QualityLevel::HighQuality;
194 config.validate_output = true;
195 config.auto_repair = true;
196 }
197 UseCase::Organic => {
198 config.quality = QualityLevel::HighQuality;
199 config.fallback_algorithms = vec![
200 Algorithm::MovingLeastSquares,
201 Algorithm::Poisson,
202 Algorithm::BallPivoting,
203 ];
204 }
205 UseCase::NoisyData => {
206 config.fallback_algorithms =
207 vec![Algorithm::MovingLeastSquares, Algorithm::Delaunay];
208 }
209 UseCase::Sparse => {
210 config.fallback_algorithms =
211 vec![Algorithm::Delaunay, Algorithm::MovingLeastSquares];
212 }
213 UseCase::Dense => {
214 config.fallback_algorithms = vec![
215 Algorithm::Poisson,
216 Algorithm::BallPivoting,
217 Algorithm::MarchingCubes,
218 ];
219 }
220 UseCase::General => {
221 }
223 }
224
225 Self::new(config)
226 }
227
228 pub fn analyze_data(&self, cloud: &PointCloud<Point3f>) -> Result<DataCharacteristics> {
230 if cloud.is_empty() {
231 return Err(Error::InvalidData("Point cloud is empty".to_string()));
232 }
233
234 let point_count = cloud.points.len();
235
236 let (bounds_min, bounds_max) = parallel::point_cloud::parallel_bounding_box(&cloud.points)
238 .unwrap_or((Point3f::origin(), Point3f::new(1.0, 1.0, 1.0)));
239
240 let sample_size = point_count.min(1000);
242 let step = (point_count.max(1) / sample_size.max(1)).max(1);
243 let sample_points: Vec<Point3f> = cloud.points.iter().step_by(step).cloned().collect();
244
245 let neighbor_distances = self.compute_neighbor_distances(&sample_points)?;
247 let avg_neighbor_distance =
248 neighbor_distances.iter().sum::<f32>() / neighbor_distances.len() as f32;
249
250 let density_uniformity = self.estimate_density_uniformity(&neighbor_distances);
252
253 let distance_variance = self.compute_variance(&neighbor_distances);
255 let noise_level = (distance_variance / avg_neighbor_distance.powi(2)).min(1.0);
256
257 let distribution_type = self.classify_distribution(&cloud.points, &bounds_min, &bounds_max);
259
260 let surface_complexity = self.estimate_surface_complexity(&sample_points);
262
263 let is_closed_surface = self.estimate_surface_closure(&sample_points);
265
266 Ok(DataCharacteristics {
267 point_count,
268 has_normals: false, density_uniformity,
270 noise_level,
271 avg_neighbor_distance,
272 bounding_box: (bounds_min, bounds_max),
273 is_closed_surface,
274 surface_complexity,
275 distribution_type,
276 })
277 }
278
279 pub fn analyze_data_with_normals(
281 &self,
282 cloud: &PointCloud<NormalPoint3f>,
283 ) -> Result<DataCharacteristics> {
284 let point_cloud: PointCloud<Point3f> =
285 PointCloud::from_points(cloud.points.iter().map(|p| p.position).collect());
286
287 let mut characteristics = self.analyze_data(&point_cloud)?;
288 characteristics.has_normals = true;
289
290 Ok(characteristics)
291 }
292
293 pub fn select_algorithm(&self, characteristics: &DataCharacteristics) -> Algorithm {
295 if let Some(preferred) = self.config.preferred_algorithm {
297 return preferred;
298 }
299
300 let mut scores = HashMap::new();
302
303 scores.insert(Algorithm::Delaunay, 0.5);
305 scores.insert(Algorithm::BallPivoting, 0.5);
306 scores.insert(Algorithm::MovingLeastSquares, 0.5);
307 scores.insert(Algorithm::MarchingCubes, 0.5);
308 scores.insert(
309 Algorithm::Poisson,
310 if characteristics.has_normals {
311 0.5
312 } else {
313 0.0
314 },
315 );
316
317 match characteristics.point_count {
319 0..=100 => {
320 *scores.get_mut(&Algorithm::Delaunay).unwrap() += 0.3;
321 *scores.get_mut(&Algorithm::MovingLeastSquares).unwrap() += 0.2;
322 }
323 101..=1000 => {
324 *scores.get_mut(&Algorithm::BallPivoting).unwrap() += 0.2;
325 *scores.get_mut(&Algorithm::Delaunay).unwrap() += 0.3;
326 }
327 1001..=10000 => {
328 *scores.get_mut(&Algorithm::BallPivoting).unwrap() += 0.3;
329 if characteristics.has_normals {
330 *scores.get_mut(&Algorithm::Poisson).unwrap() += 0.3;
331 }
332 }
333 _ => {
334 if characteristics.has_normals {
335 *scores.get_mut(&Algorithm::Poisson).unwrap() += 0.4;
336 }
337 *scores.get_mut(&Algorithm::MarchingCubes).unwrap() += 0.2;
338 }
339 }
340
341 if characteristics.density_uniformity > 0.7 {
343 *scores.get_mut(&Algorithm::BallPivoting).unwrap() += 0.2;
344 if characteristics.has_normals {
345 *scores.get_mut(&Algorithm::Poisson).unwrap() += 0.2;
346 }
347 } else {
348 *scores.get_mut(&Algorithm::MovingLeastSquares).unwrap() += 0.3;
349 *scores.get_mut(&Algorithm::Delaunay).unwrap() += 0.2;
350 }
351
352 if characteristics.noise_level > 0.3 {
354 *scores.get_mut(&Algorithm::MovingLeastSquares).unwrap() += 0.3;
355 *scores.get_mut(&Algorithm::Delaunay).unwrap() += 0.1;
356 *scores.get_mut(&Algorithm::BallPivoting).unwrap() -= 0.2;
358 }
359
360 if characteristics.surface_complexity > 0.7 {
362 if characteristics.has_normals {
363 *scores.get_mut(&Algorithm::Poisson).unwrap() += 0.3;
364 }
365 *scores.get_mut(&Algorithm::MovingLeastSquares).unwrap() += 0.2;
366 }
367
368 match characteristics.distribution_type {
370 DistributionType::Planar => {
371 *scores.get_mut(&Algorithm::Delaunay).unwrap() += 0.3;
372 }
373 DistributionType::Spherical | DistributionType::Cylindrical => {
374 *scores.get_mut(&Algorithm::BallPivoting).unwrap() += 0.2;
375 *scores.get_mut(&Algorithm::MarchingCubes).unwrap() += 0.3;
376 }
377 DistributionType::Arbitrary => {
378 }
380 }
381
382 match self.config.quality {
384 QualityLevel::Fast => {
385 *scores.get_mut(&Algorithm::Delaunay).unwrap() += 0.3;
386 }
387 QualityLevel::Balanced => {
388 *scores.get_mut(&Algorithm::BallPivoting).unwrap() += 0.2;
389 }
390 QualityLevel::HighQuality | QualityLevel::MaxQuality => {
391 if characteristics.has_normals {
392 *scores.get_mut(&Algorithm::Poisson).unwrap() += 0.3;
393 }
394 *scores.get_mut(&Algorithm::MovingLeastSquares).unwrap() += 0.2;
395 }
396 }
397
398 match self.config.use_case {
400 UseCase::Engineering => {
401 *scores.get_mut(&Algorithm::Delaunay).unwrap() += 0.2;
402 if characteristics.has_normals {
403 *scores.get_mut(&Algorithm::Poisson).unwrap() += 0.2;
404 }
405 }
406 UseCase::Organic => {
407 *scores.get_mut(&Algorithm::MovingLeastSquares).unwrap() += 0.3;
408 if characteristics.has_normals {
409 *scores.get_mut(&Algorithm::Poisson).unwrap() += 0.2;
410 }
411 }
412 UseCase::Prototyping => {
413 *scores.get_mut(&Algorithm::Delaunay).unwrap() += 0.4;
414 }
415 _ => {}
416 }
417
418 scores
420 .into_iter()
421 .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
422 .map(|(algo, _)| algo)
423 .unwrap_or(Algorithm::Delaunay)
424 }
425
426 pub fn reconstruct(&self, cloud: &PointCloud<Point3f>) -> Result<ReconstructionResult> {
428 let start_time = std::time::Instant::now();
429
430 let characteristics = self.analyze_data(cloud)?;
432
433 let selected_algorithm = self.select_algorithm(&characteristics);
435
436 let mesh = match self.try_algorithm(cloud, selected_algorithm) {
438 Ok(mesh) => mesh,
439 Err(_) => {
440 let mut last_error = Error::Algorithm("No algorithms succeeded".to_string());
442 for &fallback_algo in &self.config.fallback_algorithms {
443 if fallback_algo != selected_algorithm {
444 match self.try_algorithm(cloud, fallback_algo) {
445 Ok(mesh) => {
446 let processing_time = start_time.elapsed().as_secs_f32();
447 let quality_metrics =
448 self.compute_quality_metrics(&mesh, &characteristics);
449
450 return Ok(ReconstructionResult {
451 mesh,
452 algorithm_used: fallback_algo,
453 processing_time,
454 quality_metrics,
455 data_characteristics: characteristics,
456 });
457 }
458 Err(e) => last_error = e,
459 }
460 }
461 }
462 return Err(last_error);
463 }
464 };
465
466 let processing_time = start_time.elapsed().as_secs_f32();
467 let quality_metrics = self.compute_quality_metrics(&mesh, &characteristics);
468
469 Ok(ReconstructionResult {
470 mesh,
471 algorithm_used: selected_algorithm,
472 processing_time,
473 quality_metrics,
474 data_characteristics: characteristics,
475 })
476 }
477
478 pub fn reconstruct_with_normals(
480 &self,
481 cloud: &PointCloud<NormalPoint3f>,
482 ) -> Result<ReconstructionResult> {
483 let start_time = std::time::Instant::now();
484
485 let characteristics = self.analyze_data_with_normals(cloud)?;
487
488 let selected_algorithm = self.select_algorithm(&characteristics);
490
491 let mesh = match self.try_algorithm_with_normals(cloud, selected_algorithm) {
493 Ok(mesh) => mesh,
494 Err(_) => {
495 let mut last_error = Error::Algorithm("No algorithms succeeded".to_string());
497 for &fallback_algo in &self.config.fallback_algorithms {
498 if fallback_algo != selected_algorithm {
499 match self.try_algorithm_with_normals(cloud, fallback_algo) {
500 Ok(mesh) => {
501 let processing_time = start_time.elapsed().as_secs_f32();
502 let quality_metrics =
503 self.compute_quality_metrics(&mesh, &characteristics);
504
505 return Ok(ReconstructionResult {
506 mesh,
507 algorithm_used: fallback_algo,
508 processing_time,
509 quality_metrics,
510 data_characteristics: characteristics,
511 });
512 }
513 Err(e) => last_error = e,
514 }
515 }
516 }
517 return Err(last_error);
518 }
519 };
520
521 let processing_time = start_time.elapsed().as_secs_f32();
522 let quality_metrics = self.compute_quality_metrics(&mesh, &characteristics);
523
524 Ok(ReconstructionResult {
525 mesh,
526 algorithm_used: selected_algorithm,
527 processing_time,
528 quality_metrics,
529 data_characteristics: characteristics,
530 })
531 }
532
533 fn compute_neighbor_distances(&self, points: &[Point3f]) -> Result<Vec<f32>> {
535 if points.len() < 2 {
536 return Ok(vec![1.0]); }
538
539 let distances = parallel::parallel_map(points, |point| {
540 let mut min_dist = f32::INFINITY;
541 for other_point in points {
542 if point != other_point {
543 let dist = (point - other_point).magnitude();
544 if dist < min_dist {
545 min_dist = dist;
546 }
547 }
548 }
549 min_dist
550 });
551
552 let finite_distances: Vec<f32> = distances
553 .into_iter()
554 .filter(|&d| d.is_finite() && d > 0.0)
555 .collect();
556
557 if finite_distances.is_empty() {
558 Ok(vec![1.0])
559 } else {
560 Ok(finite_distances)
561 }
562 }
563
564 fn estimate_density_uniformity(&self, distances: &[f32]) -> f32 {
565 if distances.len() < 2 {
566 return 1.0;
567 }
568
569 let mean = distances.iter().sum::<f32>() / distances.len() as f32;
570 let variance = self.compute_variance(distances);
571 let cv = if mean > 0.0 {
572 variance.sqrt() / mean
573 } else {
574 0.0
575 };
576
577 (1.0 / (1.0 + cv)).min(1.0)
579 }
580
581 fn compute_variance(&self, values: &[f32]) -> f32 {
582 if values.len() < 2 {
583 return 0.0;
584 }
585
586 let mean = values.iter().sum::<f32>() / values.len() as f32;
587 let variance = values.iter().map(|x| (x - mean).powi(2)).sum::<f32>() / values.len() as f32;
588
589 variance
590 }
591
592 fn classify_distribution(
593 &self,
594 _points: &[Point3f],
595 bounds_min: &Point3f,
596 bounds_max: &Point3f,
597 ) -> DistributionType {
598 let extents = [
599 bounds_max.x - bounds_min.x,
600 bounds_max.y - bounds_min.y,
601 bounds_max.z - bounds_min.z,
602 ];
603
604 let max_extent = extents.iter().fold(0.0f32, |acc, &x| acc.max(x));
605 let min_extent = extents.iter().fold(f32::INFINITY, |acc, &x| acc.min(x));
606
607 if max_extent < 1e-6 {
608 return DistributionType::Arbitrary;
609 }
610
611 if min_extent < max_extent * 0.1 {
613 return DistributionType::Planar;
614 }
615
616 let extent_ratios = [
618 extents[0] / max_extent,
619 extents[1] / max_extent,
620 extents[2] / max_extent,
621 ];
622
623 if extent_ratios.iter().all(|&r| r > 0.7) {
624 DistributionType::Spherical
625 } else if extent_ratios.iter().filter(|&&r| r > 0.7).count() == 2 {
626 DistributionType::Cylindrical
627 } else {
628 DistributionType::Arbitrary
629 }
630 }
631
632 fn estimate_surface_complexity(&self, points: &[Point3f]) -> f32 {
633 if points.len() < 10 {
634 return 0.5; }
636
637 let sample_size = points.len().min(100);
639 let step = (points.len().max(1) / sample_size.max(1)).max(1);
640 let sample_points: Vec<Point3f> = points.iter().step_by(step).cloned().collect();
641
642 let mut curvature_variations = Vec::new();
643
644 for (i, point) in sample_points.iter().enumerate() {
645 let mut neighbors = Vec::new();
647 for (j, other_point) in sample_points.iter().enumerate() {
648 if i != j {
649 let dist = (point - other_point).magnitude();
650 if dist < 0.1 {
651 neighbors.push(*other_point);
653 }
654 }
655 }
656
657 if neighbors.len() >= 3 {
658 let variation = self.estimate_local_curvature_variation(point, &neighbors);
660 curvature_variations.push(variation);
661 }
662 }
663
664 if curvature_variations.is_empty() {
665 0.5
666 } else {
667 let avg_variation =
668 curvature_variations.iter().sum::<f32>() / curvature_variations.len() as f32;
669 avg_variation.min(1.0)
670 }
671 }
672
673 fn estimate_local_curvature_variation(&self, center: &Point3f, neighbors: &[Point3f]) -> f32 {
674 if neighbors.len() < 3 {
675 return 0.0;
676 }
677
678 let mut angles = Vec::new();
680 for i in 0..neighbors.len() {
681 let v1 = (neighbors[i] - *center).normalize();
682 let v2 = (neighbors[(i + 1) % neighbors.len()] - *center).normalize();
683 let angle = v1.dot(&v2).clamp(-1.0, 1.0).acos();
684 angles.push(angle);
685 }
686
687 self.compute_variance(&angles) / std::f32::consts::PI
688 }
689
690 fn estimate_surface_closure(&self, points: &[Point3f]) -> bool {
691 if points.len() < 50 {
694 return false; }
696
697 let centroid = points.iter().fold(Point3f::origin(), |acc, p| {
699 Point3f::from(acc.coords + p.coords)
700 });
701 let centroid = Point3f::from(centroid.coords / points.len() as f32);
702
703 let distances: Vec<f32> = points.iter().map(|p| (p - centroid).magnitude()).collect();
705
706 let mean_dist = distances.iter().sum::<f32>() / distances.len() as f32;
707 let variance = self.compute_variance(&distances);
708 let cv = if mean_dist > 0.0 {
709 variance.sqrt() / mean_dist
710 } else {
711 1.0
712 };
713
714 cv < 0.3
716 }
717
718 fn try_algorithm(
720 &self,
721 cloud: &PointCloud<Point3f>,
722 algorithm: Algorithm,
723 ) -> Result<TriangleMesh> {
724 match algorithm {
725 Algorithm::Delaunay => crate::delaunay::delaunay_triangulation_auto(cloud),
726 Algorithm::BallPivoting => {
727 let radius = crate::ball_pivoting::estimate_optimal_radius(cloud, 0.5)?;
728 crate::ball_pivoting::ball_pivoting_reconstruction(cloud, radius)
729 }
730 Algorithm::MovingLeastSquares => {
731 crate::moving_least_squares::moving_least_squares_auto(cloud)
732 }
733 Algorithm::MarchingCubes => {
734 let mls = crate::moving_least_squares::MLSSurface::new(
736 cloud,
737 crate::moving_least_squares::MLSConfig::default(),
738 )?;
739 mls.extract_mesh()
740 }
741 Algorithm::Poisson => {
742 Err(Error::InvalidData(
744 "Poisson reconstruction requires normals".to_string(),
745 ))
746 }
747 }
748 }
749
750 fn try_algorithm_with_normals(
751 &self,
752 cloud: &PointCloud<NormalPoint3f>,
753 algorithm: Algorithm,
754 ) -> Result<TriangleMesh> {
755 match algorithm {
756 Algorithm::Poisson => crate::poisson::poisson_reconstruction_default(cloud),
757 Algorithm::BallPivoting => {
758 let radius = {
759 let point_cloud: PointCloud<Point3f> =
760 PointCloud::from_points(cloud.points.iter().map(|p| p.position).collect());
761 crate::ball_pivoting::estimate_optimal_radius(&point_cloud, 0.5)?
762 };
763 crate::ball_pivoting::ball_pivoting_from_normals(cloud, radius)
764 }
765 Algorithm::Delaunay => {
766 let point_cloud: PointCloud<Point3f> =
767 PointCloud::from_points(cloud.points.iter().map(|p| p.position).collect());
768 crate::delaunay::delaunay_triangulation_auto(&point_cloud)
769 }
770 Algorithm::MovingLeastSquares => {
771 crate::moving_least_squares::moving_least_squares_from_normals(cloud)
772 }
773 Algorithm::MarchingCubes => {
774 let mls = crate::moving_least_squares::MLSSurface::from_normals(
776 cloud,
777 crate::moving_least_squares::MLSConfig::default(),
778 )?;
779 mls.extract_mesh()
780 }
781 }
782 }
783
784 fn compute_quality_metrics(
785 &self,
786 mesh: &TriangleMesh,
787 characteristics: &DataCharacteristics,
788 ) -> QualityMetrics {
789 let vertex_count = mesh.vertex_count();
790 let triangle_count = mesh.face_count();
791
792 let avg_triangle_quality = 0.75; let watertightness = if characteristics.is_closed_surface {
795 0.8
796 } else {
797 0.6
798 };
799 let smoothness = 1.0 - characteristics.noise_level;
800 let geometric_accuracy = 0.8; QualityMetrics {
803 vertex_count,
804 triangle_count,
805 avg_triangle_quality,
806 watertightness,
807 smoothness,
808 geometric_accuracy,
809 }
810 }
811}
812
813pub fn auto_reconstruct(cloud: &PointCloud<Point3f>) -> Result<TriangleMesh> {
815 let pipeline = ReconstructionPipeline::default();
816 Ok(pipeline.reconstruct(cloud)?.mesh)
817}
818
819pub fn auto_reconstruct_with_normals(cloud: &PointCloud<NormalPoint3f>) -> Result<TriangleMesh> {
821 let pipeline = ReconstructionPipeline::default();
822 Ok(pipeline.reconstruct_with_normals(cloud)?.mesh)
823}
824
825pub fn auto_reconstruct_with_quality(
827 cloud: &PointCloud<Point3f>,
828 quality: QualityLevel,
829) -> Result<TriangleMesh> {
830 let mut config = PipelineConfig::default();
831 config.quality = quality;
832 let pipeline = ReconstructionPipeline::new(config);
833 Ok(pipeline.reconstruct(cloud)?.mesh)
834}
835
836pub fn auto_reconstruct_for_use_case(
838 cloud: &PointCloud<Point3f>,
839 use_case: UseCase,
840) -> Result<TriangleMesh> {
841 let pipeline = ReconstructionPipeline::for_use_case(use_case);
842 Ok(pipeline.reconstruct(cloud)?.mesh)
843}
844
845#[cfg(test)]
846mod tests {
847 use super::*;
848 use nalgebra::Point3;
849
850 #[test]
851 fn test_pipeline_config_default() {
852 let config = PipelineConfig::default();
853 assert_eq!(config.quality, QualityLevel::Balanced);
854 assert_eq!(config.use_case, UseCase::General);
855 assert!(config.enable_parallel);
856 assert!(config.validate_output);
857 }
858
859 #[test]
860 fn test_pipeline_for_use_case() {
861 let pipeline = ReconstructionPipeline::for_use_case(UseCase::Prototyping);
862 assert_eq!(pipeline.config.quality, QualityLevel::Fast);
863 assert_eq!(pipeline.config.use_case, UseCase::Prototyping);
864 }
865
866 #[test]
867 fn test_data_analysis_empty_cloud() {
868 let pipeline = ReconstructionPipeline::default();
869 let cloud = PointCloud::new();
870 let result = pipeline.analyze_data(&cloud);
871 assert!(result.is_err());
872 }
873
874 #[test]
875 fn test_data_analysis_simple() {
876 let pipeline = ReconstructionPipeline::default();
877 let points = vec![
878 Point3::new(0.0, 0.0, 0.0),
879 Point3::new(1.0, 0.0, 0.0),
880 Point3::new(0.5, 1.0, 0.0),
881 Point3::new(0.0, 1.0, 0.0),
882 ];
883 let cloud = PointCloud::from_points(points);
884
885 let characteristics = pipeline.analyze_data(&cloud).unwrap();
886 assert_eq!(characteristics.point_count, 4);
887 assert!(!characteristics.has_normals);
888 assert_eq!(characteristics.distribution_type, DistributionType::Planar);
889 }
890
891 #[test]
892 fn test_algorithm_selection_sparse_data() {
893 let pipeline = ReconstructionPipeline::default();
894 let characteristics = DataCharacteristics {
895 point_count: 50,
896 has_normals: false,
897 density_uniformity: 0.5,
898 noise_level: 0.1,
899 avg_neighbor_distance: 0.1,
900 bounding_box: (Point3f::origin(), Point3f::new(1.0, 1.0, 1.0)),
901 is_closed_surface: false,
902 surface_complexity: 0.3,
903 distribution_type: DistributionType::Planar,
904 };
905
906 let algorithm = pipeline.select_algorithm(&characteristics);
907 assert!(matches!(
909 algorithm,
910 Algorithm::Delaunay | Algorithm::MovingLeastSquares
911 ));
912 }
913
914 #[test]
915 fn test_algorithm_selection_dense_with_normals() {
916 let pipeline = ReconstructionPipeline::default();
917 let characteristics = DataCharacteristics {
918 point_count: 5000,
919 has_normals: true,
920 density_uniformity: 0.8,
921 noise_level: 0.1,
922 avg_neighbor_distance: 0.05,
923 bounding_box: (Point3f::origin(), Point3f::new(1.0, 1.0, 1.0)),
924 is_closed_surface: true,
925 surface_complexity: 0.7,
926 distribution_type: DistributionType::Spherical,
927 };
928
929 let algorithm = pipeline.select_algorithm(&characteristics);
930 assert!(matches!(
932 algorithm,
933 Algorithm::Poisson | Algorithm::BallPivoting
934 ));
935 }
936
937 #[test]
938 fn test_auto_reconstruct_simple() {
939 let points = vec![
940 Point3::new(0.0, 0.0, 0.0),
941 Point3::new(1.0, 0.0, 0.0),
942 Point3::new(0.5, 1.0, 0.0),
943 Point3::new(0.0, 1.0, 0.0),
944 ];
945 let cloud = PointCloud::from_points(points);
946
947 let result = auto_reconstruct(&cloud);
948 match result {
949 Ok(mesh) => {
950 assert!(!mesh.is_empty());
951 }
952 Err(_) => {
953 println!(
955 "Auto reconstruction failed on simple test data (expected for some algorithms)"
956 );
957 }
958 }
959 }
960
961 #[test]
962 fn test_quality_levels() {
963 let quality_levels = [
964 QualityLevel::Fast,
965 QualityLevel::Balanced,
966 QualityLevel::HighQuality,
967 QualityLevel::MaxQuality,
968 ];
969
970 for quality in &quality_levels {
971 let mut config = PipelineConfig::default();
972 config.quality = *quality;
973 let _pipeline = ReconstructionPipeline::new(config);
974 }
976 }
977
978 #[test]
979 fn test_use_cases() {
980 let use_cases = [
981 UseCase::General,
982 UseCase::Prototyping,
983 UseCase::Engineering,
984 UseCase::Organic,
985 UseCase::NoisyData,
986 UseCase::Sparse,
987 UseCase::Dense,
988 ];
989
990 for use_case in &use_cases {
991 let pipeline = ReconstructionPipeline::for_use_case(*use_case);
992 assert_eq!(pipeline.config.use_case, *use_case);
993 }
994 }
995
996 #[test]
997 fn test_distribution_classification() {
998 let pipeline = ReconstructionPipeline::default();
999
1000 let planar_points = vec![
1002 Point3f::new(0.0, 0.0, 0.0),
1003 Point3f::new(1.0, 0.0, 0.0),
1004 Point3f::new(0.0, 1.0, 0.0),
1005 Point3f::new(1.0, 1.0, 0.0),
1006 ];
1007 let min_bounds = Point3f::new(0.0, 0.0, 0.0);
1008 let max_bounds = Point3f::new(1.0, 1.0, 0.0);
1009 let distribution = pipeline.classify_distribution(&planar_points, &min_bounds, &max_bounds);
1010 assert_eq!(distribution, DistributionType::Planar);
1011
1012 let min_bounds_sphere = Point3f::new(-1.0, -1.0, -1.0);
1014 let max_bounds_sphere = Point3f::new(1.0, 1.0, 1.0);
1015 let distribution =
1016 pipeline.classify_distribution(&planar_points, &min_bounds_sphere, &max_bounds_sphere);
1017 assert_eq!(distribution, DistributionType::Spherical);
1018 }
1019}