use std::f64::consts::{FRAC_PI_2, FRAC_PI_4, TAU};
use std::sync::OnceLock;
use kurbo::{Point, Vec2};
use super::rounded_polygon::{CornerRounding, RoundedPolygon, StarParams};
const CENTER: Point = Point::new(0.5, 0.5);
const ROTATE_NEG_45: f64 = -FRAC_PI_4;
const ROTATE_NEG_90: f64 = -FRAC_PI_2;
const ROTATE_NEG_135: f64 = -3.0 * FRAC_PI_4;
const ROUND_15: CornerRounding = CornerRounding {
radius: 0.15,
smoothing: 0.0,
};
const ROUND_20: CornerRounding = CornerRounding {
radius: 0.2,
smoothing: 0.0,
};
const ROUND_30: CornerRounding = CornerRounding {
radius: 0.3,
smoothing: 0.0,
};
const ROUND_50: CornerRounding = CornerRounding {
radius: 0.5,
smoothing: 0.0,
};
const ROUND_100: CornerRounding = CornerRounding {
radius: 1.0,
smoothing: 0.0,
};
#[derive(Clone, Copy)]
struct PointNRound {
point: Point,
rounding: CornerRounding,
}
fn pt(x: f64, y: f64) -> PointNRound {
PointNRound {
point: Point::new(x, y),
rounding: CornerRounding::UNROUNDED,
}
}
fn pt_r(x: f64, y: f64, rounding: CornerRounding) -> PointNRound {
PointNRound {
point: Point::new(x, y),
rounding,
}
}
fn custom_polygon(
points: &[PointNRound],
reps: usize,
center: Point,
mirroring: bool,
) -> RoundedPolygon {
let actual = repeat_points(points, reps, center, mirroring);
let vertices: Vec<Point> = actual.iter().map(|p| p.point).collect();
let rounding: Vec<CornerRounding> = actual.iter().map(|p| p.rounding).collect();
RoundedPolygon::from_vertices(
&vertices,
CornerRounding::UNROUNDED,
Some(&rounding),
Some(center),
)
}
fn repeat_points(
points: &[PointNRound],
reps: usize,
center: Point,
mirroring: bool,
) -> Vec<PointNRound> {
let mut result = Vec::new();
if mirroring {
let measures: Vec<(f64, f64)> = points
.iter()
.map(|p| {
let off = p.point - center;
(off.atan2(), off.hypot())
})
.collect();
let actual_reps = reps * 2;
let section_angle = TAU / actual_reps as f64;
for r in 0..actual_reps {
for index in 0..points.len() {
let i = if r % 2 == 0 {
index
} else {
points.len() - 1 - index
};
if i > 0 || r % 2 == 0 {
let angle = section_angle * r as f64
+ if r % 2 == 0 {
measures[i].0
} else {
section_angle - measures[i].0 + 2.0 * measures[0].0
};
let point = center + Vec2::new(angle.cos(), angle.sin()) * measures[i].1;
result.push(PointNRound {
point,
rounding: points[i].rounding,
});
}
}
}
} else {
let n = points.len();
for i in 0..(n * reps) {
let degrees = (i / n) as f64 * 360.0 / reps as f64;
let point = rotate_around(points[i % n].point, degrees.to_radians(), center);
result.push(PointNRound {
point,
rounding: points[i % n].rounding,
});
}
}
result
}
fn rotate_around(p: Point, radians: f64, center: Point) -> Point {
let off = p - center;
let (sin, cos) = radians.sin_cos();
center + Vec2::new(off.x * cos - off.y * sin, off.x * sin + off.y * cos)
}
fn rotate_z(radians: f64, p: Point) -> Point {
let (sin, cos) = radians.sin_cos();
Point::new(p.x * cos - p.y * sin, p.x * sin + p.y * cos)
}
fn scale(sx: f64, sy: f64, p: Point) -> Point {
Point::new(p.x * sx, p.y * sy)
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ShapeKind {
Circle,
Square,
Slanted,
Arch,
SemiCircle,
Oval,
Pill,
Triangle,
Arrow,
Fan,
Diamond,
ClamShell,
Pentagon,
Gem,
Sunny,
VerySunny,
Cookie4Sided,
Cookie6Sided,
Cookie7Sided,
Cookie9Sided,
Cookie12Sided,
Clover4Leaf,
Clover8Leaf,
Burst,
SoftBurst,
Boom,
SoftBoom,
Flower,
Puffy,
PuffyDiamond,
Ghostish,
PixelCircle,
PixelTriangle,
Bun,
Heart,
}
impl ShapeKind {
pub const ALL: [ShapeKind; 35] = [
ShapeKind::Circle,
ShapeKind::Square,
ShapeKind::Slanted,
ShapeKind::Arch,
ShapeKind::SemiCircle,
ShapeKind::Oval,
ShapeKind::Pill,
ShapeKind::Triangle,
ShapeKind::Arrow,
ShapeKind::Fan,
ShapeKind::Diamond,
ShapeKind::ClamShell,
ShapeKind::Pentagon,
ShapeKind::Gem,
ShapeKind::Sunny,
ShapeKind::VerySunny,
ShapeKind::Cookie4Sided,
ShapeKind::Cookie6Sided,
ShapeKind::Cookie7Sided,
ShapeKind::Cookie9Sided,
ShapeKind::Cookie12Sided,
ShapeKind::Clover4Leaf,
ShapeKind::Clover8Leaf,
ShapeKind::Burst,
ShapeKind::SoftBurst,
ShapeKind::Boom,
ShapeKind::SoftBoom,
ShapeKind::Flower,
ShapeKind::Puffy,
ShapeKind::PuffyDiamond,
ShapeKind::Ghostish,
ShapeKind::PixelCircle,
ShapeKind::PixelTriangle,
ShapeKind::Bun,
ShapeKind::Heart,
];
#[must_use]
pub fn polygon(&self) -> &'static RoundedPolygon {
match self {
ShapeKind::Circle => circle(),
ShapeKind::Square => square(),
ShapeKind::Slanted => slanted(),
ShapeKind::Arch => arch(),
ShapeKind::SemiCircle => semi_circle(),
ShapeKind::Oval => oval(),
ShapeKind::Pill => pill(),
ShapeKind::Triangle => triangle(),
ShapeKind::Arrow => arrow(),
ShapeKind::Fan => fan(),
ShapeKind::Diamond => diamond(),
ShapeKind::ClamShell => clam_shell(),
ShapeKind::Pentagon => pentagon(),
ShapeKind::Gem => gem(),
ShapeKind::Sunny => sunny(),
ShapeKind::VerySunny => very_sunny(),
ShapeKind::Cookie4Sided => cookie_4_sided(),
ShapeKind::Cookie6Sided => cookie_6_sided(),
ShapeKind::Cookie7Sided => cookie_7_sided(),
ShapeKind::Cookie9Sided => cookie_9_sided(),
ShapeKind::Cookie12Sided => cookie_12_sided(),
ShapeKind::Clover4Leaf => clover_4_leaf(),
ShapeKind::Clover8Leaf => clover_8_leaf(),
ShapeKind::Burst => burst(),
ShapeKind::SoftBurst => soft_burst(),
ShapeKind::Boom => boom(),
ShapeKind::SoftBoom => soft_boom(),
ShapeKind::Flower => flower(),
ShapeKind::Puffy => puffy(),
ShapeKind::PuffyDiamond => puffy_diamond(),
ShapeKind::Ghostish => ghostish(),
ShapeKind::PixelCircle => pixel_circle(),
ShapeKind::PixelTriangle => pixel_triangle(),
ShapeKind::Bun => bun(),
ShapeKind::Heart => heart(),
}
}
}
fn circle() -> &'static RoundedPolygon {
static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
CACHE.get_or_init(|| RoundedPolygon::circle(10, 0.5, CENTER))
}
fn square() -> &'static RoundedPolygon {
static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
CACHE.get_or_init(|| RoundedPolygon::rectangle(1.0, 1.0, ROUND_30, None, CENTER))
}
fn slanted() -> &'static RoundedPolygon {
static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
CACHE.get_or_init(|| {
custom_polygon(
&[
pt_r(0.926, 0.970, CornerRounding::new(0.189, 0.811)),
pt_r(-0.021, 0.967, CornerRounding::new(0.187, 0.057)),
],
2,
CENTER,
false,
)
.normalized()
})
}
fn arch() -> &'static RoundedPolygon {
static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
CACHE.get_or_init(|| {
RoundedPolygon::from_num_vertices(
4,
1.0,
Point::ZERO,
CornerRounding::UNROUNDED,
Some(&[ROUND_100, ROUND_100, ROUND_20, ROUND_20]),
)
.transformed_with(|p| rotate_z(ROTATE_NEG_135, p))
.normalized()
})
}
fn semi_circle() -> &'static RoundedPolygon {
static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
CACHE.get_or_init(|| {
RoundedPolygon::rectangle(
1.6,
1.0,
CornerRounding::UNROUNDED,
Some(&[ROUND_20, ROUND_20, ROUND_100, ROUND_100]),
Point::ZERO,
)
.normalized()
})
}
fn oval() -> &'static RoundedPolygon {
static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
CACHE.get_or_init(|| {
RoundedPolygon::circle(8, 1.0, Point::ZERO)
.transformed_with(|p| rotate_z(ROTATE_NEG_45, scale(1.0, 0.64, p)))
.normalized()
})
}
fn pill() -> &'static RoundedPolygon {
static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
CACHE.get_or_init(|| {
custom_polygon(
&[
pt_r(0.961, 0.039, CornerRounding::radius(0.426)),
pt(1.001, 0.428),
pt_r(1.0, 0.609, ROUND_100),
],
2,
CENTER,
true,
)
.normalized()
})
}
fn triangle() -> &'static RoundedPolygon {
static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
CACHE.get_or_init(|| {
RoundedPolygon::from_num_vertices(3, 1.0, Point::ZERO, ROUND_20, None)
.transformed_with(|p| rotate_z(ROTATE_NEG_90, p))
.normalized()
})
}
fn arrow() -> &'static RoundedPolygon {
static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
CACHE.get_or_init(|| {
custom_polygon(
&[
pt_r(0.5, 0.892, CornerRounding::radius(0.313)),
pt_r(-0.216, 1.05, CornerRounding::radius(0.207)),
pt_r(0.499, -0.16, CornerRounding::new(0.215, 1.0)),
pt_r(1.225, 1.06, CornerRounding::radius(0.211)),
],
1,
CENTER,
false,
)
.normalized()
})
}
fn fan() -> &'static RoundedPolygon {
static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
CACHE.get_or_init(|| {
custom_polygon(
&[
pt_r(1.004, 1.0, CornerRounding::new(0.148, 0.417)),
pt_r(0.0, 1.0, CornerRounding::radius(0.151)),
pt_r(0.0, -0.003, CornerRounding::radius(0.148)),
pt_r(0.978, 0.02, CornerRounding::radius(0.803)),
],
1,
CENTER,
false,
)
.normalized()
})
}
fn diamond() -> &'static RoundedPolygon {
static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
CACHE.get_or_init(|| {
custom_polygon(
&[
pt_r(0.5, 1.096, CornerRounding::new(0.151, 0.524)),
pt_r(0.04, 0.5, CornerRounding::radius(0.159)),
],
2,
CENTER,
false,
)
.normalized()
})
}
fn clam_shell() -> &'static RoundedPolygon {
static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
CACHE.get_or_init(|| {
custom_polygon(
&[
pt_r(0.171, 0.841, CornerRounding::radius(0.159)),
pt_r(-0.02, 0.5, CornerRounding::radius(0.140)),
pt_r(0.17, 0.159, CornerRounding::radius(0.159)),
],
2,
CENTER,
false,
)
.normalized()
})
}
fn pentagon() -> &'static RoundedPolygon {
static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
CACHE.get_or_init(|| {
custom_polygon(
&[
pt_r(0.5, -0.009, CornerRounding::radius(0.172)),
pt_r(1.03, 0.365, CornerRounding::radius(0.164)),
pt_r(0.828, 0.97, CornerRounding::radius(0.169)),
],
1,
CENTER,
true,
)
.normalized()
})
}
fn gem() -> &'static RoundedPolygon {
static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
CACHE.get_or_init(|| {
custom_polygon(
&[
pt_r(0.499, 1.023, CornerRounding::new(0.241, 0.778)),
pt_r(-0.005, 0.792, CornerRounding::radius(0.208)),
pt_r(0.073, 0.258, CornerRounding::radius(0.228)),
pt_r(0.433, 0.0, CornerRounding::radius(0.491)),
],
1,
CENTER,
true,
)
.normalized()
})
}
fn sunny() -> &'static RoundedPolygon {
static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
CACHE.get_or_init(|| {
RoundedPolygon::star(StarParams {
num_vertices_per_radius: 8,
inner_radius: 0.8,
rounding: ROUND_15,
..StarParams::default()
})
.normalized()
})
}
fn very_sunny() -> &'static RoundedPolygon {
static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
CACHE.get_or_init(|| {
custom_polygon(
&[
pt_r(0.5, 1.080, CornerRounding::radius(0.085)),
pt_r(0.358, 0.843, CornerRounding::radius(0.085)),
],
8,
CENTER,
false,
)
.normalized()
})
}
fn cookie_4_sided() -> &'static RoundedPolygon {
static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
CACHE.get_or_init(|| {
custom_polygon(
&[
pt_r(1.237, 1.236, CornerRounding::radius(0.258)),
pt_r(0.5, 0.918, CornerRounding::radius(0.233)),
],
4,
CENTER,
false,
)
.normalized()
})
}
fn cookie_6_sided() -> &'static RoundedPolygon {
static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
CACHE.get_or_init(|| {
custom_polygon(
&[
pt_r(0.723, 0.884, CornerRounding::radius(0.394)),
pt_r(0.5, 1.099, CornerRounding::radius(0.398)),
],
6,
CENTER,
false,
)
.normalized()
})
}
fn cookie_7_sided() -> &'static RoundedPolygon {
static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
CACHE.get_or_init(|| {
RoundedPolygon::star(StarParams {
num_vertices_per_radius: 7,
inner_radius: 0.75,
rounding: ROUND_50,
..StarParams::default()
})
.transformed_with(|p| rotate_z(ROTATE_NEG_90, p))
.normalized()
})
}
fn cookie_9_sided() -> &'static RoundedPolygon {
static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
CACHE.get_or_init(|| {
RoundedPolygon::star(StarParams {
num_vertices_per_radius: 9,
inner_radius: 0.8,
rounding: ROUND_50,
..StarParams::default()
})
.transformed_with(|p| rotate_z(ROTATE_NEG_90, p))
.normalized()
})
}
fn cookie_12_sided() -> &'static RoundedPolygon {
static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
CACHE.get_or_init(|| {
RoundedPolygon::star(StarParams {
num_vertices_per_radius: 12,
inner_radius: 0.8,
rounding: ROUND_50,
..StarParams::default()
})
.transformed_with(|p| rotate_z(ROTATE_NEG_90, p))
.normalized()
})
}
fn clover_4_leaf() -> &'static RoundedPolygon {
static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
CACHE.get_or_init(|| {
custom_polygon(
&[
pt(0.5, 0.074),
pt_r(0.725, -0.099, CornerRounding::radius(0.476)),
],
4,
CENTER,
true,
)
.normalized()
})
}
fn clover_8_leaf() -> &'static RoundedPolygon {
static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
CACHE.get_or_init(|| {
custom_polygon(
&[
pt(0.5, 0.036),
pt_r(0.758, -0.101, CornerRounding::radius(0.209)),
],
8,
CENTER,
false,
)
.normalized()
})
}
fn burst() -> &'static RoundedPolygon {
static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
CACHE.get_or_init(|| {
custom_polygon(
&[
pt_r(0.5, -0.006, CornerRounding::radius(0.006)),
pt_r(0.592, 0.158, CornerRounding::radius(0.006)),
],
12,
CENTER,
false,
)
.normalized()
})
}
fn soft_burst() -> &'static RoundedPolygon {
static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
CACHE.get_or_init(|| {
custom_polygon(
&[
pt_r(0.193, 0.277, CornerRounding::radius(0.053)),
pt_r(0.176, 0.055, CornerRounding::radius(0.053)),
],
10,
CENTER,
false,
)
.normalized()
})
}
fn boom() -> &'static RoundedPolygon {
static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
CACHE.get_or_init(|| {
custom_polygon(
&[
pt_r(0.457, 0.296, CornerRounding::radius(0.007)),
pt_r(0.5, -0.051, CornerRounding::radius(0.007)),
],
15,
CENTER,
false,
)
.normalized()
})
}
fn soft_boom() -> &'static RoundedPolygon {
static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
CACHE.get_or_init(|| {
custom_polygon(
&[
pt(0.733, 0.454),
pt_r(0.839, 0.437, CornerRounding::radius(0.532)),
pt_r(0.949, 0.449, CornerRounding::new(0.439, 1.0)),
pt_r(0.998, 0.478, CornerRounding::radius(0.174)),
],
16,
CENTER,
true,
)
.normalized()
})
}
fn flower() -> &'static RoundedPolygon {
static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
CACHE.get_or_init(|| {
custom_polygon(
&[
pt(0.370, 0.187),
pt_r(0.416, 0.049, CornerRounding::radius(0.381)),
pt_r(0.479, 0.001, CornerRounding::radius(0.095)),
],
8,
CENTER,
true,
)
.normalized()
})
}
fn puffy() -> &'static RoundedPolygon {
static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
CACHE.get_or_init(|| {
custom_polygon(
&[
pt(0.5, 0.053),
pt_r(0.545, -0.04, CornerRounding::radius(0.405)),
pt_r(0.670, -0.035, CornerRounding::radius(0.426)),
pt_r(0.717, 0.066, CornerRounding::radius(0.574)),
pt(0.722, 0.128),
pt_r(0.777, 0.002, CornerRounding::radius(0.36)),
pt_r(0.914, 0.149, CornerRounding::radius(0.66)),
pt_r(0.926, 0.289, CornerRounding::radius(0.66)),
pt(0.881, 0.346),
pt_r(0.940, 0.344, CornerRounding::radius(0.126)),
pt_r(1.003, 0.437, CornerRounding::radius(0.255)),
],
2,
CENTER,
true,
)
.transformed_with(|p| scale(1.0, 0.742, p))
.normalized()
})
}
fn puffy_diamond() -> &'static RoundedPolygon {
static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
CACHE.get_or_init(|| {
custom_polygon(
&[
pt_r(0.87, 0.13, CornerRounding::radius(0.146)),
pt(0.818, 0.357),
pt_r(1.0, 0.332, CornerRounding::radius(0.853)),
],
4,
CENTER,
true,
)
.normalized()
})
}
fn ghostish() -> &'static RoundedPolygon {
static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
CACHE.get_or_init(|| {
custom_polygon(
&[
pt_r(0.5, 0.0, ROUND_100),
pt_r(1.0, 0.0, ROUND_100),
pt_r(1.0, 1.14, CornerRounding::new(0.254, 0.106)),
pt_r(0.575, 0.906, CornerRounding::radius(0.253)),
],
1,
CENTER,
true,
)
.normalized()
})
}
fn pixel_circle() -> &'static RoundedPolygon {
static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
CACHE.get_or_init(|| {
custom_polygon(
&[
pt(0.5, 0.0),
pt(0.704, 0.0),
pt(0.704, 0.065),
pt(0.843, 0.065),
pt(0.843, 0.148),
pt(0.926, 0.148),
pt(0.926, 0.296),
pt(1.0, 0.296),
],
2,
CENTER,
true,
)
.normalized()
})
}
fn pixel_triangle() -> &'static RoundedPolygon {
static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
CACHE.get_or_init(|| {
custom_polygon(
&[
pt(0.11, 0.5),
pt(0.113, 0.0),
pt(0.287, 0.0),
pt(0.287, 0.087),
pt(0.421, 0.087),
pt(0.421, 0.17),
pt(0.56, 0.17),
pt(0.56, 0.265),
pt(0.674, 0.265),
pt(0.675, 0.344),
pt(0.789, 0.344),
pt(0.789, 0.439),
pt(0.888, 0.439),
],
1,
CENTER,
true,
)
.normalized()
})
}
fn bun() -> &'static RoundedPolygon {
static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
CACHE.get_or_init(|| {
custom_polygon(
&[
pt(0.796, 0.5),
pt_r(0.853, 0.518, ROUND_100),
pt_r(0.992, 0.631, ROUND_100),
pt_r(0.968, 1.0, ROUND_100),
],
2,
CENTER,
true,
)
.normalized()
})
}
fn heart() -> &'static RoundedPolygon {
static CACHE: OnceLock<RoundedPolygon> = OnceLock::new();
CACHE.get_or_init(|| {
custom_polygon(
&[
pt_r(0.5, 0.268, CornerRounding::radius(0.016)),
pt_r(0.792, -0.066, CornerRounding::radius(0.958)),
pt_r(1.064, 0.276, ROUND_100),
pt_r(0.501, 0.946, CornerRounding::radius(0.129)),
],
1,
CENTER,
true,
)
.normalized()
})
}
#[cfg(test)]
mod tests {
use super::*;
use kurbo::{CubicBez, ParamCurveNearest, PathEl};
#[test]
fn all_has_every_variant_exactly_once() {
assert_eq!(ShapeKind::ALL.len(), 35, "35-shape catalog count drifted");
let mut seen = std::collections::HashSet::new();
for kind in ShapeKind::ALL {
assert!(seen.insert(kind), "{kind:?} listed more than once");
}
}
#[test]
fn every_shape_is_a_closed_non_empty_outline() {
for kind in ShapeKind::ALL {
let polygon = kind.polygon();
assert!(!polygon.cubics().is_empty(), "{kind:?} has no cubics");
let path = polygon.to_path();
let elements: Vec<PathEl> = path.elements().to_vec();
let Some(PathEl::MoveTo(start)) = elements.first().copied() else {
panic!("{kind:?}: path must open with a MoveTo");
};
assert!(
matches!(elements.last(), Some(PathEl::ClosePath)),
"{kind:?}: path must end with a ClosePath"
);
let curves: Vec<PathEl> = elements
.iter()
.copied()
.filter(|el| matches!(el, PathEl::CurveTo(..)))
.collect();
assert!(!curves.is_empty(), "{kind:?}: path has no curves");
let Some(PathEl::CurveTo(_, _, end)) = curves.last().copied() else {
unreachable!("filtered to CurveTo above");
};
assert_eq!(end, start, "{kind:?}: outline does not close exactly");
}
}
#[test]
fn polygon_accessor_is_cached() {
for kind in ShapeKind::ALL {
let a = kind.polygon();
let b = kind.polygon();
assert!(
std::ptr::eq(a, b),
"{kind:?}: polygon() returned a different instance on repeat call"
);
}
}
#[test]
fn parameter_fidelity_spot_checks() {
assert_eq!(ShapeKind::Sunny.polygon().features().len(), 32);
assert_eq!(ShapeKind::Cookie9Sided.polygon().features().len(), 36);
assert_eq!(ShapeKind::Clover4Leaf.polygon().features().len(), 24);
assert_eq!(ShapeKind::Heart.polygon().features().len(), 14);
assert_eq!(ShapeKind::Pentagon.polygon().features().len(), 10);
}
#[test]
fn cookie_9_sided_has_nine_fold_rotational_symmetry() {
let cookie = ShapeKind::Cookie9Sided.polygon();
let curves: Vec<CubicBez> = cookie
.cubics()
.iter()
.map(|c| CubicBez::new(c.anchor0(), c.control0(), c.control1(), c.anchor1()))
.collect();
let center = cookie.center();
let ninth_turn = TAU / 9.0;
for cubic in cookie.cubics() {
for i in 0..4 {
let sample = cubic.point_on_curve(f64::from(i) / 4.0);
let rotated = rotate_around(sample, ninth_turn, center);
let nearest = curves
.iter()
.map(|curve| curve.nearest(rotated, 1e-12).distance_sq)
.fold(f64::INFINITY, f64::min)
.sqrt();
assert!(nearest < 1e-8, "{rotated:?} is {nearest} off the outline");
}
}
}
}