1use std::sync::Arc;
2
3use crate::{Matrix, Point, Rect};
4
5#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
7#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
8pub enum FillRule {
9 #[default]
11 NonZero,
12 EvenOdd,
14}
15
16#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
22#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
23pub enum Winding {
24 #[default]
26 Clockwise,
27 CounterClockwise,
29}
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34#[cfg_attr(feature = "serde", derive(serde::Serialize))]
35enum Verb {
36 Move,
37 Line,
38 Quad,
39 Cubic,
40 Close,
41}
42
43#[derive(Clone, Debug, PartialEq)]
49pub struct Contour {
50 pub points: Vec<Point>,
52 pub closed: bool,
54 pub has_segments: bool,
58}
59
60#[derive(Clone, Debug)]
65#[cfg_attr(feature = "serde", derive(serde::Serialize))]
66pub struct Path {
67 verbs: Vec<Verb>,
68 points: Vec<Point>,
69 bounds: Rect,
72}
73
74impl Path {
75 pub fn bounds(&self) -> Rect {
77 self.bounds
78 }
79
80 pub fn tight_bounds(&self) -> Rect {
82 let mut bounds = TightBounds::default();
83 let mut point_index = 0usize;
84 let mut cursor = Point::ZERO;
85 let mut contour_start = Point::ZERO;
86 for verb in &self.verbs {
87 match verb {
88 Verb::Move => {
89 cursor = self.points[point_index];
90 contour_start = cursor;
91 point_index += 1;
92 bounds.include(cursor);
93 }
94 Verb::Line => {
95 cursor = self.points[point_index];
96 point_index += 1;
97 bounds.include(cursor);
98 }
99 Verb::Quad => {
100 let control = self.points[point_index];
101 let end = self.points[point_index + 1];
102 point_index += 2;
103 include_quadratic_extrema(&mut bounds, cursor, control, end);
104 cursor = end;
105 }
106 Verb::Cubic => {
107 let first = self.points[point_index];
108 let second = self.points[point_index + 1];
109 let end = self.points[point_index + 2];
110 point_index += 3;
111 include_cubic_extrema(&mut bounds, cursor, first, second, end);
112 cursor = end;
113 }
114 Verb::Close => {
115 cursor = contour_start;
116 bounds.include(cursor);
117 }
118 }
119 }
120 bounds.rect()
121 }
122
123 pub fn is_empty(&self) -> bool {
125 self.verbs.is_empty()
126 }
127
128 pub fn heap_bytes(&self) -> usize {
130 self.points.len() * std::mem::size_of::<Point>() + self.verbs.len()
131 }
132
133 pub fn contains(&self, point: Point, fill_rule: FillRule) -> bool {
138 if !self.bounds.contains_inclusive(point) {
139 return false;
140 }
141 let crossings = self.walk_crossings(point);
142 match fill_rule {
143 FillRule::NonZero => crossings.is_inside_non_zero(),
144 FillRule::EvenOdd => crossings.is_inside_even_odd(),
145 }
146 }
147
148 fn walk_crossings(&self, point: Point) -> crate::winding::Crossings {
150 let mut crossings = crate::winding::Crossings::default();
151 let mut index = 0usize;
152 let mut cursor = Point::ZERO;
153 let mut contour_start = Point::ZERO;
154 let mut contour_open = false;
155 for verb in &self.verbs {
156 match verb {
157 Verb::Move => {
158 if contour_open {
161 crossings.line(cursor, contour_start, point);
162 }
163 contour_open = true;
164 contour_start = self.points[index];
165 cursor = contour_start;
166 index += 1;
167 }
168 Verb::Line => {
169 crossings.line(cursor, self.points[index], point);
170 cursor = self.points[index];
171 index += 1;
172 }
173 Verb::Quad => {
174 crossings.quad(cursor, self.points[index], self.points[index + 1], point);
175 cursor = self.points[index + 1];
176 index += 2;
177 }
178 Verb::Cubic => {
179 crossings.cubic(
180 cursor,
181 self.points[index],
182 self.points[index + 1],
183 self.points[index + 2],
184 point,
185 );
186 cursor = self.points[index + 2];
187 index += 3;
188 }
189 Verb::Close => {
190 crossings.line(cursor, contour_start, point);
191 cursor = contour_start;
192 contour_open = false;
193 }
194 }
195 }
196 if contour_open {
197 crossings.line(cursor, contour_start, point);
198 }
199 crossings
200 }
201
202 pub fn measure(&self, tolerance: f32) -> Vec<crate::ContourMeasure> {
206 self.flatten(tolerance)
207 .iter()
208 .filter_map(crate::ContourMeasure::of)
209 .collect()
210 }
211
212 pub fn flatten(&self, tolerance: f32) -> Vec<Contour> {
218 let mut out = Flattener::new(tolerance.max(1e-4));
219 let mut i = 0usize;
220 for verb in &self.verbs {
221 match verb {
222 Verb::Move => {
223 out.move_to(self.points[i]);
224 i += 1;
225 }
226 Verb::Line => {
227 out.line_to(self.points[i]);
228 i += 1;
229 }
230 Verb::Quad => {
231 out.quad_to(self.points[i], self.points[i + 1]);
232 i += 2;
233 }
234 Verb::Cubic => {
235 out.cubic_to(self.points[i], self.points[i + 1], self.points[i + 2]);
236 i += 3;
237 }
238 Verb::Close => out.close(),
239 }
240 }
241 out.finish()
242 }
243}
244
245#[derive(Default)]
246struct TightBounds(Option<(f32, f32, f32, f32)>);
247
248impl TightBounds {
249 fn include(&mut self, point: Point) {
250 self.0 = Some(match self.0 {
251 Some((left, top, right, bottom)) => (
252 left.min(point.x),
253 top.min(point.y),
254 right.max(point.x),
255 bottom.max(point.y),
256 ),
257 None => (point.x, point.y, point.x, point.y),
258 });
259 }
260
261 fn rect(self) -> Rect {
262 self.0
263 .map_or_else(Rect::default, |(left, top, right, bottom)| {
264 Rect::from_ltrb(left, top, right, bottom)
265 })
266 }
267}
268
269fn include_quadratic_extrema(bounds: &mut TightBounds, start: Point, control: Point, end: Point) {
270 bounds.include(start);
271 bounds.include(end);
272 for (start_axis, control_axis, end_axis) in
273 [(start.x, control.x, end.x), (start.y, control.y, end.y)]
274 {
275 let denominator = start_axis as f64 - 2.0 * control_axis as f64 + end_axis as f64;
276 if denominator == 0.0 {
277 continue;
278 }
279 let parameter = ((start_axis as f64 - control_axis as f64) / denominator) as f32;
280 if parameter > 0.0 && parameter < 1.0 {
281 bounds.include(eval_quad(start, control, end, parameter));
282 }
283 }
284}
285
286fn include_cubic_extrema(
287 bounds: &mut TightBounds,
288 start: Point,
289 first: Point,
290 second: Point,
291 end: Point,
292) {
293 bounds.include(start);
294 bounds.include(end);
295 for (start_axis, first_axis, second_axis, end_axis) in [
296 (start.x, first.x, second.x, end.x),
297 (start.y, first.y, second.y, end.y),
298 ] {
299 for parameter in cubic_extrema(start_axis, first_axis, second_axis, end_axis)
300 .into_iter()
301 .flatten()
302 {
303 if parameter > 0.0 && parameter < 1.0 {
304 bounds.include(eval_cubic(start, first, second, end, parameter));
305 }
306 }
307 }
308}
309
310fn cubic_extrema(start: f32, first: f32, second: f32, end: f32) -> [Option<f32>; 2] {
311 let start = start as f64;
312 let first = first as f64;
313 let second = second as f64;
314 let end = end as f64;
315 let quadratic = -start + 3.0 * first - 3.0 * second + end;
316 let linear = 2.0 * (start - 2.0 * first + second);
317 let constant = first - start;
318 if quadratic == 0.0 {
319 return [unit_root(-constant, linear), None];
320 }
321 let discriminant = linear * linear - 4.0 * quadratic * constant;
322 if discriminant < 0.0 || !discriminant.is_finite() {
323 return [None, None];
324 }
325
326 let root = discriminant.sqrt();
329 let q = -0.5 * (linear + root.copysign(linear));
330 let first_root = unit_root(q, quadratic);
331 let second_root = unit_root(constant, q).filter(|value| Some(*value) != first_root);
332 [first_root, second_root]
333}
334
335fn unit_root(numerator: f64, denominator: f64) -> Option<f32> {
336 if denominator == 0.0 {
337 return None;
338 }
339 let value = numerator / denominator;
340 (value.is_finite() && value > 0.0 && value < 1.0).then_some(value as f32)
341}
342
343#[derive(Clone, Default)]
345pub struct PathBuilder {
346 verbs: Vec<Verb>,
347 points: Vec<Point>,
348 bounds: Option<Rect>,
349 resume_point: Option<Point>,
364 contour_open: bool,
365}
366
367impl PathBuilder {
368 pub fn new() -> Self {
370 Self::default()
371 }
372
373 pub fn move_to(&mut self, p: impl Into<Point>) -> &mut Self {
375 let p = p.into();
376 self.verbs.push(Verb::Move);
377 self.push_point(p);
378 self.resume_point = Some(p);
379 self.contour_open = true;
380 self
381 }
382
383 pub fn line_to(&mut self, p: impl Into<Point>) -> &mut Self {
385 let p = p.into();
386 self.ensure_contour(p);
387 self.verbs.push(Verb::Line);
388 self.push_point(p);
389 self
390 }
391
392 pub fn quad_to(&mut self, c: impl Into<Point>, p: impl Into<Point>) -> &mut Self {
394 let (c, p) = (c.into(), p.into());
395 self.ensure_contour(c);
396 self.verbs.push(Verb::Quad);
397 self.push_point(c);
398 self.push_point(p);
399 self
400 }
401
402 pub fn cubic_to(
404 &mut self,
405 c1: impl Into<Point>,
406 c2: impl Into<Point>,
407 p: impl Into<Point>,
408 ) -> &mut Self {
409 let (c1, c2, p) = (c1.into(), c2.into(), p.into());
410 self.ensure_contour(c1);
411 self.verbs.push(Verb::Cubic);
412 self.push_point(c1);
413 self.push_point(c2);
414 self.push_point(p);
415 self
416 }
417
418 pub fn close(&mut self) -> &mut Self {
422 if self.contour_open {
423 self.verbs.push(Verb::Close);
424 self.contour_open = false;
425 }
426 self
427 }
428
429 pub fn rect(&mut self, r: Rect) -> &mut Self {
433 self.move_to((r.x, r.y))
434 .line_to((r.right(), r.y))
435 .line_to((r.right(), r.bottom()))
436 .line_to((r.x, r.bottom()))
437 .close();
438 self.resume_point = Some(Point::new(r.x, r.y));
442 self
443 }
444
445 pub fn rrect(&mut self, r: Rect, radius: f32) -> &mut Self {
447 self.rrect_radii(r, [radius; 4])
448 }
449
450 pub fn rrect_radii(&mut self, r: Rect, radii: [f32; 4]) -> &mut Self {
454 self.rrect_radii_elliptical(r, radii.map(|radius| [radius; 2]))
455 }
456
457 pub fn rrect_radii_elliptical(
462 &mut self,
463 r: impl Into<Rect>,
464 radii: [[f32; 2]; 4],
465 ) -> &mut Self {
466 self.rrect_radii_elliptical_wound(r, radii, Winding::Clockwise)
467 }
468
469 pub fn rrect_radii_elliptical_wound(
474 &mut self,
475 r: impl Into<Rect>,
476 radii: [[f32; 2]; 4],
477 winding: Winding,
478 ) -> &mut Self {
479 let r = r.into();
480 let [tl, tr, br, bl] = constrain_radii_elliptical(&r, radii);
481 let (l, t, rr, b) = (r.x, r.y, r.right(), r.bottom());
482 if [tl, tr, br, bl].iter().all(|[x, y]| *x == 0.0 && *y == 0.0) {
483 match winding {
484 Winding::Clockwise => self.rect(r),
485 Winding::CounterClockwise => self
486 .move_to((l, t))
487 .line_to((l, b))
488 .line_to((rr, b))
489 .line_to((rr, t))
490 .close(),
491 };
492 self.resume_point = Some(Point::new(l, t));
493 return self;
494 }
495 let k = |rad: f32| rad * (1.0 - KAPPA);
498 match winding {
499 Winding::Clockwise => self
500 .move_to((l + tl[0], t))
501 .line_to((rr - tr[0], t))
502 .cubic_to((rr - k(tr[0]), t), (rr, t + k(tr[1])), (rr, t + tr[1]))
503 .line_to((rr, b - br[1]))
504 .cubic_to((rr, b - k(br[1])), (rr - k(br[0]), b), (rr - br[0], b))
505 .line_to((l + bl[0], b))
506 .cubic_to((l + k(bl[0]), b), (l, b - k(bl[1])), (l, b - bl[1]))
507 .line_to((l, t + tl[1]))
508 .cubic_to((l, t + k(tl[1])), (l + k(tl[0]), t), (l + tl[0], t))
509 .close(),
510 Winding::CounterClockwise => self
514 .move_to((l + tl[0], t))
515 .cubic_to((l + k(tl[0]), t), (l, t + k(tl[1])), (l, t + tl[1]))
516 .line_to((l, b - bl[1]))
517 .cubic_to((l, b - k(bl[1])), (l + k(bl[0]), b), (l + bl[0], b))
518 .line_to((rr - br[0], b))
519 .cubic_to((rr - k(br[0]), b), (rr, b - k(br[1])), (rr, b - br[1]))
520 .line_to((rr, t + tr[1]))
521 .cubic_to((rr, t + k(tr[1])), (rr - k(tr[0]), t), (rr - tr[0], t))
522 .line_to((l + tl[0], t))
523 .close(),
524 };
525 self.resume_point = Some(Point::new(l, t));
538 self
539 }
540
541 pub fn arc(
546 &mut self,
547 center: impl Into<Point>,
548 radius: f32,
549 start_angle: f32,
550 sweep_angle: f32,
551 ) -> &mut Self {
552 self.ellipse(center, [radius; 2], 0.0, start_angle, sweep_angle)
553 }
554
555 pub fn ellipse(
564 &mut self,
565 center: impl Into<Point>,
566 radii: [f32; 2],
567 x_axis_rotation: f32,
568 start_angle: f32,
569 sweep_angle: f32,
570 ) -> &mut Self {
571 let center = center.into();
572 let [radius_x, radius_y] = radii;
573 let finite = center.x.is_finite()
578 && center.y.is_finite()
579 && radius_x.is_finite()
580 && radius_y.is_finite()
581 && x_axis_rotation.is_finite()
582 && start_angle.is_finite()
583 && sweep_angle.is_finite();
584 debug_assert!(
585 radius_x >= 0.0 && radius_y >= 0.0,
586 "negative radii draw nothing; Canvas2D throws here"
587 );
588 if !finite || radius_x < 0.0 || radius_y < 0.0 {
589 return self;
590 }
591
592 let full_turn = std::f32::consts::TAU;
597 let sweep_angle = sweep_angle.clamp(-full_turn, full_turn);
598
599 let unit_circle_to_ellipse = unit_circle_map(center, radii, x_axis_rotation);
600 let first = unit_circle_to_ellipse.map_point(unit_circle_point(start_angle));
601 if self.contour_open || self.resume_point.is_some() {
607 self.ensure_contour(first);
608 self.line_to(first);
609 } else {
610 self.move_to(first);
611 }
612 if sweep_angle != 0.0 {
613 self.push_arc_cubics(&unit_circle_to_ellipse, start_angle, sweep_angle);
614 }
615 if sweep_angle.abs() >= full_turn {
618 self.close();
619 }
620 self
621 }
622
623 pub fn arc_to(
628 &mut self,
629 corner: impl Into<Point>,
630 next: impl Into<Point>,
631 radius: f32,
632 ) -> &mut Self {
633 let (corner, next) = (corner.into(), next.into());
634 self.ensure_contour(corner);
635 let start = *self.points.last().expect("ensure_contour opened a contour");
636
637 let incoming = normalize(
641 corner.x as f64 - start.x as f64,
642 corner.y as f64 - start.y as f64,
643 );
644 let outgoing = normalize(
645 next.x as f64 - corner.x as f64,
646 next.y as f64 - corner.y as f64,
647 );
648 let (Some(incoming), Some(outgoing)) = (incoming, outgoing) else {
649 return self.line_to(corner);
650 };
651 let cosine = incoming.0 * outgoing.0 + incoming.1 * outgoing.1;
652 let sine = incoming.0 * outgoing.1 - incoming.1 * outgoing.0;
653 if radius <= 0.0 || !radius.is_finite() || sine.abs() < 1.0 / (1 << 12) as f64 {
654 return self.line_to(corner);
655 }
656
657 let tangent_length = (radius as f64 * (1.0 - cosine) / sine).abs();
658 let entry = Point::new(
659 corner.x - (tangent_length * incoming.0) as f32,
660 corner.y - (tangent_length * incoming.1) as f32,
661 );
662 let turn = sine.signum() as f32;
664 let center = Point::new(
665 entry.x + radius * turn * -(incoming.1 as f32),
666 entry.y + radius * turn * incoming.0 as f32,
667 );
668 let exit = Point::new(
669 corner.x + (tangent_length * outgoing.0) as f32,
670 corner.y + (tangent_length * outgoing.1) as f32,
671 );
672
673 let start_angle = (entry.y - center.y).atan2(entry.x - center.x);
674 let end_angle = (exit.y - center.y).atan2(exit.x - center.x);
675 let sweep = shortest_sweep(start_angle, end_angle, turn);
676
677 self.line_to(entry);
678 let map = unit_circle_map(center, [radius; 2], 0.0);
679 self.push_arc_cubics(&map, start_angle, sweep);
680 self
681 }
682
683 pub fn circle(&mut self, center: impl Into<Point>, radius: f32) -> &mut Self {
685 let c = center.into();
686 let (r, k) = (radius, radius * KAPPA);
687 self.move_to((c.x + r, c.y))
688 .cubic_to((c.x + r, c.y + k), (c.x + k, c.y + r), (c.x, c.y + r))
689 .cubic_to((c.x - k, c.y + r), (c.x - r, c.y + k), (c.x - r, c.y))
690 .cubic_to((c.x - r, c.y - k), (c.x - k, c.y - r), (c.x, c.y - r))
691 .cubic_to((c.x + k, c.y - r), (c.x + r, c.y - k), (c.x + r, c.y))
692 .close()
693 }
694
695 pub fn append(&mut self, path: &Path, transform: &Matrix) -> &mut Self {
700 if path.verbs.is_empty() {
701 return self;
704 }
705 let mut point = path.points.iter();
706 let mut cursor = Point::ZERO;
707 let mut contour_start = Point::ZERO;
708 for verb in &path.verbs {
709 let count = match verb {
710 Verb::Move | Verb::Line => 1,
711 Verb::Quad => 2,
712 Verb::Cubic => 3,
713 Verb::Close => 0,
714 };
715 self.verbs.push(*verb);
716 for _ in 0..count {
717 let Some(&p) = point.next() else {
718 return self;
719 };
720 cursor = transform.map_point(p);
721 self.push_point(cursor);
722 }
723 match verb {
724 Verb::Move => contour_start = cursor,
725 Verb::Close => cursor = contour_start,
726 _ => {}
727 }
728 }
729 if matches!(path.verbs.last(), Some(Verb::Close)) {
740 self.move_to(cursor);
741 } else {
742 self.resume_point = Some(contour_start);
746 self.contour_open = true;
747 }
748 self
749 }
750
751 pub fn build(self) -> Arc<Path> {
753 Arc::new(Path {
754 verbs: self.verbs,
755 points: self.points,
756 bounds: self.bounds.unwrap_or_default(),
757 })
758 }
759
760 fn push_arc_cubics(&mut self, map: &Matrix, start_angle: f32, sweep_angle: f32) {
766 let piece_count = (sweep_angle.abs() / std::f32::consts::FRAC_PI_2)
767 .ceil()
768 .max(1.0);
769 let step = sweep_angle / piece_count;
770 let reach = 4.0 / 3.0 * (step / 4.0).tan();
773
774 let mut angle = start_angle;
775 for _ in 0..piece_count as u32 {
776 let (from, to) = (unit_circle_point(angle), unit_circle_point(angle + step));
777 let first = Point::new(from.x - reach * from.y, from.y + reach * from.x);
778 let second = Point::new(to.x + reach * to.y, to.y - reach * to.x);
779 self.cubic_to(
780 map.map_point(first),
781 map.map_point(second),
782 map.map_point(to),
783 );
784 angle += step;
785 }
786 }
787
788 fn ensure_contour(&mut self, p: Point) {
798 if self.contour_open {
799 return;
800 }
801 self.move_to(self.resume_point.unwrap_or(p));
802 }
803
804 fn push_point(&mut self, p: Point) {
805 self.points.push(p);
806 self.bounds = Some(match self.bounds {
810 Some(b) => Rect::from_ltrb(
811 b.x.min(p.x),
812 b.y.min(p.y),
813 b.right().max(p.x),
814 b.bottom().max(p.y),
815 ),
816 None => Rect::new(p.x, p.y, 0.0, 0.0),
817 });
818 }
819}
820
821const KAPPA: f32 = 0.552_284_8;
823
824fn unit_circle_point(angle: f32) -> Point {
826 let (sine, cosine) = angle.sin_cos();
827 Point::new(cosine, sine)
828}
829
830fn unit_circle_map(center: Point, radii: [f32; 2], rotation: f32) -> Matrix {
832 let [radius_x, radius_y] = radii;
833 let (sine, cosine) = rotation.sin_cos();
834 Matrix::from_affine(
835 radius_x * cosine,
836 radius_x * sine,
837 -radius_y * sine,
838 radius_y * cosine,
839 center.x,
840 center.y,
841 )
842}
843
844fn shortest_sweep(start: f32, end: f32, direction: f32) -> f32 {
846 let mut sweep = end - start;
847 let turn = std::f32::consts::TAU;
848 while sweep > 0.0 && direction < 0.0 {
849 sweep -= turn;
850 }
851 while sweep < 0.0 && direction > 0.0 {
852 sweep += turn;
853 }
854 sweep
855}
856
857fn normalize(x: f64, y: f64) -> Option<(f64, f64)> {
859 let length = (x * x + y * y).sqrt();
860 (length.is_finite() && length > 0.0).then(|| (x / length, y / length))
861}
862
863pub fn constrain_radii(r: &Rect, radii: [f32; 4]) -> [f32; 4] {
867 constrain_radii_elliptical(r, radii.map(|v| [v; 2])).map(|[x, _]| x)
868}
869
870pub fn constrain_radii_elliptical(r: &Rect, radii: [[f32; 2]; 4]) -> [[f32; 2]; 4] {
875 let [tl, tr, br, bl] = radii.map(|[x, y]| [x.max(0.0), y.max(0.0)]);
876 let fit = |side: f32, a: f32, b: f32| if a + b <= side { 1.0 } else { side / (a + b) };
877 let f = fit(r.width, tl[0], tr[0])
878 .min(fit(r.width, bl[0], br[0]))
879 .min(fit(r.height, tl[1], bl[1]))
880 .min(fit(r.height, tr[1], br[1]));
881 [tl, tr, br, bl].map(|[x, y]| [x * f, y * f])
882}
883
884struct Flattener {
886 tolerance: f32,
887 contours: Vec<Contour>,
888 current: Vec<Point>,
889 has_segments: bool,
891}
892
893impl Flattener {
894 fn new(tolerance: f32) -> Self {
895 Self {
896 tolerance,
897 contours: Vec::new(),
898 current: Vec::new(),
899 has_segments: false,
900 }
901 }
902
903 fn move_to(&mut self, p: Point) {
904 self.flush(false);
905 self.current.push(p);
906 self.has_segments = false;
907 }
908
909 fn line_to(&mut self, p: Point) {
910 self.current.push(p);
911 self.has_segments = true;
912 }
913
914 fn quad_to(&mut self, c: Point, p: Point) {
915 let Some(&start) = self.current.last() else {
916 return;
917 };
918 self.has_segments = true;
919 let dev = second_difference(start, c, p);
920 let n = segment_count((dev / (8.0 * self.tolerance)).sqrt());
921 for i in 1..=n {
922 let t = i as f32 / n as f32;
923 self.current.push(eval_quad(start, c, p, t));
924 }
925 }
926
927 fn cubic_to(&mut self, c1: Point, c2: Point, p: Point) {
928 let Some(&start) = self.current.last() else {
929 return;
930 };
931 self.has_segments = true;
932 let dev = second_difference(start, c1, c2).max(second_difference(c1, c2, p));
933 let n = segment_count((3.0 * dev / (4.0 * self.tolerance)).sqrt());
934 for i in 1..=n {
935 let t = i as f32 / n as f32;
936 self.current.push(eval_cubic(start, c1, c2, p, t));
937 }
938 }
939
940 fn close(&mut self) {
941 if let (Some(&first), Some(&last)) = (self.current.first(), self.current.last()) {
944 if self.current.len() >= 2 && (first.x, first.y) != (last.x, last.y) {
945 self.current.push(first);
946 }
947 }
948 if !self.current.is_empty() {
954 self.has_segments = true;
955 }
956 self.flush(true);
957 }
958
959 fn finish(mut self) -> Vec<Contour> {
960 self.flush(false);
961 self.contours
962 }
963
964 fn flush(&mut self, closed: bool) {
971 if !self.current.is_empty() {
972 self.contours.push(Contour {
973 points: std::mem::take(&mut self.current),
974 closed,
975 has_segments: self.has_segments,
976 });
977 }
978 self.has_segments = false;
979 }
980}
981
982fn second_difference(a: Point, b: Point, c: Point) -> f32 {
983 let dx = a.x - 2.0 * b.x + c.x;
984 let dy = a.y - 2.0 * b.y + c.y;
985 (dx * dx + dy * dy).sqrt()
986}
987
988fn segment_count(estimate: f32) -> u32 {
989 (estimate.ceil() as u32).clamp(1, 64)
990}
991
992fn eval_quad(p0: Point, c: Point, p1: Point, t: f32) -> Point {
993 let u = 1.0 - t;
994 Point::new(
995 u * u * p0.x + 2.0 * u * t * c.x + t * t * p1.x,
996 u * u * p0.y + 2.0 * u * t * c.y + t * t * p1.y,
997 )
998}
999
1000fn eval_cubic(p0: Point, c1: Point, c2: Point, p1: Point, t: f32) -> Point {
1001 let u = 1.0 - t;
1002 let (uu, tt) = (u * u, t * t);
1003 Point::new(
1004 u * uu * p0.x + 3.0 * uu * t * c1.x + 3.0 * u * tt * c2.x + t * tt * p1.x,
1005 u * uu * p0.y + 3.0 * uu * t * c1.y + 3.0 * u * tt * c2.y + t * tt * p1.y,
1006 )
1007}
1008
1009pub fn local_tolerance(transform: &Matrix) -> f32 {
1011 0.25 / transform.max_scale().max(1e-3)
1012}
1013
1014#[cfg(test)]
1015mod tests {
1016 use super::*;
1017
1018 #[test]
1025 fn opposed_windings_cancel_under_the_non_zero_rule() {
1026 let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
1027 let radii = [[12.0, 12.0]; 4];
1028 let inside = Point::new(50.0, 50.0);
1029
1030 let mut opposed = PathBuilder::new();
1031 opposed.rrect_radii_elliptical_wound(rect, radii, Winding::Clockwise);
1032 opposed.rrect_radii_elliptical_wound(rect, radii, Winding::CounterClockwise);
1033 assert!(
1034 !opposed.build().contains(inside, FillRule::NonZero),
1035 "opposed windings must cancel"
1036 );
1037
1038 let mut agreeing = PathBuilder::new();
1039 agreeing.rrect_radii_elliptical_wound(rect, radii, Winding::Clockwise);
1040 agreeing.rrect_radii_elliptical_wound(rect, radii, Winding::Clockwise);
1041 assert!(
1042 agreeing.build().contains(inside, FillRule::NonZero),
1043 "agreeing windings must reinforce"
1044 );
1045 }
1046
1047 #[test]
1051 fn winding_reverses_the_walk_without_moving_the_outline() {
1052 let rect = Rect::new(10.0, 20.0, 80.0, 60.0);
1053 let radii = [[8.0, 14.0], [4.0, 4.0], [20.0, 6.0], [0.0, 0.0]];
1054 let wound = |winding| {
1055 let mut path = PathBuilder::new();
1056 path.rrect_radii_elliptical_wound(rect, radii, winding);
1057 path.build()
1058 };
1059 let clockwise = wound(Winding::Clockwise);
1060 let counter = wound(Winding::CounterClockwise);
1061 assert_eq!(clockwise.tight_bounds(), counter.tight_bounds());
1062 for point in [
1065 Point::new(50.0, 50.0),
1066 Point::new(14.0, 30.0),
1067 Point::new(86.0, 24.0),
1068 Point::new(74.0, 76.0),
1069 Point::new(12.0, 78.0),
1070 Point::new(5.0, 15.0),
1071 Point::new(95.0, 85.0),
1072 ] {
1073 assert_eq!(
1074 clockwise.contains(point, FillRule::NonZero),
1075 counter.contains(point, FillRule::NonZero),
1076 "the two directions disagree about {point:?}"
1077 );
1078 }
1079 }
1080
1081 #[test]
1089 fn a_segment_after_close_resumes_at_the_contour_origin() {
1090 let mut path = PathBuilder::new();
1091 path.move_to((10.0, 10.0));
1092 path.line_to((30.0, 10.0));
1093 path.close();
1094 path.line_to((30.0, 30.0));
1095 let path = path.build();
1096
1097 let contours = path.flatten(0.05);
1099 let resumed = contours.last().expect("the path continues after the close");
1100 assert_eq!(
1101 resumed.points.first().copied(),
1102 Some(Point::new(10.0, 10.0)),
1103 "the segment after close must start at the contour origin, not its own end"
1104 );
1105 assert!(crate::stroke_contains(
1106 &contours,
1107 &crate::Stroke::new(6.0),
1108 0.05,
1109 Point::new(20.0, 20.0)
1110 ));
1111 }
1112
1113 #[test]
1121 fn a_segment_after_a_shape_helper_resumes_at_the_box_corner() {
1122 let box_corner = Point::new(10.0, 10.0);
1123 for corner in [0.0f32, 8.0] {
1124 let mut path = PathBuilder::new();
1125 if corner == 0.0 {
1126 path.rect(Rect::new(10.0, 10.0, 40.0, 40.0));
1127 } else {
1128 path.rrect_radii_elliptical(Rect::new(10.0, 10.0, 40.0, 40.0), [[corner; 2]; 4]);
1129 }
1130 path.line_to((90.0, 90.0));
1131
1132 let contours = path.build().flatten(0.05);
1133 let resumed = contours.last().expect("the path continues after the shape");
1134 assert_eq!(
1135 resumed.points.first().copied(),
1136 Some(box_corner),
1137 "corner radius {corner}: the trailing segment starts at (x, y)"
1138 );
1139 }
1140
1141 let mut path = PathBuilder::new();
1145 path.rrect_radii_elliptical(Rect::new(10.0, 10.0, 40.0, 40.0), [[8.0; 2]; 4]);
1146 path.line_to((90.0, 90.0));
1147 let contours = path.build().flatten(0.05);
1148 let stroke = crate::Stroke::new(4.0);
1149 assert!(
1150 crate::stroke_contains(&contours, &stroke, 0.05, Point::new(50.0, 50.0)),
1151 "the diagonal from (10,10) must be stroked"
1152 );
1153 assert!(
1154 !crate::stroke_contains(&contours, &stroke, 0.05, Point::new(54.0, 50.0)),
1155 "the diagonal from the tangent (18,10) must not be"
1156 );
1157 }
1158
1159 #[test]
1162 fn close_still_resumes_at_the_contour_origin() {
1163 let mut path = PathBuilder::new();
1164 path.move_to((10.0, 10.0));
1165 path.line_to((30.0, 10.0));
1166 path.line_to((30.0, 30.0));
1167 path.close();
1168 path.line_to((90.0, 90.0));
1169 let contours = path.build().flatten(0.05);
1170 assert_eq!(
1171 contours
1172 .last()
1173 .and_then(|contour| contour.points.first())
1174 .copied(),
1175 Some(Point::new(10.0, 10.0)),
1176 "close resumes where the contour began, not at any box corner"
1177 );
1178 }
1179
1180 #[test]
1183 fn a_first_segment_with_no_contour_starts_at_its_own_point() {
1184 let mut path = PathBuilder::new();
1185 path.line_to((30.0, 30.0));
1186 assert_eq!(
1187 path.build().bounds(),
1188 Rect::from_ltrb(30.0, 30.0, 30.0, 30.0)
1189 );
1190 }
1191
1192 #[test]
1193 fn append_carries_verbs_through_the_transform() {
1194 let mut source = PathBuilder::new();
1195 source.rect(Rect::new(0.0, 0.0, 10.0, 10.0));
1196 let source = source.build();
1197
1198 let mut target = PathBuilder::new();
1199 target.rect(Rect::new(0.0, 0.0, 4.0, 4.0));
1200 target.append(&source, &Matrix::translation(100.0, 50.0));
1201 let target = target.build();
1202
1203 assert_eq!(target.bounds(), Rect::from_ltrb(0.0, 0.0, 110.0, 60.0));
1204 assert!(target.contains(Point::new(105.0, 55.0), FillRule::NonZero));
1205 assert!(!target.contains(Point::new(5.0, 5.0), FillRule::NonZero));
1206 }
1207
1208 #[test]
1212 fn appending_a_closed_contour_reopens_at_its_seam() {
1213 let mut source = PathBuilder::new();
1214 source.move_to((10.0, 10.0));
1215 source.line_to((20.0, 10.0));
1216 source.close();
1217 let source = source.build();
1218
1219 let mut target = PathBuilder::new();
1220 target.append(&source, &Matrix::IDENTITY);
1221 target.line_to((10.0, 40.0));
1222 let built = target.build();
1223
1224 assert_eq!(built.bounds(), Rect::from_ltrb(10.0, 10.0, 20.0, 40.0));
1226 assert!(built.contains(Point::new(10.0, 25.0), FillRule::NonZero));
1227 }
1228
1229 #[test]
1233 fn appending_an_open_contour_hands_over_its_origin() {
1234 let mut source = PathBuilder::new();
1235 source.move_to((50.0, 50.0));
1236 source.line_to((60.0, 50.0));
1237 let source = source.build();
1238
1239 let mut target = PathBuilder::new();
1240 target.move_to((0.0, 0.0));
1241 target.line_to((10.0, 0.0));
1242 target.append(&source, &Matrix::IDENTITY);
1243 target.close();
1244 target.line_to((90.0, 90.0));
1245
1246 let contours = target.build().flatten(0.05);
1247 let resumed = contours.last().expect("the path continues after the close");
1248 assert_eq!(
1249 resumed.points.first().copied(),
1250 Some(Point::new(50.0, 50.0)),
1251 "the resumed segment must start at the APPENDED contour's origin"
1252 );
1253 }
1254
1255 #[test]
1256 fn appending_nothing_leaves_an_open_contour_open() {
1257 let empty = PathBuilder::new().build();
1258 let mut target = PathBuilder::new();
1259 target.move_to((0.0, 0.0));
1260 target.line_to((10.0, 0.0));
1261 target.append(&empty, &Matrix::IDENTITY);
1262 target.line_to((10.0, 10.0));
1263 assert_eq!(
1264 target.build().bounds(),
1265 Rect::from_ltrb(0.0, 0.0, 10.0, 10.0)
1266 );
1267 }
1268
1269 #[test]
1270 fn appending_an_open_contour_leaves_it_open() {
1271 let mut source = PathBuilder::new();
1272 source.move_to((0.0, 0.0));
1273 source.line_to((10.0, 0.0));
1274 let source = source.build();
1275
1276 let mut target = PathBuilder::new();
1277 target.append(&source, &Matrix::IDENTITY);
1278 target.line_to((10.0, 10.0));
1281 assert_eq!(
1282 target.build().bounds(),
1283 Rect::from_ltrb(0.0, 0.0, 10.0, 10.0)
1284 );
1285 }
1286
1287 #[test]
1288 fn tight_bounds_use_curve_extrema_not_control_points() {
1289 let mut path = PathBuilder::new();
1290 path.move_to((0.0, 0.0));
1291 path.quad_to((100.0, 100.0), (200.0, 0.0));
1292 let path = path.build();
1293 assert_eq!(path.bounds(), Rect::new(0.0, 0.0, 200.0, 100.0));
1294 assert_eq!(path.tight_bounds(), Rect::new(0.0, 0.0, 200.0, 50.0));
1295 }
1296
1297 #[test]
1298 fn tight_bounds_keep_extrema_below_f32_epsilon() {
1299 let mut path = PathBuilder::new();
1300 path.move_to((0.0, 0.0));
1301 path.quad_to((0.0, 1.0e-8), (0.0, 0.0));
1302 let bounds = path.build().tight_bounds();
1303 assert!((bounds.height - 5.0e-9).abs() < 1.0e-12);
1304 }
1305
1306 #[test]
1307 fn cubic_extrema_preserve_the_small_root() {
1308 let roots = cubic_extrema(0.0, 1.0e-8, -0.5, -0.5);
1309 assert!(roots
1310 .into_iter()
1311 .flatten()
1312 .any(|root| (root - 1.0e-8).abs() < 1.0e-10));
1313 }
1314
1315 #[test]
1316 fn radii_constrain_together() {
1317 let r = Rect::new(0.0, 0.0, 100.0, 40.0);
1318 let out = constrain_radii(&r, [40.0, 10.0, 10.0, 40.0]);
1320 assert_eq!(out, [20.0, 5.0, 5.0, 20.0]);
1321 assert_eq!(constrain_radii(&r, [8.0, 8.0, 8.0, 8.0]), [8.0; 4]);
1323 }
1324
1325 #[test]
1326 fn per_corner_rrect_stays_in_rect() {
1327 let r = Rect::new(10.0, 10.0, 100.0, 60.0);
1328 let mut b = PathBuilder::new();
1329 b.rrect_radii(r, [30.0, 0.0, 16.0, 8.0]);
1330 assert_eq!(b.build().bounds(), r);
1331 }
1332
1333 #[test]
1334 fn bounds_cover_control_points() {
1335 let mut b = PathBuilder::new();
1336 b.move_to((10.0, 10.0)).quad_to((50.0, -20.0), (90.0, 10.0));
1337 let p = b.build();
1338 assert_eq!(p.bounds(), Rect::from_ltrb(10.0, -20.0, 90.0, 10.0));
1339 }
1340
1341 #[test]
1342 fn circle_flattens_to_radius() {
1343 let mut b = PathBuilder::new();
1344 b.circle((0.0, 0.0), 100.0);
1345 let contours = b.build().flatten(0.1);
1346 assert_eq!(contours.len(), 1);
1347 assert!(contours[0].closed, "circle closes its contour");
1348 for p in &contours[0].points {
1349 let r = (p.x * p.x + p.y * p.y).sqrt();
1350 assert!((r - 100.0).abs() < 0.5, "point off circle: r={r}");
1351 }
1352 }
1353
1354 #[test]
1355 fn finer_tolerance_means_more_segments() {
1356 let path = {
1357 let mut b = PathBuilder::new();
1358 b.circle((0.0, 0.0), 100.0);
1359 b.build()
1360 };
1361 let coarse = path.flatten(2.0)[0].points.len();
1362 let fine = path.flatten(0.05)[0].points.len();
1363 assert!(fine > coarse, "fine {fine} vs coarse {coarse}");
1364 }
1365
1366 #[test]
1367 fn small_contours_survive_for_the_stroker() {
1368 let mut b = PathBuilder::new();
1369 b.move_to((0.0, 0.0)).line_to((10.0, 0.0)); b.move_to((50.0, 50.0)); let contours = b.build().flatten(0.1);
1372 assert_eq!(contours.len(), 2);
1373 assert_eq!(contours[0].points.len(), 2);
1374 assert!(!contours[0].closed);
1375 assert_eq!(contours[1].points.len(), 1);
1376 }
1377
1378 #[test]
1379 fn close_emits_the_closing_edge_and_marks_the_contour() {
1380 let mut b = PathBuilder::new();
1381 b.move_to((0.0, 0.0))
1382 .line_to((10.0, 0.0))
1383 .line_to((10.0, 10.0))
1384 .close();
1385 let contours = b.build().flatten(0.1);
1386 assert!(contours[0].closed);
1387 assert_eq!(contours[0].points.len(), 4, "closing edge in the polyline");
1388 assert_eq!(contours[0].points[3], Point::new(0.0, 0.0));
1389 }
1390
1391 #[test]
1392 fn bounds_keep_a_first_point_at_the_origin() {
1393 let mut b = PathBuilder::new();
1394 b.move_to((0.0, 0.0)).line_to((50.0, 80.0));
1395 assert_eq!(b.build().bounds(), Rect::from_ltrb(0.0, 0.0, 50.0, 80.0));
1396
1397 let mut b = PathBuilder::new();
1398 b.move_to((0.0, 0.0)).line_to((100.0, 0.0)); assert_eq!(b.build().bounds(), Rect::from_ltrb(0.0, 0.0, 100.0, 0.0));
1400 }
1401
1402 #[test]
1403 fn curve_without_move_starts_contour() {
1404 let mut b = PathBuilder::new();
1405 b.line_to((10.0, 0.0))
1406 .line_to((10.0, 10.0))
1407 .line_to((0.0, 10.0));
1408 let contours = b.build().flatten(0.1);
1409 assert_eq!(contours.len(), 1);
1410 assert_eq!(contours[0].points.len(), 4);
1411 }
1412
1413 #[test]
1417 fn circular_rrect_is_the_equal_axes_elliptical_case() {
1418 let r = Rect::new(10.0, 20.0, 120.0, 80.0);
1419 let radii = [24.0, 8.0, 30.0, 0.0];
1420 let mut circular = PathBuilder::new();
1421 circular.rrect_radii(r, radii);
1422 let mut elliptical = PathBuilder::new();
1423 elliptical.rrect_radii_elliptical(r, radii.map(|v| [v; 2]));
1424 assert_eq!(
1425 circular.build().flatten(0.1)[0].points,
1426 elliptical.build().flatten(0.1)[0].points,
1427 );
1428 }
1429
1430 #[test]
1431 fn elliptical_radii_constrain_per_axis() {
1432 let r = Rect::new(0.0, 0.0, 100.0, 40.0);
1435 let out = constrain_radii_elliptical(
1436 &r,
1437 [[10.0, 20.0], [10.0, 20.0], [10.0, 30.0], [10.0, 30.0]],
1438 );
1439 assert_eq!(out[0], [8.0, 16.0]);
1440 assert_eq!(out[2], [8.0, 24.0]);
1441 let out = constrain_radii_elliptical(&r, [[-5.0, 10.0], [0.0; 2], [0.0; 2], [0.0; 2]]);
1443 assert_eq!(out[0], [0.0, 10.0]);
1444 }
1445
1446 #[test]
1447 fn elliptical_corner_lands_on_axis_extremes() {
1448 let r = Rect::new(0.0, 0.0, 200.0, 100.0);
1451 let mut b = PathBuilder::new();
1452 b.rrect_radii_elliptical(r, [[0.0; 2], [40.0, 10.0], [0.0; 2], [0.0; 2]]);
1453 let points = &b.build().flatten(0.05)[0].points;
1454 assert!(points
1457 .iter()
1458 .any(|p| (p.x - 160.0).abs() < 0.5 && p.y.abs() < 0.5));
1459 assert!(points
1460 .iter()
1461 .any(|p| (p.x - 200.0).abs() < 0.5 && (p.y - 10.0).abs() < 0.5));
1462 }
1463
1464 #[test]
1469 fn swept_arc_stays_on_its_circle() {
1470 let (center, radius) = (Point::new(50.0, 60.0), 40.0);
1471 let mut b = PathBuilder::new();
1472 b.arc(center, radius, 0.0, std::f32::consts::TAU);
1473 for point in &b.build().flatten(0.01)[0].points {
1474 let offset = (point.x - center.x).hypot(point.y - center.y);
1475 assert!(
1476 (offset - radius).abs() < 0.05,
1477 "point {point:?} is {offset} from the centre, not {radius}"
1478 );
1479 }
1480 }
1481
1482 #[test]
1484 fn quarter_arc_ends_where_it_should() {
1485 let mut b = PathBuilder::new();
1486 b.arc((0.0, 0.0), 100.0, 0.0, std::f32::consts::FRAC_PI_2);
1487 let points = &b.build().flatten(0.01)[0].points;
1488 let (first, last) = (points[0], *points.last().unwrap());
1489 assert!(
1490 (first.x - 100.0).abs() < 0.01 && first.y.abs() < 0.01,
1491 "{first:?}"
1492 );
1493 assert!(
1494 last.x.abs() < 0.05 && (last.y - 100.0).abs() < 0.05,
1495 "{last:?}"
1496 );
1497 }
1498
1499 #[test]
1501 fn ellipse_reaches_both_radii() {
1502 let mut b = PathBuilder::new();
1503 b.ellipse((0.0, 0.0), [80.0, 20.0], 0.0, 0.0, std::f32::consts::TAU);
1504 let points = &b.build().flatten(0.01)[0].points;
1505 let widest = points.iter().fold(0.0f32, |m, p| m.max(p.x.abs()));
1506 let tallest = points.iter().fold(0.0f32, |m, p| m.max(p.y.abs()));
1507 assert!((widest - 80.0).abs() < 0.1, "widest {widest}");
1508 assert!((tallest - 20.0).abs() < 0.1, "tallest {tallest}");
1509 }
1510
1511 #[test]
1513 fn ellipse_rotation_swaps_the_axes() {
1514 let mut b = PathBuilder::new();
1515 b.ellipse(
1516 (0.0, 0.0),
1517 [80.0, 20.0],
1518 std::f32::consts::FRAC_PI_2,
1519 0.0,
1520 std::f32::consts::TAU,
1521 );
1522 let points = &b.build().flatten(0.01)[0].points;
1523 let widest = points.iter().fold(0.0f32, |m, p| m.max(p.x.abs()));
1524 let tallest = points.iter().fold(0.0f32, |m, p| m.max(p.y.abs()));
1525 assert!((widest - 20.0).abs() < 0.1, "widest {widest}");
1526 assert!((tallest - 80.0).abs() < 0.1, "tallest {tallest}");
1527 }
1528
1529 #[test]
1533 fn arc_to_rounds_a_right_angle() {
1534 let radius = 20.0f32;
1535 let mut b = PathBuilder::new();
1536 b.move_to((0.0, 0.0))
1537 .arc_to((100.0, 0.0), (100.0, 100.0), radius);
1538 let points = &b.build().flatten(0.01)[0].points;
1539
1540 let entry = Point::new(100.0 - radius, 0.0);
1541 let exit = Point::new(100.0, radius);
1542 assert!(points
1543 .iter()
1544 .any(|p| (p.x - entry.x).abs() < 0.1 && (p.y - entry.y).abs() < 0.1));
1545 assert!(points
1546 .iter()
1547 .any(|p| (p.x - exit.x).abs() < 0.1 && (p.y - exit.y).abs() < 0.1));
1548
1549 let center = Point::new(100.0 - radius, radius);
1550 for point in points.iter().filter(|p| p.x > entry.x - 0.01) {
1551 let offset = (point.x - center.x).hypot(point.y - center.y);
1552 assert!(
1553 (offset - radius).abs() < 0.1,
1554 "{point:?} is {offset} from the centre"
1555 );
1556 }
1557 }
1558
1559 #[test]
1562 fn degenerate_arc_to_falls_back_to_a_line() {
1563 for (corner, next, radius) in [
1564 ((50.0, 0.0), (100.0, 0.0), 20.0), ((50.0, 0.0), (50.0, 50.0), 0.0), ] {
1567 let mut b = PathBuilder::new();
1568 b.move_to((0.0, 0.0)).arc_to(corner, next, radius);
1569 let points = &b.build().flatten(0.01)[0].points;
1570 assert_eq!(points.len(), 2, "expected a bare line, got {points:?}");
1571 assert!((points[1].x - corner.0).abs() < 0.01 && (points[1].y - corner.1).abs() < 0.01);
1572 }
1573 }
1574
1575 #[test]
1578 fn rect_contains_what_it_covers() {
1579 let mut b = PathBuilder::new();
1580 b.rect(Rect::new(10.0, 10.0, 80.0, 60.0));
1581 let path = b.build();
1582 assert!(path.contains(Point::new(50.0, 40.0), FillRule::NonZero));
1583 assert!(!path.contains(Point::new(5.0, 40.0), FillRule::NonZero));
1584 assert!(!path.contains(Point::new(50.0, 80.0), FillRule::NonZero));
1585 for on_outline in [
1588 Point::new(10.0, 40.0), Point::new(50.0, 10.0), Point::new(90.0, 40.0), Point::new(50.0, 70.0), Point::new(90.0, 70.0), ] {
1594 assert!(
1595 path.contains(on_outline, FillRule::NonZero),
1596 "{on_outline:?} is on the outline and must count as inside"
1597 );
1598 }
1599 }
1600
1601 #[test]
1605 fn an_enormous_sweep_stays_one_turn() {
1606 let mut b = PathBuilder::new();
1607 b.arc((0.0, 0.0), 50.0, 0.0, 1e20);
1608 let path = b.build();
1609 let contours = path.flatten(0.1);
1610 assert_eq!(contours.len(), 1);
1611 assert!(
1613 contours[0].points.len() < 1_000,
1614 "a clamped turn should stay small, got {}",
1615 contours[0].points.len()
1616 );
1617 assert!(contours[0].closed, "a full turn closes its contour");
1618 }
1619
1620 #[test]
1621 fn a_negative_sweep_turns_the_other_way() {
1622 let quarter = std::f32::consts::FRAC_PI_2;
1623 let mut clockwise = PathBuilder::new();
1624 clockwise.arc((0.0, 0.0), 50.0, 0.0, quarter);
1625 let mut anticlockwise = PathBuilder::new();
1626 anticlockwise.arc((0.0, 0.0), 50.0, 0.0, -quarter);
1627
1628 let forward = clockwise.build().bounds();
1631 let backward = anticlockwise.build().bounds();
1632 assert!(forward.bottom() > 40.0, "positive sweep reaches +y");
1633 assert!(backward.y < -40.0, "negative sweep reaches -y");
1634 }
1635
1636 #[test]
1639 fn circle_containment_is_exact_all_the_way_round() {
1640 let (center, radius) = (Point::new(0.0, 0.0), 100.0f32);
1641 let mut b = PathBuilder::new();
1642 b.circle(center, radius);
1643 let path = b.build();
1644 for step in 0..64 {
1645 let angle = step as f32 / 64.0 * std::f32::consts::TAU;
1646 let (sine, cosine) = angle.sin_cos();
1647 let inside = Point::new(cosine * radius * 0.99, sine * radius * 0.99);
1648 let outside = Point::new(cosine * radius * 1.01, sine * radius * 1.01);
1649 assert!(
1650 path.contains(inside, FillRule::NonZero),
1651 "{inside:?} should be in"
1652 );
1653 assert!(
1654 !path.contains(outside, FillRule::NonZero),
1655 "{outside:?} should be out"
1656 );
1657 }
1658 }
1659
1660 #[test]
1664 fn fill_rules_disagree_about_a_same_wound_hole() {
1665 let mut b = PathBuilder::new();
1666 b.rect(Rect::new(0.0, 0.0, 100.0, 100.0));
1667 b.rect(Rect::new(25.0, 25.0, 50.0, 50.0));
1668 let path = b.build();
1669 let middle = Point::new(50.0, 50.0);
1670 assert!(path.contains(middle, FillRule::NonZero));
1671 assert!(!path.contains(middle, FillRule::EvenOdd));
1672 let ring = Point::new(10.0, 50.0);
1674 assert!(path.contains(ring, FillRule::NonZero));
1675 assert!(path.contains(ring, FillRule::EvenOdd));
1676 }
1677
1678 #[test]
1680 fn open_contour_closes_implicitly() {
1681 let mut b = PathBuilder::new();
1682 b.move_to((0.0, 0.0))
1683 .line_to((100.0, 0.0))
1684 .line_to((100.0, 100.0));
1685 let path = b.build();
1686 assert!(path.contains(Point::new(80.0, 40.0), FillRule::NonZero));
1687 assert!(!path.contains(Point::new(20.0, 60.0), FillRule::NonZero));
1688 }
1689
1690 #[test]
1692 fn containment_handles_curves_that_double_back() {
1693 let mut b = PathBuilder::new();
1694 b.move_to((0.0, 0.0))
1695 .cubic_to((120.0, 120.0), (-20.0, 120.0), (100.0, 0.0))
1696 .close();
1697 let path = b.build();
1698 assert!(path.contains(Point::new(50.0, 40.0), FillRule::NonZero));
1699 assert!(!path.contains(Point::new(50.0, -10.0), FillRule::NonZero));
1700 assert!(!path.contains(Point::new(-30.0, 40.0), FillRule::NonZero));
1701 }
1702}