Skip to main content

runmat_plot/geometry/
stroke3d.rs

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        // Segments 0 and 4 are included for the first five points (0..3 exist here -> only 0).
440        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}