pub fn get_shape_coordinates(name: &str) -> Option<Vec<Vec<(f64, f64)>>> {
match name.to_lowercase().as_str() {
"circle" => Some(circle_coords()),
"square" => Some(square_coords()),
"diamond" => Some(diamond_coords()),
"triangle-up" => Some(triangle_up_coords()),
"triangle-down" => Some(triangle_down_coords()),
"star" => Some(star_coords()),
"cross" => Some(cross_coords()),
"plus" => Some(plus_coords()),
"hline" => Some(hline_coords()),
"vline" => Some(vline_coords()),
"asterisk" => Some(asterisk_coords()),
"bowtie" => Some(bowtie_coords()),
"square-cross" => Some(square_cross_coords()),
"circle-plus" => Some(circle_plus_coords()),
"square-plus" => Some(square_plus_coords()),
_ => None,
}
}
pub fn shape_to_svg_path(name: &str) -> Option<String> {
let paths = get_shape_coordinates(name)?;
let svg_paths: Vec<String> = paths
.iter()
.map(|path| {
let mut svg = String::new();
for (i, &(x, y)) in path.iter().enumerate() {
let cmd = if i == 0 { "M" } else { "L" };
svg.push_str(&format!("{}{:.3},{:.3} ", cmd, x, y));
}
if path.len() >= 3 {
svg.push('Z');
}
svg.trim().to_string()
})
.collect();
Some(svg_paths.join(" "))
}
fn circle_coords() -> Vec<Vec<(f64, f64)>> {
let n = 32;
let radius = 0.8;
let points: Vec<(f64, f64)> = (0..n)
.map(|i| {
let angle = 2.0 * std::f64::consts::PI * (i as f64) / (n as f64);
(radius * angle.cos(), radius * angle.sin())
})
.collect();
vec![points]
}
fn square_coords() -> Vec<Vec<(f64, f64)>> {
let s = 0.71; vec![vec![(-s, -s), (s, -s), (s, s), (-s, s)]]
}
fn diamond_coords() -> Vec<Vec<(f64, f64)>> {
let d = 0.89; vec![vec![(0.0, -d), (d, 0.0), (0.0, d), (-d, 0.0)]]
}
fn triangle_up_coords() -> Vec<Vec<(f64, f64)>> {
let r = 0.92;
let h = r * 0.75; vec![vec![(0.0, -r), (r, h), (-r, h)]]
}
fn triangle_down_coords() -> Vec<Vec<(f64, f64)>> {
let r = 0.92;
let h = r * 0.75;
vec![vec![(-r, -h), (r, -h), (0.0, r)]]
}
fn star_coords() -> Vec<Vec<(f64, f64)>> {
let outer_radius = 0.95; let inner_radius = outer_radius * 0.4; let points: Vec<(f64, f64)> = (0..10)
.map(|i| {
let angle = -std::f64::consts::PI / 2.0 + std::f64::consts::PI * (i as f64) / 5.0;
let radius = if i % 2 == 0 {
outer_radius
} else {
inner_radius
};
(radius * angle.cos(), radius * angle.sin())
})
.collect();
vec![points]
}
fn cross_coords() -> Vec<Vec<(f64, f64)>> {
let c = 0.8 / std::f64::consts::SQRT_2;
vec![
vec![(-c, -c), (c, c)], vec![(-c, c), (c, -c)], ]
}
fn plus_coords() -> Vec<Vec<(f64, f64)>> {
vec![
vec![(-0.8, 0.0), (0.8, 0.0)], vec![(0.0, -0.8), (0.0, 0.8)], ]
}
fn hline_coords() -> Vec<Vec<(f64, f64)>> {
vec![vec![(-0.8, 0.0), (0.8, 0.0)]]
}
fn vline_coords() -> Vec<Vec<(f64, f64)>> {
vec![vec![(0.0, -0.8), (0.0, 0.8)]]
}
fn asterisk_coords() -> Vec<Vec<(f64, f64)>> {
let r: f64 = 0.8;
(0..3)
.map(|i| {
let angle = (i as f64) * std::f64::consts::PI / 3.0;
let (sin, cos) = angle.sin_cos();
vec![(-r * cos, -r * sin), (r * cos, r * sin)]
})
.collect()
}
fn bowtie_coords() -> Vec<Vec<(f64, f64)>> {
vec![
vec![(-0.8, -0.8), (0.0, 0.0), (-0.8, 0.8)], vec![(0.8, -0.8), (0.0, 0.0), (0.8, 0.8)], ]
}
fn square_cross_coords() -> Vec<Vec<(f64, f64)>> {
let s = 0.71; let g = 0.12;
vec![
vec![(-s + g, -s), (s - g, -s), (0.0, -g)],
vec![(s, -s + g), (s, s - g), (g, 0.0)],
vec![(s - g, s), (-s + g, s), (0.0, g)],
vec![(-s, s - g), (-s, -s + g), (-g, 0.0)],
]
}
fn circle_plus_coords() -> Vec<Vec<(f64, f64)>> {
let r: f64 = 0.8; let g: f64 = 0.12 / std::f64::consts::SQRT_2; let n = 8;
let edge = (r * r - g * g).sqrt();
let start_angle = (g / r).asin(); let end_angle = std::f64::consts::FRAC_PI_2 - start_angle;
let mut quarters = Vec::new();
for q in 0..4 {
let base_angle = (q as f64) * std::f64::consts::FRAC_PI_2;
let mut points = Vec::new();
let (cx, cy) = match q {
0 => (g, g), 1 => (-g, g), 2 => (-g, -g), _ => (g, -g), };
points.push((cx, cy));
let (sx, sy) = match q {
0 => (edge, g), 1 => (-g, edge), 2 => (-edge, -g), _ => (g, -edge), };
points.push((sx, sy));
let arc_start = base_angle + start_angle;
let arc_span = end_angle - start_angle;
for i in 0..=n {
let t = (i as f64) / (n as f64);
let angle = arc_start + t * arc_span;
points.push((r * angle.cos(), r * angle.sin()));
}
let (ex, ey) = match q {
0 => (g, edge), 1 => (-edge, g), 2 => (-g, -edge), _ => (edge, -g), };
points.push((ex, ey));
quarters.push(points);
}
quarters
}
fn square_plus_coords() -> Vec<Vec<(f64, f64)>> {
let s = 0.71; let g = 0.12 / std::f64::consts::SQRT_2;
vec![
vec![(-s, -s), (-g, -s), (-g, -g), (-s, -g)],
vec![(g, -s), (s, -s), (s, -g), (g, -g)],
vec![(g, g), (s, g), (s, s), (g, s)],
vec![(-s, g), (-g, g), (-g, s), (-s, s)],
]
}
#[cfg(test)]
mod tests {
use super::{get_shape_coordinates, shape_to_svg_path};
use crate::plot::palettes::SHAPES;
#[test]
fn test_get_shape_coordinates_simple_shapes() {
assert_eq!(get_shape_coordinates("circle").unwrap().len(), 1);
assert_eq!(get_shape_coordinates("square").unwrap().len(), 1);
assert_eq!(get_shape_coordinates("diamond").unwrap().len(), 1);
assert_eq!(get_shape_coordinates("triangle-up").unwrap().len(), 1);
assert_eq!(get_shape_coordinates("triangle-down").unwrap().len(), 1);
assert_eq!(get_shape_coordinates("star").unwrap().len(), 1);
}
#[test]
fn test_get_shape_coordinates_open_shapes() {
assert!(get_shape_coordinates("cross").is_some());
assert!(get_shape_coordinates("plus").is_some());
assert!(get_shape_coordinates("hline").is_some());
assert!(get_shape_coordinates("vline").is_some());
assert!(get_shape_coordinates("asterisk").is_some());
assert!(get_shape_coordinates("bowtie").is_some());
}
#[test]
fn test_get_shape_coordinates_composite_shapes() {
let sq_cross = get_shape_coordinates("square-cross").unwrap();
assert!(
sq_cross.len() > 1,
"square-cross should have multiple paths"
);
let circ_plus = get_shape_coordinates("circle-plus").unwrap();
assert!(
circ_plus.len() > 1,
"circle-plus should have multiple paths"
);
let sq_plus = get_shape_coordinates("square-plus").unwrap();
assert!(sq_plus.len() > 1, "square-plus should have multiple paths");
}
#[test]
fn test_get_shape_coordinates_all_shapes_supported() {
for shape in SHAPES.iter() {
assert!(
get_shape_coordinates(shape).is_some(),
"Shape '{}' should have coordinates",
shape
);
}
}
#[test]
fn test_get_shape_coordinates_normalized() {
for shape in SHAPES.iter() {
if let Some(paths) = get_shape_coordinates(shape) {
for path in &paths {
for &(x, y) in path {
assert!((-1.0..=1.0).contains(&x), "{} x={} out of range", shape, x);
assert!((-1.0..=1.0).contains(&y), "{} y={} out of range", shape, y);
}
}
}
}
}
#[test]
fn test_get_shape_coordinates_unknown() {
assert!(get_shape_coordinates("unknown_shape").is_none());
}
#[test]
fn test_get_shape_coordinates_case_insensitive() {
assert!(get_shape_coordinates("CIRCLE").is_some());
assert!(get_shape_coordinates("Square").is_some());
assert!(get_shape_coordinates("TRIANGLE-UP").is_some());
}
#[test]
fn test_shape_to_svg_path_square() {
let path = shape_to_svg_path("square").unwrap();
assert!(path.starts_with('M'));
assert!(path.contains('L'));
assert!(path.ends_with('Z'));
}
#[test]
fn test_shape_to_svg_path_all_shapes() {
for shape in SHAPES.iter() {
assert!(
shape_to_svg_path(shape).is_some(),
"Shape '{}' should produce SVG path",
shape
);
}
}
#[test]
fn test_shape_to_svg_path_unknown() {
assert!(shape_to_svg_path("unknown").is_none());
}
#[test]
fn test_shape_to_svg_path_composite() {
let path = shape_to_svg_path("square-cross").unwrap();
assert!(path.matches('M').count() > 1);
}
#[test]
fn test_shape_to_svg_path_open_shapes_not_closed() {
let hline = shape_to_svg_path("hline").unwrap();
assert!(!hline.ends_with('Z'));
let vline = shape_to_svg_path("vline").unwrap();
assert!(!vline.ends_with('Z'));
let cross = shape_to_svg_path("cross").unwrap();
assert!(!cross.ends_with('Z'));
let plus = shape_to_svg_path("plus").unwrap();
assert!(!plus.ends_with('Z'));
}
}