sb-rust-library 0.1.7

Basic library providing common functionality I reuse in many of my projects
Documentation
use std::collections::HashSet;

use crate::math::Point;
use super::Color;
use super::Plot;

/// Circle shape used to plot data.
///
/// A single instance of `Circle` is reused to plot multiple data points.
///
/// Currently it only allows integer radii, meaning that it cant plot shapes of
/// even width. So you can plot a dot or a 3x3 "circle" or a 5x5 circle, but not
/// a 2x2 circle.
///
pub struct Circle {
  /// Color of plotted circle.
  pub color: Color,

  /// The radius (almost) of the drawn circle.
  ///
  /// The true radius is actually `radius - 0.5` (e.g. radius 1 = single pixel).
  /// TODO: Allow half-integer radii
  pub radius: i64, // TODO: Allow half-integer radii
  r2_check: i64,
}

/// Oriented point with fixed with and variable angle used to plot data.
///
/// This is suitable for plotting data points with direction such as vector
/// fields or velocity flows. For each data point, you have to also provide an
/// angle indicating the orientation.
pub struct Orientation {
  /// Color of plotted line segment.
  pub color: Color,

  /// The "radius" of the line segment. This is a placeholder hack for a more
  /// robust system that uses a proper definition of length.
  pub radius: i64,
}

impl Circle {

  /// Creates a new plottable circle with given color and radius.
  pub fn new(color: Color, radius: i64) -> Circle {
    Circle {
      color,
      radius,
      r2_check: ((radius as f32 - 0.5) * (radius as f32 - 0.5)) as i64
    }
  }

  /// Draws a circle with the given logical coordinates.
  pub fn draw(&self, plot: &mut Plot, pt: Point) {
    let c = plot.frame.pt_to_px(pt);
    for dx in 0..self.radius {
      for dy in 0..self.radius {
        if (dx * dx + dy * dy) <= self.r2_check {
          plot.put_pixel_safe(c.0 - dx, c.1 - dy, self.color);
          plot.put_pixel_safe(c.0 - dx, c.1 + dy, self.color);
          plot.put_pixel_safe(c.0 + dx, c.1 - dy, self.color);
          plot.put_pixel_safe(c.0 + dx, c.1 + dy, self.color);
        }
      }
    }
  }

  #[allow(dead_code)]
  fn draw_test_impl(&self, center_pixel: Point) -> HashSet<(i64, i64)> {
    let mut pixels = HashSet::new();
    let c = (center_pixel.x.round() as i64, center_pixel.y.round() as i64);
    for dx in 0..self.radius {
      for dy in 0..self.radius {
        if (dx * dx + dy * dy) <= self.r2_check {
          pixels.insert((c.0 + dx, c.1 + dy));
          pixels.insert((c.0 - dx, c.1 + dy));
          pixels.insert((c.0 + dx, c.1 - dy));
          pixels.insert((c.0 - dx, c.1 - dy));
        }
      }
    }
    pixels
  }
}

impl Orientation {

  /// Creates a new plottable oriented point with given color and radius.
  pub fn new(color: Color, radius: i64) -> Orientation {
    Orientation {
      color,
      radius,
    }
  }

  /// Draws an oriented point with the given logical coordinates and angle.
  pub fn draw(&self, plot: &mut Plot, pt: Point, angle: f64) {
    let c = plot.frame.pt_to_px(pt);
    let r = self.radius as f64;
    let a = angle;
    let x1 = (c.0 as f64) - (r + 0.5) * a.cos();
    let x2 = (c.0 as f64) + (r + 0.5) * a.cos();
    let y1 = (c.1 as f64) - (r + 0.5) * a.sin();
    let y2 = (c.1 as f64) + (r + 0.5) * a.sin();
    plot.draw_pixel_line((x1, y1), (x2, y2), self.color);
  }

}

#[test]
fn it_draws_a_point() {
  use std::collections::HashSet;

  let tests = vec![
    (Point::new(0.0, 0.0), 1, vec![(0, 0)]),
    (Point::new(0.4, 0.4), 1, vec![(0, 0)]),
    (Point::new(0.5, 0.1), 1, vec![(1, 0)]),
    (Point::new(-0.51, -0.4), 1, vec![(-1, 0)]),
  ];

  for (center, radius, expected) in tests.into_iter() {
    let c = Circle::new(super::RED, radius);
    let expected_set: HashSet<(i64, i64)> = expected.into_iter().collect();
    let actual_set: HashSet<(i64, i64)> = c.draw_test_impl(center).into_iter().collect();
    assert_eq!(actual_set, expected_set);
  }
}

#[test]
fn it_draws_a_3_circle_at_origin() {
  use std::collections::HashSet;

  let center = Point::new(0.0, 0.0);
  let radius = 2;
  let expected = vec![
    (-1,  1), (0,  1), (1,  1),
    (-1,  0), (0,  0), (1,  0),
    (-1, -1), (0, -1), (1, -1),
  ].into_iter().collect();

  let actual: HashSet<(i64, i64)> = Circle::new(super::RED, radius)
    .draw_test_impl(center)
    .into_iter().collect();
  assert_eq!(actual, expected);
}

#[test]
fn it_draws_a_3_circle_offset_from_origin() {
  use std::collections::HashSet;

  let center = Point::new(0.49, 0.4);
  let radius = 2;
  let expected = vec![
    (-1,  1), (0,  1), (1,  1),
    (-1,  0), (0,  0), (1,  0),
    (-1, -1), (0, -1), (1, -1),
  ].into_iter().collect();

  let actual: HashSet<(i64, i64)> = Circle::new(super::RED, radius)
    .draw_test_impl(center)
    .into_iter().collect();
  assert_eq!(actual, expected);

  let center = Point::new(0.5, 0.4);
  let radius = 2;
  let expected = vec![
    (0,  1), (1,  1), (2,  1),
    (0,  0), (1,  0), (2,  0),
    (0, -1), (1, -1), (2, -1),
  ].into_iter().collect();

  let actual: HashSet<(i64, i64)> = Circle::new(super::RED, radius)
    .draw_test_impl(center)
    .into_iter().collect();
  assert_eq!(actual, expected);
}

#[test]
fn it_draws_a_5_circle_offset_at_origin() {
  use std::collections::HashSet;

  let center = Point::new(0.0, 0.0);
  let radius = 3;
  let expected = vec![
              (-1,  2), (0,  2), (1,  2),
    (-2,  1), (-1,  1), (0,  1), (1,  1), (2, 1),
    (-2,  0), (-1,  0), (0,  0), (1,  0), (2, 0),
    (-2, -1), (-1, -1), (0, -1), (1, -1), (2,-1),
              (-1, -2), (0, -2), (1, -2)
  ].into_iter().collect();

  let actual: HashSet<(i64, i64)> = Circle::new(super::RED, radius)
    .draw_test_impl(center)
    .into_iter().collect();
  assert_eq!(actual, expected);
}