#[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),
}
}
pub fn generate_with_length(&self, points: &[(f64, f64)]) -> (String, f64) {
let path = self.generate(points);
let length = self.compute_length(points);
(path, length)
}
fn compute_length(&self, points: &[(f64, f64)]) -> f64 {
if points.len() < 2 {
return 0.0;
}
match self.curve {
CurveType::Linear => {
points
.windows(2)
.map(|w| {
let dx = w[1].0 - w[0].0;
let dy = w[1].1 - w[0].1;
(dx * dx + dy * dy).sqrt()
})
.sum()
}
CurveType::Step => {
let mut length = 0.0;
let mut cursor_x = points[0].0;
let mut prev_y = points[0].1;
let mut raw_prev_x = points[0].0;
for &(x, y) in &points[1..] {
let x_mid = (raw_prev_x + x) * 0.5;
length += (x_mid - cursor_x).abs();
length += (y - prev_y).abs();
cursor_x = x_mid;
prev_y = y;
raw_prev_x = x;
}
length += (points[points.len() - 1].0 - cursor_x).abs();
length
}
CurveType::MonotoneX => {
let n = points.len();
if n == 2 {
let dx = points[1].0 - points[0].0;
let dy = points[1].1 - points[0].1;
return (dx * dx + dy * dy).sqrt();
}
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 total = 0.0;
for i in 0..n - 1 {
let dx = points[i + 1].0 - points[i].0;
let p0 = points[i];
let p1 = (points[i].0 + dx / 3.0, points[i].1 + tangents[i] * dx / 3.0);
let p2 = (points[i + 1].0 - dx / 3.0, points[i + 1].1 - tangents[i + 1] * dx / 3.0);
let p3 = points[i + 1];
let d01 = ((p1.0 - p0.0).powi(2) + (p1.1 - p0.1).powi(2)).sqrt();
let d12 = ((p2.0 - p1.0).powi(2) + (p2.1 - p1.1).powi(2)).sqrt();
let d23 = ((p3.0 - p2.0).powi(2) + (p3.1 - p2.1).powi(2)).sqrt();
total += d01 + d12 + d23;
}
total * 1.05
}
}
}
}
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 {
#![allow(clippy::unwrap_used)]
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);
}
#[test]
fn line_linear_length() {
let gen = LineGenerator::new();
let (path, length) = gen.generate_with_length(&[(0.0, 0.0), (3.0, 0.0), (3.0, 4.0)]);
assert!(!path.is_empty());
assert!((length - 7.0).abs() < 1e-10, "expected 7.0, got {}", length);
}
#[test]
fn line_step_length() {
let gen = LineGenerator::new().curve(CurveType::Step);
let (path, length) = gen.generate_with_length(&[(0.0, 10.0), (100.0, 20.0)]);
assert!(!path.is_empty());
assert!((length - 110.0).abs() < 1e-10, "expected 110.0, got {}", length);
let (_, length3) = gen.generate_with_length(&[(0.0, 10.0), (50.0, 20.0), (100.0, 5.0)]);
assert!((length3 - 125.0).abs() < 1e-10, "expected 125.0, got {}", length3);
}
#[test]
fn line_monotone_length() {
let gen = LineGenerator::new().curve(CurveType::MonotoneX);
let points = [(0.0, 10.0), (50.0, 20.0), (100.0, 5.0), (150.0, 15.0)];
let (path, length) = gen.generate_with_length(&points);
assert!(!path.is_empty());
let chord_sum: f64 = points.windows(2).map(|w| {
let dx = w[1].0 - w[0].0;
let dy = w[1].1 - w[0].1;
(dx * dx + dy * dy).sqrt()
}).sum();
assert!(
length >= chord_sum,
"monotone length {} should be >= chord sum {}",
length, chord_sum
);
}
#[test]
fn line_single_point_length() {
let gen = LineGenerator::new();
let (_, length) = gen.generate_with_length(&[(42.0, 7.0)]);
assert!((length - 0.0).abs() < 1e-10, "single point should have length 0.0, got {}", length);
}
#[test]
fn line_empty_length() {
let gen = LineGenerator::new();
let (_, length) = gen.generate_with_length(&[]);
assert!((length - 0.0).abs() < 1e-10, "empty should have length 0.0, got {}", length);
}
}