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
23impl PathContour {
24 const INITIAL_POINTS: usize = 16;
28
29 fn starting_at(point: Point) -> Self {
30 let mut points = Vec::with_capacity(Self::INITIAL_POINTS);
31 points.push(point);
32 Self {
33 points,
34 closed: false,
35 }
36 }
37}
38
39#[derive(Clone, Debug, Default, PartialEq)]
53pub struct Path {
54 contours: Vec<PathContour>,
55 pen: Point,
56}
57
58impl Path {
59 pub fn new() -> Self {
61 Self::default()
62 }
63
64 pub fn move_to(&mut self, point: Point) {
66 self.contours.push(PathContour::starting_at(point));
67 self.pen = point;
68 }
69
70 pub fn line_to(&mut self, point: Point) {
72 self.open_contour().push(point);
73 self.pen = point;
74 }
75
76 pub fn quadratic_to(&mut self, control: Point, end: Point) {
79 let (first, second) = quad_as_cubic(self.pen, control, end);
80 self.cubic_to(first, second, end);
81 }
82
83 pub fn cubic_to(&mut self, first: Point, second: Point, end: Point) {
86 let start = self.pen;
87 flatten_cubic_into(self.open_contour(), start, first, second, end);
88 self.pen = end;
89 }
90
91 pub fn close(&mut self) {
94 if let Some(contour) = self.contours.last_mut().filter(|contour| !contour.closed) {
95 contour.closed = true;
96 if let Some(&first) = contour.points.first() {
97 self.pen = first;
98 }
99 }
100 }
101
102 pub fn reset(&mut self) {
104 self.contours.clear();
105 self.pen = Point::ZERO;
106 }
107
108 pub fn contours(&self) -> &[PathContour] {
110 &self.contours
111 }
112
113 pub fn is_empty(&self) -> bool {
115 !self
116 .contours
117 .iter()
118 .any(|contour| contour.points.len() >= 2)
119 }
120
121 pub fn bounds(&self) -> Rect {
124 let mut points = self.contours.iter().flat_map(|contour| &contour.points);
125 let Some(&first) = points.next() else {
126 return Rect::EMPTY;
127 };
128 let (min, max) = points.fold((first, first), |(min, max), point| {
129 (
130 Point::new(min.x.min(point.x), min.y.min(point.y)),
131 Point::new(max.x.max(point.x), max.y.max(point.y)),
132 )
133 });
134 Rect {
135 x: min.x,
136 y: min.y,
137 width: max.x - min.x,
138 height: max.y - min.y,
139 }
140 }
141
142 pub fn to_vector_path(&self, fill_rule: PathFillRule) -> VectorPath {
144 VectorPath::from_subpaths(
145 self.contours
146 .iter()
147 .filter(|contour| contour.points.len() >= 2)
148 .map(|contour| contour.points.clone())
149 .collect(),
150 fill_rule,
151 )
152 }
153
154 fn open_contour(&mut self) -> &mut Vec<Point> {
157 if self.contours.last().is_none_or(|contour| contour.closed) {
158 self.contours.push(PathContour::starting_at(self.pen));
159 }
160 let last = self.contours.len() - 1;
161 &mut self.contours[last].points
162 }
163}
164
165#[derive(Clone, Copy, Debug, PartialEq)]
171pub struct DashPathEffect {
172 intervals: [f32; DashPathEffect::MAX_INTERVALS],
173 count: usize,
174 phase: f32,
175}
176
177impl DashPathEffect {
178 pub const MAX_INTERVALS: usize = 8;
180
181 pub fn new(intervals: &[f32], phase: f32) -> Self {
184 let mut stored = [0.0; Self::MAX_INTERVALS];
185 let count = intervals.len().min(Self::MAX_INTERVALS);
186 stored[..count].copy_from_slice(&intervals[..count]);
187 Self {
188 intervals: stored,
189 count,
190 phase,
191 }
192 }
193
194 pub fn intervals(&self) -> &[f32] {
196 &self.intervals[..self.count]
197 }
198
199 fn period(&self) -> Option<f32> {
200 let period: f32 = self
201 .intervals()
202 .iter()
203 .map(|interval| interval.max(0.0))
204 .sum();
205 (self.count >= 2 && self.count.is_multiple_of(2) && period > 0.0 && period.is_finite())
206 .then_some(period)
207 }
208
209 pub fn for_each_dash(&self, points: &[Point], mut dash: impl FnMut(&[Point])) {
213 let Some(period) = self.period() else {
214 dash(points);
215 return;
216 };
217 let interval = |index: usize| self.intervals[index].max(0.0);
218 let mut current: Vec<Point> = Vec::new();
219 let mut into = self.phase.rem_euclid(period);
221 let mut index = 0;
222 while into >= interval(index) {
223 into -= interval(index);
224 index = (index + 1) % self.count;
225 }
226 for pair in points.windows(2) {
227 let (start, end) = (pair[0], pair[1]);
228 let length = ((end.x - start.x).powi(2) + (end.y - start.y).powi(2)).sqrt();
229 let at = |distance: f32| {
230 let t = if length > 0.0 { distance / length } else { 0.0 };
231 Point::new(
232 start.x + (end.x - start.x) * t,
233 start.y + (end.y - start.y) * t,
234 )
235 };
236 let mut walked = 0.0;
237 while walked < length {
238 let on = index.is_multiple_of(2);
239 let step = (interval(index) - into).min(length - walked);
240 if on {
241 if current.is_empty() {
242 current.push(at(walked));
243 }
244 current.push(at(walked + step));
245 }
246 walked += step;
247 into += step;
248 if into >= interval(index) {
249 into = 0.0;
250 if on && current.len() >= 2 {
251 dash(¤t);
252 }
253 current.clear();
254 index = (index + 1) % self.count;
255 }
256 }
257 }
258 if current.len() >= 2 {
259 dash(¤t);
260 }
261 }
262}
263
264#[derive(Clone, Copy, Debug, PartialEq)]
267pub enum DrawStyle {
268 Fill,
270 Stroke(Stroke),
272 DashedStroke(Stroke, DashPathEffect),
275}
276
277pub fn for_each_stroke_line(
286 points: &[Point],
287 closed: bool,
288 stroke: Stroke,
289 mut line: impl FnMut(LineGeometry),
290) {
291 let corner = match stroke.join {
292 StrokeJoin::Miter => StrokeCap::Square,
293 StrokeJoin::Round | StrokeJoin::Bevel => StrokeCap::Round,
294 };
295 let closing = closed
296 .then(|| points.first().zip(points.last()))
297 .flatten()
298 .filter(|(first, last)| first != last)
299 .map(|(&first, &last)| (last, first));
300 let edges = points
301 .windows(2)
302 .map(|pair| (pair[0], pair[1]))
303 .chain(closing)
304 .filter(|(start, end)| start != end);
305 let count = edges.clone().count();
306 for (index, (start, end)) in edges.enumerate() {
307 let start_cap = if closed || index > 0 {
308 corner
309 } else {
310 stroke.cap
311 };
312 let end_cap = if closed || index + 1 < count {
313 corner
314 } else {
315 stroke.cap
316 };
317 let with_cap = |cap| Stroke { cap, ..stroke };
318 if start_cap == end_cap {
319 line(LineGeometry::new(start, end, with_cap(start_cap)));
320 continue;
321 }
322 let half_width = stroke.half_width();
323 let length = ((end.x - start.x).powi(2) + (end.y - start.y).powi(2)).sqrt();
324 let grow = |cap: StrokeCap| {
325 if cap == StrokeCap::Square {
326 half_width / length
327 } else {
328 0.0
329 }
330 };
331 let (back, ahead) = (grow(start_cap), grow(end_cap));
332 let (dx, dy) = (end.x - start.x, end.y - start.y);
333 line(LineGeometry::new(
334 Point::new(start.x - dx * back, start.y - dy * back),
335 Point::new(end.x + dx * ahead, end.y + dy * ahead),
336 with_cap(StrokeCap::Butt),
337 ));
338 for (point, cap) in [(start, start_cap), (end, end_cap)] {
339 if cap == StrokeCap::Round {
340 line(LineGeometry::new(point, point, with_cap(StrokeCap::Round)));
341 }
342 }
343 }
344}
345
346#[cfg(test)]
347#[path = "tests/path_tests.rs"]
348mod tests;