Skip to main content

ravenrs_sheet/chart/
raster.rs

1//! A small self-written rasteriser: every primitive becomes one or more even-odd filled paths,
2//! scan-converted with 4 vertical sub-samples and exact horizontal coverage (anti-aliased).
3//! Text is not drawn (tests check geometry, the UI draws real glyphs).
4
5use crate::chart::{Prim, Rgba};
6
7/// Largest image we allocate (pixels).
8const MAX_PIXELS: u64 = 64 * 1024 * 1024;
9const SUB: usize = 4;
10
11type Path = Vec<Vec<[f32; 2]>>;
12
13struct Canvas {
14    w: usize,
15    h: usize,
16    buf: Vec<u8>,
17    acc: Vec<f32>,
18}
19
20impl Canvas {
21    fn blend(&mut self, x: usize, y: usize, c: Rgba, cov: f32) {
22        let a = (cov.clamp(0.0, 1.0) * c[3] as f32 / 255.0).clamp(0.0, 1.0);
23        if a <= 0.0 {
24            return;
25        }
26        let i = (y * self.w + x) * 4;
27        if let Some(px) = self.buf.get_mut(i..i + 4) {
28            let da = px[3] as f32 / 255.0;
29            let oa = a + da * (1.0 - a);
30            if oa <= 0.0 {
31                return;
32            }
33            for k in 0..3 {
34                let s = c[k] as f32;
35                let d = px[k] as f32;
36                px[k] = ((s * a + d * da * (1.0 - a)) / oa).round().clamp(0.0, 255.0) as u8;
37            }
38            px[3] = (oa * 255.0).round().clamp(0.0, 255.0) as u8;
39        }
40    }
41
42    /// Fills a multi-contour path with the even-odd rule.
43    fn fill(&mut self, path: &Path, color: Rgba) {
44        let mut edges: Vec<[f32; 4]> = Vec::new();
45        let (mut minx, mut miny, mut maxx, mut maxy) = (f32::INFINITY, f32::INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY);
46        for c in path {
47            if c.len() < 3 || !c.iter().all(|p| p[0].is_finite() && p[1].is_finite()) {
48                continue;
49            }
50            for (k, p) in c.iter().enumerate() {
51                let q = c.get((k + 1) % c.len()).copied().unwrap_or(*p);
52                minx = minx.min(p[0]);
53                maxx = maxx.max(p[0]);
54                miny = miny.min(p[1]);
55                maxy = maxy.max(p[1]);
56                if p[1] != q[1] {
57                    edges.push([p[0], p[1], q[0], q[1]]);
58                }
59            }
60        }
61        if edges.is_empty() {
62            return;
63        }
64        let x0 = minx.floor().max(0.0) as usize;
65        let x1 = (maxx.ceil().max(0.0) as usize).min(self.w);
66        let y0 = miny.floor().max(0.0) as usize;
67        let y1 = (maxy.ceil().max(0.0) as usize).min(self.h);
68        if x0 >= x1 || y0 >= y1 {
69            return;
70        }
71        let bw = x1 - x0;
72        if self.acc.len() < bw + 1 {
73            self.acc.resize(bw + 1, 0.0);
74        }
75        let mut xs: Vec<f32> = Vec::new();
76        for py in y0..y1 {
77            for v in self.acc.iter_mut().take(bw) {
78                *v = 0.0;
79            }
80            let mut any = false;
81            for s in 0..SUB {
82                let sy = py as f32 + (s as f32 + 0.5) / SUB as f32;
83                xs.clear();
84                for e in &edges {
85                    let (ya, yb) = if e[1] < e[3] { (e[1], e[3]) } else { (e[3], e[1]) };
86                    if sy >= ya && sy < yb {
87                        xs.push(e[0] + (sy - e[1]) * (e[2] - e[0]) / (e[3] - e[1]));
88                    }
89                }
90                xs.sort_by(f32::total_cmp);
91                for pair in xs.as_chunks::<2>().0 {
92                    let (a, b) = (pair[0].max(x0 as f32), pair[1].min(x1 as f32));
93                    if b <= a {
94                        continue;
95                    }
96                    any = true;
97                    let ia = a.floor() as usize;
98                    let ib = (b.ceil() as usize).min(x1);
99                    for px in ia..ib {
100                        let l = a.max(px as f32);
101                        let r = b.min(px as f32 + 1.0);
102                        if r > l
103                            && let Some(v) = self.acc.get_mut(px - x0)
104                        {
105                            *v += (r - l) / SUB as f32;
106                        }
107                    }
108                }
109            }
110            if !any {
111                continue;
112            }
113            for px in x0..x1 {
114                let cov = self.acc.get(px - x0).copied().unwrap_or(0.0);
115                if cov > 0.0 {
116                    self.blend(px, py, color, cov);
117                }
118            }
119        }
120    }
121}
122
123fn arc(cx: f32, cy: f32, r: f32, a0: f32, a1: f32) -> Vec<[f32; 2]> {
124    let sweep = a1 - a0;
125    let n = ((r.abs() * sweep.abs() / 2.0).ceil() as usize).clamp(4, 512);
126    (0..=n)
127        .map(|k| {
128            let a = a0 + sweep * k as f32 / n as f32;
129            [cx + r * a.sin(), cy - r * a.cos()]
130        })
131        .collect()
132}
133
134fn circle(cx: f32, cy: f32, r: f32) -> Vec<[f32; 2]> {
135    let mut c = arc(cx, cy, r, 0.0, std::f32::consts::TAU);
136    c.pop();
137    c
138}
139
140fn rounded_rect(x: f32, y: f32, w: f32, h: f32, r: f32) -> Vec<[f32; 2]> {
141    // A negative or NaN extent gives an upper bound of 0 (clamp panics if max < min or NaN).
142    let r = r.clamp(0.0, (w.min(h) / 2.0).max(0.0));
143    if r < 0.5 {
144        return vec![[x, y], [x + w, y], [x + w, y + h], [x, y + h]];
145    }
146    use std::f32::consts::{FRAC_PI_2, PI};
147    let mut v = Vec::new();
148    v.extend(arc(x + w - r, y + r, r, 0.0, FRAC_PI_2));
149    v.extend(arc(x + w - r, y + h - r, r, FRAC_PI_2, PI));
150    v.extend(arc(x + r, y + h - r, r, PI, PI + FRAC_PI_2));
151    v.extend(arc(x + r, y + r, r, PI + FRAC_PI_2, 2.0 * PI));
152    v
153}
154
155/// Thick polyline → one quad per segment (each a separate path so overlaps don't cancel).
156fn stroke_paths(pts: &[[f32; 2]], width: f32, dash: bool, closed: bool) -> Vec<Path> {
157    let width = width.clamp(1.0, 50.0);
158    let mut segs: Vec<([f32; 2], [f32; 2])> = pts.windows(2).map(|w| (w[0], w[1])).collect();
159    if closed && let (Some(a), Some(b)) = (pts.last(), pts.first()) {
160        segs.push((*a, *b));
161    }
162    if dash {
163        // The per-segment dash cap bounds each input line, but a caller-supplied vector of many
164        // long dashed lines multiplies: together they would eagerly allocate megabytes of
165        // geometry even when nothing of it reaches the canvas, so the total is capped too and
166        // the line is drawn solid once the budget is gone.
167        let (on, off) = (width * 4.0, width * 3.0);
168        let mut out = Vec::new();
169        let mut budget = 65_536usize;
170        for (a, b) in segs {
171            if budget == 0 {
172                return vec![vec![pts.to_vec()]];
173            }
174            let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
175            let len = (dx * dx + dy * dy).sqrt();
176            if !(len.is_finite() && len > 0.0) {
177                continue;
178            }
179            let mut t = 0.0;
180            while t < len && budget > 0 {
181                let e = (t + on).min(len);
182                out.push(([a[0] + dx * t / len, a[1] + dy * t / len], [a[0] + dx * e / len, a[1] + dy * e / len]));
183                t += on + off;
184                budget -= 1;
185            }
186        }
187        segs = out;
188    }
189    let hw = width / 2.0;
190    segs.into_iter()
191        .filter_map(|(a, b)| {
192            let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
193            let len = (dx * dx + dy * dy).sqrt();
194            if !(len.is_finite() && len > 1e-6) {
195                return None;
196            }
197            let (nx, ny) = (-dy / len * hw, dx / len * hw);
198            Some(vec![vec![[a[0] + nx, a[1] + ny], [b[0] + nx, b[1] + ny], [b[0] - nx, b[1] - ny], [a[0] - nx, a[1] - ny]]])
199        })
200        .collect()
201}
202
203/// Rasterises primitives to straight RGBA8 (`w*h*4` bytes) over `background`. Empty for a zero
204/// or oversized image. Text primitives are skipped.
205pub fn rasterize(prims: &[Prim], w: u32, h: u32, background: Rgba) -> Vec<u8> {
206    let px = (w as u64).saturating_mul(h as u64);
207    if w == 0 || h == 0 || px > MAX_PIXELS {
208        return vec![];
209    }
210    let (w, h) = (w as usize, h as usize);
211    let mut buf = Vec::with_capacity(w * h * 4);
212    for _ in 0..w * h {
213        buf.extend_from_slice(&background);
214    }
215    let mut cv = Canvas { w, h, buf, acc: Vec::new() };
216    for p in prims {
217        match p {
218            Prim::Rect { x, y, w, h, fill, stroke, radius } => {
219                if let Some(f) = fill {
220                    cv.fill(&vec![rounded_rect(*x, *y, *w, *h, *radius)], *f);
221                }
222                if let Some((c, sw)) = stroke {
223                    let sw = sw.clamp(0.5, 50.0);
224                    let outer = rounded_rect(x - sw / 2.0, y - sw / 2.0, w + sw, h + sw, radius + sw / 2.0);
225                    let inner = rounded_rect(x + sw / 2.0, y + sw / 2.0, (w - sw).max(0.0), (h - sw).max(0.0), (radius - sw / 2.0).max(0.0));
226                    cv.fill(&vec![outer, inner], *c);
227                }
228            }
229            Prim::Line { pts, color, width, dash } => {
230                for path in stroke_paths(pts, *width, *dash, false) {
231                    cv.fill(&path, *color);
232                }
233            }
234            Prim::Polygon { pts, fill, stroke } => {
235                cv.fill(&vec![pts.clone()], *fill);
236                if let Some((c, sw)) = stroke {
237                    for path in stroke_paths(pts, *sw, false, true) {
238                        cv.fill(&path, *c);
239                    }
240                }
241            }
242            Prim::Wedge { cx, cy, r_outer, r_inner, a0, a1, fill, stroke } => {
243                let mut c = arc(*cx, *cy, *r_outer, *a0, *a1);
244                if *r_inner > 0.0 {
245                    let mut inner = arc(*cx, *cy, *r_inner, *a0, *a1);
246                    inner.reverse();
247                    c.extend(inner);
248                } else {
249                    c.push([*cx, *cy]);
250                }
251                cv.fill(&vec![c.clone()], *fill);
252                if let Some((sc, sw)) = stroke {
253                    for path in stroke_paths(&c, *sw, false, true) {
254                        cv.fill(&path, *sc);
255                    }
256                }
257            }
258            Prim::Circle { cx, cy, r, fill, stroke } => {
259                if let Some(f) = fill {
260                    cv.fill(&vec![circle(*cx, *cy, *r)], *f);
261                }
262                if let Some((c, sw)) = stroke {
263                    let sw = sw.clamp(0.5, 50.0);
264                    cv.fill(&vec![circle(*cx, *cy, r + sw / 2.0), circle(*cx, *cy, (r - sw / 2.0).max(0.0))], *c);
265                }
266            }
267            Prim::Text { .. } => {}
268        }
269    }
270    cv.buf
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276
277    #[test]
278    fn dashed_geometry_stays_within_the_dash_budget() {
279        // Forty very long dashed lines, each allowed 10_000 dashes per segment: before the
280        // shared cap they eagerly allocated ~1.6 MB of segment pairs × points and a like
281        // number of quads before any of it reached a 4-pixel canvas (finding: dash expansion).
282        let prims: Vec<Prim> =
283            (0..40).map(|i| Prim::Line { pts: vec![[0.0, i as f32], [900.0, i as f32]], color: [0, 0, 0, 255], width: 1.0, dash: true }).collect();
284        let img = rasterize(&prims, 2, 2, [255, 255, 255, 255]);
285        assert_eq!(img.len(), 2 * 2 * 4);
286        // And the geometry itself is bounded even for one huge dashed polyline.
287        assert!(stroke_paths(&[[0.0, 0.0], [1e6, 0.0]], 1.0, true, false).len() <= 65_536);
288        let prim = [Prim::Line { pts: vec![[0.0, 0.0], [10.0, 0.0]], color: [0, 0, 0, 255], width: 1.0, dash: true }];
289        assert_eq!(rasterize(&prim, 1, 1, [0, 0, 0, 255]).len(), 4);
290    }
291}