Skip to main content

runmat_plot/plots/
surface.rs

1//! 3D surface plot implementation
2//!
3//! High-performance GPU-accelerated 3D surface rendering.
4
5use crate::context::shared_wgpu_context;
6use crate::core::{
7    BoundingBox, DrawCall, GpuVertexBuffer, ImageData, Material, PipelineType, RenderData, Vertex,
8};
9use crate::gpu::axis::OwnedAxisData;
10use crate::gpu::{util::readback_scalar_buffer_f64, ScalarType};
11use glam::{Vec3, Vec4};
12use std::sync::Arc;
13
14/// High-performance GPU-accelerated 3D surface plot
15#[derive(Debug, Clone)]
16pub struct SurfacePlot {
17    /// Grid data (Z values at X,Y coordinates)
18    pub x_data: Vec<f64>,
19    pub y_data: Vec<f64>,
20    pub z_data: Option<Vec<Vec<f64>>>, // Host data when available
21    /// Grid resolution for rendering/index generation (kept even for GPU-backed plots).
22    x_len: usize,
23    y_len: usize,
24
25    /// Surface properties
26    pub colormap: ColorMap,
27    pub shading_mode: ShadingMode,
28    pub wireframe: bool,
29    pub alpha: f32,
30    /// If true, render Z at 0 (flat), but color-map using Z values
31    pub flatten_z: bool,
32
33    /// If true, this flattened surface should behave like a 2D image for camera/UI decisions.
34    pub image_mode: bool,
35
36    /// Optional color limits override for mapping Z -> color (caxis)
37    pub color_limits: Option<(f64, f64)>,
38
39    /// Optional per-vertex color grid (for RGB images); if set, overrides colormap mapping
40    pub color_grid: Option<Vec<Vec<Vec4>>>, // [x_index][y_index] -> RGBA
41
42    /// Lighting and material
43    pub lighting_enabled: bool,
44    pub ambient_strength: f32,
45    pub diffuse_strength: f32,
46    pub specular_strength: f32,
47    pub shininess: f32,
48
49    /// Metadata
50    pub label: Option<String>,
51    pub visible: bool,
52
53    /// Generated rendering data (cached)
54    vertices: Option<Vec<Vertex>>,
55    indices: Option<Vec<u32>>,
56    bounds: Option<BoundingBox>,
57    dirty: bool,
58    gpu_vertices: Option<GpuVertexBuffer>,
59    gpu_vertex_count: Option<usize>,
60    gpu_bounds: Option<BoundingBox>,
61    gpu_source: Option<SurfaceGpuSource>,
62    gpu_color_grid_source: Option<SurfaceGpuColorGridSource>,
63}
64
65#[derive(Clone, Debug)]
66pub struct SurfaceGpuSource {
67    pub x_axis: OwnedAxisData,
68    pub y_axis: OwnedAxisData,
69    pub z_buffer: Arc<wgpu::Buffer>,
70    pub x_len: usize,
71    pub y_len: usize,
72    pub scalar: ScalarType,
73}
74
75#[derive(Clone, Debug)]
76pub struct SurfaceGpuColorGridSource {
77    pub image_buffer: Arc<wgpu::Buffer>,
78    pub rows: usize,
79    pub cols: usize,
80    pub channels: usize,
81    pub scalar: ScalarType,
82}
83
84/// Color mapping schemes
85#[derive(Debug, Clone, Copy, PartialEq)]
86pub enum ColorMap {
87    /// MATLAB-compatible colormaps
88    Jet,
89    Hot,
90    Cool,
91    Spring,
92    Summer,
93    Autumn,
94    Winter,
95    Gray,
96    Bone,
97    Copper,
98    Pink,
99    Lines,
100
101    /// Scientific colormaps
102    Viridis,
103    Plasma,
104    Inferno,
105    Magma,
106    Turbo,
107
108    /// Perceptually uniform
109    Parula,
110
111    /// Custom color ranges
112    Custom(Vec4, Vec4), // (min_color, max_color)
113}
114
115impl ColorMap {
116    pub const CANONICAL_NAMES: &[&str] = &[
117        "parula", "viridis", "plasma", "inferno", "magma", "turbo", "jet", "hot", "cool", "spring",
118        "summer", "autumn", "winter", "gray", "bone", "copper", "pink", "lines",
119    ];
120
121    pub const ALIASES: &[&str] = &["grey"];
122
123    pub fn from_name(name: &str) -> Option<Self> {
124        match name.trim().to_ascii_lowercase().as_str() {
125            "parula" => Some(Self::Parula),
126            "viridis" => Some(Self::Viridis),
127            "plasma" => Some(Self::Plasma),
128            "inferno" => Some(Self::Inferno),
129            "magma" => Some(Self::Magma),
130            "turbo" => Some(Self::Turbo),
131            "jet" => Some(Self::Jet),
132            "hot" => Some(Self::Hot),
133            "cool" => Some(Self::Cool),
134            "spring" => Some(Self::Spring),
135            "summer" => Some(Self::Summer),
136            "autumn" => Some(Self::Autumn),
137            "winter" => Some(Self::Winter),
138            "gray" | "grey" => Some(Self::Gray),
139            "bone" => Some(Self::Bone),
140            "copper" => Some(Self::Copper),
141            "pink" => Some(Self::Pink),
142            "lines" => Some(Self::Lines),
143            _ => None,
144        }
145    }
146}
147
148/// Surface shading modes
149#[derive(Debug, Clone, Copy, PartialEq)]
150pub enum ShadingMode {
151    /// Flat shading (per-face normals)
152    Flat,
153    /// Smooth shading (interpolated normals)
154    Smooth,
155    /// Faceted (flat with visible edges)
156    Faceted,
157    /// No shading (just color mapping)
158    None,
159}
160
161impl Default for ColorMap {
162    fn default() -> Self {
163        Self::Viridis
164    }
165}
166
167impl Default for ShadingMode {
168    fn default() -> Self {
169        Self::Smooth
170    }
171}
172
173impl SurfacePlot {
174    pub async fn export_scene_grid_data(
175        &self,
176    ) -> Result<(Vec<f64>, Vec<f64>, Vec<Vec<f64>>), String> {
177        if let Some(z) = &self.z_data {
178            return Ok((self.x_data.clone(), self.y_data.clone(), z.clone()));
179        }
180
181        if let Some(source) = &self.gpu_source {
182            let context = shared_wgpu_context().ok_or_else(|| {
183                "surface plot has GPU source data but no shared WGPU context is installed"
184                    .to_string()
185            })?;
186            let x = source
187                .x_axis
188                .export_f64(&context.device, &context.queue, source.x_len, source.scalar)
189                .await?;
190            let y = source
191                .y_axis
192                .export_f64(&context.device, &context.queue, source.y_len, source.scalar)
193                .await?;
194            let z_flat = readback_scalar_buffer_f64(
195                &context.device,
196                &context.queue,
197                &source.z_buffer,
198                source.x_len * source.y_len,
199                source.scalar,
200            )
201            .await?;
202            let mut z = Vec::with_capacity(source.x_len);
203            for row in 0..source.x_len {
204                let start = row * source.y_len;
205                z.push(
206                    z_flat
207                        .get(start..start + source.y_len)
208                        .ok_or_else(|| "surface GPU source grid is out of range".to_string())?
209                        .to_vec(),
210                );
211            }
212            return Ok((x, y, z));
213        }
214
215        if self.gpu_vertices.is_some() {
216            return Err(
217                "surface plot has GPU render vertices but no exportable source data".to_string(),
218            );
219        }
220
221        Ok((Vec::new(), Vec::new(), Vec::new()))
222    }
223
224    pub async fn export_scene_color_grid(&self) -> Result<Option<Vec<Vec<Vec4>>>, String> {
225        if let Some(grid) = &self.color_grid {
226            return Ok(Some(grid.clone()));
227        }
228
229        let Some(source) = &self.gpu_color_grid_source else {
230            return Ok(None);
231        };
232        let context = shared_wgpu_context().ok_or_else(|| {
233            "surface image has GPU color data but no shared WGPU context is installed".to_string()
234        })?;
235        let values = readback_scalar_buffer_f64(
236            &context.device,
237            &context.queue,
238            &source.image_buffer,
239            source.rows * source.cols * source.channels,
240            source.scalar,
241        )
242        .await?;
243        let mut grid = vec![vec![Vec4::ZERO; source.rows]; source.cols];
244        let plane = source.rows * source.cols;
245        for (col, grid_row) in grid.iter_mut().enumerate() {
246            for (row, color) in grid_row.iter_mut().enumerate() {
247                let base = row + source.rows * col;
248                let r = values.get(base).copied().unwrap_or(0.0) as f32;
249                let g = values.get(base + plane).copied().unwrap_or(0.0) as f32;
250                let b = values.get(base + (2 * plane)).copied().unwrap_or(0.0) as f32;
251                let a = if source.channels == 4 {
252                    values.get(base + (3 * plane)).copied().unwrap_or(1.0) as f32
253                } else {
254                    1.0
255                };
256                *color = Vec4::new(r, g, b, a);
257            }
258        }
259        Ok(Some(grid))
260    }
261
262    /// Create a new surface plot from meshgrid data
263    pub fn new(x_data: Vec<f64>, y_data: Vec<f64>, z_data: Vec<Vec<f64>>) -> Result<Self, String> {
264        // Validate dimensions
265        if z_data.len() != x_data.len() {
266            return Err(format!(
267                "Z data rows ({}) must match X data length ({})",
268                z_data.len(),
269                x_data.len()
270            ));
271        }
272
273        for (i, row) in z_data.iter().enumerate() {
274            if row.len() != y_data.len() {
275                return Err(format!(
276                    "Z data row {} length ({}) must match Y data length ({})",
277                    i,
278                    row.len(),
279                    y_data.len()
280                ));
281            }
282        }
283
284        Ok(Self {
285            x_len: x_data.len(),
286            y_len: y_data.len(),
287            x_data,
288            y_data,
289            z_data: Some(z_data),
290            colormap: ColorMap::default(),
291            shading_mode: ShadingMode::default(),
292            wireframe: false,
293            alpha: 1.0,
294            flatten_z: false,
295            image_mode: false,
296            color_limits: None,
297            color_grid: None,
298            lighting_enabled: true,
299            ambient_strength: 0.2,
300            diffuse_strength: 0.8,
301            specular_strength: 0.5,
302            shininess: 32.0,
303            label: None,
304            visible: true,
305            vertices: None,
306            indices: None,
307            bounds: None,
308            dirty: true,
309            gpu_vertices: None,
310            gpu_vertex_count: None,
311            gpu_bounds: None,
312            gpu_source: None,
313            gpu_color_grid_source: None,
314        })
315    }
316
317    /// Create a surface plot backed by a GPU vertex buffer.
318    pub fn from_gpu_buffer(
319        x_len: usize,
320        y_len: usize,
321        buffer: GpuVertexBuffer,
322        vertex_count: usize,
323        bounds: BoundingBox,
324    ) -> Self {
325        Self {
326            x_data: Vec::new(),
327            y_data: Vec::new(),
328            z_data: None,
329            x_len,
330            y_len,
331            colormap: ColorMap::default(),
332            shading_mode: ShadingMode::default(),
333            wireframe: false,
334            alpha: 1.0,
335            flatten_z: false,
336            image_mode: false,
337            color_limits: None,
338            color_grid: None,
339            lighting_enabled: true,
340            ambient_strength: 0.2,
341            diffuse_strength: 0.8,
342            specular_strength: 0.5,
343            shininess: 32.0,
344            label: None,
345            visible: true,
346            vertices: None,
347            indices: None,
348            bounds: Some(bounds),
349            dirty: false,
350            gpu_vertices: Some(buffer),
351            gpu_vertex_count: Some(vertex_count),
352            gpu_bounds: Some(bounds),
353            gpu_source: None,
354            gpu_color_grid_source: None,
355        }
356    }
357
358    pub fn with_gpu_source(mut self, source: SurfaceGpuSource) -> Self {
359        self.gpu_source = Some(source);
360        self
361    }
362
363    pub fn with_gpu_color_grid_source(mut self, source: SurfaceGpuColorGridSource) -> Self {
364        self.gpu_color_grid_source = Some(source);
365        self
366    }
367
368    fn drop_gpu_if_possible(&mut self) {
369        if self.gpu_vertices.is_some() && self.z_data.is_some() {
370            self.invalidate_gpu_data();
371        }
372    }
373
374    /// Create surface from a function
375    pub fn from_function<F>(
376        x_range: (f64, f64),
377        y_range: (f64, f64),
378        resolution: (usize, usize),
379        func: F,
380    ) -> Result<Self, String>
381    where
382        F: Fn(f64, f64) -> f64,
383    {
384        let (x_res, y_res) = resolution;
385        if x_res < 2 || y_res < 2 {
386            return Err("Resolution must be at least 2x2".to_string());
387        }
388
389        let x_data: Vec<f64> = (0..x_res)
390            .map(|i| x_range.0 + (x_range.1 - x_range.0) * i as f64 / (x_res - 1) as f64)
391            .collect();
392
393        let y_data: Vec<f64> = (0..y_res)
394            .map(|j| y_range.0 + (y_range.1 - y_range.0) * j as f64 / (y_res - 1) as f64)
395            .collect();
396
397        let z_data: Vec<Vec<f64>> = x_data
398            .iter()
399            .map(|&x| y_data.iter().map(|&y| func(x, y)).collect())
400            .collect();
401
402        Self::new(x_data, y_data, z_data)
403    }
404
405    fn invalidate_gpu_data(&mut self) {
406        self.gpu_vertices = None;
407        self.gpu_vertex_count = None;
408        self.gpu_bounds = None;
409        self.gpu_source = None;
410    }
411
412    /// Set color mapping
413    pub fn with_colormap(mut self, colormap: ColorMap) -> Self {
414        self.colormap = colormap;
415        self.dirty = true;
416        self.drop_gpu_if_possible();
417        self
418    }
419
420    /// Set shading mode
421    pub fn with_shading(mut self, shading: ShadingMode) -> Self {
422        self.shading_mode = shading;
423        self.dirty = true;
424        self.drop_gpu_if_possible();
425        self
426    }
427
428    /// Enable/disable wireframe
429    pub fn with_wireframe(mut self, enabled: bool) -> Self {
430        self.wireframe = enabled;
431        self.dirty = true;
432        self.drop_gpu_if_possible();
433        self
434    }
435
436    /// Set transparency
437    pub fn with_alpha(mut self, alpha: f32) -> Self {
438        self.alpha = alpha.clamp(0.0, 1.0);
439        self.dirty = true;
440        self.drop_gpu_if_possible();
441        self
442    }
443
444    /// Render surface flat in Z while mapping colors from Z values (for imagesc/imshow)
445    pub fn with_flatten_z(mut self, enabled: bool) -> Self {
446        self.flatten_z = enabled;
447        self.dirty = true;
448        self.drop_gpu_if_possible();
449        self
450    }
451
452    pub fn with_image_mode(mut self, enabled: bool) -> Self {
453        self.image_mode = enabled;
454        self.dirty = true;
455        self.drop_gpu_if_possible();
456        self
457    }
458
459    /// Override color mapping limits (caxis)
460    pub fn with_color_limits(mut self, limits: Option<(f64, f64)>) -> Self {
461        self.color_limits = limits;
462        self.dirty = true;
463        self.drop_gpu_if_possible();
464        self
465    }
466
467    /// Mutably set color mapping limits (caxis)
468    pub fn set_color_limits(&mut self, limits: Option<(f64, f64)>) {
469        self.color_limits = limits;
470        self.dirty = true;
471        self.drop_gpu_if_possible();
472    }
473
474    /// Provide explicit per-vertex colors (RGB[A])
475    pub fn with_color_grid(mut self, grid: Vec<Vec<Vec4>>) -> Self {
476        self.color_grid = Some(grid);
477        self.gpu_color_grid_source = None;
478        self.dirty = true;
479        self.drop_gpu_if_possible();
480        self
481    }
482
483    /// Set plot label for legends
484    pub fn with_label<S: Into<String>>(mut self, label: S) -> Self {
485        self.label = Some(label.into());
486        self
487    }
488
489    /// Get the number of grid points
490    pub fn len(&self) -> usize {
491        self.x_len * self.y_len
492    }
493
494    /// Check if the surface has no data
495    pub fn is_empty(&self) -> bool {
496        self.x_len == 0 || self.y_len == 0
497    }
498
499    /// Get the bounding box of the surface
500    pub fn bounds(&mut self) -> BoundingBox {
501        if self.dirty || self.bounds.is_none() {
502            self.compute_bounds();
503        }
504        self.bounds.unwrap()
505    }
506
507    /// Compute bounding box
508    fn compute_bounds(&mut self) {
509        if let Some(bounds) = self.gpu_bounds {
510            self.bounds = Some(bounds);
511            return;
512        }
513
514        let mut min_x = f32::INFINITY;
515        let mut max_x = f32::NEG_INFINITY;
516        let mut min_y = f32::INFINITY;
517        let mut max_y = f32::NEG_INFINITY;
518        let mut min_z = f32::INFINITY;
519        let mut max_z = f32::NEG_INFINITY;
520
521        for &x in &self.x_data {
522            min_x = min_x.min(x as f32);
523            max_x = max_x.max(x as f32);
524        }
525
526        for &y in &self.y_data {
527            min_y = min_y.min(y as f32);
528            max_y = max_y.max(y as f32);
529        }
530
531        if let Some(rows) = &self.z_data {
532            for row in rows {
533                for &z in row {
534                    if z.is_finite() {
535                        min_z = min_z.min(z as f32);
536                        max_z = max_z.max(z as f32);
537                    }
538                }
539            }
540        }
541
542        self.bounds = Some(BoundingBox::new(
543            Vec3::new(min_x, min_y, min_z),
544            Vec3::new(max_x, max_y, max_z),
545        ));
546    }
547
548    /// Get plot statistics for debugging
549    pub fn statistics(&self) -> SurfaceStatistics {
550        let grid_size = self.x_len * self.y_len;
551        let triangle_count = if self.x_len > 1 && self.y_len > 1 {
552            (self.x_len - 1) * (self.y_len - 1) * 2
553        } else {
554            0
555        };
556
557        SurfaceStatistics {
558            grid_points: grid_size,
559            triangle_count,
560            x_resolution: self.x_len,
561            y_resolution: self.y_len,
562            memory_usage: self.estimated_memory_usage(),
563        }
564    }
565
566    /// Estimate memory usage in bytes
567    pub fn estimated_memory_usage(&self) -> usize {
568        let data_size = std::mem::size_of::<f64>()
569            * (self.x_data.len()
570                + self.y_data.len()
571                + self
572                    .z_data
573                    .as_ref()
574                    .map_or(0, |z| z.len() * self.y_data.len()));
575
576        let vertices_size = self
577            .vertices
578            .as_ref()
579            .map_or(0, |v| v.len() * std::mem::size_of::<Vertex>());
580
581        let indices_size = self
582            .indices
583            .as_ref()
584            .map_or(0, |i| i.len() * std::mem::size_of::<u32>());
585
586        let gpu_size = self.gpu_vertex_count.unwrap_or(0) * std::mem::size_of::<Vertex>();
587
588        data_size + vertices_size + indices_size + gpu_size
589    }
590
591    /// Generate vertices for surface mesh
592    fn generate_vertices(&mut self) -> &Vec<Vertex> {
593        if self.gpu_vertices.is_some() {
594            if self.vertices.is_none() {
595                self.vertices = Some(Vec::new());
596            }
597            return self.vertices.as_ref().unwrap();
598        }
599
600        if self.dirty || self.vertices.is_none() {
601            log::trace!(
602                target: "runmat_plot",
603                "surface gen vertices {} x {}",
604                self.x_data.len(),
605                self.y_data.len()
606            );
607
608            let mut vertices = Vec::new();
609
610            // Determine color mapping range
611            let z_rows = self
612                .z_data
613                .as_ref()
614                .expect("CPU surface data missing during vertex generation");
615            let (min_z, max_z) = if let Some((lo, hi)) = self.color_limits {
616                (lo, hi)
617            } else {
618                let mut min_z = f64::INFINITY;
619                let mut max_z = f64::NEG_INFINITY;
620                for row in z_rows {
621                    for &z in row {
622                        if z.is_finite() {
623                            min_z = min_z.min(z);
624                            max_z = max_z.max(z);
625                        }
626                    }
627                }
628                (min_z, max_z)
629            };
630            let z_range = (max_z - min_z).max(f64::MIN_POSITIVE);
631
632            // Generate vertices for each grid point
633            for (i, &x) in self.x_data.iter().enumerate() {
634                for (j, &y) in self.y_data.iter().enumerate() {
635                    let z = z_rows[i][j];
636                    let z_pos = if self.flatten_z { 0.0 } else { z as f32 };
637                    let position = Vec3::new(x as f32, y as f32, z_pos);
638
639                    // Simple normal calculation (can be improved with proper gradients)
640                    let normal = Vec3::new(0.0, 0.0, 1.0); // Placeholder
641
642                    // Determine color: explicit grid (RGB) or colormap from Z
643                    let color = if let Some(grid) = &self.color_grid {
644                        let c = grid[i][j];
645                        Vec4::new(c.x, c.y, c.z, c.w)
646                    } else {
647                        let t = ((z - min_z) / z_range) as f32;
648                        let color_rgb = self.colormap.map_value(t.clamp(0.0, 1.0));
649                        Vec4::new(color_rgb.x, color_rgb.y, color_rgb.z, self.alpha)
650                    };
651
652                    vertices.push(Vertex {
653                        position: position.to_array(),
654                        normal: normal.to_array(),
655                        color: color.to_array(),
656                        tex_coords: [
657                            i as f32 / (self.x_data.len() - 1).max(1) as f32,
658                            j as f32 / (self.y_data.len() - 1).max(1) as f32,
659                        ],
660                    });
661                }
662            }
663
664            log::trace!(target: "runmat_plot", "surface vertices={}", vertices.len());
665            self.vertices = Some(vertices);
666        }
667        self.vertices.as_ref().unwrap()
668    }
669
670    /// Generate indices for surface triangulation
671    fn generate_indices(&mut self) -> &Vec<u32> {
672        if self.dirty || self.indices.is_none() {
673            log::trace!(target: "runmat_plot", "surface generating indices");
674
675            let mut indices = Vec::new();
676            let x_res = self.x_len;
677            let y_res = self.y_len;
678
679            // Generate triangle indices for surface mesh
680            for i in 0..x_res - 1 {
681                for j in 0..y_res - 1 {
682                    let base = (i * y_res + j) as u32;
683                    let next_row = base + y_res as u32;
684
685                    // Two triangles per quad
686                    // Triangle 1: (i,j), (i+1,j), (i,j+1)
687                    indices.push(base);
688                    indices.push(next_row);
689                    indices.push(base + 1);
690
691                    // Triangle 2: (i+1,j), (i+1,j+1), (i,j+1)
692                    indices.push(next_row);
693                    indices.push(next_row + 1);
694                    indices.push(base + 1);
695                }
696            }
697
698            log::trace!(target: "runmat_plot", "surface indices={}", indices.len());
699            self.indices = Some(indices);
700            self.dirty = false;
701        }
702        self.indices.as_ref().unwrap()
703    }
704
705    fn generate_wireframe_indices(&self) -> Vec<u32> {
706        let mut indices = Vec::new();
707        if self.x_len < 2 || self.y_len < 2 {
708            return indices;
709        }
710
711        // Horizontal grid edges (along Y for each X row)
712        for i in 0..self.x_len {
713            for j in 0..(self.y_len - 1) {
714                let a = (i * self.y_len + j) as u32;
715                let b = (i * self.y_len + j + 1) as u32;
716                indices.push(a);
717                indices.push(b);
718            }
719        }
720
721        // Vertical grid edges (along X for each Y column)
722        for i in 0..(self.x_len - 1) {
723            for j in 0..self.y_len {
724                let a = (i * self.y_len + j) as u32;
725                let b = ((i + 1) * self.y_len + j) as u32;
726                indices.push(a);
727                indices.push(b);
728            }
729        }
730
731        indices
732    }
733
734    /// Generate complete render data for the graphics pipeline
735    pub fn render_data(&mut self) -> RenderData {
736        log::debug!(
737            target: "runmat_plot",
738            "surface render_data start: {} x {}",
739            self.x_len,
740            self.y_len
741        );
742
743        if self.image_mode && self.z_data.is_some() && self.gpu_vertices.is_none() {
744            return self.image_render_data();
745        }
746
747        let using_gpu = self.gpu_vertices.is_some();
748        let bounds = self.bounds();
749        let vertices = if using_gpu {
750            Vec::new()
751        } else {
752            self.generate_vertices().clone()
753        };
754        let indices = if self.wireframe {
755            self.generate_wireframe_indices()
756        } else {
757            self.generate_indices().clone()
758        };
759
760        let material = Material {
761            albedo: Vec4::new(1.0, 1.0, 1.0, self.alpha),
762            ..Default::default()
763        };
764
765        let vertex_count = if using_gpu {
766            self.gpu_vertex_count.unwrap_or(0)
767        } else {
768            vertices.len()
769        };
770
771        log::debug!(
772            target: "runmat_plot",
773            "surface render_data generated: vertex_count={} (gpu={}), indices={}",
774            vertex_count,
775            using_gpu,
776            indices.len()
777        );
778
779        let draw_call = DrawCall {
780            vertex_offset: 0,
781            vertex_count,
782            index_offset: Some(0),
783            index_count: Some(indices.len()),
784            instance_count: 1,
785        };
786
787        log::trace!(target: "runmat_plot", "surface render_data done");
788
789        RenderData {
790            pipeline_type: if self.wireframe {
791                PipelineType::Lines
792            } else {
793                PipelineType::Triangles
794            },
795            vertices,
796            indices: Some(indices),
797
798            gpu_vertices: self.gpu_vertices.clone(),
799            bounds: Some(bounds),
800            material,
801            draw_calls: vec![draw_call],
802            image: None,
803        }
804    }
805
806    fn image_render_data(&mut self) -> RenderData {
807        let bounds = self.bounds();
808        let x_min = bounds.min.x;
809        let x_max = bounds.max.x;
810        let y_min = bounds.min.y;
811        let y_max = bounds.max.y;
812        let z_rows = self
813            .z_data
814            .as_ref()
815            .expect("image-mode surfaces require host color data");
816        let width = self.x_len.max(1);
817        let height = self.y_len.max(1);
818        let color_limits = self.color_limits.or_else(|| {
819            let mut min_z = f64::INFINITY;
820            let mut max_z = f64::NEG_INFINITY;
821            for row in z_rows {
822                for &z in row {
823                    if z.is_finite() {
824                        min_z = min_z.min(z);
825                        max_z = max_z.max(z);
826                    }
827                }
828            }
829            if min_z.is_finite() && max_z.is_finite() {
830                Some((min_z, max_z))
831            } else {
832                None
833            }
834        });
835        let (min_z, max_z) = color_limits.unwrap_or((0.0, 1.0));
836        let z_range = (max_z - min_z).max(f64::MIN_POSITIVE);
837        let mut data = Vec::with_capacity(width * height * 4);
838
839        for row in 0..height {
840            let y_idx = height - 1 - row;
841            for x_idx in 0..width {
842                let color = if let Some(grid) = &self.color_grid {
843                    grid[x_idx][y_idx]
844                } else {
845                    let z = z_rows[x_idx][y_idx];
846                    let t = ((z - min_z) / z_range) as f32;
847                    let rgb = self.colormap.map_value(t.clamp(0.0, 1.0));
848                    Vec4::new(rgb.x, rgb.y, rgb.z, self.alpha)
849                };
850                data.push((color.x.clamp(0.0, 1.0) * 255.0).round() as u8);
851                data.push((color.y.clamp(0.0, 1.0) * 255.0).round() as u8);
852                data.push((color.z.clamp(0.0, 1.0) * 255.0).round() as u8);
853                data.push((color.w.clamp(0.0, 1.0) * 255.0).round() as u8);
854            }
855        }
856
857        let vertices = vec![
858            Vertex {
859                position: [x_min, y_min, 0.0],
860                normal: [0.0, 0.0, 1.0],
861                color: [1.0, 1.0, 1.0, self.alpha],
862                tex_coords: [0.0, 1.0],
863            },
864            Vertex {
865                position: [x_max, y_min, 0.0],
866                normal: [0.0, 0.0, 1.0],
867                color: [1.0, 1.0, 1.0, self.alpha],
868                tex_coords: [1.0, 1.0],
869            },
870            Vertex {
871                position: [x_max, y_max, 0.0],
872                normal: [0.0, 0.0, 1.0],
873                color: [1.0, 1.0, 1.0, self.alpha],
874                tex_coords: [1.0, 0.0],
875            },
876            Vertex {
877                position: [x_min, y_max, 0.0],
878                normal: [0.0, 0.0, 1.0],
879                color: [1.0, 1.0, 1.0, self.alpha],
880                tex_coords: [0.0, 0.0],
881            },
882        ];
883        let indices = vec![0, 1, 2, 0, 2, 3];
884
885        RenderData {
886            pipeline_type: PipelineType::Textured,
887            vertices,
888            indices: Some(indices.clone()),
889            gpu_vertices: None,
890            bounds: Some(bounds),
891            material: Material {
892                albedo: Vec4::new(1.0, 1.0, 1.0, self.alpha),
893                ..Default::default()
894            },
895            draw_calls: vec![DrawCall {
896                vertex_offset: 0,
897                vertex_count: 4,
898                index_offset: Some(0),
899                index_count: Some(indices.len()),
900                instance_count: 1,
901            }],
902            image: Some(ImageData::Rgba8 {
903                width: width as u32,
904                height: height as u32,
905                data,
906            }),
907        }
908    }
909}
910
911/// Surface plot performance and data statistics
912#[derive(Debug, Clone)]
913pub struct SurfaceStatistics {
914    pub grid_points: usize,
915    pub triangle_count: usize,
916    pub x_resolution: usize,
917    pub y_resolution: usize,
918    pub memory_usage: usize,
919}
920
921impl ColorMap {
922    /// Map a normalized value [0,1] to a color
923    pub fn map_value(&self, t: f32) -> Vec3 {
924        let t = t.clamp(0.0, 1.0);
925
926        match self {
927            ColorMap::Jet => self.jet_colormap(t),
928            ColorMap::Hot => self.hot_colormap(t),
929            ColorMap::Cool => self.cool_colormap(t),
930            ColorMap::Spring => self.spring_colormap(t),
931            ColorMap::Summer => self.summer_colormap(t),
932            ColorMap::Autumn => self.autumn_colormap(t),
933            ColorMap::Winter => self.winter_colormap(t),
934            ColorMap::Gray => Vec3::splat(t),
935            ColorMap::Bone => self.bone_colormap(t),
936            ColorMap::Copper => self.copper_colormap(t),
937            ColorMap::Pink => self.pink_colormap(t),
938            ColorMap::Lines => self.lines_colormap(t),
939            ColorMap::Viridis => self.viridis_colormap(t),
940            ColorMap::Plasma => self.plasma_colormap(t),
941            ColorMap::Inferno => self.inferno_colormap(t),
942            ColorMap::Magma => self.magma_colormap(t),
943            ColorMap::Turbo => self.turbo_colormap(t),
944            ColorMap::Parula => self.parula_colormap(t),
945            ColorMap::Custom(min_color, max_color) => {
946                min_color.truncate().lerp(max_color.truncate(), t)
947            }
948        }
949    }
950
951    /// MATLAB Jet colormap
952    fn jet_colormap(&self, t: f32) -> Vec3 {
953        let r = (1.5 - 4.0 * (t - 0.75).abs()).clamp(0.0, 1.0);
954        let g = (1.5 - 4.0 * (t - 0.5).abs()).clamp(0.0, 1.0);
955        let b = (1.5 - 4.0 * (t - 0.25).abs()).clamp(0.0, 1.0);
956        Vec3::new(r, g, b)
957    }
958
959    /// Hot colormap (black -> red -> yellow -> white)
960    fn hot_colormap(&self, t: f32) -> Vec3 {
961        if t < 1.0 / 3.0 {
962            Vec3::new(3.0 * t, 0.0, 0.0)
963        } else if t < 2.0 / 3.0 {
964            Vec3::new(1.0, 3.0 * t - 1.0, 0.0)
965        } else {
966            Vec3::new(1.0, 1.0, 3.0 * t - 2.0)
967        }
968    }
969
970    /// Cool colormap (cyan -> magenta)
971    fn cool_colormap(&self, t: f32) -> Vec3 {
972        Vec3::new(t, 1.0 - t, 1.0)
973    }
974
975    /// Viridis colormap (perceptually uniform)
976    fn viridis_colormap(&self, t: f32) -> Vec3 {
977        // Simplified Viridis approximation
978        let r = (0.267004 + t * (0.993248 - 0.267004)).clamp(0.0, 1.0);
979        let g = (0.004874 + t * (0.906157 - 0.004874)).clamp(0.0, 1.0);
980        let b = (0.329415 + t * (0.143936 - 0.329415) + t * t * 0.5).clamp(0.0, 1.0);
981        Vec3::new(r, g, b)
982    }
983
984    /// Plasma colormap (perceptually uniform)
985    fn plasma_colormap(&self, t: f32) -> Vec3 {
986        // Simplified Plasma approximation
987        let r = (0.050383 + t * (0.940015 - 0.050383)).clamp(0.0, 1.0);
988        let g = (0.029803 + t * (0.975158 - 0.029803) * (1.0 - t)).clamp(0.0, 1.0);
989        let b = (0.527975 + t * (0.131326 - 0.527975)).clamp(0.0, 1.0);
990        Vec3::new(r, g, b)
991    }
992
993    /// Spring colormap (magenta -> yellow)
994    fn spring_colormap(&self, t: f32) -> Vec3 {
995        Vec3::new(1.0, t, 1.0 - t)
996    }
997
998    /// Summer colormap (green -> yellow)
999    fn summer_colormap(&self, t: f32) -> Vec3 {
1000        Vec3::new(t, 0.5 + 0.5 * t, 0.4)
1001    }
1002
1003    /// Autumn colormap (red -> yellow)
1004    fn autumn_colormap(&self, t: f32) -> Vec3 {
1005        Vec3::new(1.0, t, 0.0)
1006    }
1007
1008    /// Winter colormap (blue -> green)
1009    fn winter_colormap(&self, t: f32) -> Vec3 {
1010        Vec3::new(0.0, t, 1.0 - 0.5 * t)
1011    }
1012
1013    /// Bone colormap (black -> white with blue tint)
1014    fn bone_colormap(&self, t: f32) -> Vec3 {
1015        if t < 3.0 / 8.0 {
1016            Vec3::new(7.0 / 8.0 * t, 7.0 / 8.0 * t, 29.0 / 24.0 * t)
1017        } else {
1018            Vec3::new(
1019                (29.0 + 7.0 * t) / 24.0,
1020                (29.0 + 7.0 * t) / 24.0,
1021                (29.0 + 7.0 * t) / 24.0,
1022            )
1023        }
1024    }
1025
1026    /// Copper colormap (black -> copper)
1027    fn copper_colormap(&self, t: f32) -> Vec3 {
1028        Vec3::new((1.25 * t).min(1.0), 0.7812 * t, 0.4975 * t)
1029    }
1030
1031    /// Pink colormap (black -> pink -> white)
1032    fn pink_colormap(&self, t: f32) -> Vec3 {
1033        let sqrt_t = t.sqrt();
1034        if t < 3.0 / 8.0 {
1035            Vec3::new(14.0 / 9.0 * sqrt_t, 2.0 / 3.0 * sqrt_t, 2.0 / 3.0 * sqrt_t)
1036        } else {
1037            Vec3::new(
1038                2.0 * sqrt_t - 1.0 / 3.0,
1039                8.0 / 9.0 * sqrt_t + 1.0 / 3.0,
1040                8.0 / 9.0 * sqrt_t + 1.0 / 3.0,
1041            )
1042        }
1043    }
1044
1045    /// Lines colormap (cycling through basic colors)
1046    fn lines_colormap(&self, t: f32) -> Vec3 {
1047        let _phase = (t * 7.0) % 1.0; // For future use in color transitions
1048        let index = (t * 7.0) as usize % 7;
1049        match index {
1050            0 => Vec3::new(0.0, 0.0, 1.0),    // Blue
1051            1 => Vec3::new(0.0, 0.5, 0.0),    // Green
1052            2 => Vec3::new(1.0, 0.0, 0.0),    // Red
1053            3 => Vec3::new(0.0, 0.75, 0.75),  // Cyan
1054            4 => Vec3::new(0.75, 0.0, 0.75),  // Magenta
1055            5 => Vec3::new(0.75, 0.75, 0.0),  // Yellow
1056            _ => Vec3::new(0.25, 0.25, 0.25), // Dark gray
1057        }
1058    }
1059
1060    /// Inferno colormap (perceptually uniform)
1061    fn inferno_colormap(&self, t: f32) -> Vec3 {
1062        // Simplified Inferno approximation
1063        let r = (0.001462 + t * (0.988362 - 0.001462)).clamp(0.0, 1.0);
1064        let g = (0.000466 + t * t * (0.982895 - 0.000466)).clamp(0.0, 1.0);
1065        let b = (0.013866 + t * (1.0 - t) * (0.416065 - 0.013866)).clamp(0.0, 1.0);
1066        Vec3::new(r, g, b)
1067    }
1068
1069    /// Magma colormap (perceptually uniform)
1070    fn magma_colormap(&self, t: f32) -> Vec3 {
1071        // Simplified Magma approximation
1072        let r = (0.001462 + t * (0.987053 - 0.001462)).clamp(0.0, 1.0);
1073        let g = (0.000466 + t * t * (0.991438 - 0.000466)).clamp(0.0, 1.0);
1074        let b = (0.013866 + t * (0.644237 - 0.013866) * (1.0 - t)).clamp(0.0, 1.0);
1075        Vec3::new(r, g, b)
1076    }
1077
1078    /// Turbo colormap (improved rainbow)
1079    fn turbo_colormap(&self, t: f32) -> Vec3 {
1080        // Simplified Turbo approximation (Google's improved rainbow)
1081        let r = if t < 0.5 {
1082            (0.13 + 0.87 * (2.0 * t).powf(0.25)).clamp(0.0, 1.0)
1083        } else {
1084            (0.8685 + 0.1315 * (2.0 * (1.0 - t)).powf(0.25)).clamp(0.0, 1.0)
1085        };
1086
1087        let g = if t < 0.25 {
1088            4.0 * t
1089        } else if t < 0.75 {
1090            1.0
1091        } else {
1092            1.0 - 4.0 * (t - 0.75)
1093        }
1094        .clamp(0.0, 1.0);
1095
1096        let b = if t < 0.5 {
1097            (0.8 * (1.0 - 2.0 * t).powf(0.25)).clamp(0.0, 1.0)
1098        } else {
1099            (0.1 + 0.9 * (2.0 * t - 1.0).powf(0.25)).clamp(0.0, 1.0)
1100        };
1101
1102        Vec3::new(r, g, b)
1103    }
1104
1105    /// Parula colormap (MATLAB's default)
1106    fn parula_colormap(&self, t: f32) -> Vec3 {
1107        // Simplified Parula approximation
1108        let r = if t < 0.25 {
1109            0.2081 * (1.0 - t)
1110        } else if t < 0.5 {
1111            t - 0.25
1112        } else if t < 0.75 {
1113            1.0
1114        } else {
1115            1.0 - 0.5 * (t - 0.75)
1116        }
1117        .clamp(0.0, 1.0);
1118
1119        let g = if t < 0.125 {
1120            0.1663 * t / 0.125
1121        } else if t < 0.375 {
1122            0.1663 + (0.7079 - 0.1663) * (t - 0.125) / 0.25
1123        } else if t < 0.625 {
1124            0.7079 + (0.9839 - 0.7079) * (t - 0.375) / 0.25
1125        } else {
1126            0.9839 * (1.0 - (t - 0.625) / 0.375)
1127        }
1128        .clamp(0.0, 1.0);
1129
1130        let b = if t < 0.25 {
1131            0.5 + 0.5 * t / 0.25
1132        } else if t < 0.5 {
1133            1.0
1134        } else {
1135            1.0 - 2.0 * (t - 0.5)
1136        }
1137        .clamp(0.0, 1.0);
1138
1139        Vec3::new(r, g, b)
1140    }
1141
1142    /// Default colormap fallback
1143    #[allow(dead_code)] // Fallback method for colormap errors
1144    fn default_colormap(&self, t: f32) -> Vec3 {
1145        // Use a simple RGB transition as fallback
1146        if t < 0.5 {
1147            Vec3::new(0.0, 2.0 * t, 1.0 - 2.0 * t)
1148        } else {
1149            Vec3::new(2.0 * (t - 0.5), 1.0 - 2.0 * (t - 0.5), 0.0)
1150        }
1151    }
1152}
1153
1154/// MATLAB-compatible surface plot creation utilities
1155pub mod matlab_compat {
1156    use super::*;
1157
1158    /// Create a surface plot (equivalent to MATLAB's `surf(X, Y, Z)`)
1159    pub fn surf(x: Vec<f64>, y: Vec<f64>, z: Vec<Vec<f64>>) -> Result<SurfacePlot, String> {
1160        SurfacePlot::new(x, y, z)
1161    }
1162
1163    /// Create a mesh plot (wireframe surface)
1164    pub fn mesh(x: Vec<f64>, y: Vec<f64>, z: Vec<Vec<f64>>) -> Result<SurfacePlot, String> {
1165        Ok(SurfacePlot::new(x, y, z)?
1166            .with_wireframe(true)
1167            .with_shading(ShadingMode::None))
1168    }
1169
1170    /// Create surface from meshgrid
1171    pub fn meshgrid_surf(
1172        x_range: (f64, f64),
1173        y_range: (f64, f64),
1174        resolution: (usize, usize),
1175        func: impl Fn(f64, f64) -> f64,
1176    ) -> Result<SurfacePlot, String> {
1177        SurfacePlot::from_function(x_range, y_range, resolution, func)
1178    }
1179
1180    /// Create surface with specific colormap
1181    pub fn surf_with_colormap(
1182        x: Vec<f64>,
1183        y: Vec<f64>,
1184        z: Vec<Vec<f64>>,
1185        colormap: &str,
1186    ) -> Result<SurfacePlot, String> {
1187        let cmap =
1188            ColorMap::from_name(colormap).ok_or_else(|| format!("Unknown colormap: {colormap}"))?;
1189
1190        Ok(SurfacePlot::new(x, y, z)?.with_colormap(cmap))
1191    }
1192}
1193
1194#[cfg(test)]
1195mod tests {
1196    use super::*;
1197
1198    #[test]
1199    fn test_surface_plot_creation() {
1200        let x = vec![0.0, 1.0, 2.0];
1201        let y = vec![0.0, 1.0];
1202        let z = vec![vec![0.0, 1.0], vec![1.0, 2.0], vec![2.0, 3.0]];
1203
1204        let surface = SurfacePlot::new(x, y, z).unwrap();
1205
1206        assert_eq!(surface.x_data.len(), 3);
1207        assert_eq!(surface.y_data.len(), 2);
1208        let rows = surface.z_data.as_ref().unwrap();
1209        assert_eq!(rows.len(), 3);
1210        assert_eq!(rows[0].len(), 2);
1211        assert!(surface.visible);
1212    }
1213
1214    #[test]
1215    fn test_surface_from_function() {
1216        let surface =
1217            SurfacePlot::from_function((-2.0, 2.0), (-2.0, 2.0), (10, 10), |x, y| x * x + y * y)
1218                .unwrap();
1219
1220        assert_eq!(surface.x_data.len(), 10);
1221        assert_eq!(surface.y_data.len(), 10);
1222        let rows = surface.z_data.as_ref().unwrap();
1223        assert_eq!(rows.len(), 10);
1224
1225        // Check that function is evaluated correctly
1226        assert_eq!(rows[0][0], 8.0); // (-2)^2 + (-2)^2 = 8
1227    }
1228
1229    #[test]
1230    fn test_surface_validation() {
1231        let x = vec![0.0, 1.0];
1232        let y = vec![0.0, 1.0, 2.0];
1233        let z = vec![
1234            vec![0.0, 1.0], // Wrong: should have 3 elements to match y
1235            vec![1.0, 2.0],
1236        ];
1237
1238        assert!(SurfacePlot::new(x, y, z).is_err());
1239    }
1240
1241    #[test]
1242    fn test_surface_styling() {
1243        let x = vec![0.0, 1.0];
1244        let y = vec![0.0, 1.0];
1245        let z = vec![vec![0.0, 1.0], vec![1.0, 2.0]];
1246
1247        let surface = SurfacePlot::new(x, y, z)
1248            .unwrap()
1249            .with_colormap(ColorMap::Hot)
1250            .with_wireframe(true)
1251            .with_alpha(0.8)
1252            .with_label("Test Surface");
1253
1254        assert_eq!(surface.colormap, ColorMap::Hot);
1255        assert!(surface.wireframe);
1256        assert_eq!(surface.alpha, 0.8);
1257        assert_eq!(surface.label, Some("Test Surface".to_string()));
1258    }
1259
1260    #[test]
1261    fn image_mode_surface_uses_textured_render_data() {
1262        let x = vec![0.0, 1.0];
1263        let y = vec![10.0, 20.0];
1264        let z = vec![vec![0.0, 0.25], vec![0.75, 1.0]];
1265
1266        let mut surface = SurfacePlot::new(x, y, z)
1267            .unwrap()
1268            .with_image_mode(true)
1269            .with_colormap(ColorMap::Gray)
1270            .with_color_limits(Some((0.0, 1.0)));
1271        let render_data = surface.render_data();
1272
1273        assert_eq!(render_data.pipeline_type, PipelineType::Textured);
1274        assert_eq!(render_data.vertices.len(), 4);
1275        assert_eq!(
1276            render_data.indices.as_deref(),
1277            Some(&[0, 1, 2, 0, 2, 3][..])
1278        );
1279
1280        let Some(ImageData::Rgba8 {
1281            width,
1282            height,
1283            data,
1284        }) = render_data.image
1285        else {
1286            panic!("image-mode surfaces should carry an RGBA texture payload");
1287        };
1288        assert_eq!((width, height), (2, 2));
1289        assert_eq!(data.len(), 16);
1290
1291        // Image data is row-major, top-to-bottom. The top image row corresponds to
1292        // the highest Y data row.
1293        assert_eq!(&data[0..4], &[64, 64, 64, 255]);
1294        assert_eq!(&data[4..8], &[255, 255, 255, 255]);
1295        assert_eq!(&data[8..12], &[0, 0, 0, 255]);
1296        assert_eq!(&data[12..16], &[191, 191, 191, 255]);
1297    }
1298
1299    #[test]
1300    fn test_colormap_mapping() {
1301        let jet = ColorMap::Jet;
1302
1303        // Test boundary values
1304        let color_0 = jet.map_value(0.0);
1305        let color_1 = jet.map_value(1.0);
1306
1307        assert!(color_0.x >= 0.0 && color_0.x <= 1.0);
1308        assert!(color_1.x >= 0.0 && color_1.x <= 1.0);
1309
1310        // Test that different values give different colors
1311        let color_mid = jet.map_value(0.5);
1312        assert_ne!(color_0, color_mid);
1313        assert_ne!(color_mid, color_1);
1314    }
1315
1316    #[test]
1317    fn test_surface_statistics() {
1318        let x = vec![0.0, 1.0, 2.0, 3.0];
1319        let y = vec![0.0, 1.0, 2.0];
1320        let z = vec![
1321            vec![0.0, 1.0, 2.0],
1322            vec![1.0, 2.0, 3.0],
1323            vec![2.0, 3.0, 4.0],
1324            vec![3.0, 4.0, 5.0],
1325        ];
1326
1327        let surface = SurfacePlot::new(x, y, z).unwrap();
1328        let stats = surface.statistics();
1329
1330        assert_eq!(stats.grid_points, 12); // 4 * 3
1331        assert_eq!(stats.triangle_count, 12); // (4-1) * (3-1) * 2
1332        assert_eq!(stats.x_resolution, 4);
1333        assert_eq!(stats.y_resolution, 3);
1334        assert!(stats.memory_usage > 0);
1335    }
1336
1337    #[test]
1338    fn test_matlab_compat() {
1339        use super::matlab_compat::*;
1340
1341        let x = vec![0.0, 1.0];
1342        let y = vec![0.0, 1.0];
1343        let z = vec![vec![0.0, 1.0], vec![1.0, 2.0]];
1344
1345        let surface = surf(x.clone(), y.clone(), z.clone()).unwrap();
1346        assert!(!surface.wireframe);
1347
1348        let mesh_plot = mesh(x.clone(), y.clone(), z.clone()).unwrap();
1349        assert!(mesh_plot.wireframe);
1350
1351        let colormap_surface = surf_with_colormap(x, y, z, "viridis").unwrap();
1352        assert_eq!(colormap_surface.colormap, ColorMap::Viridis);
1353    }
1354
1355    #[test]
1356    fn colormap_from_name_accepts_canonical_names_and_aliases() {
1357        let cases = [
1358            ("parula", ColorMap::Parula),
1359            ("viridis", ColorMap::Viridis),
1360            ("plasma", ColorMap::Plasma),
1361            ("inferno", ColorMap::Inferno),
1362            ("magma", ColorMap::Magma),
1363            ("turbo", ColorMap::Turbo),
1364            ("jet", ColorMap::Jet),
1365            ("hot", ColorMap::Hot),
1366            ("cool", ColorMap::Cool),
1367            ("spring", ColorMap::Spring),
1368            ("summer", ColorMap::Summer),
1369            ("autumn", ColorMap::Autumn),
1370            ("winter", ColorMap::Winter),
1371            ("gray", ColorMap::Gray),
1372            ("grey", ColorMap::Gray),
1373            ("bone", ColorMap::Bone),
1374            ("copper", ColorMap::Copper),
1375            ("pink", ColorMap::Pink),
1376            ("lines", ColorMap::Lines),
1377        ];
1378
1379        for (name, expected) in cases {
1380            assert_eq!(ColorMap::from_name(name), Some(expected), "{name}");
1381        }
1382        for name in ColorMap::CANONICAL_NAMES
1383            .iter()
1384            .chain(ColorMap::ALIASES.iter())
1385            .copied()
1386        {
1387            assert!(
1388                ColorMap::from_name(name).is_some(),
1389                "colormap table entry should parse: {name}"
1390            );
1391        }
1392    }
1393
1394    #[test]
1395    fn colormap_from_name_normalizes_and_rejects_unknown_names() {
1396        assert_eq!(ColorMap::from_name(" Turbo "), Some(ColorMap::Turbo));
1397        assert_eq!(ColorMap::from_name("GREY"), Some(ColorMap::Gray));
1398        assert_eq!(ColorMap::from_name("hsv"), None);
1399        assert!(!ColorMap::CANONICAL_NAMES.contains(&"hsv"));
1400        assert!(!ColorMap::ALIASES.contains(&"hsv"));
1401        assert_eq!(ColorMap::from_name("not-a-colormap"), None);
1402    }
1403}