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