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}