mesh_repair/
progress.rs

1//! Progress reporting and operation estimation for long-running operations.
2//!
3//! This module provides infrastructure for:
4//! - Progress callbacks during expensive operations
5//! - Operation time estimation based on mesh complexity
6//! - Cancellation support via progress callbacks
7//!
8//! # Example
9//!
10//! ```ignore
11//! use mesh_repair::progress::{Progress, ProgressCallback};
12//!
13//! // Create a progress callback
14//! let callback: ProgressCallback = Box::new(|progress| {
15//!     println!("{}% complete: {}", (progress.fraction() * 100.0) as u32, progress.message);
16//!     true // Continue processing (return false to cancel)
17//! });
18//!
19//! // Use with an operation
20//! let result = mesh.analyze_thickness_with_progress(&params, Some(&callback))?;
21//! ```
22//!
23//! # Estimation
24//!
25//! ```ignore
26//! use mesh_repair::progress::estimate_operation_time;
27//!
28//! let estimate = estimate_operation_time(&mesh, OperationType::Remesh);
29//! println!("Estimated time: {:.1}s", estimate.estimated_seconds);
30//! ```
31
32use std::sync::Arc;
33use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
34use std::time::{Duration, Instant};
35
36/// Progress information passed to callbacks.
37#[derive(Debug, Clone)]
38pub struct Progress {
39    /// Current step (0-based).
40    pub current: u64,
41
42    /// Total number of steps.
43    pub total: u64,
44
45    /// Human-readable message describing current operation.
46    pub message: String,
47
48    /// Elapsed time since operation started.
49    pub elapsed: Duration,
50
51    /// Estimated time remaining (if available).
52    pub estimated_remaining: Option<Duration>,
53}
54
55impl Progress {
56    /// Create a new progress report.
57    pub fn new(current: u64, total: u64, message: impl Into<String>) -> Self {
58        Self {
59            current,
60            total,
61            message: message.into(),
62            elapsed: Duration::ZERO,
63            estimated_remaining: None,
64        }
65    }
66
67    /// Get progress as a fraction (0.0 to 1.0).
68    #[inline]
69    pub fn fraction(&self) -> f64 {
70        if self.total == 0 {
71            0.0
72        } else {
73            (self.current as f64) / (self.total as f64)
74        }
75    }
76
77    /// Get progress as a percentage (0 to 100).
78    #[inline]
79    pub fn percent(&self) -> u32 {
80        (self.fraction() * 100.0).round() as u32
81    }
82
83    /// Check if the operation is complete.
84    #[inline]
85    pub fn is_complete(&self) -> bool {
86        self.current >= self.total
87    }
88}
89
90/// Callback function for progress reporting.
91///
92/// Returns `true` to continue, `false` to request cancellation.
93pub type ProgressCallback = Box<dyn Fn(&Progress) -> bool + Send + Sync>;
94
95/// A thread-safe progress tracker for parallel operations.
96///
97/// This tracker uses atomic operations to allow multiple threads to
98/// update progress simultaneously without locks.
99#[derive(Debug)]
100pub struct ProgressTracker {
101    current: AtomicU64,
102    total: u64,
103    cancelled: AtomicBool,
104    start_time: Instant,
105    last_callback_time: std::sync::Mutex<Instant>,
106    callback_interval: Duration,
107}
108
109impl ProgressTracker {
110    /// Create a new progress tracker.
111    pub fn new(total: u64) -> Self {
112        Self {
113            current: AtomicU64::new(0),
114            total,
115            cancelled: AtomicBool::new(false),
116            start_time: Instant::now(),
117            last_callback_time: std::sync::Mutex::new(Instant::now()),
118            callback_interval: Duration::from_millis(100), // Don't callback too frequently
119        }
120    }
121
122    /// Create a tracker with custom callback interval.
123    pub fn with_interval(total: u64, interval: Duration) -> Self {
124        let mut tracker = Self::new(total);
125        tracker.callback_interval = interval;
126        tracker
127    }
128
129    /// Increment progress by one.
130    #[inline]
131    pub fn increment(&self) {
132        self.current.fetch_add(1, Ordering::Relaxed);
133    }
134
135    /// Increment progress by a specific amount.
136    #[inline]
137    pub fn increment_by(&self, amount: u64) {
138        self.current.fetch_add(amount, Ordering::Relaxed);
139    }
140
141    /// Set the current progress value.
142    #[inline]
143    pub fn set(&self, value: u64) {
144        self.current.store(value, Ordering::Relaxed);
145    }
146
147    /// Get the current progress value.
148    #[inline]
149    pub fn current(&self) -> u64 {
150        self.current.load(Ordering::Relaxed)
151    }
152
153    /// Get the total count.
154    #[inline]
155    pub fn total(&self) -> u64 {
156        self.total
157    }
158
159    /// Check if cancellation was requested.
160    #[inline]
161    pub fn is_cancelled(&self) -> bool {
162        self.cancelled.load(Ordering::Relaxed)
163    }
164
165    /// Request cancellation.
166    pub fn cancel(&self) {
167        self.cancelled.store(true, Ordering::Relaxed);
168    }
169
170    /// Get progress as a fraction (0.0 to 1.0).
171    #[inline]
172    pub fn fraction(&self) -> f64 {
173        if self.total == 0 {
174            0.0
175        } else {
176            (self.current() as f64) / (self.total as f64)
177        }
178    }
179
180    /// Get elapsed time.
181    #[inline]
182    pub fn elapsed(&self) -> Duration {
183        self.start_time.elapsed()
184    }
185
186    /// Estimate remaining time based on current progress.
187    pub fn estimated_remaining(&self) -> Option<Duration> {
188        let current = self.current();
189        if current == 0 {
190            return None;
191        }
192
193        let elapsed = self.elapsed();
194        let rate = current as f64 / elapsed.as_secs_f64();
195
196        if rate > 0.0 {
197            let remaining = (self.total - current) as f64 / rate;
198            Some(Duration::from_secs_f64(remaining))
199        } else {
200            None
201        }
202    }
203
204    /// Create a Progress snapshot.
205    pub fn snapshot(&self, message: impl Into<String>) -> Progress {
206        Progress {
207            current: self.current(),
208            total: self.total,
209            message: message.into(),
210            elapsed: self.elapsed(),
211            estimated_remaining: self.estimated_remaining(),
212        }
213    }
214
215    /// Call the callback if enough time has passed since last call.
216    ///
217    /// Returns `false` if the callback requested cancellation.
218    pub fn maybe_callback(
219        &self,
220        callback: Option<&ProgressCallback>,
221        message: impl Into<String>,
222    ) -> bool {
223        if self.is_cancelled() {
224            return false;
225        }
226
227        let callback = match callback {
228            Some(cb) => cb,
229            None => return true,
230        };
231
232        // Check if enough time has passed
233        let now = Instant::now();
234        {
235            let last = self.last_callback_time.lock().unwrap();
236            if now.duration_since(*last) < self.callback_interval {
237                return true;
238            }
239        }
240
241        // Update last callback time and call
242        {
243            let mut last = self.last_callback_time.lock().unwrap();
244            *last = now;
245        }
246
247        let progress = self.snapshot(message);
248        let should_continue = callback(&progress);
249
250        if !should_continue {
251            self.cancel();
252        }
253
254        should_continue
255    }
256}
257
258/// Arc-wrapped progress tracker for sharing across threads.
259pub type SharedProgressTracker = Arc<ProgressTracker>;
260
261/// Create a shared progress tracker.
262pub fn shared_tracker(total: u64) -> SharedProgressTracker {
263    Arc::new(ProgressTracker::new(total))
264}
265
266// ============================================================================
267// Operation Time Estimation
268// ============================================================================
269
270/// Types of operations that can be estimated.
271#[derive(Debug, Clone, Copy, PartialEq, Eq)]
272pub enum OperationType {
273    /// Mesh validation.
274    Validate,
275    /// Mesh repair (all steps).
276    Repair,
277    /// Hole filling.
278    FillHoles,
279    /// Winding order fix.
280    FixWinding,
281    /// Isotropic remeshing.
282    Remesh,
283    /// Mesh decimation.
284    Decimate,
285    /// Loop subdivision.
286    Subdivide,
287    /// Wall thickness analysis.
288    ThicknessAnalysis,
289    /// Self-intersection detection.
290    SelfIntersection,
291    /// Boolean operation.
292    Boolean,
293    /// Mesh morphing.
294    Morph,
295    /// ICP registration.
296    Registration,
297    /// Surface reconstruction from point cloud.
298    SurfaceReconstruction,
299    /// Lattice infill generation.
300    LatticeGeneration,
301    /// Slicing for 3D printing.
302    Slice,
303}
304
305/// Estimate of operation time and resources.
306#[derive(Debug, Clone)]
307pub struct OperationEstimate {
308    /// Estimated time in seconds.
309    pub estimated_seconds: f64,
310
311    /// Confidence level (0.0 to 1.0).
312    pub confidence: f64,
313
314    /// Estimated memory usage in bytes.
315    pub estimated_memory_bytes: u64,
316
317    /// Whether the operation supports progress callbacks.
318    pub supports_progress: bool,
319
320    /// Whether the operation supports cancellation.
321    pub supports_cancellation: bool,
322
323    /// Estimated number of iterations (if applicable).
324    pub estimated_iterations: Option<u64>,
325
326    /// Complexity description.
327    pub complexity: String,
328}
329
330impl OperationEstimate {
331    fn new(seconds: f64, complexity: &str) -> Self {
332        Self {
333            estimated_seconds: seconds,
334            confidence: 0.7,
335            estimated_memory_bytes: 0,
336            supports_progress: true,
337            supports_cancellation: true,
338            estimated_iterations: None,
339            complexity: complexity.to_string(),
340        }
341    }
342}
343
344/// Estimate the time for an operation based on mesh complexity.
345///
346/// # Arguments
347/// * `vertex_count` - Number of vertices in the mesh
348/// * `face_count` - Number of faces in the mesh
349/// * `operation` - Type of operation to estimate
350///
351/// # Returns
352/// An estimate of time and resources needed.
353///
354/// # Note
355/// Estimates are based on typical modern hardware (mid-range CPU).
356/// Actual times may vary significantly based on:
357/// - CPU speed and core count
358/// - Memory bandwidth
359/// - Mesh topology (uniform vs highly varying)
360/// - Specific parameters used
361pub fn estimate_operation_time(
362    vertex_count: usize,
363    face_count: usize,
364    operation: OperationType,
365) -> OperationEstimate {
366    let v = vertex_count as f64;
367    let f = face_count as f64;
368
369    match operation {
370        OperationType::Validate => {
371            // O(V + F) - linear scan
372            let seconds = (v + f) / 10_000_000.0;
373            OperationEstimate::new(seconds, "O(V + F)")
374        }
375
376        OperationType::Repair => {
377            // Multiple passes: O(V + F) for each step
378            let seconds = (v + f) / 1_000_000.0;
379            OperationEstimate::new(seconds, "O(V + F) multiple passes")
380        }
381
382        OperationType::FillHoles => {
383            // O(B) where B is boundary edges, typically O(V^0.5)
384            let seconds = v.sqrt() / 10_000.0;
385            OperationEstimate::new(seconds, "O(boundary edges)")
386        }
387
388        OperationType::FixWinding => {
389            // O(F) BFS through faces
390            let seconds = f / 5_000_000.0;
391            OperationEstimate::new(seconds, "O(F)")
392        }
393
394        OperationType::Remesh => {
395            // O(V * I) where I is iterations, typically 5-10
396            let iterations = 7.0;
397            let seconds = v * iterations / 500_000.0;
398            let mut est = OperationEstimate::new(seconds, "O(V × iterations)");
399            est.estimated_iterations = Some(iterations as u64);
400            est
401        }
402
403        OperationType::Decimate => {
404            // O(V log V) due to priority queue
405            let seconds = v * v.ln() / 1_000_000.0;
406            OperationEstimate::new(seconds, "O(V log V)")
407        }
408
409        OperationType::Subdivide => {
410            // O(F) per iteration, output grows 4x per iteration
411            let seconds = f / 2_000_000.0;
412            OperationEstimate::new(seconds, "O(F × 4^iterations)")
413        }
414
415        OperationType::ThicknessAnalysis => {
416            // O(V × F) for ray-triangle tests with BVH: O(V log F)
417            let seconds = v * f.ln() / 500_000.0;
418            OperationEstimate::new(seconds, "O(V log F) with BVH")
419        }
420
421        OperationType::SelfIntersection => {
422            // O(F²) worst case, O(F log F) with BVH
423            let seconds = f * f.ln() / 100_000.0;
424            OperationEstimate::new(seconds, "O(F log F) with BVH")
425        }
426
427        OperationType::Boolean => {
428            // Complex: O((F₁ + F₂) log(F₁ + F₂)) typical
429            let seconds = f * f.ln() / 50_000.0;
430            OperationEstimate::new(seconds, "O(F log F)")
431        }
432
433        OperationType::Morph => {
434            // RBF: O(V²) for full, O(V) for local
435            let seconds = v * v.sqrt() / 100_000.0;
436            OperationEstimate::new(seconds, "O(V√V) local RBF")
437        }
438
439        OperationType::Registration => {
440            // ICP: O(V × I) where I is iterations
441            let iterations = 50.0;
442            let seconds = v * iterations / 1_000_000.0;
443            let mut est = OperationEstimate::new(seconds, "O(V × iterations)");
444            est.estimated_iterations = Some(iterations as u64);
445            est
446        }
447
448        OperationType::SurfaceReconstruction => {
449            // Grid-based: O(V + G³) where G is grid dimension
450            let grid_size = (v / 10.0).powf(1.0 / 3.0).max(10.0);
451            let seconds = (v + grid_size.powi(3)) / 500_000.0;
452            OperationEstimate::new(seconds, "O(V + grid³)")
453        }
454
455        OperationType::LatticeGeneration => {
456            // O(cells) where cells depends on volume and cell size
457            let seconds = v / 100_000.0;
458            OperationEstimate::new(seconds, "O(volume / cell_size³)")
459        }
460
461        OperationType::Slice => {
462            // O(F × layers)
463            let layers = 100.0;
464            let seconds = f * layers / 10_000_000.0;
465            let mut est = OperationEstimate::new(seconds, "O(F × layers)");
466            est.estimated_iterations = Some(layers as u64);
467            est
468        }
469    }
470}
471
472// ============================================================================
473// Parallel Utilities
474// ============================================================================
475
476/// Trait for operations that can report progress.
477pub trait ProgressReporter {
478    /// Report progress during an operation.
479    fn report_progress(&self, current: u64, total: u64, message: &str) -> bool;
480}
481
482/// A no-op progress reporter that does nothing.
483pub struct NoOpProgressReporter;
484
485impl ProgressReporter for NoOpProgressReporter {
486    #[inline]
487    fn report_progress(&self, _current: u64, _total: u64, _message: &str) -> bool {
488        true
489    }
490}
491
492/// A progress reporter that calls a callback.
493pub struct CallbackProgressReporter<'a> {
494    callback: &'a ProgressCallback,
495    start_time: Instant,
496}
497
498impl<'a> CallbackProgressReporter<'a> {
499    /// Create a new callback reporter.
500    pub fn new(callback: &'a ProgressCallback) -> Self {
501        Self {
502            callback,
503            start_time: Instant::now(),
504        }
505    }
506}
507
508impl ProgressReporter for CallbackProgressReporter<'_> {
509    fn report_progress(&self, current: u64, total: u64, message: &str) -> bool {
510        let elapsed = self.start_time.elapsed();
511        let estimated_remaining = if current > 0 {
512            let rate = current as f64 / elapsed.as_secs_f64();
513            if rate > 0.0 {
514                let remaining = (total - current) as f64 / rate;
515                Some(Duration::from_secs_f64(remaining))
516            } else {
517                None
518            }
519        } else {
520            None
521        };
522
523        let progress = Progress {
524            current,
525            total,
526            message: message.to_string(),
527            elapsed,
528            estimated_remaining,
529        };
530
531        (self.callback)(&progress)
532    }
533}
534
535// ============================================================================
536// Tests
537// ============================================================================
538
539#[cfg(test)]
540mod tests {
541    use super::*;
542    use std::sync::atomic::AtomicU32;
543
544    #[test]
545    fn test_progress_fraction() {
546        let p = Progress::new(50, 100, "test");
547        assert!((p.fraction() - 0.5).abs() < 1e-10);
548        assert_eq!(p.percent(), 50);
549    }
550
551    #[test]
552    fn test_progress_complete() {
553        let p1 = Progress::new(50, 100, "incomplete");
554        assert!(!p1.is_complete());
555
556        let p2 = Progress::new(100, 100, "complete");
557        assert!(p2.is_complete());
558    }
559
560    #[test]
561    fn test_progress_zero_total() {
562        let p = Progress::new(0, 0, "empty");
563        assert!((p.fraction() - 0.0).abs() < 1e-10);
564        assert_eq!(p.percent(), 0);
565    }
566
567    #[test]
568    fn test_progress_tracker() {
569        let tracker = ProgressTracker::new(100);
570
571        assert_eq!(tracker.current(), 0);
572        assert_eq!(tracker.total(), 100);
573        assert!(!tracker.is_cancelled());
574
575        tracker.increment();
576        assert_eq!(tracker.current(), 1);
577
578        tracker.increment_by(9);
579        assert_eq!(tracker.current(), 10);
580
581        tracker.set(50);
582        assert_eq!(tracker.current(), 50);
583        assert!((tracker.fraction() - 0.5).abs() < 1e-10);
584    }
585
586    #[test]
587    fn test_progress_tracker_cancel() {
588        let tracker = ProgressTracker::new(100);
589
590        assert!(!tracker.is_cancelled());
591        tracker.cancel();
592        assert!(tracker.is_cancelled());
593    }
594
595    #[test]
596    fn test_shared_tracker() {
597        let tracker = shared_tracker(100);
598        let tracker_clone = tracker.clone();
599
600        // Both references should see the same state
601        tracker.increment();
602        assert_eq!(tracker_clone.current(), 1);
603    }
604
605    #[test]
606    fn test_progress_callback() {
607        let counter = Arc::new(AtomicU32::new(0));
608        let counter_clone = counter.clone();
609
610        let callback: ProgressCallback = Box::new(move |p| {
611            counter_clone.fetch_add(1, Ordering::SeqCst);
612            p.current < 5 // Cancel after 5 calls
613        });
614
615        let tracker = ProgressTracker::with_interval(10, Duration::ZERO);
616
617        for i in 0..10 {
618            tracker.set(i);
619            if !tracker.maybe_callback(Some(&callback), "test") {
620                break;
621            }
622        }
623
624        // Should have been called multiple times before cancellation
625        let calls = counter.load(Ordering::SeqCst);
626        assert!(calls >= 5, "Expected at least 5 calls, got {}", calls);
627    }
628
629    #[test]
630    fn test_operation_estimate() {
631        let est = estimate_operation_time(10000, 20000, OperationType::Validate);
632
633        assert!(est.estimated_seconds > 0.0);
634        assert!(est.confidence > 0.0 && est.confidence <= 1.0);
635        assert!(est.supports_progress);
636    }
637
638    #[test]
639    fn test_operation_estimates_scale() {
640        // Larger meshes should have larger estimates
641        let small = estimate_operation_time(1000, 2000, OperationType::Remesh);
642        let large = estimate_operation_time(100000, 200000, OperationType::Remesh);
643
644        assert!(
645            large.estimated_seconds > small.estimated_seconds,
646            "Large mesh estimate ({}) should be greater than small ({})",
647            large.estimated_seconds,
648            small.estimated_seconds
649        );
650    }
651
652    #[test]
653    fn test_noop_progress_reporter() {
654        let reporter = NoOpProgressReporter;
655        assert!(reporter.report_progress(50, 100, "test"));
656    }
657
658    #[test]
659    fn test_callback_progress_reporter() {
660        let called = Arc::new(AtomicBool::new(false));
661        let called_clone = called.clone();
662
663        let callback: ProgressCallback = Box::new(move |_p| {
664            called_clone.store(true, Ordering::SeqCst);
665            true
666        });
667
668        let reporter = CallbackProgressReporter::new(&callback);
669        let result = reporter.report_progress(50, 100, "test");
670
671        assert!(result);
672        assert!(called.load(Ordering::SeqCst));
673    }
674}