Skip to main content

oxml_layout/
path.rs

1//! Backend-neutral path geometry.
2
3use crate::{Point, Rect};
4
5/// One command in a path.
6#[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/// The rule used to determine which regions a path fills.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum FillRule {
17    NonZero,
18    EvenOdd,
19}
20
21/// A backend-neutral path and its fill rule.
22#[derive(Debug, Clone, PartialEq)]
23pub struct Path {
24    pub commands: Vec<PathCommand>,
25    pub fill_rule: FillRule,
26}
27
28impl Path {
29    /// Return conservative bounds containing every endpoint and cubic control.
30    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    /// Construct a closed rectangular path.
55    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    /// Construct a closed rectangle with one circular corner radius.
83    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    /// Construct a closed four-cubic approximation of an ellipse.
182    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}