rootvg_tessellation/path/
builder.rs

1// The following code was copied and modified from
2// https://github.com/iced-rs/iced/blob/31d1d5fecbef50fa319cabd5d4194f1e4aaefa21/graphics/src/geometry/path/builder.rs
3// Iced license (MIT): https://github.com/iced-rs/iced/blob/31d1d5fecbef50fa319cabd5d4194f1e4aaefa21/LICENSE
4
5use super::{ArcPath, EllipticalArcPath, Path};
6
7use rootvg_core::math::{Angle, Point, Size};
8
9use lyon::geom;
10use lyon::math;
11use lyon::path::builder::{self, SvgPathBuilder};
12
13/// A [`Path`] builder.
14///
15/// Once a [`Path`] is built, it can no longer be mutated.
16pub struct PathBuilder {
17    pub raw: builder::WithSvg<lyon::path::path::BuilderImpl>,
18}
19
20impl PathBuilder {
21    /// Creates a new [`Builder`].
22    pub fn new() -> Self {
23        Self {
24            raw: lyon::path::Path::builder().with_svg(),
25        }
26    }
27
28    /// Moves the starting point of a new sub-path to the given `Point`.
29    pub fn move_to(mut self, point: Point) -> Self {
30        self.raw.move_to(math::Point::new(point.x, point.y));
31        self
32    }
33
34    /// Connects the last point in the [`Path`] to the given `Point` with a
35    /// straight line.
36    pub fn line_to(mut self, point: Point) -> Self {
37        self.raw.line_to(math::Point::new(point.x, point.y));
38        self
39    }
40
41    /// Adds an [`Arc`] to the [`Path`] from `start_angle` to `end_angle` in
42    /// a clockwise direction.
43    pub fn arc(self, arc: ArcPath) -> Self {
44        self.ellipse(arc.into())
45    }
46
47    /// Adds a circular arc to the [`Path`] with the given control points and
48    /// radius.
49    ///
50    /// This essentially draws a straight line segment from the current
51    /// position to `a`, but fits a circular arc of `radius` tangent to that
52    /// segment and tangent to the line between `a` and `b`.
53    ///
54    /// With another `.line_to(b)`, the result will be a path connecting the
55    /// starting point and `b` with straight line segments towards `a` and a
56    /// circular arc smoothing out the corner at `a`.
57    ///
58    /// See [the HTML5 specification of `arcTo`](https://html.spec.whatwg.org/multipage/canvas.html#building-paths:dom-context-2d-arcto)
59    /// for more details and examples.
60    pub fn arc_to(mut self, a: Point, b: Point, radius: f32) -> Self {
61        let start = self.raw.current_position();
62        let mid = math::Point::new(a.x, a.y);
63        let end = math::Point::new(b.x, b.y);
64
65        if start == mid || mid == end || radius == 0.0 {
66            let _ = self.raw.line_to(mid);
67            return self;
68        }
69
70        let double_area =
71            start.x * (mid.y - end.y) + mid.x * (end.y - start.y) + end.x * (start.y - mid.y);
72
73        if double_area == 0.0 {
74            let _ = self.raw.line_to(mid);
75            return self;
76        }
77
78        let to_start = (start - mid).normalize();
79        let to_end = (end - mid).normalize();
80
81        let inner_angle = to_start.dot(to_end).acos();
82
83        let origin_angle = inner_angle / 2.0;
84
85        let origin_adjacent = radius / origin_angle.tan();
86
87        let arc_start = mid + to_start * origin_adjacent;
88        let arc_end = mid + to_end * origin_adjacent;
89
90        let sweep = to_start.cross(to_end) < 0.0;
91
92        let _ = self.raw.line_to(arc_start);
93
94        self.raw.arc_to(
95            math::Vector::new(radius, radius),
96            math::Angle::radians(0.0),
97            lyon::path::ArcFlags {
98                large_arc: false,
99                sweep,
100            },
101            arc_end,
102        );
103
104        self
105    }
106
107    /// Adds an ellipse to the [`Path`] using a clockwise direction.
108    pub fn ellipse(mut self, arc: EllipticalArcPath) -> Self {
109        let arc = geom::Arc {
110            center: math::Point::new(arc.center.x, arc.center.y),
111            radii: math::Vector::new(arc.radii.x, arc.radii.y),
112            x_rotation: math::Angle::radians(arc.rotation.radians),
113            start_angle: math::Angle::radians(arc.start_angle.radians),
114            sweep_angle: math::Angle::radians((arc.end_angle - arc.start_angle).radians),
115        };
116
117        let _ = self.raw.move_to(arc.sample(0.0));
118
119        arc.for_each_quadratic_bezier(&mut |curve| {
120            let _ = self.raw.quadratic_bezier_to(curve.ctrl, curve.to);
121        });
122
123        self
124    }
125
126    /// Adds a cubic Bézier curve to the [`Path`] given its two control points
127    /// and its end point.
128    pub fn bezier_curve_to(mut self, control_a: Point, control_b: Point, to: Point) -> Self {
129        let _ = self.raw.cubic_bezier_to(
130            math::Point::new(control_a.x, control_a.y),
131            math::Point::new(control_b.x, control_b.y),
132            math::Point::new(to.x, to.y),
133        );
134        self
135    }
136
137    /// Adds a quadratic Bézier curve to the [`Path`] given its control point
138    /// and its end point.
139    pub fn quadratic_curve_to(mut self, control: Point, to: Point) -> Self {
140        let _ = self.raw.quadratic_bezier_to(
141            math::Point::new(control.x, control.y),
142            math::Point::new(to.x, to.y),
143        );
144        self
145    }
146
147    /// Adds a rectangle to the [`Path`] given its top-left corner coordinate
148    /// and its `Size`.
149    pub fn rectangle(self, top_left: Point, size: Size) -> Self {
150        self.move_to(top_left)
151            .line_to(Point::new(top_left.x + size.width, top_left.y))
152            .line_to(Point::new(
153                top_left.x + size.width,
154                top_left.y + size.height,
155            ))
156            .line_to(Point::new(top_left.x, top_left.y + size.height))
157            .close()
158    }
159
160    /// Adds a circle to the [`Path`] given its center coordinate and its
161    /// radius.
162    pub fn circle(self, center: Point, radius: f32) -> Self {
163        self.arc(ArcPath {
164            center,
165            radius,
166            start_angle: Angle { radians: 0.0 },
167            end_angle: Angle {
168                radians: 2.0 * std::f32::consts::PI,
169            },
170        })
171    }
172
173    /// Closes the current sub-path in the [`Path`] with a straight line to
174    /// the starting point.
175    pub fn close(mut self) -> Self {
176        self.raw.close();
177        self
178    }
179
180    /// Builds the [`Path`] of this [`PathBuilder`].
181    pub fn build(self) -> Path {
182        Path {
183            raw: self.raw.build(),
184        }
185    }
186}
187
188impl Default for PathBuilder {
189    fn default() -> Self {
190        Self::new()
191    }
192}