1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
10#[repr(u8)]
11pub enum FillType {
12 #[default]
14 Winding = 0,
15 EvenOdd,
17 InverseWinding,
19 InverseEvenOdd,
21}
22
23impl FillType {
24 #[inline]
26 #[must_use]
27 pub const fn is_inverse(&self) -> bool {
28 matches!(self, Self::InverseWinding | Self::InverseEvenOdd)
29 }
30
31 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
46#[repr(u8)]
47pub enum Verb {
48 Move = 0,
50 Line,
52 Quad,
54 Conic,
56 Cubic,
58 Close,
60}
61
62impl Verb {
63 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
78#[repr(u8)]
79pub enum PathDirection {
80 #[default]
82 CW = 0,
83 CCW,
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
89#[repr(u8)]
90pub enum PathConvexity {
91 #[default]
93 Unknown = 0,
94 Convex = 1,
96 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#[derive(Debug)]
112pub struct Path {
113 pub(crate) verbs: SmallVec<[Verb; 16]>,
115 pub(crate) points: SmallVec<[Point; 32]>,
117 pub(crate) conic_weights: SmallVec<[Scalar; 4]>,
119 pub(crate) fill_type: FillType,
121 pub(crate) bounds: Option<Rect>,
123 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 #[inline]
196 #[must_use]
197 pub fn new() -> Self {
198 Self::default()
199 }
200
201 #[inline]
203 pub const fn fill_type(&self) -> FillType {
204 self.fill_type
205 }
206
207 #[inline]
209 pub const fn set_fill_type(&mut self, fill_type: FillType) {
210 self.fill_type = fill_type;
211 }
212
213 #[inline]
215 pub fn is_empty(&self) -> bool {
216 self.verbs.is_empty()
217 }
218
219 #[inline]
221 pub fn verb_count(&self) -> usize {
222 self.verbs.len()
223 }
224
225 #[inline]
227 pub fn point_count(&self) -> usize {
228 self.points.len()
229 }
230
231 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 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 #[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 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 #[inline]
284 pub fn verbs(&self) -> &[Verb] {
285 &self.verbs
286 }
287
288 #[inline]
290 pub fn points(&self) -> &[Point] {
291 &self.points
292 }
293
294 #[inline]
296 pub fn last_point(&self) -> Option<Point> {
297 self.points.last().copied()
298 }
299
300 pub fn contour_count(&self) -> usize {
302 self.verbs.iter().filter(|v| **v == Verb::Move).count()
303 }
304
305 pub fn is_closed(&self) -> bool {
307 self.verbs.last() == Some(&Verb::Close)
308 }
309
310 pub fn is_line(&self) -> bool {
312 self.verbs.len() == 2 && self.verbs[0] == Verb::Move && self.verbs[1] == Verb::Line
313 }
314
315 pub fn is_rect(&self) -> Option<Rect> {
323 if self.verbs.len() < 4 || self.points.len() < 4 {
324 return None;
325 }
326 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 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 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 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; }
428
429 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 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 }
493 }
494 _ => PathConvexity::Convex,
495 }
496 }
497
498 #[inline]
500 pub fn is_convex(&self) -> bool {
501 self.convexity() == PathConvexity::Convex
502 }
503
504 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 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 #[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 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 #[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 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 #[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 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 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 !is_inverse
824 }
825
826 #[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 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 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 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 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 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
1034fn 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
1047fn 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
1063fn 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#[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#[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#[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#[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
1141fn 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 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#[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; }
1207 if !line_end.is_finite() || !line_start.is_finite() {
1208 return None; }
1210 if line_start == line_end {
1211 } 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; } else if auto_close && next_direction == directions[0] {
1222 } 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, }
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; }
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#[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#[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#[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#[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
1356mod 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 #[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 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 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 self.last_vec = cur_vec;
1555 self.reversals += 1;
1556 return self.reversals < 3;
1557 }
1558 DirChange::Unknown => {
1559 return false;
1561 }
1562 DirChange::Invalid => unreachable!("invalid direction change flag"),
1563 }
1564 true
1565 }
1566 }
1567}
1568
1569#[derive(Debug, Clone, Copy, PartialEq)]
1571pub enum PathElement {
1572 Move(Point),
1574 Line(Point),
1576 Quad(Point, Point),
1578 Conic(Point, Point, Scalar),
1580 Cubic(Point, Point, Point),
1582 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
1595pub 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 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 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 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 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 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 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 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 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 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 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 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 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 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 assert_ne!(*verbs.last().unwrap(), Verb::Move, "must not end with Move");
1860 let closes = verbs.iter().filter(|v| **v == Verb::Close).count();
1862 assert_eq!(closes, 1, "exactly one Close preserved");
1863 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 let mut b = PathBuilder::new();
1885 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 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 assert!(path.contains(Point::new(15.0, 15.0)));
1901 path.set_fill_type(FillType::EvenOdd);
1902 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 assert!(path.contains(Point::new(100.0, 100.0)));
1914 assert!(!path.contains(Point::new(5.0, 5.0)));
1916 }
1917
1918 #[test]
1919 fn test_contains_implicit_close_unclosed_triangle() {
1920 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 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 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 assert!(
1946 path.contains(Point::new(0.7, 0.3)),
1947 "point inside quarter-disk should be contained"
1948 );
1949
1950 assert!(
1952 !path.contains(Point::new(0.9, 0.9)),
1953 "point outside quarter-disk should not be contained"
1954 );
1955 }
1956}