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::fmt::Write;
13use std::sync::Arc;
14
15#[derive(Debug, Clone)]
17pub struct SurfacePlot {
18 pub x_data: Vec<f64>,
20 pub y_data: Vec<f64>,
21 pub z_data: Option<Vec<Vec<f64>>>, pub x_grid: Option<Vec<Vec<f64>>>,
24 pub y_grid: Option<Vec<Vec<f64>>>,
25 x_len: usize,
27 y_len: usize,
28
29 pub colormap: ColorMap,
31 pub shading_mode: ShadingMode,
32 pub wireframe: bool,
33 pub alpha: f32,
34 pub flatten_z: bool,
36
37 pub image_mode: bool,
39
40 pub color_limits: Option<(f64, f64)>,
42
43 pub color_grid: Option<Vec<Vec<Vec4>>>, 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 pub label: Option<String>,
55 pub visible: bool,
56
57 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#[derive(Clone, PartialEq)]
90pub enum ColorMap {
91 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 Viridis,
107 Plasma,
108 Inferno,
109 Magma,
110 Turbo,
111
112 Parula,
114
115 ColorCube,
117
118 Custom(Vec4, Vec4), 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#[derive(Debug, Clone, Copy, PartialEq)]
282pub enum ShadingMode {
283 Flat,
285 Smooth,
287 Faceted,
289 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 pub fn new(x_data: Vec<f64>, y_data: Vec<f64>, z_data: Vec<Vec<f64>>) -> Result<Self, String> {
396 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 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 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 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 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 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 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 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 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 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 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 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 pub fn with_label<S: Into<String>>(mut self, label: S) -> Self {
733 self.label = Some(label.into());
734 self
735 }
736
737 pub fn len(&self) -> usize {
739 self.x_len * self.y_len
740 }
741
742 pub fn is_empty(&self) -> bool {
744 self.x_len == 0 || self.y_len == 0
745 }
746
747 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 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 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 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 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 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 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 let normal = Vec3::new(0.0, 0.0, 1.0); 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 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 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 indices.push(base);
970 indices.push(next_row);
971 indices.push(base + 1);
972
973 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 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 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 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#[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 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 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 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 fn cool_colormap(&self, t: f32) -> Vec3 {
1302 Vec3::new(t, 1.0 - t, 1.0)
1303 }
1304
1305 fn viridis_colormap(&self, t: f32) -> Vec3 {
1307 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 fn plasma_colormap(&self, t: f32) -> Vec3 {
1316 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 fn spring_colormap(&self, t: f32) -> Vec3 {
1325 Vec3::new(1.0, t, 1.0 - t)
1326 }
1327
1328 fn summer_colormap(&self, t: f32) -> Vec3 {
1330 Vec3::new(t, 0.5 + 0.5 * t, 0.4)
1331 }
1332
1333 fn autumn_colormap(&self, t: f32) -> Vec3 {
1335 Vec3::new(1.0, t, 0.0)
1336 }
1337
1338 fn winter_colormap(&self, t: f32) -> Vec3 {
1340 Vec3::new(0.0, t, 1.0 - 0.5 * t)
1341 }
1342
1343 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 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 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 fn lines_colormap(&self, t: f32) -> Vec3 {
1377 let _phase = (t * 7.0) % 1.0; let index = (t * 7.0) as usize % 7;
1379 match index {
1380 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), }
1388 }
1389
1390 fn inferno_colormap(&self, t: f32) -> Vec3 {
1392 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 fn magma_colormap(&self, t: f32) -> Vec3 {
1401 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 fn turbo_colormap(&self, t: f32) -> Vec3 {
1410 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 fn parula_colormap(&self, t: f32) -> Vec3 {
1437 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 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 #[allow(dead_code)] fn default_colormap(&self, t: f32) -> Vec3 {
1488 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
1497pub mod matlab_compat {
1499 use super::*;
1500
1501 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 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 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 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 assert_eq!(rows[0][0], 8.0); }
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], 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 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, 0, 0, 0, 255, 102, 102, 102, 255, 204, 204, 204, 255, ]
1670 );
1671 }
1672
1673 #[test]
1674 fn test_colormap_mapping() {
1675 let jet = ColorMap::Jet;
1676
1677 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 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); assert_eq!(stats.triangle_count, 12); 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}