Skip to main content

shuyi_ui/frame/
shape.rs

1//! Shapes in whole console pixels, by one integer rule every renderer
2//! follows (ADR-0085 §8): each gives its pixels as runs, and a renderer
3//! fills each run as a rectangle one console pixel high. So `shuyi-app`,
4//! the raster and the one-bit renderers draw the same pixels by
5//! construction.
6//!
7//! The rules, in console pixels, integers only. A point names a pixel:
8//! `(x, y)` is that pixel's centre.
9//!
10//! - **A line one pixel wide** ([`line_runs`]) is Bresenham's, from
11//!   `from` to `to`, both ends drawn: the all-octant form whose error term
12//!   starts at `dx + dy` (`dx = |x1 − x0|`, `dy = −|y1 − y0|`). Along its
13//!   major axis (`x` when `|dx| ≥ |dy|`) it takes one pixel a step; pixel
14//!   `k` from `from` is `round(k · minor / major)` along the other, a half
15//!   rounded towards `to`. That closed form gives the pixels of the walk,
16//!   so a line far longer than the clip costs no more than the clip: only
17//!   the steps inside it are taken.
18//! - **A circle's outline of radius `r`** ([`circle_outline_runs`]) is
19//!   `2r + 1` pixels across: the midpoint circle whose decision value
20//!   starts at `1 − r`, one octant walked (at most `r` steps), each step
21//!   giving its eight mirrored pixels.
22//!
23//! A run is `(y, x0, x1)`: row `y`, pixels `x0..x1`, half-open, always
24//! inside the clip. Runs may overlap (a circle's mirrored pixels where the
25//! octants meet); every renderer paints them opaque, so that doesn't
26//! matter.
27
28use crate::frame::glyph_buffer::PxRect;
29
30/// `clip`'s rows as `y0..y1` and columns as `x0..x1`, in `i64`.
31fn spans(clip: PxRect) -> ((i64, i64), (i64, i64)) {
32    let (x, y) = (i64::from(clip.x), i64::from(clip.y));
33    (
34        (x, x + i64::from(clip.w.max(0))),
35        (y, y + i64::from(clip.h.max(0))),
36    )
37}
38
39/// Calls `f(y, x0, x1)` with the part of run `x0..x1` on row `y` inside
40/// `clip`, if any. Everything inside `clip` fits in `i32`.
41pub(crate) fn clip_run(clip: PxRect, y: i64, x0: i64, x1: i64, f: &mut impl FnMut(i32, i32, i32)) {
42    let ((left, right), (top, bottom)) = spans(clip);
43    let (x0, x1) = (x0.max(left), x1.min(right));
44    if x0 >= x1 || y < top || y >= bottom {
45        return;
46    }
47    if let (Ok(y), Ok(x0), Ok(x1)) = (i32::try_from(y), i32::try_from(x0), i32::try_from(x1)) {
48        f(y, x0, x1);
49    }
50}
51
52/// Calls `f(y, x0, x1)` for the runs of the line one pixel wide from
53/// `from` to `to`, both ends drawn, inside `clip` (the module's rule).
54/// Each row of the line is one run.
55pub fn line_runs(from: (i32, i32), to: (i32, i32), clip: PxRect, mut f: impl FnMut(i32, i32, i32)) {
56    let (x0, y0) = (i64::from(from.0), i64::from(from.1));
57    let (x1, y1) = (i64::from(to.0), i64::from(to.1));
58    let (dx, dy) = ((x1 - x0).abs(), (y1 - y0).abs());
59    let (sx, sy) = ((x1 - x0).signum(), (y1 - y0).signum());
60    let x_major = dx >= dy;
61    let ((major0, major_step, major_len), (minor0, minor_step, minor_len)) = if x_major {
62        ((x0, sx, dx), (y0, sy, dy))
63    } else {
64        ((y0, sy, dy), (x0, sx, dx))
65    };
66    let (columns, rows) = spans(clip);
67    let (major_span, minor_span) = if x_major {
68        (columns, rows)
69    } else {
70        (rows, columns)
71    };
72    // Nothing is inside an empty clip; and a line of one pixel (a step of
73    // 0) has the one step 0, inside the clip or not.
74    if clip.is_empty() || major_len == 0 && !(major_span.0..major_span.1).contains(&major0) {
75        return;
76    }
77    // The steps whose major coordinate is inside the clip, the step's sign
78    // either way: from `near` to `far`, both in.
79    let (near, far) = (
80        major_step * (major_span.0 - major0),
81        major_step * (major_span.1 - 1 - major0),
82    );
83    let (first, end) = (near.min(far).max(0), (near.max(far) + 1).min(major_len + 1));
84    // The run being gathered on one row (x-major lines only): its row and
85    // its first and last pixel.
86    let mut run: Option<(i64, i64, i64)> = None;
87    for k in first..end {
88        let major = major0 + major_step * k;
89        let minor = minor0 + minor_step * minor_offset(k, major_len, minor_len);
90        if !(minor_span.0..minor_span.1).contains(&minor) {
91            continue;
92        }
93        if !x_major {
94            emit(major, minor, minor + 1, &mut f);
95            continue;
96        }
97        let (column, line) = (major, minor);
98        run = match run {
99            Some((row, first, last))
100                if row == line && (column == first - 1 || column == last + 1) =>
101            {
102                Some((row, first.min(column), last.max(column)))
103            }
104            Some((row, first, last)) => {
105                emit(row, first, last + 1, &mut f);
106                Some((line, column, column))
107            }
108            None => Some((line, column, column)),
109        };
110    }
111    if let Some((row, first, last)) = run {
112        emit(row, first, last + 1, &mut f);
113    }
114}
115
116/// Calls `f(y, x0, x1)` with a run already inside the clip (so in `i32`).
117fn emit(y: i64, x0: i64, x1: i64, f: &mut impl FnMut(i32, i32, i32)) {
118    if let (Ok(y), Ok(x0), Ok(x1)) = (i32::try_from(y), i32::try_from(x0), i32::try_from(x1)) {
119        f(y, x0, x1);
120    }
121}
122
123/// How far pixel `k` of a line is along its minor axis: `k · minor /
124/// major` rounded, a half up (`⌊(2k · minor + major) / (2 · major)⌋`),
125/// which is where Bresenham's walk puts it. `major` is at least `minor`.
126fn minor_offset(k: i64, major: i64, minor: i64) -> i64 {
127    if major == 0 {
128        return 0;
129    }
130    let (k, major, minor) = (i128::from(k), i128::from(major), i128::from(minor));
131    let offset = (2 * k * minor + major).div_euclid(2 * major);
132    // At most `minor`, which came from an `i64`.
133    i64::try_from(offset).unwrap_or(0)
134}
135
136/// Calls `f(y, x0, x1)` for the runs of the outline of the circle of
137/// `radius` round `center` inside `clip` (the module's rule): one pixel a
138/// run.
139pub fn circle_outline_runs(
140    center: (i32, i32),
141    radius: u16,
142    clip: PxRect,
143    mut f: impl FnMut(i32, i32, i32),
144) {
145    let (cx, cy) = (i64::from(center.0), i64::from(center.1));
146    let (mut x, mut y) = (i64::from(radius), 0_i64);
147    let mut d = 1 - x;
148    let mut dot = |px: i64, py: i64| clip_run(clip, py, px, px + 1, &mut f);
149    // The walk ends once `x < y`, within `radius / √2 + 1` steps: the bound
150    // only keeps a broken walk from running for ever.
151    for _ in 0..=radius {
152        if x < y {
153            break;
154        }
155        for (px, py) in [
156            (x, y),
157            (y, x),
158            (-y, x),
159            (-x, y),
160            (-x, -y),
161            (-y, -x),
162            (y, -x),
163            (x, -y),
164        ] {
165            dot(cx + px, cy + py);
166        }
167        y += 1;
168        if d < 0 {
169            d += 2 * y + 1;
170        } else {
171            x -= 1;
172            d += 2 * (y - x) + 1;
173        }
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use alloc::collections::BTreeSet;
180
181    use proptest::prelude::*;
182
183    use super::*;
184    use crate::frame::glyph_buffer::Rect;
185    use crate::prelude::*;
186
187    /// Every pixel the runs cover.
188    fn pixels(runs: impl FnOnce(&mut dyn FnMut(i32, i32, i32))) -> BTreeSet<(i32, i32)> {
189        let mut out = BTreeSet::new();
190        runs(&mut |y, x0, x1| {
191            assert!(x0 < x1, "empty run {y} {x0}..{x1}");
192            out.extend((x0..x1).map(|x| (x, y)));
193        });
194        out
195    }
196
197    fn line(from: (i32, i32), to: (i32, i32), clip: PxRect) -> BTreeSet<(i32, i32)> {
198        pixels(|f| line_runs(from, to, clip, f))
199    }
200
201    /// Bresenham's walk itself, the error term from `dx + dy`.
202    fn walked(from: (i32, i32), to: (i32, i32)) -> BTreeSet<(i32, i32)> {
203        let (mut x, mut y) = (i64::from(from.0), i64::from(from.1));
204        let (x1, y1) = (i64::from(to.0), i64::from(to.1));
205        let (dx, dy) = ((x1 - x).abs(), -(y1 - y).abs());
206        let (sx, sy) = (if x < x1 { 1 } else { -1 }, if y < y1 { 1 } else { -1 });
207        let mut err = dx + dy;
208        let mut out = BTreeSet::new();
209        loop {
210            out.insert((i32::try_from(x).unwrap(), i32::try_from(y).unwrap()));
211            if x == x1 && y == y1 {
212                return out;
213            }
214            let e2 = 2 * err;
215            if e2 >= dy {
216                err += dy;
217                x += sx;
218            }
219            if e2 <= dx {
220                err += dx;
221                y += sy;
222            }
223        }
224    }
225
226    const WIDE: PxRect = Rect::new(-100, -100, 200, 200);
227
228    #[test]
229    fn a_line_in_each_octant_is_bresenhams() {
230        // From the centre to a point in each of the eight octants, and
231        // along the axes and diagonals between them.
232        let ends = [
233            (7, 2),
234            (2, 7),
235            (-2, 7),
236            (-7, 2),
237            (-7, -2),
238            (-2, -7),
239            (2, -7),
240            (7, -2),
241            (5, 0),
242            (0, 5),
243            (-5, 0),
244            (0, -5),
245            (4, 4),
246            (-4, 4),
247            (-4, -4),
248            (4, -4),
249            (0, 0),
250        ];
251        for to in ends {
252            assert_eq!(line((0, 0), to, WIDE), walked((0, 0), to), "to {to:?}");
253            assert_eq!(line(to, (0, 0), WIDE), walked(to, (0, 0)), "from {to:?}");
254        }
255        // The shallow line rises where a half rounds up.
256        let shallow: Vec<_> = line((0, 0), (4, 1), WIDE).into_iter().collect();
257        assert_eq!(shallow, [(0, 0), (1, 0), (2, 1), (3, 1), (4, 1)]);
258    }
259
260    #[test]
261    fn a_line_is_one_run_a_row_inside_the_clip() {
262        let mut runs = Vec::new();
263        line_runs((0, 0), (9, 2), WIDE, |y, x0, x1| runs.push((y, x0, x1)));
264        assert_eq!(runs, [(0, 0, 3), (1, 3, 7), (2, 7, 10)]);
265        // Backwards, the same rows (each one run).
266        let mut back = Vec::new();
267        line_runs((9, 2), (0, 0), WIDE, |y, x0, x1| back.push((y, x0, x1)));
268        assert_eq!(back.len(), 3);
269        // A clip cuts the runs.
270        let mut cut = Vec::new();
271        line_runs((0, 0), (9, 2), Rect::new(2, 1, 3, 5), |y, x0, x1| {
272            cut.push((y, x0, x1));
273        });
274        assert_eq!(cut, [(1, 3, 5)]);
275        // An empty clip draws nothing.
276        assert!(line((0, 0), (9, 2), Rect::new(0, 0, 0, 5)).is_empty());
277    }
278
279    #[test]
280    fn a_line_far_longer_than_its_clip_takes_only_the_clips_steps() {
281        let clip = Rect::new(0, 0, 16, 16);
282        let far = line((i32::MIN, i32::MIN), (i32::MAX, i32::MAX), clip);
283        let near: BTreeSet<_> = (0..16).map(|i| (i, i)).collect();
284        assert_eq!(far, near);
285        let flat = line((i32::MIN, 3), (i32::MAX, 3), clip);
286        assert_eq!(flat, (0..16).map(|x| (x, 3)).collect());
287    }
288
289    #[test]
290    fn a_line_of_one_pixel_is_drawn_only_inside_its_clip() {
291        let clip = Rect::new(0, 0, 4, 4);
292        assert_eq!(line((2, 3), (2, 3), clip), BTreeSet::from([(2, 3)]));
293        for out in [(4, 3), (-1, 3), (2, 4), (2, -1)] {
294            assert!(line(out, out, clip).is_empty(), "{out:?}");
295        }
296        // A line from outside the clip across it starts at its edge.
297        assert_eq!(
298            line((-5, 1), (9, 1), clip),
299            (0..4).map(|x| (x, 1)).collect()
300        );
301        assert_eq!(
302            line((1, 9), (1, -5), clip),
303            (0..4).map(|y| (1, y)).collect()
304        );
305    }
306
307    /// Each step of the octant the midpoint walk takes for `radius`.
308    fn octant(radius: u16) -> Vec<(i32, i32)> {
309        let mut steps = Vec::new();
310        circle_outline_runs((0, 0), radius, WIDE, |y, x, _| {
311            if x >= y && y >= 0 {
312                steps.push((x, y));
313            }
314        });
315        steps.dedup();
316        steps
317    }
318
319    #[test]
320    fn the_midpoint_walk_takes_the_textbooks_steps_in_its_order() {
321        // Each step's eight pixels in turn, one a run.
322        let mut runs = Vec::new();
323        circle_outline_runs((10, 20), 3, WIDE, |y, x0, x1| runs.push((x0, y, x1 - x0)));
324        let points: Vec<(i32, i32)> = runs.iter().map(|&(x, y, _)| (x, y)).collect();
325        assert!(runs.iter().all(|r| r.2 == 1));
326        assert_eq!(
327            points,
328            [
329                (13, 20),
330                (10, 23),
331                (10, 23),
332                (7, 20),
333                (7, 20),
334                (10, 17),
335                (10, 17),
336                (13, 20),
337                (13, 21),
338                (11, 23),
339                (9, 23),
340                (7, 21),
341                (7, 19),
342                (9, 17),
343                (11, 17),
344                (13, 19),
345                (12, 22),
346                (12, 22),
347                (8, 22),
348                (8, 22),
349                (8, 18),
350                (8, 18),
351                (12, 18),
352                (12, 18),
353            ]
354        );
355        assert_eq!(octant(5), [(5, 0), (5, 1), (5, 2), (4, 3)]);
356        assert_eq!(
357            octant(10),
358            [
359                (10, 0),
360                (10, 1),
361                (10, 2),
362                (10, 3),
363                (9, 4),
364                (9, 5),
365                (8, 6),
366                (7, 7)
367            ]
368        );
369        let seventeen = [
370            (17, 0),
371            (17, 1),
372            (17, 2),
373            (17, 3),
374            (17, 4),
375            (16, 5),
376            (16, 6),
377            (15, 7),
378            (15, 8),
379            (14, 9),
380            (14, 10),
381            (13, 11),
382            (12, 12),
383        ];
384        assert_eq!(octant(17), seventeen);
385    }
386
387    #[test]
388    fn the_midpoint_circle_starts_from_one_less_the_radius() {
389        let circle = |r| pixels(|f| circle_outline_runs((0, 0), r, WIDE, f));
390        assert_eq!(circle(0), BTreeSet::from([(0, 0)]));
391        // Radius 1: the four neighbours and no corners (d = 0 steps in).
392        assert_eq!(
393            circle(1),
394            BTreeSet::from([(1, 0), (0, 1), (-1, 0), (0, -1)])
395        );
396        // Radius 3, the textbook octant: (3,0) (3,1) (2,2).
397        let three = circle(3);
398        for p in [(3, 0), (3, 1), (2, 2), (1, 3), (0, 3), (-3, -1), (-2, -2)] {
399            assert!(three.contains(&p), "{p:?}");
400        }
401        assert!(!three.contains(&(3, 2)));
402        assert_eq!(three.len(), 16);
403        // It is 2r + 1 pixels across, and symmetric.
404        let five = circle(5);
405        let xs: BTreeSet<_> = five.iter().map(|p| p.0).collect();
406        assert_eq!(xs.len(), 11);
407        assert!(five.iter().all(|&(x, y)| five.contains(&(y, x))));
408        // Clipped to a quarter.
409        let quarter = pixels(|f| circle_outline_runs((0, 0), 5, Rect::new(0, 0, 10, 10), f));
410        assert!(quarter.iter().all(|&(x, y)| x >= 0 && y >= 0));
411        assert!(quarter.is_subset(&five));
412    }
413
414    proptest! {
415        #[test]
416        fn the_closed_form_is_the_walk(
417            x0 in -40..40, y0 in -40..40, x1 in -40..40, y1 in -40..40,
418            cx in -50..50, cy in -50..50, cw in 0..100, ch in 0..100,
419        ) {
420            let clip = Rect::new(cx, cy, cw, ch);
421            let want: BTreeSet<_> = walked((x0, y0), (x1, y1))
422                .into_iter()
423                .filter(|&(x, y)| clip.contains(x, y))
424                .collect();
425            prop_assert_eq!(line((x0, y0), (x1, y1), clip), want);
426        }
427
428        #[test]
429        fn a_circle_never_leaves_its_clip(
430            r in 0_u16..200, cx in -300..300, cy in -300..300,
431            x in -50..50, y in -50..50, w in 0..100, h in 0..100,
432        ) {
433            let clip = Rect::new(x, y, w, h);
434            let got = pixels(|f| circle_outline_runs((cx, cy), r, clip, f));
435            prop_assert!(got.iter().all(|&(px, py)| clip.contains(px, py)));
436        }
437    }
438}