Skip to main content

threecrate_reconstruction/
pipeline.rs

1//! Unified reconstruction pipeline with automatic algorithm selection
2//!
3//! This module provides an intelligent reconstruction pipeline that automatically
4//! selects the best algorithm based on input data characteristics and user requirements.
5
6use crate::parallel;
7use std::collections::HashMap;
8use threecrate_core::{Error, NormalPoint3f, Point3f, PointCloud, Result, TriangleMesh};
9
10/// Reconstruction algorithm types available in the pipeline
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum Algorithm {
13    /// Poisson surface reconstruction (requires normals)
14    Poisson,
15    /// Ball Pivoting algorithm (good for uniform sampling)
16    BallPivoting,
17    /// Delaunay triangulation (fast, works with any point distribution)
18    Delaunay,
19    /// Moving Least Squares (smooth surfaces, handles noise well)
20    MovingLeastSquares,
21    /// Marching Cubes (volumetric approach, good for closed surfaces)
22    MarchingCubes,
23}
24
25/// Quality requirements for reconstruction
26#[derive(Debug, Clone, Copy, PartialEq)]
27pub enum QualityLevel {
28    /// Fast reconstruction with basic quality
29    Fast,
30    /// Balanced speed and quality
31    Balanced,
32    /// High quality reconstruction (slower)
33    HighQuality,
34    /// Maximum quality (slowest, best results)
35    MaxQuality,
36}
37
38/// Use case scenarios that help algorithm selection
39#[derive(Debug, Clone, Copy, PartialEq)]
40pub enum UseCase {
41    /// General purpose reconstruction
42    General,
43    /// Prototyping and quick visualization
44    Prototyping,
45    /// CAD/Engineering models (high precision)
46    Engineering,
47    /// Artistic/Organic shapes (smooth surfaces)
48    Organic,
49    /// Noisy sensor data (needs robust algorithms)
50    NoisyData,
51    /// Sparse point clouds (few points)
52    Sparse,
53    /// Dense point clouds (many points)
54    Dense,
55}
56
57/// Data characteristics analyzed from the input point cloud
58#[derive(Debug, Clone)]
59pub struct DataCharacteristics {
60    /// Number of points in the cloud
61    pub point_count: usize,
62    /// Whether normals are available
63    pub has_normals: bool,
64    /// Point density uniformity (0.0 = very non-uniform, 1.0 = perfectly uniform)
65    pub density_uniformity: f32,
66    /// Estimated noise level (0.0 = no noise, 1.0 = very noisy)
67    pub noise_level: f32,
68    /// Average nearest neighbor distance
69    pub avg_neighbor_distance: f32,
70    /// Bounding box dimensions
71    pub bounding_box: (Point3f, Point3f),
72    /// Whether the surface appears to be closed
73    pub is_closed_surface: bool,
74    /// Estimated surface complexity (0.0 = simple, 1.0 = very complex)
75    pub surface_complexity: f32,
76    /// Distribution of points (planar, spherical, arbitrary)
77    pub distribution_type: DistributionType,
78}
79
80/// Types of point distribution patterns
81#[derive(Debug, Clone, Copy, PartialEq)]
82pub enum DistributionType {
83    /// Points are mostly on a plane
84    Planar,
85    /// Points form a spherical or ellipsoidal shape
86    Spherical,
87    /// Points form a cylindrical shape
88    Cylindrical,
89    /// Arbitrary 3D distribution
90    Arbitrary,
91}
92
93/// Configuration for the unified reconstruction pipeline
94#[derive(Debug, Clone)]
95pub struct PipelineConfig {
96    /// Desired quality level
97    pub quality: QualityLevel,
98    /// Use case scenario
99    pub use_case: UseCase,
100    /// Preferred algorithm (None for automatic selection)
101    pub preferred_algorithm: Option<Algorithm>,
102    /// Fallback algorithms to try if preferred fails
103    pub fallback_algorithms: Vec<Algorithm>,
104    /// Maximum processing time in seconds (None for unlimited)
105    pub max_processing_time: Option<f32>,
106    /// Enable parallel processing
107    pub enable_parallel: bool,
108    /// Validate output mesh quality
109    pub validate_output: bool,
110    /// Attempt to repair mesh if validation fails
111    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/// Result of reconstruction attempt with metadata
134#[derive(Debug)]
135pub struct ReconstructionResult {
136    /// The reconstructed mesh
137    pub mesh: TriangleMesh,
138    /// Algorithm that was used
139    pub algorithm_used: Algorithm,
140    /// Processing time in seconds
141    pub processing_time: f32,
142    /// Quality metrics of the result
143    pub quality_metrics: QualityMetrics,
144    /// Data characteristics that were analyzed
145    pub data_characteristics: DataCharacteristics,
146}
147
148/// Quality metrics for evaluating reconstruction results
149#[derive(Debug, Clone)]
150pub struct QualityMetrics {
151    /// Number of vertices in result
152    pub vertex_count: usize,
153    /// Number of triangles in result
154    pub triangle_count: usize,
155    /// Average triangle quality (0.0 = poor, 1.0 = excellent)
156    pub avg_triangle_quality: f32,
157    /// Mesh watertightness (0.0 = many holes, 1.0 = watertight)
158    pub watertightness: f32,
159    /// Surface smoothness (0.0 = rough, 1.0 = very smooth)
160    pub smoothness: f32,
161    /// Geometric accuracy compared to input (0.0 = poor, 1.0 = perfect)
162    pub geometric_accuracy: f32,
163}
164
165/// The unified reconstruction pipeline
166pub struct ReconstructionPipeline {
167    config: PipelineConfig,
168}
169
170impl ReconstructionPipeline {
171    /// Create a new reconstruction pipeline with configuration
172    pub fn new(config: PipelineConfig) -> Self {
173        Self { config }
174    }
175
176    /// Create a pipeline with default configuration
177    pub fn default() -> Self {
178        Self::new(PipelineConfig::default())
179    }
180
181    /// Create a pipeline optimized for a specific use case
182    pub fn for_use_case(use_case: UseCase) -> Self {
183        let mut config = PipelineConfig::default();
184        config.use_case = use_case;
185
186        // Adjust configuration based on use case
187        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                // Use default configuration
222            }
223        }
224
225        Self::new(config)
226    }
227
228    /// Analyze input data characteristics
229    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        // Compute bounding box
237        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        // Sample points for analysis to avoid O(n²) complexity
241        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        // Analyze nearest neighbor distances
246        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        // Estimate density uniformity
251        let density_uniformity = self.estimate_density_uniformity(&neighbor_distances);
252
253        // Estimate noise level using distance variance
254        let distance_variance = self.compute_variance(&neighbor_distances);
255        let noise_level = (distance_variance / avg_neighbor_distance.powi(2)).min(1.0);
256
257        // Determine distribution type
258        let distribution_type = self.classify_distribution(&cloud.points, &bounds_min, &bounds_max);
259
260        // Estimate surface complexity
261        let surface_complexity = self.estimate_surface_complexity(&sample_points);
262
263        // Check if surface appears closed
264        let is_closed_surface = self.estimate_surface_closure(&sample_points);
265
266        Ok(DataCharacteristics {
267            point_count,
268            has_normals: false, // Will be overridden for NormalPoint3f clouds
269            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    /// Analyze input data characteristics for clouds with normals
280    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    /// Select the best algorithm based on data characteristics and configuration
294    pub fn select_algorithm(&self, characteristics: &DataCharacteristics) -> Algorithm {
295        // Return preferred algorithm if specified
296        if let Some(preferred) = self.config.preferred_algorithm {
297            return preferred;
298        }
299
300        // Algorithm selection logic based on characteristics
301        let mut scores = HashMap::new();
302
303        // Initialize all algorithms with base scores
304        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        // Adjust scores based on point count
318        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        // Adjust based on density uniformity
342        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        // Adjust based on noise level
353        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            // Penalize sensitive algorithms
357            *scores.get_mut(&Algorithm::BallPivoting).unwrap() -= 0.2;
358        }
359
360        // Adjust based on surface complexity
361        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        // Adjust based on distribution type
369        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                // No specific preference
379            }
380        }
381
382        // Adjust based on quality requirements
383        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        // Adjust based on use case
399        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        // Find algorithm with highest score
419        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    /// Reconstruct surface from point cloud without normals
427    pub fn reconstruct(&self, cloud: &PointCloud<Point3f>) -> Result<ReconstructionResult> {
428        let start_time = std::time::Instant::now();
429
430        // Analyze data characteristics
431        let characteristics = self.analyze_data(cloud)?;
432
433        // Select best algorithm
434        let selected_algorithm = self.select_algorithm(&characteristics);
435
436        // Try reconstruction with selected algorithm
437        let mesh = match self.try_algorithm(cloud, selected_algorithm) {
438            Ok(mesh) => mesh,
439            Err(_) => {
440                // Try fallback algorithms
441                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    /// Reconstruct surface from point cloud with normals
479    pub fn reconstruct_with_normals(
480        &self,
481        cloud: &PointCloud<NormalPoint3f>,
482    ) -> Result<ReconstructionResult> {
483        let start_time = std::time::Instant::now();
484
485        // Analyze data characteristics
486        let characteristics = self.analyze_data_with_normals(cloud)?;
487
488        // Select best algorithm
489        let selected_algorithm = self.select_algorithm(&characteristics);
490
491        // Try reconstruction with selected algorithm
492        let mesh = match self.try_algorithm_with_normals(cloud, selected_algorithm) {
493            Ok(mesh) => mesh,
494            Err(_) => {
495                // Try fallback algorithms
496                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    // Helper methods for data analysis
534    fn compute_neighbor_distances(&self, points: &[Point3f]) -> Result<Vec<f32>> {
535        if points.len() < 2 {
536            return Ok(vec![1.0]); // Default fallback
537        }
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        // Convert coefficient of variation to uniformity (0.0 = non-uniform, 1.0 = uniform)
578        (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        // Check if one dimension is much smaller (planar)
612        if min_extent < max_extent * 0.1 {
613            return DistributionType::Planar;
614        }
615
616        // Check if roughly spherical/cubic
617        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; // Default for small datasets
635        }
636
637        // Simple complexity estimation based on local curvature variation
638        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            // Find nearby points
646            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                        // Fixed radius for simplicity
652                        neighbors.push(*other_point);
653                    }
654                }
655            }
656
657            if neighbors.len() >= 3 {
658                // Estimate local curvature variation
659                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        // Simple estimation using angle variations
679        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        // Simple heuristic: if points are roughly uniformly distributed around a center,
692        // assume closed surface
693        if points.len() < 50 {
694            return false; // Too few points to determine
695        }
696
697        // Compute centroid
698        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        // Check if points are roughly uniformly distributed around centroid
704        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        // If coefficient of variation is low, points are roughly at same distance from center
715        cv < 0.3
716    }
717
718    // Helper methods for algorithm execution
719    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                // Convert point cloud to volumetric representation
735                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                // Cannot use Poisson without normals
743                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                // Convert to MLS and extract
775                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        // Simple quality metrics - in a real implementation these would be more sophisticated
793        let avg_triangle_quality = 0.75; // Placeholder
794        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; // Placeholder
801
802        QualityMetrics {
803            vertex_count,
804            triangle_count,
805            avg_triangle_quality,
806            watertightness,
807            smoothness,
808            geometric_accuracy,
809        }
810    }
811}
812
813/// Convenience function for quick reconstruction with automatic algorithm selection
814pub fn auto_reconstruct(cloud: &PointCloud<Point3f>) -> Result<TriangleMesh> {
815    let pipeline = ReconstructionPipeline::default();
816    Ok(pipeline.reconstruct(cloud)?.mesh)
817}
818
819/// Convenience function for quick reconstruction with normals
820pub 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
825/// Convenience function for reconstruction with specific quality level
826pub 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
836/// Convenience function for reconstruction optimized for a use case
837pub 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        // For sparse planar data, should prefer Delaunay
908        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        // For dense, uniform data with normals, should prefer Poisson or BallPivoting
931        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                // Algorithm may fail on simple test data - that's acceptable
954                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            // Just test that pipelines can be created with different quality levels
975        }
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        // Test planar distribution
1001        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        // Test spherical distribution
1013        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}