#[derive(Copy, Clone, Debug, Default, PartialEq)]
#[non_exhaustive]
pub enum Interpolation {
Linear,
Step(Step),
#[default]
Monotone,
}
#[derive(Copy, Clone, Debug, Default, PartialEq)]
#[non_exhaustive]
pub enum Step {
#[default]
Horizontal,
HorizontalMiddle,
Vertical,
VerticalMiddle,
}
impl std::str::FromStr for Interpolation {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"linear" => Ok(Self::Linear),
"step-horizontal" => Ok(Self::Step(Step::Horizontal)),
"step-horizontal-middle" => Ok(Self::Step(Step::HorizontalMiddle)),
"step-vertical" => Ok(Self::Step(Step::Vertical)),
"step-vertical-middle" => Ok(Self::Step(Step::VerticalMiddle)),
"monotone" => Ok(Self::Monotone),
_ => Err(format!("unknown line interpolation: `{}`", s)),
}
}
}
impl std::fmt::Display for Interpolation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Linear => write!(f, "linear"),
Self::Step(Step::Horizontal) => write!(f, "step-horizontal"),
Self::Step(Step::HorizontalMiddle) => write!(f, "step-horizontal-middle"),
Self::Step(Step::Vertical) => write!(f, "step-vertical"),
Self::Step(Step::VerticalMiddle) => write!(f, "step-vertical-middle"),
Self::Monotone => write!(f, "monotone"),
}
}
}
impl From<Step> for Interpolation {
fn from(step: Step) -> Self {
Self::Step(step)
}
}
impl Interpolation {
pub(super) fn path(self, points: &[(f64, f64)]) -> String {
match self {
Self::Linear => linear(points),
Self::Step(step) => step.path(points),
Self::Monotone => monotone(points),
}
}
}
fn linear(points: &[(f64, f64)]) -> String {
let mut need_move = true;
points
.iter()
.map(|(x, y)| {
if x.is_nan() || y.is_nan() {
need_move = true;
"".to_string()
} else if need_move {
need_move = false;
format!("M {} {} ", x, y)
} else {
format!("L {} {} ", x, y)
}
})
.collect::<String>()
}
impl Step {
fn path(self, points: &[(f64, f64)]) -> String {
let mut prev: Option<(f64, f64)> = None;
points
.iter()
.map(|&(x, y)| {
if x.is_nan() || y.is_nan() {
prev = None;
"".to_string()
} else if let Some((prev_x, prev_y)) = prev {
prev = Some((x, y));
match self {
Self::Horizontal => format!("H {} V {} ", x, y),
Self::HorizontalMiddle => {
format!("H {} V {} H {} ", (x + prev_x) / 2.0, y, x)
}
Self::Vertical => format!("V {} H {} ", y, x),
Self::VerticalMiddle => {
format!("V {} H {} V {} ", (y + prev_y) / 2.0, x, y)
}
}
} else {
prev = Some((x, y));
format!("M {} {} ", x, y)
}
})
.collect::<String>()
}
}
fn monotone(points: &[(f64, f64)]) -> String {
let mut path = String::with_capacity(points.len());
for i in 0..points.len() {
let (x_prev, y_prev) = get_or_nan(points, i.checked_sub(1));
let (x, y) = points[i];
let (x_next, y_next) = get_or_nan(points, i.checked_add(1));
let cmd = if x.is_nan() || y.is_nan() {
"".to_string()
} else if x_prev.is_nan() || y_prev.is_nan() {
format!("M {x},{y} ")
} else if x_next.is_nan() || y_next.is_nan() {
format!("L {x},{y} ")
} else {
let tangent = tangent(x_prev, x, x_next, y_prev, y, y_next);
let dx = (x - x_prev) / 3.0;
let x_c = x - dx;
let y_c = y - dx * tangent;
format!("S {x_c},{y_c} {x},{y} ")
};
path.push_str(&cmd);
}
path
}
fn get_or_nan(points: &[(f64, f64)], i: Option<usize>) -> (f64, f64) {
i.and_then(|i| points.get(i).copied())
.unwrap_or((f64::NAN, f64::NAN))
}
fn slope(x: f64, y: f64, x_next: f64, y_next: f64) -> f64 {
(y_next - y) / (x_next - x)
}
fn tangent(x_prev: f64, x: f64, x_next: f64, y_prev: f64, y: f64, y_next: f64) -> f64 {
let slope_prev = slope(x_prev, y_prev, x, y);
let slope = slope(x, y, x_next, y_next);
let dist_prev = x - x_prev;
let dist = x_next - x;
let para = (slope_prev * dist + slope * dist_prev) / (dist_prev + dist);
(slope_prev.signum() + slope.signum()) * slope_prev.abs().min(0.5 * para.abs())
}