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)]
35#[cfg_attr(feature = "serde", derive(serde::Serialize))]
36enum Verb {
37 Move,
38 Line,
39 Quad,
40 Conic,
44 Cubic,
45 Close,
46}
47
48#[derive(Clone, Debug, PartialEq)]
54pub struct Contour {
55 pub points: Vec<Point>,
57 pub closed: bool,
59 pub has_segments: bool,
63}
64
65#[derive(Clone, Debug)]
70#[cfg_attr(feature = "serde", derive(serde::Serialize))]
71pub struct Path {
72 verbs: Vec<Verb>,
73 points: Vec<Point>,
74 weights: Vec<f32>,
77 bounds: Rect,
80}
81
82impl Path {
83 pub fn bounds(&self) -> Rect {
85 self.bounds
86 }
87
88 pub fn tight_bounds(&self) -> Rect {
90 let mut bounds = TightBounds::default();
91 let mut point_index = 0usize;
92 let mut weight_index = 0usize;
93 let mut cursor = Point::ZERO;
94 let mut contour_start = Point::ZERO;
95 for verb in &self.verbs {
96 match verb {
97 Verb::Move => {
98 cursor = self.points[point_index];
99 contour_start = cursor;
100 point_index += 1;
101 bounds.include(cursor);
102 }
103 Verb::Line => {
104 cursor = self.points[point_index];
105 point_index += 1;
106 bounds.include(cursor);
107 }
108 Verb::Quad => {
109 let control = self.points[point_index];
110 let end = self.points[point_index + 1];
111 point_index += 2;
112 include_quadratic_extrema(&mut bounds, cursor, control, end);
113 cursor = end;
114 }
115 Verb::Conic => {
116 let control = self.points[point_index];
117 let end = self.points[point_index + 1];
118 let weight = self.weights[weight_index];
119 point_index += 2;
120 weight_index += 1;
121 include_conic_extrema(&mut bounds, cursor, control, end, weight);
122 cursor = end;
123 }
124 Verb::Cubic => {
125 let first = self.points[point_index];
126 let second = self.points[point_index + 1];
127 let end = self.points[point_index + 2];
128 point_index += 3;
129 include_cubic_extrema(&mut bounds, cursor, first, second, end);
130 cursor = end;
131 }
132 Verb::Close => {
133 cursor = contour_start;
134 bounds.include(cursor);
135 }
136 }
137 }
138 bounds.rect()
139 }
140
141 pub fn is_empty(&self) -> bool {
143 self.verbs.is_empty()
144 }
145
146 pub fn heap_bytes(&self) -> usize {
148 self.points.len() * std::mem::size_of::<Point>()
149 + self.weights.len() * std::mem::size_of::<f32>()
150 + self.verbs.len()
151 }
152
153 pub fn contains(&self, point: Point, fill_rule: FillRule) -> bool {
158 if !self.bounds.contains_inclusive(point) {
159 return false;
160 }
161 let crossings = self.walk_crossings(point);
162 match fill_rule {
163 FillRule::NonZero => crossings.is_inside_non_zero(),
164 FillRule::EvenOdd => crossings.is_inside_even_odd(),
165 }
166 }
167
168 fn walk_crossings(&self, point: Point) -> crate::winding::Crossings {
170 let mut crossings = crate::winding::Crossings::default();
171 let mut index = 0usize;
172 let mut weight_index = 0usize;
173 let mut cursor = Point::ZERO;
174 let mut contour_start = Point::ZERO;
175 let mut contour_open = false;
176 for verb in &self.verbs {
177 match verb {
178 Verb::Move => {
179 if contour_open {
182 crossings.line(cursor, contour_start, point);
183 }
184 contour_open = true;
185 contour_start = self.points[index];
186 cursor = contour_start;
187 index += 1;
188 }
189 Verb::Line => {
190 crossings.line(cursor, self.points[index], point);
191 cursor = self.points[index];
192 index += 1;
193 }
194 Verb::Quad => {
195 crossings.quad(cursor, self.points[index], self.points[index + 1], point);
196 cursor = self.points[index + 1];
197 index += 2;
198 }
199 Verb::Conic => {
200 crossings.conic(
201 cursor,
202 self.points[index],
203 self.points[index + 1],
204 self.weights[weight_index],
205 point,
206 );
207 cursor = self.points[index + 1];
208 index += 2;
209 weight_index += 1;
210 }
211 Verb::Cubic => {
212 crossings.cubic(
213 cursor,
214 self.points[index],
215 self.points[index + 1],
216 self.points[index + 2],
217 point,
218 );
219 cursor = self.points[index + 2];
220 index += 3;
221 }
222 Verb::Close => {
223 crossings.line(cursor, contour_start, point);
224 cursor = contour_start;
225 contour_open = false;
226 }
227 }
228 }
229 if contour_open {
230 crossings.line(cursor, contour_start, point);
231 }
232 crossings
233 }
234
235 pub fn measure(&self, tolerance: f32) -> Vec<crate::ContourMeasure> {
239 self.flatten(tolerance)
240 .iter()
241 .filter_map(crate::ContourMeasure::of)
242 .collect()
243 }
244
245 pub fn flatten(&self, tolerance: f32) -> Vec<Contour> {
251 let mut out = Flattener::new(tolerance.max(1e-4));
252 let mut i = 0usize;
253 let mut weight_index = 0usize;
254 for verb in &self.verbs {
255 match verb {
256 Verb::Move => {
257 out.move_to(self.points[i]);
258 i += 1;
259 }
260 Verb::Line => {
261 out.line_to(self.points[i]);
262 i += 1;
263 }
264 Verb::Quad => {
265 out.quad_to(self.points[i], self.points[i + 1]);
266 i += 2;
267 }
268 Verb::Conic => {
269 out.conic_to(
270 self.points[i],
271 self.points[i + 1],
272 self.weights[weight_index],
273 );
274 i += 2;
275 weight_index += 1;
276 }
277 Verb::Cubic => {
278 out.cubic_to(self.points[i], self.points[i + 1], self.points[i + 2]);
279 i += 3;
280 }
281 Verb::Close => out.close(),
282 }
283 }
284 out.finish()
285 }
286}
287
288#[derive(Default)]
289struct TightBounds(Option<(f32, f32, f32, f32)>);
290
291impl TightBounds {
292 fn include(&mut self, point: Point) {
293 self.0 = Some(match self.0 {
294 Some((left, top, right, bottom)) => (
295 left.min(point.x),
296 top.min(point.y),
297 right.max(point.x),
298 bottom.max(point.y),
299 ),
300 None => (point.x, point.y, point.x, point.y),
301 });
302 }
303
304 fn rect(self) -> Rect {
305 self.0
306 .map_or_else(Rect::default, |(left, top, right, bottom)| {
307 Rect::from_ltrb(left, top, right, bottom)
308 })
309 }
310}
311
312fn include_quadratic_extrema(bounds: &mut TightBounds, start: Point, control: Point, end: Point) {
313 bounds.include(start);
314 bounds.include(end);
315 for (start_axis, control_axis, end_axis) in
316 [(start.x, control.x, end.x), (start.y, control.y, end.y)]
317 {
318 let denominator = start_axis as f64 - 2.0 * control_axis as f64 + end_axis as f64;
319 if denominator == 0.0 {
320 continue;
321 }
322 let parameter = ((start_axis as f64 - control_axis as f64) / denominator) as f32;
323 if parameter > 0.0 && parameter < 1.0 {
324 bounds.include(eval_quad(start, control, end, parameter));
325 }
326 }
327}
328
329fn include_cubic_extrema(
330 bounds: &mut TightBounds,
331 start: Point,
332 first: Point,
333 second: Point,
334 end: Point,
335) {
336 bounds.include(start);
337 bounds.include(end);
338 for (start_axis, first_axis, second_axis, end_axis) in [
339 (start.x, first.x, second.x, end.x),
340 (start.y, first.y, second.y, end.y),
341 ] {
342 for parameter in cubic_extrema(start_axis, first_axis, second_axis, end_axis)
343 .into_iter()
344 .flatten()
345 {
346 if parameter > 0.0 && parameter < 1.0 {
347 bounds.include(eval_cubic(start, first, second, end, parameter));
348 }
349 }
350 }
351}
352
353fn include_conic_extrema(
354 bounds: &mut TightBounds,
355 start: Point,
356 control: Point,
357 end: Point,
358 weight: f32,
359) {
360 bounds.include(start);
361 bounds.include(end);
362 for (start_axis, control_axis, end_axis) in
363 [(start.x, control.x, end.x), (start.y, control.y, end.y)]
364 {
365 for parameter in conic_derivative_roots(start_axis, control_axis, end_axis, weight) {
366 let p = eval_conic(start, control, end, weight, parameter);
367 let clamp = |v: f32, a: f32, b: f32, c: f32| v.clamp(a.min(b).min(c), a.max(b).max(c));
371 bounds.include(Point::new(
372 clamp(p.x, start.x, control.x, end.x),
373 clamp(p.y, start.y, control.y, end.y),
374 ));
375 }
376 }
377}
378
379pub(crate) fn conic_derivative_roots(
385 start: f32,
386 control: f32,
387 end: f32,
388 weight: f32,
389) -> impl Iterator<Item = f32> {
390 let w = weight as f64;
397 let p10 = control as f64 - start as f64;
398 let p20 = end as f64 - start as f64;
399 let quadratic = (w - 1.0) * p20;
400 let linear = p20 - 2.0 * w * p10;
401 let constant = w * p10;
402
403 let mut roots = [None, None];
404 if quadratic == 0.0 {
405 roots[0] = unit_root(-constant, linear);
406 } else {
407 let discriminant = linear * linear - 4.0 * quadratic * constant;
408 if discriminant >= 0.0 && discriminant.is_finite() {
409 let root = discriminant.sqrt();
412 let q = -0.5 * (linear + root.copysign(linear));
413 roots[0] = unit_root(q, quadratic);
414 roots[1] = unit_root(constant, q).filter(|value| roots[0] != Some(*value));
415 }
416 }
417 roots.into_iter().flatten()
418}
419
420pub(crate) fn eval_conic(start: Point, control: Point, end: Point, weight: f32, t: f32) -> Point {
422 let u = 1.0 - t;
423 let b0 = u * u;
424 let b1 = 2.0 * weight * u * t;
425 let b2 = t * t;
426 let d = b0 + b1 + b2;
427 Point::new(
428 (b0 * start.x + b1 * control.x + b2 * end.x) / d,
429 (b0 * start.y + b1 * control.y + b2 * end.y) / d,
430 )
431}
432
433fn cubic_extrema(start: f32, first: f32, second: f32, end: f32) -> [Option<f32>; 2] {
434 let start = start as f64;
435 let first = first as f64;
436 let second = second as f64;
437 let end = end as f64;
438 let quadratic = -start + 3.0 * first - 3.0 * second + end;
439 let linear = 2.0 * (start - 2.0 * first + second);
440 let constant = first - start;
441 if quadratic == 0.0 {
442 return [unit_root(-constant, linear), None];
443 }
444 let discriminant = linear * linear - 4.0 * quadratic * constant;
445 if discriminant < 0.0 || !discriminant.is_finite() {
446 return [None, None];
447 }
448
449 let root = discriminant.sqrt();
452 let q = -0.5 * (linear + root.copysign(linear));
453 let first_root = unit_root(q, quadratic);
454 let second_root = unit_root(constant, q).filter(|value| Some(*value) != first_root);
455 [first_root, second_root]
456}
457
458fn unit_root(numerator: f64, denominator: f64) -> Option<f32> {
459 if denominator == 0.0 {
460 return None;
461 }
462 let value = numerator / denominator;
463 (value.is_finite() && value > 0.0 && value < 1.0).then_some(value as f32)
464}
465
466#[derive(Clone, Default)]
468pub struct PathBuilder {
469 verbs: Vec<Verb>,
470 points: Vec<Point>,
471 weights: Vec<f32>,
472 bounds: Option<Rect>,
473 resume_point: Option<Point>,
488 contour_open: bool,
489}
490
491impl PathBuilder {
492 pub fn new() -> Self {
494 Self::default()
495 }
496
497 pub fn move_to(&mut self, p: impl Into<Point>) -> &mut Self {
499 let p = p.into();
500 self.verbs.push(Verb::Move);
501 self.push_point(p);
502 self.resume_point = Some(p);
503 self.contour_open = true;
504 self
505 }
506
507 pub fn line_to(&mut self, p: impl Into<Point>) -> &mut Self {
509 let p = p.into();
510 self.ensure_contour(p);
511 self.verbs.push(Verb::Line);
512 self.push_point(p);
513 self
514 }
515
516 pub fn quad_to(&mut self, c: impl Into<Point>, p: impl Into<Point>) -> &mut Self {
518 let (c, p) = (c.into(), p.into());
519 self.ensure_contour(c);
520 self.verbs.push(Verb::Quad);
521 self.push_point(c);
522 self.push_point(p);
523 self
524 }
525
526 pub fn conic_to(&mut self, c: impl Into<Point>, p: impl Into<Point>, weight: f32) -> &mut Self {
539 let (c, p) = (c.into(), p.into());
540 if !weight.is_finite() {
541 return self.line_to(c).line_to(p);
542 }
543 if weight <= 0.0 {
544 return self.line_to(p);
545 }
546 if weight == 1.0 {
547 return self.quad_to(c, p);
548 }
549 self.ensure_contour(c);
550 self.verbs.push(Verb::Conic);
551 self.push_point(c);
552 self.push_point(p);
553 self.weights.push(weight);
554 self
555 }
556
557 pub fn cubic_to(
559 &mut self,
560 c1: impl Into<Point>,
561 c2: impl Into<Point>,
562 p: impl Into<Point>,
563 ) -> &mut Self {
564 let (c1, c2, p) = (c1.into(), c2.into(), p.into());
565 self.ensure_contour(c1);
566 self.verbs.push(Verb::Cubic);
567 self.push_point(c1);
568 self.push_point(c2);
569 self.push_point(p);
570 self
571 }
572
573 pub fn close(&mut self) -> &mut Self {
577 if self.contour_open {
578 self.verbs.push(Verb::Close);
579 self.contour_open = false;
580 }
581 self
582 }
583
584 pub fn rect(&mut self, r: Rect) -> &mut Self {
588 self.move_to((r.x, r.y))
589 .line_to((r.right(), r.y))
590 .line_to((r.right(), r.bottom()))
591 .line_to((r.x, r.bottom()))
592 .close();
593 self.resume_point = Some(Point::new(r.x, r.y));
597 self
598 }
599
600 pub fn rrect(&mut self, r: Rect, radius: f32) -> &mut Self {
602 self.rrect_radii(r, [radius; 4])
603 }
604
605 pub fn rrect_radii(&mut self, r: Rect, radii: [f32; 4]) -> &mut Self {
609 self.rrect_radii_elliptical(r, radii.map(|radius| [radius; 2]))
610 }
611
612 pub fn rrect_radii_elliptical(
617 &mut self,
618 r: impl Into<Rect>,
619 radii: [[f32; 2]; 4],
620 ) -> &mut Self {
621 self.rrect_radii_elliptical_wound(r, radii, Winding::Clockwise)
622 }
623
624 pub fn rrect_radii_elliptical_wound(
629 &mut self,
630 r: impl Into<Rect>,
631 radii: [[f32; 2]; 4],
632 winding: Winding,
633 ) -> &mut Self {
634 let r = r.into();
635 let [tl, tr, br, bl] = constrain_radii_elliptical(&r, radii);
636 let (l, t, rr, b) = (r.x, r.y, r.right(), r.bottom());
637 if [tl, tr, br, bl].iter().all(|[x, y]| *x == 0.0 && *y == 0.0) {
638 match winding {
639 Winding::Clockwise => self.rect(r),
640 Winding::CounterClockwise => self
641 .move_to((l, t))
642 .line_to((l, b))
643 .line_to((rr, b))
644 .line_to((rr, t))
645 .close(),
646 };
647 self.resume_point = Some(Point::new(l, t));
648 return self;
649 }
650 let w = std::f32::consts::FRAC_1_SQRT_2;
654 match winding {
655 Winding::Clockwise => self
656 .move_to((l + tl[0], t))
657 .line_to((rr - tr[0], t))
658 .conic_to((rr, t), (rr, t + tr[1]), w)
659 .line_to((rr, b - br[1]))
660 .conic_to((rr, b), (rr - br[0], b), w)
661 .line_to((l + bl[0], b))
662 .conic_to((l, b), (l, b - bl[1]), w)
663 .line_to((l, t + tl[1]))
664 .conic_to((l, t), (l + tl[0], t), w)
665 .close(),
666 Winding::CounterClockwise => self
670 .move_to((l + tl[0], t))
671 .conic_to((l, t), (l, t + tl[1]), w)
672 .line_to((l, b - bl[1]))
673 .conic_to((l, b), (l + bl[0], b), w)
674 .line_to((rr - br[0], b))
675 .conic_to((rr, b), (rr, b - br[1]), w)
676 .line_to((rr, t + tr[1]))
677 .conic_to((rr, t), (rr - tr[0], t), w)
678 .line_to((l + tl[0], t))
679 .close(),
680 };
681 self.resume_point = Some(Point::new(l, t));
694 self
695 }
696
697 pub fn rsuperellipse(&mut self, r: Rect, radius: f32) -> &mut Self {
703 self.rsuperellipse_radii(r, [[radius; 2]; 4])
704 }
705
706 pub fn rsuperellipse_radii(&mut self, r: Rect, radii: [[f32; 2]; 4]) -> &mut Self {
712 crate::superellipse::RoundSuperellipse::new(r, radii).emit(self);
713 self.resume_point = Some(Point::new(r.x, r.y));
714 self
715 }
716
717 pub fn arc(
722 &mut self,
723 center: impl Into<Point>,
724 radius: f32,
725 start_angle: f32,
726 sweep_angle: f32,
727 ) -> &mut Self {
728 self.ellipse(center, [radius; 2], 0.0, start_angle, sweep_angle)
729 }
730
731 pub fn ellipse(
740 &mut self,
741 center: impl Into<Point>,
742 radii: [f32; 2],
743 x_axis_rotation: f32,
744 start_angle: f32,
745 sweep_angle: f32,
746 ) -> &mut Self {
747 let center = center.into();
748 let [radius_x, radius_y] = radii;
749 let finite = center.x.is_finite()
754 && center.y.is_finite()
755 && radius_x.is_finite()
756 && radius_y.is_finite()
757 && x_axis_rotation.is_finite()
758 && start_angle.is_finite()
759 && sweep_angle.is_finite();
760 debug_assert!(
761 radius_x >= 0.0 && radius_y >= 0.0,
762 "negative radii draw nothing; Canvas2D throws here"
763 );
764 if !finite || radius_x < 0.0 || radius_y < 0.0 {
765 return self;
766 }
767
768 let full_turn = std::f32::consts::TAU;
773 let sweep_angle = sweep_angle.clamp(-full_turn, full_turn);
774
775 let unit_circle_to_ellipse = unit_circle_map(center, radii, x_axis_rotation);
776 let first = unit_circle_to_ellipse.map_point(unit_circle_point(start_angle));
777 if self.contour_open || self.resume_point.is_some() {
783 self.ensure_contour(first);
784 self.line_to(first);
785 } else {
786 self.move_to(first);
787 }
788 if sweep_angle != 0.0 {
789 self.push_arc_conics(&unit_circle_to_ellipse, start_angle, sweep_angle);
790 }
791 if sweep_angle.abs() >= full_turn {
794 self.close();
795 }
796 self
797 }
798
799 pub fn arc_to(
804 &mut self,
805 corner: impl Into<Point>,
806 next: impl Into<Point>,
807 radius: f32,
808 ) -> &mut Self {
809 let (corner, next) = (corner.into(), next.into());
810 self.ensure_contour(corner);
811 let start = *self.points.last().expect("ensure_contour opened a contour");
812
813 let incoming = normalize(
817 corner.x as f64 - start.x as f64,
818 corner.y as f64 - start.y as f64,
819 );
820 let outgoing = normalize(
821 next.x as f64 - corner.x as f64,
822 next.y as f64 - corner.y as f64,
823 );
824 let (Some(incoming), Some(outgoing)) = (incoming, outgoing) else {
825 return self.line_to(corner);
826 };
827 let cosine = incoming.0 * outgoing.0 + incoming.1 * outgoing.1;
828 let sine = incoming.0 * outgoing.1 - incoming.1 * outgoing.0;
829 if radius <= 0.0 || !radius.is_finite() || sine.abs() < 1.0 / (1 << 12) as f64 {
830 return self.line_to(corner);
831 }
832
833 let tangent_length = (radius as f64 * (1.0 - cosine) / sine).abs();
834 let entry = Point::new(
835 corner.x - (tangent_length * incoming.0) as f32,
836 corner.y - (tangent_length * incoming.1) as f32,
837 );
838 let turn = sine.signum() as f32;
840 let center = Point::new(
841 entry.x + radius * turn * -(incoming.1 as f32),
842 entry.y + radius * turn * incoming.0 as f32,
843 );
844 let exit = Point::new(
845 corner.x + (tangent_length * outgoing.0) as f32,
846 corner.y + (tangent_length * outgoing.1) as f32,
847 );
848
849 let start_angle = (entry.y - center.y).atan2(entry.x - center.x);
850 let end_angle = (exit.y - center.y).atan2(exit.x - center.x);
851 let sweep = shortest_sweep(start_angle, end_angle, turn);
852
853 self.line_to(entry);
854 let map = unit_circle_map(center, [radius; 2], 0.0);
855 self.push_arc_conics(&map, start_angle, sweep);
856 self
857 }
858
859 pub fn circle(&mut self, center: impl Into<Point>, radius: f32) -> &mut Self {
864 let c = center.into();
865 let r = radius;
866 let w = std::f32::consts::FRAC_1_SQRT_2;
867 self.move_to((c.x + r, c.y))
868 .conic_to((c.x + r, c.y + r), (c.x, c.y + r), w)
869 .conic_to((c.x - r, c.y + r), (c.x - r, c.y), w)
870 .conic_to((c.x - r, c.y - r), (c.x, c.y - r), w)
871 .conic_to((c.x + r, c.y - r), (c.x + r, c.y), w)
872 .close()
873 }
874
875 pub fn append(&mut self, path: &Path, transform: &Matrix) -> &mut Self {
880 if path.verbs.is_empty() {
881 return self;
884 }
885 let mut point = path.points.iter();
886 let mut weight = path.weights.iter();
887 let mut cursor = Point::ZERO;
888 let mut contour_start = Point::ZERO;
889 for verb in &path.verbs {
890 let count = match verb {
891 Verb::Move | Verb::Line => 1,
892 Verb::Quad | Verb::Conic => 2,
893 Verb::Cubic => 3,
894 Verb::Close => 0,
895 };
896 self.verbs.push(*verb);
897 for _ in 0..count {
898 let Some(&p) = point.next() else {
899 return self;
900 };
901 cursor = transform.map_point(p);
902 self.push_point(cursor);
903 }
904 if matches!(verb, Verb::Conic) {
905 let Some(&w) = weight.next() else {
910 return self;
911 };
912 self.weights.push(w);
913 }
914 match verb {
915 Verb::Move => contour_start = cursor,
916 Verb::Close => cursor = contour_start,
917 _ => {}
918 }
919 }
920 if matches!(path.verbs.last(), Some(Verb::Close)) {
931 self.move_to(cursor);
932 } else {
933 self.resume_point = Some(contour_start);
937 self.contour_open = true;
938 }
939 self
940 }
941
942 pub fn build(self) -> Arc<Path> {
944 Arc::new(Path {
945 verbs: self.verbs,
946 points: self.points,
947 weights: self.weights,
948 bounds: self.bounds.unwrap_or_default(),
949 })
950 }
951
952 fn push_arc_conics(&mut self, map: &Matrix, start_angle: f32, sweep_angle: f32) {
960 let piece_count = (sweep_angle.abs() / std::f32::consts::FRAC_PI_2)
961 .ceil()
962 .max(1.0);
963 let step = sweep_angle / piece_count;
964 let weight = (step / 2.0).cos();
965
966 let mut angle = start_angle;
967 for _ in 0..piece_count as u32 {
968 let mid = unit_circle_point(angle + step / 2.0);
969 let control = Point::new(mid.x / weight, mid.y / weight);
970 let to = unit_circle_point(angle + step);
971 self.conic_to(map.map_point(control), map.map_point(to), weight);
972 angle += step;
973 }
974 }
975
976 fn ensure_contour(&mut self, p: Point) {
986 if self.contour_open {
987 return;
988 }
989 self.move_to(self.resume_point.unwrap_or(p));
990 }
991
992 fn push_point(&mut self, p: Point) {
993 self.points.push(p);
994 self.bounds = Some(match self.bounds {
998 Some(b) => Rect::from_ltrb(
999 b.x.min(p.x),
1000 b.y.min(p.y),
1001 b.right().max(p.x),
1002 b.bottom().max(p.y),
1003 ),
1004 None => Rect::new(p.x, p.y, 0.0, 0.0),
1005 });
1006 }
1007}
1008
1009fn unit_circle_point(angle: f32) -> Point {
1011 let (sine, cosine) = angle.sin_cos();
1012 Point::new(cosine, sine)
1013}
1014
1015fn unit_circle_map(center: Point, radii: [f32; 2], rotation: f32) -> Matrix {
1017 let [radius_x, radius_y] = radii;
1018 let (sine, cosine) = rotation.sin_cos();
1019 Matrix::from_affine(
1020 radius_x * cosine,
1021 radius_x * sine,
1022 -radius_y * sine,
1023 radius_y * cosine,
1024 center.x,
1025 center.y,
1026 )
1027}
1028
1029fn shortest_sweep(start: f32, end: f32, direction: f32) -> f32 {
1031 let mut sweep = end - start;
1032 let turn = std::f32::consts::TAU;
1033 while sweep > 0.0 && direction < 0.0 {
1034 sweep -= turn;
1035 }
1036 while sweep < 0.0 && direction > 0.0 {
1037 sweep += turn;
1038 }
1039 sweep
1040}
1041
1042fn normalize(x: f64, y: f64) -> Option<(f64, f64)> {
1044 let length = (x * x + y * y).sqrt();
1045 (length.is_finite() && length > 0.0).then(|| (x / length, y / length))
1046}
1047
1048pub fn constrain_radii(r: &Rect, radii: [f32; 4]) -> [f32; 4] {
1052 constrain_radii_elliptical(r, radii.map(|v| [v; 2])).map(|[x, _]| x)
1053}
1054
1055pub fn constrain_radii_elliptical(r: &Rect, radii: [[f32; 2]; 4]) -> [[f32; 2]; 4] {
1060 let [tl, tr, br, bl] = radii.map(|[x, y]| [x.max(0.0), y.max(0.0)]);
1061 let fit = |side: f32, a: f32, b: f32| if a + b <= side { 1.0 } else { side / (a + b) };
1062 let f = fit(r.width, tl[0], tr[0])
1063 .min(fit(r.width, bl[0], br[0]))
1064 .min(fit(r.height, tl[1], bl[1]))
1065 .min(fit(r.height, tr[1], br[1]));
1066 [tl, tr, br, bl].map(|[x, y]| [x * f, y * f])
1067}
1068
1069struct Flattener {
1071 tolerance: f32,
1072 contours: Vec<Contour>,
1073 current: Vec<Point>,
1074 has_segments: bool,
1076}
1077
1078impl Flattener {
1079 fn new(tolerance: f32) -> Self {
1080 Self {
1081 tolerance,
1082 contours: Vec::new(),
1083 current: Vec::new(),
1084 has_segments: false,
1085 }
1086 }
1087
1088 fn move_to(&mut self, p: Point) {
1089 self.flush(false);
1090 self.current.push(p);
1091 self.has_segments = false;
1092 }
1093
1094 fn line_to(&mut self, p: Point) {
1095 self.current.push(p);
1096 self.has_segments = true;
1097 }
1098
1099 fn quad_to(&mut self, c: Point, p: Point) {
1100 let Some(&start) = self.current.last() else {
1101 return;
1102 };
1103 self.has_segments = true;
1104 let dev = second_difference(start, c, p);
1105 let n = segment_count((dev / (8.0 * self.tolerance)).sqrt());
1106 for i in 1..=n {
1107 let t = i as f32 / n as f32;
1108 self.current.push(eval_quad(start, c, p, t));
1109 }
1110 }
1111
1112 fn conic_to(&mut self, c: Point, p: Point, w: f32) {
1113 let Some(&start) = self.current.last() else {
1114 return;
1115 };
1116 self.has_segments = true;
1117 let n = segment_count(conic_segment_estimate(start, c, p, w, self.tolerance));
1118 for i in 1..=n {
1119 let t = i as f32 / n as f32;
1120 self.current.push(eval_conic(start, c, p, w, t));
1121 }
1122 }
1123
1124 fn cubic_to(&mut self, c1: Point, c2: Point, p: Point) {
1125 let Some(&start) = self.current.last() else {
1126 return;
1127 };
1128 self.has_segments = true;
1129 let dev = second_difference(start, c1, c2).max(second_difference(c1, c2, p));
1130 let n = segment_count((3.0 * dev / (4.0 * self.tolerance)).sqrt());
1131 for i in 1..=n {
1132 let t = i as f32 / n as f32;
1133 self.current.push(eval_cubic(start, c1, c2, p, t));
1134 }
1135 }
1136
1137 fn close(&mut self) {
1138 if let (Some(&first), Some(&last)) = (self.current.first(), self.current.last()) {
1141 if self.current.len() >= 2 && (first.x, first.y) != (last.x, last.y) {
1142 self.current.push(first);
1143 }
1144 }
1145 if !self.current.is_empty() {
1151 self.has_segments = true;
1152 }
1153 self.flush(true);
1154 }
1155
1156 fn finish(mut self) -> Vec<Contour> {
1157 self.flush(false);
1158 self.contours
1159 }
1160
1161 fn flush(&mut self, closed: bool) {
1168 if !self.current.is_empty() {
1169 self.contours.push(Contour {
1170 points: std::mem::take(&mut self.current),
1171 closed,
1172 has_segments: self.has_segments,
1173 });
1174 }
1175 self.has_segments = false;
1176 }
1177}
1178
1179fn conic_segment_estimate(start: Point, control: Point, end: Point, w: f32, tolerance: f32) -> f32 {
1183 let precision = 1.0 / tolerance;
1184 let cx = 0.5 * (start.x.min(control.x).min(end.x) + start.x.max(control.x).max(end.x));
1186 let cy = 0.5 * (start.y.min(control.y).min(end.y) + start.y.max(control.y).max(end.y));
1187 let (p0x, p0y) = (start.x - cx, start.y - cy);
1188 let (p1x, p1y) = (control.x - cx, control.y - cy);
1189 let (p2x, p2y) = (end.x - cx, end.y - cy);
1190 let max_len_sq = (p0x * p0x + p0y * p0y)
1191 .max(p1x * p1x + p1y * p1y)
1192 .max(p2x * p2x + p2y * p2y);
1193 let (dpx, dpy) = (p0x - 2.0 * w * p1x + p2x, p0y - 2.0 * w * p1y + p2y);
1194 let dw = (2.0 - 2.0 * w).abs();
1195 let rp_minus_1 = (max_len_sq.sqrt() * precision - 1.0).max(0.0);
1196 let numer = (dpx * dpx + dpy * dpy).sqrt() * precision + rp_minus_1 * dw;
1197 let denom = 4.0 * w.min(1.0);
1198 (numer / denom).sqrt()
1199}
1200
1201fn second_difference(a: Point, b: Point, c: Point) -> f32 {
1202 let dx = a.x - 2.0 * b.x + c.x;
1203 let dy = a.y - 2.0 * b.y + c.y;
1204 (dx * dx + dy * dy).sqrt()
1205}
1206
1207fn segment_count(estimate: f32) -> u32 {
1208 (estimate.ceil() as u32).clamp(1, 64)
1209}
1210
1211fn eval_quad(p0: Point, c: Point, p1: Point, t: f32) -> Point {
1212 let u = 1.0 - t;
1213 Point::new(
1214 u * u * p0.x + 2.0 * u * t * c.x + t * t * p1.x,
1215 u * u * p0.y + 2.0 * u * t * c.y + t * t * p1.y,
1216 )
1217}
1218
1219fn eval_cubic(p0: Point, c1: Point, c2: Point, p1: Point, t: f32) -> Point {
1220 let u = 1.0 - t;
1221 let (uu, tt) = (u * u, t * t);
1222 Point::new(
1223 u * uu * p0.x + 3.0 * uu * t * c1.x + 3.0 * u * tt * c2.x + t * tt * p1.x,
1224 u * uu * p0.y + 3.0 * uu * t * c1.y + 3.0 * u * tt * c2.y + t * tt * p1.y,
1225 )
1226}
1227
1228pub fn local_tolerance(transform: &Matrix) -> f32 {
1230 0.25 / transform.max_scale().max(1e-3)
1231}
1232
1233#[cfg(test)]
1234mod tests {
1235 use super::*;
1236
1237 #[test]
1244 fn opposed_windings_cancel_under_the_non_zero_rule() {
1245 let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
1246 let radii = [[12.0, 12.0]; 4];
1247 let inside = Point::new(50.0, 50.0);
1248
1249 let mut opposed = PathBuilder::new();
1250 opposed.rrect_radii_elliptical_wound(rect, radii, Winding::Clockwise);
1251 opposed.rrect_radii_elliptical_wound(rect, radii, Winding::CounterClockwise);
1252 assert!(
1253 !opposed.build().contains(inside, FillRule::NonZero),
1254 "opposed windings must cancel"
1255 );
1256
1257 let mut agreeing = PathBuilder::new();
1258 agreeing.rrect_radii_elliptical_wound(rect, radii, Winding::Clockwise);
1259 agreeing.rrect_radii_elliptical_wound(rect, radii, Winding::Clockwise);
1260 assert!(
1261 agreeing.build().contains(inside, FillRule::NonZero),
1262 "agreeing windings must reinforce"
1263 );
1264 }
1265
1266 #[test]
1270 fn winding_reverses_the_walk_without_moving_the_outline() {
1271 let rect = Rect::new(10.0, 20.0, 80.0, 60.0);
1272 let radii = [[8.0, 14.0], [4.0, 4.0], [20.0, 6.0], [0.0, 0.0]];
1273 let wound = |winding| {
1274 let mut path = PathBuilder::new();
1275 path.rrect_radii_elliptical_wound(rect, radii, winding);
1276 path.build()
1277 };
1278 let clockwise = wound(Winding::Clockwise);
1279 let counter = wound(Winding::CounterClockwise);
1280 assert_eq!(clockwise.tight_bounds(), counter.tight_bounds());
1281 for point in [
1284 Point::new(50.0, 50.0),
1285 Point::new(14.0, 30.0),
1286 Point::new(86.0, 24.0),
1287 Point::new(74.0, 76.0),
1288 Point::new(12.0, 78.0),
1289 Point::new(5.0, 15.0),
1290 Point::new(95.0, 85.0),
1291 ] {
1292 assert_eq!(
1293 clockwise.contains(point, FillRule::NonZero),
1294 counter.contains(point, FillRule::NonZero),
1295 "the two directions disagree about {point:?}"
1296 );
1297 }
1298 }
1299
1300 #[test]
1308 fn a_segment_after_close_resumes_at_the_contour_origin() {
1309 let mut path = PathBuilder::new();
1310 path.move_to((10.0, 10.0));
1311 path.line_to((30.0, 10.0));
1312 path.close();
1313 path.line_to((30.0, 30.0));
1314 let path = path.build();
1315
1316 let contours = path.flatten(0.05);
1318 let resumed = contours.last().expect("the path continues after the close");
1319 assert_eq!(
1320 resumed.points.first().copied(),
1321 Some(Point::new(10.0, 10.0)),
1322 "the segment after close must start at the contour origin, not its own end"
1323 );
1324 assert!(crate::stroke_contains(
1325 &contours,
1326 &crate::Stroke::new(6.0),
1327 0.05,
1328 Point::new(20.0, 20.0)
1329 ));
1330 }
1331
1332 #[test]
1340 fn a_segment_after_a_shape_helper_resumes_at_the_box_corner() {
1341 let box_corner = Point::new(10.0, 10.0);
1342 for corner in [0.0f32, 8.0] {
1343 let mut path = PathBuilder::new();
1344 if corner == 0.0 {
1345 path.rect(Rect::new(10.0, 10.0, 40.0, 40.0));
1346 } else {
1347 path.rrect_radii_elliptical(Rect::new(10.0, 10.0, 40.0, 40.0), [[corner; 2]; 4]);
1348 }
1349 path.line_to((90.0, 90.0));
1350
1351 let contours = path.build().flatten(0.05);
1352 let resumed = contours.last().expect("the path continues after the shape");
1353 assert_eq!(
1354 resumed.points.first().copied(),
1355 Some(box_corner),
1356 "corner radius {corner}: the trailing segment starts at (x, y)"
1357 );
1358 }
1359
1360 let mut path = PathBuilder::new();
1364 path.rrect_radii_elliptical(Rect::new(10.0, 10.0, 40.0, 40.0), [[8.0; 2]; 4]);
1365 path.line_to((90.0, 90.0));
1366 let contours = path.build().flatten(0.05);
1367 let stroke = crate::Stroke::new(4.0);
1368 assert!(
1369 crate::stroke_contains(&contours, &stroke, 0.05, Point::new(50.0, 50.0)),
1370 "the diagonal from (10,10) must be stroked"
1371 );
1372 assert!(
1373 !crate::stroke_contains(&contours, &stroke, 0.05, Point::new(54.0, 50.0)),
1374 "the diagonal from the tangent (18,10) must not be"
1375 );
1376 }
1377
1378 #[test]
1381 fn close_still_resumes_at_the_contour_origin() {
1382 let mut path = PathBuilder::new();
1383 path.move_to((10.0, 10.0));
1384 path.line_to((30.0, 10.0));
1385 path.line_to((30.0, 30.0));
1386 path.close();
1387 path.line_to((90.0, 90.0));
1388 let contours = path.build().flatten(0.05);
1389 assert_eq!(
1390 contours
1391 .last()
1392 .and_then(|contour| contour.points.first())
1393 .copied(),
1394 Some(Point::new(10.0, 10.0)),
1395 "close resumes where the contour began, not at any box corner"
1396 );
1397 }
1398
1399 #[test]
1402 fn a_first_segment_with_no_contour_starts_at_its_own_point() {
1403 let mut path = PathBuilder::new();
1404 path.line_to((30.0, 30.0));
1405 assert_eq!(
1406 path.build().bounds(),
1407 Rect::from_ltrb(30.0, 30.0, 30.0, 30.0)
1408 );
1409 }
1410
1411 #[test]
1412 fn append_carries_verbs_through_the_transform() {
1413 let mut source = PathBuilder::new();
1414 source.rect(Rect::new(0.0, 0.0, 10.0, 10.0));
1415 let source = source.build();
1416
1417 let mut target = PathBuilder::new();
1418 target.rect(Rect::new(0.0, 0.0, 4.0, 4.0));
1419 target.append(&source, &Matrix::translation(100.0, 50.0));
1420 let target = target.build();
1421
1422 assert_eq!(target.bounds(), Rect::from_ltrb(0.0, 0.0, 110.0, 60.0));
1423 assert!(target.contains(Point::new(105.0, 55.0), FillRule::NonZero));
1424 assert!(!target.contains(Point::new(5.0, 5.0), FillRule::NonZero));
1425 }
1426
1427 #[test]
1431 fn appending_a_closed_contour_reopens_at_its_seam() {
1432 let mut source = PathBuilder::new();
1433 source.move_to((10.0, 10.0));
1434 source.line_to((20.0, 10.0));
1435 source.close();
1436 let source = source.build();
1437
1438 let mut target = PathBuilder::new();
1439 target.append(&source, &Matrix::IDENTITY);
1440 target.line_to((10.0, 40.0));
1441 let built = target.build();
1442
1443 assert_eq!(built.bounds(), Rect::from_ltrb(10.0, 10.0, 20.0, 40.0));
1445 assert!(built.contains(Point::new(10.0, 25.0), FillRule::NonZero));
1446 }
1447
1448 #[test]
1452 fn appending_an_open_contour_hands_over_its_origin() {
1453 let mut source = PathBuilder::new();
1454 source.move_to((50.0, 50.0));
1455 source.line_to((60.0, 50.0));
1456 let source = source.build();
1457
1458 let mut target = PathBuilder::new();
1459 target.move_to((0.0, 0.0));
1460 target.line_to((10.0, 0.0));
1461 target.append(&source, &Matrix::IDENTITY);
1462 target.close();
1463 target.line_to((90.0, 90.0));
1464
1465 let contours = target.build().flatten(0.05);
1466 let resumed = contours.last().expect("the path continues after the close");
1467 assert_eq!(
1468 resumed.points.first().copied(),
1469 Some(Point::new(50.0, 50.0)),
1470 "the resumed segment must start at the APPENDED contour's origin"
1471 );
1472 }
1473
1474 #[test]
1475 fn appending_nothing_leaves_an_open_contour_open() {
1476 let empty = PathBuilder::new().build();
1477 let mut target = PathBuilder::new();
1478 target.move_to((0.0, 0.0));
1479 target.line_to((10.0, 0.0));
1480 target.append(&empty, &Matrix::IDENTITY);
1481 target.line_to((10.0, 10.0));
1482 assert_eq!(
1483 target.build().bounds(),
1484 Rect::from_ltrb(0.0, 0.0, 10.0, 10.0)
1485 );
1486 }
1487
1488 #[test]
1489 fn appending_an_open_contour_leaves_it_open() {
1490 let mut source = PathBuilder::new();
1491 source.move_to((0.0, 0.0));
1492 source.line_to((10.0, 0.0));
1493 let source = source.build();
1494
1495 let mut target = PathBuilder::new();
1496 target.append(&source, &Matrix::IDENTITY);
1497 target.line_to((10.0, 10.0));
1500 assert_eq!(
1501 target.build().bounds(),
1502 Rect::from_ltrb(0.0, 0.0, 10.0, 10.0)
1503 );
1504 }
1505
1506 #[test]
1507 fn tight_bounds_use_curve_extrema_not_control_points() {
1508 let mut path = PathBuilder::new();
1509 path.move_to((0.0, 0.0));
1510 path.quad_to((100.0, 100.0), (200.0, 0.0));
1511 let path = path.build();
1512 assert_eq!(path.bounds(), Rect::new(0.0, 0.0, 200.0, 100.0));
1513 assert_eq!(path.tight_bounds(), Rect::new(0.0, 0.0, 200.0, 50.0));
1514 }
1515
1516 #[test]
1517 fn tight_bounds_keep_extrema_below_f32_epsilon() {
1518 let mut path = PathBuilder::new();
1519 path.move_to((0.0, 0.0));
1520 path.quad_to((0.0, 1.0e-8), (0.0, 0.0));
1521 let bounds = path.build().tight_bounds();
1522 assert!((bounds.height - 5.0e-9).abs() < 1.0e-12);
1523 }
1524
1525 #[test]
1526 fn cubic_extrema_preserve_the_small_root() {
1527 let roots = cubic_extrema(0.0, 1.0e-8, -0.5, -0.5);
1528 assert!(roots
1529 .into_iter()
1530 .flatten()
1531 .any(|root| (root - 1.0e-8).abs() < 1.0e-10));
1532 }
1533
1534 #[test]
1535 fn radii_constrain_together() {
1536 let r = Rect::new(0.0, 0.0, 100.0, 40.0);
1537 let out = constrain_radii(&r, [40.0, 10.0, 10.0, 40.0]);
1539 assert_eq!(out, [20.0, 5.0, 5.0, 20.0]);
1540 assert_eq!(constrain_radii(&r, [8.0, 8.0, 8.0, 8.0]), [8.0; 4]);
1542 }
1543
1544 #[test]
1545 fn per_corner_rrect_stays_in_rect() {
1546 let r = Rect::new(10.0, 10.0, 100.0, 60.0);
1547 let mut b = PathBuilder::new();
1548 b.rrect_radii(r, [30.0, 0.0, 16.0, 8.0]);
1549 assert_eq!(b.build().bounds(), r);
1550 }
1551
1552 #[test]
1553 fn bounds_cover_control_points() {
1554 let mut b = PathBuilder::new();
1555 b.move_to((10.0, 10.0)).quad_to((50.0, -20.0), (90.0, 10.0));
1556 let p = b.build();
1557 assert_eq!(p.bounds(), Rect::from_ltrb(10.0, -20.0, 90.0, 10.0));
1558 }
1559
1560 #[test]
1561 fn circle_flattens_to_radius() {
1562 let mut b = PathBuilder::new();
1563 b.circle((0.0, 0.0), 100.0);
1564 let contours = b.build().flatten(0.1);
1565 assert_eq!(contours.len(), 1);
1566 assert!(contours[0].closed, "circle closes its contour");
1567 for p in &contours[0].points {
1568 let r = (p.x * p.x + p.y * p.y).sqrt();
1569 assert!((r - 100.0).abs() < 0.5, "point off circle: r={r}");
1570 }
1571 }
1572
1573 #[test]
1574 fn finer_tolerance_means_more_segments() {
1575 let path = {
1576 let mut b = PathBuilder::new();
1577 b.circle((0.0, 0.0), 100.0);
1578 b.build()
1579 };
1580 let coarse = path.flatten(2.0)[0].points.len();
1581 let fine = path.flatten(0.05)[0].points.len();
1582 assert!(fine > coarse, "fine {fine} vs coarse {coarse}");
1583 }
1584
1585 #[test]
1586 fn small_contours_survive_for_the_stroker() {
1587 let mut b = PathBuilder::new();
1588 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);
1591 assert_eq!(contours.len(), 2);
1592 assert_eq!(contours[0].points.len(), 2);
1593 assert!(!contours[0].closed);
1594 assert_eq!(contours[1].points.len(), 1);
1595 }
1596
1597 #[test]
1598 fn close_emits_the_closing_edge_and_marks_the_contour() {
1599 let mut b = PathBuilder::new();
1600 b.move_to((0.0, 0.0))
1601 .line_to((10.0, 0.0))
1602 .line_to((10.0, 10.0))
1603 .close();
1604 let contours = b.build().flatten(0.1);
1605 assert!(contours[0].closed);
1606 assert_eq!(contours[0].points.len(), 4, "closing edge in the polyline");
1607 assert_eq!(contours[0].points[3], Point::new(0.0, 0.0));
1608 }
1609
1610 #[test]
1611 fn bounds_keep_a_first_point_at_the_origin() {
1612 let mut b = PathBuilder::new();
1613 b.move_to((0.0, 0.0)).line_to((50.0, 80.0));
1614 assert_eq!(b.build().bounds(), Rect::from_ltrb(0.0, 0.0, 50.0, 80.0));
1615
1616 let mut b = PathBuilder::new();
1617 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));
1619 }
1620
1621 #[test]
1622 fn curve_without_move_starts_contour() {
1623 let mut b = PathBuilder::new();
1624 b.line_to((10.0, 0.0))
1625 .line_to((10.0, 10.0))
1626 .line_to((0.0, 10.0));
1627 let contours = b.build().flatten(0.1);
1628 assert_eq!(contours.len(), 1);
1629 assert_eq!(contours[0].points.len(), 4);
1630 }
1631
1632 #[test]
1636 fn circular_rrect_is_the_equal_axes_elliptical_case() {
1637 let r = Rect::new(10.0, 20.0, 120.0, 80.0);
1638 let radii = [24.0, 8.0, 30.0, 0.0];
1639 let mut circular = PathBuilder::new();
1640 circular.rrect_radii(r, radii);
1641 let mut elliptical = PathBuilder::new();
1642 elliptical.rrect_radii_elliptical(r, radii.map(|v| [v; 2]));
1643 assert_eq!(
1644 circular.build().flatten(0.1)[0].points,
1645 elliptical.build().flatten(0.1)[0].points,
1646 );
1647 }
1648
1649 #[test]
1650 fn elliptical_radii_constrain_per_axis() {
1651 let r = Rect::new(0.0, 0.0, 100.0, 40.0);
1654 let out = constrain_radii_elliptical(
1655 &r,
1656 [[10.0, 20.0], [10.0, 20.0], [10.0, 30.0], [10.0, 30.0]],
1657 );
1658 assert_eq!(out[0], [8.0, 16.0]);
1659 assert_eq!(out[2], [8.0, 24.0]);
1660 let out = constrain_radii_elliptical(&r, [[-5.0, 10.0], [0.0; 2], [0.0; 2], [0.0; 2]]);
1662 assert_eq!(out[0], [0.0, 10.0]);
1663 }
1664
1665 #[test]
1666 fn elliptical_corner_lands_on_axis_extremes() {
1667 let r = Rect::new(0.0, 0.0, 200.0, 100.0);
1670 let mut b = PathBuilder::new();
1671 b.rrect_radii_elliptical(r, [[0.0; 2], [40.0, 10.0], [0.0; 2], [0.0; 2]]);
1672 let points = &b.build().flatten(0.05)[0].points;
1673 assert!(points
1676 .iter()
1677 .any(|p| (p.x - 160.0).abs() < 0.5 && p.y.abs() < 0.5));
1678 assert!(points
1679 .iter()
1680 .any(|p| (p.x - 200.0).abs() < 0.5 && (p.y - 10.0).abs() < 0.5));
1681 }
1682
1683 #[test]
1688 fn swept_arc_stays_on_its_circle() {
1689 let (center, radius) = (Point::new(50.0, 60.0), 40.0);
1690 let mut b = PathBuilder::new();
1691 b.arc(center, radius, 0.0, std::f32::consts::TAU);
1692 for point in &b.build().flatten(0.01)[0].points {
1693 let offset = (point.x - center.x).hypot(point.y - center.y);
1694 assert!(
1695 (offset - radius).abs() < 0.05,
1696 "point {point:?} is {offset} from the centre, not {radius}"
1697 );
1698 }
1699 }
1700
1701 #[test]
1703 fn quarter_arc_ends_where_it_should() {
1704 let mut b = PathBuilder::new();
1705 b.arc((0.0, 0.0), 100.0, 0.0, std::f32::consts::FRAC_PI_2);
1706 let points = &b.build().flatten(0.01)[0].points;
1707 let (first, last) = (points[0], *points.last().unwrap());
1708 assert!(
1709 (first.x - 100.0).abs() < 0.01 && first.y.abs() < 0.01,
1710 "{first:?}"
1711 );
1712 assert!(
1713 last.x.abs() < 0.05 && (last.y - 100.0).abs() < 0.05,
1714 "{last:?}"
1715 );
1716 }
1717
1718 #[test]
1720 fn ellipse_reaches_both_radii() {
1721 let mut b = PathBuilder::new();
1722 b.ellipse((0.0, 0.0), [80.0, 20.0], 0.0, 0.0, std::f32::consts::TAU);
1723 let points = &b.build().flatten(0.01)[0].points;
1724 let widest = points.iter().fold(0.0f32, |m, p| m.max(p.x.abs()));
1725 let tallest = points.iter().fold(0.0f32, |m, p| m.max(p.y.abs()));
1726 assert!((widest - 80.0).abs() < 0.1, "widest {widest}");
1727 assert!((tallest - 20.0).abs() < 0.1, "tallest {tallest}");
1728 }
1729
1730 #[test]
1732 fn ellipse_rotation_swaps_the_axes() {
1733 let mut b = PathBuilder::new();
1734 b.ellipse(
1735 (0.0, 0.0),
1736 [80.0, 20.0],
1737 std::f32::consts::FRAC_PI_2,
1738 0.0,
1739 std::f32::consts::TAU,
1740 );
1741 let points = &b.build().flatten(0.01)[0].points;
1742 let widest = points.iter().fold(0.0f32, |m, p| m.max(p.x.abs()));
1743 let tallest = points.iter().fold(0.0f32, |m, p| m.max(p.y.abs()));
1744 assert!((widest - 20.0).abs() < 0.1, "widest {widest}");
1745 assert!((tallest - 80.0).abs() < 0.1, "tallest {tallest}");
1746 }
1747
1748 #[test]
1752 fn arc_to_rounds_a_right_angle() {
1753 let radius = 20.0f32;
1754 let mut b = PathBuilder::new();
1755 b.move_to((0.0, 0.0))
1756 .arc_to((100.0, 0.0), (100.0, 100.0), radius);
1757 let points = &b.build().flatten(0.01)[0].points;
1758
1759 let entry = Point::new(100.0 - radius, 0.0);
1760 let exit = Point::new(100.0, radius);
1761 assert!(points
1762 .iter()
1763 .any(|p| (p.x - entry.x).abs() < 0.1 && (p.y - entry.y).abs() < 0.1));
1764 assert!(points
1765 .iter()
1766 .any(|p| (p.x - exit.x).abs() < 0.1 && (p.y - exit.y).abs() < 0.1));
1767
1768 let center = Point::new(100.0 - radius, radius);
1769 for point in points.iter().filter(|p| p.x > entry.x - 0.01) {
1770 let offset = (point.x - center.x).hypot(point.y - center.y);
1771 assert!(
1772 (offset - radius).abs() < 0.1,
1773 "{point:?} is {offset} from the centre"
1774 );
1775 }
1776 }
1777
1778 #[test]
1781 fn degenerate_arc_to_falls_back_to_a_line() {
1782 for (corner, next, radius) in [
1783 ((50.0, 0.0), (100.0, 0.0), 20.0), ((50.0, 0.0), (50.0, 50.0), 0.0), ] {
1786 let mut b = PathBuilder::new();
1787 b.move_to((0.0, 0.0)).arc_to(corner, next, radius);
1788 let points = &b.build().flatten(0.01)[0].points;
1789 assert_eq!(points.len(), 2, "expected a bare line, got {points:?}");
1790 assert!((points[1].x - corner.0).abs() < 0.01 && (points[1].y - corner.1).abs() < 0.01);
1791 }
1792 }
1793
1794 #[test]
1797 fn rect_contains_what_it_covers() {
1798 let mut b = PathBuilder::new();
1799 b.rect(Rect::new(10.0, 10.0, 80.0, 60.0));
1800 let path = b.build();
1801 assert!(path.contains(Point::new(50.0, 40.0), FillRule::NonZero));
1802 assert!(!path.contains(Point::new(5.0, 40.0), FillRule::NonZero));
1803 assert!(!path.contains(Point::new(50.0, 80.0), FillRule::NonZero));
1804 for on_outline in [
1807 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), ] {
1813 assert!(
1814 path.contains(on_outline, FillRule::NonZero),
1815 "{on_outline:?} is on the outline and must count as inside"
1816 );
1817 }
1818 }
1819
1820 #[test]
1824 fn an_enormous_sweep_stays_one_turn() {
1825 let mut b = PathBuilder::new();
1826 b.arc((0.0, 0.0), 50.0, 0.0, 1e20);
1827 let path = b.build();
1828 let contours = path.flatten(0.1);
1829 assert_eq!(contours.len(), 1);
1830 assert!(
1832 contours[0].points.len() < 1_000,
1833 "a clamped turn should stay small, got {}",
1834 contours[0].points.len()
1835 );
1836 assert!(contours[0].closed, "a full turn closes its contour");
1837 }
1838
1839 #[test]
1840 fn a_negative_sweep_turns_the_other_way() {
1841 let quarter = std::f32::consts::FRAC_PI_2;
1842 let mut clockwise = PathBuilder::new();
1843 clockwise.arc((0.0, 0.0), 50.0, 0.0, quarter);
1844 let mut anticlockwise = PathBuilder::new();
1845 anticlockwise.arc((0.0, 0.0), 50.0, 0.0, -quarter);
1846
1847 let forward = clockwise.build().bounds();
1850 let backward = anticlockwise.build().bounds();
1851 assert!(forward.bottom() > 40.0, "positive sweep reaches +y");
1852 assert!(backward.y < -40.0, "negative sweep reaches -y");
1853 }
1854
1855 #[test]
1858 fn circle_containment_is_exact_all_the_way_round() {
1859 let (center, radius) = (Point::new(0.0, 0.0), 100.0f32);
1860 let mut b = PathBuilder::new();
1861 b.circle(center, radius);
1862 let path = b.build();
1863 for step in 0..64 {
1864 let angle = step as f32 / 64.0 * std::f32::consts::TAU;
1865 let (sine, cosine) = angle.sin_cos();
1866 let inside = Point::new(cosine * radius * 0.99, sine * radius * 0.99);
1867 let outside = Point::new(cosine * radius * 1.01, sine * radius * 1.01);
1868 assert!(
1869 path.contains(inside, FillRule::NonZero),
1870 "{inside:?} should be in"
1871 );
1872 assert!(
1873 !path.contains(outside, FillRule::NonZero),
1874 "{outside:?} should be out"
1875 );
1876 }
1877 }
1878
1879 #[test]
1883 fn fill_rules_disagree_about_a_same_wound_hole() {
1884 let mut b = PathBuilder::new();
1885 b.rect(Rect::new(0.0, 0.0, 100.0, 100.0));
1886 b.rect(Rect::new(25.0, 25.0, 50.0, 50.0));
1887 let path = b.build();
1888 let middle = Point::new(50.0, 50.0);
1889 assert!(path.contains(middle, FillRule::NonZero));
1890 assert!(!path.contains(middle, FillRule::EvenOdd));
1891 let ring = Point::new(10.0, 50.0);
1893 assert!(path.contains(ring, FillRule::NonZero));
1894 assert!(path.contains(ring, FillRule::EvenOdd));
1895 }
1896
1897 #[test]
1899 fn open_contour_closes_implicitly() {
1900 let mut b = PathBuilder::new();
1901 b.move_to((0.0, 0.0))
1902 .line_to((100.0, 0.0))
1903 .line_to((100.0, 100.0));
1904 let path = b.build();
1905 assert!(path.contains(Point::new(80.0, 40.0), FillRule::NonZero));
1906 assert!(!path.contains(Point::new(20.0, 60.0), FillRule::NonZero));
1907 }
1908
1909 #[test]
1911 fn containment_handles_curves_that_double_back() {
1912 let mut b = PathBuilder::new();
1913 b.move_to((0.0, 0.0))
1914 .cubic_to((120.0, 120.0), (-20.0, 120.0), (100.0, 0.0))
1915 .close();
1916 let path = b.build();
1917 assert!(path.contains(Point::new(50.0, 40.0), FillRule::NonZero));
1918 assert!(!path.contains(Point::new(50.0, -10.0), FillRule::NonZero));
1919 assert!(!path.contains(Point::new(-30.0, 40.0), FillRule::NonZero));
1920 }
1921
1922 #[test]
1929 fn circle_flattens_onto_the_exact_circle() {
1930 let mut b = PathBuilder::new();
1931 b.circle((10.0, -20.0), 100.0);
1932 let contours = b.build().flatten(0.1);
1933 let points: Vec<Point> = contours.iter().flat_map(|c| c.points.clone()).collect();
1934 assert!(points.len() > 20);
1935 for p in &points {
1936 let distance = (p.x - 10.0).hypot(p.y + 20.0);
1937 assert!(
1938 (distance - 100.0).abs() < 2e-3,
1939 "{p:?} is off the circle by {}",
1940 (distance - 100.0).abs()
1941 );
1942 }
1943 }
1944
1945 #[test]
1948 fn ellipse_arc_flattens_onto_the_exact_ellipse() {
1949 let mut b = PathBuilder::new();
1950 let rotation = 0.5f32;
1951 b.ellipse((5.0, 7.0), [80.0, 30.0], rotation, 0.3, 4.0);
1952 let contours = b.build().flatten(0.05);
1953 let (sin_r, cos_r) = rotation.sin_cos();
1954 for p in contours.iter().flat_map(|c| &c.points) {
1955 let (dx, dy) = (p.x - 5.0, p.y - 7.0);
1956 let (ux, uy) = (dx * cos_r + dy * sin_r, -dx * sin_r + dy * cos_r);
1958 let value = (ux / 80.0).powi(2) + (uy / 30.0).powi(2);
1959 assert!((value - 1.0).abs() < 1e-3, "{p:?} deviates: {value}");
1960 }
1961 }
1962
1963 #[test]
1967 fn conic_circle_tight_bounds_are_exact() {
1968 let mut b = PathBuilder::new();
1969 b.circle((0.0, 0.0), 50.0);
1970 let tight = b.build().tight_bounds();
1971 for (got, want) in [
1972 (tight.x, -50.0),
1973 (tight.y, -50.0),
1974 (tight.right(), 50.0),
1975 (tight.bottom(), 50.0),
1976 ] {
1977 assert!((got - want).abs() < 1e-3, "bound {got} should be {want}");
1978 }
1979 let mut arc = PathBuilder::new();
1982 arc.arc((0.0, 0.0), 50.0, -3.0, 2.5);
1983 let tight = arc.build().tight_bounds();
1984 assert!((tight.y + 50.0).abs() < 1e-3, "arc top: {}", tight.y);
1985 }
1986
1987 #[test]
1990 fn conic_containment_is_exact_on_the_curve() {
1991 let mut b = PathBuilder::new();
1992 b.circle((0.0, 0.0), 100.0);
1993 let path = b.build();
1994 for i in 0..64 {
1995 let angle = i as f32 / 64.0 * std::f32::consts::TAU;
1996 let (s, c) = angle.sin_cos();
1997 let inside = Point::new(c * 99.95, s * 99.95);
1998 let outside = Point::new(c * 100.05, s * 100.05);
1999 assert!(
2000 path.contains(inside, FillRule::NonZero),
2001 "{inside:?} at angle {i} should be inside"
2002 );
2003 assert!(
2004 !path.contains(outside, FillRule::NonZero),
2005 "{outside:?} at angle {i} should be outside"
2006 );
2007 }
2008 }
2009
2010 #[test]
2013 fn conic_windings_cancel_under_the_non_zero_rule() {
2014 let mut b = PathBuilder::new();
2015 b.circle((0.0, 0.0), 100.0);
2016 let w = std::f32::consts::FRAC_1_SQRT_2;
2019 b.move_to((60.0, 0.0))
2020 .conic_to((60.0, -60.0), (0.0, -60.0), w)
2021 .conic_to((-60.0, -60.0), (-60.0, 0.0), w)
2022 .conic_to((-60.0, 60.0), (0.0, 60.0), w)
2023 .conic_to((60.0, 60.0), (60.0, 0.0), w)
2024 .close();
2025 let path = b.build();
2026 assert!(!path.contains(Point::new(0.0, 0.0), FillRule::NonZero));
2027 assert!(path.contains(Point::new(80.0, 0.0), FillRule::NonZero));
2028 }
2029
2030 #[test]
2033 fn append_preserves_conic_weights() {
2034 let mut source = PathBuilder::new();
2035 source.circle((0.0, 0.0), 40.0);
2036 let source = source.build();
2037 let mut b = PathBuilder::new();
2038 b.append(&source, &Matrix::rotation(0.7));
2039 for p in b.build().flatten(0.05).iter().flat_map(|c| &c.points) {
2040 let distance = p.x.hypot(p.y);
2041 assert!((distance - 40.0).abs() < 1e-3, "{p:?} off by {distance}");
2042 }
2043 }
2044
2045 #[test]
2048 fn degenerate_conic_weights_lower_to_simpler_verbs() {
2049 let mut zero = PathBuilder::new();
2050 zero.move_to((0.0, 0.0))
2051 .conic_to((50.0, 50.0), (100.0, 0.0), 0.0);
2052 let contour = &zero.build().flatten(0.1)[0];
2053 assert_eq!(contour.points.len(), 2, "weight 0 must be a straight line");
2054
2055 let mut quad = PathBuilder::new();
2056 quad.move_to((0.0, 0.0))
2057 .conic_to((50.0, 50.0), (100.0, 0.0), 1.0);
2058 let mut plain = PathBuilder::new();
2059 plain
2060 .move_to((0.0, 0.0))
2061 .quad_to((50.0, 50.0), (100.0, 0.0));
2062 assert_eq!(
2063 quad.build().flatten(0.1),
2064 plain.build().flatten(0.1),
2065 "weight 1 must match the plain quadratic"
2066 );
2067
2068 let mut infinite = PathBuilder::new();
2069 infinite
2070 .move_to((0.0, 0.0))
2071 .conic_to((50.0, 50.0), (100.0, 0.0), f32::INFINITY);
2072 let contour = &infinite.build().flatten(0.1)[0];
2073 assert_eq!(
2074 contour.points,
2075 vec![
2076 Point::new(0.0, 0.0),
2077 Point::new(50.0, 50.0),
2078 Point::new(100.0, 0.0)
2079 ],
2080 "an unbounded weight collapses onto the control polygon"
2081 );
2082 }
2083
2084 #[test]
2090 fn conic_extrema_survive_large_coordinates() {
2091 let p0 = Point::new(0.0, 442_027.38);
2092 let c = Point::new(50.0, 442_028.47);
2093 let p2 = Point::new(100.0, 442_027.03);
2094 let w = 20.189_045;
2095 let mut b = PathBuilder::new();
2096 b.move_to(p0).conic_to(c, p2, w).close();
2097 let path = b.build();
2098 let bottom = path.tight_bounds().bottom();
2099 assert!(
2100 (bottom - 442_028.4).abs() < 0.05,
2101 "tight bottom {bottom} should reach the true maximum ~442028.41"
2102 );
2103 assert!(
2104 path.contains(Point::new(49.660_536, 442_028.0), FillRule::NonZero),
2105 "an interior point above the chord must be inside"
2106 );
2107 }
2108
2109 #[test]
2114 fn conic_flattening_respects_tolerance_for_large_weights() {
2115 let (p0, c, p2, w) = (
2116 Point::new(0.0, 100.0),
2117 Point::new(35.0, 100.0),
2118 Point::new(40.0, 90.0),
2119 8.3f32,
2120 );
2121 let tolerance = 0.05f32;
2122 let mut b = PathBuilder::new();
2123 b.move_to(p0).conic_to(c, p2, w);
2124 let contour = &b.build().flatten(tolerance)[0];
2125 let reference: Vec<Point> = (0..=4096)
2127 .map(|i| eval_conic(p0, c, p2, w, i as f32 / 4096.0))
2128 .collect();
2129 for pair in contour.points.windows(2) {
2130 let mid = Point::new((pair[0].x + pair[1].x) / 2.0, (pair[0].y + pair[1].y) / 2.0);
2131 let distance = reference
2132 .iter()
2133 .map(|r| (r.x - mid.x).hypot(r.y - mid.y))
2134 .fold(f32::MAX, f32::min);
2135 assert!(
2136 distance <= tolerance * 1.1,
2137 "chord midpoint {mid:?} is {distance} off the curve"
2138 );
2139 }
2140 }
2141}