scirs2_cluster/visualization/
interactive.rs

1//! Interactive 3D visualization capabilities for clustering results
2//!
3//! This module provides advanced interactive 3D visualization features including
4//! real-time manipulation, dynamic clustering updates, multi-view perspectives,
5//! and immersive exploration tools for complex clustering scenarios.
6
7use scirs2_core::ndarray::{Array1, Array2, ArrayView2};
8use scirs2_core::numeric::{Float, FromPrimitive};
9use std::collections::HashMap;
10use std::fmt::Debug;
11
12use serde::{Deserialize, Serialize};
13
14use super::{ColorScheme, ScatterPlot3D, VisualizationConfig};
15use crate::error::{ClusteringError, Result};
16
17/// Configuration for interactive 3D visualizations
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct InteractiveConfig {
20    /// Enable camera controls (rotation, zoom, pan)
21    pub enable_camera_controls: bool,
22    /// Enable point selection and highlighting
23    pub enable_point_selection: bool,
24    /// Enable cluster manipulation (drag centroids)
25    pub enable_cluster_manipulation: bool,
26    /// Show coordinate axes
27    pub show_axes: bool,
28    /// Show grid
29    pub show_grid: bool,
30    /// Enable real-time statistics display
31    pub show_realtime_stats: bool,
32    /// Enable multi-view layout
33    pub multi_view: bool,
34    /// Number of simultaneous views
35    pub view_count: usize,
36    /// Enable VR/AR mode
37    pub enable_vr_mode: bool,
38    /// Enable stereoscopic rendering
39    pub stereoscopic: bool,
40    /// Field of view for 3D perspective
41    pub field_of_view: f32,
42    /// Camera movement sensitivity
43    pub camera_sensitivity: f32,
44    /// Point highlighting on hover
45    pub highlight_on_hover: bool,
46    /// Show cluster boundaries in 3D
47    pub show_3d_boundaries: bool,
48    /// Enable temporal view (for time series clustering)
49    pub temporal_view: bool,
50}
51
52impl Default for InteractiveConfig {
53    fn default() -> Self {
54        Self {
55            enable_camera_controls: true,
56            enable_point_selection: true,
57            enable_cluster_manipulation: false,
58            show_axes: true,
59            show_grid: true,
60            show_realtime_stats: true,
61            multi_view: false,
62            view_count: 1,
63            enable_vr_mode: false,
64            stereoscopic: false,
65            field_of_view: 60.0,
66            camera_sensitivity: 1.0,
67            highlight_on_hover: true,
68            show_3d_boundaries: true,
69            temporal_view: false,
70        }
71    }
72}
73
74/// Camera state for 3D visualization
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct CameraState {
77    /// Camera position (x, y, z)
78    pub position: (f64, f64, f64),
79    /// Look-at target (x, y, z)
80    pub target: (f64, f64, f64),
81    /// Up vector (x, y, z)
82    pub up: (f64, f64, f64),
83    /// Field of view in degrees
84    pub fov: f32,
85    /// Near clipping plane
86    pub near: f64,
87    /// Far clipping plane
88    pub far: f64,
89    /// Camera rotation (euler angles: pitch, yaw, roll)
90    pub rotation: (f64, f64, f64),
91    /// Zoom level
92    pub zoom: f64,
93}
94
95impl Default for CameraState {
96    fn default() -> Self {
97        Self {
98            position: (10.0, 10.0, 10.0),
99            target: (0.0, 0.0, 0.0),
100            up: (0.0, 1.0, 0.0),
101            fov: 60.0,
102            near: 0.1,
103            far: 1000.0,
104            rotation: (0.0, 0.0, 0.0),
105            zoom: 1.0,
106        }
107    }
108}
109
110/// Interactive state management for 3D visualization
111#[derive(Debug, Clone)]
112pub struct InteractiveState {
113    /// Current camera state
114    pub camera: CameraState,
115    /// Selected points
116    pub selected_points: Vec<usize>,
117    /// Highlighted points (on hover)
118    pub highlighted_points: Vec<usize>,
119    /// Active cluster (for manipulation)
120    pub active_cluster: Option<i32>,
121    /// Mouse/touch input state
122    pub input_state: InputState,
123    /// View bounds for each dimension
124    pub view_bounds: (f64, f64, f64, f64, f64, f64),
125    /// Current time (for temporal views)
126    pub current_time: f64,
127    /// Animation playback state
128    pub animation_playing: bool,
129    /// Current view mode
130    pub view_mode: ViewMode,
131}
132
133/// Input state for interactive controls
134#[derive(Debug, Clone)]
135pub struct InputState {
136    /// Mouse position (x, y)
137    pub mouse_position: (f64, f64),
138    /// Previous mouse position
139    pub prev_mouse_position: (f64, f64),
140    /// Mouse buttons pressed
141    pub mouse_buttons: Vec<MouseButton>,
142    /// Keyboard keys pressed
143    pub keys_pressed: Vec<KeyCode>,
144    /// Touch points (for multi-touch)
145    pub touch_points: Vec<TouchPoint>,
146    /// Gesture state
147    pub gesture_state: GestureState,
148}
149
150/// Mouse button identifiers
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub enum MouseButton {
153    Left,
154    Right,
155    Middle,
156    Other(u8),
157}
158
159/// Key codes for keyboard input
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub enum KeyCode {
162    Space,
163    Enter,
164    Escape,
165    ArrowUp,
166    ArrowDown,
167    ArrowLeft,
168    ArrowRight,
169    Shift,
170    Ctrl,
171    Alt,
172    Key(char),
173}
174
175/// Touch point for multi-touch input
176#[derive(Debug, Clone)]
177pub struct TouchPoint {
178    pub id: u64,
179    pub position: (f64, f64),
180    pub pressure: f64,
181}
182
183/// Gesture recognition state
184#[derive(Debug, Clone)]
185pub struct GestureState {
186    pub is_pinching: bool,
187    pub pinch_scale: f64,
188    pub is_rotating: bool,
189    pub rotation_angle: f64,
190    pub is_panning: bool,
191    pub pan_delta: (f64, f64),
192}
193
194/// 3D view modes
195#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
196pub enum ViewMode {
197    /// Standard perspective view
198    Perspective,
199    /// Orthographic projection
200    Orthographic,
201    /// First-person view
202    FirstPerson,
203    /// Bird's eye view
204    BirdsEye,
205    /// Side view
206    Side,
207    /// Front view
208    Front,
209    /// Top view
210    Top,
211    /// Split screen (multiple views)
212    SplitScreen,
213    /// VR stereo view
214    VRStereo,
215}
216
217/// Real-time cluster statistics for interactive display
218#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct ClusterStats {
220    /// Cluster ID
221    pub cluster_id: i32,
222    /// Number of points in cluster
223    pub point_count: usize,
224    /// Cluster centroid
225    pub centroid: Array1<f64>,
226    /// Cluster diameter (maximum distance between points)
227    pub diameter: f64,
228    /// Average distance to centroid
229    pub avg_distance_to_centroid: f64,
230    /// Cluster density
231    pub density: f64,
232    /// Bounding box (min_x, max_x, min_y, max_y, min_z, max_z)
233    pub bounding_box: (f64, f64, f64, f64, f64, f64),
234    /// Cluster color
235    pub color: String,
236}
237
238/// Interactive 3D visualizer
239pub struct InteractiveVisualizer {
240    config: InteractiveConfig,
241    state: InteractiveState,
242    cluster_stats: HashMap<i32, ClusterStats>,
243    last_update: std::time::Instant,
244}
245
246impl InteractiveVisualizer {
247    /// Create a new interactive visualizer
248    pub fn new(config: InteractiveConfig) -> Self {
249        Self {
250            config,
251            state: InteractiveState {
252                camera: CameraState::default(),
253                selected_points: Vec::new(),
254                highlighted_points: Vec::new(),
255                active_cluster: None,
256                input_state: InputState {
257                    mouse_position: (0.0, 0.0),
258                    prev_mouse_position: (0.0, 0.0),
259                    mouse_buttons: Vec::new(),
260                    keys_pressed: Vec::new(),
261                    touch_points: Vec::new(),
262                    gesture_state: GestureState {
263                        is_pinching: false,
264                        pinch_scale: 1.0,
265                        is_rotating: false,
266                        rotation_angle: 0.0,
267                        is_panning: false,
268                        pan_delta: (0.0, 0.0),
269                    },
270                },
271                view_bounds: (-10.0, 10.0, -10.0, 10.0, -10.0, 10.0),
272                current_time: 0.0,
273                animation_playing: false,
274                view_mode: ViewMode::Perspective,
275            },
276            cluster_stats: HashMap::new(),
277            last_update: std::time::Instant::now(),
278        }
279    }
280
281    /// Update visualization with new data
282    pub fn update_data<F: Float + FromPrimitive + Debug>(
283        &mut self,
284        data: ArrayView2<F>,
285        labels: &Array1<i32>,
286        centroids: Option<&Array2<F>>,
287    ) -> Result<()> {
288        // Calculate cluster statistics
289        self.calculate_cluster_stats(data, labels, centroids)?;
290
291        // Update view bounds if needed
292        self.update_view_bounds(data);
293
294        // Reset selection if data changed significantly
295        self.validate_selections(data.nrows());
296
297        Ok(())
298    }
299
300    /// Handle mouse input
301    pub fn handle_mouse_input(&mut self, button: MouseButton, position: (f64, f64), pressed: bool) {
302        self.state.input_state.prev_mouse_position = self.state.input_state.mouse_position;
303        self.state.input_state.mouse_position = position;
304
305        if pressed {
306            if !self.state.input_state.mouse_buttons.contains(&button) {
307                self.state.input_state.mouse_buttons.push(button);
308            }
309        } else {
310            self.state
311                .input_state
312                .mouse_buttons
313                .retain(|&b| b != button);
314        }
315
316        // Handle camera controls
317        if self.config.enable_camera_controls {
318            self.handle_camera_input();
319        }
320    }
321
322    /// Handle keyboard input
323    pub fn handle_keyboard_input(&mut self, key: KeyCode, pressed: bool) {
324        if pressed {
325            if !self.state.input_state.keys_pressed.contains(&key) {
326                self.state.input_state.keys_pressed.push(key);
327            }
328        } else {
329            self.state.input_state.keys_pressed.retain(|&k| k != key);
330        }
331
332        // Handle special key combinations
333        self.handle_keyboard_shortcuts(key, pressed);
334    }
335
336    /// Handle touch input for mobile/tablet interfaces
337    pub fn handle_touch_input(&mut self, touchpoints: Vec<TouchPoint>) {
338        let prev_touch_count = self.state.input_state.touch_points.len();
339        self.state.input_state.touch_points = touchpoints;
340        let current_touch_count = self.state.input_state.touch_points.len();
341
342        // Gesture recognition
343        self.update_gesture_state(prev_touch_count, current_touch_count);
344
345        // Handle multi-touch gestures
346        if self.config.enable_camera_controls {
347            self.handle_touch_gestures();
348        }
349    }
350
351    /// Select points within a 3D region
352    pub fn select_points_in_region(&mut self, region: BoundingBox3D) -> Vec<usize> {
353        // This would be implemented with actual 3D point-in-box testing
354        // For now, return empty selection
355        let selected = Vec::new();
356        self.state.selected_points = selected.clone();
357        selected
358    }
359
360    /// Highlight points at screen coordinates
361    pub fn highlight_points_at(&mut self, screenpos: (f64, f64)) -> Vec<usize> {
362        // This would implement 3D picking/ray casting
363        // For now, return empty highlights
364        let highlighted = Vec::new();
365        self.state.highlighted_points = highlighted.clone();
366        highlighted
367    }
368
369    /// Get current cluster statistics
370    pub fn get_cluster_stats(&self) -> &HashMap<i32, ClusterStats> {
371        &self.cluster_stats
372    }
373
374    /// Get current interactive state
375    pub fn get_state(&self) -> &InteractiveState {
376        &self.state
377    }
378
379    /// Set camera position
380    pub fn set_camera_position(&mut self, position: (f64, f64, f64)) {
381        self.state.camera.position = position;
382    }
383
384    /// Set camera target
385    pub fn set_camera_target(&mut self, target: (f64, f64, f64)) {
386        self.state.camera.target = target;
387    }
388
389    /// Set view mode
390    pub fn set_view_mode(&mut self, mode: ViewMode) {
391        self.state.view_mode = mode;
392
393        // Adjust camera for specific view modes
394        match mode {
395            ViewMode::BirdsEye => {
396                self.state.camera.position = (0.0, 20.0, 0.0);
397                self.state.camera.target = (0.0, 0.0, 0.0);
398                self.state.camera.up = (0.0, 0.0, -1.0);
399            }
400            ViewMode::Side => {
401                self.state.camera.position = (20.0, 0.0, 0.0);
402                self.state.camera.target = (0.0, 0.0, 0.0);
403                self.state.camera.up = (0.0, 1.0, 0.0);
404            }
405            ViewMode::Front => {
406                self.state.camera.position = (0.0, 0.0, 20.0);
407                self.state.camera.target = (0.0, 0.0, 0.0);
408                self.state.camera.up = (0.0, 1.0, 0.0);
409            }
410            ViewMode::Top => {
411                self.state.camera.position = (0.0, 20.0, 0.0);
412                self.state.camera.target = (0.0, 0.0, 0.0);
413                self.state.camera.up = (0.0, 0.0, -1.0);
414            }
415            _ => {
416                // Keep current camera settings for other modes
417            }
418        }
419    }
420
421    /// Enable/disable animation playback
422    pub fn set_animation_playing(&mut self, playing: bool) {
423        self.state.animation_playing = playing;
424    }
425
426    /// Set current time for temporal views
427    pub fn set_current_time(&mut self, time: f64) {
428        self.state.current_time = time;
429    }
430
431    /// Generate export data for current view
432    pub fn export_view_state(&self) -> Result<String> {
433        #[cfg(feature = "serde")]
434        {
435            let export_data = InteractiveViewExport {
436                camera: self.state.camera.clone(),
437                view_mode: self.state.view_mode,
438                cluster_stats: self.cluster_stats.clone(),
439                view_bounds: self.state.view_bounds,
440                current_time: self.state.current_time,
441            };
442
443            return serde_json::to_string_pretty(&export_data)
444                .map_err(|e| ClusteringError::ComputationError(format!("Export failed: {}", e)));
445        }
446
447        #[cfg(not(feature = "serde"))]
448        {
449            Err(ClusteringError::ComputationError(
450                "Export requires 'serde' feature".to_string(),
451            ))
452        }
453    }
454
455    /// Calculate cluster statistics
456    fn calculate_cluster_stats<F: Float + FromPrimitive + Debug>(
457        &mut self,
458        data: ArrayView2<F>,
459        labels: &Array1<i32>,
460        centroids: Option<&Array2<F>>,
461    ) -> Result<()> {
462        self.cluster_stats.clear();
463
464        // Get unique cluster labels
465        let mut unique_labels: Vec<i32> = labels.iter().cloned().collect();
466        unique_labels.sort_unstable();
467        unique_labels.dedup();
468
469        for &cluster_id in &unique_labels {
470            // Find points in this cluster
471            let cluster_points: Vec<usize> = labels
472                .iter()
473                .enumerate()
474                .filter(|(_, &label)| label == cluster_id)
475                .map(|(idx_, _)| idx_)
476                .collect();
477
478            if cluster_points.is_empty() {
479                continue;
480            }
481
482            // Calculate centroid
483            let centroid = if let Some(cents) = centroids {
484                if cluster_id >= 0 && (cluster_id as usize) < cents.nrows() {
485                    cents
486                        .row(cluster_id as usize)
487                        .mapv(|x| x.to_f64().unwrap_or(0.0))
488                } else {
489                    // Calculate centroid from points
490                    self.calculate_centroid_from_points(data, &cluster_points)?
491                }
492            } else {
493                self.calculate_centroid_from_points(data, &cluster_points)?
494            };
495
496            // Calculate statistics
497            let (diameter, avg_distance, density, bounding_box) =
498                self.calculate_cluster_metrics(data, &cluster_points, &centroid)?;
499
500            let stats = ClusterStats {
501                cluster_id,
502                point_count: cluster_points.len(),
503                centroid,
504                diameter,
505                avg_distance_to_centroid: avg_distance,
506                density,
507                bounding_box,
508                color: format!("#{:06x}", (cluster_id.unsigned_abs() * 123456) % 0xFFFFFF),
509            };
510
511            self.cluster_stats.insert(cluster_id, stats);
512        }
513
514        Ok(())
515    }
516
517    /// Calculate centroid from points
518    fn calculate_centroid_from_points<F: Float + FromPrimitive + Debug>(
519        &self,
520        data: ArrayView2<F>,
521        point_indices: &[usize],
522    ) -> Result<Array1<f64>> {
523        let n_features = data.ncols();
524        let mut centroid = Array1::zeros(n_features);
525
526        for &idx in point_indices {
527            for j in 0..n_features {
528                centroid[j] += data[[idx, j]].to_f64().unwrap_or(0.0);
529            }
530        }
531
532        let count = point_indices.len() as f64;
533        if count > 0.0 {
534            centroid.mapv_inplace(|x| x / count);
535        }
536
537        Ok(centroid)
538    }
539
540    /// Calculate various cluster metrics
541    fn calculate_cluster_metrics<F: Float + FromPrimitive + Debug>(
542        &self,
543        data: ArrayView2<F>,
544        point_indices: &[usize],
545        centroid: &Array1<f64>,
546    ) -> Result<(f64, f64, f64, (f64, f64, f64, f64, f64, f64))> {
547        let n_features = data.ncols();
548
549        let mut max_distance = 0.0;
550        let mut total_distance = 0.0;
551        let mut min_coords = vec![f64::INFINITY; n_features];
552        let mut max_coords = vec![f64::NEG_INFINITY; n_features];
553
554        // Calculate distances and bounding box
555        for &idx in point_indices {
556            let mut distance_to_centroid = 0.0;
557
558            for j in 0..n_features {
559                let coord = data[[idx, j]].to_f64().unwrap_or(0.0);
560                let diff = coord - centroid[j];
561                distance_to_centroid += diff * diff;
562
563                min_coords[j] = min_coords[j].min(coord);
564                max_coords[j] = max_coords[j].max(coord);
565            }
566
567            distance_to_centroid = distance_to_centroid.sqrt();
568            total_distance += distance_to_centroid;
569        }
570
571        // Calculate diameter (maximum pairwise distance)
572        for i in 0..point_indices.len() {
573            for j in (i + 1)..point_indices.len() {
574                let mut distance = 0.0;
575                for k in 0..n_features {
576                    let diff = data[[point_indices[i], k]].to_f64().unwrap_or(0.0)
577                        - data[[point_indices[j], k]].to_f64().unwrap_or(0.0);
578                    distance += diff * diff;
579                }
580                distance = distance.sqrt();
581                max_distance = max_distance.max(distance);
582            }
583        }
584
585        let avg_distance = if point_indices.is_empty() {
586            0.0
587        } else {
588            total_distance / point_indices.len() as f64
589        };
590
591        // Calculate density (points per unit volume)
592        let volume = if n_features >= 3 {
593            (max_coords[0] - min_coords[0])
594                * (max_coords[1] - min_coords[1])
595                * (max_coords[2] - min_coords[2])
596        } else if n_features >= 2 {
597            (max_coords[0] - min_coords[0]) * (max_coords[1] - min_coords[1])
598        } else {
599            max_coords[0] - min_coords[0]
600        };
601
602        let density = if volume > 0.0 {
603            point_indices.len() as f64 / volume
604        } else {
605            0.0
606        };
607
608        let bounding_box = (
609            min_coords.first().copied().unwrap_or(0.0),
610            max_coords.first().copied().unwrap_or(0.0),
611            min_coords.get(1).copied().unwrap_or(0.0),
612            max_coords.get(1).copied().unwrap_or(0.0),
613            min_coords.get(2).copied().unwrap_or(0.0),
614            max_coords.get(2).copied().unwrap_or(0.0),
615        );
616
617        Ok((max_distance, avg_distance, density, bounding_box))
618    }
619
620    /// Update view bounds based on data
621    fn update_view_bounds<F: Float + FromPrimitive + Debug>(&mut self, data: ArrayView2<F>) {
622        let n_features = data.ncols();
623
624        if n_features == 0 || data.nrows() == 0 {
625            return;
626        }
627
628        let mut min_vals = vec![f64::INFINITY; n_features];
629        let mut max_vals = vec![f64::NEG_INFINITY; n_features];
630
631        for i in 0..data.nrows() {
632            for j in 0..n_features {
633                let val = data[[i, j]].to_f64().unwrap_or(0.0);
634                min_vals[j] = min_vals[j].min(val);
635                max_vals[j] = max_vals[j].max(val);
636            }
637        }
638
639        // Add some padding
640        let padding = 0.1;
641        for j in 0..n_features {
642            let range = max_vals[j] - min_vals[j];
643            min_vals[j] -= range * padding;
644            max_vals[j] += range * padding;
645        }
646
647        self.state.view_bounds = (
648            min_vals.first().copied().unwrap_or(-10.0),
649            max_vals.first().copied().unwrap_or(10.0),
650            min_vals.get(1).copied().unwrap_or(-10.0),
651            max_vals.get(1).copied().unwrap_or(10.0),
652            min_vals.get(2).copied().unwrap_or(-10.0),
653            max_vals.get(2).copied().unwrap_or(10.0),
654        );
655    }
656
657    /// Validate point selections after data changes
658    fn validate_selections(&mut self, npoints: usize) {
659        self.state.selected_points.retain(|&idx| idx < npoints);
660        self.state.highlighted_points.retain(|&idx| idx < npoints);
661    }
662
663    /// Handle camera input based on mouse state
664    fn handle_camera_input(&mut self) {
665        let mouse_delta = (
666            self.state.input_state.mouse_position.0 - self.state.input_state.prev_mouse_position.0,
667            self.state.input_state.mouse_position.1 - self.state.input_state.prev_mouse_position.1,
668        );
669
670        let sensitivity = self.config.camera_sensitivity as f64;
671
672        // Rotation with left mouse button
673        if self
674            .state
675            .input_state
676            .mouse_buttons
677            .contains(&MouseButton::Left)
678        {
679            self.state.camera.rotation.0 += mouse_delta.1 * sensitivity * 0.01;
680            self.state.camera.rotation.1 += mouse_delta.0 * sensitivity * 0.01;
681        }
682
683        // Zoom with right mouse button or scroll
684        if self
685            .state
686            .input_state
687            .mouse_buttons
688            .contains(&MouseButton::Right)
689        {
690            self.state.camera.zoom *= 1.0 + mouse_delta.1 * sensitivity * 0.01;
691            self.state.camera.zoom = self.state.camera.zoom.clamp(0.1, 10.0);
692        }
693
694        // Pan with middle mouse button
695        if self
696            .state
697            .input_state
698            .mouse_buttons
699            .contains(&MouseButton::Middle)
700        {
701            // This would update camera position based on pan delta
702        }
703    }
704
705    /// Handle keyboard shortcuts
706    fn handle_keyboard_shortcuts(&mut self, key: KeyCode, pressed: bool) {
707        if !pressed {
708            return;
709        }
710
711        match key {
712            KeyCode::Space => {
713                self.state.animation_playing = !self.state.animation_playing;
714            }
715            KeyCode::Key('1') => self.set_view_mode(ViewMode::Perspective),
716            KeyCode::Key('2') => self.set_view_mode(ViewMode::Orthographic),
717            KeyCode::Key('3') => self.set_view_mode(ViewMode::BirdsEye),
718            KeyCode::Key('4') => self.set_view_mode(ViewMode::Side),
719            KeyCode::Key('5') => self.set_view_mode(ViewMode::Front),
720            KeyCode::Key('6') => self.set_view_mode(ViewMode::Top),
721            KeyCode::Escape => {
722                self.state.selected_points.clear();
723                self.state.highlighted_points.clear();
724            }
725            _ => {}
726        }
727    }
728
729    /// Update gesture recognition state
730    fn update_gesture_state(&mut self, prev_touch_count: usize, current_touchcount: usize) {
731        // Detect pinch gesture
732        if current_touchcount == 2 {
733            let touch1 = &self.state.input_state.touch_points[0];
734            let touch2 = &self.state.input_state.touch_points[1];
735
736            let distance = ((touch1.position.0 - touch2.position.0).powi(2)
737                + (touch1.position.1 - touch2.position.1).powi(2))
738            .sqrt();
739
740            if !self.state.input_state.gesture_state.is_pinching {
741                self.state.input_state.gesture_state.is_pinching = true;
742                self.state.input_state.gesture_state.pinch_scale = distance;
743            } else {
744                let scale_factor = distance / self.state.input_state.gesture_state.pinch_scale;
745                self.state.camera.zoom *= scale_factor;
746                self.state.input_state.gesture_state.pinch_scale = distance;
747            }
748        } else {
749            self.state.input_state.gesture_state.is_pinching = false;
750        }
751    }
752
753    /// Handle multi-touch gestures
754    fn handle_touch_gestures(&mut self) {
755        // Implementation would handle pinch-to-zoom, rotation, etc.
756    }
757}
758
759/// 3D bounding box for region selection
760#[derive(Debug, Clone)]
761pub struct BoundingBox3D {
762    pub min: (f64, f64, f64),
763    pub max: (f64, f64, f64),
764}
765
766/// Export format for interactive view state
767#[derive(Debug, Clone, Serialize, Deserialize)]
768struct InteractiveViewExport {
769    camera: CameraState,
770    view_mode: ViewMode,
771    cluster_stats: HashMap<i32, ClusterStats>,
772    view_bounds: (f64, f64, f64, f64, f64, f64),
773    current_time: f64,
774}
775
776#[cfg(test)]
777mod tests {
778    use super::*;
779    use scirs2_core::ndarray::Array2;
780
781    #[test]
782    fn test_interactive_visualizer_creation() {
783        let config = InteractiveConfig::default();
784        let visualizer = InteractiveVisualizer::new(config);
785
786        assert_eq!(visualizer.state.view_mode, ViewMode::Perspective);
787        assert!(visualizer.cluster_stats.is_empty());
788    }
789
790    #[test]
791    fn test_camera_controls() {
792        let config = InteractiveConfig::default();
793        let mut visualizer = InteractiveVisualizer::new(config);
794
795        visualizer.set_camera_position((5.0, 5.0, 5.0));
796        assert_eq!(visualizer.state.camera.position, (5.0, 5.0, 5.0));
797
798        visualizer.set_camera_target((1.0, 1.0, 1.0));
799        assert_eq!(visualizer.state.camera.target, (1.0, 1.0, 1.0));
800    }
801
802    #[test]
803    fn test_view_mode_switching() {
804        let config = InteractiveConfig::default();
805        let mut visualizer = InteractiveVisualizer::new(config);
806
807        visualizer.set_view_mode(ViewMode::BirdsEye);
808        assert_eq!(visualizer.state.view_mode, ViewMode::BirdsEye);
809        assert_eq!(visualizer.state.camera.position, (0.0, 20.0, 0.0));
810    }
811
812    #[test]
813    fn test_cluster_stats_calculation() {
814        let config = InteractiveConfig::default();
815        let mut visualizer = InteractiveVisualizer::new(config);
816
817        let data = Array2::from_shape_vec(
818            (4, 3),
819            vec![1.0, 2.0, 3.0, 1.1, 2.1, 3.1, 5.0, 6.0, 7.0, 5.1, 6.1, 7.1],
820        )
821        .unwrap();
822
823        let labels = Array1::from_vec(vec![0, 0, 1, 1]);
824
825        visualizer.update_data(data.view(), &labels, None).unwrap();
826
827        let stats = visualizer.get_cluster_stats();
828        assert_eq!(stats.len(), 2);
829        assert!(stats.contains_key(&0));
830        assert!(stats.contains_key(&1));
831
832        let cluster_0_stats = &stats[&0];
833        assert_eq!(cluster_0_stats.point_count, 2);
834    }
835}