1use crate::core::{
6 BoundingBox, DrawCall, GpuVertexBuffer, Material, PipelineType, RenderData, Vertex,
7};
8use glam::{Vec3, Vec4};
9
10#[derive(Debug, Clone)]
12pub struct SurfacePlot {
13 pub x_data: Vec<f64>,
15 pub y_data: Vec<f64>,
16 pub z_data: Option<Vec<Vec<f64>>>, x_len: usize,
19 y_len: usize,
20
21 pub colormap: ColorMap,
23 pub shading_mode: ShadingMode,
24 pub wireframe: bool,
25 pub alpha: f32,
26 pub flatten_z: bool,
28
29 pub image_mode: bool,
31
32 pub color_limits: Option<(f64, f64)>,
34
35 pub color_grid: Option<Vec<Vec<Vec4>>>, pub lighting_enabled: bool,
40 pub ambient_strength: f32,
41 pub diffuse_strength: f32,
42 pub specular_strength: f32,
43 pub shininess: f32,
44
45 pub label: Option<String>,
47 pub visible: bool,
48
49 vertices: Option<Vec<Vertex>>,
51 indices: Option<Vec<u32>>,
52 bounds: Option<BoundingBox>,
53 dirty: bool,
54 gpu_vertices: Option<GpuVertexBuffer>,
55 gpu_vertex_count: Option<usize>,
56 gpu_bounds: Option<BoundingBox>,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq)]
61pub enum ColorMap {
62 Jet,
64 Hot,
65 Cool,
66 Spring,
67 Summer,
68 Autumn,
69 Winter,
70 Gray,
71 Bone,
72 Copper,
73 Pink,
74 Lines,
75
76 Viridis,
78 Plasma,
79 Inferno,
80 Magma,
81 Turbo,
82
83 Parula,
85
86 Custom(Vec4, Vec4), }
89
90impl ColorMap {
91 pub const CANONICAL_NAMES: &[&str] = &[
92 "parula", "viridis", "plasma", "inferno", "magma", "turbo", "jet", "hot", "cool", "spring",
93 "summer", "autumn", "winter", "gray", "bone", "copper", "pink", "lines",
94 ];
95
96 pub const ALIASES: &[&str] = &["grey"];
97
98 pub fn from_name(name: &str) -> Option<Self> {
99 match name.trim().to_ascii_lowercase().as_str() {
100 "parula" => Some(Self::Parula),
101 "viridis" => Some(Self::Viridis),
102 "plasma" => Some(Self::Plasma),
103 "inferno" => Some(Self::Inferno),
104 "magma" => Some(Self::Magma),
105 "turbo" => Some(Self::Turbo),
106 "jet" => Some(Self::Jet),
107 "hot" => Some(Self::Hot),
108 "cool" => Some(Self::Cool),
109 "spring" => Some(Self::Spring),
110 "summer" => Some(Self::Summer),
111 "autumn" => Some(Self::Autumn),
112 "winter" => Some(Self::Winter),
113 "gray" | "grey" => Some(Self::Gray),
114 "bone" => Some(Self::Bone),
115 "copper" => Some(Self::Copper),
116 "pink" => Some(Self::Pink),
117 "lines" => Some(Self::Lines),
118 _ => None,
119 }
120 }
121}
122
123#[derive(Debug, Clone, Copy, PartialEq)]
125pub enum ShadingMode {
126 Flat,
128 Smooth,
130 Faceted,
132 None,
134}
135
136impl Default for ColorMap {
137 fn default() -> Self {
138 Self::Viridis
139 }
140}
141
142impl Default for ShadingMode {
143 fn default() -> Self {
144 Self::Smooth
145 }
146}
147
148impl SurfacePlot {
149 pub fn new(x_data: Vec<f64>, y_data: Vec<f64>, z_data: Vec<Vec<f64>>) -> Result<Self, String> {
151 if z_data.len() != x_data.len() {
153 return Err(format!(
154 "Z data rows ({}) must match X data length ({})",
155 z_data.len(),
156 x_data.len()
157 ));
158 }
159
160 for (i, row) in z_data.iter().enumerate() {
161 if row.len() != y_data.len() {
162 return Err(format!(
163 "Z data row {} length ({}) must match Y data length ({})",
164 i,
165 row.len(),
166 y_data.len()
167 ));
168 }
169 }
170
171 Ok(Self {
172 x_len: x_data.len(),
173 y_len: y_data.len(),
174 x_data,
175 y_data,
176 z_data: Some(z_data),
177 colormap: ColorMap::default(),
178 shading_mode: ShadingMode::default(),
179 wireframe: false,
180 alpha: 1.0,
181 flatten_z: false,
182 image_mode: false,
183 color_limits: None,
184 color_grid: None,
185 lighting_enabled: true,
186 ambient_strength: 0.2,
187 diffuse_strength: 0.8,
188 specular_strength: 0.5,
189 shininess: 32.0,
190 label: None,
191 visible: true,
192 vertices: None,
193 indices: None,
194 bounds: None,
195 dirty: true,
196 gpu_vertices: None,
197 gpu_vertex_count: None,
198 gpu_bounds: None,
199 })
200 }
201
202 pub fn from_gpu_buffer(
204 x_len: usize,
205 y_len: usize,
206 buffer: GpuVertexBuffer,
207 vertex_count: usize,
208 bounds: BoundingBox,
209 ) -> Self {
210 Self {
211 x_data: Vec::new(),
212 y_data: Vec::new(),
213 z_data: None,
214 x_len,
215 y_len,
216 colormap: ColorMap::default(),
217 shading_mode: ShadingMode::default(),
218 wireframe: false,
219 alpha: 1.0,
220 flatten_z: false,
221 image_mode: false,
222 color_limits: None,
223 color_grid: None,
224 lighting_enabled: true,
225 ambient_strength: 0.2,
226 diffuse_strength: 0.8,
227 specular_strength: 0.5,
228 shininess: 32.0,
229 label: None,
230 visible: true,
231 vertices: None,
232 indices: None,
233 bounds: Some(bounds),
234 dirty: false,
235 gpu_vertices: Some(buffer),
236 gpu_vertex_count: Some(vertex_count),
237 gpu_bounds: Some(bounds),
238 }
239 }
240
241 fn drop_gpu_if_possible(&mut self) {
242 if self.gpu_vertices.is_some() && self.z_data.is_some() {
243 self.invalidate_gpu_data();
244 }
245 }
246
247 pub fn from_function<F>(
249 x_range: (f64, f64),
250 y_range: (f64, f64),
251 resolution: (usize, usize),
252 func: F,
253 ) -> Result<Self, String>
254 where
255 F: Fn(f64, f64) -> f64,
256 {
257 let (x_res, y_res) = resolution;
258 if x_res < 2 || y_res < 2 {
259 return Err("Resolution must be at least 2x2".to_string());
260 }
261
262 let x_data: Vec<f64> = (0..x_res)
263 .map(|i| x_range.0 + (x_range.1 - x_range.0) * i as f64 / (x_res - 1) as f64)
264 .collect();
265
266 let y_data: Vec<f64> = (0..y_res)
267 .map(|j| y_range.0 + (y_range.1 - y_range.0) * j as f64 / (y_res - 1) as f64)
268 .collect();
269
270 let z_data: Vec<Vec<f64>> = x_data
271 .iter()
272 .map(|&x| y_data.iter().map(|&y| func(x, y)).collect())
273 .collect();
274
275 Self::new(x_data, y_data, z_data)
276 }
277
278 fn invalidate_gpu_data(&mut self) {
279 self.gpu_vertices = None;
280 self.gpu_vertex_count = None;
281 self.gpu_bounds = None;
282 }
283
284 pub fn with_colormap(mut self, colormap: ColorMap) -> Self {
286 self.colormap = colormap;
287 self.dirty = true;
288 self.drop_gpu_if_possible();
289 self
290 }
291
292 pub fn with_shading(mut self, shading: ShadingMode) -> Self {
294 self.shading_mode = shading;
295 self.dirty = true;
296 self.drop_gpu_if_possible();
297 self
298 }
299
300 pub fn with_wireframe(mut self, enabled: bool) -> Self {
302 self.wireframe = enabled;
303 self.dirty = true;
304 self.drop_gpu_if_possible();
305 self
306 }
307
308 pub fn with_alpha(mut self, alpha: f32) -> Self {
310 self.alpha = alpha.clamp(0.0, 1.0);
311 self.dirty = true;
312 self.drop_gpu_if_possible();
313 self
314 }
315
316 pub fn with_flatten_z(mut self, enabled: bool) -> Self {
318 self.flatten_z = enabled;
319 self.dirty = true;
320 self.drop_gpu_if_possible();
321 self
322 }
323
324 pub fn with_image_mode(mut self, enabled: bool) -> Self {
325 self.image_mode = enabled;
326 self.dirty = true;
327 self.drop_gpu_if_possible();
328 self
329 }
330
331 pub fn with_color_limits(mut self, limits: Option<(f64, f64)>) -> Self {
333 self.color_limits = limits;
334 self.dirty = true;
335 self.drop_gpu_if_possible();
336 self
337 }
338
339 pub fn set_color_limits(&mut self, limits: Option<(f64, f64)>) {
341 self.color_limits = limits;
342 self.dirty = true;
343 self.drop_gpu_if_possible();
344 }
345
346 pub fn with_color_grid(mut self, grid: Vec<Vec<Vec4>>) -> Self {
348 self.color_grid = Some(grid);
349 self.dirty = true;
350 self.drop_gpu_if_possible();
351 self
352 }
353
354 pub fn with_label<S: Into<String>>(mut self, label: S) -> Self {
356 self.label = Some(label.into());
357 self
358 }
359
360 pub fn len(&self) -> usize {
362 self.x_len * self.y_len
363 }
364
365 pub fn is_empty(&self) -> bool {
367 self.x_len == 0 || self.y_len == 0
368 }
369
370 pub fn bounds(&mut self) -> BoundingBox {
372 if self.dirty || self.bounds.is_none() {
373 self.compute_bounds();
374 }
375 self.bounds.unwrap()
376 }
377
378 fn compute_bounds(&mut self) {
380 if let Some(bounds) = self.gpu_bounds {
381 self.bounds = Some(bounds);
382 return;
383 }
384
385 let mut min_x = f32::INFINITY;
386 let mut max_x = f32::NEG_INFINITY;
387 let mut min_y = f32::INFINITY;
388 let mut max_y = f32::NEG_INFINITY;
389 let mut min_z = f32::INFINITY;
390 let mut max_z = f32::NEG_INFINITY;
391
392 for &x in &self.x_data {
393 min_x = min_x.min(x as f32);
394 max_x = max_x.max(x as f32);
395 }
396
397 for &y in &self.y_data {
398 min_y = min_y.min(y as f32);
399 max_y = max_y.max(y as f32);
400 }
401
402 if let Some(rows) = &self.z_data {
403 for row in rows {
404 for &z in row {
405 if z.is_finite() {
406 min_z = min_z.min(z as f32);
407 max_z = max_z.max(z as f32);
408 }
409 }
410 }
411 }
412
413 self.bounds = Some(BoundingBox::new(
414 Vec3::new(min_x, min_y, min_z),
415 Vec3::new(max_x, max_y, max_z),
416 ));
417 }
418
419 pub fn statistics(&self) -> SurfaceStatistics {
421 let grid_size = self.x_len * self.y_len;
422 let triangle_count = if self.x_len > 1 && self.y_len > 1 {
423 (self.x_len - 1) * (self.y_len - 1) * 2
424 } else {
425 0
426 };
427
428 SurfaceStatistics {
429 grid_points: grid_size,
430 triangle_count,
431 x_resolution: self.x_len,
432 y_resolution: self.y_len,
433 memory_usage: self.estimated_memory_usage(),
434 }
435 }
436
437 pub fn estimated_memory_usage(&self) -> usize {
439 let data_size = std::mem::size_of::<f64>()
440 * (self.x_data.len()
441 + self.y_data.len()
442 + self
443 .z_data
444 .as_ref()
445 .map_or(0, |z| z.len() * self.y_data.len()));
446
447 let vertices_size = self
448 .vertices
449 .as_ref()
450 .map_or(0, |v| v.len() * std::mem::size_of::<Vertex>());
451
452 let indices_size = self
453 .indices
454 .as_ref()
455 .map_or(0, |i| i.len() * std::mem::size_of::<u32>());
456
457 let gpu_size = self.gpu_vertex_count.unwrap_or(0) * std::mem::size_of::<Vertex>();
458
459 data_size + vertices_size + indices_size + gpu_size
460 }
461
462 fn generate_vertices(&mut self) -> &Vec<Vertex> {
464 if self.gpu_vertices.is_some() {
465 if self.vertices.is_none() {
466 self.vertices = Some(Vec::new());
467 }
468 return self.vertices.as_ref().unwrap();
469 }
470
471 if self.dirty || self.vertices.is_none() {
472 log::trace!(
473 target: "runmat_plot",
474 "surface gen vertices {} x {}",
475 self.x_data.len(),
476 self.y_data.len()
477 );
478
479 let mut vertices = Vec::new();
480
481 let z_rows = self
483 .z_data
484 .as_ref()
485 .expect("CPU surface data missing during vertex generation");
486 let (min_z, max_z) = if let Some((lo, hi)) = self.color_limits {
487 (lo, hi)
488 } else {
489 let mut min_z = f64::INFINITY;
490 let mut max_z = f64::NEG_INFINITY;
491 for row in z_rows {
492 for &z in row {
493 if z.is_finite() {
494 min_z = min_z.min(z);
495 max_z = max_z.max(z);
496 }
497 }
498 }
499 (min_z, max_z)
500 };
501 let z_range = (max_z - min_z).max(f64::MIN_POSITIVE);
502
503 for (i, &x) in self.x_data.iter().enumerate() {
505 for (j, &y) in self.y_data.iter().enumerate() {
506 let z = z_rows[i][j];
507 let z_pos = if self.flatten_z { 0.0 } else { z as f32 };
508 let position = Vec3::new(x as f32, y as f32, z_pos);
509
510 let normal = Vec3::new(0.0, 0.0, 1.0); let color = if let Some(grid) = &self.color_grid {
515 let c = grid[i][j];
516 Vec4::new(c.x, c.y, c.z, c.w)
517 } else {
518 let t = ((z - min_z) / z_range) as f32;
519 let color_rgb = self.colormap.map_value(t.clamp(0.0, 1.0));
520 Vec4::new(color_rgb.x, color_rgb.y, color_rgb.z, self.alpha)
521 };
522
523 vertices.push(Vertex {
524 position: position.to_array(),
525 normal: normal.to_array(),
526 color: color.to_array(),
527 tex_coords: [
528 i as f32 / (self.x_data.len() - 1).max(1) as f32,
529 j as f32 / (self.y_data.len() - 1).max(1) as f32,
530 ],
531 });
532 }
533 }
534
535 log::trace!(target: "runmat_plot", "surface vertices={}", vertices.len());
536 self.vertices = Some(vertices);
537 }
538 self.vertices.as_ref().unwrap()
539 }
540
541 fn generate_indices(&mut self) -> &Vec<u32> {
543 if self.dirty || self.indices.is_none() {
544 log::trace!(target: "runmat_plot", "surface generating indices");
545
546 let mut indices = Vec::new();
547 let x_res = self.x_len;
548 let y_res = self.y_len;
549
550 for i in 0..x_res - 1 {
552 for j in 0..y_res - 1 {
553 let base = (i * y_res + j) as u32;
554 let next_row = base + y_res as u32;
555
556 indices.push(base);
559 indices.push(next_row);
560 indices.push(base + 1);
561
562 indices.push(next_row);
564 indices.push(next_row + 1);
565 indices.push(base + 1);
566 }
567 }
568
569 log::trace!(target: "runmat_plot", "surface indices={}", indices.len());
570 self.indices = Some(indices);
571 self.dirty = false;
572 }
573 self.indices.as_ref().unwrap()
574 }
575
576 fn generate_wireframe_indices(&self) -> Vec<u32> {
577 let mut indices = Vec::new();
578 if self.x_len < 2 || self.y_len < 2 {
579 return indices;
580 }
581
582 for i in 0..self.x_len {
584 for j in 0..(self.y_len - 1) {
585 let a = (i * self.y_len + j) as u32;
586 let b = (i * self.y_len + j + 1) as u32;
587 indices.push(a);
588 indices.push(b);
589 }
590 }
591
592 for i in 0..(self.x_len - 1) {
594 for j in 0..self.y_len {
595 let a = (i * self.y_len + j) as u32;
596 let b = ((i + 1) * self.y_len + j) as u32;
597 indices.push(a);
598 indices.push(b);
599 }
600 }
601
602 indices
603 }
604
605 pub fn render_data(&mut self) -> RenderData {
607 log::debug!(
608 target: "runmat_plot",
609 "surface render_data start: {} x {}",
610 self.x_len,
611 self.y_len
612 );
613
614 let using_gpu = self.gpu_vertices.is_some();
615 let bounds = self.bounds();
616 let vertices = if using_gpu {
617 Vec::new()
618 } else {
619 self.generate_vertices().clone()
620 };
621 let indices = if self.wireframe {
622 self.generate_wireframe_indices()
623 } else {
624 self.generate_indices().clone()
625 };
626
627 let material = Material {
628 albedo: Vec4::new(1.0, 1.0, 1.0, self.alpha),
629 ..Default::default()
630 };
631
632 let vertex_count = if using_gpu {
633 self.gpu_vertex_count.unwrap_or(0)
634 } else {
635 vertices.len()
636 };
637
638 log::debug!(
639 target: "runmat_plot",
640 "surface render_data generated: vertex_count={} (gpu={}), indices={}",
641 vertex_count,
642 using_gpu,
643 indices.len()
644 );
645
646 let draw_call = DrawCall {
647 vertex_offset: 0,
648 vertex_count,
649 index_offset: Some(0),
650 index_count: Some(indices.len()),
651 instance_count: 1,
652 };
653
654 log::trace!(target: "runmat_plot", "surface render_data done");
655
656 RenderData {
657 pipeline_type: if self.wireframe {
658 PipelineType::Lines
659 } else {
660 PipelineType::Triangles
661 },
662 vertices,
663 indices: Some(indices),
664
665 gpu_vertices: self.gpu_vertices.clone(),
666 bounds: Some(bounds),
667 material,
668 draw_calls: vec![draw_call],
669 image: None,
670 }
671 }
672}
673
674#[derive(Debug, Clone)]
676pub struct SurfaceStatistics {
677 pub grid_points: usize,
678 pub triangle_count: usize,
679 pub x_resolution: usize,
680 pub y_resolution: usize,
681 pub memory_usage: usize,
682}
683
684impl ColorMap {
685 pub fn map_value(&self, t: f32) -> Vec3 {
687 let t = t.clamp(0.0, 1.0);
688
689 match self {
690 ColorMap::Jet => self.jet_colormap(t),
691 ColorMap::Hot => self.hot_colormap(t),
692 ColorMap::Cool => self.cool_colormap(t),
693 ColorMap::Spring => self.spring_colormap(t),
694 ColorMap::Summer => self.summer_colormap(t),
695 ColorMap::Autumn => self.autumn_colormap(t),
696 ColorMap::Winter => self.winter_colormap(t),
697 ColorMap::Gray => Vec3::splat(t),
698 ColorMap::Bone => self.bone_colormap(t),
699 ColorMap::Copper => self.copper_colormap(t),
700 ColorMap::Pink => self.pink_colormap(t),
701 ColorMap::Lines => self.lines_colormap(t),
702 ColorMap::Viridis => self.viridis_colormap(t),
703 ColorMap::Plasma => self.plasma_colormap(t),
704 ColorMap::Inferno => self.inferno_colormap(t),
705 ColorMap::Magma => self.magma_colormap(t),
706 ColorMap::Turbo => self.turbo_colormap(t),
707 ColorMap::Parula => self.parula_colormap(t),
708 ColorMap::Custom(min_color, max_color) => {
709 min_color.truncate().lerp(max_color.truncate(), t)
710 }
711 }
712 }
713
714 fn jet_colormap(&self, t: f32) -> Vec3 {
716 let r = (1.5 - 4.0 * (t - 0.75).abs()).clamp(0.0, 1.0);
717 let g = (1.5 - 4.0 * (t - 0.5).abs()).clamp(0.0, 1.0);
718 let b = (1.5 - 4.0 * (t - 0.25).abs()).clamp(0.0, 1.0);
719 Vec3::new(r, g, b)
720 }
721
722 fn hot_colormap(&self, t: f32) -> Vec3 {
724 if t < 1.0 / 3.0 {
725 Vec3::new(3.0 * t, 0.0, 0.0)
726 } else if t < 2.0 / 3.0 {
727 Vec3::new(1.0, 3.0 * t - 1.0, 0.0)
728 } else {
729 Vec3::new(1.0, 1.0, 3.0 * t - 2.0)
730 }
731 }
732
733 fn cool_colormap(&self, t: f32) -> Vec3 {
735 Vec3::new(t, 1.0 - t, 1.0)
736 }
737
738 fn viridis_colormap(&self, t: f32) -> Vec3 {
740 let r = (0.267004 + t * (0.993248 - 0.267004)).clamp(0.0, 1.0);
742 let g = (0.004874 + t * (0.906157 - 0.004874)).clamp(0.0, 1.0);
743 let b = (0.329415 + t * (0.143936 - 0.329415) + t * t * 0.5).clamp(0.0, 1.0);
744 Vec3::new(r, g, b)
745 }
746
747 fn plasma_colormap(&self, t: f32) -> Vec3 {
749 let r = (0.050383 + t * (0.940015 - 0.050383)).clamp(0.0, 1.0);
751 let g = (0.029803 + t * (0.975158 - 0.029803) * (1.0 - t)).clamp(0.0, 1.0);
752 let b = (0.527975 + t * (0.131326 - 0.527975)).clamp(0.0, 1.0);
753 Vec3::new(r, g, b)
754 }
755
756 fn spring_colormap(&self, t: f32) -> Vec3 {
758 Vec3::new(1.0, t, 1.0 - t)
759 }
760
761 fn summer_colormap(&self, t: f32) -> Vec3 {
763 Vec3::new(t, 0.5 + 0.5 * t, 0.4)
764 }
765
766 fn autumn_colormap(&self, t: f32) -> Vec3 {
768 Vec3::new(1.0, t, 0.0)
769 }
770
771 fn winter_colormap(&self, t: f32) -> Vec3 {
773 Vec3::new(0.0, t, 1.0 - 0.5 * t)
774 }
775
776 fn bone_colormap(&self, t: f32) -> Vec3 {
778 if t < 3.0 / 8.0 {
779 Vec3::new(7.0 / 8.0 * t, 7.0 / 8.0 * t, 29.0 / 24.0 * t)
780 } else {
781 Vec3::new(
782 (29.0 + 7.0 * t) / 24.0,
783 (29.0 + 7.0 * t) / 24.0,
784 (29.0 + 7.0 * t) / 24.0,
785 )
786 }
787 }
788
789 fn copper_colormap(&self, t: f32) -> Vec3 {
791 Vec3::new((1.25 * t).min(1.0), 0.7812 * t, 0.4975 * t)
792 }
793
794 fn pink_colormap(&self, t: f32) -> Vec3 {
796 let sqrt_t = t.sqrt();
797 if t < 3.0 / 8.0 {
798 Vec3::new(14.0 / 9.0 * sqrt_t, 2.0 / 3.0 * sqrt_t, 2.0 / 3.0 * sqrt_t)
799 } else {
800 Vec3::new(
801 2.0 * sqrt_t - 1.0 / 3.0,
802 8.0 / 9.0 * sqrt_t + 1.0 / 3.0,
803 8.0 / 9.0 * sqrt_t + 1.0 / 3.0,
804 )
805 }
806 }
807
808 fn lines_colormap(&self, t: f32) -> Vec3 {
810 let _phase = (t * 7.0) % 1.0; let index = (t * 7.0) as usize % 7;
812 match index {
813 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), }
821 }
822
823 fn inferno_colormap(&self, t: f32) -> Vec3 {
825 let r = (0.001462 + t * (0.988362 - 0.001462)).clamp(0.0, 1.0);
827 let g = (0.000466 + t * t * (0.982895 - 0.000466)).clamp(0.0, 1.0);
828 let b = (0.013866 + t * (1.0 - t) * (0.416065 - 0.013866)).clamp(0.0, 1.0);
829 Vec3::new(r, g, b)
830 }
831
832 fn magma_colormap(&self, t: f32) -> Vec3 {
834 let r = (0.001462 + t * (0.987053 - 0.001462)).clamp(0.0, 1.0);
836 let g = (0.000466 + t * t * (0.991438 - 0.000466)).clamp(0.0, 1.0);
837 let b = (0.013866 + t * (0.644237 - 0.013866) * (1.0 - t)).clamp(0.0, 1.0);
838 Vec3::new(r, g, b)
839 }
840
841 fn turbo_colormap(&self, t: f32) -> Vec3 {
843 let r = if t < 0.5 {
845 (0.13 + 0.87 * (2.0 * t).powf(0.25)).clamp(0.0, 1.0)
846 } else {
847 (0.8685 + 0.1315 * (2.0 * (1.0 - t)).powf(0.25)).clamp(0.0, 1.0)
848 };
849
850 let g = if t < 0.25 {
851 4.0 * t
852 } else if t < 0.75 {
853 1.0
854 } else {
855 1.0 - 4.0 * (t - 0.75)
856 }
857 .clamp(0.0, 1.0);
858
859 let b = if t < 0.5 {
860 (0.8 * (1.0 - 2.0 * t).powf(0.25)).clamp(0.0, 1.0)
861 } else {
862 (0.1 + 0.9 * (2.0 * t - 1.0).powf(0.25)).clamp(0.0, 1.0)
863 };
864
865 Vec3::new(r, g, b)
866 }
867
868 fn parula_colormap(&self, t: f32) -> Vec3 {
870 let r = if t < 0.25 {
872 0.2081 * (1.0 - t)
873 } else if t < 0.5 {
874 t - 0.25
875 } else if t < 0.75 {
876 1.0
877 } else {
878 1.0 - 0.5 * (t - 0.75)
879 }
880 .clamp(0.0, 1.0);
881
882 let g = if t < 0.125 {
883 0.1663 * t / 0.125
884 } else if t < 0.375 {
885 0.1663 + (0.7079 - 0.1663) * (t - 0.125) / 0.25
886 } else if t < 0.625 {
887 0.7079 + (0.9839 - 0.7079) * (t - 0.375) / 0.25
888 } else {
889 0.9839 * (1.0 - (t - 0.625) / 0.375)
890 }
891 .clamp(0.0, 1.0);
892
893 let b = if t < 0.25 {
894 0.5 + 0.5 * t / 0.25
895 } else if t < 0.5 {
896 1.0
897 } else {
898 1.0 - 2.0 * (t - 0.5)
899 }
900 .clamp(0.0, 1.0);
901
902 Vec3::new(r, g, b)
903 }
904
905 #[allow(dead_code)] fn default_colormap(&self, t: f32) -> Vec3 {
908 if t < 0.5 {
910 Vec3::new(0.0, 2.0 * t, 1.0 - 2.0 * t)
911 } else {
912 Vec3::new(2.0 * (t - 0.5), 1.0 - 2.0 * (t - 0.5), 0.0)
913 }
914 }
915}
916
917pub mod matlab_compat {
919 use super::*;
920
921 pub fn surf(x: Vec<f64>, y: Vec<f64>, z: Vec<Vec<f64>>) -> Result<SurfacePlot, String> {
923 SurfacePlot::new(x, y, z)
924 }
925
926 pub fn mesh(x: Vec<f64>, y: Vec<f64>, z: Vec<Vec<f64>>) -> Result<SurfacePlot, String> {
928 Ok(SurfacePlot::new(x, y, z)?
929 .with_wireframe(true)
930 .with_shading(ShadingMode::None))
931 }
932
933 pub fn meshgrid_surf(
935 x_range: (f64, f64),
936 y_range: (f64, f64),
937 resolution: (usize, usize),
938 func: impl Fn(f64, f64) -> f64,
939 ) -> Result<SurfacePlot, String> {
940 SurfacePlot::from_function(x_range, y_range, resolution, func)
941 }
942
943 pub fn surf_with_colormap(
945 x: Vec<f64>,
946 y: Vec<f64>,
947 z: Vec<Vec<f64>>,
948 colormap: &str,
949 ) -> Result<SurfacePlot, String> {
950 let cmap =
951 ColorMap::from_name(colormap).ok_or_else(|| format!("Unknown colormap: {colormap}"))?;
952
953 Ok(SurfacePlot::new(x, y, z)?.with_colormap(cmap))
954 }
955}
956
957#[cfg(test)]
958mod tests {
959 use super::*;
960
961 #[test]
962 fn test_surface_plot_creation() {
963 let x = vec![0.0, 1.0, 2.0];
964 let y = vec![0.0, 1.0];
965 let z = vec![vec![0.0, 1.0], vec![1.0, 2.0], vec![2.0, 3.0]];
966
967 let surface = SurfacePlot::new(x, y, z).unwrap();
968
969 assert_eq!(surface.x_data.len(), 3);
970 assert_eq!(surface.y_data.len(), 2);
971 let rows = surface.z_data.as_ref().unwrap();
972 assert_eq!(rows.len(), 3);
973 assert_eq!(rows[0].len(), 2);
974 assert!(surface.visible);
975 }
976
977 #[test]
978 fn test_surface_from_function() {
979 let surface =
980 SurfacePlot::from_function((-2.0, 2.0), (-2.0, 2.0), (10, 10), |x, y| x * x + y * y)
981 .unwrap();
982
983 assert_eq!(surface.x_data.len(), 10);
984 assert_eq!(surface.y_data.len(), 10);
985 let rows = surface.z_data.as_ref().unwrap();
986 assert_eq!(rows.len(), 10);
987
988 assert_eq!(rows[0][0], 8.0); }
991
992 #[test]
993 fn test_surface_validation() {
994 let x = vec![0.0, 1.0];
995 let y = vec![0.0, 1.0, 2.0];
996 let z = vec![
997 vec![0.0, 1.0], vec![1.0, 2.0],
999 ];
1000
1001 assert!(SurfacePlot::new(x, y, z).is_err());
1002 }
1003
1004 #[test]
1005 fn test_surface_styling() {
1006 let x = vec![0.0, 1.0];
1007 let y = vec![0.0, 1.0];
1008 let z = vec![vec![0.0, 1.0], vec![1.0, 2.0]];
1009
1010 let surface = SurfacePlot::new(x, y, z)
1011 .unwrap()
1012 .with_colormap(ColorMap::Hot)
1013 .with_wireframe(true)
1014 .with_alpha(0.8)
1015 .with_label("Test Surface");
1016
1017 assert_eq!(surface.colormap, ColorMap::Hot);
1018 assert!(surface.wireframe);
1019 assert_eq!(surface.alpha, 0.8);
1020 assert_eq!(surface.label, Some("Test Surface".to_string()));
1021 }
1022
1023 #[test]
1024 fn test_colormap_mapping() {
1025 let jet = ColorMap::Jet;
1026
1027 let color_0 = jet.map_value(0.0);
1029 let color_1 = jet.map_value(1.0);
1030
1031 assert!(color_0.x >= 0.0 && color_0.x <= 1.0);
1032 assert!(color_1.x >= 0.0 && color_1.x <= 1.0);
1033
1034 let color_mid = jet.map_value(0.5);
1036 assert_ne!(color_0, color_mid);
1037 assert_ne!(color_mid, color_1);
1038 }
1039
1040 #[test]
1041 fn test_surface_statistics() {
1042 let x = vec![0.0, 1.0, 2.0, 3.0];
1043 let y = vec![0.0, 1.0, 2.0];
1044 let z = vec![
1045 vec![0.0, 1.0, 2.0],
1046 vec![1.0, 2.0, 3.0],
1047 vec![2.0, 3.0, 4.0],
1048 vec![3.0, 4.0, 5.0],
1049 ];
1050
1051 let surface = SurfacePlot::new(x, y, z).unwrap();
1052 let stats = surface.statistics();
1053
1054 assert_eq!(stats.grid_points, 12); assert_eq!(stats.triangle_count, 12); assert_eq!(stats.x_resolution, 4);
1057 assert_eq!(stats.y_resolution, 3);
1058 assert!(stats.memory_usage > 0);
1059 }
1060
1061 #[test]
1062 fn test_matlab_compat() {
1063 use super::matlab_compat::*;
1064
1065 let x = vec![0.0, 1.0];
1066 let y = vec![0.0, 1.0];
1067 let z = vec![vec![0.0, 1.0], vec![1.0, 2.0]];
1068
1069 let surface = surf(x.clone(), y.clone(), z.clone()).unwrap();
1070 assert!(!surface.wireframe);
1071
1072 let mesh_plot = mesh(x.clone(), y.clone(), z.clone()).unwrap();
1073 assert!(mesh_plot.wireframe);
1074
1075 let colormap_surface = surf_with_colormap(x, y, z, "viridis").unwrap();
1076 assert_eq!(colormap_surface.colormap, ColorMap::Viridis);
1077 }
1078
1079 #[test]
1080 fn colormap_from_name_accepts_canonical_names_and_aliases() {
1081 let cases = [
1082 ("parula", ColorMap::Parula),
1083 ("viridis", ColorMap::Viridis),
1084 ("plasma", ColorMap::Plasma),
1085 ("inferno", ColorMap::Inferno),
1086 ("magma", ColorMap::Magma),
1087 ("turbo", ColorMap::Turbo),
1088 ("jet", ColorMap::Jet),
1089 ("hot", ColorMap::Hot),
1090 ("cool", ColorMap::Cool),
1091 ("spring", ColorMap::Spring),
1092 ("summer", ColorMap::Summer),
1093 ("autumn", ColorMap::Autumn),
1094 ("winter", ColorMap::Winter),
1095 ("gray", ColorMap::Gray),
1096 ("grey", ColorMap::Gray),
1097 ("bone", ColorMap::Bone),
1098 ("copper", ColorMap::Copper),
1099 ("pink", ColorMap::Pink),
1100 ("lines", ColorMap::Lines),
1101 ];
1102
1103 for (name, expected) in cases {
1104 assert_eq!(ColorMap::from_name(name), Some(expected), "{name}");
1105 }
1106 for name in ColorMap::CANONICAL_NAMES
1107 .iter()
1108 .chain(ColorMap::ALIASES.iter())
1109 .copied()
1110 {
1111 assert!(
1112 ColorMap::from_name(name).is_some(),
1113 "colormap table entry should parse: {name}"
1114 );
1115 }
1116 }
1117
1118 #[test]
1119 fn colormap_from_name_normalizes_and_rejects_unknown_names() {
1120 assert_eq!(ColorMap::from_name(" Turbo "), Some(ColorMap::Turbo));
1121 assert_eq!(ColorMap::from_name("GREY"), Some(ColorMap::Gray));
1122 assert_eq!(ColorMap::from_name("hsv"), None);
1123 assert!(!ColorMap::CANONICAL_NAMES.contains(&"hsv"));
1124 assert!(!ColorMap::ALIASES.contains(&"hsv"));
1125 assert_eq!(ColorMap::from_name("not-a-colormap"), None);
1126 }
1127}