#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CurveType {
Linear,
MonotoneX,
Step,
}
pub struct LineGenerator {
curve: CurveType,
}
impl LineGenerator {
pub fn new() -> Self {
Self {
curve: CurveType::Linear,
}
}
pub fn curve(mut self, curve: CurveType) -> Self {
self.curve = curve;
self
}
pub fn generate(&self, points: &[(f64, f64)]) -> String {
if points.is_empty() {
return String::new();
}
match self.curve {
CurveType::Linear => generate_linear(points),
CurveType::MonotoneX => generate_monotone_x(points),
CurveType::Step => generate_step(points),
}
}
}
impl Default for LineGenerator {
fn default() -> Self {
Self::new()
}
}
pub(crate) fn fmt(v: f64) -> String {
if v == v.round() && v.abs() < 1e10 {
format!("{}", v as i64)
} else {
let s = format!("{:.6}", v);
s.trim_end_matches('0').trim_end_matches('.').to_string()
}
}
fn generate_linear(points: &[(f64, f64)]) -> String {
let mut path = String::new();
for (i, &(x, y)) in points.iter().enumerate() {
if i == 0 {
path.push_str(&format!("M{},{}", fmt(x), fmt(y)));
} else {
path.push_str(&format!("L{},{}", fmt(x), fmt(y)));
}
}
path
}
fn generate_step(points: &[(f64, f64)]) -> String {
let n = points.len();
if n == 0 {
return String::new();
}
if n == 1 {
return format!("M{},{}", fmt(points[0].0), fmt(points[0].1));
}
let mut path = format!("M{},{}", fmt(points[0].0), fmt(points[0].1));
let mut prev_x = points[0].0;
let mut prev_y = points[0].1;
for &(x, y) in &points[1..] {
let x_mid = prev_x * 0.5 + x * 0.5;
path.push_str(&format!("L{},{}", fmt(x_mid), fmt(prev_y)));
path.push_str(&format!("L{},{}", fmt(x_mid), fmt(y)));
prev_x = x;
prev_y = y;
}
path.push_str(&format!("L{},{}", fmt(prev_x), fmt(prev_y)));
path
}
fn generate_monotone_x(points: &[(f64, f64)]) -> String {
let n = points.len();
if n == 0 {
return String::new();
}
if n == 1 {
return format!("M{},{}", fmt(points[0].0), fmt(points[0].1));
}
if n == 2 {
return generate_linear(points);
}
let mut secants = Vec::with_capacity(n - 1);
for i in 0..n - 1 {
let dx = points[i + 1].0 - points[i].0;
if dx == 0.0 {
secants.push(0.0);
} else {
secants.push((points[i + 1].1 - points[i].1) / dx);
}
}
let mut tangents = vec![0.0; n];
tangents[0] = secants[0];
tangents[n - 1] = secants[n - 2];
for i in 1..n - 1 {
if secants[i - 1].signum() != secants[i].signum() {
tangents[i] = 0.0;
} else {
tangents[i] = (secants[i - 1] + secants[i]) / 2.0;
}
}
for i in 0..n - 1 {
if secants[i] == 0.0 {
tangents[i] = 0.0;
tangents[i + 1] = 0.0;
} else {
let alpha = tangents[i] / secants[i];
let beta = tangents[i + 1] / secants[i];
let sum_sq = alpha * alpha + beta * beta;
if sum_sq > 9.0 {
let tau = 3.0 / sum_sq.sqrt();
tangents[i] = tau * alpha * secants[i];
tangents[i + 1] = tau * beta * secants[i];
}
}
}
let mut path = format!("M{},{}", fmt(points[0].0), fmt(points[0].1));
for i in 0..n - 1 {
let dx = points[i + 1].0 - points[i].0;
let cp1x = points[i].0 + dx / 3.0;
let cp1y = points[i].1 + tangents[i] * dx / 3.0;
let cp2x = points[i + 1].0 - dx / 3.0;
let cp2y = points[i + 1].1 - tangents[i + 1] * dx / 3.0;
path.push_str(&format!(
"C{},{} {},{} {},{}",
fmt(cp1x),
fmt(cp1y),
fmt(cp2x),
fmt(cp2y),
fmt(points[i + 1].0),
fmt(points[i + 1].1),
));
}
path
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn line_linear_basic() {
let gen = LineGenerator::new();
let path = gen.generate(&[(0.0, 10.0), (50.0, 20.0), (100.0, 5.0)]);
assert_eq!(path, "M0,10L50,20L100,5");
}
#[test]
fn line_linear_single_point() {
let gen = LineGenerator::new();
let path = gen.generate(&[(0.0, 10.0)]);
assert_eq!(path, "M0,10");
}
#[test]
fn line_linear_two_points() {
let gen = LineGenerator::new();
let path = gen.generate(&[(0.0, 10.0), (50.0, 20.0)]);
assert_eq!(path, "M0,10L50,20");
}
#[test]
fn line_linear_empty() {
let gen = LineGenerator::new();
let path = gen.generate(&[]);
assert_eq!(path, "");
}
#[test]
fn line_step_basic() {
let gen = LineGenerator::new().curve(CurveType::Step);
let path = gen.generate(&[(0.0, 10.0), (50.0, 20.0), (100.0, 5.0)]);
assert_eq!(path, "M0,10L25,10L25,20L75,20L75,5L100,5");
assert!(!path.contains("C"), "Step path should NOT contain C commands");
}
#[test]
fn line_step_single_point() {
let gen = LineGenerator::new().curve(CurveType::Step);
let path = gen.generate(&[(42.0, 7.0)]);
assert_eq!(path, "M42,7");
}
#[test]
fn line_step_two_points() {
let gen = LineGenerator::new().curve(CurveType::Step);
let path = gen.generate(&[(0.0, 10.0), (100.0, 20.0)]);
assert_eq!(path, "M0,10L50,10L50,20L100,20");
}
#[test]
fn line_monotone_basic() {
let gen = LineGenerator::new().curve(CurveType::MonotoneX);
let path = gen.generate(&[
(0.0, 10.0),
(50.0, 20.0),
(100.0, 5.0),
(150.0, 15.0),
]);
assert!(path.starts_with("M"), "Path should start with M, got: {}", path);
assert!(path.contains("C"), "Path should contain C commands, got: {}", path);
}
}