Skip to main content

document_svg/svg/
geometry.rs

1//! 2D point, affine transformation matrix, and parametric curve samplers for SVG geometry.
2
3/// 2D point representation for vector geometry extraction and transformation.
4#[derive(Clone, Copy, Debug, Default, PartialEq)]
5pub struct Point2D {
6    pub x: f64,
7    pub y: f64,
8}
9
10impl Point2D {
11    #[inline]
12    pub const fn new(x: f64, y: f64) -> Self {
13        Self { x, y }
14    }
15
16    #[inline]
17    pub fn distance_to(&self, other: &Point2D) -> f64 {
18        ((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
19    }
20}
21
22/// 2D affine transformation matrix:
23/// ```text
24/// [ x' ]   [ a  c  e ] [ x ]
25/// [ y' ] = [ b  d  f ] [ y ]
26/// [ 1  ]   [ 0  0  1 ] [ 1 ]
27/// ```
28#[derive(Clone, Copy, Debug, PartialEq)]
29pub struct Transform2D {
30    pub a: f64,
31    pub b: f64,
32    pub c: f64,
33    pub d: f64,
34    pub e: f64,
35    pub f: f64,
36}
37
38impl Default for Transform2D {
39    fn default() -> Self {
40        Self::identity()
41    }
42}
43
44impl Transform2D {
45    pub const fn identity() -> Self {
46        Self {
47            a: 1.0,
48            b: 0.0,
49            c: 0.0,
50            d: 1.0,
51            e: 0.0,
52            f: 0.0,
53        }
54    }
55
56    #[inline]
57    pub fn is_identity(&self) -> bool {
58        (self.a - 1.0).abs() < 1e-9
59            && self.b.abs() < 1e-9
60            && self.c.abs() < 1e-9
61            && (self.d - 1.0).abs() < 1e-9
62            && self.e.abs() < 1e-9
63            && self.f.abs() < 1e-9
64    }
65
66    #[inline]
67    pub fn apply(&self, p: Point2D) -> Point2D {
68        Point2D::new(
69            self.a * p.x + self.c * p.y + self.e,
70            self.b * p.x + self.d * p.y + self.f,
71        )
72    }
73
74    pub fn multiply(&self, rhs: &Self) -> Self {
75        Self {
76            a: self.a * rhs.a + self.c * rhs.b,
77            b: self.b * rhs.a + self.d * rhs.b,
78            c: self.a * rhs.c + self.c * rhs.d,
79            d: self.b * rhs.c + self.d * rhs.d,
80            e: self.a * rhs.e + self.c * rhs.f + self.e,
81            f: self.b * rhs.e + self.d * rhs.f + self.f,
82        }
83    }
84
85    pub fn translate(tx: f64, ty: f64) -> Self {
86        Self {
87            a: 1.0,
88            b: 0.0,
89            c: 0.0,
90            d: 1.0,
91            e: tx,
92            f: ty,
93        }
94    }
95
96    pub fn scale(sx: f64, sy: f64) -> Self {
97        Self {
98            a: sx,
99            b: 0.0,
100            c: 0.0,
101            d: sy,
102            e: 0.0,
103            f: 0.0,
104        }
105    }
106
107    pub fn rotate(angle_rad: f64) -> Self {
108        let cos = angle_rad.cos();
109        let sin = angle_rad.sin();
110        Self {
111            a: cos,
112            b: sin,
113            c: -sin,
114            d: cos,
115            e: 0.0,
116            f: 0.0,
117        }
118    }
119}
120
121/// Parses an SVG `transform` attribute value into a `Transform2D` with zero heap allocation.
122pub fn parse_transform(s: &str) -> Transform2D {
123    let mut tf = Transform2D::identity();
124    let s = s.trim();
125    if s.is_empty() {
126        return tf;
127    }
128
129    let mut remaining = s;
130    while let Some(open_paren) = remaining.find('(') {
131        let op_name = remaining[..open_paren].trim();
132        let close_paren = match remaining[open_paren + 1..].find(')') {
133            Some(idx) => open_paren + 1 + idx,
134            None => break,
135        };
136        let args_str = &remaining[open_paren + 1..close_paren];
137
138        // Stack-allocated parsing buffer to avoid heap allocations
139        let mut args = [0.0f64; 6];
140        let mut argc = 0;
141        for part in args_str.split([' ', ',']) {
142            let part = part.trim();
143            if !part.is_empty()
144                && let Ok(v) = part.parse::<f64>()
145                && argc < 6
146            {
147                args[argc] = v;
148                argc += 1;
149            }
150        }
151
152        let op = op_name.split_whitespace().last().unwrap_or(op_name);
153
154        let cur = match op {
155            "matrix" if argc >= 6 => Transform2D {
156                a: args[0],
157                b: args[1],
158                c: args[2],
159                d: args[3],
160                e: args[4],
161                f: args[5],
162            },
163            "translate" if argc > 0 => {
164                let tx = args[0];
165                let ty = if argc > 1 { args[1] } else { 0.0 };
166                Transform2D::translate(tx, ty)
167            }
168            "scale" if argc > 0 => {
169                let sx = args[0];
170                let sy = if argc > 1 { args[1] } else { sx };
171                Transform2D::scale(sx, sy)
172            }
173            "rotate" if argc > 0 => {
174                let rad = args[0].to_radians();
175                if argc >= 3 {
176                    let cx = args[1];
177                    let cy = args[2];
178                    Transform2D::translate(cx, cy)
179                        .multiply(&Transform2D::rotate(rad))
180                        .multiply(&Transform2D::translate(-cx, -cy))
181                } else {
182                    Transform2D::rotate(rad)
183                }
184            }
185            "skewx" | "skewX" if argc > 0 => {
186                let tan = args[0].to_radians().tan();
187                Transform2D {
188                    a: 1.0,
189                    b: 0.0,
190                    c: tan,
191                    d: 1.0,
192                    e: 0.0,
193                    f: 0.0,
194                }
195            }
196            "skewy" | "skewY" if argc > 0 => {
197                let tan = args[0].to_radians().tan();
198                Transform2D {
199                    a: 1.0,
200                    b: tan,
201                    c: 0.0,
202                    d: 1.0,
203                    e: 0.0,
204                    f: 0.0,
205                }
206            }
207            _ => Transform2D::identity(),
208        };
209
210        tf = tf.multiply(&cur);
211        remaining = &remaining[close_paren + 1..];
212    }
213
214    tf
215}
216
217/// Samples a cubic bezier curve into line segments with `steps`.
218#[inline]
219pub fn sample_cubic_bezier<F>(
220    p0: Point2D,
221    p1: Point2D,
222    p2: Point2D,
223    p3: Point2D,
224    steps: usize,
225    mut on_point: F,
226) where
227    F: FnMut(Point2D),
228{
229    let n = steps.max(1);
230    for step in 1..=n {
231        let t = (step as f64) / (n as f64);
232        let u = 1.0 - t;
233        let x =
234            u * u * u * p0.x + 3.0 * u * u * t * p1.x + 3.0 * u * t * t * p2.x + t * t * t * p3.x;
235        let y =
236            u * u * u * p0.y + 3.0 * u * u * t * p1.y + 3.0 * u * t * t * p2.y + t * t * t * p3.y;
237        on_point(Point2D::new(x, y));
238    }
239}
240
241/// Samples a quadratic bezier curve into line segments with `steps`.
242#[inline]
243pub fn sample_quad_bezier<F>(p0: Point2D, p1: Point2D, p2: Point2D, steps: usize, mut on_point: F)
244where
245    F: FnMut(Point2D),
246{
247    let n = steps.max(1);
248    for step in 1..=n {
249        let t = (step as f64) / (n as f64);
250        let u = 1.0 - t;
251        let x = u * u * p0.x + 2.0 * u * t * p1.x + t * t * p2.x;
252        let y = u * u * p0.y + 2.0 * u * t * p1.y + t * t * p2.y;
253        on_point(Point2D::new(x, y));
254    }
255}
256
257/// Samples an elliptical arc curve into line segments per W3C SVG 1.1 Appendix F.6.
258#[allow(clippy::too_many_arguments)]
259pub fn sample_elliptical_arc<F>(
260    p0: Point2D,
261    p1: Point2D,
262    mut rx: f64,
263    mut ry: f64,
264    x_axis_rotation_deg: f64,
265    large_arc_flag: bool,
266    sweep_flag: bool,
267    steps: usize,
268    mut on_point: F,
269) where
270    F: FnMut(Point2D),
271{
272    if (p0.x - p1.x).abs() < 1e-9 && (p0.y - p1.y).abs() < 1e-9 {
273        return;
274    }
275    if rx.abs() < 1e-9 || ry.abs() < 1e-9 {
276        on_point(p1);
277        return;
278    }
279
280    rx = rx.abs();
281    ry = ry.abs();
282    let phi = x_axis_rotation_deg.to_radians();
283    let cos_phi = phi.cos();
284    let sin_phi = phi.sin();
285
286    // Step 1: Compute (x1', y1')
287    let dx = (p0.x - p1.x) / 2.0;
288    let dy = (p0.y - p1.y) / 2.0;
289    let x1_prime = cos_phi * dx + sin_phi * dy;
290    let y1_prime = -sin_phi * dx + cos_phi * dy;
291
292    // Check radii scaling
293    let lambda = (x1_prime * x1_prime) / (rx * rx) + (y1_prime * y1_prime) / (ry * ry);
294    if lambda > 1.0 {
295        let scale = lambda.sqrt();
296        rx *= scale;
297        ry *= scale;
298    }
299
300    // Step 2: Compute (cx', cy')
301    let sign = if large_arc_flag != sweep_flag {
302        1.0
303    } else {
304        -1.0
305    };
306    let rx_sq = rx * rx;
307    let ry_sq = ry * ry;
308    let x1_sq = x1_prime * x1_prime;
309    let y1_sq = y1_prime * y1_prime;
310
311    let numerator = (rx_sq * ry_sq - rx_sq * y1_sq - ry_sq * x1_sq).max(0.0);
312    let denominator = rx_sq * y1_sq + ry_sq * x1_sq;
313    let sq = if denominator > 0.0 {
314        (numerator / denominator).sqrt()
315    } else {
316        0.0
317    };
318    let cx_prime = sign * sq * (rx * y1_prime / ry);
319    let cy_prime = sign * sq * -(ry * x1_prime / rx);
320
321    // Step 3: Compute (cx, cy) from (cx', cy')
322    let cx = cos_phi * cx_prime - sin_phi * cy_prime + (p0.x + p1.x) / 2.0;
323    let cy = sin_phi * cx_prime + cos_phi * cy_prime + (p0.y + p1.y) / 2.0;
324
325    // Step 4: Compute theta1 and delta_theta
326    let ux = (x1_prime - cx_prime) / rx;
327    let uy = (y1_prime - cy_prime) / ry;
328    let vx = (-x1_prime - cx_prime) / rx;
329    let vy = (-y1_prime - cy_prime) / ry;
330
331    let vector_angle = |u_x: f64, u_y: f64, v_x: f64, v_y: f64| -> f64 {
332        let dot = u_x * v_x + u_y * v_y;
333        let len = (u_x * u_x + u_y * u_y).sqrt() * (v_x * v_x + v_y * v_y).sqrt();
334        let val = if len > 0.0 {
335            (dot / len).clamp(-1.0, 1.0)
336        } else {
337            0.0
338        };
339        let mut ang = val.acos();
340        if u_x * v_y - u_y * v_x < 0.0 {
341            ang = -ang;
342        }
343        ang
344    };
345
346    let theta1 = vector_angle(1.0, 0.0, ux, uy);
347    let mut d_theta = vector_angle(ux, uy, vx, vy);
348
349    let two_pi = std::f64::consts::PI * 2.0;
350    if !sweep_flag && d_theta > 0.0 {
351        d_theta -= two_pi;
352    } else if sweep_flag && d_theta < 0.0 {
353        d_theta += two_pi;
354    }
355
356    let n = steps.max(4);
357    for i in 1..=n {
358        let t = i as f64 / n as f64;
359        let angle = theta1 + t * d_theta;
360        let cos_a = angle.cos();
361        let sin_a = angle.sin();
362        let ex = rx * cos_a;
363        let ey = ry * sin_a;
364        let x = cos_phi * ex - sin_phi * ey + cx;
365        let y = sin_phi * ex + cos_phi * ey + cy;
366        on_point(Point2D::new(x, y));
367    }
368}