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 crate::plots::scatter::MarkerStyle;
8use glam::{Vec3, Vec4};
9
10#[derive(Clone, Copy, Debug)]
11pub struct Scatter3GpuStyle {
12    pub color: Vec4,
13    pub edge_color: Vec4,
14    pub edge_thickness: f32,
15    pub marker_style: MarkerStyle,
16    pub filled: bool,
17    pub has_per_point_colors: bool,
18    pub edge_from_vertex_colors: bool,
19}
20
21/// GPU-accelerated scatter3 plot for MATLAB semantics.
22#[derive(Debug, Clone)]
23pub struct Scatter3Plot {
24    /// Point positions in 3D space.
25    pub points: Vec<Vec3>,
26    /// Per-point RGBA colors.
27    pub colors: Vec<Vec4>,
28    /// Marker size in pixels.
29    pub point_size: f32,
30    /// Optional per-point marker sizes.
31    pub point_sizes: Option<Vec<f32>>,
32    /// Marker edge color.
33    pub edge_color: Vec4,
34    /// Marker edge thickness in pixels.
35    pub edge_thickness: f32,
36    /// Marker shape.
37    pub marker_style: MarkerStyle,
38    /// Whether marker faces are filled.
39    pub filled: bool,
40    /// Whether edge color should come from per-vertex colors.
41    pub edge_color_from_vertex_colors: bool,
42    /// Legend label.
43    pub label: Option<String>,
44    /// Visibility flag.
45    pub visible: bool,
46    vertices: Option<Vec<Vertex>>,
47    bounds: Option<BoundingBox>,
48    gpu_vertices: Option<GpuVertexBuffer>,
49    gpu_point_count: Option<usize>,
50    gpu_has_per_point_colors: bool,
51}
52
53impl Scatter3Plot {
54    /// Create a new scatter3 plot. Colors default to a blue colormap.
55    pub fn new(points: Vec<Vec3>) -> Result<Self, String> {
56        let default_color = Vec4::new(0.1, 0.7, 0.3, 1.0);
57        let colors = vec![default_color; points.len()];
58        Ok(Self {
59            points,
60            colors,
61            point_size: 8.0,
62            point_sizes: None,
63            edge_color: default_color,
64            edge_thickness: 1.0,
65            marker_style: MarkerStyle::Circle,
66            filled: true,
67            edge_color_from_vertex_colors: false,
68            label: None,
69            visible: true,
70            vertices: None,
71            bounds: None,
72            gpu_vertices: None,
73            gpu_point_count: None,
74            gpu_has_per_point_colors: false,
75        })
76    }
77
78    /// Build a scatter plot directly from a GPU vertex buffer, bypassing CPU copies.
79    pub fn from_gpu_buffer(
80        buffer: GpuVertexBuffer,
81        point_count: usize,
82        style: Scatter3GpuStyle,
83        point_size: f32,
84        bounds: BoundingBox,
85    ) -> Self {
86        Self {
87            points: Vec::new(),
88            colors: vec![style.color],
89            point_size,
90            point_sizes: None,
91            edge_color: style.edge_color,
92            edge_thickness: style.edge_thickness,
93            marker_style: style.marker_style,
94            filled: style.filled,
95            edge_color_from_vertex_colors: style.edge_from_vertex_colors,
96            label: None,
97            visible: true,
98            vertices: None,
99            bounds: Some(bounds),
100            gpu_vertices: Some(buffer),
101            gpu_point_count: Some(point_count),
102            gpu_has_per_point_colors: style.has_per_point_colors,
103        }
104    }
105
106    /// Override all point colors with a single RGBA value.
107    pub fn with_color(mut self, color: Vec4) -> Self {
108        self.colors = vec![color; self.points.len()];
109        self.vertices = None;
110        self.gpu_vertices = None;
111        self.gpu_point_count = None;
112        self.gpu_has_per_point_colors = false;
113        self
114    }
115
116    /// Supply per-point colors. Length must match the number of points.
117    pub fn with_colors(mut self, colors: Vec<Vec4>) -> Result<Self, String> {
118        if colors.len() != self.points.len() {
119            return Err(format!(
120                "Point cloud color count ({}) must match point count ({})",
121                colors.len(),
122                self.points.len()
123            ));
124        }
125        self.colors = colors;
126        self.vertices = None;
127        self.gpu_vertices = None;
128        self.gpu_point_count = None;
129        self.gpu_has_per_point_colors = false;
130        Ok(self)
131    }
132
133    /// Set the legend label.
134    pub fn with_label<S: Into<String>>(mut self, label: S) -> Self {
135        self.label = Some(label.into());
136        self
137    }
138
139    /// Set marker size in pixels.
140    pub fn with_point_size(mut self, size: f32) -> Self {
141        self.point_size = size.max(1.0);
142        self.point_sizes = None;
143        self.gpu_vertices = None;
144        self.gpu_point_count = None;
145        self.gpu_has_per_point_colors = false;
146        self
147    }
148
149    pub fn set_marker_style(&mut self, style: MarkerStyle) {
150        self.marker_style = style;
151        self.gpu_vertices = None;
152        self.gpu_point_count = None;
153        self.gpu_has_per_point_colors = false;
154    }
155
156    pub fn set_filled(&mut self, filled: bool) {
157        self.filled = filled;
158        self.gpu_vertices = None;
159        self.gpu_point_count = None;
160        self.gpu_has_per_point_colors = false;
161    }
162
163    pub fn set_edge_color(&mut self, color: Vec4) {
164        self.edge_color = color;
165        self.gpu_vertices = None;
166        self.gpu_point_count = None;
167        self.gpu_has_per_point_colors = false;
168    }
169
170    pub fn set_edge_thickness(&mut self, px: f32) {
171        self.edge_thickness = px.max(0.0);
172        self.gpu_vertices = None;
173        self.gpu_point_count = None;
174        self.gpu_has_per_point_colors = false;
175    }
176
177    pub fn set_edge_color_from_vertex(&mut self, enabled: bool) {
178        self.edge_color_from_vertex_colors = enabled;
179        self.gpu_vertices = None;
180        self.gpu_point_count = None;
181        self.gpu_has_per_point_colors = false;
182    }
183
184    /// Enable or disable visibility.
185    pub fn set_visible(&mut self, visible: bool) {
186        self.visible = visible;
187    }
188
189    /// Attach a GPU-resident vertex buffer that already encodes this point cloud in the renderer's vertex format.
190    /// When provided, the renderer can skip per-frame uploads and reuse the supplied buffer directly.
191    pub fn with_gpu_vertices(mut self, buffer: GpuVertexBuffer, point_count: usize) -> Self {
192        self.gpu_vertices = Some(buffer);
193        self.gpu_point_count = Some(point_count);
194        self.vertices = None;
195        self.gpu_has_per_point_colors = false;
196        self
197    }
198
199    /// Supply per-point sizes in pixels.
200    pub fn set_point_sizes(&mut self, sizes: Vec<f32>) {
201        self.point_sizes = Some(sizes);
202        self.vertices = None;
203        self.gpu_vertices = None;
204        self.gpu_point_count = None;
205        self.gpu_has_per_point_colors = false;
206    }
207
208    fn ensure_vertices(&mut self) {
209        if self.vertices.is_none() {
210            let mut verts = vertex_utils::create_point_cloud(&self.points, &self.colors);
211            if let Some(sizes) = self.point_sizes.as_ref() {
212                for (idx, vertex) in verts.iter_mut().enumerate() {
213                    let size = sizes.get(idx).copied().unwrap_or(self.point_size);
214                    vertex.normal[2] = size;
215                }
216            } else {
217                for vertex in &mut verts {
218                    vertex.normal[2] = self.point_size;
219                }
220            }
221            self.vertices = Some(verts);
222        }
223    }
224
225    fn ensure_bounds(&mut self) {
226        if self.bounds.is_none() {
227            self.bounds = Some(BoundingBox::from_points(&self.points));
228        }
229    }
230
231    /// Estimate memory required for this plot.
232    pub fn estimated_memory_usage(&self) -> usize {
233        let gpu_bytes = self
234            .gpu_point_count
235            .map(|count| count * std::mem::size_of::<Vertex>())
236            .unwrap_or(0);
237        self.points.len() * std::mem::size_of::<Vec3>()
238            + self.colors.len() * std::mem::size_of::<Vec4>()
239            + self
240                .point_sizes
241                .as_ref()
242                .map(|sizes| sizes.len() * std::mem::size_of::<f32>())
243                .unwrap_or(0)
244            + gpu_bytes
245    }
246
247    /// Generate render data for the renderer.
248    pub fn render_data(&mut self) -> RenderData {
249        let bounds = self.bounds();
250        let vertex_count = self.gpu_point_count.unwrap_or_else(|| {
251            self.ensure_vertices();
252            self.vertices
253                .as_ref()
254                .map(|v| v.len())
255                .unwrap_or(self.points.len())
256        });
257
258        let vertices = if self.gpu_vertices.is_some() {
259            Vec::new()
260        } else {
261            self.ensure_vertices();
262            self.vertices.clone().unwrap_or_default()
263        };
264
265        let is_multi_color = if self.gpu_vertices.is_some() {
266            self.gpu_has_per_point_colors || self.colors.len() > 1
267        } else if vertices.is_empty() {
268            false
269        } else {
270            let first = vertices[0].color;
271            vertices.iter().any(|v| v.color != first)
272        };
273        let has_vertex_colors = if self.gpu_vertices.is_some() {
274            self.gpu_has_per_point_colors
275        } else {
276            self.colors.len() > 1
277        };
278        let use_vertex_edge_color = self.edge_color_from_vertex_colors && has_vertex_colors;
279        let mut material = Material {
280            albedo: self.colors.first().copied().unwrap_or(Vec4::ONE),
281            roughness: self.edge_thickness,
282            metallic: match self.marker_style {
283                MarkerStyle::Circle => 0.0,
284                MarkerStyle::Square => 1.0,
285                MarkerStyle::Triangle => 2.0,
286                MarkerStyle::Diamond => 3.0,
287                MarkerStyle::Plus => 4.0,
288                MarkerStyle::Cross => 5.0,
289                MarkerStyle::Star => 6.0,
290                MarkerStyle::Hexagon => 7.0,
291            },
292            emissive: self.edge_color,
293            alpha_mode: crate::core::scene::AlphaMode::Blend,
294            double_sided: true,
295        };
296        if is_multi_color {
297            material.albedo.w = 0.0;
298        } else if self.filled {
299            material.albedo.w = 1.0;
300        }
301        material.emissive.w = if use_vertex_edge_color { 0.0 } else { 1.0 };
302
303        RenderData {
304            pipeline_type: PipelineType::Scatter3,
305            vertices,
306            indices: None,
307            gpu_vertices: self.gpu_vertices.clone(),
308            bounds: Some(bounds),
309            material,
310            draw_calls: vec![DrawCall {
311                vertex_offset: 0,
312                vertex_count,
313                index_offset: None,
314                index_count: None,
315                instance_count: 1,
316            }],
317            image: None,
318        }
319    }
320
321    /// Compute the axis-aligned bounding box.
322    pub fn bounds(&mut self) -> BoundingBox {
323        self.ensure_bounds();
324        self.bounds.unwrap_or_default()
325    }
326}
327
328#[cfg(test)]
329mod tests {
330    use super::*;
331
332    #[test]
333    fn scatter3_defaults() {
334        let points = vec![Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 2.0, 3.0)];
335        let cloud = Scatter3Plot::new(points.clone()).unwrap();
336        assert_eq!(cloud.points.len(), points.len());
337        assert_eq!(cloud.colors.len(), points.len());
338        assert!(cloud.visible);
339    }
340
341    #[test]
342    fn scatter3_custom_colors() {
343        let points = vec![Vec3::new(0.0, 0.0, 0.0)];
344        let colors = vec![Vec4::new(1.0, 0.0, 0.0, 1.0)];
345        let cloud = Scatter3Plot::new(points)
346            .unwrap()
347            .with_colors(colors)
348            .unwrap();
349        assert_eq!(cloud.colors[0], Vec4::new(1.0, 0.0, 0.0, 1.0));
350    }
351
352    #[test]
353    fn scatter3_render_data_contains_vertices() {
354        let points = vec![Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 1.0, 1.0)];
355        let mut cloud = Scatter3Plot::new(points).unwrap();
356        let render_data = cloud.render_data();
357        assert_eq!(render_data.vertices.len(), 2);
358        assert_eq!(render_data.pipeline_type, PipelineType::Scatter3);
359    }
360
361    #[test]
362    fn scatter3_marker_style_encodes_material_shape_channel() {
363        let points = vec![Vec3::new(0.0, 0.0, 0.0)];
364        let mut cloud = Scatter3Plot::new(points).unwrap();
365        cloud.set_marker_style(MarkerStyle::Diamond);
366        let render_data = cloud.render_data();
367        assert_eq!(render_data.material.metallic, 3.0);
368    }
369
370    #[test]
371    fn scatter3_default_material_uses_plot_color_not_white_override() {
372        let points = vec![Vec3::new(0.0, 0.0, 0.0)];
373        let mut cloud = Scatter3Plot::new(points).unwrap();
374        let render_data = cloud.render_data();
375        assert_ne!(render_data.material.albedo.truncate(), Vec4::ONE.truncate());
376    }
377}