use super::line::fmt;
use std::f64::consts::PI;
pub struct ArcGenerator {
inner_radius: f64,
outer_radius: f64,
corner_radius: f64,
}
impl ArcGenerator {
pub fn new(inner_radius: f64, outer_radius: f64) -> Self {
Self {
inner_radius,
outer_radius,
corner_radius: 0.0,
}
}
pub fn corner_radius(mut self, r: f64) -> Self {
self.corner_radius = r;
self
}
pub fn generate(&self, start_angle: f64, end_angle: f64) -> String {
let delta = (end_angle - start_angle).abs();
if delta >= 2.0 * PI - 1e-6 {
return self.generate_full_circle(start_angle);
}
let outer_start = angle_to_point(start_angle, self.outer_radius);
let outer_end = angle_to_point(end_angle, self.outer_radius);
let large_arc = if delta > PI { 1 } else { 0 };
if self.inner_radius > 0.0 {
let inner_start = angle_to_point(start_angle, self.inner_radius);
let inner_end = angle_to_point(end_angle, self.inner_radius);
let inner_large_arc = large_arc;
format!(
"M{},{} A{},{} 0 {},1 {},{} L{},{} A{},{} 0 {},0 {},{} Z",
fmt(outer_start.0), fmt(outer_start.1),
fmt(self.outer_radius), fmt(self.outer_radius),
large_arc,
fmt(outer_end.0), fmt(outer_end.1),
fmt(inner_end.0), fmt(inner_end.1),
fmt(self.inner_radius), fmt(self.inner_radius),
inner_large_arc,
fmt(inner_start.0), fmt(inner_start.1),
)
} else {
format!(
"M{},{} A{},{} 0 {},1 {},{} L0,0 Z",
fmt(outer_start.0), fmt(outer_start.1),
fmt(self.outer_radius), fmt(self.outer_radius),
large_arc,
fmt(outer_end.0), fmt(outer_end.1),
)
}
}
fn generate_full_circle(&self, start_angle: f64) -> String {
let mid_angle = start_angle + PI;
let outer_start = angle_to_point(start_angle, self.outer_radius);
let outer_mid = angle_to_point(mid_angle, self.outer_radius);
if self.inner_radius > 0.0 {
let inner_start = angle_to_point(start_angle, self.inner_radius);
let inner_mid = angle_to_point(mid_angle, self.inner_radius);
format!(
"M{},{} A{},{} 0 1,1 {},{} A{},{} 0 1,1 {},{} M{},{} A{},{} 0 1,0 {},{} A{},{} 0 1,0 {},{}Z",
fmt(outer_start.0), fmt(outer_start.1),
fmt(self.outer_radius), fmt(self.outer_radius),
fmt(outer_mid.0), fmt(outer_mid.1),
fmt(self.outer_radius), fmt(self.outer_radius),
fmt(outer_start.0), fmt(outer_start.1),
fmt(inner_start.0), fmt(inner_start.1),
fmt(self.inner_radius), fmt(self.inner_radius),
fmt(inner_mid.0), fmt(inner_mid.1),
fmt(self.inner_radius), fmt(self.inner_radius),
fmt(inner_start.0), fmt(inner_start.1),
)
} else {
format!(
"M{},{} A{},{} 0 1,1 {},{} A{},{} 0 1,1 {},{}Z",
fmt(outer_start.0), fmt(outer_start.1),
fmt(self.outer_radius), fmt(self.outer_radius),
fmt(outer_mid.0), fmt(outer_mid.1),
fmt(self.outer_radius), fmt(self.outer_radius),
fmt(outer_start.0), fmt(outer_start.1),
)
}
}
}
fn angle_to_point(angle: f64, radius: f64) -> (f64, f64) {
(radius * angle.sin(), -radius * angle.cos())
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::FRAC_PI_2;
#[test]
fn arc_quarter_circle() {
let gen = ArcGenerator::new(0.0, 100.0);
let path = gen.generate(0.0, FRAC_PI_2);
assert!(path.starts_with("M"), "Path should start with M, got: {}", path);
assert!(path.contains("A"), "Path should contain A command, got: {}", path);
assert!(path.ends_with("Z"), "Path should end with Z, got: {}", path);
}
#[test]
fn arc_full_circle() {
let gen = ArcGenerator::new(0.0, 100.0);
let path = gen.generate(0.0, 2.0 * PI);
assert!(path.starts_with("M"), "Path should start with M, got: {}", path);
assert!(path.contains("A"), "Path should contain A command, got: {}", path);
let arc_count = path.matches("A").count();
assert!(arc_count >= 2, "Full circle should have at least 2 arc commands, got: {}", arc_count);
}
#[test]
fn arc_doughnut() {
let gen = ArcGenerator::new(50.0, 100.0);
let path = gen.generate(0.0, FRAC_PI_2);
let arc_count = path.matches("A").count();
assert!(arc_count >= 2, "Doughnut should have at least 2 arc commands, got: {} in path: {}", arc_count, path);
}
}