use super::scale::Scale;
use std::f64::consts::TAU;
type P = (f64, f64);
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Dir {
Column,
Row,
Radial,
}
pub struct Plot {
pub x0: f64,
pub y0: f64,
pub x1: f64,
pub y1: f64,
pub dir: Dir,
}
impl Plot {
pub fn w(&self) -> f64 {
self.x1 - self.x0
}
pub fn h(&self) -> f64 {
self.y1 - self.y0
}
pub fn project(&self, x: &Scale, xv: f64, value: &Scale, v: f64) -> P {
match self.dir {
Dir::Column => (self.domain_at(x, xv), self.value_at(value, v)),
Dir::Row => (self.value_at(value, v), self.domain_at(x, xv)),
Dir::Radial => {
let (cx, cy) = self.center();
let theta = self.spoke_angle(x, xv);
let r = value.frac(v) * self.radius();
(cx + r * theta.sin(), cy - r * theta.cos())
}
}
}
pub fn is_radial(&self) -> bool {
self.dir == Dir::Radial
}
pub fn center(&self) -> P {
((self.x0 + self.x1) / 2.0, (self.y0 + self.y1) / 2.0)
}
pub fn radius(&self) -> f64 {
self.w().min(self.h()) / 2.0
}
pub fn spoke_angle(&self, x: &Scale, xv: f64) -> f64 {
let n = match x {
Scale::Band { n } => *n as f64,
_ => 1.0,
};
TAU * xv / n.max(1.0)
}
pub fn domain_at(&self, x: &Scale, v: f64) -> f64 {
match self.dir {
Dir::Row => self.y0 + x.frac(v) * self.h(),
_ => self.x_at(x, v),
}
}
pub fn value_at(&self, s: &Scale, v: f64) -> f64 {
match self.dir {
Dir::Row => self.x0 + s.frac(v) * self.w(),
_ => self.y_at(s, v),
}
}
pub fn cross(&self, axis_horizontal: bool, p: f64) -> Vec<P> {
if axis_horizontal {
vec![(p, self.y0), (p, self.y1)]
} else {
vec![(self.x0, p), (self.x1, p)]
}
}
pub fn x_at(&self, x: &Scale, v: f64) -> f64 {
self.x0 + x.frac(v) * self.w()
}
pub fn y_at(&self, value: &Scale, v: f64) -> f64 {
self.y1 - value.frac(v) * self.h()
}
pub fn slot_px(&self, x: &Scale, i: usize) -> (f64, f64) {
let (f0, f1) = x.slot(i);
(self.x0 + f0 * self.w(), self.x0 + f1 * self.w())
}
pub fn clip(&self, points: &[P]) -> Vec<Vec<P>> {
let mut runs: Vec<Vec<P>> = Vec::new();
let mut cur: Vec<P> = Vec::new();
for w in points.windows(2) {
if let Some((a, b, b_in)) = self.clip_segment(w[0], w[1]) {
if cur.is_empty() {
cur.push(a);
} else if cur.last() != Some(&a) {
runs.push(std::mem::take(&mut cur));
cur.push(a);
}
cur.push(b);
if !b_in {
runs.push(std::mem::take(&mut cur));
}
}
}
if !cur.is_empty() {
runs.push(cur);
}
runs.retain(|r| r.len() >= 2);
runs
}
fn clip_segment(&self, p0: P, p1: P) -> Option<(P, P, bool)> {
let (t0, t1) = liang_barsky(p0, p1, (self.x0, self.y0), (self.x1, self.y1))?;
let (dx, dy) = (p1.0 - p0.0, p1.1 - p0.1);
let a = (p0.0 + t0 * dx, p0.1 + t0 * dy);
let b = (p0.0 + t1 * dx, p0.1 + t1 * dy);
Some((a, b, t1 == 1.0))
}
}
pub(super) fn liang_barsky(p0: P, p1: P, min: P, max: P) -> Option<(f64, f64)> {
let (dx, dy) = (p1.0 - p0.0, p1.1 - p0.1);
let checks = [
(-dx, p0.0 - min.0),
(dx, max.0 - p0.0),
(-dy, p0.1 - min.1),
(dy, max.1 - p0.1),
];
let (mut t0, mut t1) = (0.0_f64, 1.0_f64);
for (p, q) in checks {
if p.abs() < 1e-9 {
if q < 0.0 {
return None;
}
} else {
let t = q / p;
if p < 0.0 {
t0 = t0.max(t);
} else {
t1 = t1.min(t);
}
}
}
(t0 <= t1).then_some((t0, t1))
}