1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct InteractiveConfig {
20 pub enable_camera_controls: bool,
22 pub enable_point_selection: bool,
24 pub enable_cluster_manipulation: bool,
26 pub show_axes: bool,
28 pub show_grid: bool,
30 pub show_realtime_stats: bool,
32 pub multi_view: bool,
34 pub view_count: usize,
36 pub enable_vr_mode: bool,
38 pub stereoscopic: bool,
40 pub field_of_view: f32,
42 pub camera_sensitivity: f32,
44 pub highlight_on_hover: bool,
46 pub show_3d_boundaries: bool,
48 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#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct CameraState {
77 pub position: (f64, f64, f64),
79 pub target: (f64, f64, f64),
81 pub up: (f64, f64, f64),
83 pub fov: f32,
85 pub near: f64,
87 pub far: f64,
89 pub rotation: (f64, f64, f64),
91 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#[derive(Debug, Clone)]
112pub struct InteractiveState {
113 pub camera: CameraState,
115 pub selected_points: Vec<usize>,
117 pub highlighted_points: Vec<usize>,
119 pub active_cluster: Option<i32>,
121 pub input_state: InputState,
123 pub view_bounds: (f64, f64, f64, f64, f64, f64),
125 pub current_time: f64,
127 pub animation_playing: bool,
129 pub view_mode: ViewMode,
131}
132
133#[derive(Debug, Clone)]
135pub struct InputState {
136 pub mouse_position: (f64, f64),
138 pub prev_mouse_position: (f64, f64),
140 pub mouse_buttons: Vec<MouseButton>,
142 pub keys_pressed: Vec<KeyCode>,
144 pub touch_points: Vec<TouchPoint>,
146 pub gesture_state: GestureState,
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub enum MouseButton {
153 Left,
154 Right,
155 Middle,
156 Other(u8),
157}
158
159#[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#[derive(Debug, Clone)]
177pub struct TouchPoint {
178 pub id: u64,
179 pub position: (f64, f64),
180 pub pressure: f64,
181}
182
183#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
196pub enum ViewMode {
197 Perspective,
199 Orthographic,
201 FirstPerson,
203 BirdsEye,
205 Side,
207 Front,
209 Top,
211 SplitScreen,
213 VRStereo,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct ClusterStats {
220 pub cluster_id: i32,
222 pub point_count: usize,
224 pub centroid: Array1<f64>,
226 pub diameter: f64,
228 pub avg_distance_to_centroid: f64,
230 pub density: f64,
232 pub bounding_box: (f64, f64, f64, f64, f64, f64),
234 pub color: String,
236}
237
238pub 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 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 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 self.calculate_cluster_stats(data, labels, centroids)?;
290
291 self.update_view_bounds(data);
293
294 self.validate_selections(data.nrows());
296
297 Ok(())
298 }
299
300 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 if self.config.enable_camera_controls {
318 self.handle_camera_input();
319 }
320 }
321
322 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 self.handle_keyboard_shortcuts(key, pressed);
334 }
335
336 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 self.update_gesture_state(prev_touch_count, current_touch_count);
344
345 if self.config.enable_camera_controls {
347 self.handle_touch_gestures();
348 }
349 }
350
351 pub fn select_points_in_region(&mut self, region: BoundingBox3D) -> Vec<usize> {
353 let selected = Vec::new();
356 self.state.selected_points = selected.clone();
357 selected
358 }
359
360 pub fn highlight_points_at(&mut self, screenpos: (f64, f64)) -> Vec<usize> {
362 let highlighted = Vec::new();
365 self.state.highlighted_points = highlighted.clone();
366 highlighted
367 }
368
369 pub fn get_cluster_stats(&self) -> &HashMap<i32, ClusterStats> {
371 &self.cluster_stats
372 }
373
374 pub fn get_state(&self) -> &InteractiveState {
376 &self.state
377 }
378
379 pub fn set_camera_position(&mut self, position: (f64, f64, f64)) {
381 self.state.camera.position = position;
382 }
383
384 pub fn set_camera_target(&mut self, target: (f64, f64, f64)) {
386 self.state.camera.target = target;
387 }
388
389 pub fn set_view_mode(&mut self, mode: ViewMode) {
391 self.state.view_mode = mode;
392
393 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 }
418 }
419 }
420
421 pub fn set_animation_playing(&mut self, playing: bool) {
423 self.state.animation_playing = playing;
424 }
425
426 pub fn set_current_time(&mut self, time: f64) {
428 self.state.current_time = time;
429 }
430
431 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 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 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 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 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 self.calculate_centroid_from_points(data, &cluster_points)?
491 }
492 } else {
493 self.calculate_centroid_from_points(data, &cluster_points)?
494 };
495
496 let (diameter, avg_distance, density, bounding_box) =
498 self.calculate_cluster_metrics(data, &cluster_points, ¢roid)?;
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 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 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 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 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 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 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 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 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 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 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 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 if self
696 .state
697 .input_state
698 .mouse_buttons
699 .contains(&MouseButton::Middle)
700 {
701 }
703 }
704
705 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 fn update_gesture_state(&mut self, prev_touch_count: usize, current_touchcount: usize) {
731 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 fn handle_touch_gestures(&mut self) {
755 }
757}
758
759#[derive(Debug, Clone)]
761pub struct BoundingBox3D {
762 pub min: (f64, f64, f64),
763 pub max: (f64, f64, f64),
764}
765
766#[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}