sb_rust_library/plotter/
shapes.rs

1use std::collections::HashSet;
2
3use crate::math::Point;
4use super::Color;
5use super::Plot;
6
7/// Circle shape used to plot data.
8///
9/// A single instance of `Circle` is reused to plot multiple data points.
10///
11/// Currently it only allows integer radii, meaning that it cant plot shapes of
12/// even width. So you can plot a dot or a 3x3 "circle" or a 5x5 circle, but not
13/// a 2x2 circle.
14///
15pub struct Circle {
16  /// Color of plotted circle.
17  pub color: Color,
18
19  /// The radius (almost) of the drawn circle.
20  ///
21  /// The true radius is actually `radius - 0.5` (e.g. radius 1 = single pixel).
22  /// TODO: Allow half-integer radii
23  pub radius: i64, // TODO: Allow half-integer radii
24  r2_check: i64,
25}
26
27/// Oriented point with fixed with and variable angle used to plot data.
28///
29/// This is suitable for plotting data points with direction such as vector
30/// fields or velocity flows. For each data point, you have to also provide an
31/// angle indicating the orientation.
32pub struct Orientation {
33  /// Color of plotted line segment.
34  pub color: Color,
35
36  /// The "radius" of the line segment. This is a placeholder hack for a more
37  /// robust system that uses a proper definition of length.
38  pub radius: i64,
39}
40
41impl Circle {
42
43  /// Creates a new plottable circle with given color and radius.
44  pub fn new(color: Color, radius: i64) -> Circle {
45    Circle {
46      color,
47      radius,
48      r2_check: ((radius as f32 - 0.5) * (radius as f32 - 0.5)) as i64
49    }
50  }
51
52  /// Draws a circle with the given logical coordinates.
53  pub fn draw(&self, plot: &mut Plot, pt: Point) {
54    let c = plot.frame.pt_to_px(pt);
55    for dx in 0..self.radius {
56      for dy in 0..self.radius {
57        if (dx * dx + dy * dy) <= self.r2_check {
58          plot.put_pixel_safe(c.0 - dx, c.1 - dy, self.color);
59          plot.put_pixel_safe(c.0 - dx, c.1 + dy, self.color);
60          plot.put_pixel_safe(c.0 + dx, c.1 - dy, self.color);
61          plot.put_pixel_safe(c.0 + dx, c.1 + dy, self.color);
62        }
63      }
64    }
65  }
66
67  #[allow(dead_code)]
68  fn draw_test_impl(&self, center_pixel: Point) -> HashSet<(i64, i64)> {
69    let mut pixels = HashSet::new();
70    let c = (center_pixel.x.round() as i64, center_pixel.y.round() as i64);
71    for dx in 0..self.radius {
72      for dy in 0..self.radius {
73        if (dx * dx + dy * dy) <= self.r2_check {
74          pixels.insert((c.0 + dx, c.1 + dy));
75          pixels.insert((c.0 - dx, c.1 + dy));
76          pixels.insert((c.0 + dx, c.1 - dy));
77          pixels.insert((c.0 - dx, c.1 - dy));
78        }
79      }
80    }
81    pixels
82  }
83}
84
85impl Orientation {
86
87  /// Creates a new plottable oriented point with given color and radius.
88  pub fn new(color: Color, radius: i64) -> Orientation {
89    Orientation {
90      color,
91      radius,
92    }
93  }
94
95  /// Draws an oriented point with the given logical coordinates and angle.
96  pub fn draw(&self, plot: &mut Plot, pt: Point, angle: f64) {
97    let c = plot.frame.pt_to_px(pt);
98    let r = self.radius as f64;
99    let a = angle;
100    let x1 = (c.0 as f64) - (r + 0.5) * a.cos();
101    let x2 = (c.0 as f64) + (r + 0.5) * a.cos();
102    let y1 = (c.1 as f64) - (r + 0.5) * a.sin();
103    let y2 = (c.1 as f64) + (r + 0.5) * a.sin();
104    plot.draw_pixel_line((x1, y1), (x2, y2), self.color);
105  }
106
107}
108
109#[test]
110fn it_draws_a_point() {
111  use std::collections::HashSet;
112
113  let tests = vec![
114    (Point::new(0.0, 0.0), 1, vec![(0, 0)]),
115    (Point::new(0.4, 0.4), 1, vec![(0, 0)]),
116    (Point::new(0.5, 0.1), 1, vec![(1, 0)]),
117    (Point::new(-0.51, -0.4), 1, vec![(-1, 0)]),
118  ];
119
120  for (center, radius, expected) in tests.into_iter() {
121    let c = Circle::new(super::RED, radius);
122    let expected_set: HashSet<(i64, i64)> = expected.into_iter().collect();
123    let actual_set: HashSet<(i64, i64)> = c.draw_test_impl(center).into_iter().collect();
124    assert_eq!(actual_set, expected_set);
125  }
126}
127
128#[test]
129fn it_draws_a_3_circle_at_origin() {
130  use std::collections::HashSet;
131
132  let center = Point::new(0.0, 0.0);
133  let radius = 2;
134  let expected = vec![
135    (-1,  1), (0,  1), (1,  1),
136    (-1,  0), (0,  0), (1,  0),
137    (-1, -1), (0, -1), (1, -1),
138  ].into_iter().collect();
139
140  let actual: HashSet<(i64, i64)> = Circle::new(super::RED, radius)
141    .draw_test_impl(center)
142    .into_iter().collect();
143  assert_eq!(actual, expected);
144}
145
146#[test]
147fn it_draws_a_3_circle_offset_from_origin() {
148  use std::collections::HashSet;
149
150  let center = Point::new(0.49, 0.4);
151  let radius = 2;
152  let expected = vec![
153    (-1,  1), (0,  1), (1,  1),
154    (-1,  0), (0,  0), (1,  0),
155    (-1, -1), (0, -1), (1, -1),
156  ].into_iter().collect();
157
158  let actual: HashSet<(i64, i64)> = Circle::new(super::RED, radius)
159    .draw_test_impl(center)
160    .into_iter().collect();
161  assert_eq!(actual, expected);
162
163  let center = Point::new(0.5, 0.4);
164  let radius = 2;
165  let expected = vec![
166    (0,  1), (1,  1), (2,  1),
167    (0,  0), (1,  0), (2,  0),
168    (0, -1), (1, -1), (2, -1),
169  ].into_iter().collect();
170
171  let actual: HashSet<(i64, i64)> = Circle::new(super::RED, radius)
172    .draw_test_impl(center)
173    .into_iter().collect();
174  assert_eq!(actual, expected);
175}
176
177#[test]
178fn it_draws_a_5_circle_offset_at_origin() {
179  use std::collections::HashSet;
180
181  let center = Point::new(0.0, 0.0);
182  let radius = 3;
183  let expected = vec![
184              (-1,  2), (0,  2), (1,  2),
185    (-2,  1), (-1,  1), (0,  1), (1,  1), (2, 1),
186    (-2,  0), (-1,  0), (0,  0), (1,  0), (2, 0),
187    (-2, -1), (-1, -1), (0, -1), (1, -1), (2,-1),
188              (-1, -2), (0, -2), (1, -2)
189  ].into_iter().collect();
190
191  let actual: HashSet<(i64, i64)> = Circle::new(super::RED, radius)
192    .draw_test_impl(center)
193    .into_iter().collect();
194  assert_eq!(actual, expected);
195}