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
90#[derive(Debug, Clone, Copy, PartialEq)]
92pub enum ShadingMode {
93 Flat,
95 Smooth,
97 Faceted,
99 None,
101}
102
103impl Default for ColorMap {
104 fn default() -> Self {
105 Self::Viridis
106 }
107}
108
109impl Default for ShadingMode {
110 fn default() -> Self {
111 Self::Smooth
112 }
113}
114
115impl SurfacePlot {
116 pub fn new(x_data: Vec<f64>, y_data: Vec<f64>, z_data: Vec<Vec<f64>>) -> Result<Self, String> {
118 if z_data.len() != x_data.len() {
120 return Err(format!(
121 "Z data rows ({}) must match X data length ({})",
122 z_data.len(),
123 x_data.len()
124 ));
125 }
126
127 for (i, row) in z_data.iter().enumerate() {
128 if row.len() != y_data.len() {
129 return Err(format!(
130 "Z data row {} length ({}) must match Y data length ({})",
131 i,
132 row.len(),
133 y_data.len()
134 ));
135 }
136 }
137
138 Ok(Self {
139 x_len: x_data.len(),
140 y_len: y_data.len(),
141 x_data,
142 y_data,
143 z_data: Some(z_data),
144 colormap: ColorMap::default(),
145 shading_mode: ShadingMode::default(),
146 wireframe: false,
147 alpha: 1.0,
148 flatten_z: false,
149 image_mode: false,
150 color_limits: None,
151 color_grid: None,
152 lighting_enabled: true,
153 ambient_strength: 0.2,
154 diffuse_strength: 0.8,
155 specular_strength: 0.5,
156 shininess: 32.0,
157 label: None,
158 visible: true,
159 vertices: None,
160 indices: None,
161 bounds: None,
162 dirty: true,
163 gpu_vertices: None,
164 gpu_vertex_count: None,
165 gpu_bounds: None,
166 })
167 }
168
169 pub fn from_gpu_buffer(
171 x_len: usize,
172 y_len: usize,
173 buffer: GpuVertexBuffer,
174 vertex_count: usize,
175 bounds: BoundingBox,
176 ) -> Self {
177 Self {
178 x_data: Vec::new(),
179 y_data: Vec::new(),
180 z_data: None,
181 x_len,
182 y_len,
183 colormap: ColorMap::default(),
184 shading_mode: ShadingMode::default(),
185 wireframe: false,
186 alpha: 1.0,
187 flatten_z: false,
188 image_mode: false,
189 color_limits: None,
190 color_grid: None,
191 lighting_enabled: true,
192 ambient_strength: 0.2,
193 diffuse_strength: 0.8,
194 specular_strength: 0.5,
195 shininess: 32.0,
196 label: None,
197 visible: true,
198 vertices: None,
199 indices: None,
200 bounds: Some(bounds),
201 dirty: false,
202 gpu_vertices: Some(buffer),
203 gpu_vertex_count: Some(vertex_count),
204 gpu_bounds: Some(bounds),
205 }
206 }
207
208 fn drop_gpu_if_possible(&mut self) {
209 if self.gpu_vertices.is_some() && self.z_data.is_some() {
210 self.invalidate_gpu_data();
211 }
212 }
213
214 pub fn from_function<F>(
216 x_range: (f64, f64),
217 y_range: (f64, f64),
218 resolution: (usize, usize),
219 func: F,
220 ) -> Result<Self, String>
221 where
222 F: Fn(f64, f64) -> f64,
223 {
224 let (x_res, y_res) = resolution;
225 if x_res < 2 || y_res < 2 {
226 return Err("Resolution must be at least 2x2".to_string());
227 }
228
229 let x_data: Vec<f64> = (0..x_res)
230 .map(|i| x_range.0 + (x_range.1 - x_range.0) * i as f64 / (x_res - 1) as f64)
231 .collect();
232
233 let y_data: Vec<f64> = (0..y_res)
234 .map(|j| y_range.0 + (y_range.1 - y_range.0) * j as f64 / (y_res - 1) as f64)
235 .collect();
236
237 let z_data: Vec<Vec<f64>> = x_data
238 .iter()
239 .map(|&x| y_data.iter().map(|&y| func(x, y)).collect())
240 .collect();
241
242 Self::new(x_data, y_data, z_data)
243 }
244
245 fn invalidate_gpu_data(&mut self) {
246 self.gpu_vertices = None;
247 self.gpu_vertex_count = None;
248 self.gpu_bounds = None;
249 }
250
251 pub fn with_colormap(mut self, colormap: ColorMap) -> Self {
253 self.colormap = colormap;
254 self.dirty = true;
255 self.drop_gpu_if_possible();
256 self
257 }
258
259 pub fn with_shading(mut self, shading: ShadingMode) -> Self {
261 self.shading_mode = shading;
262 self.dirty = true;
263 self.drop_gpu_if_possible();
264 self
265 }
266
267 pub fn with_wireframe(mut self, enabled: bool) -> Self {
269 self.wireframe = enabled;
270 self.dirty = true;
271 self.drop_gpu_if_possible();
272 self
273 }
274
275 pub fn with_alpha(mut self, alpha: f32) -> Self {
277 self.alpha = alpha.clamp(0.0, 1.0);
278 self.dirty = true;
279 self.drop_gpu_if_possible();
280 self
281 }
282
283 pub fn with_flatten_z(mut self, enabled: bool) -> Self {
285 self.flatten_z = enabled;
286 self.dirty = true;
287 self.drop_gpu_if_possible();
288 self
289 }
290
291 pub fn with_image_mode(mut self, enabled: bool) -> Self {
292 self.image_mode = enabled;
293 self.dirty = true;
294 self.drop_gpu_if_possible();
295 self
296 }
297
298 pub fn with_color_limits(mut self, limits: Option<(f64, f64)>) -> Self {
300 self.color_limits = limits;
301 self.dirty = true;
302 self.drop_gpu_if_possible();
303 self
304 }
305
306 pub fn set_color_limits(&mut self, limits: Option<(f64, f64)>) {
308 self.color_limits = limits;
309 self.dirty = true;
310 self.drop_gpu_if_possible();
311 }
312
313 pub fn with_color_grid(mut self, grid: Vec<Vec<Vec4>>) -> Self {
315 self.color_grid = Some(grid);
316 self.dirty = true;
317 self.drop_gpu_if_possible();
318 self
319 }
320
321 pub fn with_label<S: Into<String>>(mut self, label: S) -> Self {
323 self.label = Some(label.into());
324 self
325 }
326
327 pub fn len(&self) -> usize {
329 self.x_len * self.y_len
330 }
331
332 pub fn is_empty(&self) -> bool {
334 self.x_len == 0 || self.y_len == 0
335 }
336
337 pub fn bounds(&mut self) -> BoundingBox {
339 if self.dirty || self.bounds.is_none() {
340 self.compute_bounds();
341 }
342 self.bounds.unwrap()
343 }
344
345 fn compute_bounds(&mut self) {
347 if let Some(bounds) = self.gpu_bounds {
348 self.bounds = Some(bounds);
349 return;
350 }
351
352 let mut min_x = f32::INFINITY;
353 let mut max_x = f32::NEG_INFINITY;
354 let mut min_y = f32::INFINITY;
355 let mut max_y = f32::NEG_INFINITY;
356 let mut min_z = f32::INFINITY;
357 let mut max_z = f32::NEG_INFINITY;
358
359 for &x in &self.x_data {
360 min_x = min_x.min(x as f32);
361 max_x = max_x.max(x as f32);
362 }
363
364 for &y in &self.y_data {
365 min_y = min_y.min(y as f32);
366 max_y = max_y.max(y as f32);
367 }
368
369 if let Some(rows) = &self.z_data {
370 for row in rows {
371 for &z in row {
372 if z.is_finite() {
373 min_z = min_z.min(z as f32);
374 max_z = max_z.max(z as f32);
375 }
376 }
377 }
378 }
379
380 self.bounds = Some(BoundingBox::new(
381 Vec3::new(min_x, min_y, min_z),
382 Vec3::new(max_x, max_y, max_z),
383 ));
384 }
385
386 pub fn statistics(&self) -> SurfaceStatistics {
388 let grid_size = self.x_len * self.y_len;
389 let triangle_count = if self.x_len > 1 && self.y_len > 1 {
390 (self.x_len - 1) * (self.y_len - 1) * 2
391 } else {
392 0
393 };
394
395 SurfaceStatistics {
396 grid_points: grid_size,
397 triangle_count,
398 x_resolution: self.x_len,
399 y_resolution: self.y_len,
400 memory_usage: self.estimated_memory_usage(),
401 }
402 }
403
404 pub fn estimated_memory_usage(&self) -> usize {
406 let data_size = std::mem::size_of::<f64>()
407 * (self.x_data.len()
408 + self.y_data.len()
409 + self
410 .z_data
411 .as_ref()
412 .map_or(0, |z| z.len() * self.y_data.len()));
413
414 let vertices_size = self
415 .vertices
416 .as_ref()
417 .map_or(0, |v| v.len() * std::mem::size_of::<Vertex>());
418
419 let indices_size = self
420 .indices
421 .as_ref()
422 .map_or(0, |i| i.len() * std::mem::size_of::<u32>());
423
424 let gpu_size = self.gpu_vertex_count.unwrap_or(0) * std::mem::size_of::<Vertex>();
425
426 data_size + vertices_size + indices_size + gpu_size
427 }
428
429 fn generate_vertices(&mut self) -> &Vec<Vertex> {
431 if self.gpu_vertices.is_some() {
432 if self.vertices.is_none() {
433 self.vertices = Some(Vec::new());
434 }
435 return self.vertices.as_ref().unwrap();
436 }
437
438 if self.dirty || self.vertices.is_none() {
439 log::trace!(
440 target: "runmat_plot",
441 "surface gen vertices {} x {}",
442 self.x_data.len(),
443 self.y_data.len()
444 );
445
446 let mut vertices = Vec::new();
447
448 let z_rows = self
450 .z_data
451 .as_ref()
452 .expect("CPU surface data missing during vertex generation");
453 let (min_z, max_z) = if let Some((lo, hi)) = self.color_limits {
454 (lo, hi)
455 } else {
456 let mut min_z = f64::INFINITY;
457 let mut max_z = f64::NEG_INFINITY;
458 for row in z_rows {
459 for &z in row {
460 if z.is_finite() {
461 min_z = min_z.min(z);
462 max_z = max_z.max(z);
463 }
464 }
465 }
466 (min_z, max_z)
467 };
468 let z_range = (max_z - min_z).max(f64::MIN_POSITIVE);
469
470 for (i, &x) in self.x_data.iter().enumerate() {
472 for (j, &y) in self.y_data.iter().enumerate() {
473 let z = z_rows[i][j];
474 let z_pos = if self.flatten_z { 0.0 } else { z as f32 };
475 let position = Vec3::new(x as f32, y as f32, z_pos);
476
477 let normal = Vec3::new(0.0, 0.0, 1.0); let color = if let Some(grid) = &self.color_grid {
482 let c = grid[i][j];
483 Vec4::new(c.x, c.y, c.z, c.w)
484 } else {
485 let t = ((z - min_z) / z_range) as f32;
486 let color_rgb = self.colormap.map_value(t.clamp(0.0, 1.0));
487 Vec4::new(color_rgb.x, color_rgb.y, color_rgb.z, self.alpha)
488 };
489
490 vertices.push(Vertex {
491 position: position.to_array(),
492 normal: normal.to_array(),
493 color: color.to_array(),
494 tex_coords: [
495 i as f32 / (self.x_data.len() - 1).max(1) as f32,
496 j as f32 / (self.y_data.len() - 1).max(1) as f32,
497 ],
498 });
499 }
500 }
501
502 log::trace!(target: "runmat_plot", "surface vertices={}", vertices.len());
503 self.vertices = Some(vertices);
504 }
505 self.vertices.as_ref().unwrap()
506 }
507
508 fn generate_indices(&mut self) -> &Vec<u32> {
510 if self.dirty || self.indices.is_none() {
511 log::trace!(target: "runmat_plot", "surface generating indices");
512
513 let mut indices = Vec::new();
514 let x_res = self.x_len;
515 let y_res = self.y_len;
516
517 for i in 0..x_res - 1 {
519 for j in 0..y_res - 1 {
520 let base = (i * y_res + j) as u32;
521 let next_row = base + y_res as u32;
522
523 indices.push(base);
526 indices.push(next_row);
527 indices.push(base + 1);
528
529 indices.push(next_row);
531 indices.push(next_row + 1);
532 indices.push(base + 1);
533 }
534 }
535
536 log::trace!(target: "runmat_plot", "surface indices={}", indices.len());
537 self.indices = Some(indices);
538 self.dirty = false;
539 }
540 self.indices.as_ref().unwrap()
541 }
542
543 fn generate_wireframe_indices(&self) -> Vec<u32> {
544 let mut indices = Vec::new();
545 if self.x_len < 2 || self.y_len < 2 {
546 return indices;
547 }
548
549 for i in 0..self.x_len {
551 for j in 0..(self.y_len - 1) {
552 let a = (i * self.y_len + j) as u32;
553 let b = (i * self.y_len + j + 1) as u32;
554 indices.push(a);
555 indices.push(b);
556 }
557 }
558
559 for i in 0..(self.x_len - 1) {
561 for j in 0..self.y_len {
562 let a = (i * self.y_len + j) as u32;
563 let b = ((i + 1) * self.y_len + j) as u32;
564 indices.push(a);
565 indices.push(b);
566 }
567 }
568
569 indices
570 }
571
572 pub fn render_data(&mut self) -> RenderData {
574 log::debug!(
575 target: "runmat_plot",
576 "surface render_data start: {} x {}",
577 self.x_len,
578 self.y_len
579 );
580
581 let using_gpu = self.gpu_vertices.is_some();
582 let bounds = self.bounds();
583 let vertices = if using_gpu {
584 Vec::new()
585 } else {
586 self.generate_vertices().clone()
587 };
588 let indices = if self.wireframe {
589 self.generate_wireframe_indices()
590 } else {
591 self.generate_indices().clone()
592 };
593
594 let material = Material {
595 albedo: Vec4::new(1.0, 1.0, 1.0, self.alpha),
596 ..Default::default()
597 };
598
599 let vertex_count = if using_gpu {
600 self.gpu_vertex_count.unwrap_or(0)
601 } else {
602 vertices.len()
603 };
604
605 log::debug!(
606 target: "runmat_plot",
607 "surface render_data generated: vertex_count={} (gpu={}), indices={}",
608 vertex_count,
609 using_gpu,
610 indices.len()
611 );
612
613 let draw_call = DrawCall {
614 vertex_offset: 0,
615 vertex_count,
616 index_offset: Some(0),
617 index_count: Some(indices.len()),
618 instance_count: 1,
619 };
620
621 log::trace!(target: "runmat_plot", "surface render_data done");
622
623 RenderData {
624 pipeline_type: if self.wireframe {
625 PipelineType::Lines
626 } else {
627 PipelineType::Triangles
628 },
629 vertices,
630 indices: Some(indices),
631
632 gpu_vertices: self.gpu_vertices.clone(),
633 bounds: Some(bounds),
634 material,
635 draw_calls: vec![draw_call],
636 image: None,
637 }
638 }
639}
640
641#[derive(Debug, Clone)]
643pub struct SurfaceStatistics {
644 pub grid_points: usize,
645 pub triangle_count: usize,
646 pub x_resolution: usize,
647 pub y_resolution: usize,
648 pub memory_usage: usize,
649}
650
651impl ColorMap {
652 pub fn map_value(&self, t: f32) -> Vec3 {
654 let t = t.clamp(0.0, 1.0);
655
656 match self {
657 ColorMap::Jet => self.jet_colormap(t),
658 ColorMap::Hot => self.hot_colormap(t),
659 ColorMap::Cool => self.cool_colormap(t),
660 ColorMap::Spring => self.spring_colormap(t),
661 ColorMap::Summer => self.summer_colormap(t),
662 ColorMap::Autumn => self.autumn_colormap(t),
663 ColorMap::Winter => self.winter_colormap(t),
664 ColorMap::Gray => Vec3::splat(t),
665 ColorMap::Bone => self.bone_colormap(t),
666 ColorMap::Copper => self.copper_colormap(t),
667 ColorMap::Pink => self.pink_colormap(t),
668 ColorMap::Lines => self.lines_colormap(t),
669 ColorMap::Viridis => self.viridis_colormap(t),
670 ColorMap::Plasma => self.plasma_colormap(t),
671 ColorMap::Inferno => self.inferno_colormap(t),
672 ColorMap::Magma => self.magma_colormap(t),
673 ColorMap::Turbo => self.turbo_colormap(t),
674 ColorMap::Parula => self.parula_colormap(t),
675 ColorMap::Custom(min_color, max_color) => {
676 min_color.truncate().lerp(max_color.truncate(), t)
677 }
678 }
679 }
680
681 fn jet_colormap(&self, t: f32) -> Vec3 {
683 let r = (1.5 - 4.0 * (t - 0.75).abs()).clamp(0.0, 1.0);
684 let g = (1.5 - 4.0 * (t - 0.5).abs()).clamp(0.0, 1.0);
685 let b = (1.5 - 4.0 * (t - 0.25).abs()).clamp(0.0, 1.0);
686 Vec3::new(r, g, b)
687 }
688
689 fn hot_colormap(&self, t: f32) -> Vec3 {
691 if t < 1.0 / 3.0 {
692 Vec3::new(3.0 * t, 0.0, 0.0)
693 } else if t < 2.0 / 3.0 {
694 Vec3::new(1.0, 3.0 * t - 1.0, 0.0)
695 } else {
696 Vec3::new(1.0, 1.0, 3.0 * t - 2.0)
697 }
698 }
699
700 fn cool_colormap(&self, t: f32) -> Vec3 {
702 Vec3::new(t, 1.0 - t, 1.0)
703 }
704
705 fn viridis_colormap(&self, t: f32) -> Vec3 {
707 let r = (0.267004 + t * (0.993248 - 0.267004)).clamp(0.0, 1.0);
709 let g = (0.004874 + t * (0.906157 - 0.004874)).clamp(0.0, 1.0);
710 let b = (0.329415 + t * (0.143936 - 0.329415) + t * t * 0.5).clamp(0.0, 1.0);
711 Vec3::new(r, g, b)
712 }
713
714 fn plasma_colormap(&self, t: f32) -> Vec3 {
716 let r = (0.050383 + t * (0.940015 - 0.050383)).clamp(0.0, 1.0);
718 let g = (0.029803 + t * (0.975158 - 0.029803) * (1.0 - t)).clamp(0.0, 1.0);
719 let b = (0.527975 + t * (0.131326 - 0.527975)).clamp(0.0, 1.0);
720 Vec3::new(r, g, b)
721 }
722
723 fn spring_colormap(&self, t: f32) -> Vec3 {
725 Vec3::new(1.0, t, 1.0 - t)
726 }
727
728 fn summer_colormap(&self, t: f32) -> Vec3 {
730 Vec3::new(t, 0.5 + 0.5 * t, 0.4)
731 }
732
733 fn autumn_colormap(&self, t: f32) -> Vec3 {
735 Vec3::new(1.0, t, 0.0)
736 }
737
738 fn winter_colormap(&self, t: f32) -> Vec3 {
740 Vec3::new(0.0, t, 1.0 - 0.5 * t)
741 }
742
743 fn bone_colormap(&self, t: f32) -> Vec3 {
745 if t < 3.0 / 8.0 {
746 Vec3::new(7.0 / 8.0 * t, 7.0 / 8.0 * t, 29.0 / 24.0 * t)
747 } else {
748 Vec3::new(
749 (29.0 + 7.0 * t) / 24.0,
750 (29.0 + 7.0 * t) / 24.0,
751 (29.0 + 7.0 * t) / 24.0,
752 )
753 }
754 }
755
756 fn copper_colormap(&self, t: f32) -> Vec3 {
758 Vec3::new((1.25 * t).min(1.0), 0.7812 * t, 0.4975 * t)
759 }
760
761 fn pink_colormap(&self, t: f32) -> Vec3 {
763 let sqrt_t = t.sqrt();
764 if t < 3.0 / 8.0 {
765 Vec3::new(14.0 / 9.0 * sqrt_t, 2.0 / 3.0 * sqrt_t, 2.0 / 3.0 * sqrt_t)
766 } else {
767 Vec3::new(
768 2.0 * sqrt_t - 1.0 / 3.0,
769 8.0 / 9.0 * sqrt_t + 1.0 / 3.0,
770 8.0 / 9.0 * sqrt_t + 1.0 / 3.0,
771 )
772 }
773 }
774
775 fn lines_colormap(&self, t: f32) -> Vec3 {
777 let _phase = (t * 7.0) % 1.0; let index = (t * 7.0) as usize % 7;
779 match index {
780 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), }
788 }
789
790 fn inferno_colormap(&self, t: f32) -> Vec3 {
792 let r = (0.001462 + t * (0.988362 - 0.001462)).clamp(0.0, 1.0);
794 let g = (0.000466 + t * t * (0.982895 - 0.000466)).clamp(0.0, 1.0);
795 let b = (0.013866 + t * (1.0 - t) * (0.416065 - 0.013866)).clamp(0.0, 1.0);
796 Vec3::new(r, g, b)
797 }
798
799 fn magma_colormap(&self, t: f32) -> Vec3 {
801 let r = (0.001462 + t * (0.987053 - 0.001462)).clamp(0.0, 1.0);
803 let g = (0.000466 + t * t * (0.991438 - 0.000466)).clamp(0.0, 1.0);
804 let b = (0.013866 + t * (0.644237 - 0.013866) * (1.0 - t)).clamp(0.0, 1.0);
805 Vec3::new(r, g, b)
806 }
807
808 fn turbo_colormap(&self, t: f32) -> Vec3 {
810 let r = if t < 0.5 {
812 (0.13 + 0.87 * (2.0 * t).powf(0.25)).clamp(0.0, 1.0)
813 } else {
814 (0.8685 + 0.1315 * (2.0 * (1.0 - t)).powf(0.25)).clamp(0.0, 1.0)
815 };
816
817 let g = if t < 0.25 {
818 4.0 * t
819 } else if t < 0.75 {
820 1.0
821 } else {
822 1.0 - 4.0 * (t - 0.75)
823 }
824 .clamp(0.0, 1.0);
825
826 let b = if t < 0.5 {
827 (0.8 * (1.0 - 2.0 * t).powf(0.25)).clamp(0.0, 1.0)
828 } else {
829 (0.1 + 0.9 * (2.0 * t - 1.0).powf(0.25)).clamp(0.0, 1.0)
830 };
831
832 Vec3::new(r, g, b)
833 }
834
835 fn parula_colormap(&self, t: f32) -> Vec3 {
837 let r = if t < 0.25 {
839 0.2081 * (1.0 - t)
840 } else if t < 0.5 {
841 t - 0.25
842 } else if t < 0.75 {
843 1.0
844 } else {
845 1.0 - 0.5 * (t - 0.75)
846 }
847 .clamp(0.0, 1.0);
848
849 let g = if t < 0.125 {
850 0.1663 * t / 0.125
851 } else if t < 0.375 {
852 0.1663 + (0.7079 - 0.1663) * (t - 0.125) / 0.25
853 } else if t < 0.625 {
854 0.7079 + (0.9839 - 0.7079) * (t - 0.375) / 0.25
855 } else {
856 0.9839 * (1.0 - (t - 0.625) / 0.375)
857 }
858 .clamp(0.0, 1.0);
859
860 let b = if t < 0.25 {
861 0.5 + 0.5 * t / 0.25
862 } else if t < 0.5 {
863 1.0
864 } else {
865 1.0 - 2.0 * (t - 0.5)
866 }
867 .clamp(0.0, 1.0);
868
869 Vec3::new(r, g, b)
870 }
871
872 #[allow(dead_code)] fn default_colormap(&self, t: f32) -> Vec3 {
875 if t < 0.5 {
877 Vec3::new(0.0, 2.0 * t, 1.0 - 2.0 * t)
878 } else {
879 Vec3::new(2.0 * (t - 0.5), 1.0 - 2.0 * (t - 0.5), 0.0)
880 }
881 }
882}
883
884pub mod matlab_compat {
886 use super::*;
887
888 pub fn surf(x: Vec<f64>, y: Vec<f64>, z: Vec<Vec<f64>>) -> Result<SurfacePlot, String> {
890 SurfacePlot::new(x, y, z)
891 }
892
893 pub fn mesh(x: Vec<f64>, y: Vec<f64>, z: Vec<Vec<f64>>) -> Result<SurfacePlot, String> {
895 Ok(SurfacePlot::new(x, y, z)?
896 .with_wireframe(true)
897 .with_shading(ShadingMode::None))
898 }
899
900 pub fn meshgrid_surf(
902 x_range: (f64, f64),
903 y_range: (f64, f64),
904 resolution: (usize, usize),
905 func: impl Fn(f64, f64) -> f64,
906 ) -> Result<SurfacePlot, String> {
907 SurfacePlot::from_function(x_range, y_range, resolution, func)
908 }
909
910 pub fn surf_with_colormap(
912 x: Vec<f64>,
913 y: Vec<f64>,
914 z: Vec<Vec<f64>>,
915 colormap: &str,
916 ) -> Result<SurfacePlot, String> {
917 let cmap = match colormap {
918 "jet" => ColorMap::Jet,
919 "hot" => ColorMap::Hot,
920 "cool" => ColorMap::Cool,
921 "viridis" => ColorMap::Viridis,
922 "plasma" => ColorMap::Plasma,
923 "gray" | "grey" => ColorMap::Gray,
924 _ => return Err(format!("Unknown colormap: {colormap}")),
925 };
926
927 Ok(SurfacePlot::new(x, y, z)?.with_colormap(cmap))
928 }
929}
930
931#[cfg(test)]
932mod tests {
933 use super::*;
934
935 #[test]
936 fn test_surface_plot_creation() {
937 let x = vec![0.0, 1.0, 2.0];
938 let y = vec![0.0, 1.0];
939 let z = vec![vec![0.0, 1.0], vec![1.0, 2.0], vec![2.0, 3.0]];
940
941 let surface = SurfacePlot::new(x, y, z).unwrap();
942
943 assert_eq!(surface.x_data.len(), 3);
944 assert_eq!(surface.y_data.len(), 2);
945 let rows = surface.z_data.as_ref().unwrap();
946 assert_eq!(rows.len(), 3);
947 assert_eq!(rows[0].len(), 2);
948 assert!(surface.visible);
949 }
950
951 #[test]
952 fn test_surface_from_function() {
953 let surface =
954 SurfacePlot::from_function((-2.0, 2.0), (-2.0, 2.0), (10, 10), |x, y| x * x + y * y)
955 .unwrap();
956
957 assert_eq!(surface.x_data.len(), 10);
958 assert_eq!(surface.y_data.len(), 10);
959 let rows = surface.z_data.as_ref().unwrap();
960 assert_eq!(rows.len(), 10);
961
962 assert_eq!(rows[0][0], 8.0); }
965
966 #[test]
967 fn test_surface_validation() {
968 let x = vec![0.0, 1.0];
969 let y = vec![0.0, 1.0, 2.0];
970 let z = vec![
971 vec![0.0, 1.0], vec![1.0, 2.0],
973 ];
974
975 assert!(SurfacePlot::new(x, y, z).is_err());
976 }
977
978 #[test]
979 fn test_surface_styling() {
980 let x = vec![0.0, 1.0];
981 let y = vec![0.0, 1.0];
982 let z = vec![vec![0.0, 1.0], vec![1.0, 2.0]];
983
984 let surface = SurfacePlot::new(x, y, z)
985 .unwrap()
986 .with_colormap(ColorMap::Hot)
987 .with_wireframe(true)
988 .with_alpha(0.8)
989 .with_label("Test Surface");
990
991 assert_eq!(surface.colormap, ColorMap::Hot);
992 assert!(surface.wireframe);
993 assert_eq!(surface.alpha, 0.8);
994 assert_eq!(surface.label, Some("Test Surface".to_string()));
995 }
996
997 #[test]
998 fn test_colormap_mapping() {
999 let jet = ColorMap::Jet;
1000
1001 let color_0 = jet.map_value(0.0);
1003 let color_1 = jet.map_value(1.0);
1004
1005 assert!(color_0.x >= 0.0 && color_0.x <= 1.0);
1006 assert!(color_1.x >= 0.0 && color_1.x <= 1.0);
1007
1008 let color_mid = jet.map_value(0.5);
1010 assert_ne!(color_0, color_mid);
1011 assert_ne!(color_mid, color_1);
1012 }
1013
1014 #[test]
1015 fn test_surface_statistics() {
1016 let x = vec![0.0, 1.0, 2.0, 3.0];
1017 let y = vec![0.0, 1.0, 2.0];
1018 let z = vec![
1019 vec![0.0, 1.0, 2.0],
1020 vec![1.0, 2.0, 3.0],
1021 vec![2.0, 3.0, 4.0],
1022 vec![3.0, 4.0, 5.0],
1023 ];
1024
1025 let surface = SurfacePlot::new(x, y, z).unwrap();
1026 let stats = surface.statistics();
1027
1028 assert_eq!(stats.grid_points, 12); assert_eq!(stats.triangle_count, 12); assert_eq!(stats.x_resolution, 4);
1031 assert_eq!(stats.y_resolution, 3);
1032 assert!(stats.memory_usage > 0);
1033 }
1034
1035 #[test]
1036 fn test_matlab_compat() {
1037 use super::matlab_compat::*;
1038
1039 let x = vec![0.0, 1.0];
1040 let y = vec![0.0, 1.0];
1041 let z = vec![vec![0.0, 1.0], vec![1.0, 2.0]];
1042
1043 let surface = surf(x.clone(), y.clone(), z.clone()).unwrap();
1044 assert!(!surface.wireframe);
1045
1046 let mesh_plot = mesh(x.clone(), y.clone(), z.clone()).unwrap();
1047 assert!(mesh_plot.wireframe);
1048
1049 let colormap_surface = surf_with_colormap(x, y, z, "viridis").unwrap();
1050 assert_eq!(colormap_surface.colormap, ColorMap::Viridis);
1051 }
1052}