Skip to main content

skia_rs_path/
path.rs

1//! Path data structure and iteration.
2
3use skia_rs_core::{Point, Rect, Scalar};
4use smallvec::SmallVec;
5use std::sync::atomic::{AtomicU8, Ordering};
6
7/// Path fill type.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
9#[repr(u8)]
10pub enum FillType {
11    /// Non-zero winding rule.
12    #[default]
13    Winding = 0,
14    /// Even-odd rule.
15    EvenOdd,
16    /// Inverse non-zero winding.
17    InverseWinding,
18    /// Inverse even-odd.
19    InverseEvenOdd,
20}
21
22impl FillType {
23    /// Check if this is an inverse fill type.
24    #[inline]
25    pub const fn is_inverse(&self) -> bool {
26        matches!(self, FillType::InverseWinding | FillType::InverseEvenOdd)
27    }
28
29    /// Convert to the inverse fill type.
30    #[inline]
31    pub const fn inverse(&self) -> Self {
32        match self {
33            FillType::Winding => FillType::InverseWinding,
34            FillType::EvenOdd => FillType::InverseEvenOdd,
35            FillType::InverseWinding => FillType::Winding,
36            FillType::InverseEvenOdd => FillType::EvenOdd,
37        }
38    }
39}
40
41/// Path verb (command type).
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43#[repr(u8)]
44pub enum Verb {
45    /// Move to a point.
46    Move = 0,
47    /// Line to a point.
48    Line,
49    /// Quadratic bezier.
50    Quad,
51    /// Conic (weighted quadratic).
52    Conic,
53    /// Cubic bezier.
54    Cubic,
55    /// Close the current contour.
56    Close,
57}
58
59impl Verb {
60    /// Number of points consumed by this verb.
61    #[inline]
62    pub const fn point_count(&self) -> usize {
63        match self {
64            Verb::Move | Verb::Line => 1,
65            Verb::Quad | Verb::Conic => 2,
66            Verb::Cubic => 3,
67            Verb::Close => 0,
68        }
69    }
70}
71
72/// Path direction (clockwise or counter-clockwise).
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
74#[repr(u8)]
75pub enum PathDirection {
76    /// Clockwise direction.
77    #[default]
78    CW = 0,
79    /// Counter-clockwise direction.
80    CCW,
81}
82
83/// Path convexity.
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
85#[repr(u8)]
86pub enum PathConvexity {
87    /// Unknown convexity.
88    #[default]
89    Unknown = 0,
90    /// Path is convex.
91    Convex = 1,
92    /// Path is concave.
93    Concave = 2,
94}
95
96impl PathConvexity {
97    fn from_u8(v: u8) -> Self {
98        match v {
99            1 => PathConvexity::Convex,
100            2 => PathConvexity::Concave,
101            _ => PathConvexity::Unknown,
102        }
103    }
104}
105
106/// A 2D geometric path.
107#[derive(Debug)]
108pub struct Path {
109    /// Path verbs.
110    pub(crate) verbs: SmallVec<[Verb; 16]>,
111    /// Path points.
112    pub(crate) points: SmallVec<[Point; 32]>,
113    /// Conic weights.
114    pub(crate) conic_weights: SmallVec<[Scalar; 4]>,
115    /// Fill type.
116    pub(crate) fill_type: FillType,
117    /// Cached bounds (lazily computed).
118    pub(crate) bounds: Option<Rect>,
119    /// Cached convexity (stored as u8 for Send+Sync via AtomicU8).
120    pub(crate) convexity: AtomicU8,
121}
122
123impl Default for Path {
124    fn default() -> Self {
125        Self {
126            verbs: SmallVec::new(),
127            points: SmallVec::new(),
128            conic_weights: SmallVec::new(),
129            fill_type: FillType::default(),
130            bounds: None,
131            convexity: AtomicU8::new(PathConvexity::Unknown as u8),
132        }
133    }
134}
135
136impl Clone for Path {
137    fn clone(&self) -> Self {
138        Self {
139            verbs: self.verbs.clone(),
140            points: self.points.clone(),
141            conic_weights: self.conic_weights.clone(),
142            fill_type: self.fill_type,
143            bounds: self.bounds,
144            convexity: AtomicU8::new(self.convexity.load(Ordering::Relaxed)),
145        }
146    }
147}
148
149impl PartialEq for Path {
150    fn eq(&self, other: &Self) -> bool {
151        self.verbs == other.verbs
152            && self.points == other.points
153            && self.conic_weights == other.conic_weights
154            && self.fill_type == other.fill_type
155    }
156}
157
158#[inline]
159fn axis_of(p: Point, axis: usize) -> Scalar {
160    if axis == 0 { p.x } else { p.y }
161}
162
163#[inline]
164fn record_axis_bound(
165    axis: usize,
166    val: Scalar,
167    min_x: &mut Scalar,
168    max_x: &mut Scalar,
169    min_y: &mut Scalar,
170    max_y: &mut Scalar,
171) {
172    if axis == 0 {
173        if val < *min_x { *min_x = val; }
174        if val > *max_x { *max_x = val; }
175    } else {
176        if val < *min_y { *min_y = val; }
177        if val > *max_y { *max_y = val; }
178    }
179}
180
181impl Path {
182    /// Create a new empty path.
183    #[inline]
184    pub fn new() -> Self {
185        Self::default()
186    }
187
188    /// Get the fill type.
189    #[inline]
190    pub fn fill_type(&self) -> FillType {
191        self.fill_type
192    }
193
194    /// Set the fill type.
195    #[inline]
196    pub fn set_fill_type(&mut self, fill_type: FillType) {
197        self.fill_type = fill_type;
198    }
199
200    /// Check if the path is empty.
201    #[inline]
202    pub fn is_empty(&self) -> bool {
203        self.verbs.is_empty()
204    }
205
206    /// Get the number of verbs.
207    #[inline]
208    pub fn verb_count(&self) -> usize {
209        self.verbs.len()
210    }
211
212    /// Get the number of points.
213    #[inline]
214    pub fn point_count(&self) -> usize {
215        self.points.len()
216    }
217
218    /// Get the bounds of the path.
219    pub fn bounds(&self) -> Rect {
220        if let Some(bounds) = self.bounds {
221            return bounds;
222        }
223
224        if self.points.is_empty() {
225            return Rect::EMPTY;
226        }
227
228        let mut min_x = self.points[0].x;
229        let mut min_y = self.points[0].y;
230        let mut max_x = min_x;
231        let mut max_y = min_y;
232
233        for p in &self.points[1..] {
234            min_x = min_x.min(p.x);
235            min_y = min_y.min(p.y);
236            max_x = max_x.max(p.x);
237            max_y = max_y.max(p.y);
238        }
239
240        Rect::new(min_x, min_y, max_x, max_y)
241    }
242
243    /// Clear the path.
244    #[inline]
245    pub fn reset(&mut self) {
246        self.verbs.clear();
247        self.points.clear();
248        self.conic_weights.clear();
249        self.bounds = None;
250    }
251
252    /// Iterate over the path elements.
253    pub fn iter(&self) -> PathIter<'_> {
254        PathIter {
255            path: self,
256            verb_index: 0,
257            point_index: 0,
258            weight_index: 0,
259        }
260    }
261
262    /// Get the verbs slice.
263    #[inline]
264    pub fn verbs(&self) -> &[Verb] {
265        &self.verbs
266    }
267
268    /// Get the points slice.
269    #[inline]
270    pub fn points(&self) -> &[Point] {
271        &self.points
272    }
273
274    /// Get the last point in the path.
275    #[inline]
276    pub fn last_point(&self) -> Option<Point> {
277        self.points.last().copied()
278    }
279
280    /// Get the number of contours in the path.
281    pub fn contour_count(&self) -> usize {
282        self.verbs.iter().filter(|v| **v == Verb::Move).count()
283    }
284
285    /// Check if the path is closed.
286    pub fn is_closed(&self) -> bool {
287        self.verbs.last() == Some(&Verb::Close)
288    }
289
290    /// Check if the path represents a line.
291    pub fn is_line(&self) -> bool {
292        self.verbs.len() == 2 && self.verbs[0] == Verb::Move && self.verbs[1] == Verb::Line
293    }
294
295    /// Check if the path represents a rectangle.
296    pub fn is_rect(&self) -> Option<Rect> {
297        // A rectangle has: Move, Line, Line, Line, Line, Close (or just 4 lines)
298        if self.verbs.len() < 5 {
299            return None;
300        }
301
302        let mut line_count = 0;
303        let mut has_close = false;
304
305        for verb in &self.verbs {
306            match verb {
307                Verb::Move => {}
308                Verb::Line => line_count += 1,
309                Verb::Close => has_close = true,
310                _ => return None,
311            }
312        }
313
314        if line_count != 4 || !has_close {
315            return None;
316        }
317
318        // Check if points form a rectangle
319        if self.points.len() < 5 {
320            return None;
321        }
322
323        let p0 = self.points[0];
324        let p1 = self.points[1];
325        let p2 = self.points[2];
326        let p3 = self.points[3];
327        let p4 = self.points[4];
328
329        // Check for axis-aligned rectangle
330        let is_horizontal_1 = (p0.y - p1.y).abs() < 0.001;
331        let is_vertical_2 = (p1.x - p2.x).abs() < 0.001;
332        let is_horizontal_3 = (p2.y - p3.y).abs() < 0.001;
333        let is_vertical_4 = (p3.x - p4.x).abs() < 0.001;
334
335        if is_horizontal_1 && is_vertical_2 && is_horizontal_3 && is_vertical_4 {
336            let left = p0.x.min(p1.x).min(p2.x).min(p3.x);
337            let top = p0.y.min(p1.y).min(p2.y).min(p3.y);
338            let right = p0.x.max(p1.x).max(p2.x).max(p3.x);
339            let bottom = p0.y.max(p1.y).max(p2.y).max(p3.y);
340            return Some(Rect::new(left, top, right, bottom));
341        }
342
343        None
344    }
345
346    /// Returns true if this path is a simple oval (ellipse).
347    ///
348    /// Verifies both verb structure (Move, 4 curves, Close) AND that the
349    /// curve endpoints lie on the cardinal points of the bounding ellipse
350    /// (left, right, top, bottom). This rejects 4-cubic paths that share
351    /// the verb pattern but have arbitrary geometry.
352    pub fn is_oval(&self) -> bool {
353        let elements: Vec<_> = self.iter().collect();
354        if elements.len() != 6 {
355            return false;
356        }
357
358        let start = match elements[0] {
359            PathElement::Move(p) => p,
360            _ => return false,
361        };
362        if !matches!(elements[5], PathElement::Close) {
363            return false;
364        }
365
366        let all_cubic = elements[1..5]
367            .iter()
368            .all(|e| matches!(e, PathElement::Cubic(_, _, _)));
369        let all_conic = elements[1..5]
370            .iter()
371            .all(|e| matches!(e, PathElement::Conic(_, _, _)));
372        if !all_cubic && !all_conic {
373            return false;
374        }
375
376        let bounds = self.bounds();
377        let cx = (bounds.left + bounds.right) * 0.5;
378        let cy = (bounds.top + bounds.bottom) * 0.5;
379        if bounds.right - bounds.left <= 0.0 || bounds.bottom - bounds.top <= 0.0 {
380            return false;
381        }
382
383        let tolerance = ((bounds.right - bounds.left) + (bounds.bottom - bounds.top)) * 1e-4;
384        let on_cardinal = |p: Point| -> bool {
385            let on_h = (p.y - cy).abs() < tolerance
386                && ((p.x - bounds.left).abs() < tolerance
387                    || (p.x - bounds.right).abs() < tolerance);
388            let on_v = (p.x - cx).abs() < tolerance
389                && ((p.y - bounds.top).abs() < tolerance
390                    || (p.y - bounds.bottom).abs() < tolerance);
391            on_h || on_v
392        };
393
394        if !on_cardinal(start) {
395            return false;
396        }
397
398        for elem in &elements[1..5] {
399            let end = match *elem {
400                PathElement::Cubic(_, _, p) => p,
401                PathElement::Conic(_, p, _) => p,
402                _ => return false,
403            };
404            if !on_cardinal(end) {
405                return false;
406            }
407        }
408
409        true
410    }
411
412    /// Get the convexity of the path.
413    pub fn convexity(&self) -> PathConvexity {
414        let cached = PathConvexity::from_u8(self.convexity.load(Ordering::Relaxed));
415        if cached != PathConvexity::Unknown {
416            return cached;
417        }
418
419        // Simple convexity check based on cross product signs
420        if self.points.len() < 3 {
421            let result = PathConvexity::Convex;
422            self.convexity.store(result as u8, Ordering::Relaxed);
423            return result;
424        }
425
426        let mut sign = 0i32;
427        let n = self.points.len();
428
429        for i in 0..n {
430            let p0 = self.points[i];
431            let p1 = self.points[(i + 1) % n];
432            let p2 = self.points[(i + 2) % n];
433
434            let cross = (p1.x - p0.x) * (p2.y - p1.y) - (p1.y - p0.y) * (p2.x - p1.x);
435
436            if cross.abs() > 0.001 {
437                let current_sign = if cross > 0.0 { 1 } else { -1 };
438                if sign == 0 {
439                    sign = current_sign;
440                } else if sign != current_sign {
441                    let result = PathConvexity::Concave;
442                    self.convexity.store(result as u8, Ordering::Relaxed);
443                    return result;
444                }
445            }
446        }
447
448        let result = PathConvexity::Convex;
449        self.convexity.store(result as u8, Ordering::Relaxed);
450        result
451    }
452
453    /// Check if the path is convex.
454    #[inline]
455    pub fn is_convex(&self) -> bool {
456        self.convexity() == PathConvexity::Convex
457    }
458
459    /// Get the direction of the first contour.
460    pub fn direction(&self) -> Option<PathDirection> {
461        if self.points.len() < 3 {
462            return None;
463        }
464
465        // Calculate signed area using shoelace formula
466        let mut signed_area = 0.0;
467        let n = self.points.len();
468
469        for i in 0..n {
470            let p0 = self.points[i];
471            let p1 = self.points[(i + 1) % n];
472            signed_area += (p1.x - p0.x) * (p1.y + p0.y);
473        }
474
475        if signed_area.abs() < 0.001 {
476            return None;
477        }
478
479        Some(if signed_area > 0.0 {
480            PathDirection::CW
481        } else {
482            PathDirection::CCW
483        })
484    }
485
486    /// Reverse the path direction.
487    pub fn reverse(&mut self) {
488        if self.verbs.is_empty() {
489            return;
490        }
491
492        // Reverse points
493        self.points.reverse();
494
495        // Reverse conic weights
496        self.conic_weights.reverse();
497
498        // Reverse verbs (keeping structure)
499        // This is a simplified implementation
500        let mut new_verbs = SmallVec::new();
501        let mut i = self.verbs.len();
502
503        while i > 0 {
504            i -= 1;
505            match self.verbs[i] {
506                Verb::Move => {
507                    if !new_verbs.is_empty() {
508                        new_verbs.push(Verb::Close);
509                    }
510                    new_verbs.push(Verb::Move);
511                }
512                Verb::Close => {
513                    // Skip, will be added before next Move
514                }
515                v => new_verbs.push(v),
516            }
517        }
518
519        if !new_verbs.is_empty() && self.is_closed() {
520            new_verbs.push(Verb::Close);
521        }
522
523        self.verbs = new_verbs;
524        self.bounds = None;
525        self.convexity.store(PathConvexity::Unknown as u8, Ordering::Relaxed);
526    }
527
528    /// Transform the path by a matrix.
529    pub fn transform(&mut self, matrix: &skia_rs_core::Matrix) {
530        for point in &mut self.points {
531            *point = matrix.map_point(*point);
532        }
533        self.bounds = None;
534        self.convexity.store(PathConvexity::Unknown as u8, Ordering::Relaxed);
535    }
536
537    /// Create a transformed copy of the path.
538    pub fn transformed(&self, matrix: &skia_rs_core::Matrix) -> Self {
539        let mut result = self.clone();
540        result.transform(matrix);
541        result
542    }
543
544    /// Offset the path by (dx, dy).
545    pub fn offset(&mut self, dx: Scalar, dy: Scalar) {
546        for point in &mut self.points {
547            point.x += dx;
548            point.y += dy;
549        }
550        if let Some(ref mut bounds) = self.bounds {
551            bounds.left += dx;
552            bounds.right += dx;
553            bounds.top += dy;
554            bounds.bottom += dy;
555        }
556    }
557
558    /// Check if a point is inside the path (using fill rule).
559    pub fn contains(&self, point: Point) -> bool {
560        use crate::flatten::{flatten_cubic_adaptive, flatten_conic_adaptive, flatten_quad_adaptive};
561
562        // Ray casting algorithm
563        if !self.bounds().contains(point) {
564            return false;
565        }
566
567        let mut crossings = 0;
568        let mut current = Point::zero();
569        let mut contour_start = Point::zero();
570        const TOL: Scalar = 0.1;
571        let mut pts: Vec<Point> = Vec::with_capacity(32);
572
573        for element in self.iter() {
574            match element {
575                PathElement::Move(p) => {
576                    current = p;
577                    contour_start = p;
578                }
579                PathElement::Line(end) => {
580                    if ray_crosses_segment(point, current, end) {
581                        crossings += 1;
582                    }
583                    current = end;
584                }
585                PathElement::Quad(ctrl, end) => {
586                    pts.clear();
587                    flatten_quad_adaptive(&mut pts, current, ctrl, end, TOL);
588                    let mut prev = current;
589                    for pt in &pts {
590                        if ray_crosses_segment(point, prev, *pt) {
591                            crossings += 1;
592                        }
593                        prev = *pt;
594                    }
595                    current = end;
596                }
597                PathElement::Conic(ctrl, end, w) => {
598                    pts.clear();
599                    flatten_conic_adaptive(&mut pts, current, ctrl, end, w, TOL);
600                    let mut prev = current;
601                    for pt in &pts {
602                        if ray_crosses_segment(point, prev, *pt) {
603                            crossings += 1;
604                        }
605                        prev = *pt;
606                    }
607                    current = end;
608                }
609                PathElement::Cubic(c1, c2, end) => {
610                    pts.clear();
611                    flatten_cubic_adaptive(&mut pts, current, c1, c2, end, TOL);
612                    let mut prev = current;
613                    for pt in &pts {
614                        if ray_crosses_segment(point, prev, *pt) {
615                            crossings += 1;
616                        }
617                        prev = *pt;
618                    }
619                    current = end;
620                }
621                PathElement::Close => {
622                    if ray_crosses_segment(point, current, contour_start) {
623                        crossings += 1;
624                    }
625                    current = contour_start;
626                }
627            }
628        }
629
630        match self.fill_type {
631            FillType::Winding => crossings != 0,
632            FillType::EvenOdd => crossings % 2 != 0,
633            FillType::InverseWinding => crossings == 0,
634            FillType::InverseEvenOdd => crossings % 2 == 0,
635        }
636    }
637
638    /// Returns the tight bounding rectangle of this path.
639    ///
640    /// Unlike `bounds()`, this computes the bounds from the actual curve
641    /// extents, not the control-point bounding box. For cubic and quadratic
642    /// Bezier curves with control points outside the actual curve range,
643    /// tight_bounds() may be significantly smaller than bounds().
644    ///
645    /// Conics fall back to control-polygon bounds (exact extrema for rational
646    /// curves would require solving a quartic).
647    pub fn tight_bounds(&self) -> Rect {
648        if self.verbs.is_empty() {
649            return Rect::EMPTY;
650        }
651
652        let mut min_x = Scalar::INFINITY;
653        let mut min_y = Scalar::INFINITY;
654        let mut max_x = Scalar::NEG_INFINITY;
655        let mut max_y = Scalar::NEG_INFINITY;
656
657        let include = |p: Point, min_x: &mut Scalar, min_y: &mut Scalar, max_x: &mut Scalar, max_y: &mut Scalar| {
658            if p.x < *min_x { *min_x = p.x; }
659            if p.y < *min_y { *min_y = p.y; }
660            if p.x > *max_x { *max_x = p.x; }
661            if p.y > *max_y { *max_y = p.y; }
662        };
663
664        let mut current = Point::new(0.0, 0.0);
665
666        for elem in self.iter() {
667            match elem {
668                PathElement::Move(p) | PathElement::Line(p) => {
669                    include(p, &mut min_x, &mut min_y, &mut max_x, &mut max_y);
670                    current = p;
671                }
672                PathElement::Quad(c, p) => {
673                    include(current, &mut min_x, &mut min_y, &mut max_x, &mut max_y);
674                    include(p, &mut min_x, &mut min_y, &mut max_x, &mut max_y);
675                    // Quadratic extremum per axis: t = (start - ctrl) / (start - 2*ctrl + end)
676                    for axis in 0..2 {
677                        let s = axis_of(current, axis);
678                        let cv = axis_of(c, axis);
679                        let e = axis_of(p, axis);
680                        let denom = s - 2.0 * cv + e;
681                        if denom.abs() > 1e-9 {
682                            let t = (s - cv) / denom;
683                            if t > 0.0 && t < 1.0 {
684                                let mt = 1.0 - t;
685                                let val = mt * mt * s + 2.0 * mt * t * cv + t * t * e;
686                                record_axis_bound(axis, val, &mut min_x, &mut max_x, &mut min_y, &mut max_y);
687                            }
688                        }
689                    }
690                    current = p;
691                }
692                PathElement::Cubic(c1, c2, p) => {
693                    include(current, &mut min_x, &mut min_y, &mut max_x, &mut max_y);
694                    include(p, &mut min_x, &mut min_y, &mut max_x, &mut max_y);
695                    // Cubic extrema per axis: solve 3*a*t^2 + 2*b*t + c = 0 where
696                    // B'(t) = 3*(1-t)^2*(c1-s) + 6*(1-t)*t*(c2-c1) + 3*t^2*(e-c2)
697                    // Expanding into quadratic: a*t^2 + b*t + cc
698                    for axis in 0..2 {
699                        let s = axis_of(current, axis);
700                        let c1v = axis_of(c1, axis);
701                        let c2v = axis_of(c2, axis);
702                        let e = axis_of(p, axis);
703                        let a = 3.0 * (e - 3.0 * c2v + 3.0 * c1v - s);
704                        let b = 6.0 * (c2v - 2.0 * c1v + s);
705                        let cc = 3.0 * (c1v - s);
706
707                        let mut roots: [Scalar; 2] = [Scalar::NAN, Scalar::NAN];
708                        let mut n_roots = 0;
709
710                        if a.abs() < 1e-9 {
711                            // Linear: b*t + cc = 0
712                            if b.abs() > 1e-9 {
713                                let t = -cc / b;
714                                roots[0] = t;
715                                n_roots = 1;
716                            }
717                        } else {
718                            let disc = b * b - 4.0 * a * cc;
719                            if disc >= 0.0 {
720                                let sqrt_disc = disc.sqrt();
721                                roots[0] = (-b + sqrt_disc) / (2.0 * a);
722                                roots[1] = (-b - sqrt_disc) / (2.0 * a);
723                                n_roots = 2;
724                            }
725                        }
726
727                        for i in 0..n_roots {
728                            let t = roots[i];
729                            if t.is_finite() && t > 0.0 && t < 1.0 {
730                                let mt = 1.0 - t;
731                                let val = mt * mt * mt * s
732                                    + 3.0 * mt * mt * t * c1v
733                                    + 3.0 * mt * t * t * c2v
734                                    + t * t * t * e;
735                                record_axis_bound(axis, val, &mut min_x, &mut max_x, &mut min_y, &mut max_y);
736                            }
737                        }
738                    }
739                    current = p;
740                }
741                PathElement::Conic(c, p, _w) => {
742                    // Rational extrema require quartic solve; use control polygon
743                    // as a correct (if looser) upper bound.
744                    include(current, &mut min_x, &mut min_y, &mut max_x, &mut max_y);
745                    include(c, &mut min_x, &mut min_y, &mut max_x, &mut max_y);
746                    include(p, &mut min_x, &mut min_y, &mut max_x, &mut max_y);
747                    current = p;
748                }
749                PathElement::Close => {}
750            }
751        }
752
753        if min_x == Scalar::INFINITY {
754            return Rect::EMPTY;
755        }
756        Rect::new(min_x, min_y, max_x, max_y)
757    }
758
759    /// Get the total length of the path.
760    pub fn length(&self) -> Scalar {
761        use crate::flatten::{flatten_cubic_adaptive, flatten_conic_adaptive, flatten_quad_adaptive};
762
763        let mut total = 0.0;
764        let mut current = Point::zero();
765        let mut contour_start = Point::zero();
766        const TOL: Scalar = 0.25;
767        let mut pts: Vec<Point> = Vec::with_capacity(32);
768
769        for element in self.iter() {
770            match element {
771                PathElement::Move(p) => {
772                    current = p;
773                    contour_start = p;
774                }
775                PathElement::Line(end) => {
776                    total += current.distance(&end);
777                    current = end;
778                }
779                PathElement::Quad(ctrl, end) => {
780                    pts.clear();
781                    flatten_quad_adaptive(&mut pts, current, ctrl, end, TOL);
782                    let mut prev = current;
783                    for pt in &pts {
784                        total += prev.distance(pt);
785                        prev = *pt;
786                    }
787                    current = end;
788                }
789                PathElement::Conic(ctrl, end, w) => {
790                    pts.clear();
791                    flatten_conic_adaptive(&mut pts, current, ctrl, end, w, TOL);
792                    let mut prev = current;
793                    for pt in &pts {
794                        total += prev.distance(pt);
795                        prev = *pt;
796                    }
797                    current = end;
798                }
799                PathElement::Cubic(c1, c2, end) => {
800                    pts.clear();
801                    flatten_cubic_adaptive(&mut pts, current, c1, c2, end, TOL);
802                    let mut prev = current;
803                    for pt in &pts {
804                        total += prev.distance(pt);
805                        prev = *pt;
806                    }
807                    current = end;
808                }
809                PathElement::Close => {
810                    total += current.distance(&contour_start);
811                    current = contour_start;
812                }
813            }
814        }
815
816        total
817    }
818}
819
820/// Check if a horizontal ray from point crosses the segment.
821fn ray_crosses_segment(point: Point, p0: Point, p1: Point) -> bool {
822    // Ensure p0 is below p1
823    let (p0, p1) = if p0.y <= p1.y { (p0, p1) } else { (p1, p0) };
824
825    // Check if point is in y-range of segment
826    if point.y < p0.y || point.y >= p1.y {
827        return false;
828    }
829
830    // Calculate x-coordinate of intersection
831    let t = (point.y - p0.y) / (p1.y - p0.y);
832    let x_intersect = p0.x + t * (p1.x - p0.x);
833
834    x_intersect > point.x
835}
836
837/// A path element from iteration.
838#[derive(Debug, Clone, Copy, PartialEq)]
839pub enum PathElement {
840    /// Move to point.
841    Move(Point),
842    /// Line to point.
843    Line(Point),
844    /// Quadratic bezier (control, end).
845    Quad(Point, Point),
846    /// Conic (control, end, weight).
847    Conic(Point, Point, Scalar),
848    /// Cubic bezier (control1, control2, end).
849    Cubic(Point, Point, Point),
850    /// Close the path.
851    Close,
852}
853
854/// Iterator over path elements.
855pub struct PathIter<'a> {
856    path: &'a Path,
857    verb_index: usize,
858    point_index: usize,
859    weight_index: usize,
860}
861
862impl<'a> Iterator for PathIter<'a> {
863    type Item = PathElement;
864
865    fn next(&mut self) -> Option<Self::Item> {
866        if self.verb_index >= self.path.verbs.len() {
867            return None;
868        }
869
870        let verb = self.path.verbs[self.verb_index];
871        self.verb_index += 1;
872
873        let element = match verb {
874            Verb::Move => {
875                let p = self.path.points[self.point_index];
876                self.point_index += 1;
877                PathElement::Move(p)
878            }
879            Verb::Line => {
880                let p = self.path.points[self.point_index];
881                self.point_index += 1;
882                PathElement::Line(p)
883            }
884            Verb::Quad => {
885                let p1 = self.path.points[self.point_index];
886                let p2 = self.path.points[self.point_index + 1];
887                self.point_index += 2;
888                PathElement::Quad(p1, p2)
889            }
890            Verb::Conic => {
891                let p1 = self.path.points[self.point_index];
892                let p2 = self.path.points[self.point_index + 1];
893                let w = self.path.conic_weights[self.weight_index];
894                self.point_index += 2;
895                self.weight_index += 1;
896                PathElement::Conic(p1, p2, w)
897            }
898            Verb::Cubic => {
899                let p1 = self.path.points[self.point_index];
900                let p2 = self.path.points[self.point_index + 1];
901                let p3 = self.path.points[self.point_index + 2];
902                self.point_index += 3;
903                PathElement::Cubic(p1, p2, p3)
904            }
905            Verb::Close => PathElement::Close,
906        };
907
908        Some(element)
909    }
910}
911
912#[cfg(test)]
913mod tests {
914    use super::*;
915    use crate::PathBuilder;
916
917    #[test]
918    fn test_is_oval_true_for_actual_oval() {
919        let mut builder = PathBuilder::new();
920        builder.add_oval(&Rect::new(0.0, 0.0, 100.0, 50.0));
921        let path = builder.build();
922        assert!(path.is_oval(), "add_oval result should report is_oval=true");
923    }
924
925    #[test]
926    fn test_is_oval_false_for_random_cubics() {
927        // 4 cubics with arbitrary control points — should NOT be detected.
928        let mut builder = PathBuilder::new();
929        builder.move_to(0.0, 0.0);
930        builder.cubic_to(10.0, 0.0, 20.0, 5.0, 30.0, 10.0);
931        builder.cubic_to(40.0, 15.0, 50.0, 20.0, 60.0, 25.0);
932        builder.cubic_to(70.0, 30.0, 80.0, 35.0, 90.0, 40.0);
933        builder.cubic_to(95.0, 45.0, 100.0, 47.0, 0.0, 0.0);
934        builder.close();
935        let path = builder.build();
936        assert!(!path.is_oval(), "Random 4-cubic path should not be detected as oval");
937    }
938
939    #[test]
940    fn test_path_convexity_returns_consistent_result() {
941        let mut builder = PathBuilder::new();
942        builder.move_to(0.0, 0.0);
943        builder.line_to(10.0, 0.0);
944        builder.line_to(10.0, 10.0);
945        builder.line_to(0.0, 10.0);
946        builder.close();
947        let path = builder.build();
948
949        let c1 = path.convexity();
950        let c2 = path.convexity();
951        assert_eq!(c1, c2);
952    }
953
954    #[test]
955    fn test_tight_bounds_smaller_than_bounds_for_curves() {
956        // A cubic Bezier where control points extend far beyond the actual curve.
957        // The "loose" bounds (bounds()) include control points; tight should be tighter.
958        let mut builder = PathBuilder::new();
959        builder.move_to(0.0, 0.0);
960        builder.cubic_to(50.0, 100.0, 50.0, -100.0, 100.0, 0.0);
961        let path = builder.build();
962
963        let loose = path.bounds();
964        let tight = path.tight_bounds();
965
966        // Loose bounds include y=±100 control points
967        assert!(
968            loose.top <= -99.0 || loose.bottom >= 99.0,
969            "Loose bounds should include control points (top={}, bottom={})",
970            loose.top, loose.bottom
971        );
972
973        // Tight bounds should be strictly tighter on at least one of top/bottom
974        assert!(
975            tight.top > loose.top || tight.bottom < loose.bottom,
976            "Tight bounds should be tighter than loose for this off-axis cubic"
977        );
978
979        // For this symmetric S-cubic, actual extrema are approximately ±19.245
980        assert!(
981            tight.bottom < 30.0 && tight.top > -30.0,
982            "Tight bounds should reflect actual curve range (got top={}, bottom={})",
983            tight.top, tight.bottom
984        );
985    }
986
987    #[test]
988    fn test_tight_bounds_same_as_bounds_for_lines() {
989        // For line-only paths, tight_bounds should equal bounds
990        let mut builder = PathBuilder::new();
991        builder.move_to(10.0, 20.0);
992        builder.line_to(30.0, 40.0);
993        let path = builder.build();
994
995        let loose = path.bounds();
996        let tight = path.tight_bounds();
997        assert!((loose.left - tight.left).abs() < 1e-4);
998        assert!((loose.right - tight.right).abs() < 1e-4);
999        assert!((loose.top - tight.top).abs() < 1e-4);
1000        assert!((loose.bottom - tight.bottom).abs() < 1e-4);
1001    }
1002
1003    #[test]
1004    fn test_length_quarter_circle_close_to_pi_over_2() {
1005        // Quarter-circle arc as a conic with weight sqrt(2)/2.
1006        // Radius 1, circumference of full circle = 2π, quarter = π/2 ≈ 1.5708.
1007        let mut builder = PathBuilder::new();
1008        builder.move_to(1.0, 0.0);
1009        builder.conic_to(1.0, 1.0, 0.0, 1.0, std::f32::consts::FRAC_1_SQRT_2);
1010        let path = builder.build();
1011        let len = path.length();
1012        let expected = std::f32::consts::FRAC_PI_2;
1013        assert!(
1014            (len - expected).abs() < 0.05,
1015            "expected ~π/2 = {}, got {}",
1016            expected,
1017            len
1018        );
1019    }
1020
1021    #[test]
1022    fn test_length_tight_cubic_less_than_control_polygon() {
1023        // Straight-line cubic: all 4 control points collinear. Length = chord length.
1024        // Control polygon length = 3x chord length. Old impl would return 3x.
1025        let mut builder = PathBuilder::new();
1026        builder.move_to(0.0, 0.0);
1027        builder.cubic_to(1.0, 0.0, 2.0, 0.0, 3.0, 0.0);
1028        let path = builder.build();
1029        let len = path.length();
1030        assert!(
1031            (len - 3.0).abs() < 0.1,
1032            "expected 3.0, got {}",
1033            len
1034        );
1035    }
1036
1037    #[test]
1038    fn test_contains_conic_honors_weight() {
1039        // A quarter-circle conic + closing lines forms a filled quarter-disk.
1040        // A point inside the arc but outside the control triangle should still be contained.
1041        let mut builder = PathBuilder::new();
1042        builder.move_to(1.0, 0.0);
1043        builder.conic_to(1.0, 1.0, 0.0, 1.0, std::f32::consts::FRAC_1_SQRT_2);
1044        builder.line_to(0.0, 0.0);
1045        builder.close();
1046        let path = builder.build();
1047
1048        // Point at (0.7, 0.3) is inside the quarter-circle (r = sqrt(0.49 + 0.09) ≈ 0.76 < 1)
1049        assert!(
1050            path.contains(Point::new(0.7, 0.3)),
1051            "point inside quarter-disk should be contained"
1052        );
1053
1054        // Point at (0.9, 0.9) is outside the arc (r = sqrt(0.81 + 0.81) ≈ 1.27 > 1)
1055        assert!(
1056            !path.contains(Point::new(0.9, 0.9)),
1057            "point outside quarter-disk should not be contained"
1058        );
1059    }
1060}