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 pub fn generate_with_length(&self, points: &[(f64, f64)]) -> (String, f64) {
52 let path = self.generate(points);
53 let length = self.compute_length(points);
54 (path, length)
55 }
56
57 fn compute_length(&self, points: &[(f64, f64)]) -> f64 {
59 if points.len() < 2 {
60 return 0.0;
61 }
62 match self.curve {
63 CurveType::Linear => {
64 points
65 .windows(2)
66 .map(|w| {
67 let dx = w[1].0 - w[0].0;
68 let dy = w[1].1 - w[0].1;
69 (dx * dx + dy * dy).sqrt()
70 })
71 .sum()
72 }
73 CurveType::Step => {
74 let mut length = 0.0;
78 let mut cursor_x = points[0].0;
79 let mut prev_y = points[0].1;
80 let mut raw_prev_x = points[0].0;
81 for &(x, y) in &points[1..] {
82 let x_mid = (raw_prev_x + x) * 0.5;
83 length += (x_mid - cursor_x).abs();
84 length += (y - prev_y).abs();
85 cursor_x = x_mid;
86 prev_y = y;
87 raw_prev_x = x;
88 }
89 length += (points[points.len() - 1].0 - cursor_x).abs();
90 length
91 }
92 CurveType::MonotoneX => {
93 let n = points.len();
97 if n == 2 {
98 let dx = points[1].0 - points[0].0;
100 let dy = points[1].1 - points[0].1;
101 return (dx * dx + dy * dy).sqrt();
102 }
103
104 let mut secants = Vec::with_capacity(n - 1);
106 for i in 0..n - 1 {
107 let dx = points[i + 1].0 - points[i].0;
108 if dx == 0.0 {
109 secants.push(0.0);
110 } else {
111 secants.push((points[i + 1].1 - points[i].1) / dx);
112 }
113 }
114 let mut tangents = vec![0.0; n];
115 tangents[0] = secants[0];
116 tangents[n - 1] = secants[n - 2];
117 for i in 1..n - 1 {
118 if secants[i - 1].signum() != secants[i].signum() {
119 tangents[i] = 0.0;
120 } else {
121 tangents[i] = (secants[i - 1] + secants[i]) / 2.0;
122 }
123 }
124 for i in 0..n - 1 {
125 if secants[i] == 0.0 {
126 tangents[i] = 0.0;
127 tangents[i + 1] = 0.0;
128 } else {
129 let alpha = tangents[i] / secants[i];
130 let beta = tangents[i + 1] / secants[i];
131 let sum_sq = alpha * alpha + beta * beta;
132 if sum_sq > 9.0 {
133 let tau = 3.0 / sum_sq.sqrt();
134 tangents[i] = tau * alpha * secants[i];
135 tangents[i + 1] = tau * beta * secants[i];
136 }
137 }
138 }
139
140 let mut total = 0.0;
142 for i in 0..n - 1 {
143 let dx = points[i + 1].0 - points[i].0;
144 let p0 = points[i];
145 let p1 = (points[i].0 + dx / 3.0, points[i].1 + tangents[i] * dx / 3.0);
146 let p2 = (points[i + 1].0 - dx / 3.0, points[i + 1].1 - tangents[i + 1] * dx / 3.0);
147 let p3 = points[i + 1];
148
149 let d01 = ((p1.0 - p0.0).powi(2) + (p1.1 - p0.1).powi(2)).sqrt();
150 let d12 = ((p2.0 - p1.0).powi(2) + (p2.1 - p1.1).powi(2)).sqrt();
151 let d23 = ((p3.0 - p2.0).powi(2) + (p3.1 - p2.1).powi(2)).sqrt();
152 total += d01 + d12 + d23;
153 }
154 total * 1.05
155 }
156 }
157 }
158}
159
160impl Default for LineGenerator {
161 fn default() -> Self {
162 Self::new()
163 }
164}
165
166pub(crate) fn fmt(v: f64) -> String {
168 if v == v.round() && v.abs() < 1e10 {
169 format!("{}", v as i64)
170 } else {
171 let s = format!("{:.6}", v);
172 s.trim_end_matches('0').trim_end_matches('.').to_string()
173 }
174}
175
176fn generate_linear(points: &[(f64, f64)]) -> String {
177 let mut path = String::new();
178 for (i, &(x, y)) in points.iter().enumerate() {
179 if i == 0 {
180 path.push_str(&format!("M{},{}", fmt(x), fmt(y)));
181 } else {
182 path.push_str(&format!("L{},{}", fmt(x), fmt(y)));
183 }
184 }
185 path
186}
187
188fn generate_step(points: &[(f64, f64)]) -> String {
197 let n = points.len();
198 if n == 0 {
199 return String::new();
200 }
201 if n == 1 {
202 return format!("M{},{}", fmt(points[0].0), fmt(points[0].1));
203 }
204
205 let mut path = format!("M{},{}", fmt(points[0].0), fmt(points[0].1));
214
215 let mut prev_x = points[0].0;
216 let mut prev_y = points[0].1;
217
218 for &(x, y) in &points[1..] {
219 let x_mid = prev_x * 0.5 + x * 0.5;
220 path.push_str(&format!("L{},{}", fmt(x_mid), fmt(prev_y)));
221 path.push_str(&format!("L{},{}", fmt(x_mid), fmt(y)));
222 prev_x = x;
223 prev_y = y;
224 }
225
226 path.push_str(&format!("L{},{}", fmt(prev_x), fmt(prev_y)));
230
231 path
232}
233
234fn generate_monotone_x(points: &[(f64, f64)]) -> String {
235 let n = points.len();
236 if n == 0 {
237 return String::new();
238 }
239 if n == 1 {
240 return format!("M{},{}", fmt(points[0].0), fmt(points[0].1));
241 }
242 if n == 2 {
243 return generate_linear(points);
245 }
246
247 let mut secants = Vec::with_capacity(n - 1);
249 for i in 0..n - 1 {
250 let dx = points[i + 1].0 - points[i].0;
251 if dx == 0.0 {
252 secants.push(0.0);
253 } else {
254 secants.push((points[i + 1].1 - points[i].1) / dx);
255 }
256 }
257
258 let mut tangents = vec![0.0; n];
260 tangents[0] = secants[0];
261 tangents[n - 1] = secants[n - 2];
262 for i in 1..n - 1 {
263 if secants[i - 1].signum() != secants[i].signum() {
264 tangents[i] = 0.0;
265 } else {
266 tangents[i] = (secants[i - 1] + secants[i]) / 2.0;
267 }
268 }
269
270 for i in 0..n - 1 {
272 if secants[i] == 0.0 {
273 tangents[i] = 0.0;
274 tangents[i + 1] = 0.0;
275 } else {
276 let alpha = tangents[i] / secants[i];
277 let beta = tangents[i + 1] / secants[i];
278 let sum_sq = alpha * alpha + beta * beta;
279 if sum_sq > 9.0 {
280 let tau = 3.0 / sum_sq.sqrt();
281 tangents[i] = tau * alpha * secants[i];
282 tangents[i + 1] = tau * beta * secants[i];
283 }
284 }
285 }
286
287 let mut path = format!("M{},{}", fmt(points[0].0), fmt(points[0].1));
289 for i in 0..n - 1 {
290 let dx = points[i + 1].0 - points[i].0;
291 let cp1x = points[i].0 + dx / 3.0;
292 let cp1y = points[i].1 + tangents[i] * dx / 3.0;
293 let cp2x = points[i + 1].0 - dx / 3.0;
294 let cp2y = points[i + 1].1 - tangents[i + 1] * dx / 3.0;
295 path.push_str(&format!(
296 "C{},{} {},{} {},{}",
297 fmt(cp1x),
298 fmt(cp1y),
299 fmt(cp2x),
300 fmt(cp2y),
301 fmt(points[i + 1].0),
302 fmt(points[i + 1].1),
303 ));
304 }
305 path
306}
307
308#[cfg(test)]
309mod tests {
310 #![allow(clippy::unwrap_used)]
311 use super::*;
312
313 #[test]
314 fn line_linear_basic() {
315 let gen = LineGenerator::new();
316 let path = gen.generate(&[(0.0, 10.0), (50.0, 20.0), (100.0, 5.0)]);
317 assert_eq!(path, "M0,10L50,20L100,5");
318 }
319
320 #[test]
321 fn line_linear_single_point() {
322 let gen = LineGenerator::new();
323 let path = gen.generate(&[(0.0, 10.0)]);
324 assert_eq!(path, "M0,10");
325 }
326
327 #[test]
328 fn line_linear_two_points() {
329 let gen = LineGenerator::new();
330 let path = gen.generate(&[(0.0, 10.0), (50.0, 20.0)]);
331 assert_eq!(path, "M0,10L50,20");
332 }
333
334 #[test]
335 fn line_linear_empty() {
336 let gen = LineGenerator::new();
337 let path = gen.generate(&[]);
338 assert_eq!(path, "");
339 }
340
341 #[test]
342 fn line_step_basic() {
343 let gen = LineGenerator::new().curve(CurveType::Step);
344 let path = gen.generate(&[(0.0, 10.0), (50.0, 20.0), (100.0, 5.0)]);
345 assert_eq!(path, "M0,10L25,10L25,20L75,20L75,5L100,5");
348 assert!(!path.contains("C"), "Step path should NOT contain C commands");
349 }
350
351 #[test]
352 fn line_step_single_point() {
353 let gen = LineGenerator::new().curve(CurveType::Step);
354 let path = gen.generate(&[(42.0, 7.0)]);
355 assert_eq!(path, "M42,7");
356 }
357
358 #[test]
359 fn line_step_two_points() {
360 let gen = LineGenerator::new().curve(CurveType::Step);
361 let path = gen.generate(&[(0.0, 10.0), (100.0, 20.0)]);
362 assert_eq!(path, "M0,10L50,10L50,20L100,20");
364 }
365
366 #[test]
367 fn line_monotone_basic() {
368 let gen = LineGenerator::new().curve(CurveType::MonotoneX);
369 let path = gen.generate(&[
370 (0.0, 10.0),
371 (50.0, 20.0),
372 (100.0, 5.0),
373 (150.0, 15.0),
374 ]);
375 assert!(path.starts_with("M"), "Path should start with M, got: {}", path);
376 assert!(path.contains("C"), "Path should contain C commands, got: {}", path);
377 }
378
379 #[test]
382 fn line_linear_length() {
383 let gen = LineGenerator::new();
386 let (path, length) = gen.generate_with_length(&[(0.0, 0.0), (3.0, 0.0), (3.0, 4.0)]);
387 assert!(!path.is_empty());
388 assert!((length - 7.0).abs() < 1e-10, "expected 7.0, got {}", length);
389 }
390
391 #[test]
392 fn line_step_length() {
393 let gen = LineGenerator::new().curve(CurveType::Step);
397 let (path, length) = gen.generate_with_length(&[(0.0, 10.0), (100.0, 20.0)]);
398 assert!(!path.is_empty());
399 assert!((length - 110.0).abs() < 1e-10, "expected 110.0, got {}", length);
400
401 let (_, length3) = gen.generate_with_length(&[(0.0, 10.0), (50.0, 20.0), (100.0, 5.0)]);
405 assert!((length3 - 125.0).abs() < 1e-10, "expected 125.0, got {}", length3);
406 }
407
408 #[test]
409 fn line_monotone_length() {
410 let gen = LineGenerator::new().curve(CurveType::MonotoneX);
412 let points = [(0.0, 10.0), (50.0, 20.0), (100.0, 5.0), (150.0, 15.0)];
413 let (path, length) = gen.generate_with_length(&points);
414 assert!(!path.is_empty());
415
416 let chord_sum: f64 = points.windows(2).map(|w| {
417 let dx = w[1].0 - w[0].0;
418 let dy = w[1].1 - w[0].1;
419 (dx * dx + dy * dy).sqrt()
420 }).sum();
421 assert!(
422 length >= chord_sum,
423 "monotone length {} should be >= chord sum {}",
424 length, chord_sum
425 );
426 }
427
428 #[test]
429 fn line_single_point_length() {
430 let gen = LineGenerator::new();
431 let (_, length) = gen.generate_with_length(&[(42.0, 7.0)]);
432 assert!((length - 0.0).abs() < 1e-10, "single point should have length 0.0, got {}", length);
433 }
434
435 #[test]
436 fn line_empty_length() {
437 let gen = LineGenerator::new();
438 let (_, length) = gen.generate_with_length(&[]);
439 assert!((length - 0.0).abs() < 1e-10, "empty should have length 0.0, got {}", length);
440 }
441}