Skip to main content

agg_rust/
math_stroke.rs

1//! Stroke math — cap, join, and miter calculations for stroked paths.
2//!
3//! Port of `agg_math_stroke.h` — provides the geometry calculations for
4//! converting a path outline into a stroked polygon with configurable
5//! line caps, line joins, and miter limits.
6
7use crate::array::VertexDist;
8use crate::basics::{PointD, PI};
9use crate::math::{calc_distance, calc_intersection, cross_product};
10
11// ============================================================================
12// Enums
13// ============================================================================
14
15/// Line cap style for path endpoints.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum LineCap {
18    Butt = 0,
19    Square = 1,
20    Round = 2,
21}
22
23/// Line join style at path corners.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum LineJoin {
26    Miter = 0,
27    MiterRevert = 1,
28    Round = 2,
29    Bevel = 3,
30    MiterRound = 4,
31}
32
33/// Inner join style at sharp inward corners.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum InnerJoin {
36    Bevel = 0,
37    Miter = 1,
38    Jag = 2,
39    Round = 3,
40}
41
42// ============================================================================
43// MathStroke
44// ============================================================================
45
46/// Stroke geometry calculator.
47///
48/// Computes cap and join vertices for stroked paths. Output vertices are
49/// pushed into a `Vec<PointD>` consumer.
50///
51/// Port of C++ `agg::math_stroke<VC>`.
52pub struct MathStroke {
53    width: f64,
54    width_abs: f64,
55    width_eps: f64,
56    width_sign: i32,
57    miter_limit: f64,
58    inner_miter_limit: f64,
59    approx_scale: f64,
60    line_cap: LineCap,
61    line_join: LineJoin,
62    inner_join: InnerJoin,
63}
64
65impl MathStroke {
66    pub fn new() -> Self {
67        Self {
68            width: 0.5,
69            width_abs: 0.5,
70            width_eps: 0.5 / 1024.0,
71            width_sign: 1,
72            miter_limit: 4.0,
73            inner_miter_limit: 1.01,
74            approx_scale: 1.0,
75            line_cap: LineCap::Butt,
76            line_join: LineJoin::Miter,
77            inner_join: InnerJoin::Miter,
78        }
79    }
80
81    pub fn set_line_cap(&mut self, lc: LineCap) {
82        self.line_cap = lc;
83    }
84    pub fn line_cap(&self) -> LineCap {
85        self.line_cap
86    }
87
88    pub fn set_line_join(&mut self, lj: LineJoin) {
89        self.line_join = lj;
90    }
91    pub fn line_join(&self) -> LineJoin {
92        self.line_join
93    }
94
95    pub fn set_inner_join(&mut self, ij: InnerJoin) {
96        self.inner_join = ij;
97    }
98    pub fn inner_join(&self) -> InnerJoin {
99        self.inner_join
100    }
101
102    pub fn set_width(&mut self, w: f64) {
103        self.width = w * 0.5;
104        if self.width < 0.0 {
105            self.width_abs = -self.width;
106            self.width_sign = -1;
107        } else {
108            self.width_abs = self.width;
109            self.width_sign = 1;
110        }
111        self.width_eps = self.width / 1024.0;
112    }
113
114    pub fn width(&self) -> f64 {
115        self.width * 2.0
116    }
117
118    pub fn set_miter_limit(&mut self, ml: f64) {
119        self.miter_limit = ml;
120    }
121    pub fn miter_limit(&self) -> f64 {
122        self.miter_limit
123    }
124
125    pub fn set_miter_limit_theta(&mut self, t: f64) {
126        self.miter_limit = 1.0 / (t * 0.5).sin();
127    }
128
129    pub fn set_inner_miter_limit(&mut self, ml: f64) {
130        self.inner_miter_limit = ml;
131    }
132    pub fn inner_miter_limit(&self) -> f64 {
133        self.inner_miter_limit
134    }
135
136    pub fn set_approximation_scale(&mut self, s: f64) {
137        self.approx_scale = s;
138    }
139    pub fn approximation_scale(&self) -> f64 {
140        self.approx_scale
141    }
142
143    /// Calculate cap vertices at a line endpoint.
144    ///
145    /// Output is pushed to `vc`. `v0` is the endpoint, `v1` is the adjacent
146    /// vertex, `len` is the distance between them.
147    pub fn calc_cap(&self, vc: &mut Vec<PointD>, v0: &VertexDist, v1: &VertexDist, len: f64) {
148        vc.clear();
149
150        let mut dx1 = (v1.y - v0.y) / len;
151        let mut dy1 = (v1.x - v0.x) / len;
152        let mut dx2 = 0.0;
153        let mut dy2 = 0.0;
154
155        dx1 *= self.width;
156        dy1 *= self.width;
157
158        if self.line_cap != LineCap::Round {
159            if self.line_cap == LineCap::Square {
160                dx2 = dy1 * self.width_sign as f64;
161                dy2 = dx1 * self.width_sign as f64;
162            }
163            vc.push(PointD {
164                x: v0.x - dx1 - dx2,
165                y: v0.y + dy1 - dy2,
166            });
167            vc.push(PointD {
168                x: v0.x + dx1 - dx2,
169                y: v0.y - dy1 - dy2,
170            });
171        } else {
172            let da = (self.width_abs / (self.width_abs + 0.125 / self.approx_scale)).acos() * 2.0;
173            let n = (PI / da) as i32;
174            let da = PI / (n + 1) as f64;
175
176            vc.push(PointD {
177                x: v0.x - dx1,
178                y: v0.y + dy1,
179            });
180
181            if self.width_sign > 0 {
182                let mut a1 = dy1.atan2(-dx1);
183                a1 += da;
184                for _ in 0..n {
185                    vc.push(PointD {
186                        x: v0.x + a1.cos() * self.width,
187                        y: v0.y + a1.sin() * self.width,
188                    });
189                    a1 += da;
190                }
191            } else {
192                let mut a1 = (-dy1).atan2(dx1);
193                a1 -= da;
194                for _ in 0..n {
195                    vc.push(PointD {
196                        x: v0.x + a1.cos() * self.width,
197                        y: v0.y + a1.sin() * self.width,
198                    });
199                    a1 -= da;
200                }
201            }
202
203            vc.push(PointD {
204                x: v0.x + dx1,
205                y: v0.y - dy1,
206            });
207        }
208    }
209
210    /// Calculate join vertices at the junction of two line segments.
211    ///
212    /// `v0`→`v1` is the first segment, `v1`→`v2` is the second.
213    /// `len1` and `len2` are the segment lengths.
214    pub fn calc_join(
215        &self,
216        vc: &mut Vec<PointD>,
217        v0: &VertexDist,
218        v1: &VertexDist,
219        v2: &VertexDist,
220        len1: f64,
221        len2: f64,
222    ) {
223        let dx1 = self.width * (v1.y - v0.y) / len1;
224        let dy1 = self.width * (v1.x - v0.x) / len1;
225        let dx2 = self.width * (v2.y - v1.y) / len2;
226        let dy2 = self.width * (v2.x - v1.x) / len2;
227
228        vc.clear();
229
230        let cp = cross_product(v0.x, v0.y, v1.x, v1.y, v2.x, v2.y);
231        if cp != 0.0 && (cp > 0.0) == (self.width > 0.0) {
232            // Inner join
233            let mut limit = if len1 < len2 { len1 } else { len2 } / self.width_abs;
234            if limit < self.inner_miter_limit {
235                limit = self.inner_miter_limit;
236            }
237
238            match self.inner_join {
239                InnerJoin::Bevel => {
240                    vc.push(PointD {
241                        x: v1.x + dx1,
242                        y: v1.y - dy1,
243                    });
244                    vc.push(PointD {
245                        x: v1.x + dx2,
246                        y: v1.y - dy2,
247                    });
248                }
249                InnerJoin::Miter => {
250                    self.calc_miter(
251                        vc,
252                        v0,
253                        v1,
254                        v2,
255                        dx1,
256                        dy1,
257                        dx2,
258                        dy2,
259                        LineJoin::MiterRevert,
260                        limit,
261                        0.0,
262                    );
263                }
264                InnerJoin::Jag | InnerJoin::Round => {
265                    let d = (dx1 - dx2) * (dx1 - dx2) + (dy1 - dy2) * (dy1 - dy2);
266                    if d < len1 * len1 && d < len2 * len2 {
267                        self.calc_miter(
268                            vc,
269                            v0,
270                            v1,
271                            v2,
272                            dx1,
273                            dy1,
274                            dx2,
275                            dy2,
276                            LineJoin::MiterRevert,
277                            limit,
278                            0.0,
279                        );
280                    } else if self.inner_join == InnerJoin::Jag {
281                        vc.push(PointD {
282                            x: v1.x + dx1,
283                            y: v1.y - dy1,
284                        });
285                        vc.push(PointD { x: v1.x, y: v1.y });
286                        vc.push(PointD {
287                            x: v1.x + dx2,
288                            y: v1.y - dy2,
289                        });
290                    } else {
291                        vc.push(PointD {
292                            x: v1.x + dx1,
293                            y: v1.y - dy1,
294                        });
295                        vc.push(PointD { x: v1.x, y: v1.y });
296                        self.calc_arc(vc, v1.x, v1.y, dx2, -dy2, dx1, -dy1);
297                        vc.push(PointD { x: v1.x, y: v1.y });
298                        vc.push(PointD {
299                            x: v1.x + dx2,
300                            y: v1.y - dy2,
301                        });
302                    }
303                }
304            }
305        } else {
306            // Outer join
307            let dx = (dx1 + dx2) / 2.0;
308            let dy = (dy1 + dy2) / 2.0;
309            let dbevel = (dx * dx + dy * dy).sqrt();
310
311            if (self.line_join == LineJoin::Round || self.line_join == LineJoin::Bevel)
312                && self.approx_scale * (self.width_abs - dbevel) < self.width_eps
313            {
314                if let Some((ix, iy)) = calc_intersection(
315                    v0.x + dx1,
316                    v0.y - dy1,
317                    v1.x + dx1,
318                    v1.y - dy1,
319                    v1.x + dx2,
320                    v1.y - dy2,
321                    v2.x + dx2,
322                    v2.y - dy2,
323                ) {
324                    vc.push(PointD { x: ix, y: iy });
325                } else {
326                    vc.push(PointD {
327                        x: v1.x + dx1,
328                        y: v1.y - dy1,
329                    });
330                }
331                return;
332            }
333
334            match self.line_join {
335                LineJoin::Miter | LineJoin::MiterRevert | LineJoin::MiterRound => {
336                    self.calc_miter(
337                        vc,
338                        v0,
339                        v1,
340                        v2,
341                        dx1,
342                        dy1,
343                        dx2,
344                        dy2,
345                        self.line_join,
346                        self.miter_limit,
347                        dbevel,
348                    );
349                }
350                LineJoin::Round => {
351                    self.calc_arc(vc, v1.x, v1.y, dx1, -dy1, dx2, -dy2);
352                }
353                LineJoin::Bevel => {
354                    vc.push(PointD {
355                        x: v1.x + dx1,
356                        y: v1.y - dy1,
357                    });
358                    vc.push(PointD {
359                        x: v1.x + dx2,
360                        y: v1.y - dy2,
361                    });
362                }
363            }
364        }
365    }
366
367    fn add_vertex(vc: &mut Vec<PointD>, x: f64, y: f64) {
368        vc.push(PointD { x, y });
369    }
370
371    #[allow(clippy::too_many_arguments)]
372    fn calc_arc(
373        &self,
374        vc: &mut Vec<PointD>,
375        x: f64,
376        y: f64,
377        dx1: f64,
378        dy1: f64,
379        dx2: f64,
380        dy2: f64,
381    ) {
382        let mut a1 = (dy1 * self.width_sign as f64).atan2(dx1 * self.width_sign as f64);
383        let a2_init = (dy2 * self.width_sign as f64).atan2(dx2 * self.width_sign as f64);
384
385        let da = (self.width_abs / (self.width_abs + 0.125 / self.approx_scale)).acos() * 2.0;
386
387        Self::add_vertex(vc, x + dx1, y + dy1);
388
389        if self.width_sign > 0 {
390            let mut a2 = a2_init;
391            if a1 > a2 {
392                a2 += 2.0 * PI;
393            }
394            let n = ((a2 - a1) / da) as i32;
395            let da = (a2 - a1) / (n + 1) as f64;
396            a1 += da;
397            for _ in 0..n {
398                Self::add_vertex(vc, x + a1.cos() * self.width, y + a1.sin() * self.width);
399                a1 += da;
400            }
401        } else {
402            let mut a2 = a2_init;
403            if a1 < a2 {
404                a2 -= 2.0 * PI;
405            }
406            let n = ((a1 - a2) / da) as i32;
407            let da = (a1 - a2) / (n + 1) as f64;
408            a1 -= da;
409            for _ in 0..n {
410                Self::add_vertex(vc, x + a1.cos() * self.width, y + a1.sin() * self.width);
411                a1 -= da;
412            }
413        }
414
415        Self::add_vertex(vc, x + dx2, y + dy2);
416    }
417
418    #[allow(clippy::too_many_arguments)]
419    fn calc_miter(
420        &self,
421        vc: &mut Vec<PointD>,
422        v0: &VertexDist,
423        v1: &VertexDist,
424        v2: &VertexDist,
425        dx1: f64,
426        dy1: f64,
427        dx2: f64,
428        dy2: f64,
429        lj: LineJoin,
430        mut mlimit: f64,
431        dbevel: f64,
432    ) {
433        let mut xi = v1.x;
434        let mut yi = v1.y;
435        let mut di = 1.0;
436        let lim = self.width_abs * mlimit;
437        let mut miter_limit_exceeded = true;
438        let mut intersection_failed = true;
439
440        if let Some((ix, iy)) = calc_intersection(
441            v0.x + dx1,
442            v0.y - dy1,
443            v1.x + dx1,
444            v1.y - dy1,
445            v1.x + dx2,
446            v1.y - dy2,
447            v2.x + dx2,
448            v2.y - dy2,
449        ) {
450            xi = ix;
451            yi = iy;
452            di = calc_distance(v1.x, v1.y, xi, yi);
453            if di <= lim {
454                Self::add_vertex(vc, xi, yi);
455                miter_limit_exceeded = false;
456            }
457            intersection_failed = false;
458        } else {
459            let x2 = v1.x + dx1;
460            let y2 = v1.y - dy1;
461            if (cross_product(v0.x, v0.y, v1.x, v1.y, x2, y2) < 0.0)
462                == (cross_product(v1.x, v1.y, v2.x, v2.y, x2, y2) < 0.0)
463            {
464                Self::add_vertex(vc, v1.x + dx1, v1.y - dy1);
465                miter_limit_exceeded = false;
466            }
467        }
468
469        if miter_limit_exceeded {
470            match lj {
471                LineJoin::MiterRevert => {
472                    Self::add_vertex(vc, v1.x + dx1, v1.y - dy1);
473                    Self::add_vertex(vc, v1.x + dx2, v1.y - dy2);
474                }
475                LineJoin::MiterRound => {
476                    self.calc_arc(vc, v1.x, v1.y, dx1, -dy1, dx2, -dy2);
477                }
478                _ => {
479                    if intersection_failed {
480                        mlimit *= self.width_sign as f64;
481                        Self::add_vertex(vc, v1.x + dx1 + dy1 * mlimit, v1.y - dy1 + dx1 * mlimit);
482                        Self::add_vertex(vc, v1.x + dx2 - dy2 * mlimit, v1.y - dy2 - dx2 * mlimit);
483                    } else {
484                        let x1 = v1.x + dx1;
485                        let y1 = v1.y - dy1;
486                        let x2 = v1.x + dx2;
487                        let y2 = v1.y - dy2;
488                        di = (lim - dbevel) / (di - dbevel);
489                        Self::add_vertex(vc, x1 + (xi - x1) * di, y1 + (yi - y1) * di);
490                        Self::add_vertex(vc, x2 + (xi - x2) * di, y2 + (yi - y2) * di);
491                    }
492                }
493            }
494        }
495    }
496}
497
498impl Default for MathStroke {
499    fn default() -> Self {
500        Self::new()
501    }
502}
503
504// ============================================================================
505// Tests
506// ============================================================================
507
508#[cfg(test)]
509mod tests {
510    use super::*;
511
512    fn vd(x: f64, y: f64, dist: f64) -> VertexDist {
513        VertexDist { x, y, dist }
514    }
515
516    #[test]
517    fn test_defaults() {
518        let ms = MathStroke::new();
519        assert!((ms.width() - 1.0).abs() < 1e-10); // default width = 0.5 * 2
520        assert_eq!(ms.line_cap(), LineCap::Butt);
521        assert_eq!(ms.line_join(), LineJoin::Miter);
522        assert_eq!(ms.inner_join(), InnerJoin::Miter);
523        assert!((ms.miter_limit() - 4.0).abs() < 1e-10);
524        assert!((ms.inner_miter_limit() - 1.01).abs() < 1e-10);
525        assert!((ms.approximation_scale() - 1.0).abs() < 1e-10);
526    }
527
528    #[test]
529    fn test_width_setter() {
530        let mut ms = MathStroke::new();
531        ms.set_width(2.0);
532        assert!((ms.width() - 2.0).abs() < 1e-10);
533
534        ms.set_width(-2.0);
535        assert!((ms.width() + 2.0).abs() < 1e-10);
536    }
537
538    #[test]
539    fn test_butt_cap() {
540        let ms = MathStroke::new();
541        let mut vc = Vec::new();
542        let v0 = vd(0.0, 0.0, 10.0);
543        let v1 = vd(10.0, 0.0, 0.0);
544        ms.calc_cap(&mut vc, &v0, &v1, 10.0);
545        // Butt cap: 2 vertices
546        assert_eq!(vc.len(), 2);
547        // Perpendicular offset of ±width (0.5)
548        assert!((vc[0].y - 0.5).abs() < 1e-6);
549        assert!((vc[1].y + 0.5).abs() < 1e-6);
550    }
551
552    #[test]
553    fn test_square_cap() {
554        let mut ms = MathStroke::new();
555        ms.set_line_cap(LineCap::Square);
556        let mut vc = Vec::new();
557        let v0 = vd(0.0, 0.0, 10.0);
558        let v1 = vd(10.0, 0.0, 0.0);
559        ms.calc_cap(&mut vc, &v0, &v1, 10.0);
560        assert_eq!(vc.len(), 2);
561        // Square cap extends by width beyond the endpoint
562        assert!(vc[0].x < 0.0); // Extended backward
563    }
564
565    #[test]
566    fn test_round_cap() {
567        let mut ms = MathStroke::new();
568        ms.set_line_cap(LineCap::Round);
569        let mut vc = Vec::new();
570        let v0 = vd(0.0, 0.0, 10.0);
571        let v1 = vd(10.0, 0.0, 0.0);
572        ms.calc_cap(&mut vc, &v0, &v1, 10.0);
573        // Round cap: more than 2 vertices (arc)
574        assert!(vc.len() > 2);
575        // All points should be within width distance from v0
576        for p in &vc {
577            let d = (p.x * p.x + p.y * p.y).sqrt();
578            assert!(d < ms.width() + 1e-6);
579        }
580    }
581
582    #[test]
583    fn test_bevel_join() {
584        let mut ms = MathStroke::new();
585        ms.set_line_join(LineJoin::Bevel);
586        let mut vc = Vec::new();
587        let v0 = vd(0.0, 0.0, 10.0);
588        let v1 = vd(10.0, 0.0, 10.0);
589        let v2 = vd(10.0, 10.0, 0.0);
590        ms.calc_join(&mut vc, &v0, &v1, &v2, 10.0, 10.0);
591        // Bevel join should produce vertices
592        assert!(!vc.is_empty());
593    }
594
595    #[test]
596    fn test_miter_join() {
597        let ms = MathStroke::new(); // Default is miter join
598        let mut vc = Vec::new();
599        let v0 = vd(0.0, 0.0, 10.0);
600        let v1 = vd(10.0, 0.0, 10.0);
601        let v2 = vd(10.0, 10.0, 0.0);
602        ms.calc_join(&mut vc, &v0, &v1, &v2, 10.0, 10.0);
603        assert!(!vc.is_empty());
604    }
605
606    #[test]
607    fn test_round_join() {
608        let mut ms = MathStroke::new();
609        ms.set_line_join(LineJoin::Round);
610        let mut vc = Vec::new();
611        let v0 = vd(0.0, 0.0, 10.0);
612        let v1 = vd(10.0, 0.0, 10.0);
613        let v2 = vd(10.0, 10.0, 0.0);
614        ms.calc_join(&mut vc, &v0, &v1, &v2, 10.0, 10.0);
615        // Round join should produce arc vertices
616        assert!(vc.len() > 2);
617    }
618
619    #[test]
620    fn test_miter_limit_theta() {
621        let mut ms = MathStroke::new();
622        ms.set_miter_limit_theta(PI / 4.0); // 45 degrees
623        let expected = 1.0 / (PI / 8.0).sin();
624        assert!((ms.miter_limit() - expected).abs() < 1e-10);
625    }
626
627    #[test]
628    fn test_setters_getters() {
629        let mut ms = MathStroke::new();
630        ms.set_line_cap(LineCap::Round);
631        ms.set_line_join(LineJoin::MiterRevert);
632        ms.set_inner_join(InnerJoin::Jag);
633        ms.set_miter_limit(10.0);
634        ms.set_inner_miter_limit(2.0);
635        ms.set_approximation_scale(0.5);
636
637        assert_eq!(ms.line_cap(), LineCap::Round);
638        assert_eq!(ms.line_join(), LineJoin::MiterRevert);
639        assert_eq!(ms.inner_join(), InnerJoin::Jag);
640        assert!((ms.miter_limit() - 10.0).abs() < 1e-10);
641        assert!((ms.inner_miter_limit() - 2.0).abs() < 1e-10);
642        assert!((ms.approximation_scale() - 0.5).abs() < 1e-10);
643    }
644
645    #[test]
646    fn test_inner_join_bevel() {
647        let mut ms = MathStroke::new();
648        ms.set_inner_join(InnerJoin::Bevel);
649        let mut vc = Vec::new();
650        // Create a sharp inward corner
651        let v0 = vd(0.0, 0.0, 10.0);
652        let v1 = vd(10.0, 0.0, 10.0);
653        let v2 = vd(20.0, 0.0, 0.0);
654        ms.calc_join(&mut vc, &v0, &v1, &v2, 10.0, 10.0);
655        assert!(!vc.is_empty());
656    }
657
658    #[test]
659    fn test_collinear_segments() {
660        // Straight line continuation — should still produce output
661        let ms = MathStroke::new();
662        let mut vc = Vec::new();
663        let v0 = vd(0.0, 0.0, 10.0);
664        let v1 = vd(10.0, 0.0, 10.0);
665        let v2 = vd(20.0, 0.0, 0.0);
666        ms.calc_join(&mut vc, &v0, &v1, &v2, 10.0, 10.0);
667        assert!(!vc.is_empty());
668    }
669}