use crate::core::{
vertex_utils, BoundingBox, DrawCall, GpuVertexBuffer, Material, PipelineType, RenderData,
Vertex,
};
use crate::plots::scatter::MarkerStyle;
use glam::{Vec3, Vec4};
#[derive(Clone, Copy, Debug)]
pub struct Scatter3GpuStyle {
pub color: Vec4,
pub edge_color: Vec4,
pub edge_thickness: f32,
pub marker_style: MarkerStyle,
pub filled: bool,
pub has_per_point_colors: bool,
pub edge_from_vertex_colors: bool,
}
#[derive(Debug, Clone)]
pub struct Scatter3Plot {
pub points: Vec<Vec3>,
pub colors: Vec<Vec4>,
pub point_size: f32,
pub point_sizes: Option<Vec<f32>>,
pub edge_color: Vec4,
pub edge_thickness: f32,
pub marker_style: MarkerStyle,
pub filled: bool,
pub edge_color_from_vertex_colors: bool,
pub label: Option<String>,
pub visible: bool,
vertices: Option<Vec<Vertex>>,
bounds: Option<BoundingBox>,
gpu_vertices: Option<GpuVertexBuffer>,
gpu_point_count: Option<usize>,
gpu_has_per_point_colors: bool,
}
impl Scatter3Plot {
pub fn new(points: Vec<Vec3>) -> Result<Self, String> {
let default_color = Vec4::new(0.1, 0.7, 0.3, 1.0);
let colors = vec![default_color; points.len()];
Ok(Self {
points,
colors,
point_size: 8.0,
point_sizes: None,
edge_color: default_color,
edge_thickness: 1.0,
marker_style: MarkerStyle::Circle,
filled: true,
edge_color_from_vertex_colors: false,
label: None,
visible: true,
vertices: None,
bounds: None,
gpu_vertices: None,
gpu_point_count: None,
gpu_has_per_point_colors: false,
})
}
pub fn from_gpu_buffer(
buffer: GpuVertexBuffer,
point_count: usize,
style: Scatter3GpuStyle,
point_size: f32,
bounds: BoundingBox,
) -> Self {
Self {
points: Vec::new(),
colors: vec![style.color],
point_size,
point_sizes: None,
edge_color: style.edge_color,
edge_thickness: style.edge_thickness,
marker_style: style.marker_style,
filled: style.filled,
edge_color_from_vertex_colors: style.edge_from_vertex_colors,
label: None,
visible: true,
vertices: None,
bounds: Some(bounds),
gpu_vertices: Some(buffer),
gpu_point_count: Some(point_count),
gpu_has_per_point_colors: style.has_per_point_colors,
}
}
pub fn with_color(mut self, color: Vec4) -> Self {
self.colors = vec![color; self.points.len()];
self.vertices = None;
self.gpu_vertices = None;
self.gpu_point_count = None;
self.gpu_has_per_point_colors = false;
self
}
pub fn with_colors(mut self, colors: Vec<Vec4>) -> Result<Self, String> {
if colors.len() != self.points.len() {
return Err(format!(
"Point cloud color count ({}) must match point count ({})",
colors.len(),
self.points.len()
));
}
self.colors = colors;
self.vertices = None;
self.gpu_vertices = None;
self.gpu_point_count = None;
self.gpu_has_per_point_colors = false;
Ok(self)
}
pub fn with_label<S: Into<String>>(mut self, label: S) -> Self {
self.label = Some(label.into());
self
}
pub fn with_point_size(mut self, size: f32) -> Self {
self.point_size = size.max(1.0);
self.point_sizes = None;
self.gpu_vertices = None;
self.gpu_point_count = None;
self.gpu_has_per_point_colors = false;
self
}
pub fn set_marker_style(&mut self, style: MarkerStyle) {
self.marker_style = style;
self.gpu_vertices = None;
self.gpu_point_count = None;
self.gpu_has_per_point_colors = false;
}
pub fn set_filled(&mut self, filled: bool) {
self.filled = filled;
self.gpu_vertices = None;
self.gpu_point_count = None;
self.gpu_has_per_point_colors = false;
}
pub fn set_edge_color(&mut self, color: Vec4) {
self.edge_color = color;
self.gpu_vertices = None;
self.gpu_point_count = None;
self.gpu_has_per_point_colors = false;
}
pub fn set_edge_thickness(&mut self, px: f32) {
self.edge_thickness = px.max(0.0);
self.gpu_vertices = None;
self.gpu_point_count = None;
self.gpu_has_per_point_colors = false;
}
pub fn set_edge_color_from_vertex(&mut self, enabled: bool) {
self.edge_color_from_vertex_colors = enabled;
self.gpu_vertices = None;
self.gpu_point_count = None;
self.gpu_has_per_point_colors = false;
}
pub fn set_visible(&mut self, visible: bool) {
self.visible = visible;
}
pub fn with_gpu_vertices(mut self, buffer: GpuVertexBuffer, point_count: usize) -> Self {
self.gpu_vertices = Some(buffer);
self.gpu_point_count = Some(point_count);
self.vertices = None;
self.gpu_has_per_point_colors = false;
self
}
pub fn set_point_sizes(&mut self, sizes: Vec<f32>) {
self.point_sizes = Some(sizes);
self.vertices = None;
self.gpu_vertices = None;
self.gpu_point_count = None;
self.gpu_has_per_point_colors = false;
}
fn ensure_vertices(&mut self) {
if self.vertices.is_none() {
let mut verts = vertex_utils::create_point_cloud(&self.points, &self.colors);
if let Some(sizes) = self.point_sizes.as_ref() {
for (idx, vertex) in verts.iter_mut().enumerate() {
let size = sizes.get(idx).copied().unwrap_or(self.point_size);
vertex.normal[2] = size;
}
} else {
for vertex in &mut verts {
vertex.normal[2] = self.point_size;
}
}
self.vertices = Some(verts);
}
}
fn ensure_bounds(&mut self) {
if self.bounds.is_none() {
self.bounds = Some(BoundingBox::from_points(&self.points));
}
}
pub fn estimated_memory_usage(&self) -> usize {
let gpu_bytes = self
.gpu_point_count
.map(|count| count * std::mem::size_of::<Vertex>())
.unwrap_or(0);
self.points.len() * std::mem::size_of::<Vec3>()
+ self.colors.len() * std::mem::size_of::<Vec4>()
+ self
.point_sizes
.as_ref()
.map(|sizes| sizes.len() * std::mem::size_of::<f32>())
.unwrap_or(0)
+ gpu_bytes
}
pub fn render_data(&mut self) -> RenderData {
let bounds = self.bounds();
let vertex_count = self.gpu_point_count.unwrap_or_else(|| {
self.ensure_vertices();
self.vertices
.as_ref()
.map(|v| v.len())
.unwrap_or(self.points.len())
});
let vertices = if self.gpu_vertices.is_some() {
Vec::new()
} else {
self.ensure_vertices();
self.vertices.clone().unwrap_or_default()
};
let is_multi_color = if self.gpu_vertices.is_some() {
self.gpu_has_per_point_colors || self.colors.len() > 1
} else if vertices.is_empty() {
false
} else {
let first = vertices[0].color;
vertices.iter().any(|v| v.color != first)
};
let has_vertex_colors = if self.gpu_vertices.is_some() {
self.gpu_has_per_point_colors
} else {
self.colors.len() > 1
};
let use_vertex_edge_color = self.edge_color_from_vertex_colors && has_vertex_colors;
let mut material = Material {
albedo: self.colors.first().copied().unwrap_or(Vec4::ONE),
roughness: self.edge_thickness,
metallic: match self.marker_style {
MarkerStyle::Circle => 0.0,
MarkerStyle::Square => 1.0,
MarkerStyle::Triangle => 2.0,
MarkerStyle::Diamond => 3.0,
MarkerStyle::Plus => 4.0,
MarkerStyle::Cross => 5.0,
MarkerStyle::Star => 6.0,
MarkerStyle::Hexagon => 7.0,
},
emissive: self.edge_color,
alpha_mode: crate::core::scene::AlphaMode::Blend,
double_sided: true,
};
if is_multi_color {
material.albedo.w = 0.0;
} else if self.filled {
material.albedo.w = 1.0;
}
material.emissive.w = if use_vertex_edge_color { 0.0 } else { 1.0 };
RenderData {
pipeline_type: PipelineType::Scatter3,
vertices,
indices: None,
gpu_vertices: self.gpu_vertices.clone(),
bounds: Some(bounds),
material,
draw_calls: vec![DrawCall {
vertex_offset: 0,
vertex_count,
index_offset: None,
index_count: None,
instance_count: 1,
}],
image: None,
}
}
pub fn bounds(&mut self) -> BoundingBox {
self.ensure_bounds();
self.bounds.unwrap_or_default()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scatter3_defaults() {
let points = vec![Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 2.0, 3.0)];
let cloud = Scatter3Plot::new(points.clone()).unwrap();
assert_eq!(cloud.points.len(), points.len());
assert_eq!(cloud.colors.len(), points.len());
assert!(cloud.visible);
}
#[test]
fn scatter3_custom_colors() {
let points = vec![Vec3::new(0.0, 0.0, 0.0)];
let colors = vec![Vec4::new(1.0, 0.0, 0.0, 1.0)];
let cloud = Scatter3Plot::new(points)
.unwrap()
.with_colors(colors)
.unwrap();
assert_eq!(cloud.colors[0], Vec4::new(1.0, 0.0, 0.0, 1.0));
}
#[test]
fn scatter3_render_data_contains_vertices() {
let points = vec![Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 1.0, 1.0)];
let mut cloud = Scatter3Plot::new(points).unwrap();
let render_data = cloud.render_data();
assert_eq!(render_data.vertices.len(), 2);
assert_eq!(render_data.pipeline_type, PipelineType::Scatter3);
}
#[test]
fn scatter3_marker_style_encodes_material_shape_channel() {
let points = vec![Vec3::new(0.0, 0.0, 0.0)];
let mut cloud = Scatter3Plot::new(points).unwrap();
cloud.set_marker_style(MarkerStyle::Diamond);
let render_data = cloud.render_data();
assert_eq!(render_data.material.metallic, 3.0);
}
#[test]
fn scatter3_default_material_uses_plot_color_not_white_override() {
let points = vec![Vec3::new(0.0, 0.0, 0.0)];
let mut cloud = Scatter3Plot::new(points).unwrap();
let render_data = cloud.render_data();
assert_ne!(render_data.material.albedo.truncate(), Vec4::ONE.truncate());
}
}