Skip to main content

volren_core/interaction/
image_slice.rs

1//! Image-slice interaction style for 2D MPR viewing.
2//!
3//! Controls:
4//! - **Left drag horizontal** → adjust window width
5//! - **Left drag vertical**   → adjust window center (level)
6//! - **Middle drag**          → pan
7//! - **Right drag / Scroll**  → scroll through slices (Z translation)
8//!
9//! # VTK Equivalent
10//! `vtkInteractorStyleImage`
11
12use super::{
13    events::{InteractionContext, InteractionResult, MouseEventKind},
14    InteractionStyle, KeyEvent, MouseButton, MouseEvent,
15};
16use crate::{camera::Camera, window_level::WindowLevel};
17
18/// Drag action in progress.
19#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
20enum DragState {
21    #[default]
22    None,
23    WindowLevel,
24    Panning,
25    Slicing,
26}
27
28/// Image-slice interaction style.
29///
30/// Maintains an internal [`WindowLevel`] that the consumer can read after each
31/// event via [`ImageSliceStyle::window_level`].
32///
33/// Slice scrolling is communicated through a `slice_delta` value that the
34/// consumer should read and apply to its [`crate::reslice::SlicePlane`].
35#[derive(Debug)]
36pub struct ImageSliceStyle {
37    drag: DragState,
38    last_pos: (f64, f64),
39    window_level: WindowLevel,
40    slice_delta: f64,
41    /// Pixels per unit of window width change (default 4.0).
42    pub window_sensitivity: f64,
43    /// Pixels per unit of window center change (default 2.0).
44    pub level_sensitivity: f64,
45    /// World units per scroll tick for slicing (default 1.0).
46    pub slice_scroll_step: f64,
47}
48
49impl ImageSliceStyle {
50    /// Create with default sensitivities and the given initial window/level.
51    #[must_use]
52    pub fn new(window_level: WindowLevel) -> Self {
53        Self {
54            drag: DragState::None,
55            last_pos: (0.0, 0.0),
56            window_level,
57            slice_delta: 0.0,
58            window_sensitivity: 4.0,
59            level_sensitivity: 2.0,
60            slice_scroll_step: 1.0,
61        }
62    }
63
64    /// The current window/level (updated by drag events).
65    #[must_use]
66    pub fn window_level(&self) -> WindowLevel {
67        self.window_level
68    }
69
70    /// Consume and return the accumulated slice translation since the last call.
71    ///
72    /// The consumer should apply this to its `SlicePlane::offset_along_normal`.
73    pub fn take_slice_delta(&mut self) -> f64 {
74        let d = self.slice_delta;
75        self.slice_delta = 0.0;
76        d
77    }
78}
79
80impl InteractionStyle for ImageSliceStyle {
81    fn on_mouse_event(
82        &mut self,
83        event: &MouseEvent,
84        _ctx: &InteractionContext,
85        camera: &mut Camera,
86    ) -> InteractionResult {
87        match event.kind {
88            MouseEventKind::Press(button) => {
89                self.last_pos = event.position;
90                self.drag = match button {
91                    MouseButton::Left => DragState::WindowLevel,
92                    MouseButton::Middle => DragState::Panning,
93                    MouseButton::Right => DragState::Slicing,
94                };
95                InteractionResult::nothing()
96            }
97
98            MouseEventKind::Release(_) => {
99                self.drag = DragState::None;
100                InteractionResult::nothing()
101            }
102
103            MouseEventKind::Move => {
104                let dx = event.position.0 - self.last_pos.0;
105                let dy = event.position.1 - self.last_pos.1;
106                self.last_pos = event.position;
107
108                if dx == 0.0 && dy == 0.0 {
109                    return InteractionResult::nothing();
110                }
111
112                match self.drag {
113                    DragState::None => InteractionResult::nothing(),
114
115                    DragState::WindowLevel => {
116                        self.window_level.width =
117                            (self.window_level.width + dx * self.window_sensitivity).max(1.0);
118                        self.window_level.center += dy * self.level_sensitivity;
119                        InteractionResult::window_level_only()
120                    }
121
122                    DragState::Panning => {
123                        let scale = camera.distance() * 0.001;
124                        let right = camera.right();
125                        let up = camera.view_up_ortho();
126                        camera.pan(-right * dx * scale + up * dy * scale);
127                        InteractionResult::camera_only()
128                    }
129
130                    DragState::Slicing => {
131                        self.slice_delta += dy * self.slice_scroll_step * 0.1;
132                        InteractionResult::slice_only()
133                    }
134                }
135            }
136
137            MouseEventKind::Scroll(delta) => {
138                self.slice_delta += delta * self.slice_scroll_step;
139                InteractionResult::slice_only()
140            }
141        }
142    }
143
144    fn on_key_event(
145        &mut self,
146        _event: &KeyEvent,
147        _ctx: &InteractionContext,
148        _camera: &mut Camera,
149    ) -> InteractionResult {
150        InteractionResult::nothing()
151    }
152}
153
154// ── Tests ─────────────────────────────────────────────────────────────────────
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159    use crate::interaction::Modifiers;
160    use crate::window_level::presets;
161    use glam::DVec3;
162
163    fn default_camera() -> Camera {
164        Camera::new(DVec3::new(0.0, 0.0, 10.0), DVec3::ZERO, DVec3::Y)
165    }
166
167    fn ctx() -> InteractionContext {
168        InteractionContext {
169            viewport_width: 800.0,
170            viewport_height: 600.0,
171            volume_bounds: None,
172        }
173    }
174
175    fn mouse(pos: (f64, f64), kind: MouseEventKind) -> MouseEvent {
176        MouseEvent {
177            position: pos,
178            kind,
179            modifiers: Modifiers::default(),
180        }
181    }
182
183    #[test]
184    fn left_drag_horizontal_changes_window_width() {
185        let mut style = ImageSliceStyle::new(presets::SOFT_TISSUE);
186        let mut cam = default_camera();
187        let w0 = style.window_level().width;
188
189        style.on_mouse_event(
190            &mouse((0.0, 0.0), MouseEventKind::Press(MouseButton::Left)),
191            &ctx(),
192            &mut cam,
193        );
194        let r = style.on_mouse_event(&mouse((50.0, 0.0), MouseEventKind::Move), &ctx(), &mut cam);
195
196        assert!(r.window_level_changed);
197        assert!(style.window_level().width > w0, "width should increase");
198    }
199
200    #[test]
201    fn left_drag_vertical_changes_center() {
202        let mut style = ImageSliceStyle::new(presets::SOFT_TISSUE);
203        let mut cam = default_camera();
204        let c0 = style.window_level().center;
205
206        style.on_mouse_event(
207            &mouse((0.0, 0.0), MouseEventKind::Press(MouseButton::Left)),
208            &ctx(),
209            &mut cam,
210        );
211        style.on_mouse_event(&mouse((0.0, 30.0), MouseEventKind::Move), &ctx(), &mut cam);
212
213        assert_ne!(style.window_level().center, c0, "center should change");
214    }
215
216    #[test]
217    fn scroll_accumulates_slice_delta() {
218        let mut style = ImageSliceStyle::new(presets::SOFT_TISSUE);
219        let mut cam = default_camera();
220
221        style.on_mouse_event(
222            &mouse((400.0, 300.0), MouseEventKind::Scroll(3.0)),
223            &ctx(),
224            &mut cam,
225        );
226        let d = style.take_slice_delta();
227        assert!(d > 0.0, "scroll up should give positive delta");
228        assert_eq!(style.take_slice_delta(), 0.0, "delta consumed");
229    }
230}