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