1use kurbo::{BezPath, Point, Rect};
95
96use super::{Cubic, DISTANCE_EPSILON, Feature, FeatureKind, RoundedPolygon};
97
98const ANGLE_EPSILON: f64 = 1e-6;
102
103const MEASURE_SEGMENTS: usize = 3;
111
112fn positive_modulo(value: f64, modulus: f64) -> f64 {
122 let remainder = value.rem_euclid(modulus);
123 if remainder >= modulus { 0.0 } else { remainder }
124}
125
126fn progress_distance(p1: f64, p2: f64) -> f64 {
129 let diff = (p1 - p2).abs();
130 diff.min(1.0 - diff)
131}
132
133fn progress_in_range(progress: f64, from: f64, to: f64) -> bool {
138 if to >= from {
139 progress >= from && progress <= to
140 } else {
141 progress >= from || progress <= to
142 }
143}
144
145fn linear_map(x_values: &[f64], y_values: &[f64], x: f64) -> f64 {
153 debug_assert!((0.0..=1.0).contains(&x), "progress out of range: {x}");
154
155 let start = (0..x_values.len())
156 .find(|&i| progress_in_range(x, x_values[i], x_values[(i + 1) % x_values.len()]))
157 .expect("a validated mapping covers every progress value");
158 let end = (start + 1) % x_values.len();
159
160 let segment_size_x = positive_modulo(x_values[end] - x_values[start], 1.0);
161 let segment_size_y = positive_modulo(y_values[end] - y_values[start], 1.0);
162 let position_in_segment = if segment_size_x < 0.001 {
165 0.5
166 } else {
167 positive_modulo(x - x_values[start], 1.0) / segment_size_x
168 };
169
170 positive_modulo(y_values[start] + segment_size_y * position_in_segment, 1.0)
171}
172
173#[derive(Clone, Debug)]
179struct DoubleMapper {
180 source: Vec<f64>,
181 target: Vec<f64>,
182}
183
184impl DoubleMapper {
185 fn new(mappings: &[(f64, f64)]) -> DoubleMapper {
192 let source: Vec<f64> = mappings.iter().map(|m| m.0).collect();
193 let target: Vec<f64> = mappings.iter().map(|m| m.1).collect();
194 validate_progress(&source, "source");
195 validate_progress(&target, "target");
196 DoubleMapper { source, target }
197 }
198
199 fn map(&self, x: f64) -> f64 {
201 linear_map(&self.source, &self.target, x)
202 }
203
204 fn map_back(&self, x: f64) -> f64 {
206 linear_map(&self.target, &self.source, x)
207 }
208}
209
210fn validate_progress(values: &[f64], side: &str) {
218 assert!(
219 !values.is_empty(),
220 "a {side} mapping needs at least one value"
221 );
222
223 let mut previous = values[values.len() - 1];
224 let mut wraps = 0_u32;
225 for ¤t in values {
226 assert!(
227 (0.0..1.0).contains(¤t),
228 "{side} progress outside 0..1: {values:?}"
229 );
230 assert!(
231 progress_distance(current, previous) > DISTANCE_EPSILON,
232 "{side} progress repeats a value: {values:?}"
233 );
234 if current < previous {
235 wraps += 1;
236 assert!(
237 wraps <= 1,
238 "{side} progress wraps more than once: {values:?}"
239 );
240 }
241 previous = current;
242 }
243}
244
245fn measure_cubic(cubic: &Cubic) -> f64 {
251 closest_progress_to(cubic, f64::INFINITY).1
252}
253
254fn find_cubic_cut_point(cubic: &Cubic, measure: f64) -> f64 {
257 closest_progress_to(cubic, measure).0
258}
259
260fn closest_progress_to(cubic: &Cubic, threshold: f64) -> (f64, f64) {
267 let segments = MEASURE_SEGMENTS as f64;
268 let mut total = 0.0;
269 let mut remainder = threshold;
270 let mut previous = cubic.anchor0();
271
272 for i in 0..=MEASURE_SEGMENTS {
273 let progress = i as f64 / segments;
274 let point = cubic.point_on_curve(progress);
275 let segment = (point - previous).hypot();
276
277 if segment >= remainder {
278 let covered = if segment > 0.0 {
281 remainder / segment
282 } else {
283 1.0
284 };
285 return (progress - (1.0 - covered) / segments, threshold);
286 }
287
288 remainder -= segment;
289 total += segment;
290 previous = point;
291 }
292
293 (1.0, total)
294}
295
296#[derive(Clone, Copy, Debug)]
301struct MeasuredCubic {
302 cubic: Cubic,
303 measured_size: f64,
304 start_outline_progress: f64,
305 end_outline_progress: f64,
306}
307
308impl MeasuredCubic {
309 fn new(cubic: Cubic, start_outline_progress: f64, end_outline_progress: f64) -> MeasuredCubic {
310 debug_assert!(
311 (0.0..=1.0).contains(&start_outline_progress)
312 && (0.0..=1.0).contains(&end_outline_progress)
313 && end_outline_progress >= start_outline_progress,
314 "outline progress range must be ordered and within 0..=1, got \
315 {start_outline_progress}..{end_outline_progress}"
316 );
317 MeasuredCubic {
318 cubic,
319 measured_size: measure_cubic(&cubic),
320 start_outline_progress,
321 end_outline_progress,
322 }
323 }
324
325 fn cut_at_progress(&self, cut_outline_progress: f64) -> (MeasuredCubic, MeasuredCubic) {
337 let bounded =
338 cut_outline_progress.clamp(self.start_outline_progress, self.end_outline_progress);
339 let progress_size = self.end_outline_progress - self.start_outline_progress;
340 let progress_from_start = bounded - self.start_outline_progress;
341
342 let relative_progress = progress_from_start / progress_size;
345 let t = find_cubic_cut_point(&self.cubic, relative_progress * self.measured_size);
346 assert!(
347 (0.0..=1.0).contains(&t),
348 "a cubic cut point must be in 0..=1, got {t}"
349 );
350
351 let (before, after) = self.cubic.split(t);
352 (
353 MeasuredCubic::new(before, self.start_outline_progress, bounded),
354 MeasuredCubic::new(after, bounded, self.end_outline_progress),
355 )
356 }
357}
358
359#[derive(Clone, Copy, Debug)]
362struct ProgressableFeature {
363 progress: f64,
364 representative: Point,
365 kind: FeatureKind,
366}
367
368fn feature_representative_point(feature: &Feature) -> Point {
371 let cubics = feature.cubics();
372 let first = cubics[0].anchor0();
373 let last = cubics[cubics.len() - 1].anchor1();
374 Point::new((first.x + last.x) / 2.0, (first.y + last.y) / 2.0)
375}
376
377fn feature_dist_squared(f1: &ProgressableFeature, f2: &ProgressableFeature) -> Option<f64> {
383 if f1.kind != f2.kind {
384 return None;
385 }
386 Some((f1.representative - f2.representative).hypot2())
387}
388
389#[derive(Clone, Debug)]
392struct MeasuredPolygon {
393 cubics: Vec<MeasuredCubic>,
394 features: Vec<ProgressableFeature>,
395}
396
397impl MeasuredPolygon {
398 fn measure(polygon: &RoundedPolygon) -> MeasuredPolygon {
409 let mut cubics: Vec<Cubic> = Vec::new();
410 let mut corner_cubic_indices: Vec<(usize, &Feature)> = Vec::new();
413
414 for feature in polygon.features() {
415 let representative = feature.cubics().len() / 2;
416 for (i, cubic) in feature.cubics().iter().enumerate() {
417 if feature.is_corner() && i == representative {
418 corner_cubic_indices.push((cubics.len(), feature));
419 }
420 cubics.push(*cubic);
421 }
422 }
423
424 let mut measures = vec![0.0; cubics.len() + 1];
425 let mut total_measure = 0.0;
426 for (i, cubic) in cubics.iter().enumerate() {
427 let measure = measure_cubic(cubic);
428 debug_assert!(measure >= 0.0, "a measured cubic cannot be negative");
429 total_measure += measure;
430 measures[i + 1] = total_measure;
431 }
432 assert!(
433 total_measure > 0.0,
434 "a polygon with no measurable outline cannot be morphed"
435 );
436
437 let outline_progress: Vec<f64> = measures.iter().map(|m| m / total_measure).collect();
438 let features = corner_cubic_indices
439 .iter()
440 .map(|&(index, feature)| ProgressableFeature {
441 progress: positive_modulo(
442 (outline_progress[index] + outline_progress[index + 1]) / 2.0,
443 1.0,
444 ),
445 representative: feature_representative_point(feature),
446 kind: feature.kind(),
447 })
448 .collect();
449
450 MeasuredPolygon::assemble(features, &cubics, &outline_progress)
451 }
452
453 fn assemble(
461 features: Vec<ProgressableFeature>,
462 cubics: &[Cubic],
463 outline_progress: &[f64],
464 ) -> MeasuredPolygon {
465 debug_assert!(
466 outline_progress.len() == cubics.len() + 1,
467 "expected one more outline-progress value than curves"
468 );
469 debug_assert!(
470 outline_progress[0] == 0.0 && outline_progress[outline_progress.len() - 1] == 1.0,
471 "outline progress must run from 0 to 1"
472 );
473
474 let mut measured: Vec<MeasuredCubic> = Vec::with_capacity(cubics.len());
475 let mut start = 0.0;
476 for (i, cubic) in cubics.iter().enumerate() {
477 if outline_progress[i + 1] - outline_progress[i] > DISTANCE_EPSILON {
478 measured.push(MeasuredCubic::new(*cubic, start, outline_progress[i + 1]));
479 start = outline_progress[i + 1];
482 }
483 }
484
485 measured
488 .last_mut()
489 .expect("a measurable outline has at least one non-empty curve")
490 .end_outline_progress = 1.0;
491
492 MeasuredPolygon {
493 cubics: measured,
494 features,
495 }
496 }
497
498 fn cut_and_shift(self, cutting_point: f64) -> MeasuredPolygon {
508 assert!(
509 (0.0..=1.0).contains(&cutting_point),
510 "a cutting point must be in 0..=1, got {cutting_point}"
511 );
512 if cutting_point < DISTANCE_EPSILON {
513 return self;
514 }
515
516 let target_index = self
517 .cubics
518 .iter()
519 .position(|c| {
520 cutting_point >= c.start_outline_progress && cutting_point <= c.end_outline_progress
521 })
522 .expect("the cutting point lies on the outline");
523 let (before, after) = self.cubics[target_index].cut_at_progress(cutting_point);
524
525 let mut cubics = Vec::with_capacity(self.cubics.len() + 2);
528 cubics.push(after.cubic);
529 for i in 1..self.cubics.len() {
530 cubics.push(self.cubics[(i + target_index) % self.cubics.len()].cubic);
531 }
532 cubics.push(before.cubic);
533
534 let mut outline_progress = Vec::with_capacity(cubics.len() + 1);
537 for i in 0..=cubics.len() {
538 if i == 0 {
539 outline_progress.push(0.0);
540 } else if i == cubics.len() {
541 outline_progress.push(1.0);
542 } else {
543 let index = (target_index + i - 1) % self.cubics.len();
544 outline_progress.push(positive_modulo(
545 self.cubics[index].end_outline_progress - cutting_point,
546 1.0,
547 ));
548 }
549 }
550
551 let features = self
552 .features
553 .iter()
554 .map(|f| ProgressableFeature {
555 progress: positive_modulo(f.progress - cutting_point, 1.0),
556 ..*f
557 })
558 .collect();
559
560 MeasuredPolygon::assemble(features, &cubics, &outline_progress)
561 }
562
563 fn get(&self, index: usize) -> Option<MeasuredCubic> {
564 self.cubics.get(index).copied()
565 }
566}
567
568fn feature_mapper(
574 features1: &[ProgressableFeature],
575 features2: &[ProgressableFeature],
576) -> DoubleMapper {
577 let corners = |features: &[ProgressableFeature]| -> Vec<ProgressableFeature> {
578 features
579 .iter()
580 .copied()
581 .filter(|f| f.kind != FeatureKind::Edge)
582 .collect()
583 };
584 DoubleMapper::new(&do_mapping(&corners(features1), &corners(features2)))
585}
586
587fn do_mapping(
599 features1: &[ProgressableFeature],
600 features2: &[ProgressableFeature],
601) -> Vec<(f64, f64)> {
602 let mut candidates: Vec<(f64, usize, usize)> = Vec::new();
603 for (i1, f1) in features1.iter().enumerate() {
604 for (i2, f2) in features2.iter().enumerate() {
605 if let Some(distance) = feature_dist_squared(f1, f2) {
606 candidates.push((distance, i1, i2));
607 }
608 }
609 }
610 candidates.sort_by(|a, b| a.0.total_cmp(&b.0));
611
612 let Some(&(_, first1, first2)) = candidates.first() else {
614 return vec![(0.0, 0.0), (0.5, 0.5)];
615 };
616 if candidates.len() == 1 {
618 let (p1, p2) = (features1[first1].progress, features2[first2].progress);
619 return vec![(p1, p2), ((p1 + 0.5) % 1.0, (p2 + 0.5) % 1.0)];
620 }
621
622 let mut helper = MappingHelper::new(features1.len(), features2.len());
623 for &(_, i1, i2) in &candidates {
624 helper.add(features1[i1].progress, features2[i2].progress, i1, i2);
625 }
626 helper.mapping
627}
628
629struct MappingHelper {
632 mapping: Vec<(f64, f64)>,
633 used1: Vec<bool>,
634 used2: Vec<bool>,
635}
636
637impl MappingHelper {
638 fn new(len1: usize, len2: usize) -> MappingHelper {
639 MappingHelper {
640 mapping: Vec::new(),
641 used1: vec![false; len1],
642 used2: vec![false; len2],
643 }
644 }
645
646 fn add(&mut self, p1: f64, p2: f64, i1: usize, i2: usize) {
647 if self.used1[i1] || self.used2[i2] {
649 return;
650 }
651
652 let insertion = match self
653 .mapping
654 .binary_search_by(|probe| probe.0.total_cmp(&p1))
655 {
656 Ok(_) => panic!("two features cannot share the outline progress {p1}"),
657 Err(index) => index,
658 };
659
660 let n = self.mapping.len();
661 if n >= 1 {
662 let (before1, before2) = self.mapping[(insertion + n - 1) % n];
663 let (after1, after2) = self.mapping[insertion % n];
664
665 if progress_distance(p1, before1) < DISTANCE_EPSILON
667 || progress_distance(p1, after1) < DISTANCE_EPSILON
668 || progress_distance(p2, before2) < DISTANCE_EPSILON
669 || progress_distance(p2, after2) < DISTANCE_EPSILON
670 {
671 return;
672 }
673
674 if n > 1 && !progress_in_range(p2, before2, after2) {
678 return;
679 }
680 }
681
682 self.mapping.insert(insertion, (p1, p2));
683 self.used1[i1] = true;
684 self.used2[i2] = true;
685 }
686}
687
688#[derive(Clone, Debug)]
703pub struct Morph {
704 start: RoundedPolygon,
705 end: RoundedPolygon,
706 pairs: Vec<(Cubic, Cubic)>,
710}
711
712impl Morph {
713 #[must_use]
725 pub fn new(start: RoundedPolygon, end: RoundedPolygon) -> Morph {
726 let pairs = Morph::match_polygons(&start, &end);
727 Morph { start, end, pairs }
728 }
729
730 #[must_use]
732 pub const fn start(&self) -> &RoundedPolygon {
733 &self.start
734 }
735
736 #[must_use]
738 pub const fn end(&self) -> &RoundedPolygon {
739 &self.end
740 }
741
742 fn match_polygons(p1: &RoundedPolygon, p2: &RoundedPolygon) -> Vec<(Cubic, Cubic)> {
744 let measured1 = MeasuredPolygon::measure(p1);
745 let measured2 = MeasuredPolygon::measure(p2);
746
747 let mapper = feature_mapper(&measured1.features, &measured2.features);
748
749 let cut_point = mapper.map(0.0);
752 let bs1 = measured1;
753 let bs2 = measured2.cut_and_shift(cut_point);
754
755 let mut pairs: Vec<(Cubic, Cubic)> = Vec::new();
756 let mut curve1 = bs1.get(0);
759 let mut curve2 = bs2.get(0);
760 let mut i1 = 1;
761 let mut i2 = 1;
762
763 while let (Some(b1), Some(b2)) = (curve1, curve2) {
764 let b1_end = if i1 == bs1.cubics.len() {
768 1.0
769 } else {
770 b1.end_outline_progress
771 };
772 let b2_end = if i2 == bs2.cubics.len() {
773 1.0
774 } else {
775 mapper.map_back(positive_modulo(b2.end_outline_progress + cut_point, 1.0))
776 };
777
778 let boundary = b1_end.min(b2_end);
781
782 let (segment1, remainder1) = if b1_end > boundary + ANGLE_EPSILON {
783 let (cut, rest) = b1.cut_at_progress(boundary);
784 (cut, Some(rest))
785 } else {
786 let next = bs1.get(i1);
787 i1 += 1;
788 (b1, next)
789 };
790
791 let (segment2, remainder2) = if b2_end > boundary + ANGLE_EPSILON {
792 let (cut, rest) =
793 b2.cut_at_progress(positive_modulo(mapper.map(boundary) - cut_point, 1.0));
794 (cut, Some(rest))
795 } else {
796 let next = bs2.get(i2);
797 i2 += 1;
798 (b2, next)
799 };
800
801 pairs.push((segment1.cubic, segment2.cubic));
802 curve1 = remainder1;
803 curve2 = remainder2;
804 }
805
806 debug_assert!(
807 curve1.is_none() && curve2.is_none(),
808 "both outlines must be fully matched"
809 );
810
811 pairs
812 }
813
814 #[must_use]
819 pub fn as_cubics(&self, progress: f64) -> Vec<Cubic> {
820 let mut cubics = Vec::with_capacity(self.pairs.len());
821 self.for_each_cubic(progress, |cubic| cubics.push(cubic));
822 cubics
823 }
824
825 #[must_use]
835 pub fn path(&self, progress: f64) -> BezPath {
836 let mut path = BezPath::new();
837 let mut started = false;
838 self.for_each_cubic(progress, |cubic| {
839 if !started {
840 path.move_to(cubic.anchor0());
841 started = true;
842 }
843 path.curve_to(cubic.control0(), cubic.control1(), cubic.anchor1());
844 });
845 if started {
846 path.close_path();
847 }
848 path
849 }
850
851 fn for_each_cubic(&self, progress: f64, mut emit: impl FnMut(Cubic)) {
858 let Some((first_start, first_end)) = self.pairs.first() else {
859 return;
860 };
861 let first_anchor = interpolate(first_start, first_end, progress).anchor0();
862
863 let last = self.pairs.len() - 1;
864 for (i, (start, end)) in self.pairs.iter().enumerate() {
865 let cubic = interpolate(start, end, progress);
866 if i == last {
867 emit(Cubic::new(
868 cubic.anchor0(),
869 cubic.control0(),
870 cubic.control1(),
871 first_anchor,
872 ));
873 } else {
874 emit(cubic);
875 }
876 }
877 }
878
879 #[must_use]
887 pub fn bounds(&self) -> Rect {
888 self.start.bounds().union(self.end.bounds())
889 }
890
891 #[must_use]
897 pub fn exact_bounds(&self) -> Rect {
898 self.start.exact_bounds().union(self.end.exact_bounds())
899 }
900
901 #[must_use]
904 pub fn max_bounds(&self) -> Rect {
905 self.start.max_bounds().union(self.end.max_bounds())
906 }
907}
908
909fn interpolate(start: &Cubic, end: &Cubic, progress: f64) -> Cubic {
915 let (a, b) = (start.points(), end.points());
916 let mut points = [0.0_f64; 8];
917 for (i, point) in points.iter_mut().enumerate() {
918 *point = a[i] * (1.0 - progress) + b[i] * progress;
919 }
920 Cubic::from_raw(points)
921}
922
923#[cfg(test)]
924mod tests {
925 use super::*;
926 use crate::shapes::{CornerRounding, StarParams};
927 use kurbo::{CubicBez, ParamCurve, ParamCurveNearest, PathEl, Point, Shape};
928
929 const GOLDEN_EPSILON: f64 = 1e-12;
934
935 const EPSILON: f64 = 1e-4;
938
939 fn triangle() -> RoundedPolygon {
940 RoundedPolygon::from_num_vertices(3, 1.0, Point::ZERO, CornerRounding::UNROUNDED, None)
941 }
942
943 fn square() -> RoundedPolygon {
944 RoundedPolygon::from_num_vertices(4, 1.0, Point::ZERO, CornerRounding::UNROUNDED, None)
945 }
946
947 fn octagon() -> RoundedPolygon {
948 RoundedPolygon::from_num_vertices(8, 1.0, Point::ZERO, CornerRounding::UNROUNDED, None)
949 }
950
951 fn circle8() -> RoundedPolygon {
952 RoundedPolygon::circle(8, 1.0, Point::ZERO)
953 }
954
955 fn rounded_square() -> RoundedPolygon {
956 RoundedPolygon::rectangle(2.0, 2.0, CornerRounding::new(0.3, 0.5), None, Point::ZERO)
957 }
958
959 fn star5() -> RoundedPolygon {
960 RoundedPolygon::star(StarParams {
961 num_vertices_per_radius: 5,
962 inner_radius: 0.5,
963 rounding: CornerRounding::radius(0.2),
964 ..StarParams::default()
965 })
966 }
967
968 fn segments(path: &BezPath) -> Vec<CubicBez> {
970 let mut start = Point::ZERO;
971 let mut previous = Point::ZERO;
972 let mut out = Vec::new();
973 for element in path.elements() {
974 match *element {
975 PathEl::MoveTo(p) => {
976 start = p;
977 previous = p;
978 }
979 PathEl::CurveTo(c0, c1, p) => {
980 out.push(CubicBez::new(previous, c0, c1, p));
981 previous = p;
982 }
983 PathEl::ClosePath => previous = start,
984 other => panic!("unexpected path element {other:?}"),
985 }
986 }
987 out
988 }
989
990 fn distance_to(path: &BezPath, point: Point) -> f64 {
992 segments(path)
993 .iter()
994 .map(|segment| segment.nearest(point, 1e-12).distance_sq.sqrt())
995 .fold(f64::INFINITY, f64::min)
996 }
997
998 fn assert_same_outline(a: &BezPath, b: &BezPath, tolerance: f64, what: &str) {
1002 for (from, to, direction) in [(a, b, "a->b"), (b, a, "b->a")] {
1003 for segment in segments(from) {
1004 for i in 0..=8 {
1005 let point = segment.eval(f64::from(i) / 8.0);
1006 let distance = distance_to(to, point);
1007 assert!(
1008 distance < tolerance,
1009 "{what} ({direction}): {point:?} is {distance} off the other outline"
1010 );
1011 }
1012 }
1013 }
1014 }
1015
1016 fn assert_cubics_match_golden(cubics: &[Cubic], golden: &[[f64; 8]], name: &str) {
1017 assert_eq!(cubics.len(), golden.len(), "{name}: curve count drifted");
1018 for (i, (cubic, want)) in cubics.iter().zip(golden).enumerate() {
1019 for (j, (got, want)) in cubic.points().iter().zip(want).enumerate() {
1020 assert!(
1021 (got - want).abs() < GOLDEN_EPSILON,
1022 "{name}: curve {i} coordinate {j}: expected {want}, got {got}"
1023 );
1024 }
1025 }
1026 }
1027
1028 fn assert_close(expected: f64, actual: f64, what: &str) {
1029 assert!(
1030 (expected - actual).abs() < EPSILON,
1031 "{what}: expected {expected}, got {actual}"
1032 );
1033 }
1034
1035 #[test]
1040 fn positive_modulo_stays_below_the_modulus() {
1041 assert_close(1.0, positive_modulo(4.0, 3.0), "4 mod 3");
1042 assert_close(2.0, positive_modulo(-4.0, 3.0), "-4 mod 3");
1043 assert_eq!(positive_modulo(-1e-18, 1.0), 0.0);
1046 }
1047
1048 #[test]
1049 fn progress_range_wraps_around_zero() {
1050 assert!(progress_in_range(0.5, 0.2, 0.7));
1051 assert!(!progress_in_range(0.1, 0.2, 0.7));
1052 assert!(progress_in_range(0.8, 0.7, 0.2));
1054 assert!(progress_in_range(0.1, 0.7, 0.2));
1055 assert!(!progress_in_range(0.5, 0.7, 0.2));
1056 }
1057
1058 #[test]
1059 fn progress_distance_takes_the_short_way_round() {
1060 assert_close(0.01, progress_distance(0.99, 0.0), "0.99 to 0");
1061 assert_close(0.2, progress_distance(0.3, 0.5), "0.3 to 0.5");
1062 }
1063
1064 fn validate_mapping(mapper: &DoubleMapper, expected: impl Fn(f64) -> f64) {
1067 for i in 0..10_000 {
1068 let source = f64::from(i) / 10_000.0;
1069 let target = expected(source);
1070 assert_close(target, mapper.map(source), "map");
1071 assert_close(source, mapper.map_back(target), "map back");
1072 }
1073 }
1074
1075 #[test]
1076 fn identity_mapping() {
1077 validate_mapping(&DoubleMapper::new(&[(0.0, 0.0), (0.5, 0.5)]), |x| x);
1078 }
1079
1080 #[test]
1081 fn simple_mapping() {
1082 validate_mapping(&DoubleMapper::new(&[(0.0, 0.0), (0.5, 0.25)]), |x| {
1084 if x < 0.5 {
1085 x / 2.0
1086 } else {
1087 (3.0 * x - 1.0) / 2.0
1088 }
1089 });
1090 }
1091
1092 #[test]
1093 fn wrapping_mappings_are_still_bijections() {
1094 validate_mapping(&DoubleMapper::new(&[(0.0, 0.5), (0.1, 0.6)]), |x| {
1097 (x + 0.5) % 1.0
1098 });
1099 validate_mapping(&DoubleMapper::new(&[(0.5, 0.0), (0.1, 0.6)]), |x| {
1100 (x + 0.5) % 1.0
1101 });
1102 validate_mapping(
1103 &DoubleMapper::new(&[(0.5, 0.5), (0.75, 0.75), (0.1, 0.1), (0.49, 0.49)]),
1104 |x| x,
1105 );
1106 }
1107
1108 #[test]
1109 fn multiple_point_mapping() {
1110 validate_mapping(
1111 &DoubleMapper::new(&[(0.4, 0.2), (0.5, 0.22), (0.0, 0.8)]),
1112 |x| {
1113 if x < 0.4 {
1114 (0.8 + x) % 1.0
1115 } else if x < 0.5 {
1116 0.2 + (x - 0.4) / 5.0
1117 } else {
1118 0.22 + (x - 0.5) * 1.16
1120 }
1121 },
1122 );
1123 }
1124
1125 #[test]
1126 #[should_panic(expected = "wraps more than once")]
1127 fn a_target_wrapping_twice_is_rejected() {
1128 let _ = DoubleMapper::new(&[(0.0, 0.0), (0.3, 0.6), (0.6, 0.3), (0.9, 0.9)]);
1129 }
1130
1131 #[test]
1132 #[should_panic(expected = "wraps more than once")]
1133 fn a_source_wrapping_twice_is_rejected() {
1134 let _ = DoubleMapper::new(&[(0.0, 0.0), (0.6, 0.3), (0.3, 0.6), (0.9, 0.9)]);
1135 }
1136
1137 fn assert_measures_cleanly(polygon: &RoundedPolygon) -> MeasuredPolygon {
1144 let measured = MeasuredPolygon::measure(polygon);
1145 assert_eq!(measured.cubics[0].start_outline_progress, 0.0);
1146 assert_eq!(
1147 measured.cubics[measured.cubics.len() - 1].end_outline_progress,
1148 1.0
1149 );
1150 for (i, cubic) in measured.cubics.iter().enumerate() {
1151 if i > 0 {
1152 assert_eq!(
1153 measured.cubics[i - 1].end_outline_progress,
1154 cubic.start_outline_progress,
1155 "curve {i} does not start where {} ended",
1156 i - 1
1157 );
1158 }
1159 assert!(cubic.end_outline_progress >= cubic.start_outline_progress);
1160 }
1161 for (i, feature) in measured.features.iter().enumerate() {
1162 assert!(
1163 (0.0..1.0).contains(&feature.progress),
1164 "corner {i} has invalid progress {}",
1165 feature.progress
1166 );
1167 }
1168 measured
1169 }
1170
1171 #[test]
1172 fn a_regular_polygon_measures_one_curve_per_side() {
1173 for sides in [3, 5, 8, 12, 20] {
1174 let polygon = RoundedPolygon::from_num_vertices(
1175 sides,
1176 1.0,
1177 Point::ZERO,
1178 CornerRounding::UNROUNDED,
1179 None,
1180 );
1181 let measured = assert_measures_cleanly(&polygon);
1182 assert_eq!(measured.cubics.len(), sides, "{sides}-gon");
1184 for (i, cubic) in measured.cubics.iter().enumerate() {
1185 assert_close(
1186 i as f64 / sides as f64,
1187 cubic.start_outline_progress,
1188 "side start",
1189 );
1190 }
1191 }
1192 }
1193
1194 #[test]
1195 fn rounded_shapes_measure_cleanly() {
1196 for rounding in [0.15, 0.5, 1.0] {
1197 let hexagon = RoundedPolygon::from_num_vertices(
1198 6,
1199 1.0,
1200 Point::ZERO,
1201 CornerRounding::radius(rounding),
1202 None,
1203 );
1204 assert_measures_cleanly(&hexagon);
1205 }
1206 assert_measures_cleanly(&circle8());
1207 assert_measures_cleanly(&rounded_square());
1208 assert_measures_cleanly(&star5());
1209 }
1210
1211 #[test]
1212 fn the_measurer_approximates_a_circle_within_one_and_a_half_percent() {
1213 let polygon = RoundedPolygon::circle(4, 1.0, Point::ZERO);
1216 let measured: f64 = polygon.cubics().iter().map(measure_cubic).sum();
1217 let expected = 2.0 * std::f64::consts::PI;
1218 assert!(
1219 (expected - measured).abs() < 0.015 * expected,
1220 "circle perimeter: expected ~{expected}, measured {measured}"
1221 );
1222 }
1223
1224 #[test]
1225 fn measured_progress_matches_the_dart_goldens() {
1226 let measured = MeasuredPolygon::measure(&rounded_square());
1228 let golden_ends = [
1229 0.032907413764029836,
1230 0.06996739408559419,
1231 0.10287480784962404,
1232 0.25,
1233 0.28290741376402984,
1234 0.3199673940855942,
1235 0.352874807849624,
1236 0.5,
1237 0.5329074137640298,
1238 0.5699673940855942,
1239 0.6028748078496241,
1240 0.75,
1241 0.7829074137640298,
1242 0.8199673940855942,
1243 0.8528748078496241,
1244 1.0,
1245 ];
1246 assert_eq!(measured.cubics.len(), golden_ends.len());
1247 for (i, (cubic, want)) in measured.cubics.iter().zip(&golden_ends).enumerate() {
1248 assert!(
1249 (cubic.end_outline_progress - want).abs() < GOLDEN_EPSILON,
1250 "curve {i}: expected end {want}, got {}",
1251 cubic.end_outline_progress
1252 );
1253 }
1254
1255 let golden_corners = [
1256 0.051437403924812,
1257 0.301437403924812,
1258 0.551437403924812,
1259 0.801437403924812,
1260 ];
1261 assert_eq!(measured.features.len(), golden_corners.len());
1262 for (i, (feature, want)) in measured.features.iter().zip(&golden_corners).enumerate() {
1263 assert!(
1264 (feature.progress - want).abs() < GOLDEN_EPSILON,
1265 "corner {i}: expected {want}, got {}",
1266 feature.progress
1267 );
1268 assert_eq!(feature.kind, FeatureKind::ConvexCorner);
1269 }
1270 }
1271
1272 #[test]
1273 fn a_star_alternates_convex_and_concave_corners() {
1274 let measured = MeasuredPolygon::measure(&star5());
1275 let kinds: Vec<FeatureKind> = measured.features.iter().map(|f| f.kind).collect();
1276 assert_eq!(kinds.len(), 10);
1277 for (i, kind) in kinds.iter().enumerate() {
1278 let want = if i % 2 == 0 {
1279 FeatureKind::ConvexCorner
1280 } else {
1281 FeatureKind::ConcaveCorner
1282 };
1283 assert_eq!(*kind, want, "corner {i}");
1284 }
1285 }
1286
1287 #[test]
1288 fn cutting_and_shifting_keeps_the_same_outline() {
1289 let measured = MeasuredPolygon::measure(&circle8());
1290 let before = measured.clone();
1291 let shifted = measured.cut_and_shift(0.4);
1292
1293 assert_eq!(shifted.cubics[0].start_outline_progress, 0.0);
1294 assert_eq!(
1295 shifted.cubics[shifted.cubics.len() - 1].end_outline_progress,
1296 1.0
1297 );
1298 assert_eq!(shifted.cubics.len(), before.cubics.len() + 1);
1301 for pair in shifted.cubics.windows(2) {
1302 let (previous, next) = (pair[0].cubic.anchor1(), pair[1].cubic.anchor0());
1303 assert!((next - previous).hypot() < 1e-12);
1304 }
1305 let old_start = before
1307 .cubics
1308 .iter()
1309 .find(|c| c.start_outline_progress <= 0.4 && c.end_outline_progress >= 0.4)
1310 .expect("some curve spans 0.4")
1311 .cut_at_progress(0.4)
1312 .1
1313 .cubic
1314 .anchor0();
1315 assert!((shifted.cubics[0].cubic.anchor0() - old_start).hypot() < 1e-12);
1316 }
1317
1318 fn corners(polygon: &RoundedPolygon) -> Vec<ProgressableFeature> {
1323 MeasuredPolygon::measure(polygon)
1324 .features
1325 .into_iter()
1326 .filter(|f| f.kind != FeatureKind::Edge)
1327 .collect()
1328 }
1329
1330 fn assert_mapping_matches_golden(
1331 p1: &RoundedPolygon,
1332 p2: &RoundedPolygon,
1333 golden: &[(f64, f64)],
1334 name: &str,
1335 ) {
1336 let mapping = do_mapping(&corners(p1), &corners(p2));
1337 assert_eq!(mapping.len(), golden.len(), "{name}: pair count drifted");
1338 for (i, (got, want)) in mapping.iter().zip(golden).enumerate() {
1339 assert!(
1340 (got.0 - want.0).abs() < GOLDEN_EPSILON && (got.1 - want.1).abs() < GOLDEN_EPSILON,
1341 "{name}: pair {i}: expected {want:?}, got {got:?}"
1342 );
1343 }
1344 }
1345
1346 #[test]
1347 fn corner_pairing_matches_the_dart_goldens() {
1348 assert_mapping_matches_golden(
1349 &triangle(),
1350 &square(),
1351 &[
1352 (0.0, 0.0),
1353 (0.3333333333333335, 0.25),
1354 (0.6666666666666667, 0.75),
1355 ],
1356 "triangle -> square",
1357 );
1358 assert_mapping_matches_golden(
1359 &triangle(),
1360 &octagon(),
1361 &[
1362 (0.0, 0.0),
1363 (0.3333333333333335, 0.375),
1364 (0.6666666666666667, 0.625),
1365 ],
1366 "triangle -> octagon",
1367 );
1368 assert_mapping_matches_golden(
1369 &circle8(),
1370 &square(),
1371 &[(0.0625, 0.0), (0.3125, 0.25), (0.5625, 0.5), (0.8125, 0.75)],
1372 "circle -> square",
1373 );
1374 assert_mapping_matches_golden(
1375 &star5(),
1376 &circle8(),
1377 &[
1378 (0.0465277905440431, 0.0625),
1379 (0.24652779054404328, 0.3125),
1380 (0.44652779054404323, 0.4375),
1381 (0.6465277905440434, 0.6875),
1382 (0.8465277905440434, 0.8125),
1383 ],
1384 "star -> circle",
1385 );
1386 }
1387
1388 #[test]
1389 fn every_paired_corner_is_a_near_neighbour() {
1390 let (c1, c2) = (corners(&triangle()), corners(&square()));
1393 let mut distances: Vec<f64> = do_mapping(&c1, &c2)
1394 .iter()
1395 .map(|&(p1, p2)| {
1396 let f1 = c1.iter().find(|f| f.progress == p1).expect("mapped corner");
1397 let f2 = c2.iter().find(|f| f.progress == p2).expect("mapped corner");
1398 feature_dist_squared(f1, f2).expect("corners of the same kind")
1399 })
1400 .collect();
1401 distances.sort_by(f64::total_cmp);
1402
1403 assert_eq!(distances.len(), 3);
1404 assert!(distances[0] < 1e-6, "the exact match: {}", distances[0]);
1405 assert_close(distances[1], distances[2], "the two symmetric matches");
1406 assert!(distances[2] < 0.3, "the worst match: {}", distances[2]);
1407 }
1408
1409 #[test]
1410 fn a_convex_corner_never_maps_to_a_concave_one() {
1411 let star = corners(&star5());
1412 let circle = corners(&circle8());
1413 let concave = star
1414 .iter()
1415 .find(|f| f.kind == FeatureKind::ConcaveCorner)
1416 .expect("a star has concave corners");
1417 assert!(feature_dist_squared(concave, &circle[0]).is_none());
1418
1419 assert_eq!(do_mapping(&star, &circle).len(), 5);
1422 }
1423
1424 #[test]
1425 fn mapping_a_complicated_pair_does_not_crash() {
1426 let checkmark = RoundedPolygon::from_vertices(
1429 &[
1430 Point::new(400.0, -304.0),
1431 Point::new(240.0, -464.0),
1432 Point::new(296.0, -520.0),
1433 Point::new(400.0, -416.0),
1434 Point::new(664.0, -680.0),
1435 Point::new(720.0, -624.0),
1436 ],
1437 CornerRounding::UNROUNDED,
1438 None,
1439 None,
1440 )
1441 .normalized();
1442 let very_sunny = RoundedPolygon::star(StarParams {
1443 num_vertices_per_radius: 8,
1444 inner_radius: 0.65,
1445 rounding: CornerRounding::radius(0.15),
1446 ..StarParams::default()
1447 })
1448 .normalized();
1449
1450 let mapping = do_mapping(&corners(&checkmark), &corners(&very_sunny));
1451 assert!(!mapping.is_empty());
1452 let _ = Morph::new(checkmark, very_sunny);
1453 }
1454
1455 #[test]
1460 fn the_endpoints_are_the_source_shapes_point_for_point() {
1461 let morph = Morph::new(triangle(), square());
1466 assert_cubics_match_golden(
1467 &morph.as_cubics(0.0),
1468 &[
1469 [
1470 1.0,
1471 0.0,
1472 0.5000000000000001,
1473 0.28867513459481287,
1474 1.6653345369377348e-16,
1475 0.5773502691896257,
1476 -0.49999999999999983,
1477 0.8660254037844387,
1478 ],
1479 [
1480 -0.49999999999999983,
1481 0.8660254037844387,
1482 -0.4999999999999999,
1483 0.5773502691896258,
1484 -0.5,
1485 0.288675134594813,
1486 -0.5000000000000001,
1487 2.7755575615628914e-17,
1488 ],
1489 [
1490 -0.5000000000000001,
1491 2.7755575615628914e-17,
1492 -0.5000000000000002,
1493 -0.28867513459481275,
1494 -0.5000000000000003,
1495 -0.5773502691896255,
1496 -0.5000000000000004,
1497 -0.8660254037844384,
1498 ],
1499 [
1500 -0.5000000000000004,
1501 -0.8660254037844384,
1502 -3.3306690738754696e-16,
1503 -0.5773502691896256,
1504 0.4999999999999998,
1505 -0.2886751345948128,
1506 1.0,
1507 0.0,
1508 ],
1509 ],
1510 "triangle -> square at 0",
1511 );
1512 assert_cubics_match_golden(
1513 &morph.as_cubics(1.0),
1514 &[
1515 [
1516 1.0,
1517 0.0,
1518 0.6666666666666667,
1519 0.3333333333333333,
1520 0.3333333333333334,
1521 0.6666666666666666,
1522 6.123233995736766e-17,
1523 1.0,
1524 ],
1525 [
1526 6.123233995736766e-17,
1527 1.0,
1528 -0.33333333333333326,
1529 0.6666666666666667,
1530 -0.6666666666666666,
1531 0.3333333333333334,
1532 -1.0,
1533 1.2246467991473532e-16,
1534 ],
1535 [
1536 -1.0,
1537 1.2246467991473532e-16,
1538 -0.6666666666666669,
1539 -0.33333333333333326,
1540 -0.3333333333333335,
1541 -0.6666666666666666,
1542 -1.8369701987210297e-16,
1543 -1.0,
1544 ],
1545 [
1546 -1.8369701987210297e-16,
1547 -1.0,
1548 0.3333333333333332,
1549 -0.6666666666666667,
1550 0.6666666666666665,
1551 -0.33333333333333337,
1552 1.0,
1553 0.0,
1554 ],
1555 ],
1556 "triangle -> square at 1",
1557 );
1558 }
1559
1560 #[test]
1561 fn the_endpoints_trace_the_source_outlines() {
1562 for (name, start, end) in [
1563 ("triangle -> square", triangle(), square()),
1564 ("triangle -> octagon", triangle(), octagon()),
1565 ("circle -> square", circle8(), square()),
1566 ("rounded square -> circle", rounded_square(), circle8()),
1567 ("star -> circle", star5(), circle8()),
1568 ] {
1569 let (start_path, end_path) = (start.to_path(), end.to_path());
1570 let morph = Morph::new(start.clone(), end.clone());
1571
1572 assert_same_outline(&morph.path(0.0), &start_path, 1e-9, &format!("{name} at 0"));
1573 assert_same_outline(&morph.path(1.0), &end_path, 1e-9, &format!("{name} at 1"));
1574
1575 assert!(
1578 (morph.path(0.0).area() - start_path.area()).abs() < 1e-9,
1579 "{name}: filled area differs at 0"
1580 );
1581 assert!(
1582 (morph.path(1.0).area() - end_path.area()).abs() < 1e-9,
1583 "{name}: filled area differs at 1"
1584 );
1585 }
1586 }
1587
1588 #[test]
1589 fn a_shape_morphed_into_itself_never_moves() {
1590 for shape in [triangle(), circle8(), rounded_square(), star5()] {
1591 let path = shape.to_path();
1592 let morph = Morph::new(shape.clone(), shape.clone());
1593 for step in 0..=10 {
1594 let progress = f64::from(step) / 10.0;
1595 assert_same_outline(
1596 &morph.path(progress),
1597 &path,
1598 1e-9,
1599 &format!("self-morph at {progress}"),
1600 );
1601 }
1602 }
1603 }
1604
1605 #[test]
1610 fn the_halfway_outline_matches_the_dart_goldens() {
1611 assert_cubics_match_golden(
1614 &Morph::new(circle8(), square()).as_cubics(0.5),
1615 &[
1616 [
1617 0.8369397662556435,
1618 -0.31634171618254486,
1619 0.9039799220852145,
1620 -0.2134180278540675,
1621 0.9583333333333333,
1622 -0.10670901392703358,
1623 1.0,
1624 3.0878077872387166e-16,
1625 ],
1626 [
1627 1.0,
1628 3.0878077872387166e-16,
1629 0.9583333333333335,
1630 0.10670901392703386,
1631 0.9039799220852147,
1632 0.21341802785406744,
1633 0.8369397662556439,
1634 0.3163417161825445,
1635 ],
1636 [
1637 0.8369397662556439,
1638 0.3163417161825445,
1639 0.7028594545965018,
1640 0.522189092839499,
1641 0.5221890928395,
1642 0.7028594545965007,
1643 0.3163417161825455,
1644 0.8369397662556429,
1645 ],
1646 [
1647 0.3163417161825455,
1648 0.836939766255643,
1649 0.21341802785406783,
1650 0.9039799220852143,
1651 0.10670901392703366,
1652 0.9583333333333334,
1653 -4.967397667182656e-16,
1654 1.0,
1655 ],
1656 [
1657 -4.967397667182656e-16,
1658 1.0,
1659 -0.10670901392703397,
1660 0.9583333333333336,
1661 -0.2134180278540674,
1662 0.9039799220852148,
1663 -0.31634171618254436,
1664 0.836939766255644,
1665 ],
1666 [
1667 -0.31634171618254436,
1668 0.8369397662556439,
1669 -0.522189092839499,
1670 0.7028594545965017,
1671 -0.7028594545965008,
1672 0.5221890928395001,
1673 -0.836939766255643,
1674 0.31634171618254536,
1675 ],
1676 [
1677 -0.8369397662556431,
1678 0.31634171618254536,
1679 -0.9039799220852144,
1680 0.21341802785406774,
1681 -0.9583333333333335,
1682 0.10670901392703358,
1683 -1.0,
1684 -5.563292174903757e-16,
1685 ],
1686 [
1687 -1.0,
1688 -5.563292174903757e-16,
1689 -0.9583333333333335,
1690 -0.10670901392703397,
1691 -0.9039799220852148,
1692 -0.21341802785406735,
1693 -0.836939766255644,
1694 -0.3163417161825443,
1695 ],
1696 [
1697 -0.8369397662556439,
1698 -0.3163417161825443,
1699 -0.7028594545965017,
1700 -0.522189092839499,
1701 -0.5221890928395001,
1702 -0.7028594545965007,
1703 -0.3163417161825455,
1704 -0.836939766255643,
1705 ],
1706 [
1707 -0.3163417161825455,
1708 -0.836939766255643,
1709 -0.213418027854068,
1710 -0.9039799220852143,
1711 -0.10670901392703391,
1712 -0.9583333333333334,
1713 1.3366554194093344e-16,
1714 -1.0,
1715 ],
1716 [
1717 1.3366554194093344e-16,
1718 -1.0,
1719 0.10670901392703383,
1720 -0.9583333333333334,
1721 0.2134180278540675,
1722 -0.9039799220852145,
1723 0.3163417161825447,
1724 -0.8369397662556435,
1725 ],
1726 [
1727 0.31634171618254475,
1728 -0.8369397662556435,
1729 0.5221890928394994,
1730 -0.7028594545965013,
1731 0.7028594545965011,
1732 -0.5221890928394997,
1733 0.8369397662556435,
1734 -0.31634171618254486,
1735 ],
1736 ],
1737 "circle -> square at 0.5",
1738 );
1739 }
1740
1741 #[test]
1742 fn morphing_across_differing_corner_counts_matches_the_dart_golden() {
1743 assert_cubics_match_golden(
1746 &Morph::new(triangle(), octagon()).as_cubics(0.5),
1747 &[
1748 [
1749 1.0,
1750 0.0,
1751 0.867851130197758,
1752 0.16596365263022672,
1753 0.7357022603955158,
1754 0.33192730526045344,
1755 0.6035533905932737,
1756 0.4978909578906803,
1757 ],
1758 [
1759 0.6035533905932737,
1760 0.4978909578906803,
1761 0.40236892706218247,
1762 0.5948190167920578,
1763 0.2011844635310912,
1764 0.6917470756934354,
1765 -1.1510060200336796e-16,
1766 0.788675134594813,
1767 ],
1768 [
1769 -1.1510060200336796e-16,
1770 0.788675134594813,
1771 -0.2011844635310913,
1772 0.7879721205583732,
1773 -0.4023689270621824,
1774 0.787269106521933,
1775 -0.6035533905932736,
1776 0.7865660924854931,
1777 ],
1778 [
1779 -0.6035533905932736,
1780 0.7865660924854931,
1781 -0.6523689270621824,
1782 0.5243773949903291,
1783 -0.7011844635310912,
1784 0.26218869749516494,
1785 -0.75,
1786 7.481828364441833e-16,
1787 ],
1788 [
1789 -0.75,
1790 7.481828364441833e-16,
1791 -0.7011844635310914,
1792 -0.2621886974951638,
1793 -0.6523689270621827,
1794 -0.5243773949903283,
1795 -0.6035533905932741,
1796 -0.7865660924854929,
1797 ],
1798 [
1799 -0.6035533905932741,
1800 -0.7865660924854929,
1801 -0.40236892706218286,
1802 -0.7872691065219329,
1803 -0.20118446353109162,
1804 -0.7879721205583728,
1805 -3.971598417079695e-16,
1806 -0.7886751345948129,
1807 ],
1808 [
1809 -3.971598417079695e-16,
1810 -0.7886751345948129,
1811 0.2011844635310909,
1812 -0.6917470756934354,
1813 0.4023689270621822,
1814 -0.5948190167920578,
1815 0.6035533905932735,
1816 -0.4978909578906803,
1817 ],
1818 [
1819 0.6035533905932735,
1820 -0.4978909578906803,
1821 0.7357022603955157,
1822 -0.33192730526045355,
1823 0.8678511301977578,
1824 -0.16596365263022678,
1825 1.0,
1826 0.0,
1827 ],
1828 ],
1829 "triangle -> octagon at 0.5",
1830 );
1831 }
1832
1833 #[test]
1834 fn morphing_a_shape_with_concave_corners_matches_the_dart_golden() {
1835 let cubics = Morph::new(star5(), circle8()).as_cubics(0.5);
1841 let golden = [
1842 (0.785330199718972, -0.2682354889504666),
1843 (0.785327440564177, 0.2682427047337134),
1844 (0.7729607641303604, 0.28460842940814013),
1845 (0.6552523839451216, 0.4157139686894965),
1846 (0.486649700963247, 0.6048840395563428),
1847 (0.37615440549992074, 0.7289173135928099),
1848 (0.36220709983559274, 0.7420407503987456),
1849 (-0.0776730888836798, 0.8221309185364376),
1850 (-0.19387659974157886, 0.7513254080021173),
1851 (-0.2726062554084669, 0.7120463007873984),
1852 (-0.35736358560669745, 0.6914924591704735),
1853 (-0.4885340974522692, 0.6713420091019238),
1854 (-0.7667404101680318, 0.31848023965843286),
1855 (-0.7683818377075402, 0.29870091587759084),
1856 (-0.7679462219911052, 0.12701645351874605),
1857 (-0.7679304833770698, -0.12711291114449055),
1858 (-0.7683818377075398, -0.29870091587759007),
1859 (-0.7667216169895471, -0.31852230735243764),
1860 (-0.48831593733694323, -0.6714922788437413),
1861 (-0.3571801349513464, -0.6915865496117093),
1862 (-0.27260625540846845, -0.7120463007873981),
1863 (-0.19368034855012445, -0.7513885952154867),
1864 (-0.07741256963330226, -0.8221789242799227),
1865 (0.3622501349712382, -0.7420242928309926),
1866 (0.37615440549992063, -0.7289173135928098),
1867 (0.4867290356852201, -0.6048269625957917),
1868 (0.6553094609056724, -0.415634633967524),
1869 (0.7729607641303599, -0.2846084294081404),
1870 ];
1871
1872 assert_eq!(cubics.len(), golden.len(), "curve count drifted");
1873 for (i, (cubic, (x, y))) in cubics.iter().zip(&golden).enumerate() {
1874 let anchor = cubic.anchor0();
1875 assert!(
1876 (anchor.x - x).abs() < GOLDEN_EPSILON && (anchor.y - y).abs() < GOLDEN_EPSILON,
1877 "curve {i}: expected start ({x}, {y}), got {anchor:?}"
1878 );
1879 }
1880 }
1881
1882 #[test]
1883 fn the_halfway_outline_is_closed_simple_and_inside_both_shapes() {
1884 let (start, end) = (circle8(), square());
1885 let union = Morph::new(start.clone(), end.clone()).bounds();
1886 let morph = Morph::new(start, end);
1887 let path = morph.path(0.5);
1888
1889 let elements = path.elements();
1892 let Some(PathEl::MoveTo(first)) = elements.first().copied() else {
1893 panic!("a path opens with a MoveTo");
1894 };
1895 assert!(matches!(elements.last(), Some(PathEl::ClosePath)));
1896 let curves = segments(&path);
1897 assert_eq!(
1898 curves[curves.len() - 1].p3,
1899 first,
1900 "the outline must close exactly"
1901 );
1902 for pair in curves.windows(2) {
1903 assert_eq!(pair[0].p3, pair[1].p0, "the outline must be contiguous");
1904 }
1905
1906 for curve in &curves {
1908 for i in 0..=8 {
1909 let point = curve.eval(f64::from(i) / 8.0);
1910 assert!(
1911 union.inflate(1e-9, 1e-9).contains(point),
1912 "{point:?} escapes the union bounds {union:?}"
1913 );
1914 }
1915 }
1916
1917 let mut inside = 0_u32;
1922 let mut sign = 0_i32;
1923 for row in 0..48 {
1924 for column in 0..48 {
1925 let point = Point::new(
1926 union.x0 + union.width() * (f64::from(column) + 0.5) / 48.0,
1927 union.y0 + union.height() * (f64::from(row) + 0.5) / 48.0,
1928 );
1929 let winding = path.winding(point);
1930 assert!(
1931 (-1..=1).contains(&winding),
1932 "winding {winding} at {point:?}: the outline crosses itself"
1933 );
1934 if winding != 0 {
1935 assert!(
1936 sign == 0 || sign == winding,
1937 "winding {winding} at {point:?} disagrees with {sign} elsewhere"
1938 );
1939 sign = winding;
1940 inside += 1;
1941 }
1942 }
1943 }
1944 assert!(inside > 1000, "only {inside} of 2304 samples landed inside");
1945 }
1946
1947 #[test]
1948 fn the_bounds_hold_both_shapes() {
1949 let morph = Morph::new(rounded_square(), circle8());
1950 let bounds = morph.bounds();
1951 for polygon in [morph.start(), morph.end()] {
1952 let shape = polygon.bounds();
1953 assert!(
1954 bounds.union(shape) == bounds,
1955 "{shape:?} escapes the union {bounds:?}"
1956 );
1957 }
1958 assert!(morph.exact_bounds().area() <= bounds.area());
1959 let max = morph.max_bounds();
1962 assert!(max.width() >= morph.exact_bounds().width() - EPSILON);
1963 assert_close(max.width(), max.height(), "max bounds is a square");
1964 }
1965
1966 #[test]
1967 fn a_hundred_samples_of_each_pair_are_well_formed() {
1968 for (name, start, end) in [
1969 ("triangle -> octagon", triangle(), octagon()),
1970 ("circle -> square", circle8(), square()),
1971 ("star -> rounded square", star5(), rounded_square()),
1972 ] {
1973 let morph = Morph::new(start, end);
1974 let curve_count = morph.as_cubics(0.0).len();
1975 for step in 0..100 {
1976 let progress = f64::from(step) / 99.0;
1977 let path = morph.path(progress);
1978 let curves = segments(&path);
1979 assert_eq!(curves.len(), curve_count, "{name} at {progress}");
1980 assert!(matches!(path.elements().last(), Some(PathEl::ClosePath)));
1981 for curve in &curves {
1982 for value in [curve.p0, curve.p1, curve.p2, curve.p3] {
1983 assert!(
1984 value.x.is_finite() && value.y.is_finite(),
1985 "{name} at {progress}: non-finite point {value:?}"
1986 );
1987 }
1988 }
1989 }
1990 }
1991 }
1992}