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 {
34 let mut points =
35 crate::thread_state::with(|state| state.path_buffers.borrow_mut().points());
36 points.push(point);
37 Self {
38 points,
39 closed: false,
40 }
41 }
42}
43
44pub(crate) struct PathBuffers {
48 points: Vec<Vec<Point>>,
49 kept_points: usize,
51 contours: Vec<Vec<PathContour>>,
52}
53
54impl PathBuffers {
55 const KEPT: usize = 32;
57 const POINT_BUDGET: usize = 4096;
60
61 pub(crate) const fn new() -> Self {
62 Self {
63 points: Vec::new(),
64 kept_points: 0,
65 contours: Vec::new(),
66 }
67 }
68
69 fn points(&mut self) -> Vec<Point> {
70 match self.points.pop() {
71 Some(points) => {
72 self.kept_points -= points.capacity();
73 points
74 }
75 None => Vec::with_capacity(PathContour::INITIAL_POINTS),
76 }
77 }
78
79 fn contours(&mut self) -> Vec<PathContour> {
80 self.contours.pop().unwrap_or_default()
81 }
82
83 fn keep_points(&mut self, contours: &mut Vec<PathContour>) {
85 for PathContour { mut points, .. } in contours.drain(..) {
86 let room = points.capacity();
87 if self.kept_points + room <= Self::POINT_BUDGET {
88 points.clear();
89 self.kept_points += room;
90 self.points.push(points);
91 }
92 }
93 }
94
95 fn keep(&mut self, mut contours: Vec<PathContour>) {
97 self.keep_points(&mut contours);
98 if self.contours.len() < Self::KEPT {
99 self.contours.push(contours);
100 }
101 }
102}
103
104#[derive(Clone, Debug, Default, PartialEq)]
118pub struct Path {
119 contours: Vec<PathContour>,
120 pen: Point,
121}
122
123impl Path {
124 pub fn new() -> Self {
126 Self::default()
127 }
128
129 pub fn move_to(&mut self, point: Point) {
131 self.push_contour(point);
132 self.pen = point;
133 }
134
135 fn push_contour(&mut self, start: Point) {
136 if self.contours.capacity() == 0 {
137 self.contours =
138 crate::thread_state::with(|state| state.path_buffers.borrow_mut().contours());
139 }
140 self.contours.push(PathContour::starting_at(start));
141 }
142
143 #[inline]
145 pub fn line_to(&mut self, point: Point) {
146 match self.contours.last_mut() {
147 Some(contour) if !contour.closed => contour.points.push(point),
148 _ => self.open_contour().push(point),
149 }
150 self.pen = point;
151 }
152
153 pub fn quadratic_to(&mut self, control: Point, end: Point) {
156 let (first, second) = quad_as_cubic(self.pen, control, end);
157 self.cubic_to(first, second, end);
158 }
159
160 pub fn cubic_to(&mut self, first: Point, second: Point, end: Point) {
163 let start = self.pen;
164 flatten_cubic_into(self.open_contour(), start, first, second, end);
165 self.pen = end;
166 }
167
168 pub fn close(&mut self) {
171 if let Some(contour) = self.contours.last_mut().filter(|contour| !contour.closed) {
172 contour.closed = true;
173 if let Some(&first) = contour.points.first() {
174 self.pen = first;
175 }
176 }
177 }
178
179 pub fn reset(&mut self) {
181 if !self.contours.is_empty() {
182 crate::thread_state::with(|state| {
183 state
184 .path_buffers
185 .borrow_mut()
186 .keep_points(&mut self.contours);
187 });
188 }
189 self.pen = Point::ZERO;
190 }
191
192 pub fn contours(&self) -> &[PathContour] {
194 &self.contours
195 }
196
197 pub fn is_empty(&self) -> bool {
199 !self
200 .contours
201 .iter()
202 .any(|contour| contour.points.len() >= 2)
203 }
204
205 pub fn bounds(&self) -> Rect {
208 let mut points = self.contours.iter().flat_map(|contour| &contour.points);
209 let Some(&first) = points.next() else {
210 return Rect::EMPTY;
211 };
212 let (min, max) = points.fold((first, first), |(min, max), point| {
213 (
214 Point::new(min.x.min(point.x), min.y.min(point.y)),
215 Point::new(max.x.max(point.x), max.y.max(point.y)),
216 )
217 });
218 Rect {
219 x: min.x,
220 y: min.y,
221 width: max.x - min.x,
222 height: max.y - min.y,
223 }
224 }
225
226 pub fn to_vector_path(&self, fill_rule: PathFillRule) -> VectorPath {
228 VectorPath::from_subpaths(
229 self.contours
230 .iter()
231 .filter(|contour| contour.points.len() >= 2)
232 .map(|contour| contour.points.clone())
233 .collect(),
234 fill_rule,
235 )
236 }
237
238 fn open_contour(&mut self) -> &mut Vec<Point> {
241 if self.contours.last().is_none_or(|contour| contour.closed) {
242 self.push_contour(self.pen);
243 }
244 let last = self.contours.len() - 1;
245 &mut self.contours[last].points
246 }
247}
248
249impl Drop for Path {
250 fn drop(&mut self) {
251 if self.contours.capacity() == 0 {
252 return;
253 }
254 let contours = std::mem::take(&mut self.contours);
255 crate::thread_state::with(|state| state.path_buffers.borrow_mut().keep(contours));
256 }
257}
258
259#[derive(Clone, Copy, Debug, PartialEq)]
265pub struct DashPathEffect {
266 intervals: [f32; DashPathEffect::MAX_INTERVALS],
267 count: usize,
268 phase: f32,
269}
270
271impl DashPathEffect {
272 pub const MAX_INTERVALS: usize = 8;
274
275 pub fn new(intervals: &[f32], phase: f32) -> Self {
278 let mut stored = [0.0; Self::MAX_INTERVALS];
279 let count = intervals.len().min(Self::MAX_INTERVALS);
280 stored[..count].copy_from_slice(&intervals[..count]);
281 Self {
282 intervals: stored,
283 count,
284 phase,
285 }
286 }
287
288 pub fn intervals(&self) -> &[f32] {
290 &self.intervals[..self.count]
291 }
292
293 fn period(&self) -> Option<f32> {
294 let period: f32 = self
295 .intervals()
296 .iter()
297 .map(|interval| interval.max(0.0))
298 .sum();
299 (self.count >= 2 && self.count.is_multiple_of(2) && period > 0.0 && period.is_finite())
300 .then_some(period)
301 }
302
303 pub fn for_each_dash(&self, points: &[Point], mut dash: impl FnMut(&[Point])) {
307 let Some(period) = self.period() else {
308 dash(points);
309 return;
310 };
311 let interval = |index: usize| self.intervals[index].max(0.0);
312 let mut current: Vec<Point> = Vec::new();
313 let mut into = self.phase.rem_euclid(period);
315 let mut index = 0;
316 while into >= interval(index) {
317 into -= interval(index);
318 index = (index + 1) % self.count;
319 }
320 for pair in points.windows(2) {
321 let (start, end) = (pair[0], pair[1]);
322 let length = ((end.x - start.x).powi(2) + (end.y - start.y).powi(2)).sqrt();
323 let at = |distance: f32| {
324 let t = if length > 0.0 { distance / length } else { 0.0 };
325 Point::new(
326 start.x + (end.x - start.x) * t,
327 start.y + (end.y - start.y) * t,
328 )
329 };
330 let mut walked = 0.0;
331 while walked < length {
332 let on = index.is_multiple_of(2);
333 let step = (interval(index) - into).min(length - walked);
334 if on {
335 if current.is_empty() {
336 current.push(at(walked));
337 }
338 current.push(at(walked + step));
339 }
340 walked += step;
341 into += step;
342 if into >= interval(index) {
343 into = 0.0;
344 if on && current.len() >= 2 {
345 dash(¤t);
346 }
347 current.clear();
348 index = (index + 1) % self.count;
349 }
350 }
351 }
352 if current.len() >= 2 {
353 dash(¤t);
354 }
355 }
356}
357
358#[derive(Clone, Copy, Debug, PartialEq)]
361pub enum DrawStyle {
362 Fill,
364 Stroke(Stroke),
366 DashedStroke(Stroke, DashPathEffect),
369}
370
371pub fn for_each_stroke_line(
380 points: &[Point],
381 closed: bool,
382 stroke: Stroke,
383 mut line: impl FnMut(LineGeometry),
384) {
385 let corner = match stroke.join {
386 StrokeJoin::Miter => StrokeCap::Square,
387 StrokeJoin::Round | StrokeJoin::Bevel => StrokeCap::Round,
388 };
389 let closing = closed
390 .then(|| points.first().zip(points.last()))
391 .flatten()
392 .filter(|(first, last)| first != last)
393 .map(|(&first, &last)| (last, first));
394 let mut edges = points
395 .windows(2)
396 .map(|pair| (pair[0], pair[1]))
397 .chain(closing)
398 .filter(|(start, end)| start != end)
399 .peekable();
400 let mut first = true;
401 while let Some((start, end)) = edges.next() {
402 let start_cap = if closed || !first { corner } else { stroke.cap };
403 first = false;
404 let end_cap = if closed || edges.peek().is_some() {
405 corner
406 } else {
407 stroke.cap
408 };
409 let with_cap = |cap| Stroke { cap, ..stroke };
410 if start_cap == end_cap {
411 line(LineGeometry::new(start, end, with_cap(start_cap)));
412 continue;
413 }
414 let half_width = stroke.half_width();
415 let length = ((end.x - start.x).powi(2) + (end.y - start.y).powi(2)).sqrt();
416 let grow = |cap: StrokeCap| {
417 if cap == StrokeCap::Square {
418 half_width / length
419 } else {
420 0.0
421 }
422 };
423 let (back, ahead) = (grow(start_cap), grow(end_cap));
424 let (dx, dy) = (end.x - start.x, end.y - start.y);
425 line(LineGeometry::new(
426 Point::new(start.x - dx * back, start.y - dy * back),
427 Point::new(end.x + dx * ahead, end.y + dy * ahead),
428 with_cap(StrokeCap::Butt),
429 ));
430 for (point, cap) in [(start, start_cap), (end, end_cap)] {
431 if cap == StrokeCap::Round {
432 line(LineGeometry::new(point, point, with_cap(StrokeCap::Round)));
433 }
434 }
435 }
436}
437
438#[cfg(test)]
439#[path = "tests/path_tests.rs"]
440mod tests;