Skip to main content

bymsdfgen_core/raster/
scanline.rs

1//! Horizontal scanline through a shape. Port of `core/Scanline.{h,cpp}`.
2
3/// How the running intersection total is turned into a fill decision.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum FillRule {
6    NonZero,
7    /// "even-odd"
8    Odd,
9    Positive,
10    Negative,
11}
12
13impl FillRule {
14    #[inline]
15    pub fn interpret(self, intersections: i32) -> bool {
16        match self {
17            FillRule::NonZero => intersections != 0,
18            FillRule::Odd => intersections & 1 != 0,
19            FillRule::Positive => intersections > 0,
20            FillRule::Negative => intersections < 0,
21        }
22    }
23}
24
25#[derive(Debug, Clone, Copy)]
26pub struct Intersection {
27    pub x: f64,
28    /// After [`Scanline::set_intersections`], this holds the *accumulated* direction.
29    pub direction: i32,
30}
31
32/// A set of x-intersections at a fixed y, sorted with prefix-summed directions.
33#[derive(Debug, Clone, Default)]
34pub struct Scanline {
35    intersections: Vec<Intersection>,
36}
37
38impl Scanline {
39    pub fn new() -> Self {
40        Scanline::default()
41    }
42
43    /// Replace the intersection list and sort + accumulate directions.
44    pub fn set_intersections(&mut self, mut intersections: Vec<Intersection>) {
45        intersections.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap_or(std::cmp::Ordering::Equal));
46        let mut total = 0;
47        for i in &mut intersections {
48            total += i.direction;
49            i.direction = total;
50        }
51        self.intersections = intersections;
52    }
53
54    /// Index of the rightmost intersection with `x_i <= x`, or `None`.
55    #[inline]
56    fn move_to(&self, x: f64) -> Option<usize> {
57        if self.intersections.is_empty() {
58            return None;
59        }
60        let count = self.intersections.partition_point(|i| i.x <= x);
61        if count == 0 { None } else { Some(count - 1) }
62    }
63
64    pub fn count_intersections(&self, x: f64) -> i32 {
65        match self.move_to(x) {
66            Some(i) => i as i32 + 1,
67            None => 0,
68        }
69    }
70
71    pub fn sum_intersections(&self, x: f64) -> i32 {
72        match self.move_to(x) {
73            Some(i) => self.intersections[i].direction,
74            None => 0,
75        }
76    }
77
78    pub fn filled(&self, x: f64, fill_rule: FillRule) -> bool {
79        fill_rule.interpret(self.sum_intersections(x))
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn fill_inside_outside() {
89        let mut s = Scanline::new();
90        s.set_intersections(vec![
91            Intersection {
92                x: 1.0,
93                direction: 1,
94            },
95            Intersection {
96                x: 3.0,
97                direction: -1,
98            },
99        ]);
100        assert!(!s.filled(0.0, FillRule::NonZero));
101        assert!(s.filled(2.0, FillRule::NonZero));
102        assert!(!s.filled(4.0, FillRule::NonZero));
103    }
104}