skia_rs_path/
path.rs

1//! Path data structure and iteration.
2
3use skia_rs_core::{Point, Rect, Scalar};
4use smallvec::SmallVec;
5
6/// Path fill type.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
8#[repr(u8)]
9pub enum FillType {
10    /// Non-zero winding rule.
11    #[default]
12    Winding = 0,
13    /// Even-odd rule.
14    EvenOdd,
15    /// Inverse non-zero winding.
16    InverseWinding,
17    /// Inverse even-odd.
18    InverseEvenOdd,
19}
20
21impl FillType {
22    /// Check if this is an inverse fill type.
23    #[inline]
24    pub const fn is_inverse(&self) -> bool {
25        matches!(self, FillType::InverseWinding | FillType::InverseEvenOdd)
26    }
27
28    /// Convert to the inverse fill type.
29    #[inline]
30    pub const fn inverse(&self) -> Self {
31        match self {
32            FillType::Winding => FillType::InverseWinding,
33            FillType::EvenOdd => FillType::InverseEvenOdd,
34            FillType::InverseWinding => FillType::Winding,
35            FillType::InverseEvenOdd => FillType::EvenOdd,
36        }
37    }
38}
39
40/// Path verb (command type).
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42#[repr(u8)]
43pub enum Verb {
44    /// Move to a point.
45    Move = 0,
46    /// Line to a point.
47    Line,
48    /// Quadratic bezier.
49    Quad,
50    /// Conic (weighted quadratic).
51    Conic,
52    /// Cubic bezier.
53    Cubic,
54    /// Close the current contour.
55    Close,
56}
57
58impl Verb {
59    /// Number of points consumed by this verb.
60    #[inline]
61    pub const fn point_count(&self) -> usize {
62        match self {
63            Verb::Move | Verb::Line => 1,
64            Verb::Quad | Verb::Conic => 2,
65            Verb::Cubic => 3,
66            Verb::Close => 0,
67        }
68    }
69}
70
71/// Path direction (clockwise or counter-clockwise).
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
73#[repr(u8)]
74pub enum PathDirection {
75    /// Clockwise direction.
76    #[default]
77    CW = 0,
78    /// Counter-clockwise direction.
79    CCW,
80}
81
82/// Path convexity.
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
84#[repr(u8)]
85pub enum PathConvexity {
86    /// Unknown convexity.
87    #[default]
88    Unknown = 0,
89    /// Path is convex.
90    Convex,
91    /// Path is concave.
92    Concave,
93}
94
95/// A 2D geometric path.
96#[derive(Debug, Clone, Default)]
97pub struct Path {
98    /// Path verbs.
99    pub(crate) verbs: SmallVec<[Verb; 16]>,
100    /// Path points.
101    pub(crate) points: SmallVec<[Point; 32]>,
102    /// Conic weights.
103    pub(crate) conic_weights: SmallVec<[Scalar; 4]>,
104    /// Fill type.
105    pub(crate) fill_type: FillType,
106    /// Cached bounds (lazily computed).
107    pub(crate) bounds: Option<Rect>,
108    /// Cached convexity.
109    pub(crate) convexity: PathConvexity,
110}
111
112impl Path {
113    /// Create a new empty path.
114    #[inline]
115    pub fn new() -> Self {
116        Self::default()
117    }
118
119    /// Get the fill type.
120    #[inline]
121    pub fn fill_type(&self) -> FillType {
122        self.fill_type
123    }
124
125    /// Set the fill type.
126    #[inline]
127    pub fn set_fill_type(&mut self, fill_type: FillType) {
128        self.fill_type = fill_type;
129    }
130
131    /// Check if the path is empty.
132    #[inline]
133    pub fn is_empty(&self) -> bool {
134        self.verbs.is_empty()
135    }
136
137    /// Get the number of verbs.
138    #[inline]
139    pub fn verb_count(&self) -> usize {
140        self.verbs.len()
141    }
142
143    /// Get the number of points.
144    #[inline]
145    pub fn point_count(&self) -> usize {
146        self.points.len()
147    }
148
149    /// Get the bounds of the path.
150    pub fn bounds(&self) -> Rect {
151        if let Some(bounds) = self.bounds {
152            return bounds;
153        }
154
155        if self.points.is_empty() {
156            return Rect::EMPTY;
157        }
158
159        let mut min_x = self.points[0].x;
160        let mut min_y = self.points[0].y;
161        let mut max_x = min_x;
162        let mut max_y = min_y;
163
164        for p in &self.points[1..] {
165            min_x = min_x.min(p.x);
166            min_y = min_y.min(p.y);
167            max_x = max_x.max(p.x);
168            max_y = max_y.max(p.y);
169        }
170
171        Rect::new(min_x, min_y, max_x, max_y)
172    }
173
174    /// Clear the path.
175    #[inline]
176    pub fn reset(&mut self) {
177        self.verbs.clear();
178        self.points.clear();
179        self.conic_weights.clear();
180        self.bounds = None;
181    }
182
183    /// Iterate over the path elements.
184    pub fn iter(&self) -> PathIter<'_> {
185        PathIter {
186            path: self,
187            verb_index: 0,
188            point_index: 0,
189            weight_index: 0,
190        }
191    }
192
193    /// Get the verbs slice.
194    #[inline]
195    pub fn verbs(&self) -> &[Verb] {
196        &self.verbs
197    }
198
199    /// Get the points slice.
200    #[inline]
201    pub fn points(&self) -> &[Point] {
202        &self.points
203    }
204
205    /// Get the last point in the path.
206    #[inline]
207    pub fn last_point(&self) -> Option<Point> {
208        self.points.last().copied()
209    }
210
211    /// Get the number of contours in the path.
212    pub fn contour_count(&self) -> usize {
213        self.verbs.iter().filter(|v| **v == Verb::Move).count()
214    }
215
216    /// Check if the path is closed.
217    pub fn is_closed(&self) -> bool {
218        self.verbs.last() == Some(&Verb::Close)
219    }
220
221    /// Check if the path represents a line.
222    pub fn is_line(&self) -> bool {
223        self.verbs.len() == 2 && self.verbs[0] == Verb::Move && self.verbs[1] == Verb::Line
224    }
225
226    /// Check if the path represents a rectangle.
227    pub fn is_rect(&self) -> Option<Rect> {
228        // A rectangle has: Move, Line, Line, Line, Line, Close (or just 4 lines)
229        if self.verbs.len() < 5 {
230            return None;
231        }
232
233        let mut line_count = 0;
234        let mut has_close = false;
235
236        for verb in &self.verbs {
237            match verb {
238                Verb::Move => {}
239                Verb::Line => line_count += 1,
240                Verb::Close => has_close = true,
241                _ => return None,
242            }
243        }
244
245        if line_count != 4 || !has_close {
246            return None;
247        }
248
249        // Check if points form a rectangle
250        if self.points.len() < 5 {
251            return None;
252        }
253
254        let p0 = self.points[0];
255        let p1 = self.points[1];
256        let p2 = self.points[2];
257        let p3 = self.points[3];
258        let p4 = self.points[4];
259
260        // Check for axis-aligned rectangle
261        let is_horizontal_1 = (p0.y - p1.y).abs() < 0.001;
262        let is_vertical_2 = (p1.x - p2.x).abs() < 0.001;
263        let is_horizontal_3 = (p2.y - p3.y).abs() < 0.001;
264        let is_vertical_4 = (p3.x - p4.x).abs() < 0.001;
265
266        if is_horizontal_1 && is_vertical_2 && is_horizontal_3 && is_vertical_4 {
267            let left = p0.x.min(p1.x).min(p2.x).min(p3.x);
268            let top = p0.y.min(p1.y).min(p2.y).min(p3.y);
269            let right = p0.x.max(p1.x).max(p2.x).max(p3.x);
270            let bottom = p0.y.max(p1.y).max(p2.y).max(p3.y);
271            return Some(Rect::new(left, top, right, bottom));
272        }
273
274        None
275    }
276
277    /// Check if the path represents an oval.
278    pub fn is_oval(&self) -> bool {
279        // Ovals typically have 4 cubic curves or 4 conics
280        let cubic_count = self.verbs.iter().filter(|v| **v == Verb::Cubic).count();
281        let conic_count = self.verbs.iter().filter(|v| **v == Verb::Conic).count();
282
283        (cubic_count == 4 && self.verbs.len() == 6) // Move + 4 Cubic + Close
284            || (conic_count == 4 && self.verbs.len() == 6) // Move + 4 Conic + Close
285    }
286
287    /// Get the convexity of the path.
288    pub fn convexity(&self) -> PathConvexity {
289        if self.convexity != PathConvexity::Unknown {
290            return self.convexity;
291        }
292
293        // Simple convexity check based on cross product signs
294        if self.points.len() < 3 {
295            return PathConvexity::Convex;
296        }
297
298        let mut sign = 0i32;
299        let n = self.points.len();
300
301        for i in 0..n {
302            let p0 = self.points[i];
303            let p1 = self.points[(i + 1) % n];
304            let p2 = self.points[(i + 2) % n];
305
306            let cross = (p1.x - p0.x) * (p2.y - p1.y) - (p1.y - p0.y) * (p2.x - p1.x);
307
308            if cross.abs() > 0.001 {
309                let current_sign = if cross > 0.0 { 1 } else { -1 };
310                if sign == 0 {
311                    sign = current_sign;
312                } else if sign != current_sign {
313                    return PathConvexity::Concave;
314                }
315            }
316        }
317
318        PathConvexity::Convex
319    }
320
321    /// Check if the path is convex.
322    #[inline]
323    pub fn is_convex(&self) -> bool {
324        self.convexity() == PathConvexity::Convex
325    }
326
327    /// Get the direction of the first contour.
328    pub fn direction(&self) -> Option<PathDirection> {
329        if self.points.len() < 3 {
330            return None;
331        }
332
333        // Calculate signed area using shoelace formula
334        let mut signed_area = 0.0;
335        let n = self.points.len();
336
337        for i in 0..n {
338            let p0 = self.points[i];
339            let p1 = self.points[(i + 1) % n];
340            signed_area += (p1.x - p0.x) * (p1.y + p0.y);
341        }
342
343        if signed_area.abs() < 0.001 {
344            return None;
345        }
346
347        Some(if signed_area > 0.0 {
348            PathDirection::CW
349        } else {
350            PathDirection::CCW
351        })
352    }
353
354    /// Reverse the path direction.
355    pub fn reverse(&mut self) {
356        if self.verbs.is_empty() {
357            return;
358        }
359
360        // Reverse points
361        self.points.reverse();
362
363        // Reverse conic weights
364        self.conic_weights.reverse();
365
366        // Reverse verbs (keeping structure)
367        // This is a simplified implementation
368        let mut new_verbs = SmallVec::new();
369        let mut i = self.verbs.len();
370
371        while i > 0 {
372            i -= 1;
373            match self.verbs[i] {
374                Verb::Move => {
375                    if !new_verbs.is_empty() {
376                        new_verbs.push(Verb::Close);
377                    }
378                    new_verbs.push(Verb::Move);
379                }
380                Verb::Close => {
381                    // Skip, will be added before next Move
382                }
383                v => new_verbs.push(v),
384            }
385        }
386
387        if !new_verbs.is_empty() && self.is_closed() {
388            new_verbs.push(Verb::Close);
389        }
390
391        self.verbs = new_verbs;
392        self.bounds = None;
393        self.convexity = PathConvexity::Unknown;
394    }
395
396    /// Transform the path by a matrix.
397    pub fn transform(&mut self, matrix: &skia_rs_core::Matrix) {
398        for point in &mut self.points {
399            *point = matrix.map_point(*point);
400        }
401        self.bounds = None;
402        self.convexity = PathConvexity::Unknown;
403    }
404
405    /// Create a transformed copy of the path.
406    pub fn transformed(&self, matrix: &skia_rs_core::Matrix) -> Self {
407        let mut result = self.clone();
408        result.transform(matrix);
409        result
410    }
411
412    /// Offset the path by (dx, dy).
413    pub fn offset(&mut self, dx: Scalar, dy: Scalar) {
414        for point in &mut self.points {
415            point.x += dx;
416            point.y += dy;
417        }
418        if let Some(ref mut bounds) = self.bounds {
419            bounds.left += dx;
420            bounds.right += dx;
421            bounds.top += dy;
422            bounds.bottom += dy;
423        }
424    }
425
426    /// Check if a point is inside the path (using fill rule).
427    pub fn contains(&self, point: Point) -> bool {
428        // Ray casting algorithm
429        if !self.bounds().contains(point) {
430            return false;
431        }
432
433        let mut crossings = 0;
434        let mut current = Point::zero();
435
436        for element in self.iter() {
437            match element {
438                PathElement::Move(p) => current = p,
439                PathElement::Line(end) => {
440                    if ray_crosses_segment(point, current, end) {
441                        crossings += 1;
442                    }
443                    current = end;
444                }
445                PathElement::Quad(ctrl, end) => {
446                    // Approximate with lines
447                    for i in 1..=8 {
448                        let t = i as f32 / 8.0;
449                        let mt = 1.0 - t;
450                        let p = Point::new(
451                            mt * mt * current.x + 2.0 * mt * t * ctrl.x + t * t * end.x,
452                            mt * mt * current.y + 2.0 * mt * t * ctrl.y + t * t * end.y,
453                        );
454                        if ray_crosses_segment(point, current, p) {
455                            crossings += 1;
456                        }
457                        current = p;
458                    }
459                    current = end;
460                }
461                PathElement::Conic(ctrl, end, _w) => {
462                    // Approximate with lines
463                    for i in 1..=8 {
464                        let t = i as f32 / 8.0;
465                        let mt = 1.0 - t;
466                        let p = Point::new(
467                            mt * mt * current.x + 2.0 * mt * t * ctrl.x + t * t * end.x,
468                            mt * mt * current.y + 2.0 * mt * t * ctrl.y + t * t * end.y,
469                        );
470                        if ray_crosses_segment(point, current, p) {
471                            crossings += 1;
472                        }
473                        current = p;
474                    }
475                    current = end;
476                }
477                PathElement::Cubic(c1, c2, end) => {
478                    // Approximate with lines
479                    for i in 1..=12 {
480                        let t = i as f32 / 12.0;
481                        let mt = 1.0 - t;
482                        let mt2 = mt * mt;
483                        let t2 = t * t;
484                        let p = Point::new(
485                            mt2 * mt * current.x
486                                + 3.0 * mt2 * t * c1.x
487                                + 3.0 * mt * t2 * c2.x
488                                + t2 * t * end.x,
489                            mt2 * mt * current.y
490                                + 3.0 * mt2 * t * c1.y
491                                + 3.0 * mt * t2 * c2.y
492                                + t2 * t * end.y,
493                        );
494                        if ray_crosses_segment(point, current, p) {
495                            crossings += 1;
496                        }
497                        current = p;
498                    }
499                    current = end;
500                }
501                PathElement::Close => {}
502            }
503        }
504
505        match self.fill_type {
506            FillType::Winding => crossings != 0,
507            FillType::EvenOdd => crossings % 2 != 0,
508            FillType::InverseWinding => crossings == 0,
509            FillType::InverseEvenOdd => crossings % 2 == 0,
510        }
511    }
512
513    /// Compute tight bounds (considering curve control points).
514    pub fn tight_bounds(&self) -> Rect {
515        // For now, same as bounds (which already considers all points)
516        self.bounds()
517    }
518
519    /// Get the total length of the path.
520    pub fn length(&self) -> Scalar {
521        let mut total = 0.0;
522        let mut current = Point::zero();
523
524        for element in self.iter() {
525            match element {
526                PathElement::Move(p) => current = p,
527                PathElement::Line(end) => {
528                    total += current.distance(&end);
529                    current = end;
530                }
531                PathElement::Quad(ctrl, end) => {
532                    // Approximate
533                    total += current.distance(&ctrl) + ctrl.distance(&end);
534                    current = end;
535                }
536                PathElement::Conic(ctrl, end, _) => {
537                    total += current.distance(&ctrl) + ctrl.distance(&end);
538                    current = end;
539                }
540                PathElement::Cubic(c1, c2, end) => {
541                    total += current.distance(&c1) + c1.distance(&c2) + c2.distance(&end);
542                    current = end;
543                }
544                PathElement::Close => {}
545            }
546        }
547
548        total
549    }
550}
551
552/// Check if a horizontal ray from point crosses the segment.
553fn ray_crosses_segment(point: Point, p0: Point, p1: Point) -> bool {
554    // Ensure p0 is below p1
555    let (p0, p1) = if p0.y <= p1.y { (p0, p1) } else { (p1, p0) };
556
557    // Check if point is in y-range of segment
558    if point.y < p0.y || point.y >= p1.y {
559        return false;
560    }
561
562    // Calculate x-coordinate of intersection
563    let t = (point.y - p0.y) / (p1.y - p0.y);
564    let x_intersect = p0.x + t * (p1.x - p0.x);
565
566    x_intersect > point.x
567}
568
569/// A path element from iteration.
570#[derive(Debug, Clone, Copy, PartialEq)]
571pub enum PathElement {
572    /// Move to point.
573    Move(Point),
574    /// Line to point.
575    Line(Point),
576    /// Quadratic bezier (control, end).
577    Quad(Point, Point),
578    /// Conic (control, end, weight).
579    Conic(Point, Point, Scalar),
580    /// Cubic bezier (control1, control2, end).
581    Cubic(Point, Point, Point),
582    /// Close the path.
583    Close,
584}
585
586/// Iterator over path elements.
587pub struct PathIter<'a> {
588    path: &'a Path,
589    verb_index: usize,
590    point_index: usize,
591    weight_index: usize,
592}
593
594impl<'a> Iterator for PathIter<'a> {
595    type Item = PathElement;
596
597    fn next(&mut self) -> Option<Self::Item> {
598        if self.verb_index >= self.path.verbs.len() {
599            return None;
600        }
601
602        let verb = self.path.verbs[self.verb_index];
603        self.verb_index += 1;
604
605        let element = match verb {
606            Verb::Move => {
607                let p = self.path.points[self.point_index];
608                self.point_index += 1;
609                PathElement::Move(p)
610            }
611            Verb::Line => {
612                let p = self.path.points[self.point_index];
613                self.point_index += 1;
614                PathElement::Line(p)
615            }
616            Verb::Quad => {
617                let p1 = self.path.points[self.point_index];
618                let p2 = self.path.points[self.point_index + 1];
619                self.point_index += 2;
620                PathElement::Quad(p1, p2)
621            }
622            Verb::Conic => {
623                let p1 = self.path.points[self.point_index];
624                let p2 = self.path.points[self.point_index + 1];
625                let w = self.path.conic_weights[self.weight_index];
626                self.point_index += 2;
627                self.weight_index += 1;
628                PathElement::Conic(p1, p2, w)
629            }
630            Verb::Cubic => {
631                let p1 = self.path.points[self.point_index];
632                let p2 = self.path.points[self.point_index + 1];
633                let p3 = self.path.points[self.point_index + 2];
634                self.point_index += 3;
635                PathElement::Cubic(p1, p2, p3)
636            }
637            Verb::Close => PathElement::Close,
638        };
639
640        Some(element)
641    }
642}