1use 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#[derive(Debug, Clone)]
16pub struct SurfacePlot {
17 pub x_data: Vec<f64>,
19 pub y_data: Vec<f64>,
20 pub z_data: Option<Vec<Vec<f64>>>, x_len: usize,
23 y_len: usize,
24
25 pub colormap: ColorMap,
27 pub shading_mode: ShadingMode,
28 pub wireframe: bool,
29 pub alpha: f32,
30 pub flatten_z: bool,
32
33 pub image_mode: bool,
35
36 pub color_limits: Option<(f64, f64)>,
38
39 pub color_grid: Option<Vec<Vec<Vec4>>>, 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 pub label: Option<String>,
51 pub visible: bool,
52
53 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#[derive(Debug, Clone, Copy, PartialEq)]
86pub enum ColorMap {
87 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 Viridis,
103 Plasma,
104 Inferno,
105 Magma,
106 Turbo,
107
108 Parula,
110
111 Custom(Vec4, Vec4), }
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#[derive(Debug, Clone, Copy, PartialEq)]
150pub enum ShadingMode {
151 Flat,
153 Smooth,
155 Faceted,
157 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 pub fn new(x_data: Vec<f64>, y_data: Vec<f64>, z_data: Vec<Vec<f64>>) -> Result<Self, String> {
264 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 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 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 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 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 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 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 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 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 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 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 pub fn with_label<S: Into<String>>(mut self, label: S) -> Self {
485 self.label = Some(label.into());
486 self
487 }
488
489 pub fn len(&self) -> usize {
491 self.x_len * self.y_len
492 }
493
494 pub fn is_empty(&self) -> bool {
496 self.x_len == 0 || self.y_len == 0
497 }
498
499 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 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 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 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 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 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 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 let normal = Vec3::new(0.0, 0.0, 1.0); 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 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 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 indices.push(base);
688 indices.push(next_row);
689 indices.push(base + 1);
690
691 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 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 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 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#[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 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 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 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 fn cool_colormap(&self, t: f32) -> Vec3 {
972 Vec3::new(t, 1.0 - t, 1.0)
973 }
974
975 fn viridis_colormap(&self, t: f32) -> Vec3 {
977 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 fn plasma_colormap(&self, t: f32) -> Vec3 {
986 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 fn spring_colormap(&self, t: f32) -> Vec3 {
995 Vec3::new(1.0, t, 1.0 - t)
996 }
997
998 fn summer_colormap(&self, t: f32) -> Vec3 {
1000 Vec3::new(t, 0.5 + 0.5 * t, 0.4)
1001 }
1002
1003 fn autumn_colormap(&self, t: f32) -> Vec3 {
1005 Vec3::new(1.0, t, 0.0)
1006 }
1007
1008 fn winter_colormap(&self, t: f32) -> Vec3 {
1010 Vec3::new(0.0, t, 1.0 - 0.5 * t)
1011 }
1012
1013 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 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 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 fn lines_colormap(&self, t: f32) -> Vec3 {
1047 let _phase = (t * 7.0) % 1.0; let index = (t * 7.0) as usize % 7;
1049 match index {
1050 0 => Vec3::new(0.0, 0.0, 1.0), 1 => Vec3::new(0.0, 0.5, 0.0), 2 => Vec3::new(1.0, 0.0, 0.0), 3 => Vec3::new(0.0, 0.75, 0.75), 4 => Vec3::new(0.75, 0.0, 0.75), 5 => Vec3::new(0.75, 0.75, 0.0), _ => Vec3::new(0.25, 0.25, 0.25), }
1058 }
1059
1060 fn inferno_colormap(&self, t: f32) -> Vec3 {
1062 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 fn magma_colormap(&self, t: f32) -> Vec3 {
1071 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 fn turbo_colormap(&self, t: f32) -> Vec3 {
1080 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 fn parula_colormap(&self, t: f32) -> Vec3 {
1107 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 #[allow(dead_code)] fn default_colormap(&self, t: f32) -> Vec3 {
1145 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
1154pub mod matlab_compat {
1156 use super::*;
1157
1158 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 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 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 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 assert_eq!(rows[0][0], 8.0); }
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], 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 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 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 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); assert_eq!(stats.triangle_count, 12); 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}