1#[derive(Debug, Clone, Copy, PartialEq)]
5pub enum CurveType {
6 Linear,
8 MonotoneX,
10 Step,
13}
14
15pub struct LineGenerator {
17 curve: CurveType,
18}
19
20impl LineGenerator {
21 pub fn new() -> Self {
23 Self {
24 curve: CurveType::Linear,
25 }
26 }
27
28 pub fn curve(mut self, curve: CurveType) -> Self {
30 self.curve = curve;
31 self
32 }
33
34 pub fn generate(&self, points: &[(f64, f64)]) -> String {
36 if points.is_empty() {
37 return String::new();
38 }
39
40 match self.curve {
41 CurveType::Linear => generate_linear(points),
42 CurveType::MonotoneX => generate_monotone_x(points),
43 CurveType::Step => generate_step(points),
44 }
45 }
46}
47
48impl Default for LineGenerator {
49 fn default() -> Self {
50 Self::new()
51 }
52}
53
54pub(crate) fn fmt(v: f64) -> String {
56 if v == v.round() && v.abs() < 1e10 {
57 format!("{}", v as i64)
58 } else {
59 let s = format!("{:.6}", v);
60 s.trim_end_matches('0').trim_end_matches('.').to_string()
61 }
62}
63
64fn generate_linear(points: &[(f64, f64)]) -> String {
65 let mut path = String::new();
66 for (i, &(x, y)) in points.iter().enumerate() {
67 if i == 0 {
68 path.push_str(&format!("M{},{}", fmt(x), fmt(y)));
69 } else {
70 path.push_str(&format!("L{},{}", fmt(x), fmt(y)));
71 }
72 }
73 path
74}
75
76fn generate_step(points: &[(f64, f64)]) -> String {
85 let n = points.len();
86 if n == 0 {
87 return String::new();
88 }
89 if n == 1 {
90 return format!("M{},{}", fmt(points[0].0), fmt(points[0].1));
91 }
92
93 let mut path = format!("M{},{}", fmt(points[0].0), fmt(points[0].1));
102
103 let mut prev_x = points[0].0;
104 let mut prev_y = points[0].1;
105
106 for &(x, y) in &points[1..] {
107 let x_mid = prev_x * 0.5 + x * 0.5;
108 path.push_str(&format!("L{},{}", fmt(x_mid), fmt(prev_y)));
109 path.push_str(&format!("L{},{}", fmt(x_mid), fmt(y)));
110 prev_x = x;
111 prev_y = y;
112 }
113
114 path.push_str(&format!("L{},{}", fmt(prev_x), fmt(prev_y)));
118
119 path
120}
121
122fn generate_monotone_x(points: &[(f64, f64)]) -> String {
123 let n = points.len();
124 if n == 0 {
125 return String::new();
126 }
127 if n == 1 {
128 return format!("M{},{}", fmt(points[0].0), fmt(points[0].1));
129 }
130 if n == 2 {
131 return generate_linear(points);
133 }
134
135 let mut secants = Vec::with_capacity(n - 1);
137 for i in 0..n - 1 {
138 let dx = points[i + 1].0 - points[i].0;
139 if dx == 0.0 {
140 secants.push(0.0);
141 } else {
142 secants.push((points[i + 1].1 - points[i].1) / dx);
143 }
144 }
145
146 let mut tangents = vec![0.0; n];
148 tangents[0] = secants[0];
149 tangents[n - 1] = secants[n - 2];
150 for i in 1..n - 1 {
151 if secants[i - 1].signum() != secants[i].signum() {
152 tangents[i] = 0.0;
153 } else {
154 tangents[i] = (secants[i - 1] + secants[i]) / 2.0;
155 }
156 }
157
158 for i in 0..n - 1 {
160 if secants[i] == 0.0 {
161 tangents[i] = 0.0;
162 tangents[i + 1] = 0.0;
163 } else {
164 let alpha = tangents[i] / secants[i];
165 let beta = tangents[i + 1] / secants[i];
166 let sum_sq = alpha * alpha + beta * beta;
167 if sum_sq > 9.0 {
168 let tau = 3.0 / sum_sq.sqrt();
169 tangents[i] = tau * alpha * secants[i];
170 tangents[i + 1] = tau * beta * secants[i];
171 }
172 }
173 }
174
175 let mut path = format!("M{},{}", fmt(points[0].0), fmt(points[0].1));
177 for i in 0..n - 1 {
178 let dx = points[i + 1].0 - points[i].0;
179 let cp1x = points[i].0 + dx / 3.0;
180 let cp1y = points[i].1 + tangents[i] * dx / 3.0;
181 let cp2x = points[i + 1].0 - dx / 3.0;
182 let cp2y = points[i + 1].1 - tangents[i + 1] * dx / 3.0;
183 path.push_str(&format!(
184 "C{},{} {},{} {},{}",
185 fmt(cp1x),
186 fmt(cp1y),
187 fmt(cp2x),
188 fmt(cp2y),
189 fmt(points[i + 1].0),
190 fmt(points[i + 1].1),
191 ));
192 }
193 path
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn line_linear_basic() {
202 let gen = LineGenerator::new();
203 let path = gen.generate(&[(0.0, 10.0), (50.0, 20.0), (100.0, 5.0)]);
204 assert_eq!(path, "M0,10L50,20L100,5");
205 }
206
207 #[test]
208 fn line_linear_single_point() {
209 let gen = LineGenerator::new();
210 let path = gen.generate(&[(0.0, 10.0)]);
211 assert_eq!(path, "M0,10");
212 }
213
214 #[test]
215 fn line_linear_two_points() {
216 let gen = LineGenerator::new();
217 let path = gen.generate(&[(0.0, 10.0), (50.0, 20.0)]);
218 assert_eq!(path, "M0,10L50,20");
219 }
220
221 #[test]
222 fn line_linear_empty() {
223 let gen = LineGenerator::new();
224 let path = gen.generate(&[]);
225 assert_eq!(path, "");
226 }
227
228 #[test]
229 fn line_step_basic() {
230 let gen = LineGenerator::new().curve(CurveType::Step);
231 let path = gen.generate(&[(0.0, 10.0), (50.0, 20.0), (100.0, 5.0)]);
232 assert_eq!(path, "M0,10L25,10L25,20L75,20L75,5L100,5");
235 assert!(!path.contains("C"), "Step path should NOT contain C commands");
236 }
237
238 #[test]
239 fn line_step_single_point() {
240 let gen = LineGenerator::new().curve(CurveType::Step);
241 let path = gen.generate(&[(42.0, 7.0)]);
242 assert_eq!(path, "M42,7");
243 }
244
245 #[test]
246 fn line_step_two_points() {
247 let gen = LineGenerator::new().curve(CurveType::Step);
248 let path = gen.generate(&[(0.0, 10.0), (100.0, 20.0)]);
249 assert_eq!(path, "M0,10L50,10L50,20L100,20");
251 }
252
253 #[test]
254 fn line_monotone_basic() {
255 let gen = LineGenerator::new().curve(CurveType::MonotoneX);
256 let path = gen.generate(&[
257 (0.0, 10.0),
258 (50.0, 20.0),
259 (100.0, 5.0),
260 (150.0, 15.0),
261 ]);
262 assert!(path.starts_with("M"), "Path should start with M, got: {}", path);
263 assert!(path.contains("C"), "Path should contain C commands, got: {}", path);
264 }
265}