1use crate::{
11 LineGeometry, PathFillRule, Point, Rect, Stroke, StrokeCap, StrokeJoin, VectorPath,
12 vector_path::{flatten_cubic_into, quad_as_cubic},
13};
14
15#[derive(Clone, Debug, Default, PartialEq)]
18pub struct PathContour {
19 pub points: Vec<Point>,
20 pub closed: bool,
21}
22
23#[derive(Clone, Debug, Default, PartialEq)]
37pub struct Path {
38 contours: Vec<PathContour>,
39 pen: Point,
40}
41
42impl Path {
43 pub fn new() -> Self {
45 Self::default()
46 }
47
48 pub fn move_to(&mut self, point: Point) {
50 self.contours.push(PathContour {
51 points: vec![point],
52 closed: false,
53 });
54 self.pen = point;
55 }
56
57 pub fn line_to(&mut self, point: Point) {
59 self.open_contour().push(point);
60 self.pen = point;
61 }
62
63 pub fn quadratic_to(&mut self, control: Point, end: Point) {
66 let (first, second) = quad_as_cubic(self.pen, control, end);
67 self.cubic_to(first, second, end);
68 }
69
70 pub fn cubic_to(&mut self, first: Point, second: Point, end: Point) {
73 let start = self.pen;
74 flatten_cubic_into(self.open_contour(), start, first, second, end);
75 self.pen = end;
76 }
77
78 pub fn close(&mut self) {
81 if let Some(contour) = self.contours.last_mut().filter(|contour| !contour.closed) {
82 contour.closed = true;
83 if let Some(&first) = contour.points.first() {
84 self.pen = first;
85 }
86 }
87 }
88
89 pub fn reset(&mut self) {
91 self.contours.clear();
92 self.pen = Point::ZERO;
93 }
94
95 pub fn contours(&self) -> &[PathContour] {
97 &self.contours
98 }
99
100 pub fn is_empty(&self) -> bool {
102 !self
103 .contours
104 .iter()
105 .any(|contour| contour.points.len() >= 2)
106 }
107
108 pub fn bounds(&self) -> Rect {
111 let mut points = self.contours.iter().flat_map(|contour| &contour.points);
112 let Some(&first) = points.next() else {
113 return Rect::EMPTY;
114 };
115 let (min, max) = points.fold((first, first), |(min, max), point| {
116 (
117 Point::new(min.x.min(point.x), min.y.min(point.y)),
118 Point::new(max.x.max(point.x), max.y.max(point.y)),
119 )
120 });
121 Rect {
122 x: min.x,
123 y: min.y,
124 width: max.x - min.x,
125 height: max.y - min.y,
126 }
127 }
128
129 pub fn to_vector_path(&self, fill_rule: PathFillRule) -> VectorPath {
131 VectorPath::from_subpaths(
132 self.contours
133 .iter()
134 .filter(|contour| contour.points.len() >= 2)
135 .map(|contour| contour.points.clone())
136 .collect(),
137 fill_rule,
138 )
139 }
140
141 fn open_contour(&mut self) -> &mut Vec<Point> {
144 if self.contours.last().is_none_or(|contour| contour.closed) {
145 self.contours.push(PathContour {
146 points: vec![self.pen],
147 closed: false,
148 });
149 }
150 let last = self.contours.len() - 1;
151 &mut self.contours[last].points
152 }
153}
154
155#[derive(Clone, Copy, Debug, PartialEq)]
161pub struct DashPathEffect {
162 intervals: [f32; DashPathEffect::MAX_INTERVALS],
163 count: usize,
164 phase: f32,
165}
166
167impl DashPathEffect {
168 pub const MAX_INTERVALS: usize = 8;
170
171 pub fn new(intervals: &[f32], phase: f32) -> Self {
174 let mut stored = [0.0; Self::MAX_INTERVALS];
175 let count = intervals.len().min(Self::MAX_INTERVALS);
176 stored[..count].copy_from_slice(&intervals[..count]);
177 Self {
178 intervals: stored,
179 count,
180 phase,
181 }
182 }
183
184 pub fn intervals(&self) -> &[f32] {
186 &self.intervals[..self.count]
187 }
188
189 fn period(&self) -> Option<f32> {
190 let period: f32 = self
191 .intervals()
192 .iter()
193 .map(|interval| interval.max(0.0))
194 .sum();
195 (self.count >= 2 && self.count.is_multiple_of(2) && period > 0.0 && period.is_finite())
196 .then_some(period)
197 }
198
199 pub fn for_each_dash(&self, points: &[Point], mut dash: impl FnMut(&[Point])) {
203 let Some(period) = self.period() else {
204 dash(points);
205 return;
206 };
207 let interval = |index: usize| self.intervals[index].max(0.0);
208 let mut current: Vec<Point> = Vec::new();
209 let mut into = self.phase.rem_euclid(period);
211 let mut index = 0;
212 while into >= interval(index) {
213 into -= interval(index);
214 index = (index + 1) % self.count;
215 }
216 for pair in points.windows(2) {
217 let (start, end) = (pair[0], pair[1]);
218 let length = ((end.x - start.x).powi(2) + (end.y - start.y).powi(2)).sqrt();
219 let at = |distance: f32| {
220 let t = if length > 0.0 { distance / length } else { 0.0 };
221 Point::new(
222 start.x + (end.x - start.x) * t,
223 start.y + (end.y - start.y) * t,
224 )
225 };
226 let mut walked = 0.0;
227 while walked < length {
228 let on = index.is_multiple_of(2);
229 let step = (interval(index) - into).min(length - walked);
230 if on {
231 if current.is_empty() {
232 current.push(at(walked));
233 }
234 current.push(at(walked + step));
235 }
236 walked += step;
237 into += step;
238 if into >= interval(index) {
239 into = 0.0;
240 if on && current.len() >= 2 {
241 dash(¤t);
242 }
243 current.clear();
244 index = (index + 1) % self.count;
245 }
246 }
247 }
248 if current.len() >= 2 {
249 dash(¤t);
250 }
251 }
252}
253
254#[derive(Clone, Copy, Debug, PartialEq)]
257pub enum DrawStyle {
258 Fill,
260 Stroke(Stroke),
262 DashedStroke(Stroke, DashPathEffect),
265}
266
267pub fn for_each_stroke_line(
276 points: &[Point],
277 closed: bool,
278 stroke: Stroke,
279 mut line: impl FnMut(LineGeometry),
280) {
281 let corner = match stroke.join {
282 StrokeJoin::Miter => StrokeCap::Square,
283 StrokeJoin::Round | StrokeJoin::Bevel => StrokeCap::Round,
284 };
285 let closing = closed
286 .then(|| points.first().zip(points.last()))
287 .flatten()
288 .filter(|(first, last)| first != last)
289 .map(|(&first, &last)| (last, first));
290 let edges = points
291 .windows(2)
292 .map(|pair| (pair[0], pair[1]))
293 .chain(closing)
294 .filter(|(start, end)| start != end);
295 let count = edges.clone().count();
296 for (index, (start, end)) in edges.enumerate() {
297 let start_cap = if closed || index > 0 {
298 corner
299 } else {
300 stroke.cap
301 };
302 let end_cap = if closed || index + 1 < count {
303 corner
304 } else {
305 stroke.cap
306 };
307 let with_cap = |cap| Stroke { cap, ..stroke };
308 if start_cap == end_cap {
309 line(LineGeometry::new(start, end, with_cap(start_cap)));
310 continue;
311 }
312 let half_width = stroke.half_width();
313 let length = ((end.x - start.x).powi(2) + (end.y - start.y).powi(2)).sqrt();
314 let grow = |cap: StrokeCap| {
315 if cap == StrokeCap::Square {
316 half_width / length
317 } else {
318 0.0
319 }
320 };
321 let (back, ahead) = (grow(start_cap), grow(end_cap));
322 let (dx, dy) = (end.x - start.x, end.y - start.y);
323 line(LineGeometry::new(
324 Point::new(start.x - dx * back, start.y - dy * back),
325 Point::new(end.x + dx * ahead, end.y + dy * ahead),
326 with_cap(StrokeCap::Butt),
327 ));
328 for (point, cap) in [(start, start_cap), (end, end_cap)] {
329 if cap == StrokeCap::Round {
330 line(LineGeometry::new(point, point, with_cap(StrokeCap::Round)));
331 }
332 }
333 }
334}
335
336#[cfg(test)]
337#[path = "tests/path_tests.rs"]
338mod tests;