use std::ops::Range;
use densa_core::{Color, Rect};
use densa_draw::Canvas;
use crate::series::{points_within, with_neighbours};
use crate::{Plot, Points};
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct LineStyle {
pub color: Color,
pub width: f32,
}
impl LineStyle {
pub const fn new(color: Color) -> Self {
Self { color, width: 1.0 }
}
#[must_use]
pub fn with_width(mut self, width: f32) -> Self {
self.width = usable_width(width);
self
}
}
fn usable_width(width: f32) -> f32 {
if width.is_finite() {
width.max(0.1)
} else {
1.0
}
}
pub fn paint_line<P: Points + ?Sized>(
canvas: &mut Canvas<'_>,
plot: &Plot,
points: &P,
style: LineStyle,
) {
let area = plot.area();
if area.is_empty() {
return;
}
let state = canvas.state();
canvas.clip(area);
let range = with_neighbours(points_within(points, &plot.x()), points.len());
let at = range.map(|index| (plot.x_at(points.x(index)), plot.y_at(points.y(index))));
let shown = canvas.clip_rect();
let columns = shown.x0.floor() as i64..shown.x1.ceil() as i64;
let half = f64::from(usable_width(style.width)) / 2.0;
for_each_span(at, columns, |column, low, high| {
let top = plot.clamp_y(low - half);
let bottom = plot.clamp_y(high + half);
fill_span(canvas, column as f32, top, bottom, style.color);
});
canvas.restore(state);
}
fn fill_span(canvas: &mut Canvas<'_>, x: f32, top: f64, bottom: f64, color: Color) {
let pixel = |canvas: &mut Canvas<'_>, y: f64, coverage: f64| {
let alpha = color.a * coverage as f32;
canvas.fill_rect(Rect::new(x, y as f32, 1.0, 1.0), color.with_alpha(alpha));
};
let (inner_top, inner_bottom) = (top.ceil(), bottom.floor());
if inner_top > inner_bottom {
pixel(canvas, top.floor(), bottom - top);
return;
}
if inner_top > top {
pixel(canvas, top.floor(), inner_top - top);
}
if inner_bottom > inner_top {
let rect = Rect::from_edges(x, inner_top as f32, x + 1.0, inner_bottom as f32);
canvas.fill_rect(rect, color);
}
if bottom > inner_bottom {
pixel(canvas, inner_bottom, bottom - inner_bottom);
}
}
#[derive(Debug, Clone, Copy)]
struct Span {
column: i64,
low: f64,
high: f64,
}
impl Span {
fn new(column: i64, a: f64, b: f64) -> Self {
Self {
column,
low: a.min(b),
high: a.max(b),
}
}
fn reach(&mut self, y: f64) {
self.low = self.low.min(y);
self.high = self.high.max(y);
}
}
pub(crate) fn for_each_span(
points: impl Iterator<Item = (f64, f64)>,
columns: Range<i64>,
mut emit: impl FnMut(i64, f64, f64),
) {
let mut flush = |span: Option<Span>| {
if let Some(span) = span.filter(|span| columns.contains(&span.column)) {
emit(span.column, span.low, span.high);
}
};
let mut open: Option<Span> = None;
let mut last: Option<(f64, f64)> = None;
for (x, y) in points {
if !(x.is_finite() && y.is_finite()) {
last = None;
continue;
}
let column = x.floor() as i64;
match (last, open.as_mut()) {
(_, Some(span)) if column == span.column => span.reach(y),
(Some((x0, y0)), Some(span)) if column > span.column => {
let slope = (y - y0) / (x - x0);
let y_at = |at: f64| y0 + (at - x0) * slope;
let edge = |column: i64| y_at(column as f64);
span.reach(edge(span.column + 1));
let first = span.column + 1;
flush(open.take());
for between in first.max(columns.start)..column.min(columns.end) {
flush(Some(Span::new(between, edge(between), edge(between + 1))));
}
open = Some(Span::new(column, edge(column), y));
}
_ => {
flush(open.take());
open = Some(Span::new(column, y, y));
}
}
last = Some((x, y));
}
flush(open);
}
#[cfg(test)]
mod tests {
use super::for_each_span;
fn spans(points: &[(f64, f64)], columns: std::ops::Range<i64>) -> Vec<(i64, f64, f64)> {
let mut out = Vec::new();
for_each_span(points.iter().copied(), columns, |column, low, high| {
out.push((column, low, high));
});
out
}
#[test]
fn a_flat_line_is_one_span_per_column_it_crosses() {
let got = spans(&[(0.5, 5.0), (9.5, 5.0)], 0..100);
let want: Vec<_> = (0..10).map(|column| (column, 5.0, 5.0)).collect();
assert_eq!(got, want);
}
#[test]
fn a_slope_covers_its_rise_in_each_column_and_neighbours_meet() {
let got = spans(&[(0.0, 0.0), (4.0, 8.0)], 0..100);
assert_eq!(
got,
[
(0, 0.0, 2.0),
(1, 2.0, 4.0),
(2, 4.0, 6.0),
(3, 6.0, 8.0),
(4, 8.0, 8.0)
]
);
}
#[test]
fn points_within_one_column_become_its_lowest_and_highest() {
let got = spans(&[(3.1, 4.0), (3.4, 1.0), (3.6, 7.0), (3.9, 2.0)], 0..100);
assert_eq!(got, [(3, 1.0, 7.0)]);
}
#[test]
fn a_gap_breaks_the_line() {
let got = spans(
&[(0.0, 0.0), (1.5, f64::NAN), (3.0, 6.0), (4.0, 6.0)],
0..100,
);
assert_eq!(got, [(0, 0.0, 0.0), (3, 6.0, 6.0), (4, 6.0, 6.0)]);
let infinite = spans(&[(0.0, 0.0), (1.0, f64::INFINITY), (2.0, 0.0)], 0..100);
assert_eq!(infinite, [(0, 0.0, 0.0), (2, 0.0, 0.0)]);
}
#[test]
fn only_the_columns_asked_for_are_given() {
let got = spans(&[(-1_000.0, 0.0), (1_000.0, 2_000.0)], 0..3);
assert_eq!(
got,
[
(0, 1_000.0, 1_001.0),
(1, 1_001.0, 1_002.0),
(2, 1_002.0, 1_003.0)
]
);
let far = spans(&[(-1e30, 1.0), (1e30, 1.0)], 0..2);
assert_eq!(far, [(0, 1.0, 1.0), (1, 1.0, 1.0)]);
}
#[test]
fn a_point_left_of_the_last_starts_the_line_afresh() {
let got = spans(&[(5.0, 0.0), (2.0, 1.0), (3.0, 3.0)], 0..100);
assert_eq!(got, [(5, 0.0, 0.0), (2, 1.0, 3.0), (3, 3.0, 3.0)]);
}
#[test]
fn gaps_within_a_column_leave_it_one_span() {
let points = [
(0.1, 1.0),
(0.2, f64::NAN),
(0.3, 5.0),
(0.4, f64::NAN),
(1.5, 2.0),
];
assert_eq!(spans(&points, 0..9), [(0, 1.0, 5.0), (1, 2.0, 2.0)]);
}
#[test]
fn a_lone_point_is_a_span_of_no_height() {
assert_eq!(spans(&[(7.2, 3.0)], 0..100), [(7, 3.0, 3.0)]);
assert_eq!(spans(&[], 0..100), []);
}
}