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}