Skip to main content

rust_motion_paths/
length.rs

1//! SVG path length calculation.
2
3use crate::parser::parse_path;
4use crate::types::Instruction;
5
6/// Calculates total length of an SVG path string in pixels.
7pub fn get_length(path: &str) -> f64 {
8    let instructions = match parse_path(path) {
9        Ok(insts) => insts,
10        Err(_) => return 0.0,
11    };
12
13    get_instructions_length(&instructions)
14}
15
16/// Calculates total length of a list of [`Instruction`]s.
17pub fn get_instructions_length(instructions: &[Instruction]) -> f64 {
18    let mut total_length = 0.0;
19    let mut current_x = 0.0;
20    let mut current_y = 0.0;
21    let mut start_x = 0.0;
22    let mut start_y = 0.0;
23
24    for inst in instructions {
25        match inst {
26            Instruction::MoveTo { x, y } => {
27                current_x = *x;
28                current_y = *y;
29                start_x = *x;
30                start_y = *y;
31            }
32            Instruction::LineTo { x, y } => {
33                let dx = x - current_x;
34                let dy = y - current_y;
35                total_length += (dx * dx + dy * dy).sqrt();
36                current_x = *x;
37                current_y = *y;
38            }
39            Instruction::ClosePath => {
40                let dx = start_x - current_x;
41                let dy = start_y - current_y;
42                total_length += (dx * dx + dy * dy).sqrt();
43                current_x = start_x;
44                current_y = start_y;
45            }
46            Instruction::CubicCurveTo {
47                x1,
48                y1,
49                x2,
50                y2,
51                x,
52                y,
53            } => {
54                total_length +=
55                    cubic_bezier_length((current_x, current_y), (*x1, *y1), (*x2, *y2), (*x, *y));
56                current_x = *x;
57                current_y = *y;
58            }
59            Instruction::QuadCurveTo { x1, y1, x, y } => {
60                total_length += quad_bezier_length(current_x, current_y, *x1, *y1, *x, *y);
61                current_x = *x;
62                current_y = *y;
63            }
64            Instruction::ArcTo {
65                rx,
66                ry,
67                x_axis_rotation,
68                large_arc_flag,
69                sweep_flag,
70                x,
71                y,
72            } => {
73                total_length += arc_segment_length(
74                    (current_x, current_y),
75                    (*x, *y),
76                    *rx,
77                    *ry,
78                    *x_axis_rotation,
79                    *large_arc_flag,
80                    *sweep_flag,
81                );
82                current_x = *x;
83                current_y = *y;
84            }
85        }
86    }
87
88    total_length
89}
90
91fn cubic_bezier_length(
92    (x0, y0): (f64, f64),
93    (x1, y1): (f64, f64),
94    (x2, y2): (f64, f64),
95    (x3, y3): (f64, f64),
96) -> f64 {
97    let steps = 16;
98    let mut length = 0.0;
99    let mut prev_x = x0;
100    let mut prev_y = y0;
101
102    for i in 1..=steps {
103        let t = i as f64 / steps as f64;
104        let mt = 1.0 - t;
105
106        let px =
107            mt * mt * mt * x0 + 3.0 * mt * mt * t * x1 + 3.0 * mt * t * t * x2 + t * t * t * x3;
108
109        let py =
110            mt * mt * mt * y0 + 3.0 * mt * mt * t * y1 + 3.0 * mt * t * t * y2 + t * t * t * y3;
111
112        let dx = px - prev_x;
113        let dy = py - prev_y;
114        length += (dx * dx + dy * dy).sqrt();
115
116        prev_x = px;
117        prev_y = py;
118    }
119
120    length
121}
122
123fn quad_bezier_length(x0: f64, y0: f64, x1: f64, y1: f64, x2: f64, y2: f64) -> f64 {
124    let steps = 16;
125    let mut length = 0.0;
126    let mut prev_x = x0;
127    let mut prev_y = y0;
128
129    for i in 1..=steps {
130        let t = i as f64 / steps as f64;
131        let mt = 1.0 - t;
132
133        let px = mt * mt * x0 + 2.0 * mt * t * x1 + t * t * x2;
134        let py = mt * mt * y0 + 2.0 * mt * t * y1 + t * t * y2;
135
136        let dx = px - prev_x;
137        let dy = py - prev_y;
138        length += (dx * dx + dy * dy).sqrt();
139
140        prev_x = px;
141        prev_y = py;
142    }
143
144    length
145}
146
147/// Computes the length of an elliptical arc segment according to the SVG specification.
148pub fn arc_segment_length(
149    start: (f64, f64),
150    end: (f64, f64),
151    mut rx: f64,
152    mut ry: f64,
153    x_axis_rotation: f64,
154    large_arc_flag: bool,
155    sweep_flag: bool,
156) -> f64 {
157    let (x1, y1) = start;
158    let (x2, y2) = end;
159
160    if (x1 - x2).abs() < 1e-9 && (y1 - y2).abs() < 1e-9 {
161        return 0.0;
162    }
163
164    rx = rx.abs();
165    ry = ry.abs();
166    if rx < 1e-9 || ry < 1e-9 {
167        let dx = x2 - x1;
168        let dy = y2 - y1;
169        return (dx * dx + dy * dy).sqrt();
170    }
171
172    let phi = x_axis_rotation.to_radians();
173    let cos_phi = phi.cos();
174    let sin_phi = phi.sin();
175
176    // Step 1: Compute (x1', y1')
177    let dx = (x1 - x2) / 2.0;
178    let dy = (y1 - y2) / 2.0;
179    let x1_prime = cos_phi * dx + sin_phi * dy;
180    let y1_prime = -sin_phi * dx + cos_phi * dy;
181
182    // Step 2: Correct out-of-range radii (SVG spec F.6.2)
183    let lambda = (x1_prime * x1_prime) / (rx * rx) + (y1_prime * y1_prime) / (ry * ry);
184    if lambda > 1.0 {
185        let sqrt_lambda = lambda.sqrt();
186        rx *= sqrt_lambda;
187        ry *= sqrt_lambda;
188    }
189
190    // Step 3: Compute (cx', cy') (SVG spec F.6.5)
191    let rx_sq = rx * rx;
192    let ry_sq = ry * ry;
193    let x1_p_sq = x1_prime * x1_prime;
194    let y1_p_sq = y1_prime * y1_prime;
195
196    let num = (rx_sq * ry_sq - rx_sq * y1_p_sq - ry_sq * x1_p_sq).max(0.0);
197    let den = rx_sq * y1_p_sq + ry_sq * x1_p_sq;
198    let sq = if den < 1e-9 { 0.0 } else { num / den };
199    let mut s = sq.sqrt();
200    if large_arc_flag == sweep_flag {
201        s = -s;
202    }
203    let cx_prime = s * (rx * y1_prime / ry);
204    let cy_prime = s * (-ry * x1_prime / rx);
205
206    // Step 4: Compute (cx, cy)
207    let cx = cos_phi * cx_prime - sin_phi * cy_prime + (x1 + x2) / 2.0;
208    let cy = sin_phi * cx_prime + cos_phi * cy_prime + (y1 + y2) / 2.0;
209
210    // Step 5: Compute theta1 and delta_theta
211    let v1 = ((x1_prime - cx_prime) / rx, (y1_prime - cy_prime) / ry);
212    let v2 = ((-x1_prime - cx_prime) / rx, (-y1_prime - cy_prime) / ry);
213
214    let theta1 = vector_angle((1.0, 0.0), v1);
215    let mut delta_theta = vector_angle(v1, v2);
216
217    let two_pi = std::f64::consts::TAU;
218    if !sweep_flag && delta_theta > 0.0 {
219        delta_theta -= two_pi;
220    } else if sweep_flag && delta_theta < 0.0 {
221        delta_theta += two_pi;
222    }
223
224    // Step 6: Sample points along arc to compute length
225    let num_steps =
226        ((delta_theta.abs() / (std::f64::consts::PI / 64.0)).ceil() as usize).clamp(32, 256);
227    let mut length = 0.0;
228    let mut prev_x = x1;
229    let mut prev_y = y1;
230
231    for i in 1..=num_steps {
232        let t = i as f64 / num_steps as f64;
233        let theta = theta1 + t * delta_theta;
234        let cos_theta = theta.cos();
235        let sin_theta = theta.sin();
236
237        let px = cx + rx * cos_theta * cos_phi - ry * sin_theta * sin_phi;
238        let py = cy + rx * cos_theta * sin_phi + ry * sin_theta * cos_phi;
239
240        let seg_dx = px - prev_x;
241        let seg_dy = py - prev_y;
242        length += (seg_dx * seg_dx + seg_dy * seg_dy).sqrt();
243
244        prev_x = px;
245        prev_y = py;
246    }
247
248    length
249}
250
251pub(crate) fn vector_angle((ux, uy): (f64, f64), (vx, vy): (f64, f64)) -> f64 {
252    let dot = ux * vx + uy * vy;
253    let len_u = (ux * ux + uy * uy).sqrt();
254    let len_v = (vx * vx + vy * vy).sqrt();
255    if len_u < 1e-9 || len_v < 1e-9 {
256        return 0.0;
257    }
258    let cos_val = (dot / (len_u * len_v)).clamp(-1.0, 1.0);
259    let angle = cos_val.acos();
260    if ux * vy - uy * vx < 0.0 {
261        -angle
262    } else {
263        angle
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270    use std::f64::consts::PI;
271
272    #[test]
273    fn test_rect_path_length() {
274        let d = "M 0 0 L 100 0 L 100 100 L 0 100 Z";
275        let len = get_length(d);
276        assert_eq!(len, 400.0);
277    }
278
279    #[test]
280    fn test_quarter_circle_arc_length() {
281        // Quarter circle with radius 100 from (100, 0) to (0, 100)
282        let len = arc_segment_length((100.0, 0.0), (0.0, 100.0), 100.0, 100.0, 0.0, false, true);
283        let expected = 0.5 * PI * 100.0;
284        assert!(
285            (len - expected).abs() < 0.1,
286            "expected ~{expected}, got {len}"
287        );
288    }
289
290    #[test]
291    fn test_semi_circle_arc_length() {
292        // Semi-circle with radius 50
293        let len = arc_segment_length((50.0, 0.0), (-50.0, 0.0), 50.0, 50.0, 0.0, false, true);
294        let expected = PI * 50.0;
295        assert!(
296            (len - expected).abs() < 0.1,
297            "expected ~{expected}, got {len}"
298        );
299    }
300
301    #[test]
302    fn test_cubic_bezier_length() {
303        let d = "M 0 0 C 0 50 100 50 100 0";
304        let len = get_length(d);
305        assert!(
306            len > 100.0,
307            "Cubic bezier should be longer than chord (100.0), got {len}"
308        );
309    }
310
311    #[test]
312    fn test_quad_bezier_length() {
313        let d = "M 0 0 Q 50 100 100 0";
314        let len = get_length(d);
315        assert!(
316            len > 100.0,
317            "Quad bezier should be longer than chord (100.0), got {len}"
318        );
319    }
320
321    #[test]
322    fn test_full_circle_arc_length() {
323        // Full circle in two arcs
324        let d = "M 100 0 A 100 100 0 1 0 -100 0 A 100 100 0 1 0 100 0 Z";
325        let len = get_length(d);
326        let expected = 2.0 * PI * 100.0;
327        assert!(
328            (len - expected).abs() < 1.0,
329            "expected ~{expected}, got {len}"
330        );
331    }
332
333    #[test]
334    fn test_invalid_path_length_returns_zero() {
335        let len = get_length("invalid path string !!!");
336        assert_eq!(len, 0.0);
337    }
338
339    #[test]
340    fn test_instructions_length_empty() {
341        let len = get_instructions_length(&[]);
342        assert_eq!(len, 0.0);
343    }
344}