Skip to main content

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    /// 3D positions of the currently loaded points (first up-to-3 feature
245    /// dimensions of each sample, zero-padded). Populated by `update_data` so the
246    /// spatial selection/picking routines operate on real coordinates.
247    point_positions: Vec<[f64; 3]>,
248}
249
250impl InteractiveVisualizer {
251    /// Create a new interactive visualizer
252    pub fn new(config: InteractiveConfig) -> Self {
253        Self {
254            config,
255            state: InteractiveState {
256                camera: CameraState::default(),
257                selected_points: Vec::new(),
258                highlighted_points: Vec::new(),
259                active_cluster: None,
260                input_state: InputState {
261                    mouse_position: (0.0, 0.0),
262                    prev_mouse_position: (0.0, 0.0),
263                    mouse_buttons: Vec::new(),
264                    keys_pressed: Vec::new(),
265                    touch_points: Vec::new(),
266                    gesture_state: GestureState {
267                        is_pinching: false,
268                        pinch_scale: 1.0,
269                        is_rotating: false,
270                        rotation_angle: 0.0,
271                        is_panning: false,
272                        pan_delta: (0.0, 0.0),
273                    },
274                },
275                view_bounds: (-10.0, 10.0, -10.0, 10.0, -10.0, 10.0),
276                current_time: 0.0,
277                animation_playing: false,
278                view_mode: ViewMode::Perspective,
279            },
280            cluster_stats: HashMap::new(),
281            last_update: std::time::Instant::now(),
282            point_positions: Vec::new(),
283        }
284    }
285
286    /// Update visualization with new data
287    pub fn update_data<F: Float + FromPrimitive + Debug>(
288        &mut self,
289        data: ArrayView2<F>,
290        labels: &Array1<i32>,
291        centroids: Option<&Array2<F>>,
292    ) -> Result<()> {
293        // Calculate cluster statistics
294        self.calculate_cluster_stats(data, labels, centroids)?;
295
296        // Store real 3D positions (first up-to-three feature dimensions, zero-padded)
297        // so spatial selection and picking can run against actual coordinates.
298        self.point_positions = data
299            .rows()
300            .into_iter()
301            .map(|row| {
302                let mut pos = [0.0_f64; 3];
303                for (axis, slot) in pos.iter_mut().enumerate() {
304                    if let Some(v) = row.get(axis) {
305                        *slot = v.to_f64().unwrap_or(0.0);
306                    }
307                }
308                pos
309            })
310            .collect();
311
312        // Update view bounds if needed
313        self.update_view_bounds(data);
314
315        // Reset selection if data changed significantly
316        self.validate_selections(data.nrows());
317
318        Ok(())
319    }
320
321    /// Handle mouse input
322    pub fn handle_mouse_input(&mut self, button: MouseButton, position: (f64, f64), pressed: bool) {
323        self.state.input_state.prev_mouse_position = self.state.input_state.mouse_position;
324        self.state.input_state.mouse_position = position;
325
326        if pressed {
327            if !self.state.input_state.mouse_buttons.contains(&button) {
328                self.state.input_state.mouse_buttons.push(button);
329            }
330        } else {
331            self.state
332                .input_state
333                .mouse_buttons
334                .retain(|&b| b != button);
335        }
336
337        // Handle camera controls
338        if self.config.enable_camera_controls {
339            self.handle_camera_input();
340        }
341    }
342
343    /// Handle keyboard input
344    pub fn handle_keyboard_input(&mut self, key: KeyCode, pressed: bool) {
345        if pressed {
346            if !self.state.input_state.keys_pressed.contains(&key) {
347                self.state.input_state.keys_pressed.push(key);
348            }
349        } else {
350            self.state.input_state.keys_pressed.retain(|&k| k != key);
351        }
352
353        // Handle special key combinations
354        self.handle_keyboard_shortcuts(key, pressed);
355    }
356
357    /// Handle touch input for mobile/tablet interfaces
358    pub fn handle_touch_input(&mut self, touchpoints: Vec<TouchPoint>) {
359        let prev_touch_count = self.state.input_state.touch_points.len();
360        self.state.input_state.touch_points = touchpoints;
361        let current_touch_count = self.state.input_state.touch_points.len();
362
363        // Gesture recognition
364        self.update_gesture_state(prev_touch_count, current_touch_count);
365
366        // Handle multi-touch gestures
367        if self.config.enable_camera_controls {
368            self.handle_touch_gestures();
369        }
370    }
371
372    /// Select all loaded points whose 3D position lies inside `region`.
373    ///
374    /// Performs real axis-aligned point-in-box testing against the coordinates
375    /// captured by [`Self::update_data`]. Returns (and stores) the indices of the
376    /// contained points; an empty result now genuinely means "no point is inside
377    /// the box" rather than "unimplemented".
378    pub fn select_points_in_region(&mut self, region: BoundingBox3D) -> Vec<usize> {
379        let (min_x, min_y, min_z) = region.min;
380        let (max_x, max_y, max_z) = region.max;
381        // Normalise the box so callers may pass corners in any order.
382        let (lo_x, hi_x) = (min_x.min(max_x), min_x.max(max_x));
383        let (lo_y, hi_y) = (min_y.min(max_y), min_y.max(max_y));
384        let (lo_z, hi_z) = (min_z.min(max_z), min_z.max(max_z));
385
386        let selected: Vec<usize> = self
387            .point_positions
388            .iter()
389            .enumerate()
390            .filter(|(_, p)| {
391                p[0] >= lo_x
392                    && p[0] <= hi_x
393                    && p[1] >= lo_y
394                    && p[1] <= hi_y
395                    && p[2] >= lo_z
396                    && p[2] <= hi_z
397            })
398            .map(|(i, _)| i)
399            .collect();
400
401        self.state.selected_points = selected.clone();
402        selected
403    }
404
405    /// Highlight the point nearest to the given screen-space position.
406    ///
407    /// Picking is performed against the orthographic (x, y) projection of the
408    /// stored point positions: the closest point within the configured pick radius
409    /// is highlighted. This is a real nearest-point query rather than a stub that
410    /// always returned nothing.
411    pub fn highlight_points_at(&mut self, screenpos: (f64, f64)) -> Vec<usize> {
412        let (sx, sy) = screenpos;
413        // Pick tolerance in projected coordinate units. Points farther than this
414        // from the cursor are not considered hits.
415        const PICK_RADIUS: f64 = 0.5;
416        let pick_radius_sq = PICK_RADIUS * PICK_RADIUS;
417
418        let mut best: Option<(usize, f64)> = None;
419        for (i, p) in self.point_positions.iter().enumerate() {
420            let dx = p[0] - sx;
421            let dy = p[1] - sy;
422            let dist_sq = dx * dx + dy * dy;
423            if dist_sq <= pick_radius_sq && best.map(|(_, b)| dist_sq < b).unwrap_or(true) {
424                best = Some((i, dist_sq));
425            }
426        }
427
428        let highlighted: Vec<usize> = best.map(|(i, _)| vec![i]).unwrap_or_default();
429        self.state.highlighted_points = highlighted.clone();
430        highlighted
431    }
432
433    /// Get current cluster statistics
434    pub fn get_cluster_stats(&self) -> &HashMap<i32, ClusterStats> {
435        &self.cluster_stats
436    }
437
438    /// Get current interactive state
439    pub fn get_state(&self) -> &InteractiveState {
440        &self.state
441    }
442
443    /// Set camera position
444    pub fn set_camera_position(&mut self, position: (f64, f64, f64)) {
445        self.state.camera.position = position;
446    }
447
448    /// Set camera target
449    pub fn set_camera_target(&mut self, target: (f64, f64, f64)) {
450        self.state.camera.target = target;
451    }
452
453    /// Set view mode
454    pub fn set_view_mode(&mut self, mode: ViewMode) {
455        self.state.view_mode = mode;
456
457        // Adjust camera for specific view modes
458        match mode {
459            ViewMode::BirdsEye => {
460                self.state.camera.position = (0.0, 20.0, 0.0);
461                self.state.camera.target = (0.0, 0.0, 0.0);
462                self.state.camera.up = (0.0, 0.0, -1.0);
463            }
464            ViewMode::Side => {
465                self.state.camera.position = (20.0, 0.0, 0.0);
466                self.state.camera.target = (0.0, 0.0, 0.0);
467                self.state.camera.up = (0.0, 1.0, 0.0);
468            }
469            ViewMode::Front => {
470                self.state.camera.position = (0.0, 0.0, 20.0);
471                self.state.camera.target = (0.0, 0.0, 0.0);
472                self.state.camera.up = (0.0, 1.0, 0.0);
473            }
474            ViewMode::Top => {
475                self.state.camera.position = (0.0, 20.0, 0.0);
476                self.state.camera.target = (0.0, 0.0, 0.0);
477                self.state.camera.up = (0.0, 0.0, -1.0);
478            }
479            _ => {
480                // Keep current camera settings for other modes
481            }
482        }
483    }
484
485    /// Enable/disable animation playback
486    pub fn set_animation_playing(&mut self, playing: bool) {
487        self.state.animation_playing = playing;
488    }
489
490    /// Set current time for temporal views
491    pub fn set_current_time(&mut self, time: f64) {
492        self.state.current_time = time;
493    }
494
495    /// Generate export data for current view
496    pub fn export_view_state(&self) -> Result<String> {
497        #[cfg(feature = "serde")]
498        {
499            let export_data = InteractiveViewExport {
500                camera: self.state.camera.clone(),
501                view_mode: self.state.view_mode,
502                cluster_stats: self.cluster_stats.clone(),
503                view_bounds: self.state.view_bounds,
504                current_time: self.state.current_time,
505            };
506
507            return serde_json::to_string_pretty(&export_data)
508                .map_err(|e| ClusteringError::ComputationError(format!("Export failed: {}", e)));
509        }
510
511        #[cfg(not(feature = "serde"))]
512        {
513            Err(ClusteringError::ComputationError(
514                "Export requires 'serde' feature".to_string(),
515            ))
516        }
517    }
518
519    /// Calculate cluster statistics
520    fn calculate_cluster_stats<F: Float + FromPrimitive + Debug>(
521        &mut self,
522        data: ArrayView2<F>,
523        labels: &Array1<i32>,
524        centroids: Option<&Array2<F>>,
525    ) -> Result<()> {
526        self.cluster_stats.clear();
527
528        // Get unique cluster labels
529        let mut unique_labels: Vec<i32> = labels.iter().cloned().collect();
530        unique_labels.sort_unstable();
531        unique_labels.dedup();
532
533        for &cluster_id in &unique_labels {
534            // Find points in this cluster
535            let cluster_points: Vec<usize> = labels
536                .iter()
537                .enumerate()
538                .filter(|(_, &label)| label == cluster_id)
539                .map(|(idx_, _)| idx_)
540                .collect();
541
542            if cluster_points.is_empty() {
543                continue;
544            }
545
546            // Calculate centroid
547            let centroid = if let Some(cents) = centroids {
548                if cluster_id >= 0 && (cluster_id as usize) < cents.nrows() {
549                    cents
550                        .row(cluster_id as usize)
551                        .mapv(|x| x.to_f64().unwrap_or(0.0))
552                } else {
553                    // Calculate centroid from points
554                    self.calculate_centroid_from_points(data, &cluster_points)?
555                }
556            } else {
557                self.calculate_centroid_from_points(data, &cluster_points)?
558            };
559
560            // Calculate statistics
561            let (diameter, avg_distance, density, bounding_box) =
562                self.calculate_cluster_metrics(data, &cluster_points, &centroid)?;
563
564            let stats = ClusterStats {
565                cluster_id,
566                point_count: cluster_points.len(),
567                centroid,
568                diameter,
569                avg_distance_to_centroid: avg_distance,
570                density,
571                bounding_box,
572                color: format!("#{:06x}", (cluster_id.unsigned_abs() * 123456) % 0xFFFFFF),
573            };
574
575            self.cluster_stats.insert(cluster_id, stats);
576        }
577
578        Ok(())
579    }
580
581    /// Calculate centroid from points
582    fn calculate_centroid_from_points<F: Float + FromPrimitive + Debug>(
583        &self,
584        data: ArrayView2<F>,
585        point_indices: &[usize],
586    ) -> Result<Array1<f64>> {
587        let n_features = data.ncols();
588        let mut centroid = Array1::zeros(n_features);
589
590        for &idx in point_indices {
591            for j in 0..n_features {
592                centroid[j] += data[[idx, j]].to_f64().unwrap_or(0.0);
593            }
594        }
595
596        let count = point_indices.len() as f64;
597        if count > 0.0 {
598            centroid.mapv_inplace(|x| x / count);
599        }
600
601        Ok(centroid)
602    }
603
604    /// Calculate various cluster metrics
605    fn calculate_cluster_metrics<F: Float + FromPrimitive + Debug>(
606        &self,
607        data: ArrayView2<F>,
608        point_indices: &[usize],
609        centroid: &Array1<f64>,
610    ) -> Result<(f64, f64, f64, (f64, f64, f64, f64, f64, f64))> {
611        let n_features = data.ncols();
612
613        let mut max_distance = 0.0;
614        let mut total_distance = 0.0;
615        let mut min_coords = vec![f64::INFINITY; n_features];
616        let mut max_coords = vec![f64::NEG_INFINITY; n_features];
617
618        // Calculate distances and bounding box
619        for &idx in point_indices {
620            let mut distance_to_centroid = 0.0;
621
622            for j in 0..n_features {
623                let coord = data[[idx, j]].to_f64().unwrap_or(0.0);
624                let diff = coord - centroid[j];
625                distance_to_centroid += diff * diff;
626
627                min_coords[j] = min_coords[j].min(coord);
628                max_coords[j] = max_coords[j].max(coord);
629            }
630
631            distance_to_centroid = distance_to_centroid.sqrt();
632            total_distance += distance_to_centroid;
633        }
634
635        // Calculate diameter (maximum pairwise distance)
636        for i in 0..point_indices.len() {
637            for j in (i + 1)..point_indices.len() {
638                let mut distance = 0.0;
639                for k in 0..n_features {
640                    let diff = data[[point_indices[i], k]].to_f64().unwrap_or(0.0)
641                        - data[[point_indices[j], k]].to_f64().unwrap_or(0.0);
642                    distance += diff * diff;
643                }
644                distance = distance.sqrt();
645                max_distance = max_distance.max(distance);
646            }
647        }
648
649        let avg_distance = if point_indices.is_empty() {
650            0.0
651        } else {
652            total_distance / point_indices.len() as f64
653        };
654
655        // Calculate density (points per unit volume)
656        let volume = if n_features >= 3 {
657            (max_coords[0] - min_coords[0])
658                * (max_coords[1] - min_coords[1])
659                * (max_coords[2] - min_coords[2])
660        } else if n_features >= 2 {
661            (max_coords[0] - min_coords[0]) * (max_coords[1] - min_coords[1])
662        } else {
663            max_coords[0] - min_coords[0]
664        };
665
666        let density = if volume > 0.0 {
667            point_indices.len() as f64 / volume
668        } else {
669            0.0
670        };
671
672        let bounding_box = (
673            min_coords.first().copied().unwrap_or(0.0),
674            max_coords.first().copied().unwrap_or(0.0),
675            min_coords.get(1).copied().unwrap_or(0.0),
676            max_coords.get(1).copied().unwrap_or(0.0),
677            min_coords.get(2).copied().unwrap_or(0.0),
678            max_coords.get(2).copied().unwrap_or(0.0),
679        );
680
681        Ok((max_distance, avg_distance, density, bounding_box))
682    }
683
684    /// Update view bounds based on data
685    fn update_view_bounds<F: Float + FromPrimitive + Debug>(&mut self, data: ArrayView2<F>) {
686        let n_features = data.ncols();
687
688        if n_features == 0 || data.nrows() == 0 {
689            return;
690        }
691
692        let mut min_vals = vec![f64::INFINITY; n_features];
693        let mut max_vals = vec![f64::NEG_INFINITY; n_features];
694
695        for i in 0..data.nrows() {
696            for j in 0..n_features {
697                let val = data[[i, j]].to_f64().unwrap_or(0.0);
698                min_vals[j] = min_vals[j].min(val);
699                max_vals[j] = max_vals[j].max(val);
700            }
701        }
702
703        // Add some padding
704        let padding = 0.1;
705        for j in 0..n_features {
706            let range = max_vals[j] - min_vals[j];
707            min_vals[j] -= range * padding;
708            max_vals[j] += range * padding;
709        }
710
711        self.state.view_bounds = (
712            min_vals.first().copied().unwrap_or(-10.0),
713            max_vals.first().copied().unwrap_or(10.0),
714            min_vals.get(1).copied().unwrap_or(-10.0),
715            max_vals.get(1).copied().unwrap_or(10.0),
716            min_vals.get(2).copied().unwrap_or(-10.0),
717            max_vals.get(2).copied().unwrap_or(10.0),
718        );
719    }
720
721    /// Validate point selections after data changes
722    fn validate_selections(&mut self, npoints: usize) {
723        self.state.selected_points.retain(|&idx| idx < npoints);
724        self.state.highlighted_points.retain(|&idx| idx < npoints);
725    }
726
727    /// Handle camera input based on mouse state
728    fn handle_camera_input(&mut self) {
729        let mouse_delta = (
730            self.state.input_state.mouse_position.0 - self.state.input_state.prev_mouse_position.0,
731            self.state.input_state.mouse_position.1 - self.state.input_state.prev_mouse_position.1,
732        );
733
734        let sensitivity = self.config.camera_sensitivity as f64;
735
736        // Rotation with left mouse button
737        if self
738            .state
739            .input_state
740            .mouse_buttons
741            .contains(&MouseButton::Left)
742        {
743            self.state.camera.rotation.0 += mouse_delta.1 * sensitivity * 0.01;
744            self.state.camera.rotation.1 += mouse_delta.0 * sensitivity * 0.01;
745        }
746
747        // Zoom with right mouse button or scroll
748        if self
749            .state
750            .input_state
751            .mouse_buttons
752            .contains(&MouseButton::Right)
753        {
754            self.state.camera.zoom *= 1.0 + mouse_delta.1 * sensitivity * 0.01;
755            self.state.camera.zoom = self.state.camera.zoom.clamp(0.1, 10.0);
756        }
757
758        // Pan with middle mouse button
759        if self
760            .state
761            .input_state
762            .mouse_buttons
763            .contains(&MouseButton::Middle)
764        {
765            // This would update camera position based on pan delta
766        }
767    }
768
769    /// Handle keyboard shortcuts
770    fn handle_keyboard_shortcuts(&mut self, key: KeyCode, pressed: bool) {
771        if !pressed {
772            return;
773        }
774
775        match key {
776            KeyCode::Space => {
777                self.state.animation_playing = !self.state.animation_playing;
778            }
779            KeyCode::Key('1') => self.set_view_mode(ViewMode::Perspective),
780            KeyCode::Key('2') => self.set_view_mode(ViewMode::Orthographic),
781            KeyCode::Key('3') => self.set_view_mode(ViewMode::BirdsEye),
782            KeyCode::Key('4') => self.set_view_mode(ViewMode::Side),
783            KeyCode::Key('5') => self.set_view_mode(ViewMode::Front),
784            KeyCode::Key('6') => self.set_view_mode(ViewMode::Top),
785            KeyCode::Escape => {
786                self.state.selected_points.clear();
787                self.state.highlighted_points.clear();
788            }
789            _ => {}
790        }
791    }
792
793    /// Update gesture recognition state
794    fn update_gesture_state(&mut self, prev_touch_count: usize, current_touchcount: usize) {
795        // Detect pinch gesture
796        if current_touchcount == 2 {
797            let touch1 = &self.state.input_state.touch_points[0];
798            let touch2 = &self.state.input_state.touch_points[1];
799
800            let distance = ((touch1.position.0 - touch2.position.0).powi(2)
801                + (touch1.position.1 - touch2.position.1).powi(2))
802            .sqrt();
803
804            if !self.state.input_state.gesture_state.is_pinching {
805                self.state.input_state.gesture_state.is_pinching = true;
806                self.state.input_state.gesture_state.pinch_scale = distance;
807            } else {
808                let scale_factor = distance / self.state.input_state.gesture_state.pinch_scale;
809                self.state.camera.zoom *= scale_factor;
810                self.state.input_state.gesture_state.pinch_scale = distance;
811            }
812        } else {
813            self.state.input_state.gesture_state.is_pinching = false;
814        }
815    }
816
817    /// Handle multi-touch gestures
818    fn handle_touch_gestures(&mut self) {
819        // Implementation would handle pinch-to-zoom, rotation, etc.
820    }
821}
822
823/// 3D bounding box for region selection
824#[derive(Debug, Clone)]
825pub struct BoundingBox3D {
826    pub min: (f64, f64, f64),
827    pub max: (f64, f64, f64),
828}
829
830/// Export format for interactive view state
831#[derive(Debug, Clone, Serialize, Deserialize)]
832struct InteractiveViewExport {
833    camera: CameraState,
834    view_mode: ViewMode,
835    cluster_stats: HashMap<i32, ClusterStats>,
836    view_bounds: (f64, f64, f64, f64, f64, f64),
837    current_time: f64,
838}
839
840#[cfg(test)]
841mod tests {
842    use super::*;
843    use scirs2_core::ndarray::Array2;
844
845    #[test]
846    fn test_interactive_visualizer_creation() {
847        let config = InteractiveConfig::default();
848        let visualizer = InteractiveVisualizer::new(config);
849
850        assert_eq!(visualizer.state.view_mode, ViewMode::Perspective);
851        assert!(visualizer.cluster_stats.is_empty());
852    }
853
854    #[test]
855    fn test_camera_controls() {
856        let config = InteractiveConfig::default();
857        let mut visualizer = InteractiveVisualizer::new(config);
858
859        visualizer.set_camera_position((5.0, 5.0, 5.0));
860        assert_eq!(visualizer.state.camera.position, (5.0, 5.0, 5.0));
861
862        visualizer.set_camera_target((1.0, 1.0, 1.0));
863        assert_eq!(visualizer.state.camera.target, (1.0, 1.0, 1.0));
864    }
865
866    #[test]
867    fn test_view_mode_switching() {
868        let config = InteractiveConfig::default();
869        let mut visualizer = InteractiveVisualizer::new(config);
870
871        visualizer.set_view_mode(ViewMode::BirdsEye);
872        assert_eq!(visualizer.state.view_mode, ViewMode::BirdsEye);
873        assert_eq!(visualizer.state.camera.position, (0.0, 20.0, 0.0));
874    }
875
876    #[test]
877    fn test_cluster_stats_calculation() {
878        let config = InteractiveConfig::default();
879        let mut visualizer = InteractiveVisualizer::new(config);
880
881        let data = Array2::from_shape_vec(
882            (4, 3),
883            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],
884        )
885        .expect("Operation failed");
886
887        let labels = Array1::from_vec(vec![0, 0, 1, 1]);
888
889        visualizer
890            .update_data(data.view(), &labels, None)
891            .expect("Operation failed");
892
893        let stats = visualizer.get_cluster_stats();
894        assert_eq!(stats.len(), 2);
895        assert!(stats.contains_key(&0));
896        assert!(stats.contains_key(&1));
897
898        let cluster_0_stats = &stats[&0];
899        assert_eq!(cluster_0_stats.point_count, 2);
900    }
901}