Skip to main content

ringkernel_wavesim3d/visualization/
slice.rs

1//! Slice rendering for 3D volume visualization.
2//!
3//! Renders 2D slices through the 3D pressure field.
4
5use super::{ColorMap, Vertex3D};
6use crate::simulation::SimulationGrid3D;
7
8/// Which axis the slice is perpendicular to.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum SliceAxis {
11    /// XY plane (perpendicular to Z)
12    #[default]
13    XY,
14    /// XZ plane (perpendicular to Y)
15    XZ,
16    /// YZ plane (perpendicular to X)
17    YZ,
18}
19
20/// Configuration for a slice through the volume.
21#[derive(Debug, Clone)]
22pub struct SliceConfig {
23    /// Which axis the slice is perpendicular to
24    pub axis: SliceAxis,
25    /// Position along the axis (0.0 to 1.0)
26    pub position: f32,
27    /// Opacity of the slice
28    pub opacity: f32,
29    /// Color map for rendering
30    pub color_map: ColorMap,
31    /// Whether to show grid lines on the slice
32    pub show_grid: bool,
33}
34
35impl Default for SliceConfig {
36    fn default() -> Self {
37        Self {
38            axis: SliceAxis::XY,
39            position: 0.5,
40            opacity: 0.8,
41            color_map: ColorMap::BlueWhiteRed,
42            show_grid: false,
43        }
44    }
45}
46
47impl SliceConfig {
48    pub fn new(axis: SliceAxis, position: f32) -> Self {
49        Self {
50            axis,
51            position: position.clamp(0.0, 1.0),
52            ..Default::default()
53        }
54    }
55}
56
57/// Renderer for 2D slices through 3D data.
58pub struct SliceRenderer {
59    /// Active slices
60    pub slices: Vec<SliceConfig>,
61    /// Maximum pressure value for normalization
62    max_pressure: f32,
63}
64
65impl Default for SliceRenderer {
66    fn default() -> Self {
67        Self::new()
68    }
69}
70
71impl SliceRenderer {
72    pub fn new() -> Self {
73        Self {
74            slices: vec![SliceConfig::default()],
75            max_pressure: 1.0,
76        }
77    }
78
79    /// Add a slice configuration.
80    pub fn add_slice(&mut self, config: SliceConfig) {
81        self.slices.push(config);
82    }
83
84    /// Remove a slice by index.
85    pub fn remove_slice(&mut self, index: usize) {
86        if index < self.slices.len() {
87            self.slices.remove(index);
88        }
89    }
90
91    /// Clear all slices.
92    pub fn clear(&mut self) {
93        self.slices.clear();
94    }
95
96    /// Set the maximum pressure for color normalization.
97    pub fn set_max_pressure(&mut self, max: f32) {
98        self.max_pressure = max.max(0.001);
99    }
100
101    /// Generate vertices for all slices.
102    pub fn generate_vertices(
103        &self,
104        grid: &SimulationGrid3D,
105        grid_physical_size: (f32, f32, f32),
106    ) -> Vec<Vertex3D> {
107        let mut vertices = Vec::new();
108
109        for config in &self.slices {
110            vertices.extend(self.generate_slice_vertices(grid, grid_physical_size, config));
111        }
112
113        vertices
114    }
115
116    /// Generate vertices for a single slice.
117    pub fn generate_slice_vertices(
118        &self,
119        grid: &SimulationGrid3D,
120        grid_physical_size: (f32, f32, f32),
121        config: &SliceConfig,
122    ) -> Vec<Vertex3D> {
123        let (phys_w, phys_h, phys_d) = grid_physical_size;
124        let (w, h, d) = (grid.width, grid.height, grid.depth);
125
126        match config.axis {
127            SliceAxis::XY => {
128                let z_idx = ((d as f32 - 1.0) * config.position) as usize;
129                let z_pos = phys_d * config.position;
130                let slice_data = grid.get_xy_slice(z_idx);
131
132                self.create_quad_grid(
133                    &slice_data,
134                    w,
135                    h,
136                    |x, y| [x * phys_w / w as f32, y * phys_h / h as f32, z_pos],
137                    [0.0, 0.0, 1.0],
138                    config,
139                )
140            }
141            SliceAxis::XZ => {
142                let y_idx = ((h as f32 - 1.0) * config.position) as usize;
143                let y_pos = phys_h * config.position;
144                let slice_data = grid.get_xz_slice(y_idx);
145
146                self.create_quad_grid(
147                    &slice_data,
148                    w,
149                    d,
150                    |x, z| [x * phys_w / w as f32, y_pos, z * phys_d / d as f32],
151                    [0.0, 1.0, 0.0],
152                    config,
153                )
154            }
155            SliceAxis::YZ => {
156                let x_idx = ((w as f32 - 1.0) * config.position) as usize;
157                let x_pos = phys_w * config.position;
158                let slice_data = grid.get_yz_slice(x_idx);
159
160                self.create_quad_grid(
161                    &slice_data,
162                    h,
163                    d,
164                    |y, z| [x_pos, y * phys_h / h as f32, z * phys_d / d as f32],
165                    [1.0, 0.0, 0.0],
166                    config,
167                )
168            }
169        }
170    }
171
172    /// Create a grid of quads from slice data.
173    fn create_quad_grid<F>(
174        &self,
175        data: &[f32],
176        width: usize,
177        height: usize,
178        pos_fn: F,
179        normal: [f32; 3],
180        config: &SliceConfig,
181    ) -> Vec<Vertex3D>
182    where
183        F: Fn(f32, f32) -> [f32; 3],
184    {
185        let mut vertices = Vec::with_capacity((width - 1) * (height - 1) * 6);
186
187        for y in 0..height - 1 {
188            for x in 0..width - 1 {
189                // Four corners of the quad
190                let idx00 = y * width + x;
191                let idx10 = y * width + x + 1;
192                let idx01 = (y + 1) * width + x;
193                let idx11 = (y + 1) * width + x + 1;
194
195                // Get pressure values and colors
196                let p00 = data.get(idx00).copied().unwrap_or(0.0);
197                let p10 = data.get(idx10).copied().unwrap_or(0.0);
198                let p01 = data.get(idx01).copied().unwrap_or(0.0);
199                let p11 = data.get(idx11).copied().unwrap_or(0.0);
200
201                let mut c00 = config.color_map.map(p00, self.max_pressure);
202                let mut c10 = config.color_map.map(p10, self.max_pressure);
203                let mut c01 = config.color_map.map(p01, self.max_pressure);
204                let mut c11 = config.color_map.map(p11, self.max_pressure);
205
206                // Apply opacity
207                c00[3] *= config.opacity;
208                c10[3] *= config.opacity;
209                c01[3] *= config.opacity;
210                c11[3] *= config.opacity;
211
212                // Positions
213                let pos00 = pos_fn(x as f32, y as f32);
214                let pos10 = pos_fn(x as f32 + 1.0, y as f32);
215                let pos01 = pos_fn(x as f32, y as f32 + 1.0);
216                let pos11 = pos_fn(x as f32 + 1.0, y as f32 + 1.0);
217
218                // Two triangles for the quad
219                vertices.push(Vertex3D::new(pos00, c00).with_normal(normal));
220                vertices.push(Vertex3D::new(pos10, c10).with_normal(normal));
221                vertices.push(Vertex3D::new(pos01, c01).with_normal(normal));
222
223                vertices.push(Vertex3D::new(pos10, c10).with_normal(normal));
224                vertices.push(Vertex3D::new(pos11, c11).with_normal(normal));
225                vertices.push(Vertex3D::new(pos01, c01).with_normal(normal));
226            }
227        }
228
229        vertices
230    }
231
232    /// Generate grid line vertices for slices.
233    pub fn generate_grid_lines(
234        &self,
235        grid_physical_size: (f32, f32, f32),
236        divisions: u32,
237    ) -> Vec<Vertex3D> {
238        let mut vertices = Vec::new();
239        let (phys_w, phys_h, phys_d) = grid_physical_size;
240        let line_color = [0.5, 0.5, 0.5, 0.3];
241
242        for config in &self.slices {
243            if !config.show_grid {
244                continue;
245            }
246
247            match config.axis {
248                SliceAxis::XY => {
249                    let z = phys_d * config.position;
250                    for i in 0..=divisions {
251                        let t = i as f32 / divisions as f32;
252                        // Horizontal lines
253                        vertices.push(Vertex3D::new([0.0, t * phys_h, z], line_color));
254                        vertices.push(Vertex3D::new([phys_w, t * phys_h, z], line_color));
255                        // Vertical lines
256                        vertices.push(Vertex3D::new([t * phys_w, 0.0, z], line_color));
257                        vertices.push(Vertex3D::new([t * phys_w, phys_h, z], line_color));
258                    }
259                }
260                SliceAxis::XZ => {
261                    let y = phys_h * config.position;
262                    for i in 0..=divisions {
263                        let t = i as f32 / divisions as f32;
264                        // X lines
265                        vertices.push(Vertex3D::new([0.0, y, t * phys_d], line_color));
266                        vertices.push(Vertex3D::new([phys_w, y, t * phys_d], line_color));
267                        // Z lines
268                        vertices.push(Vertex3D::new([t * phys_w, y, 0.0], line_color));
269                        vertices.push(Vertex3D::new([t * phys_w, y, phys_d], line_color));
270                    }
271                }
272                SliceAxis::YZ => {
273                    let x = phys_w * config.position;
274                    for i in 0..=divisions {
275                        let t = i as f32 / divisions as f32;
276                        // Y lines
277                        vertices.push(Vertex3D::new([x, 0.0, t * phys_d], line_color));
278                        vertices.push(Vertex3D::new([x, phys_h, t * phys_d], line_color));
279                        // Z lines
280                        vertices.push(Vertex3D::new([x, t * phys_h, 0.0], line_color));
281                        vertices.push(Vertex3D::new([x, t * phys_h, phys_d], line_color));
282                    }
283                }
284            }
285        }
286
287        vertices
288    }
289}
290
291/// Multi-slice configuration for volume exploration.
292pub struct MultiSliceView {
293    /// XY slice position (0-1)
294    pub xy_position: f32,
295    /// XZ slice position (0-1)
296    pub xz_position: f32,
297    /// YZ slice position (0-1)
298    pub yz_position: f32,
299    /// Show XY slice
300    pub show_xy: bool,
301    /// Show XZ slice
302    pub show_xz: bool,
303    /// Show YZ slice
304    pub show_yz: bool,
305    /// Color map
306    pub color_map: ColorMap,
307    /// Opacity
308    pub opacity: f32,
309}
310
311impl Default for MultiSliceView {
312    fn default() -> Self {
313        Self {
314            xy_position: 0.5,
315            xz_position: 0.5,
316            yz_position: 0.5,
317            show_xy: true,
318            show_xz: false,
319            show_yz: false,
320            color_map: ColorMap::BlueWhiteRed,
321            opacity: 0.8,
322        }
323    }
324}
325
326impl MultiSliceView {
327    /// Convert to slice renderer configuration.
328    pub fn to_slice_configs(&self) -> Vec<SliceConfig> {
329        let mut configs = Vec::new();
330
331        if self.show_xy {
332            configs.push(SliceConfig {
333                axis: SliceAxis::XY,
334                position: self.xy_position,
335                opacity: self.opacity,
336                color_map: self.color_map,
337                show_grid: false,
338            });
339        }
340
341        if self.show_xz {
342            configs.push(SliceConfig {
343                axis: SliceAxis::XZ,
344                position: self.xz_position,
345                opacity: self.opacity,
346                color_map: self.color_map,
347                show_grid: false,
348            });
349        }
350
351        if self.show_yz {
352            configs.push(SliceConfig {
353                axis: SliceAxis::YZ,
354                position: self.yz_position,
355                opacity: self.opacity,
356                color_map: self.color_map,
357                show_grid: false,
358            });
359        }
360
361        configs
362    }
363}
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368    use crate::simulation::physics::{AcousticParams3D, Environment};
369
370    fn create_test_grid() -> SimulationGrid3D {
371        let params = AcousticParams3D::new(Environment::default(), 0.1);
372        SimulationGrid3D::new(16, 16, 16, params)
373    }
374
375    #[test]
376    fn test_slice_config() {
377        let config = SliceConfig::default();
378        assert_eq!(config.axis, SliceAxis::XY);
379        assert!((config.position - 0.5).abs() < 0.001);
380    }
381
382    #[test]
383    fn test_slice_renderer() {
384        let grid = create_test_grid();
385        let renderer = SliceRenderer::new();
386
387        let vertices = renderer.generate_vertices(&grid, (1.6, 1.6, 1.6));
388        assert!(!vertices.is_empty());
389    }
390
391    #[test]
392    fn test_multi_slice_view() {
393        let view = MultiSliceView::default();
394        let configs = view.to_slice_configs();
395
396        // Default should have only XY slice
397        assert_eq!(configs.len(), 1);
398        assert_eq!(configs[0].axis, SliceAxis::XY);
399    }
400
401    #[test]
402    fn test_all_axes() {
403        let grid = create_test_grid();
404        let mut renderer = SliceRenderer::new();
405
406        renderer.clear();
407        renderer.add_slice(SliceConfig::new(SliceAxis::XY, 0.5));
408        renderer.add_slice(SliceConfig::new(SliceAxis::XZ, 0.5));
409        renderer.add_slice(SliceConfig::new(SliceAxis::YZ, 0.5));
410
411        let vertices = renderer.generate_vertices(&grid, (1.6, 1.6, 1.6));
412        assert!(vertices.len() > 100); // Should have many vertices
413    }
414}