Skip to main content

motion_core/path2d/
construction.rs

1//! Path constructors for rectangles, polylines and SVG path-data import.
2
3use super::{MAX_PATH_VERBS, MAX_SVG_PATH_BYTES, Path2D, PathVerb, invalid};
4use crate::{Point, Rect, ValidationError, Vector2};
5
6impl Path2D {
7    /// Builds a closed rectangle, rejecting invalid bounds before any mutation.
8    pub fn from_rect(bounds: Rect) -> Result<Self, ValidationError> {
9        crate::drawing::validate_rect(bounds, "path2d", "bounds")?;
10        let mut path = Self::new();
11        path.try_move_to(Point::new(bounds.left, bounds.top))?;
12        path.try_line_to(Point::new(bounds.right, bounds.top))?;
13        path.try_line_to(Point::new(bounds.right, bounds.bottom))?;
14        path.try_line_to(Point::new(bounds.left, bounds.bottom))?;
15        path.try_close()?;
16        Ok(path)
17    }
18
19    /// Builds a bounded polyline, rejecting nonfinite points and verb overflow.
20    pub fn polyline(points: impl IntoIterator<Item = Point>) -> Result<Self, ValidationError> {
21        let mut path = Self::new();
22        for (index, point) in points.into_iter().take(MAX_PATH_VERBS + 1).enumerate() {
23            if index == 0 {
24                path.try_move_to(point)?;
25            } else {
26                path.try_line_to(point)?;
27            }
28        }
29        Ok(path)
30    }
31
32    /// Imports SVG path data, not an XML document, without rasterizing geometry.
33    ///
34    /// Supports absolute/relative M/L/H/V/C/S/Q/T/A/Z, including repeated argument
35    /// groups and reflected smooth controls. Empty data produces an empty path.
36    /// A/a arcs become bounded cubic segments; zero radii become lines and
37    /// coincident endpoints emit nothing. Syntax, nonfinite
38    /// arithmetic and overflowing control extents return InvalidValue; input
39    /// beyond MAX_SVG_PATH_BYTES or MAX_PATH_VERBS returns StructuralLimit.
40    /// Parsed commands and expanded verbs have independent MAX_PATH_VERBS caps. Errors
41    /// discard the entire result; there is no partial-path recovery, I/O or cache.
42    ///
43    /// ```
44    /// use motion_core::prelude::*;
45    /// use motion_core::{
46    ///     DisplayList, RasterLimits, ResourceStore, ValidatedDisplayList, MAX_PATH_VERBS,
47    ///     MAX_STROKE_VERBS, MAX_SVG_PATH_BYTES, sample_offsets,
48    /// };
49    /// let path = Path2D::parse_svg("M1 1h6v6H1z")?;
50    /// assert_eq!(path, Path2D::from_rect(Rect::new(1.0, 1.0, 7.0, 7.0))?);
51    /// let frame = Node::from(path.fill(Color::WHITE)).compile(
52    ///     Size::new(8.0, 8.0),
53    ///     PixelSize::new(8, 8),
54    ///     ValidationLimits::default(),
55    /// )?;
56    /// # Ok::<(), motion_core::ValidationError>(())
57    /// ```
58    pub fn parse_svg(path_data: &str) -> Result<Self, ValidationError> {
59        if path_data.len() > MAX_SVG_PATH_BYTES {
60            return Err(ValidationError::limit(
61                "path2d.svg",
62                "svg_bytes",
63                "SVG path data exceeds 1048576 bytes",
64            ));
65        }
66        let mut result = Self::new();
67        let mut previous_cubic = false;
68        let mut previous_quadratic = false;
69        for (index, segment) in svgtypes::PathParser::from(path_data).enumerate() {
70            let path = format!("path2d.svg.commands[{index}]");
71            if index >= MAX_PATH_VERBS {
72                return Err(ValidationError::limit(
73                    path,
74                    "commands",
75                    "SVG path data exceeds 16384 commands",
76                ));
77            }
78            let segment = segment
79                .map_err(|error| ValidationError::invalid(&path, "data", error.to_string()))?;
80            let current = result.data.current.unwrap_or(Point::ORIGIN);
81            let absolute = segment.is_abs();
82            let point = |x, y| {
83                if absolute {
84                    Point::new(x, y)
85                } else {
86                    Point::new(current.x + x, current.y + y)
87                }
88            };
89            let reflect = |control: Point| {
90                Point::new(
91                    current.x + (current.x - control.x),
92                    current.y + (current.y - control.y),
93                )
94            };
95            let verb =
96                match segment {
97                    svgtypes::PathSegment::MoveTo { x, y, .. } => PathVerb::MoveTo(point(x, y)),
98                    svgtypes::PathSegment::LineTo { x, y, .. } => PathVerb::LineTo(point(x, y)),
99                    svgtypes::PathSegment::HorizontalLineTo { x, .. } => PathVerb::LineTo(
100                        Point::new(if absolute { x } else { current.x + x }, current.y),
101                    ),
102                    svgtypes::PathSegment::VerticalLineTo { y, .. } => PathVerb::LineTo(
103                        Point::new(current.x, if absolute { y } else { current.y + y }),
104                    ),
105                    svgtypes::PathSegment::CurveTo {
106                        x1,
107                        y1,
108                        x2,
109                        y2,
110                        x,
111                        y,
112                        ..
113                    } => PathVerb::CubicTo(point(x1, y1), point(x2, y2), point(x, y)),
114                    svgtypes::PathSegment::SmoothCurveTo { x2, y2, x, y, .. } => {
115                        let control = match result.verbs().last() {
116                            Some(PathVerb::CubicTo(_, previous, _)) if previous_cubic => {
117                                reflect(*previous)
118                            }
119                            _ => current,
120                        };
121                        PathVerb::CubicTo(control, point(x2, y2), point(x, y))
122                    }
123                    svgtypes::PathSegment::Quadratic { x1, y1, x, y, .. } => {
124                        PathVerb::QuadraticTo(point(x1, y1), point(x, y))
125                    }
126                    svgtypes::PathSegment::SmoothQuadratic { x, y, .. } => {
127                        let control = match result.verbs().last() {
128                            Some(PathVerb::QuadraticTo(previous, _)) if previous_quadratic => {
129                                reflect(*previous)
130                            }
131                            _ => current,
132                        };
133                        PathVerb::QuadraticTo(control, point(x, y))
134                    }
135                    svgtypes::PathSegment::ClosePath { .. } => PathVerb::Close,
136                    svgtypes::PathSegment::EllipticalArc {
137                        rx,
138                        ry,
139                        x_axis_rotation,
140                        large_arc,
141                        sweep,
142                        x,
143                        y,
144                        ..
145                    } => {
146                        let converted = (|| -> Result<(), ValidationError> {
147                            if result.data.current.is_none() {
148                                return Err(invalid("data", "SVG arcs require a current point"));
149                            }
150                            if ![rx, ry, x_axis_rotation, x, y]
151                                .iter()
152                                .all(|value| value.is_finite())
153                            {
154                                return Err(invalid("data", "SVG arc parameters must be finite"));
155                            }
156                            let end = point(x, y);
157                            let arc = kurbo::SvgArc {
158                                from: kurbo::Point::new(current.x, current.y),
159                                to: kurbo::Point::new(end.x, end.y),
160                                radii: kurbo::Vec2::new(rx.abs(), ry.abs()),
161                                x_rotation: x_axis_rotation.to_radians(),
162                                large_arc,
163                                sweep,
164                            };
165                            if current != end {
166                                if let Some(arc) = kurbo::Arc::from_svg_arc(&arc) {
167                                    let mut converted = Self::new();
168                                    converted.elliptical_arc(
169                                        Point::new(arc.center.x, arc.center.y),
170                                        Vector2::new(arc.radii.x, arc.radii.y),
171                                        arc.start_angle,
172                                        arc.sweep_angle,
173                                        arc.x_rotation,
174                                    )?;
175                                    for (index, verb) in
176                                        converted.verbs().iter().copied().enumerate().skip(1)
177                                    {
178                                        let verb = if index + 1 == converted.verbs().len() {
179                                            match verb {
180                                                PathVerb::CubicTo(first, second, _) => {
181                                                    PathVerb::CubicTo(first, second, end)
182                                                }
183                                                _ => verb,
184                                            }
185                                        } else {
186                                            verb
187                                        };
188                                        result.push(verb)?;
189                                    }
190                                } else {
191                                    result.try_line_to(end)?;
192                                }
193                            }
194                            Ok(())
195                        })();
196                        converted.map_err(|mut error| {
197                            error.path = path;
198                            error
199                        })?;
200                        previous_cubic = false;
201                        previous_quadratic = false;
202                        continue;
203                    }
204                };
205            previous_cubic = matches!(verb, PathVerb::CubicTo(..));
206            previous_quadratic = matches!(verb, PathVerb::QuadraticTo(..));
207            result.push(verb).map_err(|mut error| {
208                error.path = path;
209                error
210            })?;
211        }
212        Ok(result)
213    }
214}