1use crate::{Point, Rect};
4
5#[derive(Debug, Clone, PartialEq)]
7pub enum PathCommand {
8 MoveTo(Point),
9 LineTo(Point),
10 CurveTo { c1: Point, c2: Point, to: Point },
11 Close,
12}
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum FillRule {
17 NonZero,
18 EvenOdd,
19}
20
21#[derive(Debug, Clone, PartialEq)]
23pub struct Path {
24 pub commands: Vec<PathCommand>,
25 pub fill_rule: FillRule,
26}
27
28impl Path {
29 pub fn bounds(&self) -> Option<Rect> {
31 let mut points = self.commands.iter().flat_map(|command| match command {
32 PathCommand::MoveTo(point) | PathCommand::LineTo(point) => [Some(*point), None, None],
33 PathCommand::CurveTo { c1, c2, to } => [Some(*c1), Some(*c2), Some(*to)],
34 PathCommand::Close => [None, None, None],
35 });
36 let first = points.find_map(|point| point)?;
37 let (mut min_x, mut min_y, mut max_x, mut max_y) = (first.x, first.y, first.x, first.y);
38
39 for point in points.flatten() {
40 min_x = min_x.min(point.x);
41 min_y = min_y.min(point.y);
42 max_x = max_x.max(point.x);
43 max_y = max_y.max(point.y);
44 }
45
46 Some(Rect {
47 x: min_x,
48 y: min_y,
49 width: max_x - min_x,
50 height: max_y - min_y,
51 })
52 }
53
54 pub fn rect(rect: Rect) -> Path {
56 let right = rect.x + rect.width;
57 let bottom = rect.y + rect.height;
58 Path {
59 commands: vec![
60 PathCommand::MoveTo(Point {
61 x: rect.x,
62 y: rect.y,
63 }),
64 PathCommand::LineTo(Point {
65 x: right,
66 y: rect.y,
67 }),
68 PathCommand::LineTo(Point {
69 x: right,
70 y: bottom,
71 }),
72 PathCommand::LineTo(Point {
73 x: rect.x,
74 y: bottom,
75 }),
76 PathCommand::Close,
77 ],
78 fill_rule: FillRule::NonZero,
79 }
80 }
81
82 pub fn round_rect(rect: Rect, radius: f64) -> Path {
84 let radius = radius
85 .max(0.0)
86 .min(rect.width.abs() / 2.0)
87 .min(rect.height.abs() / 2.0);
88 if radius == 0.0 {
89 return Self::rect(rect);
90 }
91
92 const KAPPA: f64 = 0.552_284_749_830_793_6;
93 let right = rect.x + rect.width;
94 let bottom = rect.y + rect.height;
95 let control = radius * KAPPA;
96
97 Path {
98 commands: vec![
99 PathCommand::MoveTo(Point {
100 x: rect.x + radius,
101 y: rect.y,
102 }),
103 PathCommand::LineTo(Point {
104 x: right - radius,
105 y: rect.y,
106 }),
107 PathCommand::CurveTo {
108 c1: Point {
109 x: right - radius + control,
110 y: rect.y,
111 },
112 c2: Point {
113 x: right,
114 y: rect.y + radius - control,
115 },
116 to: Point {
117 x: right,
118 y: rect.y + radius,
119 },
120 },
121 PathCommand::LineTo(Point {
122 x: right,
123 y: bottom - radius,
124 }),
125 PathCommand::CurveTo {
126 c1: Point {
127 x: right,
128 y: bottom - radius + control,
129 },
130 c2: Point {
131 x: right - radius + control,
132 y: bottom,
133 },
134 to: Point {
135 x: right - radius,
136 y: bottom,
137 },
138 },
139 PathCommand::LineTo(Point {
140 x: rect.x + radius,
141 y: bottom,
142 }),
143 PathCommand::CurveTo {
144 c1: Point {
145 x: rect.x + radius - control,
146 y: bottom,
147 },
148 c2: Point {
149 x: rect.x,
150 y: bottom - radius + control,
151 },
152 to: Point {
153 x: rect.x,
154 y: bottom - radius,
155 },
156 },
157 PathCommand::LineTo(Point {
158 x: rect.x,
159 y: rect.y + radius,
160 }),
161 PathCommand::CurveTo {
162 c1: Point {
163 x: rect.x,
164 y: rect.y + radius - control,
165 },
166 c2: Point {
167 x: rect.x + radius - control,
168 y: rect.y,
169 },
170 to: Point {
171 x: rect.x + radius,
172 y: rect.y,
173 },
174 },
175 PathCommand::Close,
176 ],
177 fill_rule: FillRule::NonZero,
178 }
179 }
180
181 pub fn ellipse(rect: Rect) -> Path {
183 const KAPPA: f64 = 0.552_284_749_830_793_6;
184 let cx = rect.x + rect.width / 2.0;
185 let cy = rect.y + rect.height / 2.0;
186 let rx = rect.width / 2.0;
187 let ry = rect.height / 2.0;
188 let dx = rx * KAPPA;
189 let dy = ry * KAPPA;
190
191 Path {
192 commands: vec![
193 PathCommand::MoveTo(Point { x: cx + rx, y: cy }),
194 PathCommand::CurveTo {
195 c1: Point {
196 x: cx + rx,
197 y: cy + dy,
198 },
199 c2: Point {
200 x: cx + dx,
201 y: cy + ry,
202 },
203 to: Point { x: cx, y: cy + ry },
204 },
205 PathCommand::CurveTo {
206 c1: Point {
207 x: cx - dx,
208 y: cy + ry,
209 },
210 c2: Point {
211 x: cx - rx,
212 y: cy + dy,
213 },
214 to: Point { x: cx - rx, y: cy },
215 },
216 PathCommand::CurveTo {
217 c1: Point {
218 x: cx - rx,
219 y: cy - dy,
220 },
221 c2: Point {
222 x: cx - dx,
223 y: cy - ry,
224 },
225 to: Point { x: cx, y: cy - ry },
226 },
227 PathCommand::CurveTo {
228 c1: Point {
229 x: cx + dx,
230 y: cy - ry,
231 },
232 c2: Point {
233 x: cx + rx,
234 y: cy - dy,
235 },
236 to: Point { x: cx + rx, y: cy },
237 },
238 PathCommand::Close,
239 ],
240 fill_rule: FillRule::NonZero,
241 }
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::{FillRule, Path, PathCommand};
248 use crate::{Point, Rect};
249
250 #[test]
251 fn empty_path_has_no_bounds() {
252 let path = Path {
253 commands: Vec::new(),
254 fill_rule: FillRule::NonZero,
255 };
256 assert_eq!(path.bounds(), None);
257 }
258
259 #[test]
260 fn bounds_include_cubic_control_points() {
261 let path = Path {
262 commands: vec![
263 PathCommand::MoveTo(Point { x: 0.0, y: 0.0 }),
264 PathCommand::CurveTo {
265 c1: Point { x: -2.0, y: 4.0 },
266 c2: Point { x: 6.0, y: -3.0 },
267 to: Point { x: 1.0, y: 2.0 },
268 },
269 ],
270 fill_rule: FillRule::NonZero,
271 };
272 assert_eq!(
273 path.bounds(),
274 Some(Rect {
275 x: -2.0,
276 y: -3.0,
277 width: 8.0,
278 height: 7.0,
279 })
280 );
281 }
282
283 #[test]
284 fn rect_constructor_emits_a_closed_nonzero_path() {
285 let path = Path::rect(Rect {
286 x: 1.0,
287 y: 2.0,
288 width: 3.0,
289 height: 4.0,
290 });
291 assert_eq!(path.fill_rule, FillRule::NonZero);
292 assert!(matches!(path.commands.last(), Some(PathCommand::Close)));
293 }
294
295 #[test]
296 fn round_rect_clamps_the_radius_to_half_the_shorter_side() {
297 let rect = Rect {
298 x: 1.0,
299 y: 2.0,
300 width: 10.0,
301 height: 4.0,
302 };
303 assert_eq!(Path::round_rect(rect, 99.0), Path::round_rect(rect, 2.0));
304 assert_eq!(Path::round_rect(rect, -1.0), Path::rect(rect));
305 }
306
307 #[test]
308 fn round_rect_with_zero_radius_matches_rect() {
309 let rect = Rect {
310 x: 1.0,
311 y: 2.0,
312 width: 3.0,
313 height: 4.0,
314 };
315 assert_eq!(Path::round_rect(rect, 0.0), Path::rect(rect));
316 }
317
318 #[test]
319 fn ellipse_path_bounds_contain_the_ellipse_and_lie_within_its_control_hull() {
320 let rect = Rect {
321 x: 10.0,
322 y: 20.0,
323 width: 30.0,
324 height: 40.0,
325 };
326 assert_eq!(Path::ellipse(rect).bounds(), Some(rect));
327 }
328
329 #[test]
330 fn bounds_do_not_depend_on_the_fill_rule() {
331 let commands = Path::rect(Rect {
332 x: 1.0,
333 y: 2.0,
334 width: 3.0,
335 height: 4.0,
336 })
337 .commands;
338 let nonzero = Path {
339 commands: commands.clone(),
340 fill_rule: FillRule::NonZero,
341 };
342 let evenodd = Path {
343 commands,
344 fill_rule: FillRule::EvenOdd,
345 };
346 assert_eq!(nonzero.bounds(), evenodd.bounds());
347 }
348}