Skip to main content

astrelis_geometry/
path.rs

1//! Path primitives for vector graphics.
2//!
3//! A path is a sequence of drawing commands that define a shape.
4
5use crate::{CubicBezier, QuadraticBezier};
6use glam::Vec2;
7
8/// A command in a path.
9#[derive(Debug, Clone, Copy, PartialEq)]
10pub enum PathCommand {
11    /// Move to a new position without drawing.
12    MoveTo(Vec2),
13    /// Draw a line to a position.
14    LineTo(Vec2),
15    /// Draw a quadratic Bezier curve.
16    QuadTo {
17        /// Control point
18        control: Vec2,
19        /// End point
20        to: Vec2,
21    },
22    /// Draw a cubic Bezier curve.
23    CubicTo {
24        /// First control point
25        control1: Vec2,
26        /// Second control point
27        control2: Vec2,
28        /// End point
29        to: Vec2,
30    },
31    /// Draw an arc.
32    ArcTo {
33        /// Radii of the ellipse
34        radii: Vec2,
35        /// X-axis rotation in radians
36        x_rotation: f32,
37        /// Use large arc
38        large_arc: bool,
39        /// Sweep direction (clockwise if true)
40        sweep: bool,
41        /// End point
42        to: Vec2,
43    },
44    /// Close the current sub-path by drawing a line to the start.
45    Close,
46}
47
48/// A 2D path consisting of drawing commands.
49#[derive(Debug, Clone, Default, PartialEq)]
50pub struct Path {
51    commands: Vec<PathCommand>,
52}
53
54impl Path {
55    /// Create a new empty path.
56    pub fn new() -> Self {
57        Self::default()
58    }
59
60    /// Create a path from a list of commands.
61    pub fn from_commands(commands: Vec<PathCommand>) -> Self {
62        Self { commands }
63    }
64
65    /// Get the commands in this path.
66    pub fn commands(&self) -> &[PathCommand] {
67        &self.commands
68    }
69
70    /// Check if the path is empty.
71    pub fn is_empty(&self) -> bool {
72        self.commands.is_empty()
73    }
74
75    /// Get the number of commands.
76    pub fn len(&self) -> usize {
77        self.commands.len()
78    }
79
80    /// Get the bounding box of the path.
81    ///
82    /// Returns (min, max) corners.
83    pub fn bounds(&self) -> Option<(Vec2, Vec2)> {
84        if self.commands.is_empty() {
85            return None;
86        }
87
88        let mut min = Vec2::splat(f32::INFINITY);
89        let mut max = Vec2::splat(f32::NEG_INFINITY);
90        let mut current = Vec2::ZERO;
91
92        for cmd in &self.commands {
93            match cmd {
94                PathCommand::MoveTo(to) | PathCommand::LineTo(to) => {
95                    min = min.min(*to);
96                    max = max.max(*to);
97                    current = *to;
98                }
99                PathCommand::QuadTo { control, to } => {
100                    // Include control point for conservative bounds
101                    min = min.min(*control).min(*to);
102                    max = max.max(*control).max(*to);
103                    current = *to;
104                }
105                PathCommand::CubicTo {
106                    control1,
107                    control2,
108                    to,
109                } => {
110                    // Include control points for conservative bounds
111                    min = min.min(*control1).min(*control2).min(*to);
112                    max = max.max(*control1).max(*control2).max(*to);
113                    current = *to;
114                }
115                PathCommand::ArcTo { to, radii, .. } => {
116                    // Conservative bounds: include endpoint and radii
117                    min = min.min(*to).min(current - *radii);
118                    max = max.max(*to).max(current + *radii);
119                    current = *to;
120                }
121                PathCommand::Close => {}
122            }
123        }
124
125        if min.x.is_finite() && min.y.is_finite() && max.x.is_finite() && max.y.is_finite() {
126            Some((min, max))
127        } else {
128            None
129        }
130    }
131
132    /// Reverse the path direction.
133    pub fn reverse(&self) -> Self {
134        let mut reversed = Vec::new();
135        let mut subpath_start = Vec2::ZERO;
136        let mut current = Vec2::ZERO;
137        let mut subpath_commands = Vec::new();
138
139        for cmd in &self.commands {
140            match cmd {
141                PathCommand::MoveTo(to) => {
142                    // Flush previous subpath
143                    if !subpath_commands.is_empty() {
144                        reversed.push(PathCommand::MoveTo(current));
145                        for rcmd in subpath_commands.drain(..).rev() {
146                            reversed.push(rcmd);
147                        }
148                    }
149                    subpath_start = *to;
150                    current = *to;
151                }
152                PathCommand::LineTo(to) => {
153                    subpath_commands.push(PathCommand::LineTo(current));
154                    current = *to;
155                }
156                PathCommand::QuadTo { control, to } => {
157                    subpath_commands.push(PathCommand::QuadTo {
158                        control: *control,
159                        to: current,
160                    });
161                    current = *to;
162                }
163                PathCommand::CubicTo {
164                    control1,
165                    control2,
166                    to,
167                } => {
168                    subpath_commands.push(PathCommand::CubicTo {
169                        control1: *control2,
170                        control2: *control1,
171                        to: current,
172                    });
173                    current = *to;
174                }
175                PathCommand::ArcTo {
176                    radii,
177                    x_rotation,
178                    large_arc,
179                    sweep,
180                    to,
181                } => {
182                    subpath_commands.push(PathCommand::ArcTo {
183                        radii: *radii,
184                        x_rotation: *x_rotation,
185                        large_arc: *large_arc,
186                        sweep: !sweep,
187                        to: current,
188                    });
189                    current = *to;
190                }
191                PathCommand::Close => {
192                    subpath_commands.push(PathCommand::LineTo(current));
193                    current = subpath_start;
194                }
195            }
196        }
197
198        // Flush final subpath
199        if !subpath_commands.is_empty() {
200            reversed.push(PathCommand::MoveTo(current));
201            for rcmd in subpath_commands.drain(..).rev() {
202                reversed.push(rcmd);
203            }
204        }
205
206        Self { commands: reversed }
207    }
208}
209
210/// Builder for constructing paths.
211#[derive(Debug, Default)]
212pub struct PathBuilder {
213    commands: Vec<PathCommand>,
214    current_pos: Vec2,
215    subpath_start: Vec2,
216}
217
218impl PathBuilder {
219    /// Create a new path builder.
220    pub fn new() -> Self {
221        Self::default()
222    }
223
224    /// Move to a new position without drawing.
225    pub fn move_to(&mut self, to: Vec2) -> &mut Self {
226        self.commands.push(PathCommand::MoveTo(to));
227        self.current_pos = to;
228        self.subpath_start = to;
229        self
230    }
231
232    /// Draw a line to a position.
233    pub fn line_to(&mut self, to: Vec2) -> &mut Self {
234        self.commands.push(PathCommand::LineTo(to));
235        self.current_pos = to;
236        self
237    }
238
239    /// Draw a horizontal line to x coordinate.
240    pub fn horizontal_line_to(&mut self, x: f32) -> &mut Self {
241        let to = Vec2::new(x, self.current_pos.y);
242        self.line_to(to)
243    }
244
245    /// Draw a vertical line to y coordinate.
246    pub fn vertical_line_to(&mut self, y: f32) -> &mut Self {
247        let to = Vec2::new(self.current_pos.x, y);
248        self.line_to(to)
249    }
250
251    /// Draw a quadratic Bezier curve.
252    pub fn quad_to(&mut self, control: Vec2, to: Vec2) -> &mut Self {
253        self.commands.push(PathCommand::QuadTo { control, to });
254        self.current_pos = to;
255        self
256    }
257
258    /// Draw a smooth quadratic Bezier (control point reflected from previous).
259    pub fn smooth_quad_to(&mut self, to: Vec2) -> &mut Self {
260        // Reflect previous control point
261        let control = if let Some(PathCommand::QuadTo { control, to: prev }) =
262            self.commands.last().copied()
263        {
264            prev * 2.0 - control
265        } else {
266            self.current_pos
267        };
268        self.quad_to(control, to)
269    }
270
271    /// Draw a cubic Bezier curve.
272    pub fn cubic_to(&mut self, control1: Vec2, control2: Vec2, to: Vec2) -> &mut Self {
273        self.commands.push(PathCommand::CubicTo {
274            control1,
275            control2,
276            to,
277        });
278        self.current_pos = to;
279        self
280    }
281
282    /// Draw a smooth cubic Bezier (first control point reflected from previous).
283    pub fn smooth_cubic_to(&mut self, control2: Vec2, to: Vec2) -> &mut Self {
284        // Reflect previous control2
285        let control1 = if let Some(PathCommand::CubicTo {
286            control2, to: prev, ..
287        }) = self.commands.last().copied()
288        {
289            prev * 2.0 - control2
290        } else {
291            self.current_pos
292        };
293        self.cubic_to(control1, control2, to)
294    }
295
296    /// Draw an arc.
297    pub fn arc_to(
298        &mut self,
299        radii: Vec2,
300        x_rotation: f32,
301        large_arc: bool,
302        sweep: bool,
303        to: Vec2,
304    ) -> &mut Self {
305        self.commands.push(PathCommand::ArcTo {
306            radii,
307            x_rotation,
308            large_arc,
309            sweep,
310            to,
311        });
312        self.current_pos = to;
313        self
314    }
315
316    /// Close the current sub-path.
317    pub fn close(&mut self) -> &mut Self {
318        self.commands.push(PathCommand::Close);
319        self.current_pos = self.subpath_start;
320        self
321    }
322
323    /// Add a rectangle to the path.
324    pub fn rect(&mut self, position: Vec2, size: Vec2) -> &mut Self {
325        self.move_to(position);
326        self.line_to(position + Vec2::new(size.x, 0.0));
327        self.line_to(position + size);
328        self.line_to(position + Vec2::new(0.0, size.y));
329        self.close()
330    }
331
332    /// Add a rounded rectangle to the path.
333    pub fn rounded_rect(&mut self, position: Vec2, size: Vec2, radius: f32) -> &mut Self {
334        let r = radius.min(size.x / 2.0).min(size.y / 2.0);
335        let radii = Vec2::splat(r);
336
337        // Start at top-left corner (after the curve)
338        self.move_to(position + Vec2::new(r, 0.0));
339
340        // Top edge
341        self.line_to(position + Vec2::new(size.x - r, 0.0));
342        // Top-right corner
343        self.arc_to(radii, 0.0, false, true, position + Vec2::new(size.x, r));
344
345        // Right edge
346        self.line_to(position + Vec2::new(size.x, size.y - r));
347        // Bottom-right corner
348        self.arc_to(
349            radii,
350            0.0,
351            false,
352            true,
353            position + Vec2::new(size.x - r, size.y),
354        );
355
356        // Bottom edge
357        self.line_to(position + Vec2::new(r, size.y));
358        // Bottom-left corner
359        self.arc_to(
360            radii,
361            0.0,
362            false,
363            true,
364            position + Vec2::new(0.0, size.y - r),
365        );
366
367        // Left edge
368        self.line_to(position + Vec2::new(0.0, r));
369        // Top-left corner
370        self.arc_to(radii, 0.0, false, true, position + Vec2::new(r, 0.0));
371
372        self.close()
373    }
374
375    /// Add a circle to the path.
376    pub fn circle(&mut self, center: Vec2, radius: f32) -> &mut Self {
377        let r = Vec2::splat(radius);
378
379        // Start at rightmost point
380        self.move_to(center + Vec2::new(radius, 0.0));
381
382        // Draw four arcs
383        self.arc_to(r, 0.0, false, true, center + Vec2::new(0.0, radius));
384        self.arc_to(r, 0.0, false, true, center + Vec2::new(-radius, 0.0));
385        self.arc_to(r, 0.0, false, true, center + Vec2::new(0.0, -radius));
386        self.arc_to(r, 0.0, false, true, center + Vec2::new(radius, 0.0));
387
388        self.close()
389    }
390
391    /// Add an ellipse to the path.
392    pub fn ellipse(&mut self, center: Vec2, radii: Vec2) -> &mut Self {
393        // Start at rightmost point
394        self.move_to(center + Vec2::new(radii.x, 0.0));
395
396        // Draw four arcs
397        self.arc_to(radii, 0.0, false, true, center + Vec2::new(0.0, radii.y));
398        self.arc_to(radii, 0.0, false, true, center + Vec2::new(-radii.x, 0.0));
399        self.arc_to(radii, 0.0, false, true, center + Vec2::new(0.0, -radii.y));
400        self.arc_to(radii, 0.0, false, true, center + Vec2::new(radii.x, 0.0));
401
402        self.close()
403    }
404
405    /// Add a polygon to the path.
406    pub fn polygon(&mut self, points: &[Vec2]) -> &mut Self {
407        if points.is_empty() {
408            return self;
409        }
410
411        self.move_to(points[0]);
412        for point in &points[1..] {
413            self.line_to(*point);
414        }
415        self.close()
416    }
417
418    /// Get the current position.
419    pub fn current_pos(&self) -> Vec2 {
420        self.current_pos
421    }
422
423    /// Build the path.
424    pub fn build(self) -> Path {
425        Path {
426            commands: self.commands,
427        }
428    }
429}
430
431/// Extension trait for extracting curve segments from paths.
432pub trait PathCurves {
433    /// Iterator over quadratic curves in the path.
434    fn quadratic_curves(&self) -> impl Iterator<Item = QuadraticBezier> + '_;
435    /// Iterator over cubic curves in the path.
436    fn cubic_curves(&self) -> impl Iterator<Item = CubicBezier> + '_;
437}
438
439impl PathCurves for Path {
440    fn quadratic_curves(&self) -> impl Iterator<Item = QuadraticBezier> + '_ {
441        let mut current = Vec2::ZERO;
442        self.commands.iter().filter_map(move |cmd| match cmd {
443            PathCommand::MoveTo(to) | PathCommand::LineTo(to) => {
444                current = *to;
445                None
446            }
447            PathCommand::QuadTo { control, to } => {
448                let curve = QuadraticBezier::new(current, *control, *to);
449                current = *to;
450                Some(curve)
451            }
452            PathCommand::CubicTo { to, .. } => {
453                current = *to;
454                None
455            }
456            PathCommand::ArcTo { to, .. } => {
457                current = *to;
458                None
459            }
460            PathCommand::Close => None,
461        })
462    }
463
464    fn cubic_curves(&self) -> impl Iterator<Item = CubicBezier> + '_ {
465        let mut current = Vec2::ZERO;
466        self.commands.iter().filter_map(move |cmd| match cmd {
467            PathCommand::MoveTo(to) | PathCommand::LineTo(to) => {
468                current = *to;
469                None
470            }
471            PathCommand::QuadTo { to, .. } => {
472                current = *to;
473                None
474            }
475            PathCommand::CubicTo {
476                control1,
477                control2,
478                to,
479            } => {
480                let curve = CubicBezier::new(current, *control1, *control2, *to);
481                current = *to;
482                Some(curve)
483            }
484            PathCommand::ArcTo { to, .. } => {
485                current = *to;
486                None
487            }
488            PathCommand::Close => None,
489        })
490    }
491}
492
493#[cfg(test)]
494mod tests {
495    use super::*;
496
497    #[test]
498    fn test_path_builder_line() {
499        let mut builder = PathBuilder::new();
500        builder
501            .move_to(Vec2::new(0.0, 0.0))
502            .line_to(Vec2::new(100.0, 0.0))
503            .line_to(Vec2::new(100.0, 100.0))
504            .close();
505        let path = builder.build();
506
507        assert_eq!(path.len(), 4);
508    }
509
510    #[test]
511    fn test_path_bounds() {
512        let mut builder = PathBuilder::new();
513        builder
514            .move_to(Vec2::new(10.0, 20.0))
515            .line_to(Vec2::new(100.0, 50.0))
516            .line_to(Vec2::new(50.0, 100.0));
517        let path = builder.build();
518
519        let (min, max) = path.bounds().unwrap();
520        assert_eq!(min, Vec2::new(10.0, 20.0));
521        assert_eq!(max, Vec2::new(100.0, 100.0));
522    }
523
524    #[test]
525    fn test_circle_path() {
526        let mut builder = PathBuilder::new();
527        builder.circle(Vec2::new(50.0, 50.0), 25.0);
528        let path = builder.build();
529
530        // Should have: move, 4 arcs, close
531        assert!(!path.is_empty());
532    }
533
534    #[test]
535    fn test_rect_path() {
536        let mut builder = PathBuilder::new();
537        builder.rect(Vec2::new(10.0, 10.0), Vec2::new(80.0, 60.0));
538        let path = builder.build();
539
540        let (min, max) = path.bounds().unwrap();
541        assert_eq!(min, Vec2::new(10.0, 10.0));
542        assert_eq!(max, Vec2::new(90.0, 70.0));
543    }
544}