1use std::f64::consts::PI;
11
12use kurbo::{Affine, BezPath, Point, Rect, Vec2};
13
14pub const DISTANCE_EPSILON: f64 = 1e-5;
20
21const COLLINEAR_SIN_EPSILON: f64 = 1e-3;
25
26fn lerp(start: f64, stop: f64, fraction: f64) -> f64 {
34 start * (1.0 - fraction) + stop * fraction
35}
36
37fn lerp_point(start: Point, stop: Point, fraction: f64) -> Point {
39 Point::new(
40 lerp(start.x, stop.x, fraction),
41 lerp(start.y, stop.y, fraction),
42 )
43}
44
45fn rotate90(v: Vec2) -> Vec2 {
47 Vec2::new(-v.y, v.x)
48}
49
50fn direction(v: Vec2) -> Vec2 {
57 let d = v.hypot();
58 debug_assert!(d > 0.0, "cannot take the direction of a zero-length vector");
59 v / d
60}
61
62fn radial_to_cartesian(radius: f64, angle_radians: f64) -> Vec2 {
64 Vec2::new(angle_radians.cos(), angle_radians.sin()) * radius
65}
66
67fn convex(previous: Point, current: Point, next: Point) -> bool {
72 (current - previous).cross(next - current) > 0.0
73}
74
75fn calculate_center(vertices: &[Point]) -> Point {
77 let n = vertices.len() as f64;
78 let sum = vertices.iter().fold(Vec2::ZERO, |acc, p| acc + p.to_vec2());
79 (sum / n).to_point()
80}
81
82#[derive(Clone, Copy, Debug, PartialEq)]
101pub struct CornerRounding {
102 pub radius: f64,
105 pub smoothing: f64,
107}
108
109impl CornerRounding {
110 pub const UNROUNDED: CornerRounding = CornerRounding {
112 radius: 0.0,
113 smoothing: 0.0,
114 };
115
116 #[must_use]
122 pub fn radius(radius: f64) -> CornerRounding {
123 CornerRounding::new(radius, 0.0)
124 }
125
126 #[must_use]
132 pub fn new(radius: f64, smoothing: f64) -> CornerRounding {
133 assert!(radius >= 0.0, "corner radius must be >= 0, got {radius}");
134 assert!(
135 (0.0..=1.0).contains(&smoothing),
136 "corner smoothing must be in 0..=1, got {smoothing}"
137 );
138 CornerRounding { radius, smoothing }
139 }
140}
141
142impl Default for CornerRounding {
143 fn default() -> Self {
144 CornerRounding::UNROUNDED
145 }
146}
147
148#[derive(Clone, Copy, Debug, PartialEq)]
159pub struct Cubic {
160 points: [f64; 8],
161}
162
163impl Cubic {
164 #[must_use]
166 pub const fn new(anchor0: Point, control0: Point, control1: Point, anchor1: Point) -> Cubic {
167 Cubic {
168 points: [
169 anchor0.x, anchor0.y, control0.x, control0.y, control1.x, control1.y, anchor1.x,
170 anchor1.y,
171 ],
172 }
173 }
174
175 #[must_use]
177 pub const fn from_raw(points: [f64; 8]) -> Cubic {
178 Cubic { points }
179 }
180
181 #[must_use]
183 pub const fn points(&self) -> &[f64; 8] {
184 &self.points
185 }
186
187 #[must_use]
190 pub fn straight_line(p0: Point, p1: Point) -> Cubic {
191 Cubic::new(
192 p0,
193 lerp_point(p0, p1, 1.0 / 3.0),
194 lerp_point(p0, p1, 2.0 / 3.0),
195 p1,
196 )
197 }
198
199 #[must_use]
201 pub fn empty(p: Point) -> Cubic {
202 Cubic::new(p, p, p, p)
203 }
204
205 #[must_use]
212 pub fn circular_arc(center: Point, p0: Point, p1: Point) -> Cubic {
213 let p0d = direction(p0 - center);
214 let p1d = direction(p1 - center);
215 let rotated_p0 = rotate90(p0d);
216 let rotated_p1 = rotate90(p1d);
217 let clockwise = rotated_p0.dot(p1 - center) >= 0.0;
218 let cosa = p0d.dot(p1d);
219
220 if cosa > 0.999 {
222 return Cubic::straight_line(p0, p1);
223 }
224
225 let k = (p0 - center).hypot() * 4.0 / 3.0
226 * ((2.0 * (1.0 - cosa)).sqrt() - (1.0 - cosa * cosa).sqrt())
227 / (1.0 - cosa)
228 * if clockwise { 1.0 } else { -1.0 };
229
230 Cubic::new(p0, p0 + rotated_p0 * k, p1 - rotated_p1 * k, p1)
231 }
232
233 #[must_use]
235 pub const fn anchor0(&self) -> Point {
236 Point::new(self.points[0], self.points[1])
237 }
238
239 #[must_use]
241 pub const fn control0(&self) -> Point {
242 Point::new(self.points[2], self.points[3])
243 }
244
245 #[must_use]
247 pub const fn control1(&self) -> Point {
248 Point::new(self.points[4], self.points[5])
249 }
250
251 #[must_use]
253 pub const fn anchor1(&self) -> Point {
254 Point::new(self.points[6], self.points[7])
255 }
256
257 #[must_use]
260 pub fn point_on_curve(&self, t: f64) -> Point {
261 let u = 1.0 - t;
262 let (a0, c0, c1, a1) = (
263 self.anchor0(),
264 self.control0(),
265 self.control1(),
266 self.anchor1(),
267 );
268 Point::new(
269 a0.x * (u * u * u)
270 + c0.x * (3.0 * t * u * u)
271 + c1.x * (3.0 * t * t * u)
272 + a1.x * (t * t * t),
273 a0.y * (u * u * u)
274 + c0.y * (3.0 * t * u * u)
275 + c1.y * (3.0 * t * t * u)
276 + a1.y * (t * t * t),
277 )
278 }
279
280 #[must_use]
284 pub fn zero_length(&self) -> bool {
285 (self.points[0] - self.points[6]).abs() < DISTANCE_EPSILON
286 && (self.points[1] - self.points[7]).abs() < DISTANCE_EPSILON
287 }
288
289 #[must_use]
292 pub fn split(&self, t: f64) -> (Cubic, Cubic) {
293 let u = 1.0 - t;
294 let point = self.point_on_curve(t);
295 let (a0, c0, c1, a1) = (
296 self.anchor0(),
297 self.control0(),
298 self.control1(),
299 self.anchor1(),
300 );
301
302 (
303 Cubic::new(
304 a0,
305 Point::new(a0.x * u + c0.x * t, a0.y * u + c0.y * t),
306 Point::new(
307 a0.x * (u * u) + c0.x * (2.0 * u * t) + c1.x * (t * t),
308 a0.y * (u * u) + c0.y * (2.0 * u * t) + c1.y * (t * t),
309 ),
310 point,
311 ),
312 Cubic::new(
313 point,
314 Point::new(
315 c0.x * (u * u) + c1.x * (2.0 * u * t) + a1.x * (t * t),
316 c0.y * (u * u) + c1.y * (2.0 * u * t) + a1.y * (t * t),
317 ),
318 Point::new(c1.x * u + a1.x * t, c1.y * u + a1.y * t),
319 a1,
320 ),
321 )
322 }
323
324 #[must_use]
326 pub fn reversed(&self) -> Cubic {
327 Cubic::new(
328 self.anchor1(),
329 self.control1(),
330 self.control0(),
331 self.anchor0(),
332 )
333 }
334
335 #[must_use]
337 pub fn transformed_with(&self, f: impl Fn(Point) -> Point) -> Cubic {
338 Cubic::new(
339 f(self.anchor0()),
340 f(self.control0()),
341 f(self.control1()),
342 f(self.anchor1()),
343 )
344 }
345
346 #[must_use]
349 pub fn approximate_bounds(&self) -> Rect {
350 let xs = [
351 self.points[0],
352 self.points[2],
353 self.points[4],
354 self.points[6],
355 ];
356 let ys = [
357 self.points[1],
358 self.points[3],
359 self.points[5],
360 self.points[7],
361 ];
362 Rect::new(
363 xs.iter().copied().fold(f64::INFINITY, f64::min),
364 ys.iter().copied().fold(f64::INFINITY, f64::min),
365 xs.iter().copied().fold(f64::NEG_INFINITY, f64::max),
366 ys.iter().copied().fold(f64::NEG_INFINITY, f64::max),
367 )
368 }
369
370 #[must_use]
373 pub fn exact_bounds(&self) -> Rect {
374 if self.zero_length() {
376 let p = self.anchor0();
377 return Rect::new(p.x, p.y, p.x, p.y);
378 }
379
380 let (a0, c0, c1, a1) = (
381 self.anchor0(),
382 self.control0(),
383 self.control1(),
384 self.anchor1(),
385 );
386 let mut min = Point::new(a0.x.min(a1.x), a0.y.min(a1.y));
387 let mut max = Point::new(a0.x.max(a1.x), a0.y.max(a1.y));
388
389 for axis_x in [true, false] {
392 let (p0, p1, p2, p3) = if axis_x {
393 (a0.x, c0.x, c1.x, a1.x)
394 } else {
395 (a0.y, c0.y, c1.y, a1.y)
396 };
397 let a = -p0 + 3.0 * p1 - 3.0 * p2 + p3;
398 let b = 2.0 * p0 - 4.0 * p1 + 2.0 * p2;
399 let c = -p0 + p1;
400
401 let mut roots = [None, None];
402 if a.abs() < DISTANCE_EPSILON {
403 if b != 0.0 {
405 roots[0] = Some(2.0 * c / (-2.0 * b));
406 }
407 } else {
408 let s = b * b - 4.0 * a * c;
409 if s >= 0.0 {
410 roots[0] = Some((-b + s.sqrt()) / (2.0 * a));
411 roots[1] = Some((-b - s.sqrt()) / (2.0 * a));
412 }
413 }
414
415 for t in roots.into_iter().flatten() {
416 if !(0.0..=1.0).contains(&t) {
417 continue;
418 }
419 let p = self.point_on_curve(t);
420 if axis_x {
421 min.x = min.x.min(p.x);
422 max.x = max.x.max(p.x);
423 } else {
424 min.y = min.y.min(p.y);
425 max.y = max.y.max(p.y);
426 }
427 }
428 }
429
430 Rect::new(min.x, min.y, max.x, max.y)
431 }
432}
433
434#[derive(Clone, Copy, Debug, PartialEq, Eq)]
440pub enum FeatureKind {
441 Edge,
444 ConvexCorner,
446 ConcaveCorner,
448}
449
450#[derive(Clone, Debug, PartialEq)]
459pub struct Feature {
460 cubics: Vec<Cubic>,
461 kind: FeatureKind,
462}
463
464impl Feature {
465 #[must_use]
467 pub fn edge(cubic: Cubic) -> Feature {
468 Feature {
469 cubics: vec![cubic],
470 kind: FeatureKind::Edge,
471 }
472 }
473
474 #[must_use]
484 pub fn ignorable(cubics: Vec<Cubic>) -> Feature {
485 Feature::validated(cubics, FeatureKind::Edge)
486 }
487
488 #[must_use]
494 pub fn convex_corner(cubics: Vec<Cubic>) -> Feature {
495 Feature::validated(cubics, FeatureKind::ConvexCorner)
496 }
497
498 #[must_use]
504 pub fn concave_corner(cubics: Vec<Cubic>) -> Feature {
505 Feature::validated(cubics, FeatureKind::ConcaveCorner)
506 }
507
508 fn validated(cubics: Vec<Cubic>, kind: FeatureKind) -> Feature {
509 assert!(!cubics.is_empty(), "a feature needs at least one cubic");
510 for pair in cubics.windows(2) {
511 let (prev, next) = (pair[0].anchor1(), pair[1].anchor0());
512 assert!(
513 (next.x - prev.x).abs() <= DISTANCE_EPSILON
514 && (next.y - prev.y).abs() <= DISTANCE_EPSILON,
515 "a feature must be continuous: {next:?} does not meet {prev:?}"
516 );
517 }
518 Feature { cubics, kind }
519 }
520
521 fn unchecked(cubics: Vec<Cubic>, kind: FeatureKind) -> Feature {
524 Feature { cubics, kind }
525 }
526
527 #[must_use]
529 pub fn cubics(&self) -> &[Cubic] {
530 &self.cubics
531 }
532
533 #[must_use]
535 pub const fn kind(&self) -> FeatureKind {
536 self.kind
537 }
538
539 #[must_use]
541 pub const fn is_ignorable(&self) -> bool {
542 matches!(self.kind, FeatureKind::Edge)
543 }
544
545 #[must_use]
547 pub const fn is_edge(&self) -> bool {
548 matches!(self.kind, FeatureKind::Edge)
549 }
550
551 #[must_use]
553 pub const fn is_corner(&self) -> bool {
554 !self.is_edge()
555 }
556
557 #[must_use]
559 pub const fn is_convex_corner(&self) -> bool {
560 matches!(self.kind, FeatureKind::ConvexCorner)
561 }
562
563 #[must_use]
565 pub const fn is_concave_corner(&self) -> bool {
566 matches!(self.kind, FeatureKind::ConcaveCorner)
567 }
568
569 #[must_use]
571 pub fn transformed_with(&self, f: impl Fn(Point) -> Point + Copy) -> Feature {
572 Feature {
573 cubics: self.cubics.iter().map(|c| c.transformed_with(f)).collect(),
574 kind: self.kind,
575 }
576 }
577
578 #[must_use]
585 pub fn reversed(&self) -> Feature {
586 Feature {
587 cubics: self.cubics.iter().rev().map(Cubic::reversed).collect(),
588 kind: match self.kind {
589 FeatureKind::Edge => FeatureKind::Edge,
590 FeatureKind::ConvexCorner => FeatureKind::ConcaveCorner,
591 FeatureKind::ConcaveCorner => FeatureKind::ConvexCorner,
592 },
593 }
594 }
595}
596
597#[derive(Clone, Copy, Debug)]
604pub struct StarParams<'a> {
605 pub num_vertices_per_radius: usize,
607 pub radius: f64,
609 pub inner_radius: f64,
611 pub rounding: CornerRounding,
613 pub inner_rounding: Option<CornerRounding>,
616 pub per_vertex_rounding: Option<&'a [CornerRounding]>,
618 pub center: Point,
620}
621
622impl Default for StarParams<'_> {
623 fn default() -> Self {
624 StarParams {
625 num_vertices_per_radius: 5,
626 radius: 1.0,
627 inner_radius: 0.5,
628 rounding: CornerRounding::UNROUNDED,
629 inner_rounding: None,
630 per_vertex_rounding: None,
631 center: Point::ZERO,
632 }
633 }
634}
635
636#[derive(Clone, Copy, Debug)]
639pub struct PillStarParams<'a> {
640 pub width: f64,
642 pub height: f64,
644 pub num_vertices_per_radius: usize,
646 pub inner_radius_ratio: f64,
648 pub rounding: CornerRounding,
650 pub inner_rounding: Option<CornerRounding>,
653 pub per_vertex_rounding: Option<&'a [CornerRounding]>,
655 pub vertex_spacing: f64,
660 pub start_location: f64,
663 pub center: Point,
665}
666
667impl Default for PillStarParams<'_> {
668 fn default() -> Self {
669 PillStarParams {
670 width: 2.0,
671 height: 1.0,
672 num_vertices_per_radius: 8,
673 inner_radius_ratio: 0.5,
674 rounding: CornerRounding::UNROUNDED,
675 inner_rounding: None,
676 per_vertex_rounding: None,
677 vertex_spacing: 0.5,
678 start_location: 0.0,
679 center: Point::ZERO,
680 }
681 }
682}
683
684#[derive(Clone, Debug, PartialEq)]
696pub struct RoundedPolygon {
697 features: Vec<Feature>,
698 center: Point,
699 cubics: Vec<Cubic>,
700}
701
702impl RoundedPolygon {
703 #[must_use]
716 pub fn from_features(features: Vec<Feature>, center: Option<Point>) -> RoundedPolygon {
717 assert!(
718 features.len() >= 2,
719 "a polygon needs at least 2 features, got {}",
720 features.len()
721 );
722
723 let center = center.unwrap_or_else(|| {
724 let anchors: Vec<Point> = features
725 .iter()
726 .flat_map(|f| f.cubics().iter().map(Cubic::anchor0))
727 .collect();
728 calculate_center(&anchors)
729 });
730
731 let cubics = init_cubics(&features, center);
732
733 debug_assert!(
734 is_contiguous(&cubics),
735 "a polygon must be contiguous: every cubic's start anchor must meet \
736 the previous cubic's end anchor"
737 );
738
739 RoundedPolygon {
740 features,
741 center,
742 cubics,
743 }
744 }
745
746 #[must_use]
759 pub fn from_vertices(
760 vertices: &[Point],
761 rounding: CornerRounding,
762 per_vertex_rounding: Option<&[CornerRounding]>,
763 center: Option<Point>,
764 ) -> RoundedPolygon {
765 let n = vertices.len();
766 assert!(n >= 3, "a polygon needs at least 3 vertices, got {n}");
767 if let Some(pvr) = per_vertex_rounding {
768 assert!(
769 pvr.len() == n,
770 "per-vertex rounding must have one entry per vertex ({n}), got {}",
771 pvr.len()
772 );
773 }
774
775 let corners: Vec<RoundedCorner> = (0..n)
776 .map(|i| {
777 RoundedCorner::new(
778 vertices[(i + n - 1) % n],
779 vertices[i],
780 vertices[(i + 1) % n],
781 per_vertex_rounding.map_or(rounding, |pvr| pvr[i]),
782 )
783 })
784 .collect();
785
786 let cut_adjusts: Vec<(f64, f64)> = (0..n)
792 .map(|i| {
793 let next = (i + 1) % n;
794 let expected_round_cut =
795 corners[i].expected_round_cut + corners[next].expected_round_cut;
796 let expected_cut = corners[i].expected_cut() + corners[next].expected_cut();
797 let side_size = (vertices[i] - vertices[next]).hypot();
798
799 if expected_round_cut > side_size {
800 (side_size / expected_round_cut, 0.0)
803 } else if expected_cut > side_size {
804 (
806 1.0,
807 (side_size - expected_round_cut) / (expected_cut - expected_round_cut),
808 )
809 } else {
810 (1.0, 1.0)
811 }
812 })
813 .collect();
814
815 let corner_cubics: Vec<Vec<Cubic>> = (0..n)
816 .map(|i| {
817 let mut allowed_cuts = [0.0_f64; 2];
820 for (delta, cut) in allowed_cuts.iter_mut().enumerate() {
821 let (round_cut_ratio, cut_ratio) = cut_adjusts[(i + n - 1 + delta) % n];
822 *cut = corners[i].expected_round_cut * round_cut_ratio
823 + (corners[i].expected_cut() - corners[i].expected_round_cut) * cut_ratio;
824 }
825 corners[i].cubics(allowed_cuts[0], allowed_cuts[1])
826 })
827 .collect();
828
829 let mut features = Vec::with_capacity(2 * n);
831 for i in 0..n {
832 let next = (i + 1) % n;
833 let cvx = convex(vertices[(i + n - 1) % n], vertices[i], vertices[next]);
834 let edge = Cubic::straight_line(
835 corner_cubics[i]
836 .last()
837 .expect("a corner always yields at least one cubic")
838 .anchor1(),
839 corner_cubics[next]
840 .first()
841 .expect("a corner always yields at least one cubic")
842 .anchor0(),
843 );
844 features.push(Feature::unchecked(
845 corner_cubics[i].clone(),
846 if cvx {
847 FeatureKind::ConvexCorner
848 } else {
849 FeatureKind::ConcaveCorner
850 },
851 ));
852 features.push(Feature::unchecked(vec![edge], FeatureKind::Edge));
853 }
854
855 let center = center.unwrap_or_else(|| calculate_center(vertices));
856 RoundedPolygon::from_features(features, Some(center))
857 }
858
859 #[must_use]
867 pub fn from_num_vertices(
868 num_vertices: usize,
869 radius: f64,
870 center: Point,
871 rounding: CornerRounding,
872 per_vertex_rounding: Option<&[CornerRounding]>,
873 ) -> RoundedPolygon {
874 assert!(
875 num_vertices >= 3,
876 "a polygon needs at least 3 vertices, got {num_vertices}"
877 );
878 let vertices = vertices_from_num_vertices(num_vertices, radius, center);
879 RoundedPolygon::from_vertices(&vertices, rounding, per_vertex_rounding, Some(center))
880 }
881
882 #[must_use]
889 pub fn circle(num_vertices: usize, radius: f64, center: Point) -> RoundedPolygon {
890 assert!(
891 num_vertices >= 3,
892 "a circle needs at least 3 vertices, got {num_vertices}"
893 );
894 let theta = PI / num_vertices as f64;
898 let polygon_radius = radius / theta.cos();
899 RoundedPolygon::from_num_vertices(
900 num_vertices,
901 polygon_radius,
902 center,
903 CornerRounding::radius(radius),
904 None,
905 )
906 }
907
908 #[must_use]
919 pub fn rectangle(
920 width: f64,
921 height: f64,
922 rounding: CornerRounding,
923 per_vertex_rounding: Option<&[CornerRounding]>,
924 center: Point,
925 ) -> RoundedPolygon {
926 let (left, right) = (center.x - width / 2.0, center.x + width / 2.0);
927 let (top, bottom) = (center.y - height / 2.0, center.y + height / 2.0);
928 let vertices = [
929 Point::new(right, bottom),
930 Point::new(left, bottom),
931 Point::new(left, top),
932 Point::new(right, top),
933 ];
934 RoundedPolygon::from_vertices(&vertices, rounding, per_vertex_rounding, Some(center))
935 }
936
937 #[must_use]
950 pub fn star(params: StarParams<'_>) -> RoundedPolygon {
951 let StarParams {
952 num_vertices_per_radius,
953 radius,
954 inner_radius,
955 rounding,
956 inner_rounding,
957 per_vertex_rounding,
958 center,
959 } = params;
960 assert!(
961 radius > 0.0 && inner_radius > 0.0,
962 "star radii must both be greater than 0, got {radius} and {inner_radius}"
963 );
964 assert!(
965 inner_radius < radius,
966 "a star's inner radius ({inner_radius}) must be less than its outer one ({radius})"
967 );
968
969 let alternating = alternating_rounding(
970 num_vertices_per_radius,
971 rounding,
972 inner_rounding,
973 per_vertex_rounding,
974 );
975 let vertices =
976 star_vertices_from_num_vertices(num_vertices_per_radius, radius, inner_radius, center);
977 RoundedPolygon::from_vertices(
978 &vertices,
979 rounding,
980 alternating.as_deref().or(per_vertex_rounding),
981 Some(center),
982 )
983 }
984
985 #[must_use]
995 pub fn pill(width: f64, height: f64, smoothing: f64, center: Point) -> RoundedPolygon {
996 assert!(
997 width > 0.0 && height > 0.0,
998 "a pill needs a positive width and height, got {width} and {height}"
999 );
1000 let (w_half, h_half) = (width / 2.0, height / 2.0);
1001 let vertices = [
1002 Point::new(center.x + w_half, center.y + h_half),
1003 Point::new(center.x - w_half, center.y + h_half),
1004 Point::new(center.x - w_half, center.y - h_half),
1005 Point::new(center.x + w_half, center.y - h_half),
1006 ];
1007 RoundedPolygon::from_vertices(
1008 &vertices,
1009 CornerRounding::new(w_half.min(h_half), smoothing),
1010 None,
1011 Some(center),
1012 )
1013 }
1014
1015 #[must_use]
1031 pub fn pill_star(params: PillStarParams<'_>) -> RoundedPolygon {
1032 let PillStarParams {
1033 width,
1034 height,
1035 num_vertices_per_radius,
1036 inner_radius_ratio,
1037 rounding,
1038 inner_rounding,
1039 per_vertex_rounding,
1040 vertex_spacing,
1041 start_location,
1042 center,
1043 } = params;
1044 assert!(
1045 width > 0.0 && height > 0.0,
1046 "a pill star needs a positive width and height, got {width} and {height}"
1047 );
1048 assert!(
1049 inner_radius_ratio > 0.0 && inner_radius_ratio <= 1.0,
1050 "a pill star's inner radius ratio must be in (0, 1], got {inner_radius_ratio}"
1051 );
1052 assert!(
1053 (0.0..=1.0).contains(&vertex_spacing),
1054 "vertex spacing must be in 0..=1, got {vertex_spacing}"
1055 );
1056 assert!(
1057 (0.0..=1.0).contains(&start_location),
1058 "start location must be in 0..=1, got {start_location}"
1059 );
1060
1061 let alternating = alternating_rounding(
1062 num_vertices_per_radius,
1063 rounding,
1064 inner_rounding,
1065 per_vertex_rounding,
1066 );
1067 let vertices = pill_star_vertices_from_num_vertices(
1068 num_vertices_per_radius,
1069 width,
1070 height,
1071 inner_radius_ratio,
1072 vertex_spacing,
1073 start_location,
1074 center,
1075 );
1076 RoundedPolygon::from_vertices(
1077 &vertices,
1078 rounding,
1079 alternating.as_deref().or(per_vertex_rounding),
1080 Some(center),
1081 )
1082 }
1083
1084 #[must_use]
1086 pub fn features(&self) -> &[Feature] {
1087 &self.features
1088 }
1089
1090 #[must_use]
1093 pub fn cubics(&self) -> &[Cubic] {
1094 &self.cubics
1095 }
1096
1097 #[must_use]
1099 pub const fn center(&self) -> Point {
1100 self.center
1101 }
1102
1103 #[must_use]
1108 pub fn transformed_with(&self, f: impl Fn(Point) -> Point + Copy) -> RoundedPolygon {
1109 RoundedPolygon::from_features(
1110 self.features
1111 .iter()
1112 .map(|feat| feat.transformed_with(f))
1113 .collect(),
1114 Some(f(self.center)),
1115 )
1116 }
1117
1118 #[must_use]
1120 pub fn transformed(&self, transform: Affine) -> RoundedPolygon {
1121 self.transformed_with(|p| transform * p)
1122 }
1123
1124 #[must_use]
1127 pub fn normalized(&self) -> RoundedPolygon {
1128 let bounds = self.bounds();
1129 let (width, height) = (bounds.width(), bounds.height());
1130 let side = width.max(height);
1131
1132 let offset_x = (side - width) / 2.0 - bounds.x0;
1134 let offset_y = (side - height) / 2.0 - bounds.y0;
1135
1136 self.transformed_with(|p| Point::new((p.x + offset_x) / side, (p.y + offset_y) / side))
1137 }
1138
1139 #[must_use]
1142 pub fn bounds(&self) -> Rect {
1143 self.fold_bounds(Cubic::approximate_bounds)
1144 }
1145
1146 #[must_use]
1149 pub fn exact_bounds(&self) -> Rect {
1150 self.fold_bounds(Cubic::exact_bounds)
1151 }
1152
1153 #[must_use]
1160 pub fn max_bounds(&self) -> Rect {
1161 let mut max_dist_squared = 0.0_f64;
1162 for cubic in &self.cubics {
1163 let anchor = (cubic.anchor0() - self.center).hypot2();
1164 let middle = (cubic.point_on_curve(0.5) - self.center).hypot2();
1165 max_dist_squared = max_dist_squared.max(anchor.max(middle));
1166 }
1167 let distance = max_dist_squared.sqrt();
1168 Rect::new(
1169 self.center.x - distance,
1170 self.center.y - distance,
1171 self.center.x + distance,
1172 self.center.y + distance,
1173 )
1174 }
1175
1176 fn fold_bounds(&self, per_cubic: impl Fn(&Cubic) -> Rect) -> Rect {
1177 let mut min = Point::new(f64::INFINITY, f64::INFINITY);
1178 let mut max = Point::new(f64::NEG_INFINITY, f64::NEG_INFINITY);
1179 for cubic in &self.cubics {
1180 let b = per_cubic(cubic);
1181 min.x = min.x.min(b.x0);
1182 min.y = min.y.min(b.y0);
1183 max.x = max.x.max(b.x1);
1184 max.y = max.y.max(b.y1);
1185 }
1186 Rect::new(min.x, min.y, max.x, max.y)
1187 }
1188
1189 #[must_use]
1196 pub fn to_path(&self) -> BezPath {
1197 let mut path = BezPath::new();
1198 for (i, cubic) in self.cubics.iter().enumerate() {
1199 if i == 0 {
1200 path.move_to(cubic.anchor0());
1201 }
1202 path.curve_to(cubic.control0(), cubic.control1(), cubic.anchor1());
1203 }
1204 if !self.cubics.is_empty() {
1205 path.close_path();
1206 }
1207 path
1208 }
1209}
1210
1211fn init_cubics(features: &[Feature], center: Point) -> Vec<Cubic> {
1221 let mut cubics: Vec<Cubic> = Vec::new();
1222 let mut first_cubic: Option<Cubic> = None;
1223 let mut last_cubic: Option<Cubic> = None;
1224 let mut split_start: Option<[Cubic; 2]> = None;
1225 let mut split_end: Option<[Cubic; 2]> = None;
1226
1227 if let Some(first) = features.first()
1228 && first.cubics().len() == 3
1229 {
1230 let (start, end) = first.cubics()[1].split(0.5);
1231 split_start = Some([first.cubics()[0], start]);
1232 split_end = Some([end, first.cubics()[2]]);
1233 }
1234
1235 for i in 0..=features.len() {
1238 let feature_cubics: &[Cubic] = if i == features.len() {
1239 match split_start.as_ref() {
1240 Some(s) => s.as_slice(),
1241 None => break,
1242 }
1243 } else if i == 0 {
1244 match split_end.as_ref() {
1245 Some(s) => s.as_slice(),
1246 None => features[0].cubics(),
1247 }
1248 } else {
1249 features[i].cubics()
1250 };
1251
1252 for cubic in feature_cubics {
1253 if cubic.zero_length() {
1254 if let Some(last) = last_cubic.as_mut() {
1255 last.points[6] = cubic.points[6];
1258 last.points[7] = cubic.points[7];
1259 }
1260 } else {
1261 if let Some(last) = last_cubic {
1262 cubics.push(last);
1263 }
1264 last_cubic = Some(*cubic);
1265 first_cubic.get_or_insert(*cubic);
1266 }
1267 }
1268 }
1269
1270 match (last_cubic, first_cubic) {
1271 (Some(last), Some(first)) => cubics.push(Cubic::new(
1272 last.anchor0(),
1273 last.control0(),
1274 last.control1(),
1275 first.anchor0(),
1276 )),
1277 _ => cubics.push(Cubic::empty(center)),
1279 }
1280
1281 cubics
1282}
1283
1284fn is_contiguous(cubics: &[Cubic]) -> bool {
1286 let Some(mut prev) = cubics.last().copied() else {
1287 return true;
1288 };
1289 for cubic in cubics {
1290 let (start, end) = (cubic.anchor0(), prev.anchor1());
1291 if (start.x - end.x).abs() > DISTANCE_EPSILON || (start.y - end.y).abs() > DISTANCE_EPSILON
1292 {
1293 return false;
1294 }
1295 prev = *cubic;
1296 }
1297 true
1298}
1299
1300fn alternating_rounding(
1303 num_vertices_per_radius: usize,
1304 rounding: CornerRounding,
1305 inner_rounding: Option<CornerRounding>,
1306 per_vertex_rounding: Option<&[CornerRounding]>,
1307) -> Option<Vec<CornerRounding>> {
1308 match (per_vertex_rounding, inner_rounding) {
1309 (None, Some(inner)) => Some(
1310 (0..num_vertices_per_radius)
1311 .flat_map(|_| [rounding, inner])
1312 .collect(),
1313 ),
1314 _ => None,
1315 }
1316}
1317
1318fn vertices_from_num_vertices(num_vertices: usize, radius: f64, center: Point) -> Vec<Point> {
1321 (0..num_vertices)
1322 .map(|i| center + radial_to_cartesian(radius, PI / num_vertices as f64 * 2.0 * i as f64))
1323 .collect()
1324}
1325
1326fn star_vertices_from_num_vertices(
1328 num_vertices_per_radius: usize,
1329 radius: f64,
1330 inner_radius: f64,
1331 center: Point,
1332) -> Vec<Point> {
1333 let n = num_vertices_per_radius as f64;
1334 let mut result = Vec::with_capacity(num_vertices_per_radius * 2);
1335 for i in 0..num_vertices_per_radius {
1336 result.push(center + radial_to_cartesian(radius, PI / n * 2.0 * i as f64));
1337 result.push(center + radial_to_cartesian(inner_radius, PI / n * (2.0 * i as f64 + 1.0)));
1338 }
1339 result
1340}
1341
1342fn pill_star_vertices_from_num_vertices(
1352 num_vertices_per_radius: usize,
1353 width: f64,
1354 height: f64,
1355 inner_radius: f64,
1356 vertex_spacing: f64,
1357 start_location: f64,
1358 center: Point,
1359) -> Vec<Point> {
1360 let endcap_radius = width.min(height);
1361 let v_seg_len = (height - width).max(0.0);
1362 let h_seg_len = (width - height).max(0.0);
1363 let v_seg_half = v_seg_len / 2.0;
1364 let h_seg_half = h_seg_len / 2.0;
1365
1366 let circle_perimeter = 2.0 * PI * endcap_radius * lerp(inner_radius, 1.0, vertex_spacing);
1367 let perimeter = 2.0 * h_seg_len + 2.0 * v_seg_len + circle_perimeter;
1368
1369 let mut sections = [0.0_f64; 11];
1371 sections[1] = v_seg_len / 2.0;
1372 sections[2] = sections[1] + circle_perimeter / 4.0;
1373 sections[3] = sections[2] + h_seg_len;
1374 sections[4] = sections[3] + circle_perimeter / 4.0;
1375 sections[5] = sections[4] + v_seg_len;
1376 sections[6] = sections[5] + circle_perimeter / 4.0;
1377 sections[7] = sections[6] + h_seg_len;
1378 sections[8] = sections[7] + circle_perimeter / 4.0;
1379 sections[9] = sections[8] + v_seg_len / 2.0;
1380 sections[10] = perimeter;
1381
1382 let t_per_vertex = perimeter / (2.0 * num_vertices_per_radius as f64);
1383 let mut inner = false;
1384 let mut curr_sec_index = 0_usize;
1385 let mut sec_start = 0.0;
1386 let mut sec_end = sections[1];
1387 let mut t = start_location * perimeter;
1390
1391 let rect_br = Vec2::new(h_seg_half, v_seg_half);
1392 let rect_bl = Vec2::new(-h_seg_half, v_seg_half);
1393 let rect_tl = Vec2::new(-h_seg_half, -v_seg_half);
1394 let rect_tr = Vec2::new(h_seg_half, -v_seg_half);
1395
1396 let mut result = Vec::with_capacity(num_vertices_per_radius * 2);
1397 for _ in 0..(num_vertices_per_radius * 2) {
1398 let bounded_t = t.rem_euclid(perimeter);
1401 if bounded_t < sec_start {
1402 curr_sec_index = 0;
1403 }
1404 while bounded_t >= sections[(curr_sec_index + 1) % sections.len()] {
1405 curr_sec_index = (curr_sec_index + 1) % sections.len();
1406 sec_start = sections[curr_sec_index];
1407 sec_end = sections[(curr_sec_index + 1) % sections.len()];
1408 }
1409
1410 let t_in_section = bounded_t - sec_start;
1411 let t_proportion = t_in_section / (sec_end - sec_start);
1412
1413 let curr_radius = if inner {
1416 endcap_radius * inner_radius
1417 } else {
1418 endcap_radius
1419 };
1420 let vertex = match curr_sec_index {
1421 0 => Vec2::new(curr_radius, t_proportion * v_seg_half),
1422 1 => radial_to_cartesian(curr_radius, t_proportion * PI / 2.0) + rect_br,
1423 2 => Vec2::new(h_seg_half - t_proportion * h_seg_len, curr_radius),
1424 3 => radial_to_cartesian(curr_radius, PI / 2.0 + (t_proportion * PI / 2.0)) + rect_bl,
1425 4 => Vec2::new(-curr_radius, v_seg_half - t_proportion * v_seg_len),
1426 5 => radial_to_cartesian(curr_radius, PI + (t_proportion * PI / 2.0)) + rect_tl,
1427 6 => Vec2::new(-h_seg_half + t_proportion * h_seg_len, -curr_radius),
1428 7 => radial_to_cartesian(curr_radius, PI * 1.5 + (t_proportion * PI / 2.0)) + rect_tr,
1429 _ => Vec2::new(curr_radius, -v_seg_half + t_proportion * v_seg_half),
1430 };
1431
1432 result.push(center + vertex);
1433 t += t_per_vertex;
1434 inner = !inner;
1435 }
1436
1437 result
1438}
1439
1440struct RoundedCorner {
1449 p0: Point,
1451 p1: Point,
1453 p2: Point,
1455 d1: Vec2,
1457 d2: Vec2,
1459 corner_radius: f64,
1460 smoothing: f64,
1461 expected_round_cut: f64,
1463}
1464
1465impl RoundedCorner {
1466 fn new(p0: Point, p1: Point, p2: Point, rounding: CornerRounding) -> RoundedCorner {
1467 let v01 = p0 - p1;
1468 let v21 = p2 - p1;
1469 let d01 = v01.hypot();
1470 let d21 = v21.hypot();
1471
1472 if d01 <= 0.0 || d21 <= 0.0 {
1473 return RoundedCorner {
1475 p0,
1476 p1,
1477 p2,
1478 d1: Vec2::ZERO,
1479 d2: Vec2::ZERO,
1480 corner_radius: 0.0,
1481 smoothing: 0.0,
1482 expected_round_cut: 0.0,
1483 };
1484 }
1485
1486 let d1 = v01 / d01;
1487 let d2 = v21 / d21;
1488 let cos_angle = d1.dot(d2);
1491 let sin_angle = (1.0 - cos_angle * cos_angle).sqrt();
1492 let expected_round_cut = if sin_angle > COLLINEAR_SIN_EPSILON {
1495 rounding.radius * (cos_angle + 1.0) / sin_angle
1496 } else {
1497 0.0
1498 };
1499
1500 RoundedCorner {
1501 p0,
1502 p1,
1503 p2,
1504 d1,
1505 d2,
1506 corner_radius: rounding.radius,
1507 smoothing: rounding.smoothing,
1508 expected_round_cut,
1509 }
1510 }
1511
1512 fn expected_cut(&self) -> f64 {
1515 (1.0 + self.smoothing) * self.expected_round_cut
1516 }
1517
1518 fn cubics(&self, allowed_cut0: f64, allowed_cut1: f64) -> Vec<Cubic> {
1521 let allowed_cut = allowed_cut0.min(allowed_cut1);
1524
1525 if self.expected_round_cut < DISTANCE_EPSILON
1527 || allowed_cut < DISTANCE_EPSILON
1528 || self.corner_radius < DISTANCE_EPSILON
1529 {
1530 return vec![Cubic::straight_line(self.p1, self.p1)];
1531 }
1532
1533 let actual_round_cut = allowed_cut.min(self.expected_round_cut);
1534 let actual_smoothing0 = self.actual_smoothing(allowed_cut0);
1536 let actual_smoothing1 = self.actual_smoothing(allowed_cut1);
1537 let actual_r = self.corner_radius * actual_round_cut / self.expected_round_cut;
1538 let center_distance = (actual_r * actual_r + actual_round_cut * actual_round_cut).sqrt();
1539 let center = self.p1 + direction((self.d1 + self.d2) / 2.0) * center_distance;
1540 let circle_intersection0 = self.p1 + self.d1 * actual_round_cut;
1541 let circle_intersection2 = self.p1 + self.d2 * actual_round_cut;
1542
1543 let flanking0 = self.flanking_curve(
1544 actual_round_cut,
1545 actual_smoothing0,
1546 self.p0,
1547 circle_intersection0,
1548 circle_intersection2,
1549 center,
1550 actual_r,
1551 );
1552 let flanking2 = self
1553 .flanking_curve(
1554 actual_round_cut,
1555 actual_smoothing1,
1556 self.p2,
1557 circle_intersection2,
1558 circle_intersection0,
1559 center,
1560 actual_r,
1561 )
1562 .reversed();
1563
1564 vec![
1565 flanking0,
1566 Cubic::circular_arc(center, flanking0.anchor1(), flanking2.anchor0()),
1567 flanking2,
1568 ]
1569 }
1570
1571 fn actual_smoothing(&self, allowed_cut: f64) -> f64 {
1575 let expected_cut = self.expected_cut();
1576 if allowed_cut > expected_cut {
1577 self.smoothing
1578 } else if allowed_cut > self.expected_round_cut {
1579 self.smoothing * (allowed_cut - self.expected_round_cut)
1580 / (expected_cut - self.expected_round_cut)
1581 } else {
1582 0.0
1583 }
1584 }
1585
1586 #[expect(
1595 clippy::too_many_arguments,
1596 reason = "a faithful port of upstream's _computeFlankingCurve; every \
1597 argument is one of its own, minus the two carried on self"
1598 )]
1599 fn flanking_curve(
1600 &self,
1601 actual_round_cut: f64,
1602 actual_smoothing: f64,
1603 side_start: Point,
1604 circle_intersection: Point,
1605 other_circle_intersection: Point,
1606 circle_center: Point,
1607 actual_r: f64,
1608 ) -> Cubic {
1609 let side_direction = direction(side_start - self.p1);
1610 let curve_start = self.p1 + side_direction * actual_round_cut * (1.0 + actual_smoothing);
1611
1612 let midpoint = lerp_point(circle_intersection, other_circle_intersection, 0.5);
1616 let p = lerp_point(circle_intersection, midpoint, actual_smoothing);
1617
1618 let curve_end = circle_center + direction(p - circle_center) * actual_r;
1620
1621 let circle_tangent = rotate90(curve_end - circle_center);
1624 let anchor_end = line_intersection(side_start, side_direction, curve_end, circle_tangent)
1625 .unwrap_or(circle_intersection);
1626
1627 let anchor_start = ((curve_start.to_vec2() + anchor_end.to_vec2() * 2.0) / 3.0).to_point();
1630
1631 Cubic::new(curve_start, anchor_start, anchor_end, curve_end)
1632 }
1633}
1634
1635fn line_intersection(p0: Point, d0: Vec2, p1: Point, d1: Vec2) -> Option<Point> {
1638 let rotated_d1 = rotate90(d1);
1639 let den = d0.dot(rotated_d1);
1640
1641 if den.abs() < DISTANCE_EPSILON {
1642 return None;
1643 }
1644
1645 let num = (p1 - p0).dot(rotated_d1);
1646
1647 if den.abs() < DISTANCE_EPSILON * num.abs() {
1650 return None;
1651 }
1652
1653 Some(p0 + d0 * (num / den))
1654}
1655
1656#[cfg(test)]
1657mod tests {
1658 use super::*;
1659 use kurbo::{CubicBez, ParamCurveNearest, PathEl};
1660
1661 const EPSILON: f64 = 1e-4;
1664
1665 const GOLDEN_EPSILON: f64 = 1e-12;
1670
1671 fn p(x: f64, y: f64) -> Point {
1672 Point::new(x, y)
1673 }
1674
1675 fn assert_close(expected: f64, actual: f64, what: &str) {
1676 assert!(
1677 (expected - actual).abs() < EPSILON,
1678 "{what}: expected {expected}, got {actual}"
1679 );
1680 }
1681
1682 fn assert_matches_golden(polygon: &RoundedPolygon, golden: &[[f64; 8]], name: &str) {
1685 assert_eq!(
1686 polygon.cubics().len(),
1687 golden.len(),
1688 "{name}: cubic count drifted from upstream"
1689 );
1690 for (i, (cubic, want)) in polygon.cubics().iter().zip(golden).enumerate() {
1691 for (j, (got, want)) in cubic.points().iter().zip(want).enumerate() {
1692 assert!(
1693 (got - want).abs() < GOLDEN_EPSILON,
1694 "{name}: cubic {i} coordinate {j}: expected {want}, got {got}"
1695 );
1696 }
1697 }
1698 }
1699
1700 fn assert_bounds(bounds: Rect, want: [f64; 4], name: &str) {
1701 let got = [bounds.x0, bounds.y0, bounds.x1, bounds.y1];
1702 for (i, (got, want)) in got.iter().zip(&want).enumerate() {
1703 assert!(
1704 (got - want).abs() < GOLDEN_EPSILON,
1705 "{name}: bound {i}: expected {want}, got {got}"
1706 );
1707 }
1708 }
1709
1710 fn kinds(polygon: &RoundedPolygon) -> Vec<FeatureKind> {
1711 polygon.features().iter().map(Feature::kind).collect()
1712 }
1713
1714 #[test]
1719 fn straight_line_puts_its_controls_at_the_thirds() {
1720 let line = Cubic::straight_line(p(1.0, 0.0), p(0.0, 1.0));
1721 assert_eq!(line.anchor0(), p(1.0, 0.0));
1722 assert_eq!(line.anchor1(), p(0.0, 1.0));
1723 assert_close(line.control0().x, 2.0 / 3.0, "control0.x");
1724 assert_close(line.control0().y, 1.0 / 3.0, "control0.y");
1725 assert_close(line.control1().x, 1.0 / 3.0, "control1.x");
1726 assert_close(line.control1().y, 2.0 / 3.0, "control1.y");
1727 }
1728
1729 #[test]
1730 fn circular_arc_keeps_its_endpoints_and_degrades_to_a_line() {
1731 let arc = Cubic::circular_arc(Point::ZERO, p(1.0, 0.0), p(0.0, 1.0));
1732 assert_eq!(arc.anchor0(), p(1.0, 0.0));
1733 assert_eq!(arc.anchor1(), p(0.0, 1.0));
1734 for i in 0..=20 {
1736 let on_curve = arc.point_on_curve(f64::from(i) / 20.0);
1737 assert!((on_curve.to_vec2().hypot() - 1.0).abs() < 3e-4);
1738 }
1739
1740 let degenerate = Cubic::circular_arc(Point::ZERO, p(1.0, 0.0), p(1.0, 0.001));
1742 assert_eq!(degenerate, Cubic::straight_line(p(1.0, 0.0), p(1.0, 0.001)));
1743 }
1744
1745 #[test]
1746 fn split_covers_the_same_curve() {
1747 let cubic = Cubic::new(p(1.0, 0.0), p(1.0, 0.5), p(0.5, 1.0), p(0.0, 1.0));
1748 let (a, b) = cubic.split(0.5);
1749 assert_eq!(a.anchor0(), cubic.anchor0());
1750 assert_eq!(b.anchor1(), cubic.anchor1());
1751 assert_eq!(a.anchor1(), b.anchor0());
1752 for i in 0..=10 {
1753 let t = f64::from(i) / 10.0;
1754 let whole = cubic.point_on_curve(t / 2.0);
1755 let half = a.point_on_curve(t);
1756 assert!((whole - half).hypot() < 1e-12, "t = {t}");
1757 }
1758 }
1759
1760 #[test]
1761 fn reversed_swaps_the_ends() {
1762 let cubic = Cubic::new(p(1.0, 0.0), p(1.0, 0.5), p(0.5, 1.0), p(0.0, 1.0));
1763 let back = cubic.reversed();
1764 assert_eq!(back.anchor0(), cubic.anchor1());
1765 assert_eq!(back.control0(), cubic.control1());
1766 assert_eq!(back.control1(), cubic.control0());
1767 assert_eq!(back.anchor1(), cubic.anchor0());
1768 }
1769
1770 #[test]
1771 fn empty_cubic_has_zero_length() {
1772 assert!(Cubic::empty(p(10.0, 10.0)).zero_length());
1773 assert!(!Cubic::straight_line(p(0.0, 0.0), p(1.0, 0.0)).zero_length());
1774 }
1775
1776 #[test]
1777 fn exact_bounds_are_tighter_than_approximate_ones() {
1778 let cubic = Cubic::new(p(0.0, 0.0), p(0.0, 1.0), p(1.0, 1.0), p(1.0, 0.0));
1779 let exact = cubic.exact_bounds();
1780 let approximate = cubic.approximate_bounds();
1781 assert_bounds(approximate, [0.0, 0.0, 1.0, 1.0], "approximate");
1782 assert_bounds(exact, [0.0, 0.0, 1.0, 0.75], "exact");
1784 }
1785
1786 #[test]
1791 fn feature_kinds_report_themselves() {
1792 let edge = Feature::edge(Cubic::straight_line(p(0.0, 0.0), p(1.0, 0.0)));
1793 assert!(edge.is_edge() && edge.is_ignorable() && !edge.is_corner());
1794
1795 let convex = Feature::convex_corner(vec![Cubic::empty(p(1.0, 0.0))]);
1796 assert!(convex.is_corner() && convex.is_convex_corner() && !convex.is_ignorable());
1797
1798 let concave = Feature::concave_corner(vec![Cubic::empty(p(1.0, 0.0))]);
1799 assert!(concave.is_concave_corner() && !concave.is_convex_corner());
1800
1801 assert!(Feature::ignorable(vec![Cubic::empty(p(0.0, 0.0))]).is_ignorable());
1803 }
1804
1805 #[test]
1806 fn reversing_a_corner_flips_its_indentation() {
1807 let corner = Feature::convex_corner(vec![
1808 Cubic::straight_line(p(0.0, 0.0), p(1.0, 0.0)),
1809 Cubic::straight_line(p(1.0, 0.0), p(1.0, 1.0)),
1810 ]);
1811 let back = corner.reversed();
1812 assert_eq!(back.kind(), FeatureKind::ConcaveCorner);
1813 assert_eq!(back.cubics()[0].anchor0(), p(1.0, 1.0));
1814 assert_eq!(back.cubics()[1].anchor1(), p(0.0, 0.0));
1815 assert_eq!(corner.reversed().reversed(), corner);
1816 }
1817
1818 #[test]
1819 #[should_panic(expected = "must be continuous")]
1820 fn a_discontinuous_feature_is_rejected() {
1821 let _ = Feature::convex_corner(vec![
1822 Cubic::straight_line(p(0.0, 0.0), p(1.0, 0.0)),
1823 Cubic::straight_line(p(10.0, 10.0), p(20.0, 20.0)),
1824 ]);
1825 }
1826
1827 #[test]
1828 #[should_panic(expected = "at least one cubic")]
1829 fn an_empty_feature_is_rejected() {
1830 let _ = Feature::convex_corner(Vec::new());
1831 }
1832
1833 #[test]
1838 fn an_unrounded_polygon_reproduces_its_vertex_polyline() {
1839 let vertices = [
1840 p(1.0, 0.0),
1841 p(0.0, 1.0),
1842 p(-1.0, 0.0),
1843 p(0.0, -1.0),
1844 p(0.5, -0.5),
1845 ];
1846 let polygon =
1847 RoundedPolygon::from_vertices(&vertices, CornerRounding::UNROUNDED, None, None);
1848
1849 assert_eq!(polygon.cubics().len(), vertices.len());
1852 for (i, cubic) in polygon.cubics().iter().enumerate() {
1853 let from = vertices[i];
1854 let to = vertices[(i + 1) % vertices.len()];
1855 assert_eq!(cubic.anchor0(), from, "cubic {i} start");
1856 assert_eq!(cubic.anchor1(), to, "cubic {i} end");
1857 assert_eq!(cubic.control0(), lerp_point(from, to, 1.0 / 3.0));
1858 assert_eq!(cubic.control1(), lerp_point(from, to, 2.0 / 3.0));
1859 }
1860 }
1861
1862 #[test]
1863 fn to_path_is_closed_and_lands_exactly_on_its_start() {
1864 for polygon in [
1865 RoundedPolygon::rectangle(2.0, 2.0, CornerRounding::UNROUNDED, None, Point::ZERO),
1866 RoundedPolygon::rectangle(2.0, 2.0, CornerRounding::new(0.5, 0.2), None, Point::ZERO),
1867 RoundedPolygon::circle(8, 1.0, Point::ZERO),
1868 RoundedPolygon::star(StarParams::default()),
1869 ] {
1870 let path = polygon.to_path();
1871 let elements: Vec<PathEl> = path.elements().to_vec();
1872
1873 let Some(PathEl::MoveTo(start)) = elements.first().copied() else {
1874 panic!("a path must open with a MoveTo, got {:?}", elements.first());
1875 };
1876 assert_eq!(start, polygon.cubics()[0].anchor0());
1877 assert!(matches!(elements.last(), Some(PathEl::ClosePath)));
1878
1879 let curves: Vec<PathEl> = elements
1880 .iter()
1881 .copied()
1882 .filter(|el| matches!(el, PathEl::CurveTo(..)))
1883 .collect();
1884 assert_eq!(curves.len(), polygon.cubics().len());
1885
1886 let Some(PathEl::CurveTo(_, _, end)) = curves.last().copied() else {
1888 unreachable!("filtered to CurveTo above");
1889 };
1890 assert_eq!(end, start, "the outline must close exactly on its start");
1891 }
1892 }
1893
1894 #[test]
1895 fn the_circle_constructor_is_round() {
1896 let radius = 3.0;
1897 let center = p(1.0, -2.0);
1898 for (num_vertices, tolerance) in [(8, 5e-6), (12, 1e-6), (20, 1e-6), (32, 1e-6)] {
1905 let circle = RoundedPolygon::circle(num_vertices, radius, center);
1906 let mut worst = 0.0_f64;
1907 for cubic in circle.cubics() {
1908 for i in 0..=16 {
1909 let on_curve = cubic.point_on_curve(f64::from(i) / 16.0);
1910 worst = worst.max(((on_curve - center).hypot() - radius).abs() / radius);
1911 }
1912 }
1913 assert!(
1914 worst < tolerance,
1915 "{num_vertices}-gon circle deviates by {worst} of its radius (> {tolerance})"
1916 );
1917 }
1918 }
1919
1920 #[test]
1921 fn rectangle_bounds_are_exact() {
1922 let rectangle =
1923 RoundedPolygon::rectangle(4.0, 2.0, CornerRounding::UNROUNDED, None, p(1.0, 3.0));
1924 assert_bounds(rectangle.bounds(), [-1.0, 2.0, 3.0, 4.0], "approximate");
1925 assert_bounds(rectangle.exact_bounds(), [-1.0, 2.0, 3.0, 4.0], "exact");
1926 assert_eq!(rectangle.center(), p(1.0, 3.0));
1927 }
1928
1929 #[test]
1930 fn a_rounded_square_is_symmetric_under_a_quarter_turn() {
1931 let square =
1932 RoundedPolygon::rectangle(2.0, 2.0, CornerRounding::new(0.5, 0.3), None, Point::ZERO);
1933 let turned = square.transformed(Affine::rotate(std::f64::consts::FRAC_PI_2));
1934
1935 let curves: Vec<CubicBez> = square
1941 .cubics()
1942 .iter()
1943 .map(|c| CubicBez::new(c.anchor0(), c.control0(), c.control1(), c.anchor1()))
1944 .collect();
1945 for cubic in turned.cubics() {
1946 for i in 0..8 {
1947 let want = cubic.point_on_curve(f64::from(i) / 8.0);
1948 let nearest = curves
1949 .iter()
1950 .map(|curve| curve.nearest(want, 1e-12).distance_sq)
1951 .fold(f64::INFINITY, f64::min)
1952 .sqrt();
1953 assert!(nearest < 1e-9, "{want:?} is {nearest} off the outline");
1954 }
1955 }
1956 }
1957
1958 #[test]
1959 fn the_center_defaults_to_the_vertex_average() {
1960 let polygon = RoundedPolygon::from_vertices(
1961 &[p(0.0, 0.0), p(1.0, 0.0), p(0.0, 1.0), p(1.0, 1.0)],
1962 CornerRounding::UNROUNDED,
1963 None,
1964 None,
1965 );
1966 assert_close(polygon.center().x, 0.5, "center.x");
1967 assert_close(polygon.center().y, 0.5, "center.y");
1968 }
1969
1970 #[test]
1971 fn normalized_fits_the_unit_box() {
1972 for polygon in [
1973 RoundedPolygon::star(StarParams::default()),
1974 RoundedPolygon::pill(2.0, 1.0, 0.0, Point::ZERO),
1975 RoundedPolygon::circle(10, 4.0, p(-7.0, 3.0)),
1976 ] {
1977 let bounds = polygon.normalized().bounds();
1978 assert!(bounds.x0 >= -EPSILON && bounds.y0 >= -EPSILON, "{bounds:?}");
1979 assert!(
1980 bounds.x1 <= 1.0 + EPSILON && bounds.y1 <= 1.0 + EPSILON,
1981 "{bounds:?}"
1982 );
1983 assert_close(bounds.width().max(bounds.height()), 1.0, "longer axis");
1985 }
1986 }
1987
1988 #[test]
1989 fn rebuilding_from_features_reproduces_the_polygon() {
1990 let mut shapes = vec![
1991 RoundedPolygon::rectangle(2.0, 2.0, CornerRounding::UNROUNDED, None, Point::ZERO),
1992 RoundedPolygon::rectangle(2.0, 2.0, CornerRounding::new(0.5, 0.2), None, Point::ZERO),
1993 RoundedPolygon::pill(2.0, 1.0, 0.0, Point::ZERO),
1994 RoundedPolygon::pill_star(PillStarParams {
1995 rounding: CornerRounding::new(0.5, 0.2),
1996 ..PillStarParams::default()
1997 }),
1998 ];
1999 for n in 3..=20 {
2000 shapes.push(RoundedPolygon::circle(n, 1.0, Point::ZERO));
2001 shapes.push(RoundedPolygon::star(StarParams {
2002 num_vertices_per_radius: n,
2003 ..StarParams::default()
2004 }));
2005 shapes.push(RoundedPolygon::star(StarParams {
2006 num_vertices_per_radius: n,
2007 rounding: CornerRounding::new(0.5, 0.2),
2008 ..StarParams::default()
2009 }));
2010 }
2011
2012 for base in shapes {
2013 let rebuilt = RoundedPolygon::from_features(base.features().to_vec(), None);
2014 assert_eq!(kinds(&rebuilt), kinds(&base));
2015 assert_eq!(rebuilt.cubics().len(), base.cubics().len());
2016 for (got, want) in rebuilt.cubics().iter().zip(base.cubics()) {
2017 for (got, want) in got.points().iter().zip(want.points()) {
2018 assert_close(*want, *got, "rebuilt cubic");
2019 }
2020 }
2021 }
2022 }
2023
2024 #[test]
2025 fn a_full_size_shape_matches_its_canonical_one() {
2026 let radius = 400.0;
2029 let inner_radius_factor = 0.35;
2030 let rounding_factor = 0.32;
2031
2032 let full_size = RoundedPolygon::star(StarParams {
2033 num_vertices_per_radius: 4,
2034 radius,
2035 inner_radius: radius * inner_radius_factor,
2036 rounding: CornerRounding::radius(radius * rounding_factor),
2037 inner_rounding: Some(CornerRounding::radius(radius * rounding_factor)),
2038 per_vertex_rounding: None,
2039 center: p(radius, radius),
2040 })
2041 .transformed_with(|point| {
2042 Point::new((point.x - radius) / radius, (point.y - radius) / radius)
2043 });
2044
2045 let canonical = RoundedPolygon::star(StarParams {
2046 num_vertices_per_radius: 4,
2047 radius: 1.0,
2048 inner_radius: inner_radius_factor,
2049 rounding: CornerRounding::radius(rounding_factor),
2050 inner_rounding: Some(CornerRounding::radius(rounding_factor)),
2051 per_vertex_rounding: None,
2052 center: Point::ZERO,
2053 });
2054
2055 assert_eq!(full_size.cubics().len(), canonical.cubics().len());
2056 for (got, want) in full_size.cubics().iter().zip(canonical.cubics()) {
2057 for (got, want) in got.points().iter().zip(want.points()) {
2058 assert_close(*want, *got, "full-size vs canonical");
2059 }
2060 }
2061 }
2062
2063 #[test]
2068 fn rounding_takes_space_before_smoothing_does() {
2069 let polygon = RoundedPolygon::from_vertices(
2073 &[p(0.0, 0.0), p(1.0, 0.0), p(0.5, 1.0)],
2074 CornerRounding::UNROUNDED,
2075 Some(&[
2076 CornerRounding::radius(1.0),
2077 CornerRounding::new(1.0, 1.0),
2078 CornerRounding::UNROUNDED,
2079 ]),
2080 None,
2081 );
2082
2083 let lower_edge = polygon
2084 .features()
2085 .iter()
2086 .find(|f| f.is_edge())
2087 .expect("a polygon has edges");
2088 assert_eq!(lower_edge.cubics().len(), 1);
2089 let cubic = lower_edge.cubics()[0];
2090 assert_close(0.5, cubic.anchor0().x, "edge start x");
2091 assert_close(0.0, cubic.anchor0().y, "edge start y");
2092 assert_close(0.5, cubic.anchor1().x, "edge end x");
2093 assert_close(0.0, cubic.anchor1().y, "edge end y");
2094 }
2095
2096 fn uneven_smooth_case(
2107 rounding0: CornerRounding,
2108 rounding3: CornerRounding,
2109 expected_v0_sx: f64,
2110 expected_v0_sy: f64,
2111 expected_v3_sy: f64,
2112 ) {
2113 let polygon = RoundedPolygon::from_vertices(
2114 &[p(0.0, 0.0), p(5.0, 0.0), p(5.0, 1.0), p(0.0, 1.0)],
2115 CornerRounding::UNROUNDED,
2116 Some(&[
2117 rounding0,
2118 CornerRounding::UNROUNDED,
2119 CornerRounding::UNROUNDED,
2120 rounding3,
2121 ]),
2122 None,
2123 );
2124
2125 let edges: Vec<&Feature> = polygon.features().iter().filter(|f| f.is_edge()).collect();
2126 let (e01, e30) = (edges[0], edges[3]);
2127 let context = format!("r0 = {rounding0:?}, r3 = {rounding3:?}");
2128
2129 assert_close(expected_v0_sx, e01.cubics()[0].anchor0().x, &context);
2130 assert_close(expected_v0_sy, e30.cubics()[0].anchor1().y, &context);
2131 assert_close(expected_v3_sy, 1.0 - e30.cubics()[0].anchor0().y, &context);
2132 }
2133
2134 #[test]
2135 fn uneven_smoothing_against_a_plain_rounded_neighbour() {
2136 for i in 0..=20 {
2139 let smooth = f64::from(i) / 20.0;
2140 uneven_smooth_case(
2141 CornerRounding::new(0.4, smooth),
2142 CornerRounding::radius(0.5),
2143 0.4 * (1.0 + smooth),
2144 (0.4 * (1.0 + smooth)).min(0.5),
2145 0.5,
2146 );
2147 }
2148 }
2149
2150 #[test]
2151 fn uneven_smoothing_when_both_neighbours_want_smoothing() {
2152 for i in 0..=20 {
2155 let smooth = f64::from(i) / 20.0;
2156 let wanted_v0 = 0.4 * smooth;
2157 let wanted_v3 = 0.2;
2158 let factor = (0.4 / (wanted_v0 + wanted_v3)).min(1.0);
2159 uneven_smooth_case(
2160 CornerRounding::new(0.4, smooth),
2161 CornerRounding::new(0.2, 1.0),
2162 0.4 * (1.0 + smooth),
2163 0.4 + factor * wanted_v0,
2164 0.2 + factor * wanted_v3,
2165 );
2166 }
2167 }
2168
2169 #[test]
2170 fn uneven_smoothing_with_no_room_left_on_the_shared_side() {
2171 for i in 0..=20 {
2174 let smooth = f64::from(i) / 20.0;
2175 uneven_smooth_case(
2176 CornerRounding::new(0.4, smooth),
2177 CornerRounding::radius(0.6),
2178 0.4 * (1.0 + smooth),
2179 0.4,
2180 0.6,
2181 );
2182 }
2183 }
2184
2185 #[rustfmt::skip]
2195 const TRIANGLE_SPACE_USAGE: [[f64; 8]; 5] = [
2196 [0.2371344439404332, 0.14655714625849423, 0.2903596433552479, 0.06043696454733296, 0.38514116108158136, -5.551115123125783e-17, 0.5, -5.551115123125783e-17],
2197 [0.5, -5.551115123125783e-17, 0.7297176778368372, -5.551115123125783e-17, 0.879126070905334, 0.241747858189332, 0.7763932022500211, 0.44721359549995787],
2198 [0.7763932022500211, 0.44721359549995787, 0.6842621348333474, 0.6314757303333053, 0.5921310674166738, 0.8157378651666526, 0.5, 1.0],
2199 [0.5, 1.0, 0.40786893258332635, 0.8157378651666527, 0.31573786516665264, 0.6314757303333053, 0.223606797749979, 0.44721359549995787],
2200 [0.223606797749979, 0.44721359549995787, 0.17224036342232252, 0.3444807268446449, 0.18390924452561846, 0.23267732796965546, 0.2371344439404332, 0.14655714625849423],
2201 ];
2202
2203 #[rustfmt::skip]
2204 const SQUARE_UNROUNDED: [[f64; 8]; 4] = [
2205 [1.0, 1.0, 0.3333333333333334, 1.0, -0.33333333333333326, 1.0, -1.0, 1.0],
2206 [-1.0, 1.0, -1.0, 0.3333333333333334, -1.0, -0.33333333333333326, -1.0, -1.0],
2207 [-1.0, -1.0, -0.3333333333333334, -1.0, 0.33333333333333326, -1.0, 1.0, -1.0],
2208 [1.0, -1.0, 1.0, -0.3333333333333334, 1.0, 0.33333333333333326, 1.0, 1.0],
2209 ];
2210
2211 #[rustfmt::skip]
2212 const SQUARE_ROUNDED_SMOOTHED: [[f64; 8]; 17] = [
2213 [0.8535533905932737, 0.8535533905932736, 0.7754029376043261, 0.9317038435822215, 0.6714276261466594, 0.9840294381024499, 0.5552157630374233, 0.9969418673368095],
2214 [0.5552157630374233, 0.9969418673368095, 0.5276925690687082, 1.0, 0.4851283793791388, 1.0, 0.4, 1.0],
2215 [0.4, 1.0, 0.1333333333333334, 1.0, -0.1333333333333333, 1.0, -0.4, 1.0],
2216 [-0.4, 1.0, -0.4851283793791388, 1.0, -0.5276925690687082, 1.0, -0.5552157630374233, 0.9969418673368095],
2217 [-0.5552157630374233, 0.9969418673368095, -0.7876394892558956, 0.9711170088680903, -0.9711170088680903, 0.7876394892558956, -0.9969418673368095, 0.5552157630374233],
2218 [-0.9969418673368095, 0.5552157630374233, -1.0, 0.5276925690687082, -1.0, 0.4851283793791388, -1.0, 0.4],
2219 [-1.0, 0.4, -1.0, 0.1333333333333334, -1.0, -0.1333333333333333, -1.0, -0.4],
2220 [-1.0, -0.4, -1.0, -0.4851283793791388, -1.0, -0.5276925690687082, -0.9969418673368095, -0.5552157630374233],
2221 [-0.9969418673368095, -0.5552157630374233, -0.9711170088680903, -0.7876394892558956, -0.7876394892558956, -0.9711170088680903, -0.5552157630374233, -0.9969418673368095],
2222 [-0.5552157630374233, -0.9969418673368095, -0.5276925690687082, -1.0, -0.4851283793791388, -1.0, -0.4, -1.0],
2223 [-0.4, -1.0, -0.1333333333333334, -1.0, 0.1333333333333333, -1.0, 0.4, -1.0],
2224 [0.4, -1.0, 0.4851283793791388, -1.0, 0.5276925690687082, -1.0, 0.5552157630374233, -0.9969418673368095],
2225 [0.5552157630374233, -0.9969418673368095, 0.7876394892558956, -0.9711170088680903, 0.9711170088680903, -0.7876394892558956, 0.9969418673368095, -0.5552157630374233],
2226 [0.9969418673368095, -0.5552157630374233, 1.0, -0.5276925690687082, 1.0, -0.4851283793791388, 1.0, -0.4],
2227 [1.0, -0.4, 1.0, -0.1333333333333334, 1.0, 0.1333333333333333, 1.0, 0.4],
2228 [1.0, 0.4, 1.0, 0.4851283793791388, 1.0, 0.5276925690687082, 0.9969418673368095, 0.5552157630374233],
2229 [0.9969418673368095, 0.5552157630374233, 0.9840294381024499, 0.6714276261466594, 0.9317038435822214, 0.7754029376043262, 0.8535533905932737, 0.8535533905932736],
2230 ];
2231
2232 #[rustfmt::skip]
2233 const CIRCLE_8: [[f64; 8]; 9] = [
2234 [1.0, 1.3877787807814457e-17, 1.0, 0.1300846945207343, 0.9746265108370957, 0.2601693890414686, 0.9238795325112868, 0.3826834323650898],
2235 [0.9238795325112868, 0.3826834323650898, 0.8223855758596692, 0.6277115190123321, 0.6277115190123322, 0.822385575859669, 0.38268343236508984, 0.9238795325112871],
2236 [0.38268343236508984, 0.9238795325112871, 0.13765534571784743, 1.0253734891629047, -0.13765534571784727, 1.0253734891629047, -0.3826834323650897, 0.9238795325112868],
2237 [-0.3826834323650897, 0.9238795325112868, -0.6277115190123324, 0.8223855758596691, -0.8223855758596692, 0.6277115190123326, -0.9238795325112871, 0.38268343236508984],
2238 [-0.9238795325112871, 0.38268343236508984, -1.0253734891629047, 0.13765534571784738, -1.0253734891629047, -0.1376553457178471, -0.9238795325112867, -0.3826834323650896],
2239 [-0.9238795325112867, -0.3826834323650896, -0.8223855758596691, -0.6277115190123324, -0.6277115190123326, -0.822385575859669, -0.38268343236508995, -0.9238795325112871],
2240 [-0.38268343236508995, -0.9238795325112871, -0.1376553457178475, -1.0253734891629047, 0.137655345717847, -1.0253734891629047, 0.38268343236508956, -0.9238795325112871],
2241 [0.38268343236508956, -0.9238795325112871, 0.6277115190123322, -0.8223855758596694, 0.822385575859669, -0.6277115190123328, 0.9238795325112867, -0.3826834323650897],
2242 [0.9238795325112867, -0.3826834323650897, 0.9746265108370956, -0.2601693890414685, 1.0, -0.1300846945207343, 1.0, 1.3877787807814457e-17],
2243 ];
2244
2245 #[rustfmt::skip]
2246 const STAR_5: [[f64; 8]; 11] = [
2247 [0.6480743810254861, -4.85722573273506e-17, 0.6480743810254861, 0.0996540589356331, 0.5961576437278834, 0.19930811787126626, 0.4923241691326778, 0.2505530011089714],
2248 [0.4923241691326778, 0.2505530011089714, 0.43734769396117146, 0.27768551364375393, 0.39924248711635457, 0.3301328314207743, 0.3904265993855176, 0.39080297384891327],
2249 [0.3904265993855176, 0.39080297384891327, 0.3571256555953457, 0.6199770586860098, 0.07549264735137257, 0.7114851701738012, -0.08615352937847076, 0.5456532445175477],
2250 [-0.08615352937847076, 0.5456532445175477, -0.12894674734696346, 0.501751917036087, -0.19060226717022227, 0.48171882426403184, -0.25102726060038916, 0.4920825218521362],
2251 [-0.25102726060038916, 0.4920825218521362, -0.4792753249105761, 0.5312301291467034, -0.6533340963592308, 0.2916587830253511, -0.5455699785393353, 0.08667925007453033],
2252 [-0.5455699785393353, 0.08667925007453033, -0.5170411665603402, 0.032414225004965255, -0.5170411665603402, -0.032414225004965144, -0.5455699785393353, -0.08667925007453012],
2253 [-0.5455699785393353, -0.08667925007453012, -0.6533340963592309, -0.29165878302535075, -0.47927532491057623, -0.5312301291467032, -0.2510272606003892, -0.4920825218521361],
2254 [-0.2510272606003892, -0.4920825218521361, -0.19060226717022244, -0.48171882426403173, -0.12894674734696351, -0.501751917036087, -0.08615352937847082, -0.5456532445175477],
2255 [-0.08615352937847082, -0.5456532445175477, 0.07549264735137243, -0.7114851701738012, 0.3571256555953456, -0.6199770586860099, 0.39042659938551744, -0.3908029738489133],
2256 [0.39042659938551744, -0.3908029738489133, 0.3992424871163545, -0.33013283142077454, 0.43734769396117146, -0.27768551364375416, 0.49232416913267785, -0.25055300110897155],
2257 [0.49232416913267785, -0.25055300110897155, 0.5961576437278834, -0.19930811787126637, 0.6480743810254861, -0.09965405893563323, 0.6480743810254861, -4.85722573273506e-17],
2258 ];
2259
2260 #[rustfmt::skip]
2261 const PILL: [[f64; 8]; 11] = [
2262 [0.8830446015004401, 0.3213671315821969, 0.8096338575801304, 0.4088670230999255, 0.7063570243187614, 0.47130983399174997, 0.5868926669545238, 0.4923917794089684],
2263 [0.5868926669545238, 0.4923917794089684, 0.5437794169386783, 0.5, 0.4791862779591189, 0.5, 0.35, 0.5],
2264 [0.35, 0.5, 0.11666666666666668, 0.5, -0.11666666666666664, 0.5, -0.35, 0.5],
2265 [-0.35, 0.5, -0.4791862779591189, 0.5, -0.5437794169386783, 0.5, -0.5868926669545238, 0.4923917794089684],
2266 [-0.5868926669545238, 0.4923917794089684, -0.8258213816829989, 0.4502278885745316, -1.0, 0.2426205358416706, -1.0, 0.0],
2267 [-1.0, 0.0, -1.0, -0.2426205358416706, -0.8258213816829989, -0.4502278885745316, -0.5868926669545238, -0.4923917794089684],
2268 [-0.5868926669545238, -0.4923917794089684, -0.5437794169386783, -0.5, -0.4791862779591189, -0.5, -0.35, -0.5],
2269 [-0.35, -0.5, -0.11666666666666668, -0.5, 0.11666666666666664, -0.5, 0.35, -0.5],
2270 [0.35, -0.5, 0.4791862779591189, -0.5, 0.5437794169386783, -0.5, 0.5868926669545238, -0.4923917794089684],
2271 [0.5868926669545238, -0.4923917794089684, 0.8258213816829989, -0.4502278885745316, 1.0, -0.2426205358416706, 1.0, 0.0],
2272 [1.0, 0.0, 1.0, 0.1213102679208353, 0.9564553454207497, 0.23386724006446818, 0.8830446015004401, 0.3213671315821969],
2273 ];
2274
2275 #[rustfmt::skip]
2276 const NORMALIZED_ARCH: [[f64; 8]; 8] = [
2277 [0.9798474571599122, 0.5468644038342292, 0.9798474571599124, 0.6525169448765878, 0.9395423714797368, 0.7581694859189465, 0.8589322001193854, 0.8387796572792977],
2278 [0.8589322001193854, 0.8387796572792977, 0.6977118573986831, 1.0, 0.4363220359499508, 1.0, 0.27510169322924843, 0.8387796572792977],
2279 [0.27510169322924843, 0.8387796572792977, 0.20531864277993192, 0.7689966068299814, 0.13553559233061546, 0.6992135563806647, 0.06575254188129889, 0.6294305059313483],
2280 [0.06575254188129889, 0.6294305059313483, 0.020152542840087787, 0.583830506890137, 0.020152542840087787, 0.5098983007783213, 0.06575254188129889, 0.4642983017371102],
2281 [0.06575254188129889, 0.4642983017371102, 0.20531864277993184, 0.32473220083847715, 0.3448847436785648, 0.18516609993984418, 0.4844508445771978, 0.045599999041211176],
2282 [0.4844508445771978, 0.045599999041211176, 0.5300508436184089, 6.481820712280062e-17, 0.6039830497302248, 0.0, 0.649583048771436, 0.04559999904121111],
2283 [0.649583048771436, 0.04559999904121111, 0.7193660992207523, 0.11538304949052762, 0.789149149670069, 0.18516609993984412, 0.8589322001193855, 0.25494915038916066],
2284 [0.8589322001193855, 0.25494915038916066, 0.9395423714797366, 0.3355593217495118, 0.9798474571599123, 0.4412118627918704, 0.9798474571599122, 0.5468644038342292],
2285 ];
2286
2287 #[test]
2288 fn parity_uneven_cut_competition() {
2289 let polygon = RoundedPolygon::from_vertices(
2290 &[p(0.0, 0.0), p(1.0, 0.0), p(0.5, 1.0)],
2291 CornerRounding::UNROUNDED,
2292 Some(&[
2293 CornerRounding::radius(1.0),
2294 CornerRounding::new(1.0, 1.0),
2295 CornerRounding::UNROUNDED,
2296 ]),
2297 None,
2298 );
2299 assert_matches_golden(&polygon, &TRIANGLE_SPACE_USAGE, "triangle");
2300 assert_close(0.5, polygon.center().x, "center.x");
2301 assert_close(1.0 / 3.0, polygon.center().y, "center.y");
2302 assert_bounds(
2303 polygon.bounds(),
2304 [
2305 0.17224036342232252,
2306 -5.551115123125783e-17,
2307 0.879126070905334,
2308 1.0,
2309 ],
2310 "triangle bounds",
2311 );
2312 }
2313
2314 #[test]
2315 fn parity_square() {
2316 let unrounded =
2317 RoundedPolygon::rectangle(2.0, 2.0, CornerRounding::UNROUNDED, None, Point::ZERO);
2318 assert_matches_golden(&unrounded, &SQUARE_UNROUNDED, "square");
2319 let r = std::f64::consts::SQRT_2;
2321 assert_bounds(unrounded.max_bounds(), [-r, -r, r, r], "square max bounds");
2322
2323 let rounded =
2324 RoundedPolygon::rectangle(2.0, 2.0, CornerRounding::new(0.5, 0.2), None, Point::ZERO);
2325 assert_matches_golden(&rounded, &SQUARE_ROUNDED_SMOOTHED, "rounded square");
2326 assert_eq!(
2327 kinds(&rounded),
2328 [
2329 FeatureKind::ConvexCorner,
2330 FeatureKind::Edge,
2331 FeatureKind::ConvexCorner,
2332 FeatureKind::Edge,
2333 FeatureKind::ConvexCorner,
2334 FeatureKind::Edge,
2335 FeatureKind::ConvexCorner,
2336 FeatureKind::Edge,
2337 ]
2338 );
2339 }
2340
2341 #[test]
2342 fn parity_circle() {
2343 let circle = RoundedPolygon::circle(8, 1.0, Point::ZERO);
2344 assert_matches_golden(&circle, &CIRCLE_8, "circle");
2345 }
2346
2347 #[test]
2348 fn parity_star() {
2349 let star = RoundedPolygon::star(StarParams {
2350 rounding: CornerRounding::new(0.3, 0.4),
2351 inner_rounding: Some(CornerRounding::radius(0.2)),
2352 ..StarParams::default()
2353 });
2354 assert_matches_golden(&star, &STAR_5, "star");
2355 let want: Vec<FeatureKind> = (0..5)
2357 .flat_map(|_| {
2358 [
2359 FeatureKind::ConvexCorner,
2360 FeatureKind::Edge,
2361 FeatureKind::ConcaveCorner,
2362 FeatureKind::Edge,
2363 ]
2364 })
2365 .collect();
2366 assert_eq!(kinds(&star), want);
2367 }
2368
2369 #[test]
2370 fn parity_pill_and_pill_star() {
2371 let pill = RoundedPolygon::pill(2.0, 1.0, 0.3, Point::ZERO);
2372 assert_matches_golden(&pill, &PILL, "pill");
2373 assert_bounds(pill.bounds(), [-1.0, -0.5, 1.0, 0.5], "pill bounds");
2374
2375 let pill_star = RoundedPolygon::pill_star(PillStarParams {
2379 rounding: CornerRounding::new(0.2, 0.1),
2380 inner_rounding: Some(CornerRounding::radius(0.1)),
2381 ..PillStarParams::default()
2382 });
2383 assert_eq!(pill_star.cubics().len(), 49);
2384 assert_eq!(pill_star.features().len(), 32);
2385 assert_bounds(
2386 pill_star.bounds(),
2387 [
2388 -1.258_127_058_178_750_3,
2389 -0.910_050_473_510_705_7,
2390 1.221_825_670_243_362_5,
2391 0.910_050_473_510_705_7,
2392 ],
2393 "pill star bounds",
2394 );
2395 }
2396
2397 #[test]
2398 fn parity_normalized_arch() {
2399 let round_100 = CornerRounding::radius(1.0);
2400 let round_20 = CornerRounding::radius(0.2);
2401 let arch = RoundedPolygon::from_num_vertices(
2402 4,
2403 1.0,
2404 Point::ZERO,
2405 CornerRounding::UNROUNDED,
2406 Some(&[round_100, round_100, round_20, round_20]),
2407 )
2408 .normalized();
2409
2410 assert_matches_golden(&arch, &NORMALIZED_ARCH, "normalized arch");
2411 assert_bounds(
2412 arch.bounds(),
2413 [0.020_152_542_840_087_787, 0.0, 0.979_847_457_159_912_4, 1.0],
2414 "arch bounds",
2415 );
2416 assert!((arch.center().x - 0.567_016_946_674_317).abs() < GOLDEN_EPSILON);
2417 assert!((arch.center().y - 0.546_864_403_834_229_2).abs() < GOLDEN_EPSILON);
2418 }
2419}