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