Skip to main content

skia_rs_path/
path.rs

1//! Path data structure and iteration.
2
3use skia_rs_core::cast::scalar_from_i32;
4use skia_rs_core::{Point, Rect, Scalar};
5use smallvec::SmallVec;
6use std::sync::atomic::{AtomicU8, Ordering};
7
8/// Path fill type.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
10#[repr(u8)]
11pub enum FillType {
12    /// Non-zero winding rule.
13    #[default]
14    Winding = 0,
15    /// Even-odd rule.
16    EvenOdd,
17    /// Inverse non-zero winding.
18    InverseWinding,
19    /// Inverse even-odd.
20    InverseEvenOdd,
21}
22
23impl FillType {
24    /// Check if this is an inverse fill type.
25    #[inline]
26    #[must_use]
27    pub const fn is_inverse(&self) -> bool {
28        matches!(self, Self::InverseWinding | Self::InverseEvenOdd)
29    }
30
31    /// Convert to the inverse fill type.
32    #[inline]
33    #[must_use]
34    pub const fn inverse(&self) -> Self {
35        match self {
36            Self::Winding => Self::InverseWinding,
37            Self::EvenOdd => Self::InverseEvenOdd,
38            Self::InverseWinding => Self::Winding,
39            Self::InverseEvenOdd => Self::EvenOdd,
40        }
41    }
42}
43
44/// Path verb (command type).
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
46#[repr(u8)]
47pub enum Verb {
48    /// Move to a point.
49    Move = 0,
50    /// Line to a point.
51    Line,
52    /// Quadratic bezier.
53    Quad,
54    /// Conic (weighted quadratic).
55    Conic,
56    /// Cubic bezier.
57    Cubic,
58    /// Close the current contour.
59    Close,
60}
61
62impl Verb {
63    /// Number of points consumed by this verb.
64    #[inline]
65    #[must_use]
66    pub const fn point_count(&self) -> usize {
67        match self {
68            Self::Move | Self::Line => 1,
69            Self::Quad | Self::Conic => 2,
70            Self::Cubic => 3,
71            Self::Close => 0,
72        }
73    }
74}
75
76/// Path direction (clockwise or counter-clockwise).
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
78#[repr(u8)]
79pub enum PathDirection {
80    /// Clockwise direction.
81    #[default]
82    CW = 0,
83    /// Counter-clockwise direction.
84    CCW,
85}
86
87/// Path convexity.
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
89#[repr(u8)]
90pub enum PathConvexity {
91    /// Unknown convexity.
92    #[default]
93    Unknown = 0,
94    /// Path is convex.
95    Convex = 1,
96    /// Path is concave.
97    Concave = 2,
98}
99
100impl PathConvexity {
101    const fn from_u8(v: u8) -> Self {
102        match v {
103            1 => Self::Convex,
104            2 => Self::Concave,
105            _ => Self::Unknown,
106        }
107    }
108}
109
110/// A 2D geometric path.
111#[derive(Debug)]
112pub struct Path {
113    /// Path verbs.
114    pub(crate) verbs: SmallVec<[Verb; 16]>,
115    /// Path points.
116    pub(crate) points: SmallVec<[Point; 32]>,
117    /// Conic weights.
118    pub(crate) conic_weights: SmallVec<[Scalar; 4]>,
119    /// Fill type.
120    pub(crate) fill_type: FillType,
121    /// Cached bounds (lazily computed).
122    pub(crate) bounds: Option<Rect>,
123    /// Cached convexity (stored as u8 for Send+Sync via `AtomicU8`).
124    pub(crate) convexity: AtomicU8,
125}
126
127impl Default for Path {
128    fn default() -> Self {
129        Self {
130            verbs: SmallVec::new(),
131            points: SmallVec::new(),
132            conic_weights: SmallVec::new(),
133            fill_type: FillType::default(),
134            bounds: None,
135            convexity: AtomicU8::new(PathConvexity::Unknown as u8),
136        }
137    }
138}
139
140impl Clone for Path {
141    fn clone(&self) -> Self {
142        Self {
143            verbs: self.verbs.clone(),
144            points: self.points.clone(),
145            conic_weights: self.conic_weights.clone(),
146            fill_type: self.fill_type,
147            bounds: self.bounds,
148            convexity: AtomicU8::new(self.convexity.load(Ordering::Relaxed)),
149        }
150    }
151}
152
153impl PartialEq for Path {
154    fn eq(&self, other: &Self) -> bool {
155        self.verbs == other.verbs
156            && self.points == other.points
157            && self.conic_weights == other.conic_weights
158            && self.fill_type == other.fill_type
159    }
160}
161
162#[inline]
163const fn axis_of(p: Point, axis: usize) -> Scalar {
164    if axis == 0 { p.x } else { p.y }
165}
166
167#[inline]
168fn record_axis_bound(
169    axis: usize,
170    val: Scalar,
171    min_x: &mut Scalar,
172    max_x: &mut Scalar,
173    min_y: &mut Scalar,
174    max_y: &mut Scalar,
175) {
176    if axis == 0 {
177        if val < *min_x {
178            *min_x = val;
179        }
180        if val > *max_x {
181            *max_x = val;
182        }
183    } else {
184        if val < *min_y {
185            *min_y = val;
186        }
187        if val > *max_y {
188            *max_y = val;
189        }
190    }
191}
192
193impl Path {
194    /// Create a new empty path.
195    #[inline]
196    #[must_use]
197    pub fn new() -> Self {
198        Self::default()
199    }
200
201    /// Get the fill type.
202    #[inline]
203    pub const fn fill_type(&self) -> FillType {
204        self.fill_type
205    }
206
207    /// Set the fill type.
208    #[inline]
209    pub const fn set_fill_type(&mut self, fill_type: FillType) {
210        self.fill_type = fill_type;
211    }
212
213    /// Check if the path is empty.
214    #[inline]
215    pub fn is_empty(&self) -> bool {
216        self.verbs.is_empty()
217    }
218
219    /// Get the number of verbs.
220    #[inline]
221    pub fn verb_count(&self) -> usize {
222        self.verbs.len()
223    }
224
225    /// Get the number of points.
226    #[inline]
227    pub fn point_count(&self) -> usize {
228        self.points.len()
229    }
230
231    /// Get the bounds of the path.
232    pub fn bounds(&self) -> Rect {
233        if let Some(bounds) = self.bounds {
234            return bounds;
235        }
236
237        if self.points.is_empty() {
238            return Rect::EMPTY;
239        }
240
241        // Track finiteness like SkPath::isFinite / computeBounds: any non-finite
242        // coordinate yields empty bounds (upstream accumulates min/max and checks
243        // the accumulator for finiteness at the end).
244        let mut min_x = self.points[0].x;
245        let mut min_y = self.points[0].y;
246        let mut max_x = min_x;
247        let mut max_y = min_y;
248
249        for p in &self.points[1..] {
250            min_x = min_x.min(p.x);
251            min_y = min_y.min(p.y);
252            max_x = max_x.max(p.x);
253            max_y = max_y.max(p.y);
254        }
255
256        if !(min_x.is_finite() && min_y.is_finite() && max_x.is_finite() && max_y.is_finite()) {
257            return Rect::EMPTY;
258        }
259
260        Rect::new(min_x, min_y, max_x, max_y)
261    }
262
263    /// Clear the path.
264    #[inline]
265    pub fn reset(&mut self) {
266        self.verbs.clear();
267        self.points.clear();
268        self.conic_weights.clear();
269        self.bounds = None;
270    }
271
272    /// Iterate over the path elements.
273    pub const fn iter(&self) -> PathIter<'_> {
274        PathIter {
275            path: self,
276            verb_index: 0,
277            point_index: 0,
278            weight_index: 0,
279        }
280    }
281
282    /// Get the verbs slice.
283    #[inline]
284    pub fn verbs(&self) -> &[Verb] {
285        &self.verbs
286    }
287
288    /// Get the points slice.
289    #[inline]
290    pub fn points(&self) -> &[Point] {
291        &self.points
292    }
293
294    /// Get the last point in the path.
295    #[inline]
296    pub fn last_point(&self) -> Option<Point> {
297        self.points.last().copied()
298    }
299
300    /// Get the number of contours in the path.
301    pub fn contour_count(&self) -> usize {
302        self.verbs.iter().filter(|v| **v == Verb::Move).count()
303    }
304
305    /// Check if the path is closed.
306    pub fn is_closed(&self) -> bool {
307        self.verbs.last() == Some(&Verb::Close)
308    }
309
310    /// Check if the path represents a line.
311    pub fn is_line(&self) -> bool {
312        self.verbs.len() == 2 && self.verbs[0] == Verb::Move && self.verbs[1] == Verb::Line
313    }
314
315    /// Check if the path represents a rectangle.
316    ///
317    /// Ported from `SkPathPriv::IsRectContour` (single contour, `allowPartial =
318    /// false`) with the `trivial_rect` fast-path: accepts the crate's own
319    /// `add_rect` output (Move + 3 Lines + Close) and rejects non-rectangular
320    /// horizontal/vertical staircases by counting direction changes and
321    /// validating closure.
322    pub fn is_rect(&self) -> Option<Rect> {
323        if self.verbs.len() < 4 || self.points.len() < 4 {
324            return None;
325        }
326        // Only line segments are allowed (kLine segment mask).
327        for v in &self.verbs {
328            if matches!(v, Verb::Quad | Verb::Conic | Verb::Cubic) {
329                return None;
330            }
331        }
332
333        if let Some(r) = trivial_rect(&self.points, &self.verbs) {
334            return Some(r);
335        }
336        is_rect_contour(&self.points, &self.verbs)
337    }
338
339    /// Returns true if this path is a simple oval (ellipse).
340    ///
341    /// Verifies both verb structure (Move, 4 curves, Close) AND that the
342    /// curve endpoints lie on the cardinal points of the bounding ellipse
343    /// (left, right, top, bottom). This rejects 4-cubic paths that share
344    /// the verb pattern but have arbitrary geometry.
345    pub fn is_oval(&self) -> bool {
346        let elements: Vec<_> = self.iter().collect();
347        if elements.len() != 6 {
348            return false;
349        }
350
351        let PathElement::Move(start) = elements[0] else {
352            return false;
353        };
354        if !matches!(elements[5], PathElement::Close) {
355            return false;
356        }
357
358        let all_cubic = elements[1..5]
359            .iter()
360            .all(|e| matches!(e, PathElement::Cubic(_, _, _)));
361        let all_conic = elements[1..5]
362            .iter()
363            .all(|e| matches!(e, PathElement::Conic(_, _, _)));
364        if !all_cubic && !all_conic {
365            return false;
366        }
367
368        let bounds = self.bounds();
369        let cx = (bounds.left + bounds.right) * 0.5;
370        let cy = (bounds.top + bounds.bottom) * 0.5;
371        if bounds.right - bounds.left <= 0.0 || bounds.bottom - bounds.top <= 0.0 {
372            return false;
373        }
374
375        let tolerance = ((bounds.right - bounds.left) + (bounds.bottom - bounds.top)) * 1e-4;
376        let on_cardinal = |p: Point| -> bool {
377            let on_h = (p.y - cy).abs() < tolerance
378                && ((p.x - bounds.left).abs() < tolerance
379                    || (p.x - bounds.right).abs() < tolerance);
380            let on_v = (p.x - cx).abs() < tolerance
381                && ((p.y - bounds.top).abs() < tolerance
382                    || (p.y - bounds.bottom).abs() < tolerance);
383            on_h || on_v
384        };
385
386        if !on_cardinal(start) {
387            return false;
388        }
389
390        for elem in &elements[1..5] {
391            let (PathElement::Cubic(_, _, end) | PathElement::Conic(_, end, _)) = *elem else {
392                return false;
393            };
394            if !on_cardinal(end) {
395                return false;
396            }
397        }
398
399        true
400    }
401
402    /// Get the convexity of the path.
403    ///
404    /// Ported from `SkPathPriv::ComputeConvexity`: per-contour and verb-aware.
405    /// Any second contour is Concave; convexity along a single contour is
406    /// determined by cross-product direction changes (including the closing
407    /// edges) via the `Convexicator`, with no fixed magnitude threshold.
408    pub fn convexity(&self) -> PathConvexity {
409        let cached = PathConvexity::from_u8(self.convexity.load(Ordering::Relaxed));
410        if cached != PathConvexity::Unknown {
411            return cached;
412        }
413
414        let result = self.compute_convexity();
415        self.convexity.store(result as u8, Ordering::Relaxed);
416        result
417    }
418
419    fn compute_convexity(&self) -> PathConvexity {
420        // Count trailing Move verbs to trim (like trim_trailing_moves).
421        let mut vb_count = self.verbs.len();
422        while vb_count > 0 && self.verbs[vb_count - 1] == Verb::Move {
423            vb_count -= 1;
424        }
425        if vb_count == 0 {
426            return PathConvexity::Convex; // degenerate
427        }
428
429        // Quick concave test by counting direction-sign changes.
430        if convex::is_concave_by_sign(&self.points) {
431            return PathConvexity::Concave;
432        }
433
434        let mut contour_count = 0;
435        let mut needs_close = false;
436        let mut state = convex::Convexicator::new();
437
438        for elem in self.iter().take(vb_count) {
439            let is_move = matches!(elem, PathElement::Move(_));
440
441            if contour_count == 0 {
442                if let PathElement::Move(p) = elem {
443                    state.set_move_pt(p);
444                } else {
445                    contour_count += 1;
446                    needs_close = true;
447                }
448            }
449
450            if contour_count == 1 {
451                match elem {
452                    PathElement::Close | PathElement::Move(_) => {
453                        if !state.close() {
454                            return PathConvexity::Concave;
455                        }
456                        needs_close = false;
457                        contour_count += 1;
458                    }
459                    PathElement::Line(p) => {
460                        if !state.add_pt(p) {
461                            return PathConvexity::Concave;
462                        }
463                    }
464                    PathElement::Quad(c, e) | PathElement::Conic(c, e, _) => {
465                        if !state.add_pt(c) || !state.add_pt(e) {
466                            return PathConvexity::Concave;
467                        }
468                    }
469                    PathElement::Cubic(c1, c2, e) => {
470                        if !state.add_pt(c1) || !state.add_pt(c2) || !state.add_pt(e) {
471                            return PathConvexity::Concave;
472                        }
473                    }
474                }
475            } else if contour_count >= 2 && !is_move {
476                // The first contour has closed; any drawing verb means multiple
477                // contours, which cannot be convex.
478                return PathConvexity::Concave;
479            }
480        }
481
482        if needs_close && !state.close() {
483            return PathConvexity::Concave;
484        }
485
486        match state.first_direction() {
487            convex::FirstDir::Unknown => {
488                if state.reversals() >= 3 {
489                    PathConvexity::Concave
490                } else {
491                    PathConvexity::Convex // kConvex_Degenerate
492                }
493            }
494            _ => PathConvexity::Convex,
495        }
496    }
497
498    /// Check if the path is convex.
499    #[inline]
500    pub fn is_convex(&self) -> bool {
501        self.convexity() == PathConvexity::Convex
502    }
503
504    /// Get the first direction of the path.
505    ///
506    /// Ported from `SkPathPriv::ComputeFirstDirection`: loops over all contours
507    /// and keeps the cross product of the contour that contains the global
508    /// y-max (bottom-most point in y-down device space); `cross > 0 => CW`.
509    pub fn direction(&self) -> Option<PathDirection> {
510        if self.points.is_empty() {
511            return None;
512        }
513        let bounds = self.bounds();
514        if bounds == Rect::EMPTY {
515            return None;
516        }
517
518        // initialize with our logical y-min (top)
519        let mut ymax = bounds.top;
520        let mut ymax_cross = 0.0f32;
521
522        for (start, end) in self.contour_point_ranges() {
523            let pts = &self.points[start..end];
524            let n = pts.len();
525            if n < 3 {
526                continue;
527            }
528            let index = find_max_y(pts);
529            if pts[index].y < ymax {
530                continue;
531            }
532
533            // If there is more than 1 distinct point at the y-max, take the
534            // x-min and x-max of them and subtract to compute the direction.
535            #[allow(
536                clippy::float_cmp,
537                reason = "exact y equality mirrors upstream SkPathPriv::ComputeFirstDirection's bitwise comparison"
538            )]
539            let same_y = pts[(index + 1) % n].y == pts[index].y;
540            let cross = if same_y {
541                let (min_index, max_index) = find_min_max_x_at_y(pts, index);
542                if min_index == max_index {
543                    try_crossprod(pts, index)
544                } else {
545                    let min_i32 = i32::try_from(min_index).unwrap_or(i32::MAX);
546                    let max_i32 = i32::try_from(max_index).unwrap_or(i32::MAX);
547                    scalar_from_i32(min_i32) - scalar_from_i32(max_i32)
548                }
549            } else {
550                try_crossprod(pts, index)
551            };
552
553            if cross != 0.0 {
554                ymax = pts[index].y;
555                ymax_cross = cross;
556            }
557        }
558
559        if ymax_cross == 0.0 {
560            None
561        } else if ymax_cross > 0.0 {
562            Some(PathDirection::CW)
563        } else {
564            Some(PathDirection::CCW)
565        }
566    }
567
568    /// Return `(start, end)` point-index ranges, one per contour (Move..next Move).
569    fn contour_point_ranges(&self) -> Vec<(usize, usize)> {
570        let mut ranges = Vec::new();
571        let mut pt = 0usize;
572        let mut contour_start: Option<usize> = None;
573        for &v in &self.verbs {
574            match v {
575                Verb::Move => {
576                    if let Some(s) = contour_start.take() {
577                        ranges.push((s, pt));
578                    }
579                    contour_start = Some(pt);
580                    pt += 1;
581                }
582                Verb::Line => pt += 1,
583                Verb::Quad | Verb::Conic => pt += 2,
584                Verb::Cubic => pt += 3,
585                Verb::Close => {}
586            }
587        }
588        if let Some(s) = contour_start {
589            ranges.push((s, pt));
590        }
591        ranges
592    }
593
594    /// Reverse the path direction.
595    ///
596    /// Ported from `SkPathPriv::ReverseAddPath`: walks contours back-to-front,
597    /// emitting `Move` first, points in reverse order with per-verb point-count
598    /// handling, and preserving one `Close` per originally-closed contour.
599    #[allow(
600        clippy::cast_possible_wrap,
601        clippy::cast_sign_loss,
602        reason = "faithful port of SkPathPriv::ReverseAddPath's signed back-to-front index walk; converting to unsigned arithmetic risks changing underflow/panic behavior"
603    )]
604    pub fn reverse(&mut self) {
605        if self.verbs.is_empty() {
606            return;
607        }
608
609        let mut nv: SmallVec<[Verb; 16]> = SmallVec::new();
610        let mut np: SmallVec<[Point; 32]> = SmallVec::new();
611        let mut nw: SmallVec<[Scalar; 4]> = SmallVec::new();
612
613        let mut p: isize = self.points.len() as isize;
614        let mut wi: isize = self.conic_weights.len() as isize;
615        let mut need_move = true;
616        let mut need_close = false;
617
618        let mut vi = self.verbs.len();
619        while vi > 0 {
620            vi -= 1;
621            let v = self.verbs[vi];
622            let n = v.point_count() as isize;
623
624            if need_move {
625                p -= 1;
626                let mp = self.points[p as usize];
627                nv.push(Verb::Move);
628                np.push(mp);
629                need_move = false;
630            }
631            p -= n;
632            match v {
633                Verb::Move => {
634                    if need_close {
635                        nv.push(Verb::Close);
636                        need_close = false;
637                    }
638                    need_move = true;
639                    p += 1;
640                }
641                Verb::Line => {
642                    nv.push(Verb::Line);
643                    np.push(self.points[p as usize]);
644                }
645                Verb::Quad => {
646                    nv.push(Verb::Quad);
647                    np.push(self.points[(p + 1) as usize]);
648                    np.push(self.points[p as usize]);
649                }
650                Verb::Conic => {
651                    wi -= 1;
652                    nv.push(Verb::Conic);
653                    np.push(self.points[(p + 1) as usize]);
654                    np.push(self.points[p as usize]);
655                    nw.push(self.conic_weights[wi as usize]);
656                }
657                Verb::Cubic => {
658                    nv.push(Verb::Cubic);
659                    np.push(self.points[(p + 2) as usize]);
660                    np.push(self.points[(p + 1) as usize]);
661                    np.push(self.points[p as usize]);
662                }
663                Verb::Close => {
664                    need_close = true;
665                }
666            }
667        }
668        if need_close {
669            nv.push(Verb::Close);
670        }
671
672        self.verbs = nv;
673        self.points = np;
674        self.conic_weights = nw;
675        self.bounds = None;
676        self.convexity
677            .store(PathConvexity::Unknown as u8, Ordering::Relaxed);
678    }
679
680    /// Transform the path by a matrix.
681    pub fn transform(&mut self, matrix: &skia_rs_core::Matrix) {
682        for point in &mut self.points {
683            *point = matrix.map_point(*point);
684        }
685        self.bounds = None;
686        self.convexity
687            .store(PathConvexity::Unknown as u8, Ordering::Relaxed);
688    }
689
690    /// Create a transformed copy of the path.
691    #[must_use]
692    pub fn transformed(&self, matrix: &skia_rs_core::Matrix) -> Self {
693        let mut result = self.clone();
694        result.transform(matrix);
695        result
696    }
697
698    /// Offset the path by (dx, dy).
699    pub fn offset(&mut self, dx: Scalar, dy: Scalar) {
700        for point in &mut self.points {
701            point.x += dx;
702            point.y += dy;
703        }
704        if let Some(ref mut bounds) = self.bounds {
705            bounds.left += dx;
706            bounds.right += dx;
707            bounds.top += dy;
708            bounds.bottom += dy;
709        }
710    }
711
712    /// Check if a point is inside the path (using the fill rule).
713    ///
714    /// Ported from `SkPathPriv::Contains`: accumulates **signed** winding
715    /// (+1/-1 per crossing direction) via `winding_line` on each edge, treats
716    /// every contour as implicitly closed for hit-testing (like
717    /// `SkPathEdgeIter`), uses inclusive bounds for the early-out, and XORs the
718    /// result with the inverse-fill flag.
719    pub fn contains(&self, point: Point) -> bool {
720        use crate::flatten::{
721            flatten_conic_adaptive, flatten_cubic_adaptive, flatten_quad_adaptive,
722        };
723        const TOL: Scalar = 0.1;
724
725        let is_inverse = self.fill_type.is_inverse();
726        if self.is_empty() {
727            return is_inverse;
728        }
729
730        let bounds = self.bounds();
731        if bounds == Rect::EMPTY || !contains_inclusive(&bounds, point) {
732            return is_inverse;
733        }
734
735        let x = point.x;
736        let y = point.y;
737        let mut w = 0i32;
738        let mut on_curve_count = 0i32;
739
740        let mut current = Point::zero();
741        let mut contour_start = Point::zero();
742        let mut needs_close_line = false;
743        let mut pts: Vec<Point> = Vec::with_capacity(32);
744
745        for element in self {
746            match element {
747                PathElement::Move(p) => {
748                    if needs_close_line {
749                        w += winding_line(current, contour_start, x, y, &mut on_curve_count);
750                        needs_close_line = false;
751                    }
752                    current = p;
753                    contour_start = p;
754                }
755                PathElement::Line(end) => {
756                    w += winding_line(current, end, x, y, &mut on_curve_count);
757                    current = end;
758                    needs_close_line = true;
759                }
760                PathElement::Quad(ctrl, end) => {
761                    pts.clear();
762                    flatten_quad_adaptive(&mut pts, current, ctrl, end, TOL);
763                    let mut prev = current;
764                    for pt in &pts {
765                        w += winding_line(prev, *pt, x, y, &mut on_curve_count);
766                        prev = *pt;
767                    }
768                    current = end;
769                    needs_close_line = true;
770                }
771                PathElement::Conic(ctrl, end, weight) => {
772                    pts.clear();
773                    flatten_conic_adaptive(&mut pts, current, ctrl, end, weight, TOL);
774                    let mut prev = current;
775                    for pt in &pts {
776                        w += winding_line(prev, *pt, x, y, &mut on_curve_count);
777                        prev = *pt;
778                    }
779                    current = end;
780                    needs_close_line = true;
781                }
782                PathElement::Cubic(c1, c2, end) => {
783                    pts.clear();
784                    flatten_cubic_adaptive(&mut pts, current, c1, c2, end, TOL);
785                    let mut prev = current;
786                    for pt in &pts {
787                        w += winding_line(prev, *pt, x, y, &mut on_curve_count);
788                        prev = *pt;
789                    }
790                    current = end;
791                    needs_close_line = true;
792                }
793                PathElement::Close => {
794                    if needs_close_line {
795                        w += winding_line(current, contour_start, x, y, &mut on_curve_count);
796                        needs_close_line = false;
797                    }
798                    current = contour_start;
799                }
800            }
801        }
802        if needs_close_line {
803            w += winding_line(current, contour_start, x, y, &mut on_curve_count);
804        }
805
806        let even_odd_fill = matches!(self.fill_type, FillType::EvenOdd | FillType::InverseEvenOdd);
807        if even_odd_fill {
808            w &= 1;
809        }
810        if w != 0 {
811            return !is_inverse;
812        }
813        if on_curve_count <= 1 {
814            return (on_curve_count != 0) ^ is_inverse;
815        }
816        if (on_curve_count & 1) != 0 || even_odd_fill {
817            return ((on_curve_count & 1) != 0) ^ is_inverse;
818        }
819        // Winding fill with the point touching an even number of curves: upstream
820        // resolves coincidence via tangent comparison. With flattened curves an
821        // exact even on-curve hit is essentially unreachable; treat as a boundary
822        // touch (inside for non-inverse fills).
823        !is_inverse
824    }
825
826    /// Returns the tight bounding rectangle of this path.
827    ///
828    /// Unlike `bounds()`, this computes the bounds from the actual curve
829    /// extents, not the control-point bounding box. For cubic and quadratic
830    /// Bezier curves with control points outside the actual curve range,
831    /// `tight_bounds()` may be significantly smaller than `bounds()`.
832    ///
833    /// Conics fall back to control-polygon bounds (exact extrema for rational
834    /// curves would require solving a quartic).
835    #[allow(
836        clippy::too_many_lines,
837        clippy::many_single_char_names,
838        reason = "faithful port of Skia's per-verb curve-extrema tight-bounds computation; short names (s, e, cv, c1v, c2v, a, b, cc) mirror the algebraic derivation"
839    )]
840    pub fn tight_bounds(&self) -> Rect {
841        if self.verbs.is_empty() {
842            return Rect::EMPTY;
843        }
844
845        let mut min_x = Scalar::INFINITY;
846        let mut min_y = Scalar::INFINITY;
847        let mut max_x = Scalar::NEG_INFINITY;
848        let mut max_y = Scalar::NEG_INFINITY;
849
850        let include = |p: Point,
851                       min_x: &mut Scalar,
852                       min_y: &mut Scalar,
853                       max_x: &mut Scalar,
854                       max_y: &mut Scalar| {
855            if p.x < *min_x {
856                *min_x = p.x;
857            }
858            if p.y < *min_y {
859                *min_y = p.y;
860            }
861            if p.x > *max_x {
862                *max_x = p.x;
863            }
864            if p.y > *max_y {
865                *max_y = p.y;
866            }
867        };
868
869        let mut current = Point::new(0.0, 0.0);
870
871        for elem in self {
872            match elem {
873                PathElement::Move(p) | PathElement::Line(p) => {
874                    include(p, &mut min_x, &mut min_y, &mut max_x, &mut max_y);
875                    current = p;
876                }
877                PathElement::Quad(c, p) => {
878                    include(current, &mut min_x, &mut min_y, &mut max_x, &mut max_y);
879                    include(p, &mut min_x, &mut min_y, &mut max_x, &mut max_y);
880                    // Quadratic extremum per axis: t = (start - ctrl) / (start - 2*ctrl + end)
881                    for axis in 0..2 {
882                        let s = axis_of(current, axis);
883                        let cv = axis_of(c, axis);
884                        let e = axis_of(p, axis);
885                        let denom = 2.0f32.mul_add(-cv, s) + e;
886                        if denom.abs() > 1e-9 {
887                            let t = (s - cv) / denom;
888                            if t > 0.0 && t < 1.0 {
889                                let mt = 1.0 - t;
890                                let val =
891                                    (t * t).mul_add(e, (mt * mt).mul_add(s, 2.0 * mt * t * cv));
892                                record_axis_bound(
893                                    axis, val, &mut min_x, &mut max_x, &mut min_y, &mut max_y,
894                                );
895                            }
896                        }
897                    }
898                    current = p;
899                }
900                PathElement::Cubic(c1, c2, p) => {
901                    include(current, &mut min_x, &mut min_y, &mut max_x, &mut max_y);
902                    include(p, &mut min_x, &mut min_y, &mut max_x, &mut max_y);
903                    // Cubic extrema per axis: solve 3*a*t^2 + 2*b*t + c = 0 where
904                    // B'(t) = 3*(1-t)^2*(c1-s) + 6*(1-t)*t*(c2-c1) + 3*t^2*(e-c2)
905                    // Expanding into quadratic: a*t^2 + b*t + cc
906                    for axis in 0..2 {
907                        let s = axis_of(current, axis);
908                        let c1v = axis_of(c1, axis);
909                        let c2v = axis_of(c2, axis);
910                        let e = axis_of(p, axis);
911                        let a = 3.0 * (3.0f32.mul_add(c1v, 3.0f32.mul_add(-c2v, e)) - s);
912                        let b = 6.0 * (2.0f32.mul_add(-c1v, c2v) + s);
913                        let cc = 3.0 * (c1v - s);
914
915                        let mut roots: [Scalar; 2] = [Scalar::NAN, Scalar::NAN];
916                        let mut n_roots = 0;
917
918                        if a.abs() < 1e-9 {
919                            // Linear: b*t + cc = 0
920                            if b.abs() > 1e-9 {
921                                let t = -cc / b;
922                                roots[0] = t;
923                                n_roots = 1;
924                            }
925                        } else {
926                            let disc = b * b - 4.0 * a * cc;
927                            if disc >= 0.0 {
928                                let sqrt_disc = disc.sqrt();
929                                roots[0] = (-b + sqrt_disc) / (2.0 * a);
930                                roots[1] = (-b - sqrt_disc) / (2.0 * a);
931                                n_roots = 2;
932                            }
933                        }
934
935                        for &t in &roots[..n_roots] {
936                            if t.is_finite() && t > 0.0 && t < 1.0 {
937                                let mt = 1.0 - t;
938                                let val = (t * t * t).mul_add(
939                                    e,
940                                    (3.0 * mt * t * t).mul_add(
941                                        c2v,
942                                        (mt * mt * mt).mul_add(s, 3.0 * mt * mt * t * c1v),
943                                    ),
944                                );
945                                record_axis_bound(
946                                    axis, val, &mut min_x, &mut max_x, &mut min_y, &mut max_y,
947                                );
948                            }
949                        }
950                    }
951                    current = p;
952                }
953                PathElement::Conic(c, p, _w) => {
954                    // Rational extrema require quartic solve; use control polygon
955                    // as a correct (if looser) upper bound.
956                    include(current, &mut min_x, &mut min_y, &mut max_x, &mut max_y);
957                    include(c, &mut min_x, &mut min_y, &mut max_x, &mut max_y);
958                    include(p, &mut min_x, &mut min_y, &mut max_x, &mut max_y);
959                    current = p;
960                }
961                PathElement::Close => {}
962            }
963        }
964
965        if min_x == Scalar::INFINITY {
966            return Rect::EMPTY;
967        }
968        Rect::new(min_x, min_y, max_x, max_y)
969    }
970
971    /// Get the total length of the path.
972    pub fn length(&self) -> Scalar {
973        use crate::flatten::{
974            flatten_conic_adaptive, flatten_cubic_adaptive, flatten_quad_adaptive,
975        };
976
977        const TOL: Scalar = 0.25;
978        let mut total = 0.0;
979        let mut current = Point::zero();
980        let mut contour_start = Point::zero();
981        let mut pts: Vec<Point> = Vec::with_capacity(32);
982
983        for element in self {
984            match element {
985                PathElement::Move(p) => {
986                    current = p;
987                    contour_start = p;
988                }
989                PathElement::Line(end) => {
990                    total += current.distance(&end);
991                    current = end;
992                }
993                PathElement::Quad(ctrl, end) => {
994                    pts.clear();
995                    flatten_quad_adaptive(&mut pts, current, ctrl, end, TOL);
996                    let mut prev = current;
997                    for pt in &pts {
998                        total += prev.distance(pt);
999                        prev = *pt;
1000                    }
1001                    current = end;
1002                }
1003                PathElement::Conic(ctrl, end, w) => {
1004                    pts.clear();
1005                    flatten_conic_adaptive(&mut pts, current, ctrl, end, w, TOL);
1006                    let mut prev = current;
1007                    for pt in &pts {
1008                        total += prev.distance(pt);
1009                        prev = *pt;
1010                    }
1011                    current = end;
1012                }
1013                PathElement::Cubic(c1, c2, end) => {
1014                    pts.clear();
1015                    flatten_cubic_adaptive(&mut pts, current, c1, c2, end, TOL);
1016                    let mut prev = current;
1017                    for pt in &pts {
1018                        total += prev.distance(pt);
1019                        prev = *pt;
1020                    }
1021                    current = end;
1022                }
1023                PathElement::Close => {
1024                    total += current.distance(&contour_start);
1025                    current = contour_start;
1026                }
1027            }
1028        }
1029
1030        total
1031    }
1032}
1033
1034/// Returns the first pt with the maximum Y coordinate (ported from `find_max_y`).
1035fn find_max_y(pts: &[Point]) -> usize {
1036    let mut max = pts[0].y;
1037    let mut first_index = 0;
1038    for (i, p) in pts.iter().enumerate().skip(1) {
1039        if p.y > max {
1040            max = p.y;
1041            first_index = i;
1042        }
1043    }
1044    first_index
1045}
1046
1047/// Find a point different from `pts[index]`, moving by `inc` (ported from `find_diff_pt`).
1048fn find_diff_pt(pts: &[Point], index: usize, inc: usize) -> usize {
1049    let n = pts.len();
1050    let mut i = index;
1051    loop {
1052        i = (i + inc) % n;
1053        if i == index {
1054            break;
1055        }
1056        if pts[index] != pts[i] {
1057            break;
1058        }
1059    }
1060    i
1061}
1062
1063/// From `index` moving forward, find the xmin and xmax of contiguous same-Y points.
1064/// Returns `(min_index, max_index)` (ported from `find_min_max_x_at_y`).
1065fn find_min_max_x_at_y(pts: &[Point], index: usize) -> (usize, usize) {
1066    let y = pts[index].y;
1067    let mut min = pts[index].x;
1068    let mut max = min;
1069    let mut min_index = index;
1070    let mut max_index = index;
1071    #[allow(
1072        clippy::float_cmp,
1073        reason = "exact y equality mirrors upstream find_min_max_x_at_y's bitwise comparison"
1074    )]
1075    for (i, p) in pts.iter().enumerate().skip(index + 1) {
1076        if p.y != y {
1077            break;
1078        }
1079        let x = p.x;
1080        if x < min {
1081            min = x;
1082            min_index = i;
1083        } else if x > max {
1084            max = x;
1085            max_index = i;
1086        }
1087    }
1088    (min_index, max_index)
1089}
1090
1091/// cross product of (p1 - p0) and (p2 - p0) (ported from `cross_prod`, f64 promotion).
1092#[allow(
1093    clippy::cast_possible_truncation,
1094    reason = "f64 promotion is deliberately used to recover precision near-zero, then narrowed back to Scalar (f32); this is the whole point of the promotion and matches upstream cross_prod"
1095)]
1096fn cross_prod(p0: Point, p1: Point, p2: Point) -> Scalar {
1097    let cross = (p1.x - p0.x).mul_add(p2.y - p0.y, -((p1.y - p0.y) * (p2.x - p0.x)));
1098    if cross == 0.0 {
1099        let p0x = f64::from(p0.x);
1100        let p0y = f64::from(p0.y);
1101        let p1x = f64::from(p1.x);
1102        let p1y = f64::from(p1.y);
1103        let p2x = f64::from(p2.x);
1104        let p2y = f64::from(p2.y);
1105        return (p1x - p0x).mul_add(p2y - p0y, -((p1y - p0y) * (p2x - p0x))) as Scalar;
1106    }
1107    cross
1108}
1109
1110/// The `TRY_CROSSPROD` path from `ComputeFirstDirection`.
1111#[allow(
1112    clippy::float_cmp,
1113    reason = "exact equality mirrors upstream TRY_CROSSPROD's bitwise comparisons"
1114)]
1115fn try_crossprod(pts: &[Point], index: usize) -> Scalar {
1116    let n = pts.len();
1117    let prev = find_diff_pt(pts, index, n - 1);
1118    if prev == index {
1119        return 0.0;
1120    }
1121    let next = find_diff_pt(pts, index, 1);
1122    let mut cross = cross_prod(pts[prev], pts[index], pts[next]);
1123    if cross == 0.0 && pts[prev].y == pts[index].y && pts[next].y == pts[index].y {
1124        cross = pts[index].x - pts[next].x;
1125    }
1126    cross
1127}
1128
1129/// Ported from `rect_make_dir`: encode an axis-aligned edge direction (0..3).
1130#[inline]
1131fn rect_make_dir(dx: Scalar, dy: Scalar) -> i32 {
1132    i32::from(dx != 0.0) | (i32::from(dx > 0.0 || dy > 0.0) << 1)
1133}
1134
1135/// Build a sorted rect spanning two corner points.
1136#[inline]
1137const fn rect_from_corners(a: Point, b: Point) -> Rect {
1138    Rect::new(a.x.min(b.x), a.y.min(b.y), a.x.max(b.x), a.y.max(b.y))
1139}
1140
1141/// Fast-path port of `trivial_rect`: exactly Move, Line, Line, Line, Close
1142/// over 4 points forming three right corners.
1143fn trivial_rect(pts: &[Point], vbs: &[Verb]) -> Option<Rect> {
1144    const TRIVIAL: [Verb; 5] = [Verb::Move, Verb::Line, Verb::Line, Verb::Line, Verb::Close];
1145    if pts.len() != 4 || vbs.len() != TRIVIAL.len() || vbs != TRIVIAL {
1146        return None;
1147    }
1148    let v0 = Point::new(pts[1].x - pts[0].x, pts[1].y - pts[0].y);
1149    let v1 = Point::new(pts[2].x - pts[1].x, pts[2].y - pts[1].y);
1150    let v2 = Point::new(pts[3].x - pts[2].x, pts[3].y - pts[2].y);
1151    let v3 = Point::new(pts[0].x - pts[3].x, pts[0].y - pts[3].y);
1152
1153    // One vector axis-aligned, and each following vector orthogonal to the last.
1154    let axis_aligned = |a: &Point| (a.x == 0.0) ^ (a.y == 0.0);
1155    let orthogonal =
1156        |a: &Point, b: &Point| ((a.x == 0.0) ^ (b.x == 0.0)) && ((a.y == 0.0) ^ (b.y == 0.0));
1157
1158    if !(axis_aligned(&v0) && orthogonal(&v0, &v1) && orthogonal(&v1, &v2) && orthogonal(&v2, &v3))
1159    {
1160        return None;
1161    }
1162    Some(rect_from_corners(pts[0], pts[2]))
1163}
1164
1165/// General port of `SkPathPriv::IsRectContour` for a single contour
1166/// (`allowPartial = false`).
1167#[allow(
1168    clippy::too_many_lines,
1169    reason = "faithful port of SkPathPriv::IsRectContour's edge-direction state machine"
1170)]
1171fn is_rect_contour(points: &[Point], verbs: &[Verb]) -> Option<Rect> {
1172    let verb_cnt = verbs.len();
1173    let mut pi = 0usize;
1174    let mut curr_verb = 0usize;
1175
1176    let mut corners = 0usize;
1177    let mut line_start = Point::zero();
1178    let mut first_pt = Point::zero();
1179    let mut last_pt = Point::zero();
1180    let mut first_corner = Point::zero();
1181    let mut third_corner = Point::zero();
1182    let mut directions = [-1i32; 5];
1183    let mut closed_or_moved = false;
1184    let mut auto_close = false;
1185
1186    while curr_verb < verb_cnt {
1187        let verb = verbs[curr_verb];
1188        match verb {
1189            Verb::Close | Verb::Line => {
1190                if verb == Verb::Close {
1191                    auto_close = true;
1192                } else {
1193                    last_pt = points[pi];
1194                }
1195                let line_end = if verb == Verb::Close {
1196                    first_pt
1197                } else {
1198                    let p = points[pi];
1199                    pi += 1;
1200                    p
1201                };
1202                let dx = line_end.x - line_start.x;
1203                let dy = line_end.y - line_start.y;
1204                if dx != 0.0 && dy != 0.0 {
1205                    return None; // diagonal
1206                }
1207                if !line_end.is_finite() || !line_start.is_finite() {
1208                    return None; // infinity or NaN
1209                }
1210                if line_start == line_end {
1211                    // single point on side is OK
1212                } else {
1213                    let next_direction = rect_make_dir(dx, dy);
1214                    if corners == 0 {
1215                        directions[0] = next_direction;
1216                        corners = 1;
1217                        closed_or_moved = false;
1218                        line_start = line_end;
1219                    } else if closed_or_moved {
1220                        return None; // closed followed by a line
1221                    } else if auto_close && next_direction == directions[0] {
1222                        // colinear with first
1223                    } else {
1224                        closed_or_moved = auto_close;
1225                        if directions[corners - 1] == next_direction {
1226                            if corners == 3 && verb == Verb::Line {
1227                                third_corner = line_end;
1228                            }
1229                        } else {
1230                            directions[corners] = next_direction;
1231                            corners += 1;
1232                            match corners {
1233                                2 => first_corner = line_start,
1234                                3 => {
1235                                    if (directions[0] ^ directions[2]) != 2 {
1236                                        return None;
1237                                    }
1238                                    third_corner = line_end;
1239                                }
1240                                4 => {
1241                                    if (directions[1] ^ directions[3]) != 2 {
1242                                        return None;
1243                                    }
1244                                }
1245                                _ => return None, // too many direction changes
1246                            }
1247                        }
1248                        line_start = line_end;
1249                    }
1250                }
1251            }
1252            Verb::Quad | Verb::Conic | Verb::Cubic => return None,
1253            Verb::Move => {
1254                if corners == 0 {
1255                    first_pt = points[pi];
1256                } else {
1257                    let cx = first_pt.x - last_pt.x;
1258                    let cy = first_pt.y - last_pt.y;
1259                    if cx != 0.0 && cy != 0.0 {
1260                        return None; // diagonal
1261                    }
1262                }
1263                line_start = points[pi];
1264                pi += 1;
1265                closed_or_moved = true;
1266            }
1267        }
1268        curr_verb += 1;
1269    }
1270
1271    if !(3..=4).contains(&corners) {
1272        return None;
1273    }
1274    let cx = first_pt.x - last_pt.x;
1275    let cy = first_pt.y - last_pt.y;
1276    if cx != 0.0 && cy != 0.0 {
1277        return None;
1278    }
1279    Some(rect_from_corners(first_corner, third_corner))
1280}
1281
1282/// Inclusive point-in-rect test (ported from `contains_inclusive`).
1283#[inline]
1284fn contains_inclusive(r: &Rect, p: Point) -> bool {
1285    r.left <= p.x && p.x <= r.right && r.top <= p.y && p.y <= r.bottom
1286}
1287
1288/// True if `b` lies between `a` and `c` (inclusive), ported from Skia's `between`.
1289#[inline]
1290fn between(a: Scalar, b: Scalar, c: Scalar) -> bool {
1291    (a - b) * (c - b) <= 0.0
1292}
1293
1294#[inline]
1295fn sign_as_int(x: Scalar) -> i32 {
1296    if x < 0.0 { -1 } else { i32::from(x > 0.0) }
1297}
1298
1299/// Ported from `checkOnCurve`.
1300#[inline]
1301#[allow(
1302    clippy::float_cmp,
1303    reason = "exact equality mirrors upstream checkOnCurve's bitwise comparisons"
1304)]
1305fn check_on_curve(x: Scalar, y: Scalar, start: Point, end: Point) -> bool {
1306    if start.y == end.y {
1307        between(start.x, x, end.x) && x != end.x
1308    } else {
1309        x == start.x && y == start.y
1310    }
1311}
1312
1313/// Signed winding contribution of a line edge for the point (x, y).
1314///
1315/// Ported from `winding_line` in `SkPathPriv.cpp`: returns +1/-1 for a crossing
1316/// to the right of the point (sign encodes the edge's y-direction), 0 otherwise,
1317/// and increments `on_curve_count` when the point lies on the edge.
1318#[allow(
1319    clippy::float_cmp,
1320    reason = "exact equality mirrors upstream winding_line's bitwise comparisons"
1321)]
1322fn winding_line(a: Point, b: Point, x: Scalar, y: Scalar, on_curve_count: &mut i32) -> i32 {
1323    let x0 = a.x;
1324    let mut y0 = a.y;
1325    let x1 = b.x;
1326    let mut y1 = b.y;
1327
1328    let dy = y1 - y0;
1329    let (mut dir, swapped) = if y0 > y1 { (-1, true) } else { (1, false) };
1330    if swapped {
1331        std::mem::swap(&mut y0, &mut y1);
1332    }
1333    if y < y0 || y > y1 {
1334        return 0;
1335    }
1336    if check_on_curve(x, y, a, b) {
1337        *on_curve_count += 1;
1338        return 0;
1339    }
1340    if y == y1 {
1341        return 0;
1342    }
1343    let cross = (x1 - x0).mul_add(y - a.y, -(dy * (x - x0)));
1344
1345    if cross == 0.0 {
1346        if x != x1 || y != b.y {
1347            *on_curve_count += 1;
1348        }
1349        dir = 0;
1350    } else if sign_as_int(cross) == dir {
1351        dir = 0;
1352    }
1353    dir
1354}
1355
1356/// Convexity computation, ported from the `Convexicator` in `SkPathPriv.cpp`.
1357mod convex {
1358    use super::sign_as_int;
1359    use skia_rs_core::Point;
1360
1361    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1362    pub enum FirstDir {
1363        Cw,
1364        Ccw,
1365        Unknown,
1366    }
1367
1368    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1369    enum DirChange {
1370        Left,
1371        Right,
1372        Straight,
1373        Backwards,
1374        Unknown,
1375        Invalid,
1376    }
1377
1378    #[inline]
1379    fn vsub(a: Point, b: Point) -> Point {
1380        Point::new(a.x - b.x, a.y - b.y)
1381    }
1382
1383    /// Quick concave test: counts direction-sign changes around the contour.
1384    /// Ported from `Convexicator::IsConcaveBySign`.
1385    #[allow(
1386        clippy::similar_names,
1387        reason = "dxes/dyes and last_sx/last_sy mirror upstream Convexicator field names"
1388    )]
1389    pub fn is_concave_by_sign(pts: &[Point]) -> bool {
1390        let count = pts.len();
1391        if count <= 3 {
1392            return false;
1393        }
1394        let first_pt = pts[0];
1395        let mut curr_pt = pts[0];
1396        let mut dxes = 0i32;
1397        let mut dyes = 0i32;
1398        let mut last_sx = 2i32;
1399        let mut last_sy = 2i32;
1400
1401        let process = |next: Point,
1402                       curr: &mut Point,
1403                       dxes: &mut i32,
1404                       dyes: &mut i32,
1405                       last_sx: &mut i32,
1406                       last_sy: &mut i32|
1407         -> Option<bool> {
1408            let vx = next.x - curr.x;
1409            let vy = next.y - curr.y;
1410            if vx != 0.0 || vy != 0.0 {
1411                if !vx.is_finite() || !vy.is_finite() {
1412                    return Some(true);
1413                }
1414                let sx = i32::from(vx < 0.0);
1415                let sy = i32::from(vy < 0.0);
1416                *dxes += i32::from(sx != *last_sx);
1417                *dyes += i32::from(sy != *last_sy);
1418                if *dxes > 3 || *dyes > 3 {
1419                    return Some(true);
1420                }
1421                *last_sx = sx;
1422                *last_sy = sy;
1423            }
1424            *curr = next;
1425            None
1426        };
1427
1428        for &p in &pts[1..count] {
1429            if let Some(r) = process(
1430                p,
1431                &mut curr_pt,
1432                &mut dxes,
1433                &mut dyes,
1434                &mut last_sx,
1435                &mut last_sy,
1436            ) {
1437                return r;
1438            }
1439        }
1440        // closing edge back to the first point
1441        if let Some(r) = process(
1442            first_pt,
1443            &mut curr_pt,
1444            &mut dxes,
1445            &mut dyes,
1446            &mut last_sx,
1447            &mut last_sy,
1448        ) {
1449            return r;
1450        }
1451        false
1452    }
1453
1454    /// Only valid for a single contour.
1455    pub struct Convexicator {
1456        first_pt: Point,
1457        first_vec: Point,
1458        last_pt: Point,
1459        last_vec: Point,
1460        expected_dir: DirChange,
1461        first_direction: FirstDir,
1462        reversals: i32,
1463    }
1464
1465    impl Convexicator {
1466        pub const fn new() -> Self {
1467            Self {
1468                first_pt: Point::zero(),
1469                first_vec: Point::zero(),
1470                last_pt: Point::zero(),
1471                last_vec: Point::zero(),
1472                expected_dir: DirChange::Invalid,
1473                first_direction: FirstDir::Unknown,
1474                reversals: 0,
1475            }
1476        }
1477
1478        pub const fn first_direction(&self) -> FirstDir {
1479            self.first_direction
1480        }
1481
1482        pub const fn reversals(&self) -> i32 {
1483            self.reversals
1484        }
1485
1486        pub const fn set_move_pt(&mut self, pt: Point) {
1487            self.first_pt = pt;
1488            self.last_pt = pt;
1489            self.expected_dir = DirChange::Invalid;
1490        }
1491
1492        pub fn add_pt(&mut self, pt: Point) -> bool {
1493            if self.last_pt == pt {
1494                return true;
1495            }
1496            if self.first_pt == self.last_pt
1497                && self.expected_dir == DirChange::Invalid
1498                && self.last_vec.is_zero()
1499            {
1500                self.last_vec = vsub(pt, self.last_pt);
1501                self.first_vec = self.last_vec;
1502            } else if !self.add_vec(vsub(pt, self.last_pt)) {
1503                return false;
1504            }
1505            self.last_pt = pt;
1506            true
1507        }
1508
1509        pub fn close(&mut self) -> bool {
1510            let fp = self.first_pt;
1511            let fv = self.first_vec;
1512            self.add_pt(fp) && self.add_vec(fv)
1513        }
1514
1515        fn direction_change(&self, cur_vec: Point) -> DirChange {
1516            let cross = self.last_vec.cross(&cur_vec);
1517            if !cross.is_finite() {
1518                return DirChange::Unknown;
1519            }
1520            if cross == 0.0 {
1521                return if self.last_vec.dot(&cur_vec) < 0.0 {
1522                    DirChange::Backwards
1523                } else {
1524                    DirChange::Straight
1525                };
1526            }
1527            if sign_as_int(cross) == 1 {
1528                DirChange::Right
1529            } else {
1530                DirChange::Left
1531            }
1532        }
1533
1534        fn add_vec(&mut self, cur_vec: Point) -> bool {
1535            let dir = self.direction_change(cur_vec);
1536            match dir {
1537                DirChange::Left | DirChange::Right => {
1538                    if self.expected_dir == DirChange::Invalid {
1539                        self.expected_dir = dir;
1540                        self.first_direction = if dir == DirChange::Right {
1541                            FirstDir::Cw
1542                        } else {
1543                            FirstDir::Ccw
1544                        };
1545                    } else if dir != self.expected_dir {
1546                        self.first_direction = FirstDir::Unknown;
1547                        return false;
1548                    }
1549                    self.last_vec = cur_vec;
1550                }
1551                DirChange::Straight => {}
1552                DirChange::Backwards => {
1553                    // allow the path to reverse direction twice
1554                    self.last_vec = cur_vec;
1555                    self.reversals += 1;
1556                    return self.reversals < 3;
1557                }
1558                DirChange::Unknown => {
1559                    // Non-finite cross product: cannot determine convexity.
1560                    return false;
1561                }
1562                DirChange::Invalid => unreachable!("invalid direction change flag"),
1563            }
1564            true
1565        }
1566    }
1567}
1568
1569/// A path element from iteration.
1570#[derive(Debug, Clone, Copy, PartialEq)]
1571pub enum PathElement {
1572    /// Move to point.
1573    Move(Point),
1574    /// Line to point.
1575    Line(Point),
1576    /// Quadratic bezier (control, end).
1577    Quad(Point, Point),
1578    /// Conic (control, end, weight).
1579    Conic(Point, Point, Scalar),
1580    /// Cubic bezier (control1, control2, end).
1581    Cubic(Point, Point, Point),
1582    /// Close the path.
1583    Close,
1584}
1585
1586impl<'a> IntoIterator for &'a Path {
1587    type Item = PathElement;
1588    type IntoIter = PathIter<'a>;
1589
1590    fn into_iter(self) -> PathIter<'a> {
1591        self.iter()
1592    }
1593}
1594
1595/// Iterator over path elements.
1596pub struct PathIter<'a> {
1597    path: &'a Path,
1598    verb_index: usize,
1599    point_index: usize,
1600    weight_index: usize,
1601}
1602
1603impl Iterator for PathIter<'_> {
1604    type Item = PathElement;
1605
1606    fn next(&mut self) -> Option<Self::Item> {
1607        if self.verb_index >= self.path.verbs.len() {
1608            return None;
1609        }
1610
1611        let verb = self.path.verbs[self.verb_index];
1612        self.verb_index += 1;
1613
1614        let element = match verb {
1615            Verb::Move => {
1616                let p = self.path.points[self.point_index];
1617                self.point_index += 1;
1618                PathElement::Move(p)
1619            }
1620            Verb::Line => {
1621                let p = self.path.points[self.point_index];
1622                self.point_index += 1;
1623                PathElement::Line(p)
1624            }
1625            Verb::Quad => {
1626                let p1 = self.path.points[self.point_index];
1627                let p2 = self.path.points[self.point_index + 1];
1628                self.point_index += 2;
1629                PathElement::Quad(p1, p2)
1630            }
1631            Verb::Conic => {
1632                let p1 = self.path.points[self.point_index];
1633                let p2 = self.path.points[self.point_index + 1];
1634                let w = self.path.conic_weights[self.weight_index];
1635                self.point_index += 2;
1636                self.weight_index += 1;
1637                PathElement::Conic(p1, p2, w)
1638            }
1639            Verb::Cubic => {
1640                let p1 = self.path.points[self.point_index];
1641                let p2 = self.path.points[self.point_index + 1];
1642                let p3 = self.path.points[self.point_index + 2];
1643                self.point_index += 3;
1644                PathElement::Cubic(p1, p2, p3)
1645            }
1646            Verb::Close => PathElement::Close,
1647        };
1648
1649        Some(element)
1650    }
1651}
1652
1653#[cfg(test)]
1654mod tests {
1655    use super::*;
1656    use crate::PathBuilder;
1657
1658    #[test]
1659    fn test_is_oval_true_for_actual_oval() {
1660        let mut builder = PathBuilder::new();
1661        builder.add_oval(&Rect::new(0.0, 0.0, 100.0, 50.0));
1662        let path = builder.build();
1663        assert!(path.is_oval(), "add_oval result should report is_oval=true");
1664    }
1665
1666    #[test]
1667    fn test_is_oval_false_for_random_cubics() {
1668        // 4 cubics with arbitrary control points — should NOT be detected.
1669        let mut builder = PathBuilder::new();
1670        builder.move_to(0.0, 0.0);
1671        builder.cubic_to(10.0, 0.0, 20.0, 5.0, 30.0, 10.0);
1672        builder.cubic_to(40.0, 15.0, 50.0, 20.0, 60.0, 25.0);
1673        builder.cubic_to(70.0, 30.0, 80.0, 35.0, 90.0, 40.0);
1674        builder.cubic_to(95.0, 45.0, 100.0, 47.0, 0.0, 0.0);
1675        builder.close();
1676        let path = builder.build();
1677        assert!(
1678            !path.is_oval(),
1679            "Random 4-cubic path should not be detected as oval"
1680        );
1681    }
1682
1683    #[test]
1684    fn test_path_convexity_returns_consistent_result() {
1685        let mut builder = PathBuilder::new();
1686        builder.move_to(0.0, 0.0);
1687        builder.line_to(10.0, 0.0);
1688        builder.line_to(10.0, 10.0);
1689        builder.line_to(0.0, 10.0);
1690        builder.close();
1691        let path = builder.build();
1692
1693        let c1 = path.convexity();
1694        let c2 = path.convexity();
1695        assert_eq!(c1, c2);
1696    }
1697
1698    #[test]
1699    fn test_tight_bounds_smaller_than_bounds_for_curves() {
1700        // A cubic Bezier where control points extend far beyond the actual curve.
1701        // The "loose" bounds (bounds()) include control points; tight should be tighter.
1702        let mut builder = PathBuilder::new();
1703        builder.move_to(0.0, 0.0);
1704        builder.cubic_to(50.0, 100.0, 50.0, -100.0, 100.0, 0.0);
1705        let path = builder.build();
1706
1707        let loose = path.bounds();
1708        let tight = path.tight_bounds();
1709
1710        // Loose bounds include y=±100 control points
1711        assert!(
1712            loose.top <= -99.0 || loose.bottom >= 99.0,
1713            "Loose bounds should include control points (top={}, bottom={})",
1714            loose.top,
1715            loose.bottom
1716        );
1717
1718        // Tight bounds should be strictly tighter on at least one of top/bottom
1719        assert!(
1720            tight.top > loose.top || tight.bottom < loose.bottom,
1721            "Tight bounds should be tighter than loose for this off-axis cubic"
1722        );
1723
1724        // For this symmetric S-cubic, actual extrema are approximately ±19.245
1725        assert!(
1726            tight.bottom < 30.0 && tight.top > -30.0,
1727            "Tight bounds should reflect actual curve range (got top={}, bottom={})",
1728            tight.top,
1729            tight.bottom
1730        );
1731    }
1732
1733    #[test]
1734    fn test_tight_bounds_same_as_bounds_for_lines() {
1735        // For line-only paths, tight_bounds should equal bounds
1736        let mut builder = PathBuilder::new();
1737        builder.move_to(10.0, 20.0);
1738        builder.line_to(30.0, 40.0);
1739        let path = builder.build();
1740
1741        let loose = path.bounds();
1742        let tight = path.tight_bounds();
1743        assert!((loose.left - tight.left).abs() < 1e-4);
1744        assert!((loose.right - tight.right).abs() < 1e-4);
1745        assert!((loose.top - tight.top).abs() < 1e-4);
1746        assert!((loose.bottom - tight.bottom).abs() < 1e-4);
1747    }
1748
1749    #[test]
1750    fn test_length_quarter_circle_close_to_pi_over_2() {
1751        // Quarter-circle arc as a conic with weight sqrt(2)/2.
1752        // Radius 1, circumference of full circle = 2π, quarter = π/2 ≈ 1.5708.
1753        let mut builder = PathBuilder::new();
1754        builder.move_to(1.0, 0.0);
1755        builder.conic_to(1.0, 1.0, 0.0, 1.0, std::f32::consts::FRAC_1_SQRT_2);
1756        let path = builder.build();
1757        let len = path.length();
1758        let expected = std::f32::consts::FRAC_PI_2;
1759        assert!(
1760            (len - expected).abs() < 0.05,
1761            "expected ~π/2 = {expected}, got {len}"
1762        );
1763    }
1764
1765    #[test]
1766    fn test_length_tight_cubic_less_than_control_polygon() {
1767        // Straight-line cubic: all 4 control points collinear. Length = chord length.
1768        // Control polygon length = 3x chord length. Old impl would return 3x.
1769        let mut builder = PathBuilder::new();
1770        builder.move_to(0.0, 0.0);
1771        builder.cubic_to(1.0, 0.0, 2.0, 0.0, 3.0, 0.0);
1772        let path = builder.build();
1773        let len = path.length();
1774        assert!((len - 3.0).abs() < 0.1, "expected 3.0, got {len}");
1775    }
1776
1777    #[test]
1778    fn test_direction_add_rect_is_cw() {
1779        // add_rect emits CW geometry in y-down device space; must report CW.
1780        let mut b = PathBuilder::new();
1781        b.add_rect(&Rect::new(0.0, 0.0, 10.0, 10.0));
1782        let path = b.build();
1783        assert_eq!(path.direction(), Some(PathDirection::CW));
1784    }
1785
1786    #[test]
1787    fn test_is_rect_accepts_add_rect_output() {
1788        // add_rect emits Move + 3 Lines + Close (the 4th edge is the close).
1789        let mut b = PathBuilder::new();
1790        b.add_rect(&Rect::new(1.0, 2.0, 5.0, 8.0));
1791        let path = b.build();
1792        let r = path
1793            .is_rect()
1794            .expect("add_rect output must be recognized as a rect");
1795        assert!((r.left - 1.0).abs() < 1e-4 && (r.top - 2.0).abs() < 1e-4);
1796        assert!((r.right - 5.0).abs() < 1e-4 && (r.bottom - 8.0).abs() < 1e-4);
1797    }
1798
1799    #[test]
1800    fn test_is_rect_rejects_hv_staircase() {
1801        // A horizontal/vertical staircase has too many direction changes.
1802        let mut b = PathBuilder::new();
1803        b.move_to(0.0, 0.0);
1804        b.line_to(10.0, 0.0);
1805        b.line_to(10.0, 10.0);
1806        b.line_to(20.0, 10.0);
1807        b.line_to(20.0, 20.0);
1808        b.line_to(0.0, 20.0);
1809        b.close();
1810        let path = b.build();
1811        assert_eq!(path.is_rect(), None, "staircase must not be a rect");
1812    }
1813
1814    #[test]
1815    fn test_convexity_two_contours_is_concave() {
1816        let mut b = PathBuilder::new();
1817        b.add_rect(&Rect::new(0.0, 0.0, 10.0, 10.0));
1818        b.add_rect(&Rect::new(20.0, 20.0, 30.0, 30.0));
1819        let path = b.build();
1820        assert_eq!(path.convexity(), PathConvexity::Concave);
1821    }
1822
1823    #[test]
1824    fn test_convexity_single_rect_is_convex() {
1825        let mut b = PathBuilder::new();
1826        b.add_rect(&Rect::new(0.0, 0.0, 10.0, 10.0));
1827        let path = b.build();
1828        assert_eq!(path.convexity(), PathConvexity::Convex);
1829    }
1830
1831    #[test]
1832    fn test_convexity_concave_l_shape() {
1833        // An L-shape is concave.
1834        let mut b = PathBuilder::new();
1835        b.move_to(0.0, 0.0);
1836        b.line_to(20.0, 0.0);
1837        b.line_to(20.0, 10.0);
1838        b.line_to(10.0, 10.0);
1839        b.line_to(10.0, 20.0);
1840        b.line_to(0.0, 20.0);
1841        b.close();
1842        let path = b.build();
1843        assert_eq!(path.convexity(), PathConvexity::Concave);
1844    }
1845
1846    #[test]
1847    fn test_reverse_produces_valid_verb_stream() {
1848        // Reversing must not produce a Move at the end or doubled/spurious Close.
1849        let mut b = PathBuilder::new();
1850        b.move_to(0.0, 0.0);
1851        b.line_to(10.0, 0.0);
1852        b.line_to(10.0, 10.0);
1853        b.close();
1854        let mut path = b.build();
1855        path.reverse();
1856        let verbs = path.verbs();
1857        assert_eq!(verbs[0], Verb::Move, "reversed path must start with Move");
1858        // Move must never appear except as first verb of a contour, never trailing.
1859        assert_ne!(*verbs.last().unwrap(), Verb::Move, "must not end with Move");
1860        // Exactly one Close for the single closed contour.
1861        let closes = verbs.iter().filter(|v| **v == Verb::Close).count();
1862        assert_eq!(closes, 1, "exactly one Close preserved");
1863        // The reversed closed triangle still contains its centroid.
1864        assert!(path.contains(Point::new(6.6, 3.3)));
1865    }
1866
1867    #[test]
1868    fn test_bounds_non_finite_is_empty() {
1869        let mut b = PathBuilder::new();
1870        b.move_to(0.0, 0.0);
1871        b.line_to(Scalar::INFINITY, 10.0);
1872        let path = b.build();
1873        assert_eq!(
1874            path.bounds(),
1875            Rect::EMPTY,
1876            "non-finite path has empty bounds"
1877        );
1878    }
1879
1880    #[test]
1881    fn test_contains_signed_winding_self_overlap() {
1882        // Even-odd vs winding differ on a self-overlapping figure-eight-like path.
1883        // Two nested CCW+CW squares: winding cancels in the inner region.
1884        let mut b = PathBuilder::new();
1885        // outer CW
1886        b.move_to(0.0, 0.0);
1887        b.line_to(30.0, 0.0);
1888        b.line_to(30.0, 30.0);
1889        b.line_to(0.0, 30.0);
1890        b.close();
1891        // inner, same CW winding -> winding rule keeps interior filled
1892        b.move_to(10.0, 10.0);
1893        b.line_to(20.0, 10.0);
1894        b.line_to(20.0, 20.0);
1895        b.line_to(10.0, 20.0);
1896        b.close();
1897        let mut path = b.build();
1898        path.set_fill_type(FillType::Winding);
1899        // both wound same way -> center has winding 2 -> filled
1900        assert!(path.contains(Point::new(15.0, 15.0)));
1901        path.set_fill_type(FillType::EvenOdd);
1902        // even-odd -> center crosses two boundaries -> empty (hole)
1903        assert!(!path.contains(Point::new(15.0, 15.0)));
1904    }
1905
1906    #[test]
1907    fn test_contains_inverse_fill_outside_bounds() {
1908        let mut b = PathBuilder::new();
1909        b.add_rect(&Rect::new(0.0, 0.0, 10.0, 10.0));
1910        let mut path = b.build();
1911        path.set_fill_type(FillType::InverseWinding);
1912        // Point outside bounds is contained for inverse fill.
1913        assert!(path.contains(Point::new(100.0, 100.0)));
1914        // Point inside the rect is NOT contained for inverse fill.
1915        assert!(!path.contains(Point::new(5.0, 5.0)));
1916    }
1917
1918    #[test]
1919    fn test_contains_implicit_close_unclosed_triangle() {
1920        // Triangle without explicit Close must still hit-test as closed.
1921        let mut b = PathBuilder::new();
1922        b.move_to(0.0, 0.0);
1923        b.line_to(10.0, 0.0);
1924        b.line_to(5.0, 10.0);
1925        // no close()
1926        let path = b.build();
1927        assert!(
1928            path.contains(Point::new(5.0, 3.0)),
1929            "implicit closing edge fills the triangle"
1930        );
1931    }
1932
1933    #[test]
1934    fn test_contains_conic_honors_weight() {
1935        // A quarter-circle conic + closing lines forms a filled quarter-disk.
1936        // A point inside the arc but outside the control triangle should still be contained.
1937        let mut builder = PathBuilder::new();
1938        builder.move_to(1.0, 0.0);
1939        builder.conic_to(1.0, 1.0, 0.0, 1.0, std::f32::consts::FRAC_1_SQRT_2);
1940        builder.line_to(0.0, 0.0);
1941        builder.close();
1942        let path = builder.build();
1943
1944        // Point at (0.7, 0.3) is inside the quarter-circle (r = sqrt(0.49 + 0.09) ≈ 0.76 < 1)
1945        assert!(
1946            path.contains(Point::new(0.7, 0.3)),
1947            "point inside quarter-disk should be contained"
1948        );
1949
1950        // Point at (0.9, 0.9) is outside the arc (r = sqrt(0.81 + 0.81) ≈ 1.27 > 1)
1951        assert!(
1952            !path.contains(Point::new(0.9, 0.9)),
1953            "point outside quarter-disk should not be contained"
1954        );
1955    }
1956}