1use std::f64::consts::{FRAC_PI_2, FRAC_PI_4, TAU};
73use std::sync::OnceLock;
74
75use kurbo::{Point, Vec2};
76
77use super::rounded_polygon::{CornerRounding, RoundedPolygon, StarParams};
78
79const CENTER: Point = Point::new(0.5, 0.5);
83
84const ROTATE_NEG_45: f64 = -FRAC_PI_4;
87const ROTATE_NEG_90: f64 = -FRAC_PI_2;
88const ROTATE_NEG_135: f64 = -3.0 * FRAC_PI_4;
89
90const ROUND_15: CornerRounding = CornerRounding {
94 radius: 0.15,
95 smoothing: 0.0,
96};
97const ROUND_20: CornerRounding = CornerRounding {
98 radius: 0.2,
99 smoothing: 0.0,
100};
101const ROUND_30: CornerRounding = CornerRounding {
102 radius: 0.3,
103 smoothing: 0.0,
104};
105const ROUND_50: CornerRounding = CornerRounding {
106 radius: 0.5,
107 smoothing: 0.0,
108};
109const ROUND_100: CornerRounding = CornerRounding {
110 radius: 1.0,
111 smoothing: 0.0,
112};
113
114#[derive(Clone, Copy)]
121struct PointNRound {
122 point: Point,
123 rounding: CornerRounding,
124}
125
126fn pt(x: f64, y: f64) -> PointNRound {
128 PointNRound {
129 point: Point::new(x, y),
130 rounding: CornerRounding::UNROUNDED,
131 }
132}
133
134fn pt_r(x: f64, y: f64, rounding: CornerRounding) -> PointNRound {
136 PointNRound {
137 point: Point::new(x, y),
138 rounding,
139 }
140}
141
142fn custom_polygon(
146 points: &[PointNRound],
147 reps: usize,
148 center: Point,
149 mirroring: bool,
150) -> RoundedPolygon {
151 let actual = repeat_points(points, reps, center, mirroring);
152 let vertices: Vec<Point> = actual.iter().map(|p| p.point).collect();
153 let rounding: Vec<CornerRounding> = actual.iter().map(|p| p.rounding).collect();
154 RoundedPolygon::from_vertices(
155 &vertices,
156 CornerRounding::UNROUNDED,
157 Some(&rounding),
158 Some(center),
159 )
160}
161
162fn repeat_points(
172 points: &[PointNRound],
173 reps: usize,
174 center: Point,
175 mirroring: bool,
176) -> Vec<PointNRound> {
177 let mut result = Vec::new();
178
179 if mirroring {
180 let measures: Vec<(f64, f64)> = points
183 .iter()
184 .map(|p| {
185 let off = p.point - center;
186 (off.atan2(), off.hypot())
187 })
188 .collect();
189 let actual_reps = reps * 2;
190 let section_angle = TAU / actual_reps as f64;
191
192 for r in 0..actual_reps {
193 for index in 0..points.len() {
194 let i = if r % 2 == 0 {
195 index
196 } else {
197 points.len() - 1 - index
198 };
199 if i > 0 || r % 2 == 0 {
202 let angle = section_angle * r as f64
203 + if r % 2 == 0 {
204 measures[i].0
205 } else {
206 section_angle - measures[i].0 + 2.0 * measures[0].0
207 };
208 let point = center + Vec2::new(angle.cos(), angle.sin()) * measures[i].1;
209 result.push(PointNRound {
210 point,
211 rounding: points[i].rounding,
212 });
213 }
214 }
215 }
216 } else {
217 let n = points.len();
218 for i in 0..(n * reps) {
219 let degrees = (i / n) as f64 * 360.0 / reps as f64;
220 let point = rotate_around(points[i % n].point, degrees.to_radians(), center);
221 result.push(PointNRound {
222 point,
223 rounding: points[i % n].rounding,
224 });
225 }
226 }
227
228 result
229}
230
231fn rotate_around(p: Point, radians: f64, center: Point) -> Point {
233 let off = p - center;
234 let (sin, cos) = radians.sin_cos();
235 center + Vec2::new(off.x * cos - off.y * sin, off.x * sin + off.y * cos)
236}
237
238fn rotate_z(radians: f64, p: Point) -> Point {
241 let (sin, cos) = radians.sin_cos();
242 Point::new(p.x * cos - p.y * sin, p.x * sin + p.y * cos)
243}
244
245fn scale(sx: f64, sy: f64, p: Point) -> Point {
250 Point::new(p.x * sx, p.y * sy)
251}
252
253#[non_exhaustive]
263#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
264pub enum ShapeKind {
265 Circle,
266 Square,
267 Slanted,
268 Arch,
269 SemiCircle,
270 Oval,
271 Pill,
272 Triangle,
273 Arrow,
274 Fan,
275 Diamond,
276 ClamShell,
277 Pentagon,
278 Gem,
279 Sunny,
280 VerySunny,
281 Cookie4Sided,
282 Cookie6Sided,
283 Cookie7Sided,
284 Cookie9Sided,
285 Cookie12Sided,
286 Clover4Leaf,
287 Clover8Leaf,
288 Burst,
289 SoftBurst,
290 Boom,
291 SoftBoom,
292 Flower,
293 Puffy,
294 PuffyDiamond,
295 Ghostish,
296 PixelCircle,
297 PixelTriangle,
298 Bun,
299 Heart,
300}
301
302impl ShapeKind {
303 pub const ALL: [ShapeKind; 35] = [
306 ShapeKind::Circle,
307 ShapeKind::Square,
308 ShapeKind::Slanted,
309 ShapeKind::Arch,
310 ShapeKind::SemiCircle,
311 ShapeKind::Oval,
312 ShapeKind::Pill,
313 ShapeKind::Triangle,
314 ShapeKind::Arrow,
315 ShapeKind::Fan,
316 ShapeKind::Diamond,
317 ShapeKind::ClamShell,
318 ShapeKind::Pentagon,
319 ShapeKind::Gem,
320 ShapeKind::Sunny,
321 ShapeKind::VerySunny,
322 ShapeKind::Cookie4Sided,
323 ShapeKind::Cookie6Sided,
324 ShapeKind::Cookie7Sided,
325 ShapeKind::Cookie9Sided,
326 ShapeKind::Cookie12Sided,
327 ShapeKind::Clover4Leaf,
328 ShapeKind::Clover8Leaf,
329 ShapeKind::Burst,
330 ShapeKind::SoftBurst,
331 ShapeKind::Boom,
332 ShapeKind::SoftBoom,
333 ShapeKind::Flower,
334 ShapeKind::Puffy,
335 ShapeKind::PuffyDiamond,
336 ShapeKind::Ghostish,
337 ShapeKind::PixelCircle,
338 ShapeKind::PixelTriangle,
339 ShapeKind::Bun,
340 ShapeKind::Heart,
341 ];
342
343 #[must_use]
348 pub fn polygon(&self) -> &'static RoundedPolygon {
349 match self {
350 ShapeKind::Circle => circle(),
351 ShapeKind::Square => square(),
352 ShapeKind::Slanted => slanted(),
353 ShapeKind::Arch => arch(),
354 ShapeKind::SemiCircle => semi_circle(),
355 ShapeKind::Oval => oval(),
356 ShapeKind::Pill => pill(),
357 ShapeKind::Triangle => triangle(),
358 ShapeKind::Arrow => arrow(),
359 ShapeKind::Fan => fan(),
360 ShapeKind::Diamond => diamond(),
361 ShapeKind::ClamShell => clam_shell(),
362 ShapeKind::Pentagon => pentagon(),
363 ShapeKind::Gem => gem(),
364 ShapeKind::Sunny => sunny(),
365 ShapeKind::VerySunny => very_sunny(),
366 ShapeKind::Cookie4Sided => cookie_4_sided(),
367 ShapeKind::Cookie6Sided => cookie_6_sided(),
368 ShapeKind::Cookie7Sided => cookie_7_sided(),
369 ShapeKind::Cookie9Sided => cookie_9_sided(),
370 ShapeKind::Cookie12Sided => cookie_12_sided(),
371 ShapeKind::Clover4Leaf => clover_4_leaf(),
372 ShapeKind::Clover8Leaf => clover_8_leaf(),
373 ShapeKind::Burst => burst(),
374 ShapeKind::SoftBurst => soft_burst(),
375 ShapeKind::Boom => boom(),
376 ShapeKind::SoftBoom => soft_boom(),
377 ShapeKind::Flower => flower(),
378 ShapeKind::Puffy => puffy(),
379 ShapeKind::PuffyDiamond => puffy_diamond(),
380 ShapeKind::Ghostish => ghostish(),
381 ShapeKind::PixelCircle => pixel_circle(),
382 ShapeKind::PixelTriangle => pixel_triangle(),
383 ShapeKind::Bun => bun(),
384 ShapeKind::Heart => heart(),
385 }
386 }
387}
388
389fn circle() -> &'static RoundedPolygon {
395 static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
396 CACHE.get_or_init(|| RoundedPolygon::circle(10, 0.5, CENTER))
397}
398
399fn square() -> &'static RoundedPolygon {
400 static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
401 CACHE.get_or_init(|| RoundedPolygon::rectangle(1.0, 1.0, ROUND_30, None, CENTER))
402}
403
404fn slanted() -> &'static RoundedPolygon {
405 static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
406 CACHE.get_or_init(|| {
407 custom_polygon(
408 &[
409 pt_r(0.926, 0.970, CornerRounding::new(0.189, 0.811)),
410 pt_r(-0.021, 0.967, CornerRounding::new(0.187, 0.057)),
411 ],
412 2,
413 CENTER,
414 false,
415 )
416 .normalized()
417 })
418}
419
420fn arch() -> &'static RoundedPolygon {
421 static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
422 CACHE.get_or_init(|| {
423 RoundedPolygon::from_num_vertices(
424 4,
425 1.0,
426 Point::ZERO,
427 CornerRounding::UNROUNDED,
428 Some(&[ROUND_100, ROUND_100, ROUND_20, ROUND_20]),
429 )
430 .transformed_with(|p| rotate_z(ROTATE_NEG_135, p))
431 .normalized()
432 })
433}
434
435fn semi_circle() -> &'static RoundedPolygon {
436 static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
437 CACHE.get_or_init(|| {
438 RoundedPolygon::rectangle(
439 1.6,
440 1.0,
441 CornerRounding::UNROUNDED,
442 Some(&[ROUND_20, ROUND_20, ROUND_100, ROUND_100]),
443 Point::ZERO,
444 )
445 .normalized()
446 })
447}
448
449fn oval() -> &'static RoundedPolygon {
450 static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
451 CACHE.get_or_init(|| {
452 RoundedPolygon::circle(8, 1.0, Point::ZERO)
453 .transformed_with(|p| rotate_z(ROTATE_NEG_45, scale(1.0, 0.64, p)))
454 .normalized()
455 })
456}
457
458fn pill() -> &'static RoundedPolygon {
459 static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
460 CACHE.get_or_init(|| {
461 custom_polygon(
462 &[
463 pt_r(0.961, 0.039, CornerRounding::radius(0.426)),
464 pt(1.001, 0.428),
465 pt_r(1.0, 0.609, ROUND_100),
466 ],
467 2,
468 CENTER,
469 true,
470 )
471 .normalized()
472 })
473}
474
475fn triangle() -> &'static RoundedPolygon {
476 static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
477 CACHE.get_or_init(|| {
478 RoundedPolygon::from_num_vertices(3, 1.0, Point::ZERO, ROUND_20, None)
479 .transformed_with(|p| rotate_z(ROTATE_NEG_90, p))
480 .normalized()
481 })
482}
483
484fn arrow() -> &'static RoundedPolygon {
485 static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
486 CACHE.get_or_init(|| {
487 custom_polygon(
488 &[
489 pt_r(0.5, 0.892, CornerRounding::radius(0.313)),
490 pt_r(-0.216, 1.05, CornerRounding::radius(0.207)),
491 pt_r(0.499, -0.16, CornerRounding::new(0.215, 1.0)),
492 pt_r(1.225, 1.06, CornerRounding::radius(0.211)),
493 ],
494 1,
495 CENTER,
496 false,
497 )
498 .normalized()
499 })
500}
501
502fn fan() -> &'static RoundedPolygon {
503 static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
504 CACHE.get_or_init(|| {
505 custom_polygon(
506 &[
507 pt_r(1.004, 1.0, CornerRounding::new(0.148, 0.417)),
508 pt_r(0.0, 1.0, CornerRounding::radius(0.151)),
509 pt_r(0.0, -0.003, CornerRounding::radius(0.148)),
510 pt_r(0.978, 0.02, CornerRounding::radius(0.803)),
511 ],
512 1,
513 CENTER,
514 false,
515 )
516 .normalized()
517 })
518}
519
520fn diamond() -> &'static RoundedPolygon {
521 static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
522 CACHE.get_or_init(|| {
523 custom_polygon(
524 &[
525 pt_r(0.5, 1.096, CornerRounding::new(0.151, 0.524)),
526 pt_r(0.04, 0.5, CornerRounding::radius(0.159)),
527 ],
528 2,
529 CENTER,
530 false,
531 )
532 .normalized()
533 })
534}
535
536fn clam_shell() -> &'static RoundedPolygon {
537 static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
538 CACHE.get_or_init(|| {
539 custom_polygon(
540 &[
541 pt_r(0.171, 0.841, CornerRounding::radius(0.159)),
542 pt_r(-0.02, 0.5, CornerRounding::radius(0.140)),
543 pt_r(0.17, 0.159, CornerRounding::radius(0.159)),
544 ],
545 2,
546 CENTER,
547 false,
548 )
549 .normalized()
550 })
551}
552
553fn pentagon() -> &'static RoundedPolygon {
554 static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
555 CACHE.get_or_init(|| {
556 custom_polygon(
557 &[
558 pt_r(0.5, -0.009, CornerRounding::radius(0.172)),
559 pt_r(1.03, 0.365, CornerRounding::radius(0.164)),
560 pt_r(0.828, 0.97, CornerRounding::radius(0.169)),
561 ],
562 1,
563 CENTER,
564 true,
565 )
566 .normalized()
567 })
568}
569
570fn gem() -> &'static RoundedPolygon {
571 static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
572 CACHE.get_or_init(|| {
573 custom_polygon(
574 &[
575 pt_r(0.499, 1.023, CornerRounding::new(0.241, 0.778)),
576 pt_r(-0.005, 0.792, CornerRounding::radius(0.208)),
577 pt_r(0.073, 0.258, CornerRounding::radius(0.228)),
578 pt_r(0.433, 0.0, CornerRounding::radius(0.491)),
579 ],
580 1,
581 CENTER,
582 true,
583 )
584 .normalized()
585 })
586}
587
588fn sunny() -> &'static RoundedPolygon {
589 static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
590 CACHE.get_or_init(|| {
591 RoundedPolygon::star(StarParams {
592 num_vertices_per_radius: 8,
593 inner_radius: 0.8,
594 rounding: ROUND_15,
595 ..StarParams::default()
596 })
597 .normalized()
598 })
599}
600
601fn very_sunny() -> &'static RoundedPolygon {
602 static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
603 CACHE.get_or_init(|| {
604 custom_polygon(
605 &[
606 pt_r(0.5, 1.080, CornerRounding::radius(0.085)),
607 pt_r(0.358, 0.843, CornerRounding::radius(0.085)),
608 ],
609 8,
610 CENTER,
611 false,
612 )
613 .normalized()
614 })
615}
616
617fn cookie_4_sided() -> &'static RoundedPolygon {
618 static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
619 CACHE.get_or_init(|| {
620 custom_polygon(
621 &[
622 pt_r(1.237, 1.236, CornerRounding::radius(0.258)),
623 pt_r(0.5, 0.918, CornerRounding::radius(0.233)),
624 ],
625 4,
626 CENTER,
627 false,
628 )
629 .normalized()
630 })
631}
632
633fn cookie_6_sided() -> &'static RoundedPolygon {
634 static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
635 CACHE.get_or_init(|| {
636 custom_polygon(
637 &[
638 pt_r(0.723, 0.884, CornerRounding::radius(0.394)),
639 pt_r(0.5, 1.099, CornerRounding::radius(0.398)),
640 ],
641 6,
642 CENTER,
643 false,
644 )
645 .normalized()
646 })
647}
648
649fn cookie_7_sided() -> &'static RoundedPolygon {
650 static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
651 CACHE.get_or_init(|| {
652 RoundedPolygon::star(StarParams {
653 num_vertices_per_radius: 7,
654 inner_radius: 0.75,
655 rounding: ROUND_50,
656 ..StarParams::default()
657 })
658 .transformed_with(|p| rotate_z(ROTATE_NEG_90, p))
659 .normalized()
660 })
661}
662
663fn cookie_9_sided() -> &'static RoundedPolygon {
664 static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
665 CACHE.get_or_init(|| {
666 RoundedPolygon::star(StarParams {
667 num_vertices_per_radius: 9,
668 inner_radius: 0.8,
669 rounding: ROUND_50,
670 ..StarParams::default()
671 })
672 .transformed_with(|p| rotate_z(ROTATE_NEG_90, p))
673 .normalized()
674 })
675}
676
677fn cookie_12_sided() -> &'static RoundedPolygon {
678 static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
679 CACHE.get_or_init(|| {
680 RoundedPolygon::star(StarParams {
681 num_vertices_per_radius: 12,
682 inner_radius: 0.8,
683 rounding: ROUND_50,
684 ..StarParams::default()
685 })
686 .transformed_with(|p| rotate_z(ROTATE_NEG_90, p))
687 .normalized()
688 })
689}
690
691fn clover_4_leaf() -> &'static RoundedPolygon {
692 static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
693 CACHE.get_or_init(|| {
694 custom_polygon(
695 &[
696 pt(0.5, 0.074),
697 pt_r(0.725, -0.099, CornerRounding::radius(0.476)),
698 ],
699 4,
700 CENTER,
701 true,
702 )
703 .normalized()
704 })
705}
706
707fn clover_8_leaf() -> &'static RoundedPolygon {
708 static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
709 CACHE.get_or_init(|| {
710 custom_polygon(
711 &[
712 pt(0.5, 0.036),
713 pt_r(0.758, -0.101, CornerRounding::radius(0.209)),
714 ],
715 8,
716 CENTER,
717 false,
718 )
719 .normalized()
720 })
721}
722
723fn burst() -> &'static RoundedPolygon {
724 static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
725 CACHE.get_or_init(|| {
726 custom_polygon(
727 &[
728 pt_r(0.5, -0.006, CornerRounding::radius(0.006)),
729 pt_r(0.592, 0.158, CornerRounding::radius(0.006)),
730 ],
731 12,
732 CENTER,
733 false,
734 )
735 .normalized()
736 })
737}
738
739fn soft_burst() -> &'static RoundedPolygon {
740 static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
741 CACHE.get_or_init(|| {
742 custom_polygon(
743 &[
744 pt_r(0.193, 0.277, CornerRounding::radius(0.053)),
745 pt_r(0.176, 0.055, CornerRounding::radius(0.053)),
746 ],
747 10,
748 CENTER,
749 false,
750 )
751 .normalized()
752 })
753}
754
755fn boom() -> &'static RoundedPolygon {
756 static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
757 CACHE.get_or_init(|| {
758 custom_polygon(
759 &[
760 pt_r(0.457, 0.296, CornerRounding::radius(0.007)),
761 pt_r(0.5, -0.051, CornerRounding::radius(0.007)),
762 ],
763 15,
764 CENTER,
765 false,
766 )
767 .normalized()
768 })
769}
770
771fn soft_boom() -> &'static RoundedPolygon {
772 static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
773 CACHE.get_or_init(|| {
774 custom_polygon(
775 &[
776 pt(0.733, 0.454),
777 pt_r(0.839, 0.437, CornerRounding::radius(0.532)),
778 pt_r(0.949, 0.449, CornerRounding::new(0.439, 1.0)),
779 pt_r(0.998, 0.478, CornerRounding::radius(0.174)),
780 ],
781 16,
782 CENTER,
783 true,
784 )
785 .normalized()
786 })
787}
788
789fn flower() -> &'static RoundedPolygon {
790 static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
791 CACHE.get_or_init(|| {
792 custom_polygon(
793 &[
794 pt(0.370, 0.187),
795 pt_r(0.416, 0.049, CornerRounding::radius(0.381)),
796 pt_r(0.479, 0.001, CornerRounding::radius(0.095)),
797 ],
798 8,
799 CENTER,
800 true,
801 )
802 .normalized()
803 })
804}
805
806fn puffy() -> &'static RoundedPolygon {
807 static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
808 CACHE.get_or_init(|| {
809 custom_polygon(
810 &[
811 pt(0.5, 0.053),
812 pt_r(0.545, -0.04, CornerRounding::radius(0.405)),
813 pt_r(0.670, -0.035, CornerRounding::radius(0.426)),
814 pt_r(0.717, 0.066, CornerRounding::radius(0.574)),
815 pt(0.722, 0.128),
816 pt_r(0.777, 0.002, CornerRounding::radius(0.36)),
817 pt_r(0.914, 0.149, CornerRounding::radius(0.66)),
818 pt_r(0.926, 0.289, CornerRounding::radius(0.66)),
819 pt(0.881, 0.346),
820 pt_r(0.940, 0.344, CornerRounding::radius(0.126)),
821 pt_r(1.003, 0.437, CornerRounding::radius(0.255)),
822 ],
823 2,
824 CENTER,
825 true,
826 )
827 .transformed_with(|p| scale(1.0, 0.742, p))
828 .normalized()
829 })
830}
831
832fn puffy_diamond() -> &'static RoundedPolygon {
833 static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
834 CACHE.get_or_init(|| {
835 custom_polygon(
836 &[
837 pt_r(0.87, 0.13, CornerRounding::radius(0.146)),
838 pt(0.818, 0.357),
839 pt_r(1.0, 0.332, CornerRounding::radius(0.853)),
840 ],
841 4,
842 CENTER,
843 true,
844 )
845 .normalized()
846 })
847}
848
849fn ghostish() -> &'static RoundedPolygon {
850 static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
851 CACHE.get_or_init(|| {
852 custom_polygon(
853 &[
854 pt_r(0.5, 0.0, ROUND_100),
855 pt_r(1.0, 0.0, ROUND_100),
856 pt_r(1.0, 1.14, CornerRounding::new(0.254, 0.106)),
857 pt_r(0.575, 0.906, CornerRounding::radius(0.253)),
858 ],
859 1,
860 CENTER,
861 true,
862 )
863 .normalized()
864 })
865}
866
867fn pixel_circle() -> &'static RoundedPolygon {
868 static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
869 CACHE.get_or_init(|| {
870 custom_polygon(
871 &[
872 pt(0.5, 0.0),
873 pt(0.704, 0.0),
874 pt(0.704, 0.065),
875 pt(0.843, 0.065),
876 pt(0.843, 0.148),
877 pt(0.926, 0.148),
878 pt(0.926, 0.296),
879 pt(1.0, 0.296),
880 ],
881 2,
882 CENTER,
883 true,
884 )
885 .normalized()
886 })
887}
888
889fn pixel_triangle() -> &'static RoundedPolygon {
890 static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
891 CACHE.get_or_init(|| {
892 custom_polygon(
893 &[
894 pt(0.11, 0.5),
895 pt(0.113, 0.0),
896 pt(0.287, 0.0),
897 pt(0.287, 0.087),
898 pt(0.421, 0.087),
899 pt(0.421, 0.17),
900 pt(0.56, 0.17),
901 pt(0.56, 0.265),
902 pt(0.674, 0.265),
903 pt(0.675, 0.344),
904 pt(0.789, 0.344),
905 pt(0.789, 0.439),
906 pt(0.888, 0.439),
907 ],
908 1,
909 CENTER,
910 true,
911 )
912 .normalized()
913 })
914}
915
916fn bun() -> &'static RoundedPolygon {
917 static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
918 CACHE.get_or_init(|| {
919 custom_polygon(
920 &[
921 pt(0.796, 0.5),
922 pt_r(0.853, 0.518, ROUND_100),
923 pt_r(0.992, 0.631, ROUND_100),
924 pt_r(0.968, 1.0, ROUND_100),
925 ],
926 2,
927 CENTER,
928 true,
929 )
930 .normalized()
931 })
932}
933
934fn heart() -> &'static RoundedPolygon {
935 static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
936 CACHE.get_or_init(|| {
937 custom_polygon(
938 &[
939 pt_r(0.5, 0.268, CornerRounding::radius(0.016)),
940 pt_r(0.792, -0.066, CornerRounding::radius(0.958)),
941 pt_r(1.064, 0.276, ROUND_100),
942 pt_r(0.501, 0.946, CornerRounding::radius(0.129)),
943 ],
944 1,
945 CENTER,
946 true,
947 )
948 .normalized()
949 })
950}
951
952#[cfg(test)]
953mod tests {
954 use super::*;
955 use kurbo::{CubicBez, ParamCurveNearest, PathEl};
956
957 #[test]
958 fn all_has_every_variant_exactly_once() {
959 assert_eq!(ShapeKind::ALL.len(), 35, "35-shape catalog count drifted");
960 let mut seen = std::collections::HashSet::new();
961 for kind in ShapeKind::ALL {
962 assert!(seen.insert(kind), "{kind:?} listed more than once");
963 }
964 }
965
966 #[test]
967 fn every_shape_is_a_closed_non_empty_outline() {
968 for kind in ShapeKind::ALL {
969 let polygon = kind.polygon();
970 assert!(!polygon.cubics().is_empty(), "{kind:?} has no cubics");
971
972 let path = polygon.to_path();
973 let elements: Vec<PathEl> = path.elements().to_vec();
974 let Some(PathEl::MoveTo(start)) = elements.first().copied() else {
975 panic!("{kind:?}: path must open with a MoveTo");
976 };
977 assert!(
978 matches!(elements.last(), Some(PathEl::ClosePath)),
979 "{kind:?}: path must end with a ClosePath"
980 );
981
982 let curves: Vec<PathEl> = elements
983 .iter()
984 .copied()
985 .filter(|el| matches!(el, PathEl::CurveTo(..)))
986 .collect();
987 assert!(!curves.is_empty(), "{kind:?}: path has no curves");
988 let Some(PathEl::CurveTo(_, _, end)) = curves.last().copied() else {
989 unreachable!("filtered to CurveTo above");
990 };
991 assert_eq!(end, start, "{kind:?}: outline does not close exactly");
992 }
993 }
994
995 #[test]
996 fn polygon_accessor_is_cached() {
997 for kind in ShapeKind::ALL {
998 let a = kind.polygon();
999 let b = kind.polygon();
1000 assert!(
1001 std::ptr::eq(a, b),
1002 "{kind:?}: polygon() returned a different instance on repeat call"
1003 );
1004 }
1005 }
1006
1007 #[test]
1014 fn parameter_fidelity_spot_checks() {
1015 assert_eq!(ShapeKind::Sunny.polygon().features().len(), 32);
1017
1018 assert_eq!(ShapeKind::Cookie9Sided.polygon().features().len(), 36);
1022
1023 assert_eq!(ShapeKind::Clover4Leaf.polygon().features().len(), 24);
1026
1027 assert_eq!(ShapeKind::Heart.polygon().features().len(), 14);
1030
1031 assert_eq!(ShapeKind::Pentagon.polygon().features().len(), 10);
1034 }
1035
1036 #[test]
1044 fn cookie_9_sided_has_nine_fold_rotational_symmetry() {
1045 let cookie = ShapeKind::Cookie9Sided.polygon();
1046 let curves: Vec<CubicBez> = cookie
1047 .cubics()
1048 .iter()
1049 .map(|c| CubicBez::new(c.anchor0(), c.control0(), c.control1(), c.anchor1()))
1050 .collect();
1051 let center = cookie.center();
1052 let ninth_turn = TAU / 9.0;
1053
1054 for cubic in cookie.cubics() {
1055 for i in 0..4 {
1056 let sample = cubic.point_on_curve(f64::from(i) / 4.0);
1057 let rotated = rotate_around(sample, ninth_turn, center);
1058 let nearest = curves
1059 .iter()
1060 .map(|curve| curve.nearest(rotated, 1e-12).distance_sq)
1061 .fold(f64::INFINITY, f64::min)
1062 .sqrt();
1063 assert!(nearest < 1e-8, "{rotated:?} is {nearest} off the outline");
1064 }
1065 }
1066 }
1067}