1use std::sync::Arc;
2
3use crate::{Matrix, Point, Rect};
4
5#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
6#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
7pub enum FillRule {
8 #[default]
9 NonZero,
10 EvenOdd,
11}
12
13#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
19#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
20pub enum Winding {
21 #[default]
22 Clockwise,
23 CounterClockwise,
24}
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize))]
30enum Verb {
31 Move,
32 Line,
33 Quad,
34 Cubic,
35 Close,
36}
37
38#[derive(Clone, Debug, PartialEq)]
45pub struct Contour {
46 pub points: Vec<Point>,
47 pub closed: bool,
48 pub has_segments: bool,
70}
71
72#[derive(Clone, Debug)]
77#[cfg_attr(feature = "serde", derive(serde::Serialize))]
78pub struct Path {
79 verbs: Vec<Verb>,
80 points: Vec<Point>,
81 bounds: Rect,
84}
85
86impl Path {
87 pub fn bounds(&self) -> Rect {
88 self.bounds
89 }
90
91 pub fn tight_bounds(&self) -> Rect {
96 let mut bounds = TightBounds::default();
97 let mut point_index = 0usize;
98 let mut cursor = Point::ZERO;
99 let mut contour_start = Point::ZERO;
100 for verb in &self.verbs {
101 match verb {
102 Verb::Move => {
103 cursor = self.points[point_index];
104 contour_start = cursor;
105 point_index += 1;
106 bounds.include(cursor);
107 }
108 Verb::Line => {
109 cursor = self.points[point_index];
110 point_index += 1;
111 bounds.include(cursor);
112 }
113 Verb::Quad => {
114 let control = self.points[point_index];
115 let end = self.points[point_index + 1];
116 point_index += 2;
117 include_quadratic_extrema(&mut bounds, cursor, control, end);
118 cursor = end;
119 }
120 Verb::Cubic => {
121 let first = self.points[point_index];
122 let second = self.points[point_index + 1];
123 let end = self.points[point_index + 2];
124 point_index += 3;
125 include_cubic_extrema(&mut bounds, cursor, first, second, end);
126 cursor = end;
127 }
128 Verb::Close => {
129 cursor = contour_start;
130 bounds.include(cursor);
131 }
132 }
133 }
134 bounds.rect()
135 }
136
137 pub fn is_empty(&self) -> bool {
138 self.verbs.is_empty()
139 }
140
141 pub fn heap_bytes(&self) -> usize {
143 self.points.len() * std::mem::size_of::<Point>() + self.verbs.len()
144 }
145
146 pub fn contains(&self, point: Point, fill_rule: FillRule) -> bool {
155 if !self.bounds.contains_inclusive(point) {
156 return false;
157 }
158 let crossings = self.walk_crossings(point);
159 match fill_rule {
160 FillRule::NonZero => crossings.is_inside_non_zero(),
161 FillRule::EvenOdd => crossings.is_inside_even_odd(),
162 }
163 }
164
165 fn walk_crossings(&self, point: Point) -> crate::winding::Crossings {
167 let mut crossings = crate::winding::Crossings::default();
168 let mut index = 0usize;
169 let mut cursor = Point::ZERO;
170 let mut contour_start = Point::ZERO;
171 let mut contour_open = false;
172 for verb in &self.verbs {
173 match verb {
174 Verb::Move => {
175 if contour_open {
178 crossings.line(cursor, contour_start, point);
179 }
180 contour_open = true;
181 contour_start = self.points[index];
182 cursor = contour_start;
183 index += 1;
184 }
185 Verb::Line => {
186 crossings.line(cursor, self.points[index], point);
187 cursor = self.points[index];
188 index += 1;
189 }
190 Verb::Quad => {
191 crossings.quad(cursor, self.points[index], self.points[index + 1], point);
192 cursor = self.points[index + 1];
193 index += 2;
194 }
195 Verb::Cubic => {
196 crossings.cubic(
197 cursor,
198 self.points[index],
199 self.points[index + 1],
200 self.points[index + 2],
201 point,
202 );
203 cursor = self.points[index + 2];
204 index += 3;
205 }
206 Verb::Close => {
207 crossings.line(cursor, contour_start, point);
208 cursor = contour_start;
209 contour_open = false;
210 }
211 }
212 }
213 if contour_open {
214 crossings.line(cursor, contour_start, point);
215 }
216 crossings
217 }
218
219 pub fn measure(&self, tolerance: f32) -> Vec<crate::ContourMeasure> {
224 self.flatten(tolerance)
225 .iter()
226 .filter_map(crate::ContourMeasure::of)
227 .collect()
228 }
229
230 pub fn flatten(&self, tolerance: f32) -> Vec<Contour> {
234 let mut out = Flattener::new(tolerance.max(1e-4));
235 let mut i = 0usize;
236 for verb in &self.verbs {
237 match verb {
238 Verb::Move => {
239 out.move_to(self.points[i]);
240 i += 1;
241 }
242 Verb::Line => {
243 out.line_to(self.points[i]);
244 i += 1;
245 }
246 Verb::Quad => {
247 out.quad_to(self.points[i], self.points[i + 1]);
248 i += 2;
249 }
250 Verb::Cubic => {
251 out.cubic_to(self.points[i], self.points[i + 1], self.points[i + 2]);
252 i += 3;
253 }
254 Verb::Close => out.close(),
255 }
256 }
257 out.finish()
258 }
259}
260
261#[derive(Default)]
262struct TightBounds(Option<(f32, f32, f32, f32)>);
263
264impl TightBounds {
265 fn include(&mut self, point: Point) {
266 self.0 = Some(match self.0 {
267 Some((left, top, right, bottom)) => (
268 left.min(point.x),
269 top.min(point.y),
270 right.max(point.x),
271 bottom.max(point.y),
272 ),
273 None => (point.x, point.y, point.x, point.y),
274 });
275 }
276
277 fn rect(self) -> Rect {
278 self.0
279 .map_or_else(Rect::default, |(left, top, right, bottom)| {
280 Rect::from_ltrb(left, top, right, bottom)
281 })
282 }
283}
284
285fn include_quadratic_extrema(bounds: &mut TightBounds, start: Point, control: Point, end: Point) {
286 bounds.include(start);
287 bounds.include(end);
288 for (start_axis, control_axis, end_axis) in
289 [(start.x, control.x, end.x), (start.y, control.y, end.y)]
290 {
291 let denominator = start_axis as f64 - 2.0 * control_axis as f64 + end_axis as f64;
292 if denominator == 0.0 {
293 continue;
294 }
295 let parameter = ((start_axis as f64 - control_axis as f64) / denominator) as f32;
296 if parameter > 0.0 && parameter < 1.0 {
297 bounds.include(eval_quad(start, control, end, parameter));
298 }
299 }
300}
301
302fn include_cubic_extrema(
303 bounds: &mut TightBounds,
304 start: Point,
305 first: Point,
306 second: Point,
307 end: Point,
308) {
309 bounds.include(start);
310 bounds.include(end);
311 for (start_axis, first_axis, second_axis, end_axis) in [
312 (start.x, first.x, second.x, end.x),
313 (start.y, first.y, second.y, end.y),
314 ] {
315 for parameter in cubic_extrema(start_axis, first_axis, second_axis, end_axis)
316 .into_iter()
317 .flatten()
318 {
319 if parameter > 0.0 && parameter < 1.0 {
320 bounds.include(eval_cubic(start, first, second, end, parameter));
321 }
322 }
323 }
324}
325
326fn cubic_extrema(start: f32, first: f32, second: f32, end: f32) -> [Option<f32>; 2] {
327 let start = start as f64;
328 let first = first as f64;
329 let second = second as f64;
330 let end = end as f64;
331 let quadratic = -start + 3.0 * first - 3.0 * second + end;
332 let linear = 2.0 * (start - 2.0 * first + second);
333 let constant = first - start;
334 if quadratic == 0.0 {
335 return [unit_root(-constant, linear), None];
336 }
337 let discriminant = linear * linear - 4.0 * quadratic * constant;
338 if discriminant < 0.0 || !discriminant.is_finite() {
339 return [None, None];
340 }
341
342 let root = discriminant.sqrt();
345 let q = -0.5 * (linear + root.copysign(linear));
346 let first_root = unit_root(q, quadratic);
347 let second_root = unit_root(constant, q).filter(|value| Some(*value) != first_root);
348 [first_root, second_root]
349}
350
351fn unit_root(numerator: f64, denominator: f64) -> Option<f32> {
352 if denominator == 0.0 {
353 return None;
354 }
355 let value = numerator / denominator;
356 (value.is_finite() && value > 0.0 && value < 1.0).then_some(value as f32)
357}
358
359#[derive(Clone, Default)]
361pub struct PathBuilder {
362 verbs: Vec<Verb>,
363 points: Vec<Point>,
364 bounds: Option<Rect>,
365 resume_point: Option<Point>,
380 contour_open: bool,
381}
382
383impl PathBuilder {
384 pub fn new() -> Self {
385 Self::default()
386 }
387
388 pub fn move_to(&mut self, p: impl Into<Point>) -> &mut Self {
389 let p = p.into();
390 self.verbs.push(Verb::Move);
391 self.push_point(p);
392 self.resume_point = Some(p);
393 self.contour_open = true;
394 self
395 }
396
397 pub fn line_to(&mut self, p: impl Into<Point>) -> &mut Self {
398 let p = p.into();
399 self.ensure_contour(p);
400 self.verbs.push(Verb::Line);
401 self.push_point(p);
402 self
403 }
404
405 pub fn quad_to(&mut self, c: impl Into<Point>, p: impl Into<Point>) -> &mut Self {
406 let (c, p) = (c.into(), p.into());
407 self.ensure_contour(c);
408 self.verbs.push(Verb::Quad);
409 self.push_point(c);
410 self.push_point(p);
411 self
412 }
413
414 pub fn cubic_to(
415 &mut self,
416 c1: impl Into<Point>,
417 c2: impl Into<Point>,
418 p: impl Into<Point>,
419 ) -> &mut Self {
420 let (c1, c2, p) = (c1.into(), c2.into(), p.into());
421 self.ensure_contour(c1);
422 self.verbs.push(Verb::Cubic);
423 self.push_point(c1);
424 self.push_point(c2);
425 self.push_point(p);
426 self
427 }
428
429 pub fn close(&mut self) -> &mut Self {
430 if self.contour_open {
431 self.verbs.push(Verb::Close);
432 self.contour_open = false;
433 }
434 self
435 }
436
437 pub fn rect(&mut self, r: Rect) -> &mut Self {
440 self.move_to((r.x, r.y))
441 .line_to((r.right(), r.y))
442 .line_to((r.right(), r.bottom()))
443 .line_to((r.x, r.bottom()))
444 .close();
445 self.resume_point = Some(Point::new(r.x, r.y));
449 self
450 }
451
452 pub fn rrect(&mut self, r: Rect, radius: f32) -> &mut Self {
454 self.rrect_radii(r, [radius; 4])
455 }
456
457 pub fn rrect_radii(&mut self, r: Rect, radii: [f32; 4]) -> &mut Self {
460 self.rrect_radii_elliptical(r, radii.map(|radius| [radius; 2]))
461 }
462
463 pub fn rrect_radii_elliptical(
468 &mut self,
469 r: impl Into<Rect>,
470 radii: [[f32; 2]; 4],
471 ) -> &mut Self {
472 self.rrect_radii_elliptical_wound(r, radii, Winding::Clockwise)
473 }
474
475 pub fn rrect_radii_elliptical_wound(
484 &mut self,
485 r: impl Into<Rect>,
486 radii: [[f32; 2]; 4],
487 winding: Winding,
488 ) -> &mut Self {
489 let r = r.into();
490 let [tl, tr, br, bl] = constrain_radii_elliptical(&r, radii);
491 let (l, t, rr, b) = (r.x, r.y, r.right(), r.bottom());
492 if [tl, tr, br, bl].iter().all(|[x, y]| *x == 0.0 && *y == 0.0) {
493 match winding {
494 Winding::Clockwise => self.rect(r),
495 Winding::CounterClockwise => self
496 .move_to((l, t))
497 .line_to((l, b))
498 .line_to((rr, b))
499 .line_to((rr, t))
500 .close(),
501 };
502 self.resume_point = Some(Point::new(l, t));
503 return self;
504 }
505 let k = |rad: f32| rad * (1.0 - KAPPA);
508 match winding {
509 Winding::Clockwise => self
510 .move_to((l + tl[0], t))
511 .line_to((rr - tr[0], t))
512 .cubic_to((rr - k(tr[0]), t), (rr, t + k(tr[1])), (rr, t + tr[1]))
513 .line_to((rr, b - br[1]))
514 .cubic_to((rr, b - k(br[1])), (rr - k(br[0]), b), (rr - br[0], b))
515 .line_to((l + bl[0], b))
516 .cubic_to((l + k(bl[0]), b), (l, b - k(bl[1])), (l, b - bl[1]))
517 .line_to((l, t + tl[1]))
518 .cubic_to((l, t + k(tl[1])), (l + k(tl[0]), t), (l + tl[0], t))
519 .close(),
520 Winding::CounterClockwise => self
524 .move_to((l + tl[0], t))
525 .cubic_to((l + k(tl[0]), t), (l, t + k(tl[1])), (l, t + tl[1]))
526 .line_to((l, b - bl[1]))
527 .cubic_to((l, b - k(bl[1])), (l + k(bl[0]), b), (l + bl[0], b))
528 .line_to((rr - br[0], b))
529 .cubic_to((rr - k(br[0]), b), (rr, b - k(br[1])), (rr, b - br[1]))
530 .line_to((rr, t + tr[1]))
531 .cubic_to((rr, t + k(tr[1])), (rr - k(tr[0]), t), (rr - tr[0], t))
532 .line_to((l + tl[0], t))
533 .close(),
534 };
535 self.resume_point = Some(Point::new(l, t));
548 self
549 }
550
551 pub fn arc(
556 &mut self,
557 center: impl Into<Point>,
558 radius: f32,
559 start_angle: f32,
560 sweep_angle: f32,
561 ) -> &mut Self {
562 self.ellipse(center, [radius; 2], 0.0, start_angle, sweep_angle)
563 }
564
565 pub fn ellipse(
581 &mut self,
582 center: impl Into<Point>,
583 radii: [f32; 2],
584 x_axis_rotation: f32,
585 start_angle: f32,
586 sweep_angle: f32,
587 ) -> &mut Self {
588 let center = center.into();
589 let [radius_x, radius_y] = radii;
590 let finite = center.x.is_finite()
595 && center.y.is_finite()
596 && radius_x.is_finite()
597 && radius_y.is_finite()
598 && x_axis_rotation.is_finite()
599 && start_angle.is_finite()
600 && sweep_angle.is_finite();
601 debug_assert!(
602 radius_x >= 0.0 && radius_y >= 0.0,
603 "negative radii draw nothing; Canvas2D throws here"
604 );
605 if !finite || radius_x < 0.0 || radius_y < 0.0 {
606 return self;
607 }
608
609 let full_turn = std::f32::consts::TAU;
614 let sweep_angle = sweep_angle.clamp(-full_turn, full_turn);
615
616 let unit_circle_to_ellipse = unit_circle_map(center, radii, x_axis_rotation);
617 let first = unit_circle_to_ellipse.map_point(unit_circle_point(start_angle));
618 if self.contour_open || self.resume_point.is_some() {
624 self.ensure_contour(first);
625 self.line_to(first);
626 } else {
627 self.move_to(first);
628 }
629 if sweep_angle != 0.0 {
630 self.push_arc_cubics(&unit_circle_to_ellipse, start_angle, sweep_angle);
631 }
632 if sweep_angle.abs() >= full_turn {
635 self.close();
636 }
637 self
638 }
639
640 pub fn arc_to(
647 &mut self,
648 corner: impl Into<Point>,
649 next: impl Into<Point>,
650 radius: f32,
651 ) -> &mut Self {
652 let (corner, next) = (corner.into(), next.into());
653 self.ensure_contour(corner);
654 let start = *self.points.last().expect("ensure_contour opened a contour");
655
656 let incoming = normalize(
660 corner.x as f64 - start.x as f64,
661 corner.y as f64 - start.y as f64,
662 );
663 let outgoing = normalize(
664 next.x as f64 - corner.x as f64,
665 next.y as f64 - corner.y as f64,
666 );
667 let (Some(incoming), Some(outgoing)) = (incoming, outgoing) else {
668 return self.line_to(corner);
669 };
670 let cosine = incoming.0 * outgoing.0 + incoming.1 * outgoing.1;
671 let sine = incoming.0 * outgoing.1 - incoming.1 * outgoing.0;
672 if radius <= 0.0 || !radius.is_finite() || sine.abs() < 1.0 / (1 << 12) as f64 {
673 return self.line_to(corner);
674 }
675
676 let tangent_length = (radius as f64 * (1.0 - cosine) / sine).abs();
677 let entry = Point::new(
678 corner.x - (tangent_length * incoming.0) as f32,
679 corner.y - (tangent_length * incoming.1) as f32,
680 );
681 let turn = sine.signum() as f32;
683 let center = Point::new(
684 entry.x + radius * turn * -(incoming.1 as f32),
685 entry.y + radius * turn * incoming.0 as f32,
686 );
687 let exit = Point::new(
688 corner.x + (tangent_length * outgoing.0) as f32,
689 corner.y + (tangent_length * outgoing.1) as f32,
690 );
691
692 let start_angle = (entry.y - center.y).atan2(entry.x - center.x);
693 let end_angle = (exit.y - center.y).atan2(exit.x - center.x);
694 let sweep = shortest_sweep(start_angle, end_angle, turn);
695
696 self.line_to(entry);
697 let map = unit_circle_map(center, [radius; 2], 0.0);
698 self.push_arc_cubics(&map, start_angle, sweep);
699 self
700 }
701
702 pub fn circle(&mut self, center: impl Into<Point>, radius: f32) -> &mut Self {
703 let c = center.into();
704 let (r, k) = (radius, radius * KAPPA);
705 self.move_to((c.x + r, c.y))
706 .cubic_to((c.x + r, c.y + k), (c.x + k, c.y + r), (c.x, c.y + r))
707 .cubic_to((c.x - k, c.y + r), (c.x - r, c.y + k), (c.x - r, c.y))
708 .cubic_to((c.x - r, c.y - k), (c.x - k, c.y - r), (c.x, c.y - r))
709 .cubic_to((c.x + k, c.y - r), (c.x + r, c.y - k), (c.x + r, c.y))
710 .close()
711 }
712
713 pub fn append(&mut self, path: &Path, transform: &Matrix) -> &mut Self {
719 if path.verbs.is_empty() {
720 return self;
723 }
724 let mut point = path.points.iter();
725 let mut cursor = Point::ZERO;
726 let mut contour_start = Point::ZERO;
727 for verb in &path.verbs {
728 let count = match verb {
729 Verb::Move | Verb::Line => 1,
730 Verb::Quad => 2,
731 Verb::Cubic => 3,
732 Verb::Close => 0,
733 };
734 self.verbs.push(*verb);
735 for _ in 0..count {
736 let Some(&p) = point.next() else {
737 return self;
738 };
739 cursor = transform.map_point(p);
740 self.push_point(cursor);
741 }
742 match verb {
743 Verb::Move => contour_start = cursor,
744 Verb::Close => cursor = contour_start,
745 _ => {}
746 }
747 }
748 if matches!(path.verbs.last(), Some(Verb::Close)) {
759 self.move_to(cursor);
760 } else {
761 self.resume_point = Some(contour_start);
765 self.contour_open = true;
766 }
767 self
768 }
769
770 pub fn build(self) -> Arc<Path> {
771 Arc::new(Path {
772 verbs: self.verbs,
773 points: self.points,
774 bounds: self.bounds.unwrap_or_default(),
775 })
776 }
777
778 fn push_arc_cubics(&mut self, map: &Matrix, start_angle: f32, sweep_angle: f32) {
784 let piece_count = (sweep_angle.abs() / std::f32::consts::FRAC_PI_2)
785 .ceil()
786 .max(1.0);
787 let step = sweep_angle / piece_count;
788 let reach = 4.0 / 3.0 * (step / 4.0).tan();
791
792 let mut angle = start_angle;
793 for _ in 0..piece_count as u32 {
794 let (from, to) = (unit_circle_point(angle), unit_circle_point(angle + step));
795 let first = Point::new(from.x - reach * from.y, from.y + reach * from.x);
796 let second = Point::new(to.x + reach * to.y, to.y - reach * to.x);
797 self.cubic_to(
798 map.map_point(first),
799 map.map_point(second),
800 map.map_point(to),
801 );
802 angle += step;
803 }
804 }
805
806 fn ensure_contour(&mut self, p: Point) {
816 if self.contour_open {
817 return;
818 }
819 self.move_to(self.resume_point.unwrap_or(p));
820 }
821
822 fn push_point(&mut self, p: Point) {
823 self.points.push(p);
824 self.bounds = Some(match self.bounds {
828 Some(b) => Rect::from_ltrb(
829 b.x.min(p.x),
830 b.y.min(p.y),
831 b.right().max(p.x),
832 b.bottom().max(p.y),
833 ),
834 None => Rect::new(p.x, p.y, 0.0, 0.0),
835 });
836 }
837}
838
839const KAPPA: f32 = 0.552_284_8;
841
842fn unit_circle_point(angle: f32) -> Point {
844 let (sine, cosine) = angle.sin_cos();
845 Point::new(cosine, sine)
846}
847
848fn unit_circle_map(center: Point, radii: [f32; 2], rotation: f32) -> Matrix {
851 let [radius_x, radius_y] = radii;
852 let (sine, cosine) = rotation.sin_cos();
853 Matrix::from_affine(
854 radius_x * cosine,
855 radius_x * sine,
856 -radius_y * sine,
857 radius_y * cosine,
858 center.x,
859 center.y,
860 )
861}
862
863fn shortest_sweep(start: f32, end: f32, direction: f32) -> f32 {
867 let mut sweep = end - start;
868 let turn = std::f32::consts::TAU;
869 while sweep > 0.0 && direction < 0.0 {
870 sweep -= turn;
871 }
872 while sweep < 0.0 && direction > 0.0 {
873 sweep += turn;
874 }
875 sweep
876}
877
878fn normalize(x: f64, y: f64) -> Option<(f64, f64)> {
880 let length = (x * x + y * y).sqrt();
881 (length.is_finite() && length > 0.0).then(|| (x / length, y / length))
882}
883
884pub fn constrain_radii(r: &Rect, radii: [f32; 4]) -> [f32; 4] {
888 constrain_radii_elliptical(r, radii.map(|v| [v; 2])).map(|[x, _]| x)
889}
890
891pub fn constrain_radii_elliptical(r: &Rect, radii: [[f32; 2]; 4]) -> [[f32; 2]; 4] {
896 let [tl, tr, br, bl] = radii.map(|[x, y]| [x.max(0.0), y.max(0.0)]);
897 let fit = |side: f32, a: f32, b: f32| if a + b <= side { 1.0 } else { side / (a + b) };
898 let f = fit(r.width, tl[0], tr[0])
899 .min(fit(r.width, bl[0], br[0]))
900 .min(fit(r.height, tl[1], bl[1]))
901 .min(fit(r.height, tr[1], br[1]));
902 [tl, tr, br, bl].map(|[x, y]| [x * f, y * f])
903}
904
905struct Flattener {
910 tolerance: f32,
911 contours: Vec<Contour>,
912 current: Vec<Point>,
913 has_segments: bool,
915}
916
917impl Flattener {
918 fn new(tolerance: f32) -> Self {
919 Self {
920 tolerance,
921 contours: Vec::new(),
922 current: Vec::new(),
923 has_segments: false,
924 }
925 }
926
927 fn move_to(&mut self, p: Point) {
928 self.flush(false);
929 self.current.push(p);
930 self.has_segments = false;
931 }
932
933 fn line_to(&mut self, p: Point) {
934 self.current.push(p);
935 self.has_segments = true;
936 }
937
938 fn quad_to(&mut self, c: Point, p: Point) {
939 let Some(&start) = self.current.last() else {
940 return;
941 };
942 self.has_segments = true;
943 let dev = second_difference(start, c, p);
944 let n = segment_count((dev / (8.0 * self.tolerance)).sqrt());
945 for i in 1..=n {
946 let t = i as f32 / n as f32;
947 self.current.push(eval_quad(start, c, p, t));
948 }
949 }
950
951 fn cubic_to(&mut self, c1: Point, c2: Point, p: Point) {
952 let Some(&start) = self.current.last() else {
953 return;
954 };
955 self.has_segments = true;
956 let dev = second_difference(start, c1, c2).max(second_difference(c1, c2, p));
957 let n = segment_count((3.0 * dev / (4.0 * self.tolerance)).sqrt());
958 for i in 1..=n {
959 let t = i as f32 / n as f32;
960 self.current.push(eval_cubic(start, c1, c2, p, t));
961 }
962 }
963
964 fn close(&mut self) {
965 if let (Some(&first), Some(&last)) = (self.current.first(), self.current.last()) {
968 if self.current.len() >= 2 && (first.x, first.y) != (last.x, last.y) {
969 self.current.push(first);
970 }
971 }
972 if !self.current.is_empty() {
978 self.has_segments = true;
979 }
980 self.flush(true);
981 }
982
983 fn finish(mut self) -> Vec<Contour> {
984 self.flush(false);
985 self.contours
986 }
987
988 fn flush(&mut self, closed: bool) {
993 if !self.current.is_empty() {
994 self.contours.push(Contour {
995 points: std::mem::take(&mut self.current),
996 closed,
997 has_segments: self.has_segments,
998 });
999 }
1000 self.has_segments = false;
1001 }
1002}
1003
1004fn second_difference(a: Point, b: Point, c: Point) -> f32 {
1005 let dx = a.x - 2.0 * b.x + c.x;
1006 let dy = a.y - 2.0 * b.y + c.y;
1007 (dx * dx + dy * dy).sqrt()
1008}
1009
1010fn segment_count(estimate: f32) -> u32 {
1011 (estimate.ceil() as u32).clamp(1, 64)
1012}
1013
1014fn eval_quad(p0: Point, c: Point, p1: Point, t: f32) -> Point {
1015 let u = 1.0 - t;
1016 Point::new(
1017 u * u * p0.x + 2.0 * u * t * c.x + t * t * p1.x,
1018 u * u * p0.y + 2.0 * u * t * c.y + t * t * p1.y,
1019 )
1020}
1021
1022fn eval_cubic(p0: Point, c1: Point, c2: Point, p1: Point, t: f32) -> Point {
1023 let u = 1.0 - t;
1024 let (uu, tt) = (u * u, t * t);
1025 Point::new(
1026 u * uu * p0.x + 3.0 * uu * t * c1.x + 3.0 * u * tt * c2.x + t * tt * p1.x,
1027 u * uu * p0.y + 3.0 * uu * t * c1.y + 3.0 * u * tt * c2.y + t * tt * p1.y,
1028 )
1029}
1030
1031pub fn local_tolerance(transform: &Matrix) -> f32 {
1034 0.25 / transform.max_scale().max(1e-3)
1035}
1036
1037#[cfg(test)]
1038mod tests {
1039 use super::*;
1040
1041 #[test]
1048 fn opposed_windings_cancel_under_the_non_zero_rule() {
1049 let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
1050 let radii = [[12.0, 12.0]; 4];
1051 let inside = Point::new(50.0, 50.0);
1052
1053 let mut opposed = PathBuilder::new();
1054 opposed.rrect_radii_elliptical_wound(rect, radii, Winding::Clockwise);
1055 opposed.rrect_radii_elliptical_wound(rect, radii, Winding::CounterClockwise);
1056 assert!(
1057 !opposed.build().contains(inside, FillRule::NonZero),
1058 "opposed windings must cancel"
1059 );
1060
1061 let mut agreeing = PathBuilder::new();
1062 agreeing.rrect_radii_elliptical_wound(rect, radii, Winding::Clockwise);
1063 agreeing.rrect_radii_elliptical_wound(rect, radii, Winding::Clockwise);
1064 assert!(
1065 agreeing.build().contains(inside, FillRule::NonZero),
1066 "agreeing windings must reinforce"
1067 );
1068 }
1069
1070 #[test]
1074 fn winding_reverses_the_walk_without_moving_the_outline() {
1075 let rect = Rect::new(10.0, 20.0, 80.0, 60.0);
1076 let radii = [[8.0, 14.0], [4.0, 4.0], [20.0, 6.0], [0.0, 0.0]];
1077 let wound = |winding| {
1078 let mut path = PathBuilder::new();
1079 path.rrect_radii_elliptical_wound(rect, radii, winding);
1080 path.build()
1081 };
1082 let clockwise = wound(Winding::Clockwise);
1083 let counter = wound(Winding::CounterClockwise);
1084 assert_eq!(clockwise.tight_bounds(), counter.tight_bounds());
1085 for point in [
1088 Point::new(50.0, 50.0),
1089 Point::new(14.0, 30.0),
1090 Point::new(86.0, 24.0),
1091 Point::new(74.0, 76.0),
1092 Point::new(12.0, 78.0),
1093 Point::new(5.0, 15.0),
1094 Point::new(95.0, 85.0),
1095 ] {
1096 assert_eq!(
1097 clockwise.contains(point, FillRule::NonZero),
1098 counter.contains(point, FillRule::NonZero),
1099 "the two directions disagree about {point:?}"
1100 );
1101 }
1102 }
1103
1104 #[test]
1112 fn a_segment_after_close_resumes_at_the_contour_origin() {
1113 let mut path = PathBuilder::new();
1114 path.move_to((10.0, 10.0));
1115 path.line_to((30.0, 10.0));
1116 path.close();
1117 path.line_to((30.0, 30.0));
1118 let path = path.build();
1119
1120 let contours = path.flatten(0.05);
1122 let resumed = contours.last().expect("the path continues after the close");
1123 assert_eq!(
1124 resumed.points.first().copied(),
1125 Some(Point::new(10.0, 10.0)),
1126 "the segment after close must start at the contour origin, not its own end"
1127 );
1128 assert!(crate::stroke_contains(
1129 &contours,
1130 &crate::Stroke::new(6.0),
1131 0.05,
1132 Point::new(20.0, 20.0)
1133 ));
1134 }
1135
1136 #[test]
1144 fn a_segment_after_a_shape_helper_resumes_at_the_box_corner() {
1145 let box_corner = Point::new(10.0, 10.0);
1146 for corner in [0.0f32, 8.0] {
1147 let mut path = PathBuilder::new();
1148 if corner == 0.0 {
1149 path.rect(Rect::new(10.0, 10.0, 40.0, 40.0));
1150 } else {
1151 path.rrect_radii_elliptical(Rect::new(10.0, 10.0, 40.0, 40.0), [[corner; 2]; 4]);
1152 }
1153 path.line_to((90.0, 90.0));
1154
1155 let contours = path.build().flatten(0.05);
1156 let resumed = contours.last().expect("the path continues after the shape");
1157 assert_eq!(
1158 resumed.points.first().copied(),
1159 Some(box_corner),
1160 "corner radius {corner}: the trailing segment starts at (x, y)"
1161 );
1162 }
1163
1164 let mut path = PathBuilder::new();
1168 path.rrect_radii_elliptical(Rect::new(10.0, 10.0, 40.0, 40.0), [[8.0; 2]; 4]);
1169 path.line_to((90.0, 90.0));
1170 let contours = path.build().flatten(0.05);
1171 let stroke = crate::Stroke::new(4.0);
1172 assert!(
1173 crate::stroke_contains(&contours, &stroke, 0.05, Point::new(50.0, 50.0)),
1174 "the diagonal from (10,10) must be stroked"
1175 );
1176 assert!(
1177 !crate::stroke_contains(&contours, &stroke, 0.05, Point::new(54.0, 50.0)),
1178 "the diagonal from the tangent (18,10) must not be"
1179 );
1180 }
1181
1182 #[test]
1185 fn close_still_resumes_at_the_contour_origin() {
1186 let mut path = PathBuilder::new();
1187 path.move_to((10.0, 10.0));
1188 path.line_to((30.0, 10.0));
1189 path.line_to((30.0, 30.0));
1190 path.close();
1191 path.line_to((90.0, 90.0));
1192 let contours = path.build().flatten(0.05);
1193 assert_eq!(
1194 contours
1195 .last()
1196 .and_then(|contour| contour.points.first())
1197 .copied(),
1198 Some(Point::new(10.0, 10.0)),
1199 "close resumes where the contour began, not at any box corner"
1200 );
1201 }
1202
1203 #[test]
1206 fn a_first_segment_with_no_contour_starts_at_its_own_point() {
1207 let mut path = PathBuilder::new();
1208 path.line_to((30.0, 30.0));
1209 assert_eq!(
1210 path.build().bounds(),
1211 Rect::from_ltrb(30.0, 30.0, 30.0, 30.0)
1212 );
1213 }
1214
1215 #[test]
1216 fn append_carries_verbs_through_the_transform() {
1217 let mut source = PathBuilder::new();
1218 source.rect(Rect::new(0.0, 0.0, 10.0, 10.0));
1219 let source = source.build();
1220
1221 let mut target = PathBuilder::new();
1222 target.rect(Rect::new(0.0, 0.0, 4.0, 4.0));
1223 target.append(&source, &Matrix::translation(100.0, 50.0));
1224 let target = target.build();
1225
1226 assert_eq!(target.bounds(), Rect::from_ltrb(0.0, 0.0, 110.0, 60.0));
1227 assert!(target.contains(Point::new(105.0, 55.0), FillRule::NonZero));
1228 assert!(!target.contains(Point::new(5.0, 5.0), FillRule::NonZero));
1229 }
1230
1231 #[test]
1235 fn appending_a_closed_contour_reopens_at_its_seam() {
1236 let mut source = PathBuilder::new();
1237 source.move_to((10.0, 10.0));
1238 source.line_to((20.0, 10.0));
1239 source.close();
1240 let source = source.build();
1241
1242 let mut target = PathBuilder::new();
1243 target.append(&source, &Matrix::IDENTITY);
1244 target.line_to((10.0, 40.0));
1245 let built = target.build();
1246
1247 assert_eq!(built.bounds(), Rect::from_ltrb(10.0, 10.0, 20.0, 40.0));
1249 assert!(built.contains(Point::new(10.0, 25.0), FillRule::NonZero));
1250 }
1251
1252 #[test]
1256 fn appending_an_open_contour_hands_over_its_origin() {
1257 let mut source = PathBuilder::new();
1258 source.move_to((50.0, 50.0));
1259 source.line_to((60.0, 50.0));
1260 let source = source.build();
1261
1262 let mut target = PathBuilder::new();
1263 target.move_to((0.0, 0.0));
1264 target.line_to((10.0, 0.0));
1265 target.append(&source, &Matrix::IDENTITY);
1266 target.close();
1267 target.line_to((90.0, 90.0));
1268
1269 let contours = target.build().flatten(0.05);
1270 let resumed = contours.last().expect("the path continues after the close");
1271 assert_eq!(
1272 resumed.points.first().copied(),
1273 Some(Point::new(50.0, 50.0)),
1274 "the resumed segment must start at the APPENDED contour's origin"
1275 );
1276 }
1277
1278 #[test]
1279 fn appending_nothing_leaves_an_open_contour_open() {
1280 let empty = PathBuilder::new().build();
1281 let mut target = PathBuilder::new();
1282 target.move_to((0.0, 0.0));
1283 target.line_to((10.0, 0.0));
1284 target.append(&empty, &Matrix::IDENTITY);
1285 target.line_to((10.0, 10.0));
1286 assert_eq!(
1287 target.build().bounds(),
1288 Rect::from_ltrb(0.0, 0.0, 10.0, 10.0)
1289 );
1290 }
1291
1292 #[test]
1293 fn appending_an_open_contour_leaves_it_open() {
1294 let mut source = PathBuilder::new();
1295 source.move_to((0.0, 0.0));
1296 source.line_to((10.0, 0.0));
1297 let source = source.build();
1298
1299 let mut target = PathBuilder::new();
1300 target.append(&source, &Matrix::IDENTITY);
1301 target.line_to((10.0, 10.0));
1304 assert_eq!(
1305 target.build().bounds(),
1306 Rect::from_ltrb(0.0, 0.0, 10.0, 10.0)
1307 );
1308 }
1309
1310 #[test]
1311 fn tight_bounds_use_curve_extrema_not_control_points() {
1312 let mut path = PathBuilder::new();
1313 path.move_to((0.0, 0.0));
1314 path.quad_to((100.0, 100.0), (200.0, 0.0));
1315 let path = path.build();
1316 assert_eq!(path.bounds(), Rect::new(0.0, 0.0, 200.0, 100.0));
1317 assert_eq!(path.tight_bounds(), Rect::new(0.0, 0.0, 200.0, 50.0));
1318 }
1319
1320 #[test]
1321 fn tight_bounds_keep_extrema_below_f32_epsilon() {
1322 let mut path = PathBuilder::new();
1323 path.move_to((0.0, 0.0));
1324 path.quad_to((0.0, 1.0e-8), (0.0, 0.0));
1325 let bounds = path.build().tight_bounds();
1326 assert!((bounds.height - 5.0e-9).abs() < 1.0e-12);
1327 }
1328
1329 #[test]
1330 fn cubic_extrema_preserve_the_small_root() {
1331 let roots = cubic_extrema(0.0, 1.0e-8, -0.5, -0.5);
1332 assert!(roots
1333 .into_iter()
1334 .flatten()
1335 .any(|root| (root - 1.0e-8).abs() < 1.0e-10));
1336 }
1337
1338 #[test]
1339 fn radii_constrain_together() {
1340 let r = Rect::new(0.0, 0.0, 100.0, 40.0);
1341 let out = constrain_radii(&r, [40.0, 10.0, 10.0, 40.0]);
1343 assert_eq!(out, [20.0, 5.0, 5.0, 20.0]);
1344 assert_eq!(constrain_radii(&r, [8.0, 8.0, 8.0, 8.0]), [8.0; 4]);
1346 }
1347
1348 #[test]
1349 fn per_corner_rrect_stays_in_rect() {
1350 let r = Rect::new(10.0, 10.0, 100.0, 60.0);
1351 let mut b = PathBuilder::new();
1352 b.rrect_radii(r, [30.0, 0.0, 16.0, 8.0]);
1353 assert_eq!(b.build().bounds(), r);
1354 }
1355
1356 #[test]
1357 fn bounds_cover_control_points() {
1358 let mut b = PathBuilder::new();
1359 b.move_to((10.0, 10.0)).quad_to((50.0, -20.0), (90.0, 10.0));
1360 let p = b.build();
1361 assert_eq!(p.bounds(), Rect::from_ltrb(10.0, -20.0, 90.0, 10.0));
1362 }
1363
1364 #[test]
1365 fn circle_flattens_to_radius() {
1366 let mut b = PathBuilder::new();
1367 b.circle((0.0, 0.0), 100.0);
1368 let contours = b.build().flatten(0.1);
1369 assert_eq!(contours.len(), 1);
1370 assert!(contours[0].closed, "circle closes its contour");
1371 for p in &contours[0].points {
1372 let r = (p.x * p.x + p.y * p.y).sqrt();
1373 assert!((r - 100.0).abs() < 0.5, "point off circle: r={r}");
1374 }
1375 }
1376
1377 #[test]
1378 fn finer_tolerance_means_more_segments() {
1379 let path = {
1380 let mut b = PathBuilder::new();
1381 b.circle((0.0, 0.0), 100.0);
1382 b.build()
1383 };
1384 let coarse = path.flatten(2.0)[0].points.len();
1385 let fine = path.flatten(0.05)[0].points.len();
1386 assert!(fine > coarse, "fine {fine} vs coarse {coarse}");
1387 }
1388
1389 #[test]
1390 fn small_contours_survive_for_the_stroker() {
1391 let mut b = PathBuilder::new();
1392 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);
1395 assert_eq!(contours.len(), 2);
1396 assert_eq!(contours[0].points.len(), 2);
1397 assert!(!contours[0].closed);
1398 assert_eq!(contours[1].points.len(), 1);
1399 }
1400
1401 #[test]
1402 fn close_emits_the_closing_edge_and_marks_the_contour() {
1403 let mut b = PathBuilder::new();
1404 b.move_to((0.0, 0.0))
1405 .line_to((10.0, 0.0))
1406 .line_to((10.0, 10.0))
1407 .close();
1408 let contours = b.build().flatten(0.1);
1409 assert!(contours[0].closed);
1410 assert_eq!(contours[0].points.len(), 4, "closing edge in the polyline");
1411 assert_eq!(contours[0].points[3], Point::new(0.0, 0.0));
1412 }
1413
1414 #[test]
1415 fn bounds_keep_a_first_point_at_the_origin() {
1416 let mut b = PathBuilder::new();
1417 b.move_to((0.0, 0.0)).line_to((50.0, 80.0));
1418 assert_eq!(b.build().bounds(), Rect::from_ltrb(0.0, 0.0, 50.0, 80.0));
1419
1420 let mut b = PathBuilder::new();
1421 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));
1423 }
1424
1425 #[test]
1426 fn curve_without_move_starts_contour() {
1427 let mut b = PathBuilder::new();
1428 b.line_to((10.0, 0.0))
1429 .line_to((10.0, 10.0))
1430 .line_to((0.0, 10.0));
1431 let contours = b.build().flatten(0.1);
1432 assert_eq!(contours.len(), 1);
1433 assert_eq!(contours[0].points.len(), 4);
1434 }
1435
1436 #[test]
1440 fn circular_rrect_is_the_equal_axes_elliptical_case() {
1441 let r = Rect::new(10.0, 20.0, 120.0, 80.0);
1442 let radii = [24.0, 8.0, 30.0, 0.0];
1443 let mut circular = PathBuilder::new();
1444 circular.rrect_radii(r, radii);
1445 let mut elliptical = PathBuilder::new();
1446 elliptical.rrect_radii_elliptical(r, radii.map(|v| [v; 2]));
1447 assert_eq!(
1448 circular.build().flatten(0.1)[0].points,
1449 elliptical.build().flatten(0.1)[0].points,
1450 );
1451 }
1452
1453 #[test]
1454 fn elliptical_radii_constrain_per_axis() {
1455 let r = Rect::new(0.0, 0.0, 100.0, 40.0);
1458 let out = constrain_radii_elliptical(
1459 &r,
1460 [[10.0, 20.0], [10.0, 20.0], [10.0, 30.0], [10.0, 30.0]],
1461 );
1462 assert_eq!(out[0], [8.0, 16.0]);
1463 assert_eq!(out[2], [8.0, 24.0]);
1464 let out = constrain_radii_elliptical(&r, [[-5.0, 10.0], [0.0; 2], [0.0; 2], [0.0; 2]]);
1466 assert_eq!(out[0], [0.0, 10.0]);
1467 }
1468
1469 #[test]
1470 fn elliptical_corner_lands_on_axis_extremes() {
1471 let r = Rect::new(0.0, 0.0, 200.0, 100.0);
1474 let mut b = PathBuilder::new();
1475 b.rrect_radii_elliptical(r, [[0.0; 2], [40.0, 10.0], [0.0; 2], [0.0; 2]]);
1476 let points = &b.build().flatten(0.05)[0].points;
1477 assert!(points
1480 .iter()
1481 .any(|p| (p.x - 160.0).abs() < 0.5 && p.y.abs() < 0.5));
1482 assert!(points
1483 .iter()
1484 .any(|p| (p.x - 200.0).abs() < 0.5 && (p.y - 10.0).abs() < 0.5));
1485 }
1486
1487 #[test]
1492 fn swept_arc_stays_on_its_circle() {
1493 let (center, radius) = (Point::new(50.0, 60.0), 40.0);
1494 let mut b = PathBuilder::new();
1495 b.arc(center, radius, 0.0, std::f32::consts::TAU);
1496 for point in &b.build().flatten(0.01)[0].points {
1497 let offset = (point.x - center.x).hypot(point.y - center.y);
1498 assert!(
1499 (offset - radius).abs() < 0.05,
1500 "point {point:?} is {offset} from the centre, not {radius}"
1501 );
1502 }
1503 }
1504
1505 #[test]
1507 fn quarter_arc_ends_where_it_should() {
1508 let mut b = PathBuilder::new();
1509 b.arc((0.0, 0.0), 100.0, 0.0, std::f32::consts::FRAC_PI_2);
1510 let points = &b.build().flatten(0.01)[0].points;
1511 let (first, last) = (points[0], *points.last().unwrap());
1512 assert!(
1513 (first.x - 100.0).abs() < 0.01 && first.y.abs() < 0.01,
1514 "{first:?}"
1515 );
1516 assert!(
1517 last.x.abs() < 0.05 && (last.y - 100.0).abs() < 0.05,
1518 "{last:?}"
1519 );
1520 }
1521
1522 #[test]
1524 fn ellipse_reaches_both_radii() {
1525 let mut b = PathBuilder::new();
1526 b.ellipse((0.0, 0.0), [80.0, 20.0], 0.0, 0.0, std::f32::consts::TAU);
1527 let points = &b.build().flatten(0.01)[0].points;
1528 let widest = points.iter().fold(0.0f32, |m, p| m.max(p.x.abs()));
1529 let tallest = points.iter().fold(0.0f32, |m, p| m.max(p.y.abs()));
1530 assert!((widest - 80.0).abs() < 0.1, "widest {widest}");
1531 assert!((tallest - 20.0).abs() < 0.1, "tallest {tallest}");
1532 }
1533
1534 #[test]
1536 fn ellipse_rotation_swaps_the_axes() {
1537 let mut b = PathBuilder::new();
1538 b.ellipse(
1539 (0.0, 0.0),
1540 [80.0, 20.0],
1541 std::f32::consts::FRAC_PI_2,
1542 0.0,
1543 std::f32::consts::TAU,
1544 );
1545 let points = &b.build().flatten(0.01)[0].points;
1546 let widest = points.iter().fold(0.0f32, |m, p| m.max(p.x.abs()));
1547 let tallest = points.iter().fold(0.0f32, |m, p| m.max(p.y.abs()));
1548 assert!((widest - 20.0).abs() < 0.1, "widest {widest}");
1549 assert!((tallest - 80.0).abs() < 0.1, "tallest {tallest}");
1550 }
1551
1552 #[test]
1556 fn arc_to_rounds_a_right_angle() {
1557 let radius = 20.0f32;
1558 let mut b = PathBuilder::new();
1559 b.move_to((0.0, 0.0))
1560 .arc_to((100.0, 0.0), (100.0, 100.0), radius);
1561 let points = &b.build().flatten(0.01)[0].points;
1562
1563 let entry = Point::new(100.0 - radius, 0.0);
1564 let exit = Point::new(100.0, radius);
1565 assert!(points
1566 .iter()
1567 .any(|p| (p.x - entry.x).abs() < 0.1 && (p.y - entry.y).abs() < 0.1));
1568 assert!(points
1569 .iter()
1570 .any(|p| (p.x - exit.x).abs() < 0.1 && (p.y - exit.y).abs() < 0.1));
1571
1572 let center = Point::new(100.0 - radius, radius);
1573 for point in points.iter().filter(|p| p.x > entry.x - 0.01) {
1574 let offset = (point.x - center.x).hypot(point.y - center.y);
1575 assert!(
1576 (offset - radius).abs() < 0.1,
1577 "{point:?} is {offset} from the centre"
1578 );
1579 }
1580 }
1581
1582 #[test]
1585 fn degenerate_arc_to_falls_back_to_a_line() {
1586 for (corner, next, radius) in [
1587 ((50.0, 0.0), (100.0, 0.0), 20.0), ((50.0, 0.0), (50.0, 50.0), 0.0), ] {
1590 let mut b = PathBuilder::new();
1591 b.move_to((0.0, 0.0)).arc_to(corner, next, radius);
1592 let points = &b.build().flatten(0.01)[0].points;
1593 assert_eq!(points.len(), 2, "expected a bare line, got {points:?}");
1594 assert!((points[1].x - corner.0).abs() < 0.01 && (points[1].y - corner.1).abs() < 0.01);
1595 }
1596 }
1597
1598 #[test]
1601 fn rect_contains_what_it_covers() {
1602 let mut b = PathBuilder::new();
1603 b.rect(Rect::new(10.0, 10.0, 80.0, 60.0));
1604 let path = b.build();
1605 assert!(path.contains(Point::new(50.0, 40.0), FillRule::NonZero));
1606 assert!(!path.contains(Point::new(5.0, 40.0), FillRule::NonZero));
1607 assert!(!path.contains(Point::new(50.0, 80.0), FillRule::NonZero));
1608 for on_outline in [
1611 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), ] {
1617 assert!(
1618 path.contains(on_outline, FillRule::NonZero),
1619 "{on_outline:?} is on the outline and must count as inside"
1620 );
1621 }
1622 }
1623
1624 #[test]
1628 fn an_enormous_sweep_stays_one_turn() {
1629 let mut b = PathBuilder::new();
1630 b.arc((0.0, 0.0), 50.0, 0.0, 1e20);
1631 let path = b.build();
1632 let contours = path.flatten(0.1);
1633 assert_eq!(contours.len(), 1);
1634 assert!(
1636 contours[0].points.len() < 1_000,
1637 "a clamped turn should stay small, got {}",
1638 contours[0].points.len()
1639 );
1640 assert!(contours[0].closed, "a full turn closes its contour");
1641 }
1642
1643 #[test]
1644 fn a_negative_sweep_turns_the_other_way() {
1645 let quarter = std::f32::consts::FRAC_PI_2;
1646 let mut clockwise = PathBuilder::new();
1647 clockwise.arc((0.0, 0.0), 50.0, 0.0, quarter);
1648 let mut anticlockwise = PathBuilder::new();
1649 anticlockwise.arc((0.0, 0.0), 50.0, 0.0, -quarter);
1650
1651 let forward = clockwise.build().bounds();
1654 let backward = anticlockwise.build().bounds();
1655 assert!(forward.bottom() > 40.0, "positive sweep reaches +y");
1656 assert!(backward.y < -40.0, "negative sweep reaches -y");
1657 }
1658
1659 #[test]
1662 fn circle_containment_is_exact_all_the_way_round() {
1663 let (center, radius) = (Point::new(0.0, 0.0), 100.0f32);
1664 let mut b = PathBuilder::new();
1665 b.circle(center, radius);
1666 let path = b.build();
1667 for step in 0..64 {
1668 let angle = step as f32 / 64.0 * std::f32::consts::TAU;
1669 let (sine, cosine) = angle.sin_cos();
1670 let inside = Point::new(cosine * radius * 0.99, sine * radius * 0.99);
1671 let outside = Point::new(cosine * radius * 1.01, sine * radius * 1.01);
1672 assert!(
1673 path.contains(inside, FillRule::NonZero),
1674 "{inside:?} should be in"
1675 );
1676 assert!(
1677 !path.contains(outside, FillRule::NonZero),
1678 "{outside:?} should be out"
1679 );
1680 }
1681 }
1682
1683 #[test]
1687 fn fill_rules_disagree_about_a_same_wound_hole() {
1688 let mut b = PathBuilder::new();
1689 b.rect(Rect::new(0.0, 0.0, 100.0, 100.0));
1690 b.rect(Rect::new(25.0, 25.0, 50.0, 50.0));
1691 let path = b.build();
1692 let middle = Point::new(50.0, 50.0);
1693 assert!(path.contains(middle, FillRule::NonZero));
1694 assert!(!path.contains(middle, FillRule::EvenOdd));
1695 let ring = Point::new(10.0, 50.0);
1697 assert!(path.contains(ring, FillRule::NonZero));
1698 assert!(path.contains(ring, FillRule::EvenOdd));
1699 }
1700
1701 #[test]
1703 fn open_contour_closes_implicitly() {
1704 let mut b = PathBuilder::new();
1705 b.move_to((0.0, 0.0))
1706 .line_to((100.0, 0.0))
1707 .line_to((100.0, 100.0));
1708 let path = b.build();
1709 assert!(path.contains(Point::new(80.0, 40.0), FillRule::NonZero));
1710 assert!(!path.contains(Point::new(20.0, 60.0), FillRule::NonZero));
1711 }
1712
1713 #[test]
1715 fn containment_handles_curves_that_double_back() {
1716 let mut b = PathBuilder::new();
1717 b.move_to((0.0, 0.0))
1718 .cubic_to((120.0, 120.0), (-20.0, 120.0), (100.0, 0.0))
1719 .close();
1720 let path = b.build();
1721 assert!(path.contains(Point::new(50.0, 40.0), FillRule::NonZero));
1722 assert!(!path.contains(Point::new(50.0, -10.0), FillRule::NonZero));
1723 assert!(!path.contains(Point::new(-30.0, 40.0), FillRule::NonZero));
1724 }
1725}