1use 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#[derive(Debug, Clone)]
23pub struct Scatter3Plot {
24 pub points: Vec<Vec3>,
26 pub colors: Vec<Vec4>,
28 pub point_size: f32,
30 pub point_sizes: Option<Vec<f32>>,
32 pub edge_color: Vec4,
34 pub edge_thickness: f32,
36 pub marker_style: MarkerStyle,
38 pub filled: bool,
40 pub edge_color_from_vertex_colors: bool,
42 pub label: Option<String>,
44 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 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 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 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 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 pub fn with_label<S: Into<String>>(mut self, label: S) -> Self {
135 self.label = Some(label.into());
136 self
137 }
138
139 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 pub fn set_visible(&mut self, visible: bool) {
186 self.visible = visible;
187 }
188
189 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 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 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 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 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}