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