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