1use crate::core::{
4 vertex_utils, BoundingBox, DrawCall, GpuVertexBuffer, Material, PipelineType, RenderData,
5 Vertex,
6};
7use glam::{Vec3, Vec4};
8
9#[derive(Debug, Clone)]
10pub struct ContourPlot {
11 pub base_z: f32,
12 pub force_3d: bool,
13 pub label: Option<String>,
14 pub visible: bool,
15 pub line_width: f32,
16 vertices: Option<Vec<Vertex>>,
17 gpu_vertices: Option<GpuVertexBuffer>,
18 vertex_count: usize,
19 bounds: Option<BoundingBox>,
20}
21
22impl ContourPlot {
23 pub fn from_vertices(vertices: Vec<Vertex>, base_z: f32, bounds: BoundingBox) -> Self {
25 Self {
26 base_z,
27 force_3d: false,
28 label: None,
29 visible: true,
30 line_width: 1.0,
31 vertex_count: vertices.len(),
32 vertices: Some(vertices),
33 gpu_vertices: None,
34 bounds: Some(bounds),
35 }
36 }
37
38 pub fn from_gpu_buffer(
40 buffer: GpuVertexBuffer,
41 vertex_count: usize,
42 base_z: f32,
43 bounds: BoundingBox,
44 ) -> Self {
45 Self {
46 base_z,
47 force_3d: false,
48 label: None,
49 visible: true,
50 line_width: 1.0,
51 vertex_count,
52 vertices: None,
53 gpu_vertices: Some(buffer),
54 bounds: Some(bounds),
55 }
56 }
57
58 pub fn with_label<S: Into<String>>(mut self, label: S) -> Self {
59 self.label = Some(label.into());
60 self
61 }
62
63 pub fn with_force_3d(mut self, force_3d: bool) -> Self {
64 self.force_3d = force_3d;
65 self
66 }
67
68 pub fn is_3d(&self) -> bool {
69 self.force_3d || (self.bounds().max.z - self.bounds().min.z).abs() > f32::EPSILON
70 }
71
72 pub fn set_visible(&mut self, visible: bool) {
73 self.visible = visible;
74 }
75
76 pub fn with_line_width(mut self, line_width: f32) -> Self {
77 self.line_width = line_width.max(0.5);
78 self
79 }
80
81 pub fn vertices(&mut self) -> &Vec<Vertex> {
82 if self.vertices.is_none() {
83 self.vertices = Some(Vec::new());
84 }
85 self.vertices.as_ref().unwrap()
86 }
87
88 pub fn bounds(&self) -> BoundingBox {
89 self.bounds.unwrap_or_default()
90 }
91
92 pub fn cpu_vertices(&self) -> Option<&[Vertex]> {
93 self.vertices.as_deref()
94 }
95
96 pub fn render_data_with_viewport(&mut self, viewport_px: Option<(u32, u32)>) -> RenderData {
97 if self.gpu_vertices.is_some() {
98 return self.render_data();
99 }
100
101 let bounds = self.bounds();
102 let (vertices, vertex_count, pipeline_type, render_bounds) = if self.line_width > 1.0 {
103 let Some(viewport_px) = viewport_px else {
104 return self.render_data();
105 };
106 let data_per_px = crate::core::data_units_per_px(&bounds, viewport_px);
107 let width_data = self.line_width.max(0.1) * data_per_px;
108 let verts = self.vertices().clone();
109 let mut thick = Vec::new();
110 for segment in verts.chunks_exact(2) {
111 let color = Vec4::from_array(segment[0].color);
112 if self.is_3d() {
113 thick.extend(create_thick_segment_3d(
114 Vec3::from_array(segment[0].position),
115 Vec3::from_array(segment[1].position),
116 color,
117 width_data * 0.5,
118 ));
119 } else {
120 let x = [segment[0].position[0] as f64, segment[1].position[0] as f64];
121 let y = [segment[0].position[1] as f64, segment[1].position[1] as f64];
122 thick.extend(vertex_utils::create_thick_polyline(
123 &x, &y, color, width_data,
124 ));
125 }
126 }
127 let count = thick.len();
128 let render_bounds = expanded_bounds_for_vertices(bounds, &thick);
129 (thick, count, PipelineType::Triangles, render_bounds)
130 } else {
131 let verts = self.vertices().clone();
132 let count = verts.len();
133 (verts, count, PipelineType::Lines, bounds)
134 };
135
136 let material = Material {
137 albedo: Vec4::ONE,
138 roughness: self.line_width.max(0.0),
139 ..Default::default()
140 };
141
142 let draw_call = DrawCall {
143 vertex_offset: 0,
144 vertex_count,
145 index_offset: None,
146 index_count: None,
147 instance_count: 1,
148 };
149
150 RenderData {
151 pipeline_type,
152 vertices,
153 indices: None,
154 gpu_vertices: None,
155 bounds: Some(render_bounds),
156 material,
157 draw_calls: vec![draw_call],
158 image: None,
159 }
160 }
161
162 pub fn render_data(&mut self) -> RenderData {
163 let using_gpu = self.gpu_vertices.is_some();
164 let bounds = self.bounds();
165 let (vertices, vertex_count, gpu_vertices, pipeline_type, render_bounds) = if using_gpu {
166 (
167 Vec::new(),
168 self.vertex_count,
169 self.gpu_vertices.clone(),
170 PipelineType::Lines,
171 bounds,
172 )
173 } else {
174 let verts = self.vertices().clone();
175 if self.line_width > 1.0 {
176 let mut thick = Vec::new();
177 for segment in verts.chunks_exact(2) {
178 let color = Vec4::from_array(segment[0].color);
179 if self.is_3d() {
180 thick.extend(create_thick_segment_3d(
181 Vec3::from_array(segment[0].position),
182 Vec3::from_array(segment[1].position),
183 color,
184 self.line_width.max(0.5) * 0.01,
185 ));
186 } else {
187 let x = [segment[0].position[0] as f64, segment[1].position[0] as f64];
188 let y = [segment[0].position[1] as f64, segment[1].position[1] as f64];
189 thick.extend(vertex_utils::create_thick_polyline(
190 &x,
191 &y,
192 color,
193 self.line_width,
194 ));
195 }
196 }
197 let count = thick.len();
198 let render_bounds = expanded_bounds_for_vertices(bounds, &thick);
199 (thick, count, None, PipelineType::Triangles, render_bounds)
200 } else {
201 let count = verts.len();
202 (verts, count, None, PipelineType::Lines, bounds)
203 }
204 };
205
206 let material = Material {
207 albedo: Vec4::ONE,
208 roughness: self.line_width.max(0.0),
209 ..Default::default()
210 };
211
212 let draw_call = DrawCall {
213 vertex_offset: 0,
214 vertex_count,
215 index_offset: None,
216 index_count: None,
217 instance_count: 1,
218 };
219
220 RenderData {
221 pipeline_type,
222 vertices,
223 indices: None,
224 gpu_vertices,
225 bounds: Some(render_bounds),
226 material,
227 draw_calls: vec![draw_call],
228 image: None,
229 }
230 }
231
232 pub fn estimated_memory_usage(&self) -> usize {
233 self.vertices
234 .as_ref()
235 .map(|v| v.len() * std::mem::size_of::<Vertex>())
236 .unwrap_or(0)
237 }
238}
239
240pub fn contour_bounds(x_min: f32, x_max: f32, y_min: f32, y_max: f32, base_z: f32) -> BoundingBox {
241 BoundingBox::new(
242 Vec3::new(x_min, y_min, base_z),
243 Vec3::new(x_max, y_max, base_z),
244 )
245}
246
247pub fn contour_bounds_3d(
248 x_min: f32,
249 x_max: f32,
250 y_min: f32,
251 y_max: f32,
252 z_min: f32,
253 z_max: f32,
254) -> BoundingBox {
255 BoundingBox::new(
256 Vec3::new(x_min, y_min, z_min),
257 Vec3::new(x_max, y_max, z_max),
258 )
259}
260
261fn expanded_bounds_for_vertices(mut bounds: BoundingBox, vertices: &[Vertex]) -> BoundingBox {
262 for vertex in vertices {
263 bounds.expand(Vec3::from_array(vertex.position));
264 }
265 bounds
266}
267
268fn create_thick_segment_3d(start: Vec3, end: Vec3, color: Vec4, half_width: f32) -> Vec<Vertex> {
269 let dir = (end - start).normalize_or_zero();
270 let mut normal = dir.cross(Vec3::Z);
271 if normal.length_squared() < 1e-6 {
272 normal = dir.cross(Vec3::X);
273 }
274 let normal = normal.normalize_or_zero() * half_width.max(0.0001);
275 let v0 = start + normal;
276 let v1 = end + normal;
277 let v2 = end - normal;
278 let v3 = start - normal;
279 vec![
280 Vertex::new(v0, color),
281 Vertex::new(v1, color),
282 Vertex::new(v2, color),
283 Vertex::new(v0, color),
284 Vertex::new(v2, color),
285 Vertex::new(v3, color),
286 ]
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292
293 fn test_vertex(x: f32, y: f32, z: f32) -> Vertex {
294 Vertex::new(Vec3::new(x, y, z), Vec4::ONE)
295 }
296
297 #[test]
298 fn viewport_thick_contour_bounds_include_extruded_geometry() {
299 let bounds = BoundingBox::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0));
300 let mut contour = ContourPlot::from_vertices(
301 vec![test_vertex(0.0, 0.0, 0.0), test_vertex(1.0, 0.0, 0.0)],
302 0.0,
303 bounds,
304 )
305 .with_line_width(2.0);
306
307 let render_data = contour.render_data_with_viewport(Some((100, 100)));
308 let render_bounds = render_data.bounds.expect("bounds");
309
310 assert!(render_bounds.min.y < bounds.min.y);
311 assert!(render_bounds.max.y > bounds.max.y);
312 }
313
314 #[test]
315 fn non_viewport_thick_3d_contour_bounds_include_extruded_geometry() {
316 let bounds = BoundingBox::new(Vec3::new(0.0, 0.0, 1.0), Vec3::new(0.0, 1.0, 1.0));
317 let mut contour = ContourPlot::from_vertices(
318 vec![test_vertex(0.0, 0.0, 1.0), test_vertex(0.0, 1.0, 1.0)],
319 0.0,
320 bounds,
321 )
322 .with_force_3d(true)
323 .with_line_width(2.0);
324
325 let render_data = contour.render_data();
326 let render_bounds = render_data.bounds.expect("bounds");
327
328 assert!(render_bounds.min.x < bounds.min.x);
329 assert!(render_bounds.max.x > bounds.max.x);
330 }
331
332 #[test]
333 fn viewport_thick_3d_contour_uses_half_width_data() {
334 let bounds = BoundingBox::new(Vec3::new(0.0, 0.0, 1.0), Vec3::new(1.0, 1.0, 1.0));
335 let mut contour = ContourPlot::from_vertices(
336 vec![test_vertex(0.0, 0.0, 1.0), test_vertex(1.0, 0.0, 1.0)],
337 0.0,
338 bounds,
339 )
340 .with_force_3d(true)
341 .with_line_width(4.0);
342
343 let render_data = contour.render_data_with_viewport(Some((100, 100)));
344 let render_bounds = render_data.bounds.expect("bounds");
345
346 assert!((render_bounds.min.y - -0.02).abs() < 1e-6);
347 }
348}