1use crate::core::Vertex;
2use crate::plots::line::LineStyle;
3use glam::{Vec3, Vec4};
4
5const EPS: f32 = 1e-6;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum StrokeCap3D {
9 Butt,
10 Square,
11}
12
13#[derive(Debug, Clone, Copy)]
14pub struct StrokeStyle3D {
15 pub half_width_data: f32,
16 pub line_style: LineStyle,
17 pub cap: StrokeCap3D,
18}
19
20impl StrokeStyle3D {
21 pub fn new(half_width_data: f32, line_style: LineStyle, cap: StrokeCap3D) -> Self {
22 Self {
23 half_width_data,
24 line_style,
25 cap,
26 }
27 }
28}
29
30#[inline]
31pub fn line_style_includes_segment(segment: usize, style: LineStyle) -> bool {
32 match style {
33 LineStyle::None => false,
34 LineStyle::Solid => true,
35 LineStyle::Dashed => (segment % 4) < 2,
36 LineStyle::Dotted => segment.is_multiple_of(4),
37 LineStyle::DashDot => {
38 let m = segment % 6;
39 m < 2 || m == 3
40 }
41 }
42}
43
44pub fn create_line_vertices_dashed(points: &[Vec3], color: Vec4, style: LineStyle) -> Vec<Vertex> {
45 let mut out = Vec::new();
46 if points.len() < 2 {
47 return out;
48 }
49 for i in 0..points.len() - 1 {
50 if !line_style_includes_segment(i, style) {
51 continue;
52 }
53 let a = points[i];
54 let b = points[i + 1];
55 if !a.is_finite() || !b.is_finite() {
56 continue;
57 }
58 if (b - a).length_squared() <= EPS {
59 continue;
60 }
61 out.push(Vertex::new(a, color));
62 out.push(Vertex::new(b, color));
63 }
64 out
65}
66
67pub fn tessellate_polyline(points: &[Vec3], color: Vec4, style: StrokeStyle3D) -> Vec<Vertex> {
68 let mut out = Vec::new();
69 if points.len() < 2 {
70 return out;
71 }
72
73 let half_width = style.half_width_data.max(1e-6);
74 let sides: Vec<Vec3> = (0..points.len()).map(|i| side_at(points, i)).collect();
75
76 for i in 0..points.len() - 1 {
77 if !line_style_includes_segment(i, style.line_style) {
78 continue;
79 }
80 let start = points[i];
81 let end = points[i + 1];
82 if !start.is_finite() || !end.is_finite() {
83 continue;
84 }
85 let dir = end - start;
86 let len = dir.length();
87 if len <= EPS {
88 continue;
89 }
90 let dir_n = dir / len;
91 let mut a = start;
92 let mut b = end;
93 if style.cap == StrokeCap3D::Square {
94 if i == 0 {
95 a -= dir_n * half_width;
96 }
97 if i == points.len() - 2 {
98 b += dir_n * half_width;
99 }
100 }
101
102 let sa = sides[i] * half_width;
103 let sb = sides[i + 1] * half_width;
104 let v0 = a + sa;
105 let v1 = b + sb;
106 let v2 = b - sb;
107 let v3 = a - sa;
108
109 out.push(Vertex::new(v0, color));
110 out.push(Vertex::new(v1, color));
111 out.push(Vertex::new(v2, color));
112 out.push(Vertex::new(v0, color));
113 out.push(Vertex::new(v2, color));
114 out.push(Vertex::new(v3, color));
115 }
116
117 out
118}
119
120pub fn tessellate_polyline_tube(
121 points: &[Vec3],
122 color: Vec4,
123 style: StrokeStyle3D,
124 radial_segments: usize,
125) -> Vec<Vertex> {
126 let mut out = Vec::new();
127 if points.len() < 2 {
128 return out;
129 }
130
131 let half_width = style.half_width_data.max(1e-6);
132 let radial_segments = radial_segments.max(3);
133 let mut seg = 0usize;
134 while seg + 1 < points.len() {
135 if !line_style_includes_segment(seg, style.line_style)
136 || !is_valid_segment(points[seg], points[seg + 1])
137 {
138 seg += 1;
139 continue;
140 }
141
142 let run_start = seg;
143 let mut run_end = seg;
144 while run_end + 1 < points.len() - 1
145 && line_style_includes_segment(run_end + 1, style.line_style)
146 && is_valid_segment(points[run_end + 1], points[run_end + 2])
147 {
148 run_end += 1;
149 }
150
151 let run = &points[run_start..=run_end + 1];
152 out.extend(tessellate_tube_run(
153 run,
154 color,
155 half_width,
156 style.cap,
157 radial_segments,
158 ));
159 seg = run_end + 1;
160 }
161
162 out
163}
164
165fn side_at(points: &[Vec3], idx: usize) -> Vec3 {
166 let prev = prev_dir(points, idx);
167 let next = next_dir(points, idx);
168 let tangent = match (prev, next) {
169 (Some(a), Some(b)) => {
170 let s = a + b;
171 if s.length_squared() > EPS {
172 s.normalize()
173 } else {
174 b
175 }
176 }
177 (Some(a), None) => a,
178 (None, Some(b)) => b,
179 (None, None) => Vec3::X,
180 };
181
182 let mut ref_axis = if tangent.z.abs() < 0.95 {
183 Vec3::Z
184 } else {
185 Vec3::X
186 };
187 let mut side = tangent.cross(ref_axis);
188 if side.length_squared() <= EPS {
189 ref_axis = Vec3::Y;
190 side = tangent.cross(ref_axis);
191 }
192 if side.length_squared() <= EPS {
193 Vec3::Y
194 } else {
195 side.normalize()
196 }
197}
198
199fn prev_dir(points: &[Vec3], idx: usize) -> Option<Vec3> {
200 if idx == 0 {
201 return None;
202 }
203 let mut j = idx;
204 while j > 0 {
205 let a = points[j - 1];
206 let b = points[j];
207 if a.is_finite() && b.is_finite() {
208 let d = b - a;
209 let len2 = d.length_squared();
210 if len2 > EPS {
211 return Some(d / len2.sqrt());
212 }
213 }
214 j -= 1;
215 }
216 None
217}
218
219fn next_dir(points: &[Vec3], idx: usize) -> Option<Vec3> {
220 if idx + 1 >= points.len() {
221 return None;
222 }
223 let mut j = idx;
224 while j + 1 < points.len() {
225 let a = points[j];
226 let b = points[j + 1];
227 if a.is_finite() && b.is_finite() {
228 let d = b - a;
229 let len2 = d.length_squared();
230 if len2 > EPS {
231 return Some(d / len2.sqrt());
232 }
233 }
234 j += 1;
235 }
236 None
237}
238
239fn is_valid_segment(a: Vec3, b: Vec3) -> bool {
240 a.is_finite() && b.is_finite() && (b - a).length_squared() > EPS
241}
242
243fn tessellate_tube_run(
244 run: &[Vec3],
245 color: Vec4,
246 radius: f32,
247 cap: StrokeCap3D,
248 radial_segments: usize,
249) -> Vec<Vertex> {
250 let mut out = Vec::new();
251 if run.len() < 2 {
252 return out;
253 }
254
255 let tangents = run_tangents(run);
256 if tangents.is_empty() {
257 return out;
258 }
259 let (normals, binormals) = parallel_transport_frames(&tangents);
260 let mut centers = run.to_vec();
261 if cap == StrokeCap3D::Square {
262 centers[0] -= tangents[0] * radius;
263 let last = centers.len() - 1;
264 centers[last] += tangents[last] * radius;
265 }
266
267 let mut rings: Vec<Vec<Vec3>> = Vec::with_capacity(centers.len());
268 for i in 0..centers.len() {
269 let center = centers[i];
270 let n = normals[i];
271 let b = binormals[i];
272 let mut ring = Vec::with_capacity(radial_segments);
273 for s in 0..radial_segments {
274 let theta = std::f32::consts::TAU * (s as f32) / (radial_segments as f32);
275 let offset = n * theta.cos() * radius + b * theta.sin() * radius;
276 ring.push(center + offset);
277 }
278 rings.push(ring);
279 }
280
281 for i in 0..rings.len() - 1 {
282 let a = &rings[i];
283 let b = &rings[i + 1];
284 for s in 0..radial_segments {
285 let n = (s + 1) % radial_segments;
286 let v00 = a[s];
287 let v01 = a[n];
288 let v10 = b[s];
289 let v11 = b[n];
290 out.push(Vertex::new(v00, color));
291 out.push(Vertex::new(v10, color));
292 out.push(Vertex::new(v11, color));
293 out.push(Vertex::new(v00, color));
294 out.push(Vertex::new(v11, color));
295 out.push(Vertex::new(v01, color));
296 }
297 }
298
299 let start_center = centers[0];
300 let start_ring = &rings[0];
301 for s in 0..radial_segments {
302 let n = (s + 1) % radial_segments;
303 out.push(Vertex::new(start_center, color));
304 out.push(Vertex::new(start_ring[n], color));
305 out.push(Vertex::new(start_ring[s], color));
306 }
307 let last = rings.len() - 1;
308 let end_center = centers[last];
309 let end_ring = &rings[last];
310 for s in 0..radial_segments {
311 let n = (s + 1) % radial_segments;
312 out.push(Vertex::new(end_center, color));
313 out.push(Vertex::new(end_ring[s], color));
314 out.push(Vertex::new(end_ring[n], color));
315 }
316
317 out
318}
319
320fn run_tangents(run: &[Vec3]) -> Vec<Vec3> {
321 let mut seg_dirs = Vec::with_capacity(run.len().saturating_sub(1));
322 for i in 0..run.len() - 1 {
323 let d = run[i + 1] - run[i];
324 let len2 = d.length_squared();
325 if len2 <= EPS {
326 return Vec::new();
327 }
328 seg_dirs.push(d / len2.sqrt());
329 }
330 let mut tangents = Vec::with_capacity(run.len());
331 tangents.push(seg_dirs[0]);
332 for i in 1..run.len() - 1 {
333 let s = seg_dirs[i - 1] + seg_dirs[i];
334 if s.length_squared() > EPS {
335 tangents.push(s.normalize());
336 } else {
337 tangents.push(seg_dirs[i]);
338 }
339 }
340 tangents.push(*seg_dirs.last().unwrap_or(&Vec3::X));
341 tangents
342}
343
344fn parallel_transport_frames(tangents: &[Vec3]) -> (Vec<Vec3>, Vec<Vec3>) {
345 let mut normals = Vec::with_capacity(tangents.len());
346 let mut binormals = Vec::with_capacity(tangents.len());
347
348 let t0 = tangents[0];
349 let mut n0 = orthogonal_unit(t0);
350 let mut b0 = t0.cross(n0);
351 if b0.length_squared() <= EPS {
352 n0 = Vec3::Y;
353 b0 = t0.cross(n0);
354 }
355 b0 = b0.normalize_or_zero();
356 n0 = b0.cross(t0).normalize_or_zero();
357 normals.push(n0);
358 binormals.push(b0);
359
360 for i in 1..tangents.len() {
361 let prev_t = tangents[i - 1];
362 let t = tangents[i];
363 let mut n = normals[i - 1];
364
365 let axis = prev_t.cross(t);
366 let axis_len = axis.length();
367 if axis_len > EPS {
368 let axis_u = axis / axis_len;
369 let cos_theta = prev_t.dot(t).clamp(-1.0, 1.0);
370 let sin_theta = axis_len.clamp(0.0, 1.0);
371 n = rotate_about_axis(n, axis_u, cos_theta, sin_theta);
372 }
373
374 n = (n - t * n.dot(t)).normalize_or_zero();
375 if n.length_squared() <= EPS {
376 n = orthogonal_unit(t);
377 }
378 let mut b = t.cross(n).normalize_or_zero();
379 if b.length_squared() <= EPS {
380 b = orthogonal_unit(t.cross(Vec3::X));
381 }
382 n = b.cross(t).normalize_or_zero();
383 normals.push(n);
384 binormals.push(b);
385 }
386
387 (normals, binormals)
388}
389
390fn orthogonal_unit(t: Vec3) -> Vec3 {
391 let ref_axis = if t.z.abs() < 0.95 { Vec3::Z } else { Vec3::X };
392 let mut n = ref_axis - t * ref_axis.dot(t);
393 if n.length_squared() <= EPS {
394 n = Vec3::Y - t * Vec3::Y.dot(t);
395 }
396 if n.length_squared() <= EPS {
397 Vec3::X
398 } else {
399 n.normalize()
400 }
401}
402
403fn rotate_about_axis(v: Vec3, axis_u: Vec3, cos_theta: f32, sin_theta: f32) -> Vec3 {
404 v * cos_theta + axis_u.cross(v) * sin_theta + axis_u * axis_u.dot(v) * (1.0 - cos_theta)
405}
406
407#[cfg(test)]
408mod tests {
409 use super::*;
410
411 #[test]
412 fn tessellated_polyline_has_continuous_shared_joint_side() {
413 let points = vec![
414 Vec3::new(0.0, 0.0, 0.0),
415 Vec3::new(1.0, 0.5, 0.0),
416 Vec3::new(2.0, 0.0, 0.0),
417 ];
418 let tris = tessellate_polyline(
419 &points,
420 Vec4::ONE,
421 StrokeStyle3D::new(0.1, LineStyle::Solid, StrokeCap3D::Butt),
422 );
423 assert_eq!(tris.len(), 12);
424 let shared_a = Vec3::from_array(tris[1].position);
425 let shared_b = Vec3::from_array(tris[6].position);
426 assert!((shared_a - shared_b).length() < 1e-5);
427 }
428
429 #[test]
430 fn dotted_style_uses_sparse_single_segments() {
431 let points = vec![
432 Vec3::new(0.0, 0.0, 0.0),
433 Vec3::new(1.0, 0.0, 0.0),
434 Vec3::new(2.0, 0.0, 0.0),
435 Vec3::new(3.0, 0.0, 0.0),
436 Vec3::new(4.0, 0.0, 0.0),
437 ];
438 let verts = create_line_vertices_dashed(&points, Vec4::ONE, LineStyle::Dotted);
439 assert_eq!(verts.len(), 2);
441 }
442
443 #[test]
444 fn tube_tessellation_produces_dense_triangles_for_visible_3d_width() {
445 let points = vec![
446 Vec3::new(0.0, 0.0, 0.0),
447 Vec3::new(1.0, 0.5, 0.25),
448 Vec3::new(2.0, 0.0, 0.5),
449 ];
450 let verts = tessellate_polyline_tube(
451 &points,
452 Vec4::ONE,
453 StrokeStyle3D::new(0.1, LineStyle::Solid, StrokeCap3D::Square),
454 8,
455 );
456 assert!(!verts.is_empty());
457 assert!(verts.len() > 48);
458 }
459}