Skip to main content

orinium_browser/engine/renderer_model/
path.rs

1//! Path model for the renderer: a sequence of move/line/curve commands with
2//! helpers for bounding boxes and polygon conversion for GPU rasterization.
3
4use crate::engine::renderer_model::geom::Rect;
5
6/// A single path drawing command.
7#[derive(Debug, Clone)]
8pub enum PathCommand {
9    /// Move the current point to `(x, y)` without drawing.
10    MoveTo { x: f32, y: f32 },
11    /// Draw a straight line to `(x, y)`.
12    LineTo { x: f32, y: f32 },
13    /// Draw a quadratic Bézier curve to `(x, y)` with control point `(cx, cy)`.
14    QuadTo { cx: f32, cy: f32, x: f32, y: f32 },
15    /// Draw a cubic Bézier curve to `(x, y)` with control points
16    /// `(c1x, c1y)` and `(c2x, c2y)`.
17    CubicTo {
18        c1x: f32,
19        c1y: f32,
20        c2x: f32,
21        c2y: f32,
22        x: f32,
23        y: f32,
24    },
25    /// Close the current subpath back to its starting point.
26    Close,
27}
28
29/// A path made of [`PathCommand`]s.
30///
31/// Used by `DrawCommand::Fill` and `DrawCommand::PushClip`. The GPU rasterizer
32/// converts the path into a polygon with [`Path::as_polygon_vertices`],
33/// flattening curved segments into line segments.
34#[derive(Debug, Clone)]
35pub struct Path {
36    pub commands: Vec<PathCommand>,
37    current: Option<(f32, f32)>,
38    start: Option<(f32, f32)>,
39}
40
41impl Path {
42    /// Creates an empty path.
43    pub fn new() -> Self {
44        Path {
45            commands: Vec::new(),
46            current: None,
47            start: None,
48        }
49    }
50
51    /// Starts a new subpath at `(x, y)`.
52    pub fn move_to(&mut self, x: f32, y: f32) {
53        self.commands.push(PathCommand::MoveTo { x, y });
54        self.current = Some((x, y));
55        self.start = Some((x, y));
56    }
57
58    /// Pushes an implicit [`PathCommand::MoveTo`] when the path has no current
59    /// point yet, so a following line segment starts at `(x, y)`.
60    fn ensure_current(&mut self, x: f32, y: f32) {
61        if self.current.is_none() {
62            self.commands.push(PathCommand::MoveTo { x, y });
63        }
64        self.current = Some((x, y));
65    }
66
67    /// Draws a straight line to `(x, y)`.
68    ///
69    /// If the path has no current point yet, the line is treated as a
70    /// starting point.
71    pub fn line_to(&mut self, x: f32, y: f32) {
72        self.ensure_current(x, y);
73        self.commands.push(PathCommand::LineTo { x, y });
74    }
75
76    /// Draws a quadratic Bézier curve to `p` with control point `c`.
77    ///
78    /// If the path has no current point yet, the curve is dropped and the
79    /// path is moved to `p` instead (a curve has no defined start point).
80    pub fn quad_to(&mut self, c: (f32, f32), p: (f32, f32)) {
81        if self.current.is_none() {
82            self.move_to(p.0, p.1);
83            return;
84        }
85        self.commands.push(PathCommand::QuadTo {
86            cx: c.0,
87            cy: c.1,
88            x: p.0,
89            y: p.1,
90        });
91        self.current = Some((p.0, p.1));
92    }
93
94    /// Draws a cubic Bézier curve to `p` with control points `c1` and `c2`.
95    ///
96    /// If the path has no current point yet, the curve is dropped and the
97    /// path is moved to `p` instead (a curve has no defined start point).
98    pub fn cubic_to(&mut self, c1: (f32, f32), c2: (f32, f32), p: (f32, f32)) {
99        if self.current.is_none() {
100            self.move_to(p.0, p.1);
101            return;
102        }
103        self.commands.push(PathCommand::CubicTo {
104            c1x: c1.0,
105            c1y: c1.1,
106            c2x: c2.0,
107            c2y: c2.1,
108            x: p.0,
109            y: p.1,
110        });
111        self.current = Some((p.0, p.1));
112    }
113
114    /// Closes the current subpath, returning the current point to the
115    /// subpath start.
116    pub fn close(&mut self) {
117        self.commands.push(PathCommand::Close);
118        self.current = self.start;
119    }
120
121    /// Computes the axis-aligned bounding box of the path, or `None` for an
122    /// empty path.
123    ///
124    /// Curve bounds include the control points, so they may overestimate the
125    /// true extent.
126    pub fn bounding_box(&self) -> Option<Rect> {
127        let mut min_x = f32::INFINITY;
128        let mut min_y = f32::INFINITY;
129        let mut max_x = f32::NEG_INFINITY;
130        let mut max_y = f32::NEG_INFINITY;
131        for cmd in &self.commands {
132            match cmd {
133                PathCommand::MoveTo { x, y } | PathCommand::LineTo { x, y } => {
134                    min_x = min_x.min(*x);
135                    min_y = min_y.min(*y);
136                    max_x = max_x.max(*x);
137                    max_y = max_y.max(*y);
138                }
139                PathCommand::QuadTo { cx, cy, x, y } => {
140                    min_x = min_x.min(*x).min(*cx);
141                    min_y = min_y.min(*y).min(*cy);
142                    max_x = max_x.max(*x).max(*cx);
143                    max_y = max_y.max(*y).max(*cy);
144                }
145                PathCommand::CubicTo {
146                    c1x,
147                    c1y,
148                    c2x,
149                    c2y,
150                    x,
151                    y,
152                } => {
153                    min_x = min_x.min(*x).min(*c1x).min(*c2x);
154                    min_y = min_y.min(*y).min(*c1y).min(*c2y);
155                    max_x = max_x.max(*x).max(*c1x).max(*c2x);
156                    max_y = max_y.max(*y).max(*c1y).max(*c2y);
157                }
158                PathCommand::Close => {}
159            }
160        }
161        if min_x.is_finite() && min_y.is_finite() && max_x.is_finite() && max_y.is_finite() {
162            Some(Rect::new(min_x, min_y, max_x - min_x, max_y - min_y))
163        } else {
164            None
165        }
166    }
167    /// Returns the path flattened into a list of vertices per subpath,
168    /// curving segments into line segments.
169    ///
170    /// Each entry is one closed ring (its closing edge back to the first
171    /// point is implicit). Multiple `MoveTo`s yield multiple rings.
172    pub fn subpaths(&self) -> Vec<Vec<(f32, f32)>> {
173        let mut rings: Vec<Vec<(f32, f32)>> = Vec::new();
174        let mut current: Vec<(f32, f32)> = Vec::new();
175        let mut cur_point: Option<(f32, f32)> = None;
176
177        for cmd in &self.commands {
178            match cmd {
179                PathCommand::MoveTo { x, y } => {
180                    if !current.is_empty() {
181                        rings.push(std::mem::take(&mut current));
182                    }
183                    current.push((*x, *y));
184                    cur_point = Some((*x, *y));
185                }
186                PathCommand::LineTo { x, y } => {
187                    current.push((*x, *y));
188                    cur_point = Some((*x, *y));
189                }
190                PathCommand::QuadTo { cx, cy, x, y } => {
191                    if let Some(p0) = cur_point {
192                        flatten_quad(p0, (*cx, *cy), (*x, *y), &mut current);
193                    } else {
194                        current.push((*x, *y));
195                    }
196                    cur_point = Some((*x, *y));
197                }
198                PathCommand::CubicTo {
199                    c1x,
200                    c1y,
201                    c2x,
202                    c2y,
203                    x,
204                    y,
205                } => {
206                    if let Some(p0) = cur_point {
207                        flatten_cubic(p0, (*c1x, *c1y), (*c2x, *c2y), (*x, *y), &mut current);
208                    } else {
209                        current.push((*x, *y));
210                    }
211                    cur_point = Some((*x, *y));
212                }
213                PathCommand::Close => {
214                    // The ring's closing edge is implicit; nothing to emit.
215                }
216            }
217        }
218        if !current.is_empty() {
219            rings.push(current);
220        }
221        rings
222    }
223
224    /// Returns the path as a single flat list of vertices, or `None` if the
225    /// path has fewer than three vertices total.
226    ///
227    /// Prefer [`Path::subpaths`] when subpaths must be triangulated
228    /// independently.
229    pub fn as_polygon_vertices(&self) -> Option<Vec<(f32, f32)>> {
230        let rings = self.subpaths();
231        let total: usize = rings.iter().map(Vec::len).sum();
232        if total < 3 {
233            return None;
234        }
235        Some(rings.into_iter().flatten().collect())
236    }
237
238    pub fn commands(&self) -> &[PathCommand] {
239        &self.commands
240    }
241}
242
243/// Maximum allowed deviation of flattened line segments from the original
244/// Bézier curve, in logical pixels.
245const FLATTEN_TOLERANCE: f32 = 0.25;
246
247/// Recursively subdivide a cubic Bézier with de Casteljau's algorithm until
248/// both control points lie within [`FLATTEN_TOLERANCE`] of the chord, then
249/// append the segment endpoint to `out`.
250fn flatten_cubic(
251    p0: (f32, f32),
252    c1: (f32, f32),
253    c2: (f32, f32),
254    p1: (f32, f32),
255    out: &mut Vec<(f32, f32)>,
256) {
257    // Distance from a control point to the chord (p0..p1).
258    let flatness = |p: (f32, f32)| -> f32 {
259        let (dx, dy) = (p1.0 - p0.0, p1.1 - p0.1);
260        let len_sq = dx * dx + dy * dy;
261        if len_sq <= f32::EPSILON {
262            ((p.0 - p0.0).powi(2) + (p.1 - p0.1).powi(2)).sqrt()
263        } else {
264            let t = (((p.0 - p0.0) * dx + (p.1 - p0.1) * dy) / len_sq).clamp(0.0, 1.0);
265            let (qx, qy) = (p0.0 + t * dx, p0.1 + t * dy);
266            ((p.0 - qx).powi(2) + (p.1 - qy).powi(2)).sqrt()
267        }
268    };
269
270    if flatness(c1) <= FLATTEN_TOLERANCE && flatness(c2) <= FLATTEN_TOLERANCE {
271        out.push(p1);
272        return;
273    }
274
275    // Split at t = 0.5 and recurse on both halves.
276    let mid = |a: (f32, f32), b: (f32, f32)| ((a.0 + b.0) * 0.5, (a.1 + b.1) * 0.5);
277    let m01 = mid(p0, c1);
278    let m12 = mid(c1, c2);
279    let m23 = mid(c2, p1);
280    let m012 = mid(m01, m12);
281    let m123 = mid(m12, m23);
282    let m0123 = mid(m012, m123);
283
284    flatten_cubic(p0, m01, m012, m0123, out);
285    flatten_cubic(m0123, m123, m23, p1, out);
286}
287
288/// Flatten a quadratic Bézier into line segments by converting it to a cubic
289/// and delegating to [`flatten_cubic`].
290fn flatten_quad(p0: (f32, f32), c: (f32, f32), p1: (f32, f32), out: &mut Vec<(f32, f32)>) {
291    let c1 = (
292        p0.0 + (c.0 - p0.0) * 2.0 / 3.0,
293        p0.1 + (c.1 - p0.1) * 2.0 / 3.0,
294    );
295    let c2 = (
296        p1.0 + (c.0 - p1.0) * 2.0 / 3.0,
297        p1.1 + (c.1 - p1.1) * 2.0 / 3.0,
298    );
299    flatten_cubic(p0, c1, c2, p1, out);
300}
301
302impl Default for Path {
303    fn default() -> Self {
304        Self::new()
305    }
306}
307
308// Shape helpers
309/// Builds a closed rectangle path with top-left corner at `(x, y)`.
310pub fn rect_path(x: f32, y: f32, w: f32, h: f32) -> Path {
311    let mut path = Path::new();
312    path.move_to(x, y);
313    path.line_to(x + w, y);
314    path.line_to(x + w, y + h);
315    path.line_to(x, y + h);
316    path.close();
317    path
318}
319
320/// Builds a closed ellipse path centered at `(cx, cy)` with radii `rx`/`ry`.
321pub fn ellipse_path(cx: f32, cy: f32, rx: f32, ry: f32) -> Path {
322    let k = 4.0 * (std::f32::consts::SQRT_2 - 1.0) / 3.0;
323    let mut path = Path::new();
324    path.move_to(cx + rx, cy);
325    path.cubic_to(
326        (cx + rx, cy - k * ry),
327        (cx + k * rx, cy - ry),
328        (cx, cy - ry),
329    );
330    path.cubic_to(
331        (cx - k * rx, cy - ry),
332        (cx - rx, cy - k * ry),
333        (cx - rx, cy),
334    );
335    path.cubic_to(
336        (cx - rx, cy + k * ry),
337        (cx - k * rx, cy + ry),
338        (cx, cy + ry),
339    );
340    path.cubic_to(
341        (cx + k * rx, cy + ry),
342        (cx + rx, cy + k * ry),
343        (cx + rx, cy),
344    );
345    path.close();
346    path
347}
348
349/// Builds a closed polygon path from the given vertex list.
350pub fn polygon_path(points: &[(f32, f32)]) -> Path {
351    if points.is_empty() {
352        return Path::new();
353    }
354    let mut path = Path::new();
355    path.move_to(points[0].0, points[0].1);
356    for p in &points[1..] {
357        path.line_to(p.0, p.1);
358    }
359    path.close();
360    path
361}
362
363/// Translate every coordinate in `path` by `(ox, oy)`.
364pub fn offset_path(path: &Path, ox: f32, oy: f32) -> Path {
365    let mut out = Path::new();
366    for cmd in path.commands() {
367        match *cmd {
368            PathCommand::MoveTo { x, y } => out.move_to(x + ox, y + oy),
369            PathCommand::LineTo { x, y } => out.line_to(x + ox, y + oy),
370            PathCommand::QuadTo { cx, cy, x, y } => {
371                out.quad_to((cx + ox, cy + oy), (x + ox, y + oy))
372            }
373            PathCommand::CubicTo {
374                c1x,
375                c1y,
376                c2x,
377                c2y,
378                x,
379                y,
380            } => out.cubic_to((c1x + ox, c1y + oy), (c2x + ox, c2y + oy), (x + ox, y + oy)),
381            PathCommand::Close => out.close(),
382        }
383    }
384    out
385}
386
387/// Scale a set of corner radii `(rx, ry)` (CSS order TL, TR, BR, BL) down
388/// proportionally so no opposing pair exceeds the box dimensions, following
389/// the CSS `border-radius` clamping rule.
390pub fn clamp_radii(radii: [(f32, f32); 4], w: f32, h: f32) -> [(f32, f32); 4] {
391    let constraints = [
392        if w > 0.0 && radii[0].0 + radii[1].0 > 0.0 {
393            w / (radii[0].0 + radii[1].0)
394        } else {
395            1.0
396        },
397        if w > 0.0 && radii[2].0 + radii[3].0 > 0.0 {
398            w / (radii[2].0 + radii[3].0)
399        } else {
400            1.0
401        },
402        if h > 0.0 && radii[0].1 + radii[3].1 > 0.0 {
403            h / (radii[0].1 + radii[3].1)
404        } else {
405            1.0
406        },
407        if h > 0.0 && radii[1].1 + radii[2].1 > 0.0 {
408            h / (radii[1].1 + radii[2].1)
409        } else {
410            1.0
411        },
412    ];
413    let f = constraints.into_iter().fold(1.0f32, f32::min).max(0.0);
414    if f >= 1.0 {
415        return radii;
416    }
417    radii.map(|(rx, ry)| (rx * f, ry * f))
418}
419
420/// Append a single cubic Bézier approximating a quarter ellipse arc centered at
421/// `(cx, cy)` with radii `(rx, ry)`, from point `from` to point `to`. The sweep
422/// direction is derived from the relative position of the two endpoints.
423pub(crate) fn append_quarter_ellipse(
424    path: &mut Path,
425    cx: f32,
426    cy: f32,
427    rx: f32,
428    ry: f32,
429    from: (f32, f32),
430    to: (f32, f32),
431) {
432    if rx <= 0.0 || ry <= 0.0 {
433        path.line_to(to.0, to.1);
434        return;
435    }
436    let k = 4.0 * (std::f32::consts::SQRT_2 - 1.0) / 3.0;
437    let f = (from.0 - cx, from.1 - cy);
438    let t = (to.0 - cx, to.1 - cy);
439    // In y-down screen coordinates, the sign of the cross product tells us the
440    // sweep direction between the two radial vectors.
441    let sign = if f.0 * t.1 - f.1 * t.0 >= 0.0 {
442        1.0
443    } else {
444        -1.0
445    };
446    let fu = (f.0 / rx, f.1 / ry);
447    let tu = (t.0 / rx, t.1 / ry);
448    let cp1 = (from.0 - sign * k * rx * fu.1, from.1 + sign * k * ry * fu.0);
449    let cp2 = (to.0 + sign * k * rx * tu.1, to.1 - sign * k * ry * tu.0);
450    path.cubic_to(cp1, cp2, to);
451}
452
453/// Builds a closed rounded rectangle path with top-left corner at `(x, y)`.
454///
455/// Corner radii are given as `(rx, ry)` pairs in CSS order (TL, TR, BR, BL) and
456/// are clamped so opposing radii fit within the box.
457#[allow(clippy::too_many_arguments)]
458pub fn rounded_rect_path(
459    x: f32,
460    y: f32,
461    w: f32,
462    h: f32,
463    tl: (f32, f32),
464    tr: (f32, f32),
465    br: (f32, f32),
466    bl: (f32, f32),
467) -> Path {
468    let radii = clamp_radii([tl, tr, br, bl], w, h);
469    let (tl, tr, br, bl) = (radii[0], radii[1], radii[2], radii[3]);
470    let mut path = Path::new();
471    path.move_to(x + w - tr.0, y);
472    append_quarter_ellipse(
473        &mut path,
474        x + w - tr.0,
475        y + tr.1,
476        tr.0,
477        tr.1,
478        (x + w - tr.0, y),
479        (x + w, y + tr.1),
480    );
481    path.line_to(x + w, y + h - br.1);
482    append_quarter_ellipse(
483        &mut path,
484        x + w - br.0,
485        y + h - br.1,
486        br.0,
487        br.1,
488        (x + w, y + h - br.1),
489        (x + w - br.0, y + h),
490    );
491    path.line_to(x + bl.0, y + h);
492    append_quarter_ellipse(
493        &mut path,
494        x + bl.0,
495        y + h - bl.1,
496        bl.0,
497        bl.1,
498        (x + bl.0, y + h),
499        (x, y + h - bl.1),
500    );
501    path.line_to(x, y + tl.1);
502    append_quarter_ellipse(
503        &mut path,
504        x + tl.0,
505        y + tl.1,
506        tl.0,
507        tl.1,
508        (x, y + tl.1),
509        (x + tl.0, y),
510    );
511    path.close();
512    path
513}
514#[cfg(test)]
515mod tests {
516    use super::*;
517
518    fn assert_points_on_ellipse(
519        points: &[(f32, f32)],
520        cx: f32,
521        cy: f32,
522        rx: f32,
523        ry: f32,
524        tol: f32,
525    ) {
526        for (px, py) in points {
527            let v = ((px - cx) / rx).powi(2) + ((py - cy) / ry).powi(2);
528            assert!(
529                (v - 1.0).abs() < tol,
530                "point ({px},{py}) not on ellipse: {v}"
531            );
532        }
533    }
534
535    #[test]
536    fn test_rect_path_vertices_and_bounds() {
537        let path = rect_path(10.0, 20.0, 100.0, 50.0);
538        assert_eq!(
539            path.as_polygon_vertices().unwrap(),
540            vec![(10.0, 20.0), (110.0, 20.0), (110.0, 70.0), (10.0, 70.0)]
541        );
542        let bb = path.bounding_box().unwrap();
543        assert!((bb.x - 10.0).abs() < 1e-6);
544        assert!((bb.y - 20.0).abs() < 1e-6);
545        assert!((bb.width - 100.0).abs() < 1e-6);
546        assert!((bb.height - 50.0).abs() < 1e-6);
547    }
548
549    #[test]
550    fn test_polygon_path() {
551        let points = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0)];
552        let path = polygon_path(&points);
553        assert_eq!(path.as_polygon_vertices().unwrap(), points.to_vec());
554    }
555
556    #[test]
557    fn test_ellipse_path_flattens() {
558        let path = ellipse_path(0.0, 0.0, 50.0, 30.0);
559        let verts = path.as_polygon_vertices().unwrap();
560        assert!(
561            verts.len() > 8,
562            "expected a flattened ellipse, got {} vertices",
563            verts.len()
564        );
565        assert_eq!(verts.first().copied(), Some((50.0, 0.0)));
566        assert_eq!(verts.last().copied(), Some((50.0, 0.0)));
567        assert_points_on_ellipse(&verts, 0.0, 0.0, 50.0, 30.0, 0.01);
568    }
569
570    #[test]
571    fn test_rounded_rect_corners_on_ellipse() {
572        // Regression: `append_quarter_ellipse` mirrored the second control
573        // point, drifting corner arcs off the true ellipse. The circle stayed
574        // exact because `ellipse_path` inlines its own control points.
575        let path = rounded_rect_path(
576            100.0,
577            100.0,
578            200.0,
579            200.0,
580            (50.0, 50.0),
581            (50.0, 50.0),
582            (50.0, 50.0),
583            (50.0, 50.0),
584        );
585        let verts = path.as_polygon_vertices().unwrap();
586        let corners = [
587            ((150.0, 150.0), (-1.0, -1.0)),
588            ((250.0, 150.0), (1.0, -1.0)),
589            ((250.0, 250.0), (1.0, 1.0)),
590            ((150.0, 250.0), (-1.0, 1.0)),
591        ];
592        for (px, py) in verts {
593            for ((cx, cy), (sx, sy)) in corners {
594                if (px - cx) * sx > 0.0 && (py - cy) * sy > 0.0 {
595                    let v = ((px - cx) / 50.0).powi(2) + ((py - cy) / 50.0).powi(2);
596                    assert!(
597                        (v - 1.0).abs() < 0.01,
598                        "corner point ({px},{py}) not on radius-50 arc: {v}"
599                    );
600                }
601            }
602        }
603    }
604
605    #[test]
606    fn test_quad_curve_flattens() {
607        let mut path = Path::new();
608        path.move_to(0.0, 0.0);
609        path.quad_to((10.0, 20.0), (30.0, 0.0));
610        let verts = path.as_polygon_vertices().unwrap();
611        assert_eq!(verts.first().copied(), Some((0.0, 0.0)));
612        assert_eq!(verts.last().copied(), Some((30.0, 0.0)));
613        assert!(verts.len() > 2);
614        for &(_, y) in &verts[1..verts.len() - 1] {
615            assert!(y > 0.0, "quad should bulge upward, got y={y}");
616        }
617    }
618
619    #[test]
620    fn test_cubic_curve_flattens() {
621        let mut path = Path::new();
622        path.move_to(0.0, 0.0);
623        path.cubic_to((10.0, 20.0), (20.0, 20.0), (30.0, 0.0));
624        let verts = path.as_polygon_vertices().unwrap();
625        assert_eq!(verts.first().copied(), Some((0.0, 0.0)));
626        assert_eq!(verts.last().copied(), Some((30.0, 0.0)));
627        assert!(verts.len() > 2);
628    }
629
630    #[test]
631    fn test_empty_and_degenerate_paths() {
632        assert_eq!(Path::new().as_polygon_vertices(), None);
633        let mut path = Path::new();
634        path.move_to(0.0, 0.0);
635        path.line_to(10.0, 0.0);
636        assert_eq!(path.as_polygon_vertices(), None);
637    }
638
639    #[test]
640    fn test_curve_without_current_point_moves() {
641        let mut path = Path::new();
642        path.quad_to((10.0, 10.0), (20.0, 20.0));
643        assert_eq!(path.as_polygon_vertices(), None);
644        assert_eq!(path.commands().len(), 1);
645    }
646
647    #[test]
648    fn test_curve_bounding_box_includes_controls() {
649        let mut path = Path::new();
650        path.move_to(0.0, 0.0);
651        path.quad_to((100.0, 0.0), (50.0, 50.0));
652        let bb = path.bounding_box().unwrap();
653        assert!((bb.x - 0.0).abs() < 1e-6);
654        assert!((bb.width - 100.0).abs() < 1e-6);
655        assert!((bb.y - 0.0).abs() < 1e-6);
656        assert!((bb.height - 50.0).abs() < 1e-6);
657    }
658
659    #[test]
660    fn test_subpaths_split_on_move_to() {
661        let mut path = Path::new();
662        path.move_to(0.0, 0.0);
663        path.line_to(10.0, 0.0);
664        path.line_to(10.0, 10.0);
665        path.close();
666        path.move_to(20.0, 20.0);
667        path.line_to(30.0, 20.0);
668        path.line_to(30.0, 30.0);
669        path.close();
670
671        let rings = path.subpaths();
672        assert_eq!(rings.len(), 2);
673        assert_eq!(rings[0].len(), 3);
674        assert_eq!(rings[1].len(), 3);
675    }
676}