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, Copy, Debug, PartialEq)]
70pub enum PathElement {
71 MoveTo(Point),
72 LineTo(Point),
73 QuadTo(Point, Point),
74 ConicTo(Point, Point, f32),
75 CubicTo(Point, Point, Point),
76 Close,
77}
78
79#[derive(Clone, Debug)]
84#[cfg_attr(feature = "serde", derive(serde::Serialize))]
85pub struct Path {
86 verbs: Vec<Verb>,
87 points: Vec<Point>,
88 weights: Vec<f32>,
91 bounds: Rect,
94}
95
96impl Path {
97 pub fn elements(&self) -> impl Iterator<Item = PathElement> + '_ {
100 let mut point = 0;
101 let mut weight = 0;
102 self.verbs.iter().map(move |verb| {
103 let element = match verb {
104 Verb::Move => PathElement::MoveTo(self.points[point]),
105 Verb::Line => PathElement::LineTo(self.points[point]),
106 Verb::Quad => PathElement::QuadTo(self.points[point], self.points[point + 1]),
107 Verb::Conic => {
108 let element = PathElement::ConicTo(
109 self.points[point],
110 self.points[point + 1],
111 self.weights[weight],
112 );
113 weight += 1;
114 element
115 }
116 Verb::Cubic => PathElement::CubicTo(
117 self.points[point],
118 self.points[point + 1],
119 self.points[point + 2],
120 ),
121 Verb::Close => PathElement::Close,
122 };
123 point += match verb {
124 Verb::Move | Verb::Line => 1,
125 Verb::Quad | Verb::Conic => 2,
126 Verb::Cubic => 3,
127 Verb::Close => 0,
128 };
129 element
130 })
131 }
132
133 pub fn bounds(&self) -> Rect {
135 self.bounds
136 }
137
138 pub fn tight_bounds(&self) -> Rect {
140 let mut bounds = TightBounds::default();
141 let mut point_index = 0usize;
142 let mut weight_index = 0usize;
143 let mut cursor = Point::ZERO;
144 let mut contour_start = Point::ZERO;
145 for verb in &self.verbs {
146 match verb {
147 Verb::Move => {
148 cursor = self.points[point_index];
149 contour_start = cursor;
150 point_index += 1;
151 bounds.include(cursor);
152 }
153 Verb::Line => {
154 cursor = self.points[point_index];
155 point_index += 1;
156 bounds.include(cursor);
157 }
158 Verb::Quad => {
159 let control = self.points[point_index];
160 let end = self.points[point_index + 1];
161 point_index += 2;
162 include_quadratic_extrema(&mut bounds, cursor, control, end);
163 cursor = end;
164 }
165 Verb::Conic => {
166 let control = self.points[point_index];
167 let end = self.points[point_index + 1];
168 let weight = self.weights[weight_index];
169 point_index += 2;
170 weight_index += 1;
171 include_conic_extrema(&mut bounds, cursor, control, end, weight);
172 cursor = end;
173 }
174 Verb::Cubic => {
175 let first = self.points[point_index];
176 let second = self.points[point_index + 1];
177 let end = self.points[point_index + 2];
178 point_index += 3;
179 include_cubic_extrema(&mut bounds, cursor, first, second, end);
180 cursor = end;
181 }
182 Verb::Close => {
183 cursor = contour_start;
184 bounds.include(cursor);
185 }
186 }
187 }
188 bounds.rect()
189 }
190
191 pub fn is_empty(&self) -> bool {
193 self.verbs.is_empty()
194 }
195
196 pub fn heap_bytes(&self) -> usize {
198 self.points.len() * std::mem::size_of::<Point>()
199 + self.weights.len() * std::mem::size_of::<f32>()
200 + self.verbs.len()
201 }
202
203 pub fn contains(&self, point: Point, fill_rule: FillRule) -> bool {
208 if !self.bounds.contains_inclusive(point) {
209 return false;
210 }
211 let crossings = self.walk_crossings(point);
212 match fill_rule {
213 FillRule::NonZero => crossings.is_inside_non_zero(),
214 FillRule::EvenOdd => crossings.is_inside_even_odd(),
215 }
216 }
217
218 fn walk_crossings(&self, point: Point) -> crate::winding::Crossings {
220 let mut crossings = crate::winding::Crossings::default();
221 let mut index = 0usize;
222 let mut weight_index = 0usize;
223 let mut cursor = Point::ZERO;
224 let mut contour_start = Point::ZERO;
225 let mut contour_open = false;
226 for verb in &self.verbs {
227 match verb {
228 Verb::Move => {
229 if contour_open {
232 crossings.line(cursor, contour_start, point);
233 }
234 contour_open = true;
235 contour_start = self.points[index];
236 cursor = contour_start;
237 index += 1;
238 }
239 Verb::Line => {
240 crossings.line(cursor, self.points[index], point);
241 cursor = self.points[index];
242 index += 1;
243 }
244 Verb::Quad => {
245 crossings.quad(cursor, self.points[index], self.points[index + 1], point);
246 cursor = self.points[index + 1];
247 index += 2;
248 }
249 Verb::Conic => {
250 crossings.conic(
251 cursor,
252 self.points[index],
253 self.points[index + 1],
254 self.weights[weight_index],
255 point,
256 );
257 cursor = self.points[index + 1];
258 index += 2;
259 weight_index += 1;
260 }
261 Verb::Cubic => {
262 crossings.cubic(
263 cursor,
264 self.points[index],
265 self.points[index + 1],
266 self.points[index + 2],
267 point,
268 );
269 cursor = self.points[index + 2];
270 index += 3;
271 }
272 Verb::Close => {
273 crossings.line(cursor, contour_start, point);
274 cursor = contour_start;
275 contour_open = false;
276 }
277 }
278 }
279 if contour_open {
280 crossings.line(cursor, contour_start, point);
281 }
282 crossings
283 }
284
285 pub fn measure(&self, tolerance: f32) -> Vec<crate::ContourMeasure> {
289 self.flatten(tolerance)
290 .iter()
291 .filter_map(crate::ContourMeasure::of)
292 .collect()
293 }
294
295 pub fn flatten(&self, tolerance: f32) -> Vec<Contour> {
301 let mut out = Flattener::new(tolerance.max(1e-4));
302 let mut i = 0usize;
303 let mut weight_index = 0usize;
304 for verb in &self.verbs {
305 match verb {
306 Verb::Move => {
307 out.move_to(self.points[i]);
308 i += 1;
309 }
310 Verb::Line => {
311 out.line_to(self.points[i]);
312 i += 1;
313 }
314 Verb::Quad => {
315 out.quad_to(self.points[i], self.points[i + 1]);
316 i += 2;
317 }
318 Verb::Conic => {
319 out.conic_to(
320 self.points[i],
321 self.points[i + 1],
322 self.weights[weight_index],
323 );
324 i += 2;
325 weight_index += 1;
326 }
327 Verb::Cubic => {
328 out.cubic_to(self.points[i], self.points[i + 1], self.points[i + 2]);
329 i += 3;
330 }
331 Verb::Close => out.close(),
332 }
333 }
334 out.finish()
335 }
336}
337
338#[derive(Default)]
339struct TightBounds(Option<(f32, f32, f32, f32)>);
340
341impl TightBounds {
342 fn include(&mut self, point: Point) {
343 self.0 = Some(match self.0 {
344 Some((left, top, right, bottom)) => (
345 left.min(point.x),
346 top.min(point.y),
347 right.max(point.x),
348 bottom.max(point.y),
349 ),
350 None => (point.x, point.y, point.x, point.y),
351 });
352 }
353
354 fn rect(self) -> Rect {
355 self.0
356 .map_or_else(Rect::default, |(left, top, right, bottom)| {
357 Rect::from_ltrb(left, top, right, bottom)
358 })
359 }
360}
361
362fn include_quadratic_extrema(bounds: &mut TightBounds, start: Point, control: Point, end: Point) {
363 bounds.include(start);
364 bounds.include(end);
365 for (start_axis, control_axis, end_axis) in
366 [(start.x, control.x, end.x), (start.y, control.y, end.y)]
367 {
368 let denominator = start_axis as f64 - 2.0 * control_axis as f64 + end_axis as f64;
369 if denominator == 0.0 {
370 continue;
371 }
372 let parameter = ((start_axis as f64 - control_axis as f64) / denominator) as f32;
373 if parameter > 0.0 && parameter < 1.0 {
374 bounds.include(eval_quad(start, control, end, parameter));
375 }
376 }
377}
378
379fn include_cubic_extrema(
380 bounds: &mut TightBounds,
381 start: Point,
382 first: Point,
383 second: Point,
384 end: Point,
385) {
386 bounds.include(start);
387 bounds.include(end);
388 for (start_axis, first_axis, second_axis, end_axis) in [
389 (start.x, first.x, second.x, end.x),
390 (start.y, first.y, second.y, end.y),
391 ] {
392 for parameter in cubic_extrema(start_axis, first_axis, second_axis, end_axis)
393 .into_iter()
394 .flatten()
395 {
396 if parameter > 0.0 && parameter < 1.0 {
397 bounds.include(eval_cubic(start, first, second, end, parameter));
398 }
399 }
400 }
401}
402
403fn include_conic_extrema(
404 bounds: &mut TightBounds,
405 start: Point,
406 control: Point,
407 end: Point,
408 weight: f32,
409) {
410 bounds.include(start);
411 bounds.include(end);
412 for (start_axis, control_axis, end_axis) in
413 [(start.x, control.x, end.x), (start.y, control.y, end.y)]
414 {
415 for parameter in conic_derivative_roots(start_axis, control_axis, end_axis, weight) {
416 let p = eval_conic(start, control, end, weight, parameter);
417 let clamp = |v: f32, a: f32, b: f32, c: f32| v.clamp(a.min(b).min(c), a.max(b).max(c));
421 bounds.include(Point::new(
422 clamp(p.x, start.x, control.x, end.x),
423 clamp(p.y, start.y, control.y, end.y),
424 ));
425 }
426 }
427}
428
429pub(crate) fn conic_derivative_roots(
435 start: f32,
436 control: f32,
437 end: f32,
438 weight: f32,
439) -> impl Iterator<Item = f32> {
440 let w = weight as f64;
447 let p10 = control as f64 - start as f64;
448 let p20 = end as f64 - start as f64;
449 let quadratic = (w - 1.0) * p20;
450 let linear = p20 - 2.0 * w * p10;
451 let constant = w * p10;
452
453 let mut roots = [None, None];
454 if quadratic == 0.0 {
455 roots[0] = unit_root(-constant, linear);
456 } else {
457 let discriminant = linear * linear - 4.0 * quadratic * constant;
458 if discriminant >= 0.0 && discriminant.is_finite() {
459 let root = discriminant.sqrt();
462 let q = -0.5 * (linear + root.copysign(linear));
463 roots[0] = unit_root(q, quadratic);
464 roots[1] = unit_root(constant, q).filter(|value| roots[0] != Some(*value));
465 }
466 }
467 roots.into_iter().flatten()
468}
469
470pub(crate) fn eval_conic(start: Point, control: Point, end: Point, weight: f32, t: f32) -> Point {
472 let u = 1.0 - t;
473 let b0 = u * u;
474 let b1 = 2.0 * weight * u * t;
475 let b2 = t * t;
476 let d = b0 + b1 + b2;
477 Point::new(
478 (b0 * start.x + b1 * control.x + b2 * end.x) / d,
479 (b0 * start.y + b1 * control.y + b2 * end.y) / d,
480 )
481}
482
483fn cubic_extrema(start: f32, first: f32, second: f32, end: f32) -> [Option<f32>; 2] {
484 let start = start as f64;
485 let first = first as f64;
486 let second = second as f64;
487 let end = end as f64;
488 let quadratic = -start + 3.0 * first - 3.0 * second + end;
489 let linear = 2.0 * (start - 2.0 * first + second);
490 let constant = first - start;
491 if quadratic == 0.0 {
492 return [unit_root(-constant, linear), None];
493 }
494 let discriminant = linear * linear - 4.0 * quadratic * constant;
495 if discriminant < 0.0 || !discriminant.is_finite() {
496 return [None, None];
497 }
498
499 let root = discriminant.sqrt();
502 let q = -0.5 * (linear + root.copysign(linear));
503 let first_root = unit_root(q, quadratic);
504 let second_root = unit_root(constant, q).filter(|value| Some(*value) != first_root);
505 [first_root, second_root]
506}
507
508fn unit_root(numerator: f64, denominator: f64) -> Option<f32> {
509 if denominator == 0.0 {
510 return None;
511 }
512 let value = numerator / denominator;
513 (value.is_finite() && value > 0.0 && value < 1.0).then_some(value as f32)
514}
515
516#[derive(Clone, Default)]
518pub struct PathBuilder {
519 verbs: Vec<Verb>,
520 points: Vec<Point>,
521 weights: Vec<f32>,
522 bounds: Option<Rect>,
523 resume_point: Option<Point>,
538 contour_open: bool,
539}
540
541impl PathBuilder {
542 pub fn new() -> Self {
544 Self::default()
545 }
546
547 pub fn move_to(&mut self, p: impl Into<Point>) -> &mut Self {
549 let p = p.into();
550 self.verbs.push(Verb::Move);
551 self.push_point(p);
552 self.resume_point = Some(p);
553 self.contour_open = true;
554 self
555 }
556
557 pub fn line_to(&mut self, p: impl Into<Point>) -> &mut Self {
559 let p = p.into();
560 self.ensure_contour(p);
561 self.verbs.push(Verb::Line);
562 self.push_point(p);
563 self
564 }
565
566 pub fn quad_to(&mut self, c: impl Into<Point>, p: impl Into<Point>) -> &mut Self {
568 let (c, p) = (c.into(), p.into());
569 self.ensure_contour(c);
570 self.verbs.push(Verb::Quad);
571 self.push_point(c);
572 self.push_point(p);
573 self
574 }
575
576 pub fn conic_to(&mut self, c: impl Into<Point>, p: impl Into<Point>, weight: f32) -> &mut Self {
589 let (c, p) = (c.into(), p.into());
590 if !weight.is_finite() {
591 return self.line_to(c).line_to(p);
592 }
593 if weight <= 0.0 {
594 return self.line_to(p);
595 }
596 if weight == 1.0 {
597 return self.quad_to(c, p);
598 }
599 self.ensure_contour(c);
600 self.verbs.push(Verb::Conic);
601 self.push_point(c);
602 self.push_point(p);
603 self.weights.push(weight);
604 self
605 }
606
607 pub fn cubic_to(
609 &mut self,
610 c1: impl Into<Point>,
611 c2: impl Into<Point>,
612 p: impl Into<Point>,
613 ) -> &mut Self {
614 let (c1, c2, p) = (c1.into(), c2.into(), p.into());
615 self.ensure_contour(c1);
616 self.verbs.push(Verb::Cubic);
617 self.push_point(c1);
618 self.push_point(c2);
619 self.push_point(p);
620 self
621 }
622
623 pub fn close(&mut self) -> &mut Self {
627 if self.contour_open {
628 self.verbs.push(Verb::Close);
629 self.contour_open = false;
630 }
631 self
632 }
633
634 pub fn rect(&mut self, r: Rect) -> &mut Self {
638 self.move_to((r.x, r.y))
639 .line_to((r.right(), r.y))
640 .line_to((r.right(), r.bottom()))
641 .line_to((r.x, r.bottom()))
642 .close();
643 self.resume_point = Some(Point::new(r.x, r.y));
647 self
648 }
649
650 pub fn rrect(&mut self, r: Rect, radius: f32) -> &mut Self {
652 self.rrect_radii(r, [radius; 4])
653 }
654
655 pub fn rrect_radii(&mut self, r: Rect, radii: [f32; 4]) -> &mut Self {
659 self.rrect_radii_elliptical(r, radii.map(|radius| [radius; 2]))
660 }
661
662 pub fn rrect_radii_elliptical(
667 &mut self,
668 r: impl Into<Rect>,
669 radii: [[f32; 2]; 4],
670 ) -> &mut Self {
671 self.rrect_radii_elliptical_wound(r, radii, Winding::Clockwise)
672 }
673
674 pub fn rrect_radii_elliptical_wound(
679 &mut self,
680 r: impl Into<Rect>,
681 radii: [[f32; 2]; 4],
682 winding: Winding,
683 ) -> &mut Self {
684 let r = r.into();
685 let [tl, tr, br, bl] = constrain_radii_elliptical(&r, radii);
686 let (l, t, rr, b) = (r.x, r.y, r.right(), r.bottom());
687 if [tl, tr, br, bl].iter().all(|[x, y]| *x == 0.0 && *y == 0.0) {
688 match winding {
689 Winding::Clockwise => self.rect(r),
690 Winding::CounterClockwise => self
691 .move_to((l, t))
692 .line_to((l, b))
693 .line_to((rr, b))
694 .line_to((rr, t))
695 .close(),
696 };
697 self.resume_point = Some(Point::new(l, t));
698 return self;
699 }
700 let w = std::f32::consts::FRAC_1_SQRT_2;
704 match winding {
705 Winding::Clockwise => self
706 .move_to((l + tl[0], t))
707 .line_to((rr - tr[0], t))
708 .conic_to((rr, t), (rr, t + tr[1]), w)
709 .line_to((rr, b - br[1]))
710 .conic_to((rr, b), (rr - br[0], b), w)
711 .line_to((l + bl[0], b))
712 .conic_to((l, b), (l, b - bl[1]), w)
713 .line_to((l, t + tl[1]))
714 .conic_to((l, t), (l + tl[0], t), w)
715 .close(),
716 Winding::CounterClockwise => self
720 .move_to((l + tl[0], t))
721 .conic_to((l, t), (l, t + tl[1]), w)
722 .line_to((l, b - bl[1]))
723 .conic_to((l, b), (l + bl[0], b), w)
724 .line_to((rr - br[0], b))
725 .conic_to((rr, b), (rr, b - br[1]), w)
726 .line_to((rr, t + tr[1]))
727 .conic_to((rr, t), (rr - tr[0], t), w)
728 .line_to((l + tl[0], t))
729 .close(),
730 };
731 self.resume_point = Some(Point::new(l, t));
744 self
745 }
746
747 pub fn rsuperellipse(&mut self, r: Rect, radius: f32) -> &mut Self {
753 self.rsuperellipse_radii(r, [[radius; 2]; 4])
754 }
755
756 pub fn rsuperellipse_radii(&mut self, r: Rect, radii: [[f32; 2]; 4]) -> &mut Self {
762 crate::superellipse::RoundSuperellipse::new(r, radii).emit(self);
763 self.resume_point = Some(Point::new(r.x, r.y));
764 self
765 }
766
767 pub fn arc(
772 &mut self,
773 center: impl Into<Point>,
774 radius: f32,
775 start_angle: f32,
776 sweep_angle: f32,
777 ) -> &mut Self {
778 self.ellipse(center, [radius; 2], 0.0, start_angle, sweep_angle)
779 }
780
781 pub fn ellipse(
790 &mut self,
791 center: impl Into<Point>,
792 radii: [f32; 2],
793 x_axis_rotation: f32,
794 start_angle: f32,
795 sweep_angle: f32,
796 ) -> &mut Self {
797 let center = center.into();
798 let [radius_x, radius_y] = radii;
799 let finite = center.x.is_finite()
804 && center.y.is_finite()
805 && radius_x.is_finite()
806 && radius_y.is_finite()
807 && x_axis_rotation.is_finite()
808 && start_angle.is_finite()
809 && sweep_angle.is_finite();
810 debug_assert!(
811 radius_x >= 0.0 && radius_y >= 0.0,
812 "negative radii draw nothing; Canvas2D throws here"
813 );
814 if !finite || radius_x < 0.0 || radius_y < 0.0 {
815 return self;
816 }
817
818 let full_turn = std::f32::consts::TAU;
823 let sweep_angle = sweep_angle.clamp(-full_turn, full_turn);
824
825 let unit_circle_to_ellipse = unit_circle_map(center, radii, x_axis_rotation);
826 let first = unit_circle_to_ellipse.map_point(unit_circle_point(start_angle));
827 if self.contour_open || self.resume_point.is_some() {
833 self.ensure_contour(first);
834 self.line_to(first);
835 } else {
836 self.move_to(first);
837 }
838 if sweep_angle != 0.0 {
839 self.push_arc_conics(&unit_circle_to_ellipse, start_angle, sweep_angle);
840 }
841 if sweep_angle.abs() >= full_turn {
844 self.close();
845 }
846 self
847 }
848
849 pub fn arc_to(
854 &mut self,
855 corner: impl Into<Point>,
856 next: impl Into<Point>,
857 radius: f32,
858 ) -> &mut Self {
859 let (corner, next) = (corner.into(), next.into());
860 self.ensure_contour(corner);
861 let start = *self.points.last().expect("ensure_contour opened a contour");
862
863 let incoming = normalize(
867 corner.x as f64 - start.x as f64,
868 corner.y as f64 - start.y as f64,
869 );
870 let outgoing = normalize(
871 next.x as f64 - corner.x as f64,
872 next.y as f64 - corner.y as f64,
873 );
874 let (Some(incoming), Some(outgoing)) = (incoming, outgoing) else {
875 return self.line_to(corner);
876 };
877 let cosine = incoming.0 * outgoing.0 + incoming.1 * outgoing.1;
878 let sine = incoming.0 * outgoing.1 - incoming.1 * outgoing.0;
879 if radius <= 0.0 || !radius.is_finite() || sine.abs() < 1.0 / (1 << 12) as f64 {
880 return self.line_to(corner);
881 }
882
883 let tangent_length = (radius as f64 * (1.0 - cosine) / sine).abs();
884 let entry = Point::new(
885 corner.x - (tangent_length * incoming.0) as f32,
886 corner.y - (tangent_length * incoming.1) as f32,
887 );
888 let turn = sine.signum() as f32;
890 let center = Point::new(
891 entry.x + radius * turn * -(incoming.1 as f32),
892 entry.y + radius * turn * incoming.0 as f32,
893 );
894 let exit = Point::new(
895 corner.x + (tangent_length * outgoing.0) as f32,
896 corner.y + (tangent_length * outgoing.1) as f32,
897 );
898
899 let start_angle = (entry.y - center.y).atan2(entry.x - center.x);
900 let end_angle = (exit.y - center.y).atan2(exit.x - center.x);
901 let sweep = shortest_sweep(start_angle, end_angle, turn);
902
903 self.line_to(entry);
904 let map = unit_circle_map(center, [radius; 2], 0.0);
905 self.push_arc_conics(&map, start_angle, sweep);
906 self
907 }
908
909 pub fn circle(&mut self, center: impl Into<Point>, radius: f32) -> &mut Self {
914 let c = center.into();
915 let r = radius;
916 let w = std::f32::consts::FRAC_1_SQRT_2;
917 self.move_to((c.x + r, c.y))
918 .conic_to((c.x + r, c.y + r), (c.x, c.y + r), w)
919 .conic_to((c.x - r, c.y + r), (c.x - r, c.y), w)
920 .conic_to((c.x - r, c.y - r), (c.x, c.y - r), w)
921 .conic_to((c.x + r, c.y - r), (c.x + r, c.y), w)
922 .close()
923 }
924
925 pub fn append(&mut self, path: &Path, transform: &Matrix) -> &mut Self {
930 if path.verbs.is_empty() {
931 return self;
934 }
935 let mut point = path.points.iter();
936 let mut weight = path.weights.iter();
937 let mut cursor = Point::ZERO;
938 let mut contour_start = Point::ZERO;
939 for verb in &path.verbs {
940 let count = match verb {
941 Verb::Move | Verb::Line => 1,
942 Verb::Quad | Verb::Conic => 2,
943 Verb::Cubic => 3,
944 Verb::Close => 0,
945 };
946 self.verbs.push(*verb);
947 for _ in 0..count {
948 let Some(&p) = point.next() else {
949 return self;
950 };
951 cursor = transform.map_point(p);
952 self.push_point(cursor);
953 }
954 if matches!(verb, Verb::Conic) {
955 let Some(&w) = weight.next() else {
960 return self;
961 };
962 self.weights.push(w);
963 }
964 match verb {
965 Verb::Move => contour_start = cursor,
966 Verb::Close => cursor = contour_start,
967 _ => {}
968 }
969 }
970 if matches!(path.verbs.last(), Some(Verb::Close)) {
981 self.move_to(cursor);
982 } else {
983 self.resume_point = Some(contour_start);
987 self.contour_open = true;
988 }
989 self
990 }
991
992 pub fn build(self) -> Arc<Path> {
994 Arc::new(Path {
995 verbs: self.verbs,
996 points: self.points,
997 weights: self.weights,
998 bounds: self.bounds.unwrap_or_default(),
999 })
1000 }
1001
1002 fn push_arc_conics(&mut self, map: &Matrix, start_angle: f32, sweep_angle: f32) {
1010 let piece_count = (sweep_angle.abs() / std::f32::consts::FRAC_PI_2)
1011 .ceil()
1012 .max(1.0);
1013 let step = sweep_angle / piece_count;
1014 let weight = (step / 2.0).cos();
1015
1016 let mut angle = start_angle;
1017 for _ in 0..piece_count as u32 {
1018 let mid = unit_circle_point(angle + step / 2.0);
1019 let control = Point::new(mid.x / weight, mid.y / weight);
1020 let to = unit_circle_point(angle + step);
1021 self.conic_to(map.map_point(control), map.map_point(to), weight);
1022 angle += step;
1023 }
1024 }
1025
1026 fn ensure_contour(&mut self, p: Point) {
1036 if self.contour_open {
1037 return;
1038 }
1039 self.move_to(self.resume_point.unwrap_or(p));
1040 }
1041
1042 fn push_point(&mut self, p: Point) {
1043 self.points.push(p);
1044 self.bounds = Some(match self.bounds {
1048 Some(b) => Rect::from_ltrb(
1049 b.x.min(p.x),
1050 b.y.min(p.y),
1051 b.right().max(p.x),
1052 b.bottom().max(p.y),
1053 ),
1054 None => Rect::new(p.x, p.y, 0.0, 0.0),
1055 });
1056 }
1057}
1058
1059fn unit_circle_point(angle: f32) -> Point {
1061 let (sine, cosine) = angle.sin_cos();
1062 Point::new(cosine, sine)
1063}
1064
1065fn unit_circle_map(center: Point, radii: [f32; 2], rotation: f32) -> Matrix {
1067 let [radius_x, radius_y] = radii;
1068 let (sine, cosine) = rotation.sin_cos();
1069 Matrix::from_affine(
1070 radius_x * cosine,
1071 radius_x * sine,
1072 -radius_y * sine,
1073 radius_y * cosine,
1074 center.x,
1075 center.y,
1076 )
1077}
1078
1079fn shortest_sweep(start: f32, end: f32, direction: f32) -> f32 {
1081 let mut sweep = end - start;
1082 let turn = std::f32::consts::TAU;
1083 while sweep > 0.0 && direction < 0.0 {
1084 sweep -= turn;
1085 }
1086 while sweep < 0.0 && direction > 0.0 {
1087 sweep += turn;
1088 }
1089 sweep
1090}
1091
1092fn normalize(x: f64, y: f64) -> Option<(f64, f64)> {
1094 let length = (x * x + y * y).sqrt();
1095 (length.is_finite() && length > 0.0).then(|| (x / length, y / length))
1096}
1097
1098pub fn constrain_radii(r: &Rect, radii: [f32; 4]) -> [f32; 4] {
1102 constrain_radii_elliptical(r, radii.map(|v| [v; 2])).map(|[x, _]| x)
1103}
1104
1105pub fn constrain_radii_elliptical(r: &Rect, radii: [[f32; 2]; 4]) -> [[f32; 2]; 4] {
1110 let [tl, tr, br, bl] = radii.map(|[x, y]| [x.max(0.0), y.max(0.0)]);
1111 let fit = |side: f32, a: f32, b: f32| if a + b <= side { 1.0 } else { side / (a + b) };
1112 let f = fit(r.width, tl[0], tr[0])
1113 .min(fit(r.width, bl[0], br[0]))
1114 .min(fit(r.height, tl[1], bl[1]))
1115 .min(fit(r.height, tr[1], br[1]));
1116 [tl, tr, br, bl].map(|[x, y]| [x * f, y * f])
1117}
1118
1119struct Flattener {
1121 tolerance: f32,
1122 contours: Vec<Contour>,
1123 current: Vec<Point>,
1124 has_segments: bool,
1126}
1127
1128impl Flattener {
1129 fn new(tolerance: f32) -> Self {
1130 Self {
1131 tolerance,
1132 contours: Vec::new(),
1133 current: Vec::new(),
1134 has_segments: false,
1135 }
1136 }
1137
1138 fn move_to(&mut self, p: Point) {
1139 self.flush(false);
1140 self.current.push(p);
1141 self.has_segments = false;
1142 }
1143
1144 fn line_to(&mut self, p: Point) {
1145 self.current.push(p);
1146 self.has_segments = true;
1147 }
1148
1149 fn quad_to(&mut self, c: Point, p: Point) {
1150 let Some(&start) = self.current.last() else {
1151 return;
1152 };
1153 self.has_segments = true;
1154 let dev = second_difference(start, c, p);
1155 let n = segment_count((dev / (8.0 * self.tolerance)).sqrt());
1156 for i in 1..=n {
1157 let t = i as f32 / n as f32;
1158 self.current.push(eval_quad(start, c, p, t));
1159 }
1160 }
1161
1162 fn conic_to(&mut self, c: Point, p: Point, w: f32) {
1163 let Some(&start) = self.current.last() else {
1164 return;
1165 };
1166 self.has_segments = true;
1167 let n = segment_count(conic_segment_estimate(start, c, p, w, self.tolerance));
1168 for i in 1..=n {
1169 let t = i as f32 / n as f32;
1170 self.current.push(eval_conic(start, c, p, w, t));
1171 }
1172 }
1173
1174 fn cubic_to(&mut self, c1: Point, c2: Point, p: Point) {
1175 let Some(&start) = self.current.last() else {
1176 return;
1177 };
1178 self.has_segments = true;
1179 let dev = second_difference(start, c1, c2).max(second_difference(c1, c2, p));
1180 let n = segment_count((3.0 * dev / (4.0 * self.tolerance)).sqrt());
1181 for i in 1..=n {
1182 let t = i as f32 / n as f32;
1183 self.current.push(eval_cubic(start, c1, c2, p, t));
1184 }
1185 }
1186
1187 fn close(&mut self) {
1188 if let (Some(&first), Some(&last)) = (self.current.first(), self.current.last()) {
1191 if self.current.len() >= 2 && (first.x, first.y) != (last.x, last.y) {
1192 self.current.push(first);
1193 }
1194 }
1195 if !self.current.is_empty() {
1201 self.has_segments = true;
1202 }
1203 self.flush(true);
1204 }
1205
1206 fn finish(mut self) -> Vec<Contour> {
1207 self.flush(false);
1208 self.contours
1209 }
1210
1211 fn flush(&mut self, closed: bool) {
1218 if !self.current.is_empty() {
1219 self.contours.push(Contour {
1220 points: std::mem::take(&mut self.current),
1221 closed,
1222 has_segments: self.has_segments,
1223 });
1224 }
1225 self.has_segments = false;
1226 }
1227}
1228
1229fn conic_segment_estimate(start: Point, control: Point, end: Point, w: f32, tolerance: f32) -> f32 {
1233 let precision = 1.0 / tolerance;
1234 let cx = 0.5 * (start.x.min(control.x).min(end.x) + start.x.max(control.x).max(end.x));
1236 let cy = 0.5 * (start.y.min(control.y).min(end.y) + start.y.max(control.y).max(end.y));
1237 let (p0x, p0y) = (start.x - cx, start.y - cy);
1238 let (p1x, p1y) = (control.x - cx, control.y - cy);
1239 let (p2x, p2y) = (end.x - cx, end.y - cy);
1240 let max_len_sq = (p0x * p0x + p0y * p0y)
1241 .max(p1x * p1x + p1y * p1y)
1242 .max(p2x * p2x + p2y * p2y);
1243 let (dpx, dpy) = (p0x - 2.0 * w * p1x + p2x, p0y - 2.0 * w * p1y + p2y);
1244 let dw = (2.0 - 2.0 * w).abs();
1245 let rp_minus_1 = (max_len_sq.sqrt() * precision - 1.0).max(0.0);
1246 let numer = (dpx * dpx + dpy * dpy).sqrt() * precision + rp_minus_1 * dw;
1247 let denom = 4.0 * w.min(1.0);
1248 (numer / denom).sqrt()
1249}
1250
1251fn second_difference(a: Point, b: Point, c: Point) -> f32 {
1252 let dx = a.x - 2.0 * b.x + c.x;
1253 let dy = a.y - 2.0 * b.y + c.y;
1254 (dx * dx + dy * dy).sqrt()
1255}
1256
1257fn segment_count(estimate: f32) -> u32 {
1258 (estimate.ceil() as u32).clamp(1, 64)
1259}
1260
1261fn eval_quad(p0: Point, c: Point, p1: Point, t: f32) -> Point {
1262 let u = 1.0 - t;
1263 Point::new(
1264 u * u * p0.x + 2.0 * u * t * c.x + t * t * p1.x,
1265 u * u * p0.y + 2.0 * u * t * c.y + t * t * p1.y,
1266 )
1267}
1268
1269fn eval_cubic(p0: Point, c1: Point, c2: Point, p1: Point, t: f32) -> Point {
1270 let u = 1.0 - t;
1271 let (uu, tt) = (u * u, t * t);
1272 Point::new(
1273 u * uu * p0.x + 3.0 * uu * t * c1.x + 3.0 * u * tt * c2.x + t * tt * p1.x,
1274 u * uu * p0.y + 3.0 * uu * t * c1.y + 3.0 * u * tt * c2.y + t * tt * p1.y,
1275 )
1276}
1277
1278pub fn local_tolerance(transform: &Matrix) -> f32 {
1280 0.25 / transform.max_scale().max(1e-3)
1281}
1282
1283#[cfg(test)]
1284mod tests {
1285 use super::*;
1286
1287 #[test]
1294 fn opposed_windings_cancel_under_the_non_zero_rule() {
1295 let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
1296 let radii = [[12.0, 12.0]; 4];
1297 let inside = Point::new(50.0, 50.0);
1298
1299 let mut opposed = PathBuilder::new();
1300 opposed.rrect_radii_elliptical_wound(rect, radii, Winding::Clockwise);
1301 opposed.rrect_radii_elliptical_wound(rect, radii, Winding::CounterClockwise);
1302 assert!(
1303 !opposed.build().contains(inside, FillRule::NonZero),
1304 "opposed windings must cancel"
1305 );
1306
1307 let mut agreeing = PathBuilder::new();
1308 agreeing.rrect_radii_elliptical_wound(rect, radii, Winding::Clockwise);
1309 agreeing.rrect_radii_elliptical_wound(rect, radii, Winding::Clockwise);
1310 assert!(
1311 agreeing.build().contains(inside, FillRule::NonZero),
1312 "agreeing windings must reinforce"
1313 );
1314 }
1315
1316 #[test]
1320 fn winding_reverses_the_walk_without_moving_the_outline() {
1321 let rect = Rect::new(10.0, 20.0, 80.0, 60.0);
1322 let radii = [[8.0, 14.0], [4.0, 4.0], [20.0, 6.0], [0.0, 0.0]];
1323 let wound = |winding| {
1324 let mut path = PathBuilder::new();
1325 path.rrect_radii_elliptical_wound(rect, radii, winding);
1326 path.build()
1327 };
1328 let clockwise = wound(Winding::Clockwise);
1329 let counter = wound(Winding::CounterClockwise);
1330 assert_eq!(clockwise.tight_bounds(), counter.tight_bounds());
1331 for point in [
1334 Point::new(50.0, 50.0),
1335 Point::new(14.0, 30.0),
1336 Point::new(86.0, 24.0),
1337 Point::new(74.0, 76.0),
1338 Point::new(12.0, 78.0),
1339 Point::new(5.0, 15.0),
1340 Point::new(95.0, 85.0),
1341 ] {
1342 assert_eq!(
1343 clockwise.contains(point, FillRule::NonZero),
1344 counter.contains(point, FillRule::NonZero),
1345 "the two directions disagree about {point:?}"
1346 );
1347 }
1348 }
1349
1350 #[test]
1358 fn a_segment_after_close_resumes_at_the_contour_origin() {
1359 let mut path = PathBuilder::new();
1360 path.move_to((10.0, 10.0));
1361 path.line_to((30.0, 10.0));
1362 path.close();
1363 path.line_to((30.0, 30.0));
1364 let path = path.build();
1365
1366 let contours = path.flatten(0.05);
1368 let resumed = contours.last().expect("the path continues after the close");
1369 assert_eq!(
1370 resumed.points.first().copied(),
1371 Some(Point::new(10.0, 10.0)),
1372 "the segment after close must start at the contour origin, not its own end"
1373 );
1374 assert!(crate::stroke_contains(
1375 &contours,
1376 &crate::Stroke::new(6.0),
1377 0.05,
1378 Point::new(20.0, 20.0)
1379 ));
1380 }
1381
1382 #[test]
1390 fn a_segment_after_a_shape_helper_resumes_at_the_box_corner() {
1391 let box_corner = Point::new(10.0, 10.0);
1392 for corner in [0.0f32, 8.0] {
1393 let mut path = PathBuilder::new();
1394 if corner == 0.0 {
1395 path.rect(Rect::new(10.0, 10.0, 40.0, 40.0));
1396 } else {
1397 path.rrect_radii_elliptical(Rect::new(10.0, 10.0, 40.0, 40.0), [[corner; 2]; 4]);
1398 }
1399 path.line_to((90.0, 90.0));
1400
1401 let contours = path.build().flatten(0.05);
1402 let resumed = contours.last().expect("the path continues after the shape");
1403 assert_eq!(
1404 resumed.points.first().copied(),
1405 Some(box_corner),
1406 "corner radius {corner}: the trailing segment starts at (x, y)"
1407 );
1408 }
1409
1410 let mut path = PathBuilder::new();
1414 path.rrect_radii_elliptical(Rect::new(10.0, 10.0, 40.0, 40.0), [[8.0; 2]; 4]);
1415 path.line_to((90.0, 90.0));
1416 let contours = path.build().flatten(0.05);
1417 let stroke = crate::Stroke::new(4.0);
1418 assert!(
1419 crate::stroke_contains(&contours, &stroke, 0.05, Point::new(50.0, 50.0)),
1420 "the diagonal from (10,10) must be stroked"
1421 );
1422 assert!(
1423 !crate::stroke_contains(&contours, &stroke, 0.05, Point::new(54.0, 50.0)),
1424 "the diagonal from the tangent (18,10) must not be"
1425 );
1426 }
1427
1428 #[test]
1431 fn close_still_resumes_at_the_contour_origin() {
1432 let mut path = PathBuilder::new();
1433 path.move_to((10.0, 10.0));
1434 path.line_to((30.0, 10.0));
1435 path.line_to((30.0, 30.0));
1436 path.close();
1437 path.line_to((90.0, 90.0));
1438 let contours = path.build().flatten(0.05);
1439 assert_eq!(
1440 contours
1441 .last()
1442 .and_then(|contour| contour.points.first())
1443 .copied(),
1444 Some(Point::new(10.0, 10.0)),
1445 "close resumes where the contour began, not at any box corner"
1446 );
1447 }
1448
1449 #[test]
1452 fn a_first_segment_with_no_contour_starts_at_its_own_point() {
1453 let mut path = PathBuilder::new();
1454 path.line_to((30.0, 30.0));
1455 assert_eq!(
1456 path.build().bounds(),
1457 Rect::from_ltrb(30.0, 30.0, 30.0, 30.0)
1458 );
1459 }
1460
1461 #[test]
1462 fn append_carries_verbs_through_the_transform() {
1463 let mut source = PathBuilder::new();
1464 source.rect(Rect::new(0.0, 0.0, 10.0, 10.0));
1465 let source = source.build();
1466
1467 let mut target = PathBuilder::new();
1468 target.rect(Rect::new(0.0, 0.0, 4.0, 4.0));
1469 target.append(&source, &Matrix::translation(100.0, 50.0));
1470 let target = target.build();
1471
1472 assert_eq!(target.bounds(), Rect::from_ltrb(0.0, 0.0, 110.0, 60.0));
1473 assert!(target.contains(Point::new(105.0, 55.0), FillRule::NonZero));
1474 assert!(!target.contains(Point::new(5.0, 5.0), FillRule::NonZero));
1475 }
1476
1477 #[test]
1481 fn appending_a_closed_contour_reopens_at_its_seam() {
1482 let mut source = PathBuilder::new();
1483 source.move_to((10.0, 10.0));
1484 source.line_to((20.0, 10.0));
1485 source.close();
1486 let source = source.build();
1487
1488 let mut target = PathBuilder::new();
1489 target.append(&source, &Matrix::IDENTITY);
1490 target.line_to((10.0, 40.0));
1491 let built = target.build();
1492
1493 assert_eq!(built.bounds(), Rect::from_ltrb(10.0, 10.0, 20.0, 40.0));
1495 assert!(built.contains(Point::new(10.0, 25.0), FillRule::NonZero));
1496 }
1497
1498 #[test]
1502 fn appending_an_open_contour_hands_over_its_origin() {
1503 let mut source = PathBuilder::new();
1504 source.move_to((50.0, 50.0));
1505 source.line_to((60.0, 50.0));
1506 let source = source.build();
1507
1508 let mut target = PathBuilder::new();
1509 target.move_to((0.0, 0.0));
1510 target.line_to((10.0, 0.0));
1511 target.append(&source, &Matrix::IDENTITY);
1512 target.close();
1513 target.line_to((90.0, 90.0));
1514
1515 let contours = target.build().flatten(0.05);
1516 let resumed = contours.last().expect("the path continues after the close");
1517 assert_eq!(
1518 resumed.points.first().copied(),
1519 Some(Point::new(50.0, 50.0)),
1520 "the resumed segment must start at the APPENDED contour's origin"
1521 );
1522 }
1523
1524 #[test]
1525 fn appending_nothing_leaves_an_open_contour_open() {
1526 let empty = PathBuilder::new().build();
1527 let mut target = PathBuilder::new();
1528 target.move_to((0.0, 0.0));
1529 target.line_to((10.0, 0.0));
1530 target.append(&empty, &Matrix::IDENTITY);
1531 target.line_to((10.0, 10.0));
1532 assert_eq!(
1533 target.build().bounds(),
1534 Rect::from_ltrb(0.0, 0.0, 10.0, 10.0)
1535 );
1536 }
1537
1538 #[test]
1539 fn appending_an_open_contour_leaves_it_open() {
1540 let mut source = PathBuilder::new();
1541 source.move_to((0.0, 0.0));
1542 source.line_to((10.0, 0.0));
1543 let source = source.build();
1544
1545 let mut target = PathBuilder::new();
1546 target.append(&source, &Matrix::IDENTITY);
1547 target.line_to((10.0, 10.0));
1550 assert_eq!(
1551 target.build().bounds(),
1552 Rect::from_ltrb(0.0, 0.0, 10.0, 10.0)
1553 );
1554 }
1555
1556 #[test]
1557 fn tight_bounds_use_curve_extrema_not_control_points() {
1558 let mut path = PathBuilder::new();
1559 path.move_to((0.0, 0.0));
1560 path.quad_to((100.0, 100.0), (200.0, 0.0));
1561 let path = path.build();
1562 assert_eq!(path.bounds(), Rect::new(0.0, 0.0, 200.0, 100.0));
1563 assert_eq!(path.tight_bounds(), Rect::new(0.0, 0.0, 200.0, 50.0));
1564 }
1565
1566 #[test]
1567 fn tight_bounds_keep_extrema_below_f32_epsilon() {
1568 let mut path = PathBuilder::new();
1569 path.move_to((0.0, 0.0));
1570 path.quad_to((0.0, 1.0e-8), (0.0, 0.0));
1571 let bounds = path.build().tight_bounds();
1572 assert!((bounds.height - 5.0e-9).abs() < 1.0e-12);
1573 }
1574
1575 #[test]
1576 fn cubic_extrema_preserve_the_small_root() {
1577 let roots = cubic_extrema(0.0, 1.0e-8, -0.5, -0.5);
1578 assert!(roots
1579 .into_iter()
1580 .flatten()
1581 .any(|root| (root - 1.0e-8).abs() < 1.0e-10));
1582 }
1583
1584 #[test]
1585 fn radii_constrain_together() {
1586 let r = Rect::new(0.0, 0.0, 100.0, 40.0);
1587 let out = constrain_radii(&r, [40.0, 10.0, 10.0, 40.0]);
1589 assert_eq!(out, [20.0, 5.0, 5.0, 20.0]);
1590 assert_eq!(constrain_radii(&r, [8.0, 8.0, 8.0, 8.0]), [8.0; 4]);
1592 }
1593
1594 #[test]
1595 fn per_corner_rrect_stays_in_rect() {
1596 let r = Rect::new(10.0, 10.0, 100.0, 60.0);
1597 let mut b = PathBuilder::new();
1598 b.rrect_radii(r, [30.0, 0.0, 16.0, 8.0]);
1599 assert_eq!(b.build().bounds(), r);
1600 }
1601
1602 #[test]
1603 fn bounds_cover_control_points() {
1604 let mut b = PathBuilder::new();
1605 b.move_to((10.0, 10.0)).quad_to((50.0, -20.0), (90.0, 10.0));
1606 let p = b.build();
1607 assert_eq!(p.bounds(), Rect::from_ltrb(10.0, -20.0, 90.0, 10.0));
1608 }
1609
1610 #[test]
1611 fn circle_flattens_to_radius() {
1612 let mut b = PathBuilder::new();
1613 b.circle((0.0, 0.0), 100.0);
1614 let contours = b.build().flatten(0.1);
1615 assert_eq!(contours.len(), 1);
1616 assert!(contours[0].closed, "circle closes its contour");
1617 for p in &contours[0].points {
1618 let r = (p.x * p.x + p.y * p.y).sqrt();
1619 assert!((r - 100.0).abs() < 0.5, "point off circle: r={r}");
1620 }
1621 }
1622
1623 #[test]
1624 fn finer_tolerance_means_more_segments() {
1625 let path = {
1626 let mut b = PathBuilder::new();
1627 b.circle((0.0, 0.0), 100.0);
1628 b.build()
1629 };
1630 let coarse = path.flatten(2.0)[0].points.len();
1631 let fine = path.flatten(0.05)[0].points.len();
1632 assert!(fine > coarse, "fine {fine} vs coarse {coarse}");
1633 }
1634
1635 #[test]
1636 fn small_contours_survive_for_the_stroker() {
1637 let mut b = PathBuilder::new();
1638 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);
1641 assert_eq!(contours.len(), 2);
1642 assert_eq!(contours[0].points.len(), 2);
1643 assert!(!contours[0].closed);
1644 assert_eq!(contours[1].points.len(), 1);
1645 }
1646
1647 #[test]
1648 fn close_emits_the_closing_edge_and_marks_the_contour() {
1649 let mut b = PathBuilder::new();
1650 b.move_to((0.0, 0.0))
1651 .line_to((10.0, 0.0))
1652 .line_to((10.0, 10.0))
1653 .close();
1654 let contours = b.build().flatten(0.1);
1655 assert!(contours[0].closed);
1656 assert_eq!(contours[0].points.len(), 4, "closing edge in the polyline");
1657 assert_eq!(contours[0].points[3], Point::new(0.0, 0.0));
1658 }
1659
1660 #[test]
1661 fn bounds_keep_a_first_point_at_the_origin() {
1662 let mut b = PathBuilder::new();
1663 b.move_to((0.0, 0.0)).line_to((50.0, 80.0));
1664 assert_eq!(b.build().bounds(), Rect::from_ltrb(0.0, 0.0, 50.0, 80.0));
1665
1666 let mut b = PathBuilder::new();
1667 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));
1669 }
1670
1671 #[test]
1672 fn curve_without_move_starts_contour() {
1673 let mut b = PathBuilder::new();
1674 b.line_to((10.0, 0.0))
1675 .line_to((10.0, 10.0))
1676 .line_to((0.0, 10.0));
1677 let contours = b.build().flatten(0.1);
1678 assert_eq!(contours.len(), 1);
1679 assert_eq!(contours[0].points.len(), 4);
1680 }
1681
1682 #[test]
1686 fn circular_rrect_is_the_equal_axes_elliptical_case() {
1687 let r = Rect::new(10.0, 20.0, 120.0, 80.0);
1688 let radii = [24.0, 8.0, 30.0, 0.0];
1689 let mut circular = PathBuilder::new();
1690 circular.rrect_radii(r, radii);
1691 let mut elliptical = PathBuilder::new();
1692 elliptical.rrect_radii_elliptical(r, radii.map(|v| [v; 2]));
1693 assert_eq!(
1694 circular.build().flatten(0.1)[0].points,
1695 elliptical.build().flatten(0.1)[0].points,
1696 );
1697 }
1698
1699 #[test]
1700 fn elliptical_radii_constrain_per_axis() {
1701 let r = Rect::new(0.0, 0.0, 100.0, 40.0);
1704 let out = constrain_radii_elliptical(
1705 &r,
1706 [[10.0, 20.0], [10.0, 20.0], [10.0, 30.0], [10.0, 30.0]],
1707 );
1708 assert_eq!(out[0], [8.0, 16.0]);
1709 assert_eq!(out[2], [8.0, 24.0]);
1710 let out = constrain_radii_elliptical(&r, [[-5.0, 10.0], [0.0; 2], [0.0; 2], [0.0; 2]]);
1712 assert_eq!(out[0], [0.0, 10.0]);
1713 }
1714
1715 #[test]
1716 fn elliptical_corner_lands_on_axis_extremes() {
1717 let r = Rect::new(0.0, 0.0, 200.0, 100.0);
1720 let mut b = PathBuilder::new();
1721 b.rrect_radii_elliptical(r, [[0.0; 2], [40.0, 10.0], [0.0; 2], [0.0; 2]]);
1722 let points = &b.build().flatten(0.05)[0].points;
1723 assert!(points
1726 .iter()
1727 .any(|p| (p.x - 160.0).abs() < 0.5 && p.y.abs() < 0.5));
1728 assert!(points
1729 .iter()
1730 .any(|p| (p.x - 200.0).abs() < 0.5 && (p.y - 10.0).abs() < 0.5));
1731 }
1732
1733 #[test]
1738 fn swept_arc_stays_on_its_circle() {
1739 let (center, radius) = (Point::new(50.0, 60.0), 40.0);
1740 let mut b = PathBuilder::new();
1741 b.arc(center, radius, 0.0, std::f32::consts::TAU);
1742 for point in &b.build().flatten(0.01)[0].points {
1743 let offset = (point.x - center.x).hypot(point.y - center.y);
1744 assert!(
1745 (offset - radius).abs() < 0.05,
1746 "point {point:?} is {offset} from the centre, not {radius}"
1747 );
1748 }
1749 }
1750
1751 #[test]
1753 fn quarter_arc_ends_where_it_should() {
1754 let mut b = PathBuilder::new();
1755 b.arc((0.0, 0.0), 100.0, 0.0, std::f32::consts::FRAC_PI_2);
1756 let points = &b.build().flatten(0.01)[0].points;
1757 let (first, last) = (points[0], *points.last().unwrap());
1758 assert!(
1759 (first.x - 100.0).abs() < 0.01 && first.y.abs() < 0.01,
1760 "{first:?}"
1761 );
1762 assert!(
1763 last.x.abs() < 0.05 && (last.y - 100.0).abs() < 0.05,
1764 "{last:?}"
1765 );
1766 }
1767
1768 #[test]
1770 fn ellipse_reaches_both_radii() {
1771 let mut b = PathBuilder::new();
1772 b.ellipse((0.0, 0.0), [80.0, 20.0], 0.0, 0.0, std::f32::consts::TAU);
1773 let points = &b.build().flatten(0.01)[0].points;
1774 let widest = points.iter().fold(0.0f32, |m, p| m.max(p.x.abs()));
1775 let tallest = points.iter().fold(0.0f32, |m, p| m.max(p.y.abs()));
1776 assert!((widest - 80.0).abs() < 0.1, "widest {widest}");
1777 assert!((tallest - 20.0).abs() < 0.1, "tallest {tallest}");
1778 }
1779
1780 #[test]
1782 fn ellipse_rotation_swaps_the_axes() {
1783 let mut b = PathBuilder::new();
1784 b.ellipse(
1785 (0.0, 0.0),
1786 [80.0, 20.0],
1787 std::f32::consts::FRAC_PI_2,
1788 0.0,
1789 std::f32::consts::TAU,
1790 );
1791 let points = &b.build().flatten(0.01)[0].points;
1792 let widest = points.iter().fold(0.0f32, |m, p| m.max(p.x.abs()));
1793 let tallest = points.iter().fold(0.0f32, |m, p| m.max(p.y.abs()));
1794 assert!((widest - 20.0).abs() < 0.1, "widest {widest}");
1795 assert!((tallest - 80.0).abs() < 0.1, "tallest {tallest}");
1796 }
1797
1798 #[test]
1802 fn arc_to_rounds_a_right_angle() {
1803 let radius = 20.0f32;
1804 let mut b = PathBuilder::new();
1805 b.move_to((0.0, 0.0))
1806 .arc_to((100.0, 0.0), (100.0, 100.0), radius);
1807 let points = &b.build().flatten(0.01)[0].points;
1808
1809 let entry = Point::new(100.0 - radius, 0.0);
1810 let exit = Point::new(100.0, radius);
1811 assert!(points
1812 .iter()
1813 .any(|p| (p.x - entry.x).abs() < 0.1 && (p.y - entry.y).abs() < 0.1));
1814 assert!(points
1815 .iter()
1816 .any(|p| (p.x - exit.x).abs() < 0.1 && (p.y - exit.y).abs() < 0.1));
1817
1818 let center = Point::new(100.0 - radius, radius);
1819 for point in points.iter().filter(|p| p.x > entry.x - 0.01) {
1820 let offset = (point.x - center.x).hypot(point.y - center.y);
1821 assert!(
1822 (offset - radius).abs() < 0.1,
1823 "{point:?} is {offset} from the centre"
1824 );
1825 }
1826 }
1827
1828 #[test]
1831 fn degenerate_arc_to_falls_back_to_a_line() {
1832 for (corner, next, radius) in [
1833 ((50.0, 0.0), (100.0, 0.0), 20.0), ((50.0, 0.0), (50.0, 50.0), 0.0), ] {
1836 let mut b = PathBuilder::new();
1837 b.move_to((0.0, 0.0)).arc_to(corner, next, radius);
1838 let points = &b.build().flatten(0.01)[0].points;
1839 assert_eq!(points.len(), 2, "expected a bare line, got {points:?}");
1840 assert!((points[1].x - corner.0).abs() < 0.01 && (points[1].y - corner.1).abs() < 0.01);
1841 }
1842 }
1843
1844 #[test]
1847 fn rect_contains_what_it_covers() {
1848 let mut b = PathBuilder::new();
1849 b.rect(Rect::new(10.0, 10.0, 80.0, 60.0));
1850 let path = b.build();
1851 assert!(path.contains(Point::new(50.0, 40.0), FillRule::NonZero));
1852 assert!(!path.contains(Point::new(5.0, 40.0), FillRule::NonZero));
1853 assert!(!path.contains(Point::new(50.0, 80.0), FillRule::NonZero));
1854 for on_outline in [
1857 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), ] {
1863 assert!(
1864 path.contains(on_outline, FillRule::NonZero),
1865 "{on_outline:?} is on the outline and must count as inside"
1866 );
1867 }
1868 }
1869
1870 #[test]
1874 fn an_enormous_sweep_stays_one_turn() {
1875 let mut b = PathBuilder::new();
1876 b.arc((0.0, 0.0), 50.0, 0.0, 1e20);
1877 let path = b.build();
1878 let contours = path.flatten(0.1);
1879 assert_eq!(contours.len(), 1);
1880 assert!(
1882 contours[0].points.len() < 1_000,
1883 "a clamped turn should stay small, got {}",
1884 contours[0].points.len()
1885 );
1886 assert!(contours[0].closed, "a full turn closes its contour");
1887 }
1888
1889 #[test]
1890 fn a_negative_sweep_turns_the_other_way() {
1891 let quarter = std::f32::consts::FRAC_PI_2;
1892 let mut clockwise = PathBuilder::new();
1893 clockwise.arc((0.0, 0.0), 50.0, 0.0, quarter);
1894 let mut anticlockwise = PathBuilder::new();
1895 anticlockwise.arc((0.0, 0.0), 50.0, 0.0, -quarter);
1896
1897 let forward = clockwise.build().bounds();
1900 let backward = anticlockwise.build().bounds();
1901 assert!(forward.bottom() > 40.0, "positive sweep reaches +y");
1902 assert!(backward.y < -40.0, "negative sweep reaches -y");
1903 }
1904
1905 #[test]
1908 fn circle_containment_is_exact_all_the_way_round() {
1909 let (center, radius) = (Point::new(0.0, 0.0), 100.0f32);
1910 let mut b = PathBuilder::new();
1911 b.circle(center, radius);
1912 let path = b.build();
1913 for step in 0..64 {
1914 let angle = step as f32 / 64.0 * std::f32::consts::TAU;
1915 let (sine, cosine) = angle.sin_cos();
1916 let inside = Point::new(cosine * radius * 0.99, sine * radius * 0.99);
1917 let outside = Point::new(cosine * radius * 1.01, sine * radius * 1.01);
1918 assert!(
1919 path.contains(inside, FillRule::NonZero),
1920 "{inside:?} should be in"
1921 );
1922 assert!(
1923 !path.contains(outside, FillRule::NonZero),
1924 "{outside:?} should be out"
1925 );
1926 }
1927 }
1928
1929 #[test]
1933 fn fill_rules_disagree_about_a_same_wound_hole() {
1934 let mut b = PathBuilder::new();
1935 b.rect(Rect::new(0.0, 0.0, 100.0, 100.0));
1936 b.rect(Rect::new(25.0, 25.0, 50.0, 50.0));
1937 let path = b.build();
1938 let middle = Point::new(50.0, 50.0);
1939 assert!(path.contains(middle, FillRule::NonZero));
1940 assert!(!path.contains(middle, FillRule::EvenOdd));
1941 let ring = Point::new(10.0, 50.0);
1943 assert!(path.contains(ring, FillRule::NonZero));
1944 assert!(path.contains(ring, FillRule::EvenOdd));
1945 }
1946
1947 #[test]
1949 fn open_contour_closes_implicitly() {
1950 let mut b = PathBuilder::new();
1951 b.move_to((0.0, 0.0))
1952 .line_to((100.0, 0.0))
1953 .line_to((100.0, 100.0));
1954 let path = b.build();
1955 assert!(path.contains(Point::new(80.0, 40.0), FillRule::NonZero));
1956 assert!(!path.contains(Point::new(20.0, 60.0), FillRule::NonZero));
1957 }
1958
1959 #[test]
1961 fn containment_handles_curves_that_double_back() {
1962 let mut b = PathBuilder::new();
1963 b.move_to((0.0, 0.0))
1964 .cubic_to((120.0, 120.0), (-20.0, 120.0), (100.0, 0.0))
1965 .close();
1966 let path = b.build();
1967 assert!(path.contains(Point::new(50.0, 40.0), FillRule::NonZero));
1968 assert!(!path.contains(Point::new(50.0, -10.0), FillRule::NonZero));
1969 assert!(!path.contains(Point::new(-30.0, 40.0), FillRule::NonZero));
1970 }
1971
1972 #[test]
1979 fn circle_flattens_onto_the_exact_circle() {
1980 let mut b = PathBuilder::new();
1981 b.circle((10.0, -20.0), 100.0);
1982 let contours = b.build().flatten(0.1);
1983 let points: Vec<Point> = contours.iter().flat_map(|c| c.points.clone()).collect();
1984 assert!(points.len() > 20);
1985 for p in &points {
1986 let distance = (p.x - 10.0).hypot(p.y + 20.0);
1987 assert!(
1988 (distance - 100.0).abs() < 2e-3,
1989 "{p:?} is off the circle by {}",
1990 (distance - 100.0).abs()
1991 );
1992 }
1993 }
1994
1995 #[test]
1998 fn ellipse_arc_flattens_onto_the_exact_ellipse() {
1999 let mut b = PathBuilder::new();
2000 let rotation = 0.5f32;
2001 b.ellipse((5.0, 7.0), [80.0, 30.0], rotation, 0.3, 4.0);
2002 let contours = b.build().flatten(0.05);
2003 let (sin_r, cos_r) = rotation.sin_cos();
2004 for p in contours.iter().flat_map(|c| &c.points) {
2005 let (dx, dy) = (p.x - 5.0, p.y - 7.0);
2006 let (ux, uy) = (dx * cos_r + dy * sin_r, -dx * sin_r + dy * cos_r);
2008 let value = (ux / 80.0).powi(2) + (uy / 30.0).powi(2);
2009 assert!((value - 1.0).abs() < 1e-3, "{p:?} deviates: {value}");
2010 }
2011 }
2012
2013 #[test]
2017 fn conic_circle_tight_bounds_are_exact() {
2018 let mut b = PathBuilder::new();
2019 b.circle((0.0, 0.0), 50.0);
2020 let tight = b.build().tight_bounds();
2021 for (got, want) in [
2022 (tight.x, -50.0),
2023 (tight.y, -50.0),
2024 (tight.right(), 50.0),
2025 (tight.bottom(), 50.0),
2026 ] {
2027 assert!((got - want).abs() < 1e-3, "bound {got} should be {want}");
2028 }
2029 let mut arc = PathBuilder::new();
2032 arc.arc((0.0, 0.0), 50.0, -3.0, 2.5);
2033 let tight = arc.build().tight_bounds();
2034 assert!((tight.y + 50.0).abs() < 1e-3, "arc top: {}", tight.y);
2035 }
2036
2037 #[test]
2040 fn conic_containment_is_exact_on_the_curve() {
2041 let mut b = PathBuilder::new();
2042 b.circle((0.0, 0.0), 100.0);
2043 let path = b.build();
2044 for i in 0..64 {
2045 let angle = i as f32 / 64.0 * std::f32::consts::TAU;
2046 let (s, c) = angle.sin_cos();
2047 let inside = Point::new(c * 99.95, s * 99.95);
2048 let outside = Point::new(c * 100.05, s * 100.05);
2049 assert!(
2050 path.contains(inside, FillRule::NonZero),
2051 "{inside:?} at angle {i} should be inside"
2052 );
2053 assert!(
2054 !path.contains(outside, FillRule::NonZero),
2055 "{outside:?} at angle {i} should be outside"
2056 );
2057 }
2058 }
2059
2060 #[test]
2063 fn conic_windings_cancel_under_the_non_zero_rule() {
2064 let mut b = PathBuilder::new();
2065 b.circle((0.0, 0.0), 100.0);
2066 let w = std::f32::consts::FRAC_1_SQRT_2;
2069 b.move_to((60.0, 0.0))
2070 .conic_to((60.0, -60.0), (0.0, -60.0), w)
2071 .conic_to((-60.0, -60.0), (-60.0, 0.0), w)
2072 .conic_to((-60.0, 60.0), (0.0, 60.0), w)
2073 .conic_to((60.0, 60.0), (60.0, 0.0), w)
2074 .close();
2075 let path = b.build();
2076 assert!(!path.contains(Point::new(0.0, 0.0), FillRule::NonZero));
2077 assert!(path.contains(Point::new(80.0, 0.0), FillRule::NonZero));
2078 }
2079
2080 #[test]
2083 fn append_preserves_conic_weights() {
2084 let mut source = PathBuilder::new();
2085 source.circle((0.0, 0.0), 40.0);
2086 let source = source.build();
2087 let mut b = PathBuilder::new();
2088 b.append(&source, &Matrix::rotation(0.7));
2089 for p in b.build().flatten(0.05).iter().flat_map(|c| &c.points) {
2090 let distance = p.x.hypot(p.y);
2091 assert!((distance - 40.0).abs() < 1e-3, "{p:?} off by {distance}");
2092 }
2093 }
2094
2095 #[test]
2098 fn degenerate_conic_weights_lower_to_simpler_verbs() {
2099 let mut zero = PathBuilder::new();
2100 zero.move_to((0.0, 0.0))
2101 .conic_to((50.0, 50.0), (100.0, 0.0), 0.0);
2102 let contour = &zero.build().flatten(0.1)[0];
2103 assert_eq!(contour.points.len(), 2, "weight 0 must be a straight line");
2104
2105 let mut quad = PathBuilder::new();
2106 quad.move_to((0.0, 0.0))
2107 .conic_to((50.0, 50.0), (100.0, 0.0), 1.0);
2108 let mut plain = PathBuilder::new();
2109 plain
2110 .move_to((0.0, 0.0))
2111 .quad_to((50.0, 50.0), (100.0, 0.0));
2112 assert_eq!(
2113 quad.build().flatten(0.1),
2114 plain.build().flatten(0.1),
2115 "weight 1 must match the plain quadratic"
2116 );
2117
2118 let mut infinite = PathBuilder::new();
2119 infinite
2120 .move_to((0.0, 0.0))
2121 .conic_to((50.0, 50.0), (100.0, 0.0), f32::INFINITY);
2122 let contour = &infinite.build().flatten(0.1)[0];
2123 assert_eq!(
2124 contour.points,
2125 vec![
2126 Point::new(0.0, 0.0),
2127 Point::new(50.0, 50.0),
2128 Point::new(100.0, 0.0)
2129 ],
2130 "an unbounded weight collapses onto the control polygon"
2131 );
2132 }
2133
2134 #[test]
2140 fn conic_extrema_survive_large_coordinates() {
2141 let p0 = Point::new(0.0, 442_027.38);
2142 let c = Point::new(50.0, 442_028.47);
2143 let p2 = Point::new(100.0, 442_027.03);
2144 let w = 20.189_045;
2145 let mut b = PathBuilder::new();
2146 b.move_to(p0).conic_to(c, p2, w).close();
2147 let path = b.build();
2148 let bottom = path.tight_bounds().bottom();
2149 assert!(
2150 (bottom - 442_028.4).abs() < 0.05,
2151 "tight bottom {bottom} should reach the true maximum ~442028.41"
2152 );
2153 assert!(
2154 path.contains(Point::new(49.660_536, 442_028.0), FillRule::NonZero),
2155 "an interior point above the chord must be inside"
2156 );
2157 }
2158
2159 #[test]
2164 fn conic_flattening_respects_tolerance_for_large_weights() {
2165 let (p0, c, p2, w) = (
2166 Point::new(0.0, 100.0),
2167 Point::new(35.0, 100.0),
2168 Point::new(40.0, 90.0),
2169 8.3f32,
2170 );
2171 let tolerance = 0.05f32;
2172 let mut b = PathBuilder::new();
2173 b.move_to(p0).conic_to(c, p2, w);
2174 let contour = &b.build().flatten(tolerance)[0];
2175 let reference: Vec<Point> = (0..=4096)
2177 .map(|i| eval_conic(p0, c, p2, w, i as f32 / 4096.0))
2178 .collect();
2179 for pair in contour.points.windows(2) {
2180 let mid = Point::new((pair[0].x + pair[1].x) / 2.0, (pair[0].y + pair[1].y) / 2.0);
2181 let distance = reference
2182 .iter()
2183 .map(|r| (r.x - mid.x).hypot(r.y - mid.y))
2184 .fold(f32::MAX, f32::min);
2185 assert!(
2186 distance <= tolerance * 1.1,
2187 "chord midpoint {mid:?} is {distance} off the curve"
2188 );
2189 }
2190 }
2191}
2192
2193#[cfg(test)]
2194mod element_tests {
2195 use super::*;
2196
2197 #[test]
2198 fn elements_rebuild_an_equal_path() {
2199 let mut builder = PathBuilder::new();
2200 builder
2201 .move_to((0.0, 0.0))
2202 .line_to((10.0, 0.0))
2203 .quad_to((10.0, 5.0), (10.0, 10.0))
2204 .conic_to((5.0, 10.0), (0.0, 10.0), 0.7)
2205 .cubic_to((0.0, 7.0), (0.0, 3.0), (0.0, 0.0))
2206 .close();
2207 let path = builder.build();
2208 let mut rebuilt = PathBuilder::new();
2209 for element in path.elements() {
2210 match element {
2211 PathElement::MoveTo(p) => rebuilt.move_to(p),
2212 PathElement::LineTo(p) => rebuilt.line_to(p),
2213 PathElement::QuadTo(c, p) => rebuilt.quad_to(c, p),
2214 PathElement::ConicTo(c, p, w) => rebuilt.conic_to(c, p, w),
2215 PathElement::CubicTo(a, b, p) => rebuilt.cubic_to(a, b, p),
2216 PathElement::Close => rebuilt.close(),
2217 };
2218 }
2219 let rebuilt = rebuilt.build();
2220 assert_eq!(path.elements().count(), 6);
2221 assert_eq!(rebuilt.bounds(), path.bounds());
2222 assert!(path.elements().eq(rebuilt.elements()));
2223 }
2224}