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