Skip to main content

runmat_plot/plots/
scatter3.rs

1//! 3D scatter plot implementation for MATLAB's `scatter3`.
2
3use crate::core::{
4    vertex_utils, BoundingBox, DrawCall, GpuVertexBuffer, Material, PipelineType, RenderData,
5    Vertex,
6};
7use glam::{Vec3, Vec4};
8
9/// GPU-accelerated scatter3 plot for MATLAB semantics.
10#[derive(Debug, Clone)]
11pub struct Scatter3Plot {
12    /// Point positions in 3D space.
13    pub points: Vec<Vec3>,
14    /// Per-point RGBA colors.
15    pub colors: Vec<Vec4>,
16    /// Marker size in pixels.
17    pub point_size: f32,
18    /// Optional per-point marker sizes.
19    pub point_sizes: Option<Vec<f32>>,
20    /// Legend label.
21    pub label: Option<String>,
22    /// Visibility flag.
23    pub visible: bool,
24    vertices: Option<Vec<Vertex>>,
25    bounds: Option<BoundingBox>,
26    gpu_vertices: Option<GpuVertexBuffer>,
27    gpu_point_count: Option<usize>,
28}
29
30impl Scatter3Plot {
31    /// Create a new scatter3 plot. Colors default to a blue colormap.
32    pub fn new(points: Vec<Vec3>) -> Result<Self, String> {
33        let default_color = Vec4::new(0.1, 0.7, 0.3, 1.0);
34        let colors = vec![default_color; points.len()];
35        Ok(Self {
36            points,
37            colors,
38            point_size: 8.0,
39            point_sizes: None,
40            label: None,
41            visible: true,
42            vertices: None,
43            bounds: None,
44            gpu_vertices: None,
45            gpu_point_count: None,
46        })
47    }
48
49    /// Build a scatter plot directly from a GPU vertex buffer, bypassing CPU copies.
50    pub fn from_gpu_buffer(
51        buffer: GpuVertexBuffer,
52        point_count: usize,
53        color: Vec4,
54        point_size: f32,
55        bounds: BoundingBox,
56    ) -> Self {
57        Self {
58            points: Vec::new(),
59            colors: vec![color],
60            point_size,
61            point_sizes: None,
62            label: None,
63            visible: true,
64            vertices: None,
65            bounds: Some(bounds),
66            gpu_vertices: Some(buffer),
67            gpu_point_count: Some(point_count),
68        }
69    }
70
71    /// Override all point colors with a single RGBA value.
72    pub fn with_color(mut self, color: Vec4) -> Self {
73        self.colors = vec![color; self.points.len()];
74        self.vertices = None;
75        self.gpu_vertices = None;
76        self.gpu_point_count = None;
77        self
78    }
79
80    /// Supply per-point colors. Length must match the number of points.
81    pub fn with_colors(mut self, colors: Vec<Vec4>) -> Result<Self, String> {
82        if colors.len() != self.points.len() {
83            return Err(format!(
84                "Point cloud color count ({}) must match point count ({})",
85                colors.len(),
86                self.points.len()
87            ));
88        }
89        self.colors = colors;
90        self.vertices = None;
91        self.gpu_vertices = None;
92        self.gpu_point_count = None;
93        Ok(self)
94    }
95
96    /// Set the legend label.
97    pub fn with_label<S: Into<String>>(mut self, label: S) -> Self {
98        self.label = Some(label.into());
99        self
100    }
101
102    /// Set marker size in pixels.
103    pub fn with_point_size(mut self, size: f32) -> Self {
104        self.point_size = size.max(1.0);
105        self.point_sizes = None;
106        self.gpu_vertices = None;
107        self.gpu_point_count = None;
108        self
109    }
110
111    /// Enable or disable visibility.
112    pub fn set_visible(&mut self, visible: bool) {
113        self.visible = visible;
114    }
115
116    /// Attach a GPU-resident vertex buffer that already encodes this point cloud in the renderer's vertex format.
117    /// When provided, the renderer can skip per-frame uploads and reuse the supplied buffer directly.
118    pub fn with_gpu_vertices(mut self, buffer: GpuVertexBuffer, point_count: usize) -> Self {
119        self.gpu_vertices = Some(buffer);
120        self.gpu_point_count = Some(point_count);
121        self.vertices = None;
122        self
123    }
124
125    /// Supply per-point sizes in pixels.
126    pub fn set_point_sizes(&mut self, sizes: Vec<f32>) {
127        self.point_sizes = Some(sizes);
128        self.vertices = None;
129        self.gpu_vertices = None;
130        self.gpu_point_count = None;
131    }
132
133    fn ensure_vertices(&mut self) {
134        if self.vertices.is_none() {
135            let mut verts = vertex_utils::create_point_cloud(&self.points, &self.colors);
136            if let Some(sizes) = self.point_sizes.as_ref() {
137                for (idx, vertex) in verts.iter_mut().enumerate() {
138                    let size = sizes.get(idx).copied().unwrap_or(self.point_size);
139                    vertex.normal[2] = size;
140                }
141            } else {
142                for vertex in &mut verts {
143                    vertex.normal[2] = self.point_size;
144                }
145            }
146            self.vertices = Some(verts);
147        }
148    }
149
150    fn ensure_bounds(&mut self) {
151        if self.bounds.is_none() {
152            self.bounds = Some(BoundingBox::from_points(&self.points));
153        }
154    }
155
156    /// Estimate memory required for this plot.
157    pub fn estimated_memory_usage(&self) -> usize {
158        let gpu_bytes = self
159            .gpu_point_count
160            .map(|count| count * std::mem::size_of::<Vertex>())
161            .unwrap_or(0);
162        self.points.len() * std::mem::size_of::<Vec3>()
163            + self.colors.len() * std::mem::size_of::<Vec4>()
164            + self
165                .point_sizes
166                .as_ref()
167                .map(|sizes| sizes.len() * std::mem::size_of::<f32>())
168                .unwrap_or(0)
169            + gpu_bytes
170    }
171
172    /// Generate render data for the renderer.
173    pub fn render_data(&mut self) -> RenderData {
174        let bounds = self.bounds();
175        let vertex_count = self.gpu_point_count.unwrap_or_else(|| {
176            self.ensure_vertices();
177            self.vertices
178                .as_ref()
179                .map(|v| v.len())
180                .unwrap_or(self.points.len())
181        });
182
183        let vertices = if self.gpu_vertices.is_some() {
184            Vec::new()
185        } else {
186            self.ensure_vertices();
187            self.vertices.clone().unwrap_or_default()
188        };
189
190        RenderData {
191            pipeline_type: PipelineType::Scatter3,
192            vertices,
193            indices: None,
194            gpu_vertices: self.gpu_vertices.clone(),
195            bounds: Some(bounds),
196            material: Material {
197                albedo: Vec4::ONE,
198                roughness: 0.0,
199                metallic: 0.0,
200                emissive: Vec4::ZERO,
201                alpha_mode: crate::core::scene::AlphaMode::Blend,
202                double_sided: true,
203            },
204            draw_calls: vec![DrawCall {
205                vertex_offset: 0,
206                vertex_count,
207                index_offset: None,
208                index_count: None,
209                instance_count: 1,
210            }],
211            image: None,
212        }
213    }
214
215    /// Compute the axis-aligned bounding box.
216    pub fn bounds(&mut self) -> BoundingBox {
217        self.ensure_bounds();
218        self.bounds.unwrap_or_default()
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    #[test]
227    fn scatter3_defaults() {
228        let points = vec![Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 2.0, 3.0)];
229        let cloud = Scatter3Plot::new(points.clone()).unwrap();
230        assert_eq!(cloud.points.len(), points.len());
231        assert_eq!(cloud.colors.len(), points.len());
232        assert!(cloud.visible);
233    }
234
235    #[test]
236    fn scatter3_custom_colors() {
237        let points = vec![Vec3::new(0.0, 0.0, 0.0)];
238        let colors = vec![Vec4::new(1.0, 0.0, 0.0, 1.0)];
239        let cloud = Scatter3Plot::new(points)
240            .unwrap()
241            .with_colors(colors)
242            .unwrap();
243        assert_eq!(cloud.colors[0], Vec4::new(1.0, 0.0, 0.0, 1.0));
244    }
245
246    #[test]
247    fn scatter3_render_data_contains_vertices() {
248        let points = vec![Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 1.0, 1.0)];
249        let mut cloud = Scatter3Plot::new(points).unwrap();
250        let render_data = cloud.render_data();
251        assert_eq!(render_data.vertices.len(), 2);
252        assert_eq!(render_data.pipeline_type, PipelineType::Scatter3);
253    }
254}