1use crate::core::{BoundingBox, DrawCall, Material, PipelineType, RenderData, Vertex};
6use crate::plots::surface::ColorMap;
7use glam::{Vec3, Vec4};
8
9#[derive(Debug, Clone)]
11pub struct PointCloudPlot {
12 pub positions: Vec<Vec3>,
14
15 pub values: Option<Vec<f64>>,
17 pub colors: Option<Vec<Vec4>>,
18 pub sizes: Option<Vec<f32>>,
19
20 pub default_color: Vec4,
22 pub default_size: f32,
23 pub colormap: ColorMap,
24
25 pub point_style: PointStyle,
27 pub size_mode: SizeMode,
28
29 pub label: Option<String>,
31 pub visible: bool,
32
33 vertices: Option<Vec<Vertex>>,
35 bounds: Option<BoundingBox>,
36 dirty: bool,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq)]
41pub enum PointStyle {
42 Circle,
44 Square,
46 Sphere,
48 Custom,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq)]
54pub enum SizeMode {
55 Fixed,
57 Proportional,
59 Perspective,
61}
62
63impl Default for PointStyle {
64 fn default() -> Self {
65 Self::Circle
66 }
67}
68
69impl Default for SizeMode {
70 fn default() -> Self {
71 Self::Fixed
72 }
73}
74
75impl PointCloudPlot {
76 pub fn new(positions: Vec<Vec3>) -> Self {
78 Self {
79 positions,
80 values: None,
81 colors: None,
82 sizes: None,
83 default_color: Vec4::new(0.0, 0.5, 1.0, 1.0), default_size: 3.0,
85 colormap: ColorMap::Viridis,
86 point_style: PointStyle::default(),
87 size_mode: SizeMode::default(),
88 label: None,
89 visible: true,
90 vertices: None,
91 bounds: None,
92 dirty: true,
93 }
94 }
95
96 pub fn with_values(mut self, values: Vec<f64>) -> Result<Self, String> {
98 if values.len() != self.positions.len() {
99 return Err(format!(
100 "Values length ({}) must match positions length ({})",
101 values.len(),
102 self.positions.len()
103 ));
104 }
105 self.values = Some(values);
106 self.dirty = true;
107 Ok(self)
108 }
109
110 pub fn with_colors(mut self, colors: Vec<Vec4>) -> Result<Self, String> {
112 if colors.len() != self.positions.len() {
113 return Err(format!(
114 "Colors length ({}) must match positions length ({})",
115 colors.len(),
116 self.positions.len()
117 ));
118 }
119 self.colors = Some(colors);
120 self.dirty = true;
121 Ok(self)
122 }
123
124 pub fn with_sizes(mut self, sizes: Vec<f32>) -> Result<Self, String> {
126 if sizes.len() != self.positions.len() {
127 return Err(format!(
128 "Sizes length ({}) must match positions length ({})",
129 sizes.len(),
130 self.positions.len()
131 ));
132 }
133 self.sizes = Some(sizes);
134 self.dirty = true;
135 Ok(self)
136 }
137
138 pub fn with_default_color(mut self, color: Vec4) -> Self {
140 self.default_color = color;
141 self.dirty = true;
142 self
143 }
144
145 pub fn with_default_size(mut self, size: f32) -> Self {
147 self.default_size = size.max(0.1);
148 self.dirty = true;
149 self
150 }
151
152 pub fn with_colormap(mut self, colormap: ColorMap) -> Self {
154 self.colormap = colormap;
155 self.dirty = true;
156 self
157 }
158
159 pub fn with_point_style(mut self, style: PointStyle) -> Self {
161 self.point_style = style;
162 self.dirty = true;
163 self
164 }
165
166 pub fn with_size_mode(mut self, mode: SizeMode) -> Self {
168 self.size_mode = mode;
169 self.dirty = true;
170 self
171 }
172
173 pub fn with_label<S: Into<String>>(mut self, label: S) -> Self {
175 self.label = Some(label.into());
176 self
177 }
178
179 pub fn len(&self) -> usize {
181 self.positions.len()
182 }
183
184 pub fn is_empty(&self) -> bool {
186 self.positions.is_empty()
187 }
188
189 pub fn generate_vertices(&mut self) -> &Vec<Vertex> {
191 if self.dirty || self.vertices.is_none() {
192 self.compute_vertices();
193 self.dirty = false;
194 }
195 self.vertices.as_ref().unwrap()
196 }
197
198 pub fn bounds(&mut self) -> BoundingBox {
200 if self.dirty || self.bounds.is_none() {
201 self.compute_bounds();
202 }
203 self.bounds.unwrap()
204 }
205
206 fn compute_vertices(&mut self) {
208 let mut vertices = Vec::with_capacity(self.positions.len());
209
210 let (value_min, value_max) = if let Some(ref values) = self.values {
212 let min = values.iter().copied().fold(f64::INFINITY, f64::min);
213 let max = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
214 (min, max)
215 } else {
216 (0.0, 1.0)
217 };
218
219 for (i, &position) in self.positions.iter().enumerate() {
220 let color = if let Some(ref colors) = self.colors {
222 colors[i]
223 } else if let Some(ref values) = self.values {
224 let normalized = if value_max > value_min {
225 ((values[i] - value_min) / (value_max - value_min)).clamp(0.0, 1.0)
226 } else {
227 0.5
228 };
229 let rgb = self.colormap.map_value(normalized as f32);
230 Vec4::new(rgb.x, rgb.y, rgb.z, self.default_color.w)
231 } else {
232 self.default_color
233 };
234
235 let size = if let Some(ref sizes) = self.sizes {
237 sizes[i]
238 } else {
239 match self.size_mode {
240 SizeMode::Fixed => self.default_size,
241 SizeMode::Proportional => {
242 if let Some(ref values) = self.values {
243 let normalized = if value_max > value_min {
244 ((values[i] - value_min) / (value_max - value_min)).clamp(0.0, 1.0)
245 } else {
246 0.5
247 };
248 self.default_size * (0.5 + normalized as f32)
249 } else {
250 self.default_size
251 }
252 }
253 SizeMode::Perspective => self.default_size, }
255 };
256
257 vertices.push(Vertex {
258 position: position.to_array(),
259 color: color.to_array(),
260 normal: [size, 0.0, 0.0], tex_coords: [i as f32, 0.0], });
263 }
264
265 self.vertices = Some(vertices);
266 }
267
268 fn compute_bounds(&mut self) {
270 if self.positions.is_empty() {
271 self.bounds = Some(BoundingBox::new(Vec3::ZERO, Vec3::ZERO));
272 return;
273 }
274
275 let mut min = self.positions[0];
276 let mut max = self.positions[0];
277
278 for &pos in &self.positions[1..] {
279 min = min.min(pos);
280 max = max.max(pos);
281 }
282
283 let expansion = Vec3::splat(self.default_size * 0.01);
285 min -= expansion;
286 max += expansion;
287
288 self.bounds = Some(BoundingBox::new(min, max));
289 }
290
291 pub fn render_data(&mut self) -> RenderData {
293 println!(
294 "DEBUG: PointCloudPlot::render_data() called with {} points",
295 self.positions.len()
296 );
297
298 let vertices = self.generate_vertices().clone();
299 let vertex_count = vertices.len();
300
301 println!("DEBUG: Generated {vertex_count} vertices for point cloud");
302
303 let material = Material {
304 albedo: self.default_color,
305 ..Default::default()
306 };
307
308 let draw_call = DrawCall {
309 vertex_offset: 0,
310 vertex_count,
311 index_offset: None,
312 index_count: None,
313 instance_count: 1,
314 };
315
316 println!("DEBUG: PointCloudPlot render_data completed successfully");
317
318 RenderData {
319 pipeline_type: PipelineType::Points,
320 vertices,
321 indices: None,
322 material,
323 draw_calls: vec![draw_call],
324 }
325 }
326
327 pub fn statistics(&self) -> PointCloudStatistics {
329 PointCloudStatistics {
330 point_count: self.positions.len(),
331 has_values: self.values.is_some(),
332 has_colors: self.colors.is_some(),
333 has_sizes: self.sizes.is_some(),
334 memory_usage: self.estimated_memory_usage(),
335 }
336 }
337
338 pub fn estimated_memory_usage(&self) -> usize {
340 let positions_size = self.positions.len() * std::mem::size_of::<Vec3>();
341 let values_size = self
342 .values
343 .as_ref()
344 .map_or(0, |v| v.len() * std::mem::size_of::<f64>());
345 let colors_size = self
346 .colors
347 .as_ref()
348 .map_or(0, |c| c.len() * std::mem::size_of::<Vec4>());
349 let sizes_size = self
350 .sizes
351 .as_ref()
352 .map_or(0, |s| s.len() * std::mem::size_of::<f32>());
353 let vertices_size = self
354 .vertices
355 .as_ref()
356 .map_or(0, |v| v.len() * std::mem::size_of::<Vertex>());
357
358 positions_size + values_size + colors_size + sizes_size + vertices_size
359 }
360}
361
362#[derive(Debug, Clone)]
364pub struct PointCloudStatistics {
365 pub point_count: usize,
366 pub has_values: bool,
367 pub has_colors: bool,
368 pub has_sizes: bool,
369 pub memory_usage: usize,
370}
371
372pub mod matlab_compat {
374 use super::*;
375
376 pub fn scatter3(x: Vec<f64>, y: Vec<f64>, z: Vec<f64>) -> Result<PointCloudPlot, String> {
378 if x.len() != y.len() || y.len() != z.len() {
379 return Err("X, Y, and Z vectors must have the same length".to_string());
380 }
381
382 let positions: Vec<Vec3> = x
383 .into_iter()
384 .zip(y)
385 .zip(z)
386 .map(|((x, y), z)| Vec3::new(x as f32, y as f32, z as f32))
387 .collect();
388
389 Ok(PointCloudPlot::new(positions))
390 }
391
392 pub fn scatter3_with_colors(
394 x: Vec<f64>,
395 y: Vec<f64>,
396 z: Vec<f64>,
397 colors: Vec<Vec4>,
398 ) -> Result<PointCloudPlot, String> {
399 scatter3(x, y, z)?.with_colors(colors)
400 }
401
402 pub fn scatter3_with_values(
404 x: Vec<f64>,
405 y: Vec<f64>,
406 z: Vec<f64>,
407 values: Vec<f64>,
408 colormap: &str,
409 ) -> Result<PointCloudPlot, String> {
410 let cmap = match colormap {
411 "jet" => ColorMap::Jet,
412 "hot" => ColorMap::Hot,
413 "cool" => ColorMap::Cool,
414 "viridis" => ColorMap::Viridis,
415 "plasma" => ColorMap::Plasma,
416 "gray" | "grey" => ColorMap::Gray,
417 _ => return Err(format!("Unknown colormap: {colormap}")),
418 };
419
420 Ok(scatter3(x, y, z)?.with_values(values)?.with_colormap(cmap))
421 }
422
423 pub fn point_cloud_from_matrix(points: Vec<Vec<f64>>) -> Result<PointCloudPlot, String> {
425 if points.is_empty() {
426 return Err("Points matrix cannot be empty".to_string());
427 }
428
429 let dim = points[0].len();
430 if dim < 3 {
431 return Err("Points must have at least 3 dimensions (X, Y, Z)".to_string());
432 }
433
434 let positions: Vec<Vec3> = points
435 .into_iter()
436 .map(|point| {
437 if point.len() != dim {
438 Vec3::ZERO } else {
440 Vec3::new(point[0] as f32, point[1] as f32, point[2] as f32)
441 }
442 })
443 .collect();
444
445 Ok(PointCloudPlot::new(positions))
446 }
447}
448
449#[cfg(test)]
450mod tests {
451 use super::*;
452
453 #[test]
454 fn test_point_cloud_creation() {
455 let positions = vec![
456 Vec3::new(0.0, 0.0, 0.0),
457 Vec3::new(1.0, 1.0, 1.0),
458 Vec3::new(2.0, 2.0, 2.0),
459 ];
460
461 let cloud = PointCloudPlot::new(positions.clone());
462
463 assert_eq!(cloud.positions, positions);
464 assert_eq!(cloud.len(), 3);
465 assert!(!cloud.is_empty());
466 assert!(cloud.visible);
467 assert!(cloud.values.is_none());
468 assert!(cloud.colors.is_none());
469 assert!(cloud.sizes.is_none());
470 }
471
472 #[test]
473 fn test_point_cloud_with_values() {
474 let positions = vec![Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 1.0, 1.0)];
475 let values = vec![0.5, 1.5];
476
477 let cloud = PointCloudPlot::new(positions)
478 .with_values(values.clone())
479 .unwrap();
480
481 assert_eq!(cloud.values, Some(values));
482 }
483
484 #[test]
485 fn test_point_cloud_with_colors() {
486 let positions = vec![Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 1.0, 1.0)];
487 let colors = vec![Vec4::new(1.0, 0.0, 0.0, 1.0), Vec4::new(0.0, 1.0, 0.0, 1.0)];
488
489 let cloud = PointCloudPlot::new(positions)
490 .with_colors(colors.clone())
491 .unwrap();
492
493 assert_eq!(cloud.colors, Some(colors));
494 }
495
496 #[test]
497 fn test_point_cloud_validation() {
498 let positions = vec![Vec3::new(0.0, 0.0, 0.0)];
499 let wrong_values = vec![1.0, 2.0]; let result = PointCloudPlot::new(positions).with_values(wrong_values);
502 assert!(result.is_err());
503 }
504
505 #[test]
506 fn test_point_cloud_styling() {
507 let positions = vec![Vec3::new(0.0, 0.0, 0.0)];
508
509 let cloud = PointCloudPlot::new(positions)
510 .with_default_color(Vec4::new(1.0, 0.0, 0.0, 1.0))
511 .with_default_size(5.0)
512 .with_colormap(ColorMap::Hot)
513 .with_point_style(PointStyle::Sphere)
514 .with_size_mode(SizeMode::Proportional)
515 .with_label("Test Cloud");
516
517 assert_eq!(cloud.default_color, Vec4::new(1.0, 0.0, 0.0, 1.0));
518 assert_eq!(cloud.default_size, 5.0);
519 assert_eq!(cloud.colormap, ColorMap::Hot);
520 assert_eq!(cloud.point_style, PointStyle::Sphere);
521 assert_eq!(cloud.size_mode, SizeMode::Proportional);
522 assert_eq!(cloud.label, Some("Test Cloud".to_string()));
523 }
524
525 #[test]
526 fn test_point_cloud_bounds() {
527 let positions = vec![
528 Vec3::new(-1.0, -2.0, -3.0),
529 Vec3::new(1.0, 2.0, 3.0),
530 Vec3::new(0.0, 0.0, 0.0),
531 ];
532
533 let mut cloud = PointCloudPlot::new(positions);
534 let bounds = cloud.bounds();
535
536 assert!(bounds.min.x <= -1.0);
538 assert!(bounds.min.y <= -2.0);
539 assert!(bounds.min.z <= -3.0);
540 assert!(bounds.max.x >= 1.0);
541 assert!(bounds.max.y >= 2.0);
542 assert!(bounds.max.z >= 3.0);
543 }
544
545 #[test]
546 fn test_point_cloud_vertex_generation() {
547 let positions = vec![Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 1.0, 1.0)];
548
549 let mut cloud = PointCloudPlot::new(positions);
550 let vertices = cloud.generate_vertices();
551
552 assert_eq!(vertices.len(), 2);
553 assert_eq!(vertices[0].position, [0.0, 0.0, 0.0]);
554 assert_eq!(vertices[1].position, [1.0, 1.0, 1.0]);
555 }
556
557 #[test]
558 fn test_point_cloud_statistics() {
559 let positions = vec![
560 Vec3::new(0.0, 0.0, 0.0),
561 Vec3::new(1.0, 1.0, 1.0),
562 Vec3::new(2.0, 2.0, 2.0),
563 ];
564 let values = vec![0.0, 1.0, 2.0];
565
566 let cloud = PointCloudPlot::new(positions).with_values(values).unwrap();
567
568 let stats = cloud.statistics();
569
570 assert_eq!(stats.point_count, 3);
571 assert!(stats.has_values);
572 assert!(!stats.has_colors);
573 assert!(!stats.has_sizes);
574 assert!(stats.memory_usage > 0);
575 }
576
577 #[test]
578 fn test_matlab_compat() {
579 use super::matlab_compat::*;
580
581 let x = vec![0.0, 1.0, 2.0];
582 let y = vec![0.0, 1.0, 2.0];
583 let z = vec![0.0, 1.0, 2.0];
584
585 let cloud = scatter3(x.clone(), y.clone(), z.clone()).unwrap();
586 assert_eq!(cloud.len(), 3);
587
588 let values = vec![0.0, 0.5, 1.0];
589 let cloud_with_values = scatter3_with_values(x, y, z, values, "viridis").unwrap();
590 assert!(cloud_with_values.values.is_some());
591 assert_eq!(cloud_with_values.colormap, ColorMap::Viridis);
592 }
593}