use crate::core::{Affine2D, Bbox, Path, color::Rgba};
use crate::render::{CapStyle, GraphicsContext, JoinStyle, Renderer};
use crate::artist::Artist;
#[derive(Debug, Clone, PartialEq)]
pub struct Line2D {
xdata: Vec<f64>,
ydata: Vec<f64>,
color: Rgba,
linewidth: f64,
dashes: Option<(f64, Vec<f64>)>,
cap: CapStyle,
join: JoinStyle,
visible: bool,
zorder: f64,
}
impl Line2D {
#[must_use]
pub fn new(xdata: Vec<f64>, ydata: Vec<f64>) -> Self {
Self {
xdata,
ydata,
color: Rgba::BLACK,
linewidth: 1.5,
dashes: None,
cap: CapStyle::Butt,
join: JoinStyle::Miter,
visible: true,
zorder: 2.0,
}
}
#[must_use]
pub fn with_color(mut self, color: Rgba) -> Self {
self.color = color;
self
}
#[must_use]
pub fn with_linewidth(mut self, linewidth: f64) -> Self {
self.linewidth = linewidth;
self
}
#[must_use]
pub fn with_dashes(mut self, dashes: Option<(f64, Vec<f64>)>) -> Self {
self.dashes = dashes;
self
}
#[must_use]
pub fn with_cap(mut self, cap: CapStyle) -> Self {
self.cap = cap;
self
}
#[must_use]
pub fn with_join(mut self, join: JoinStyle) -> Self {
self.join = join;
self
}
#[must_use]
pub fn with_visible(mut self, visible: bool) -> Self {
self.visible = visible;
self
}
#[must_use]
pub fn with_zorder(mut self, zorder: f64) -> Self {
self.zorder = zorder;
self
}
#[must_use]
pub fn points(&self) -> Vec<[f64; 2]> {
self.xdata
.iter()
.zip(self.ydata.iter())
.map(|(&x, &y)| [x, y])
.collect()
}
pub fn draw_path(&self, renderer: &mut dyn Renderer, path: &Path, transform: &Affine2D) {
if !self.visible || path.vertices().len() < 2 {
return;
}
let gc = GraphicsContext {
line_width: self.linewidth,
dashes: self.dashes.clone(),
cap: self.cap,
join: self.join,
stroke: Some(self.color),
..GraphicsContext::new()
};
renderer.draw_path(&gc, path, transform, None);
}
}
impl Artist for Line2D {
fn draw(&self, renderer: &mut dyn Renderer, transform: &Affine2D) {
if !self.visible {
return;
}
let points = self.points();
if points.len() < 2 {
return;
}
let path = Path::from_polyline(&points);
self.draw_path(renderer, &path, transform);
}
fn zorder(&self) -> f64 {
self.zorder
}
fn visible(&self) -> bool {
self.visible
}
fn data_extents(&self) -> Option<Bbox> {
let mut xmin = f64::INFINITY;
let mut ymin = f64::INFINITY;
let mut xmax = f64::NEG_INFINITY;
let mut ymax = f64::NEG_INFINITY;
let mut any = false;
for (&x, &y) in self.xdata.iter().zip(self.ydata.iter()) {
if !x.is_finite() || !y.is_finite() {
continue;
}
xmin = xmin.min(x);
ymin = ymin.min(y);
xmax = xmax.max(x);
ymax = ymax.max(y);
any = true;
}
if any {
Some(Bbox::from_extents(xmin, ymin, xmax, ymax))
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Default)]
struct MockRenderer {
calls: Vec<(usize, Option<Rgba>)>,
}
impl Renderer for MockRenderer {
fn draw_path(
&mut self,
gc: &GraphicsContext,
path: &Path,
_transform: &Affine2D,
_fill: Option<Rgba>,
) {
self.calls.push((path.vertices().len(), gc.stroke));
}
fn canvas_size(&self) -> (f64, f64) {
(100.0, 100.0)
}
}
#[test]
fn three_point_line_draws_once_with_color() {
let line = Line2D::new(vec![0.0, 1.0, 2.0], vec![0.0, 1.0, 0.0]).with_color(Rgba::RED);
let mut r = MockRenderer::default();
line.draw(&mut r, &Affine2D::identity());
assert_eq!(r.calls, vec![(3, Some(Rgba::RED))]);
}
#[test]
fn invisible_line_draws_nothing() {
let line = Line2D::new(vec![0.0, 1.0], vec![0.0, 1.0]).with_visible(false);
let mut r = MockRenderer::default();
line.draw(&mut r, &Affine2D::identity());
assert!(r.calls.is_empty());
}
#[test]
fn single_point_line_draws_nothing() {
let line = Line2D::new(vec![0.0], vec![0.0]);
let mut r = MockRenderer::default();
line.draw(&mut r, &Affine2D::identity());
assert!(r.calls.is_empty());
}
#[test]
fn data_extents_bounds_finite_points() {
let line = Line2D::new(vec![1.0, 3.0, 2.0], vec![-1.0, 4.0, 0.0]);
let e = line.data_extents().expect("non-empty");
assert_eq!(e.xmin(), 1.0);
assert_eq!(e.xmax(), 3.0);
assert_eq!(e.ymin(), -1.0);
assert_eq!(e.ymax(), 4.0);
}
#[test]
fn data_extents_ignores_nan_point() {
let line = Line2D::new(vec![1.0, f64::NAN, 2.0], vec![1.0, 5.0, 2.0]);
let e = line.data_extents().expect("non-empty");
assert_eq!(e.xmin(), 1.0);
assert_eq!(e.xmax(), 2.0);
assert_eq!(e.ymin(), 1.0);
assert_eq!(e.ymax(), 2.0);
}
#[test]
fn data_extents_empty_is_none() {
let line = Line2D::new(vec![], vec![]);
assert!(line.data_extents().is_none());
}
#[test]
fn default_zorder_is_two() {
let line = Line2D::new(vec![0.0, 1.0], vec![0.0, 1.0]);
assert_eq!(line.zorder(), 2.0);
}
}