Skip to main content

sequoia_draw/
path.rs

1use sequoia_core::{Point, Rect};
2
3use crate::Transform;
4use crate::flatten::{Contour, flatten};
5use crate::raster;
6use crate::svg_path::parse_svg_path;
7
8#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
9#[non_exhaustive]
10pub enum FillRule {
11    #[default]
12    NonZero,
13
14    EvenOdd,
15}
16
17#[derive(Clone, Debug, PartialEq, Eq)]
18pub struct PathError {
19    pub position: usize,
20
21    pub message: &'static str,
22}
23
24impl std::fmt::Display for PathError {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        write!(f, "{} (position {})", self.message, self.position)
27    }
28}
29
30impl std::error::Error for PathError {}
31
32#[derive(Clone, Copy, Debug, PartialEq)]
33#[non_exhaustive]
34pub enum PathSeg {
35    Move(Point),
36
37    Line(Point),
38
39    Quad(Point, Point),
40
41    Cubic(Point, Point, Point),
42
43    Close,
44}
45
46#[derive(Clone, Copy, Debug, Default, PartialEq)]
47pub struct Radius {
48    pub tl: f32,
49
50    pub tr: f32,
51
52    pub br: f32,
53
54    pub bl: f32,
55}
56
57impl Radius {
58    pub const ZERO: Self = Self {
59        tl: 0.0,
60        tr: 0.0,
61        br: 0.0,
62        bl: 0.0,
63    };
64
65    pub const fn all(r: f32) -> Self {
66        Self {
67            tl: r,
68            tr: r,
69            br: r,
70            bl: r,
71        }
72    }
73
74    pub const fn new(tl: f32, tr: f32, br: f32, bl: f32) -> Self {
75        Self { tl, tr, br, bl }
76    }
77
78    pub fn uniform(&self) -> Option<f32> {
79        if self.tl > 0.0 && self.tl == self.tr && self.tr == self.br && self.br == self.bl {
80            Some(self.tl)
81        } else {
82            None
83        }
84    }
85}
86
87impl From<f32> for Radius {
88    fn from(r: f32) -> Self {
89        Self::all(r)
90    }
91}
92
93impl From<[f32; 4]> for Radius {
94    fn from([tl, tr, br, bl]: [f32; 4]) -> Self {
95        Self { tl, tr, br, bl }
96    }
97}
98
99#[derive(Clone, Copy, Debug, Default, PartialEq)]
100enum Hint {
101    #[default]
102    None,
103    Rect(Rect),
104    RoundRect(Rect, f32),
105    Circle(Point, f32),
106}
107
108#[derive(Clone, Debug, Default, PartialEq)]
109pub struct Path {
110    segs: Vec<PathSeg>,
111    current: Point,
112    start: Point,
113    hint: Hint,
114}
115
116impl Path {
117    pub fn new() -> Self {
118        Self {
119            segs: Vec::new(),
120            current: Point::ZERO,
121            start: Point::ZERO,
122            hint: Hint::None,
123        }
124    }
125
126    pub fn is_empty(&self) -> bool {
127        self.segs.is_empty()
128    }
129
130    pub fn segs(&self) -> &[PathSeg] {
131        &self.segs
132    }
133
134    fn push(&mut self, seg: PathSeg) -> &mut Self {
135        self.hint = Hint::None;
136        self.segs.push(seg);
137        self
138    }
139
140    pub fn move_to(&mut self, p: impl Into<Point>) -> &mut Self {
141        let p = p.into();
142        self.current = p;
143        self.start = p;
144        self.push(PathSeg::Move(p))
145    }
146
147    pub fn line_to(&mut self, p: impl Into<Point>) -> &mut Self {
148        let p = p.into();
149        self.current = p;
150        self.push(PathSeg::Line(p))
151    }
152
153    pub fn quad_to(&mut self, c: impl Into<Point>, p: impl Into<Point>) -> &mut Self {
154        let (c, p) = (c.into(), p.into());
155        self.current = p;
156        self.push(PathSeg::Quad(c, p))
157    }
158
159    pub fn cubic_to(
160        &mut self,
161        c1: impl Into<Point>,
162        c2: impl Into<Point>,
163        p: impl Into<Point>,
164    ) -> &mut Self {
165        let (c1, c2, p) = (c1.into(), c2.into(), p.into());
166        self.current = p;
167        self.push(PathSeg::Cubic(c1, c2, p))
168    }
169
170    pub fn close(&mut self) -> &mut Self {
171        self.current = self.start;
172        self.push(PathSeg::Close)
173    }
174
175    pub fn polyline(&mut self, points: &[Point]) -> &mut Self {
176        let Some((first, rest)) = points.split_first() else {
177            return self;
178        };
179        self.move_to(*first);
180        for p in rest {
181            self.line_to(*p);
182        }
183        self
184    }
185
186    pub fn spline(&mut self, points: &[Point], tension: f32) -> &mut Self {
187        let n = points.len();
188        if n < 2 {
189            return self;
190        }
191        let tension = if tension.is_finite() { tension } else { 1.0 };
192        self.move_to(points[0]);
193        for i in 0..n - 1 {
194            let p0 = if i == 0 { points[0] } else { points[i - 1] };
195            let p1 = points[i];
196            let p2 = points[i + 1];
197            let p3 = if i + 2 < n {
198                points[i + 2]
199            } else {
200                points[n - 1]
201            };
202            let c1 = Point::new(
203                p1.x + (p2.x - p0.x) / 6.0 * tension,
204                p1.y + (p2.y - p0.y) / 6.0 * tension,
205            );
206            let c2 = Point::new(
207                p2.x - (p3.x - p1.x) / 6.0 * tension,
208                p2.y - (p3.y - p1.y) / 6.0 * tension,
209            );
210            self.cubic_to(c1, c2, p2);
211        }
212        self
213    }
214
215    pub fn polygon(&mut self, points: &[Point]) -> &mut Self {
216        self.polyline(points);
217        if !points.is_empty() {
218            self.close();
219        }
220        self
221    }
222
223    fn is_first_contour(&self) -> bool {
224        !self.segs.iter().any(|seg| matches!(seg, PathSeg::Move(_)))
225    }
226
227    pub fn rect(&mut self, rect: Rect) -> &mut Self {
228        let single = self.is_first_contour();
229        let (x0, y0) = (rect.left(), rect.top());
230        let (x1, y1) = (rect.right(), rect.bottom());
231        self.move_to((x0, y0));
232        self.line_to((x1, y0));
233        self.line_to((x1, y1));
234        self.line_to((x0, y1));
235        self.close();
236        if single {
237            self.hint = Hint::Rect(rect);
238        }
239        self
240    }
241
242    pub fn rounded_rect(&mut self, rect: Rect, radius: impl Into<Radius>) -> &mut Self {
243        let single = self.is_first_contour();
244        let radius = radius.into();
245        let max = (rect.size.width.min(rect.size.height) * 0.5).max(0.0);
246        let r = Radius {
247            tl: radius.tl.clamp(0.0, max),
248            tr: radius.tr.clamp(0.0, max),
249            br: radius.br.clamp(0.0, max),
250            bl: radius.bl.clamp(0.0, max),
251        };
252        let (x0, y0) = (rect.left(), rect.top());
253        let (x1, y1) = (rect.right(), rect.bottom());
254
255        self.move_to((x0 + r.tl, y0));
256        self.line_to((x1 - r.tr, y0));
257        if r.tr > 0.0 {
258            self.arc_corner((x1 - r.tr, y0 + r.tr), r.tr, -90.0, 0.0);
259        }
260        self.line_to((x1, y1 - r.br));
261        if r.br > 0.0 {
262            self.arc_corner((x1 - r.br, y1 - r.br), r.br, 0.0, 90.0);
263        }
264        self.line_to((x0 + r.bl, y1));
265        if r.bl > 0.0 {
266            self.arc_corner((x0 + r.bl, y1 - r.bl), r.bl, 90.0, 180.0);
267        }
268        self.line_to((x0, y0 + r.tl));
269        if r.tl > 0.0 {
270            self.arc_corner((x0 + r.tl, y0 + r.tl), r.tl, 180.0, 270.0);
271        }
272        self.close();
273        if let Some(uniform) = r.uniform()
274            && single
275        {
276            self.hint = Hint::RoundRect(rect, uniform);
277        }
278        self
279    }
280
281    pub fn circle(&mut self, center: impl Into<Point>, radius: f32) -> &mut Self {
282        let single = self.is_first_contour();
283        let center = center.into();
284        if radius <= 0.0 {
285            return self;
286        }
287        self.ellipse(center, radius, radius);
288        if single {
289            self.hint = Hint::Circle(center, radius);
290        }
291        self
292    }
293
294    pub fn ellipse(&mut self, center: impl Into<Point>, rx: f32, ry: f32) -> &mut Self {
295        let c = center.into();
296        if rx <= 0.0 || ry <= 0.0 {
297            return self;
298        }
299        let k = 0.552_284_8;
300        let (kx, ky) = (rx * k, ry * k);
301        self.move_to((c.x + rx, c.y));
302        self.cubic_to((c.x + rx, c.y + ky), (c.x + kx, c.y + ry), (c.x, c.y + ry));
303        self.cubic_to((c.x - kx, c.y + ry), (c.x - rx, c.y + ky), (c.x - rx, c.y));
304        self.cubic_to((c.x - rx, c.y - ky), (c.x - kx, c.y - ry), (c.x, c.y - ry));
305        self.cubic_to((c.x + kx, c.y - ry), (c.x + rx, c.y - ky), (c.x + rx, c.y));
306        self.close();
307        self
308    }
309
310    pub fn arc(
311        &mut self,
312        center: impl Into<Point>,
313        radius: f32,
314        from_deg: f32,
315        to_deg: f32,
316    ) -> &mut Self {
317        let c = center.into();
318        if radius <= 0.0 {
319            return self;
320        }
321        let from = from_deg.to_radians();
322        let start = Point::new(c.x + from.cos() * radius, c.y + from.sin() * radius);
323        if self.segs.is_empty() {
324            self.move_to(start);
325        } else if self.current != start {
326            self.line_to(start);
327        }
328        self.arc_corner(c, radius, from_deg, to_deg);
329        self
330    }
331
332    fn arc_corner(
333        &mut self,
334        center: impl Into<Point>,
335        radius: f32,
336        from_deg: f32,
337        to_deg: f32,
338    ) -> &mut Self {
339        let center = center.into();
340        let from = from_deg.to_radians();
341        let to = to_deg.to_radians();
342        let span = to - from;
343        if span.abs() < 1e-6 {
344            return self;
345        }
346        let steps = (span.abs() / (std::f32::consts::FRAC_PI_2)).ceil().max(1.0) as usize;
347        let step = span / steps as f32;
348        let k = 4.0 / 3.0 * (step / 4.0).tan();
349        let mut a = from;
350        let mut p = Point::new(center.x + a.cos() * radius, center.y + a.sin() * radius);
351        for _ in 0..steps {
352            let b = a + step;
353            let q = Point::new(center.x + b.cos() * radius, center.y + b.sin() * radius);
354            let c1 = Point::new(p.x - a.sin() * radius * k, p.y + a.cos() * radius * k);
355            let c2 = Point::new(q.x + b.sin() * radius * k, q.y - b.cos() * radius * k);
356            self.cubic_to(c1, c2, q);
357            a = b;
358            p = q;
359        }
360        self
361    }
362
363    pub fn from_svg(data: &str) -> Result<Self, PathError> {
364        parse_svg_path(data)
365    }
366
367    pub fn as_rect(&self) -> Option<Rect> {
368        match self.hint {
369            Hint::Rect(r) => Some(r),
370            _ => None,
371        }
372    }
373
374    pub fn as_rounded_rect(&self) -> Option<(Rect, f32)> {
375        match self.hint {
376            Hint::RoundRect(r, radius) => Some((r, radius)),
377            _ => None,
378        }
379    }
380
381    pub fn as_circle(&self) -> Option<(Point, f32)> {
382        match self.hint {
383            Hint::Circle(c, r) => Some((c, r)),
384            _ => None,
385        }
386    }
387
388    pub fn to_svg(&self) -> String {
389        crate::svg_path::segs_to_debug(self)
390    }
391
392    pub fn contours(&self, tolerance: f32) -> Vec<Contour> {
393        flatten(self, tolerance)
394    }
395
396    pub fn bounds(&self) -> Rect {
397        bounds_of_contours(&self.contours(crate::TOLERANCE))
398    }
399
400    pub fn bounds_with(&self, tolerance: f32) -> Rect {
401        bounds_of_contours(&self.contours(tolerance))
402    }
403
404    pub fn contains(&self, p: Point, rule: FillRule) -> bool {
405        let contours = self.contours(crate::TOLERANCE);
406        raster::contains(&contours, p, rule)
407    }
408
409    pub fn transformed(&self, t: Transform) -> Self {
410        let mut out = Self {
411            segs: Vec::with_capacity(self.segs.len()),
412            current: t.apply(self.current),
413            start: t.apply(self.start),
414            hint: Hint::None,
415        };
416        out.segs = self
417            .segs
418            .iter()
419            .map(|seg| match *seg {
420                PathSeg::Move(p) => PathSeg::Move(t.apply(p)),
421                PathSeg::Line(p) => PathSeg::Line(t.apply(p)),
422                PathSeg::Quad(c, p) => PathSeg::Quad(t.apply(c), t.apply(p)),
423                PathSeg::Cubic(c1, c2, p) => PathSeg::Cubic(t.apply(c1), t.apply(c2), t.apply(p)),
424                PathSeg::Close => PathSeg::Close,
425            })
426            .collect();
427        out.hint = match self.hint {
428            Hint::Rect(r) if t.m[1] == 0.0 && t.m[2] == 0.0 => Hint::Rect(transform_rect(r, t)),
429            Hint::Circle(c, r) => match t.similarity_scale() {
430                Some(s) => Hint::Circle(t.apply(c), r * s),
431                None => Hint::None,
432            },
433            Hint::RoundRect(r, radius)
434                if t.m[1] == 0.0 && t.m[2] == 0.0 && (t.m[0].abs() - t.m[3].abs()).abs() < 1e-6 =>
435            {
436                Hint::RoundRect(transform_rect(r, t), radius * t.m[0].abs())
437            }
438            _ => Hint::None,
439        };
440        out
441    }
442
443    pub fn translated(&self, dx: f32, dy: f32) -> Self {
444        self.transformed(Transform::translate(dx, dy))
445    }
446
447    pub fn scaled(&self, sx: f32, sy: f32) -> Self {
448        self.transformed(Transform::scale(sx, sy))
449    }
450
451    pub fn rotated(&self, radians: f32, pivot: Point) -> Self {
452        self.transformed(Transform::rotate_around(radians, pivot))
453    }
454
455    pub(crate) fn fingerprint(&self) -> u64 {
456        let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
457        hash ^= match self.hint {
458            Hint::None => 7,
459            Hint::Rect(_) => 8,
460            Hint::RoundRect(_, _) => 9,
461            Hint::Circle(_, _) => 10,
462        };
463        for seg in &self.segs {
464            match *seg {
465                PathSeg::Move(p) => {
466                    hash ^= 1;
467                    hash_point(&mut hash, p);
468                }
469                PathSeg::Line(p) => {
470                    hash ^= 2;
471                    hash_point(&mut hash, p);
472                }
473                PathSeg::Quad(c, p) => {
474                    hash ^= 3;
475                    hash_point(&mut hash, c);
476                    hash_point(&mut hash, p);
477                }
478                PathSeg::Cubic(c1, c2, p) => {
479                    hash ^= 4;
480                    hash_point(&mut hash, c1);
481                    hash_point(&mut hash, c2);
482                    hash_point(&mut hash, p);
483                }
484                PathSeg::Close => hash ^= 5,
485            }
486            hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
487        }
488        hash
489    }
490}
491
492impl std::fmt::Display for Path {
493    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
494        f.write_str(&self.to_svg())
495    }
496}
497
498fn hash_point(hash: &mut u64, p: Point) {
499    for v in [p.x, p.y] {
500        for byte in v.to_bits().to_le_bytes() {
501            *hash ^= u64::from(byte);
502            *hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
503        }
504    }
505}
506
507fn bounds_of_contours(contours: &[Contour]) -> Rect {
508    let mut min_x = f32::INFINITY;
509    let mut min_y = f32::INFINITY;
510    let mut max_x = f32::NEG_INFINITY;
511    let mut max_y = f32::NEG_INFINITY;
512    for c in contours {
513        for p in &c.points {
514            min_x = min_x.min(p.x);
515            min_y = min_y.min(p.y);
516            max_x = max_x.max(p.x);
517            max_y = max_y.max(p.y);
518        }
519    }
520    if !min_x.is_finite() {
521        return Rect::ZERO;
522    }
523    Rect::from_xywh(min_x, min_y, max_x - min_x, max_y - min_y)
524}
525
526fn transform_rect(r: Rect, t: Transform) -> Rect {
527    let a = t.apply(Point::new(r.left(), r.top()));
528    let b = t.apply(Point::new(r.right(), r.bottom()));
529    Rect::from_xywh(
530        a.x.min(b.x),
531        a.y.min(b.y),
532        (b.x - a.x).abs(),
533        (b.y - a.y).abs(),
534    )
535}