1use super::line::fmt;
4use std::f64::consts::PI;
5
6pub struct ArcGenerator {
8 inner_radius: f64,
9 outer_radius: f64,
10 corner_radius: f64,
11}
12
13impl ArcGenerator {
14 pub fn new(inner_radius: f64, outer_radius: f64) -> Self {
17 Self {
18 inner_radius,
19 outer_radius,
20 corner_radius: 0.0,
21 }
22 }
23
24 pub fn corner_radius(mut self, r: f64) -> Self {
26 self.corner_radius = r;
27 self
28 }
29
30 pub fn generate(&self, start_angle: f64, end_angle: f64) -> String {
35 let delta = (end_angle - start_angle).abs();
36
37 if delta >= 2.0 * PI - 1e-6 {
39 return self.generate_full_circle(start_angle);
40 }
41
42 let outer_start = angle_to_point(start_angle, self.outer_radius);
43 let outer_end = angle_to_point(end_angle, self.outer_radius);
44 let large_arc = if delta > PI { 1 } else { 0 };
45
46 if self.inner_radius > 0.0 {
47 let inner_start = angle_to_point(start_angle, self.inner_radius);
49 let inner_end = angle_to_point(end_angle, self.inner_radius);
50 let inner_large_arc = large_arc;
52
53 format!(
54 "M{},{} A{},{} 0 {},1 {},{} L{},{} A{},{} 0 {},0 {},{} Z",
55 fmt(outer_start.0), fmt(outer_start.1),
56 fmt(self.outer_radius), fmt(self.outer_radius),
57 large_arc,
58 fmt(outer_end.0), fmt(outer_end.1),
59 fmt(inner_end.0), fmt(inner_end.1),
60 fmt(self.inner_radius), fmt(self.inner_radius),
61 inner_large_arc,
62 fmt(inner_start.0), fmt(inner_start.1),
63 )
64 } else {
65 format!(
67 "M{},{} A{},{} 0 {},1 {},{} L0,0 Z",
68 fmt(outer_start.0), fmt(outer_start.1),
69 fmt(self.outer_radius), fmt(self.outer_radius),
70 large_arc,
71 fmt(outer_end.0), fmt(outer_end.1),
72 )
73 }
74 }
75
76 fn generate_full_circle(&self, start_angle: f64) -> String {
78 let mid_angle = start_angle + PI;
79 let outer_start = angle_to_point(start_angle, self.outer_radius);
80 let outer_mid = angle_to_point(mid_angle, self.outer_radius);
81
82 if self.inner_radius > 0.0 {
83 let inner_start = angle_to_point(start_angle, self.inner_radius);
84 let inner_mid = angle_to_point(mid_angle, self.inner_radius);
85
86 format!(
87 "M{},{} A{},{} 0 1,1 {},{} A{},{} 0 1,1 {},{} M{},{} A{},{} 0 1,0 {},{} A{},{} 0 1,0 {},{}Z",
88 fmt(outer_start.0), fmt(outer_start.1),
89 fmt(self.outer_radius), fmt(self.outer_radius),
90 fmt(outer_mid.0), fmt(outer_mid.1),
91 fmt(self.outer_radius), fmt(self.outer_radius),
92 fmt(outer_start.0), fmt(outer_start.1),
93 fmt(inner_start.0), fmt(inner_start.1),
94 fmt(self.inner_radius), fmt(self.inner_radius),
95 fmt(inner_mid.0), fmt(inner_mid.1),
96 fmt(self.inner_radius), fmt(self.inner_radius),
97 fmt(inner_start.0), fmt(inner_start.1),
98 )
99 } else {
100 format!(
101 "M{},{} A{},{} 0 1,1 {},{} A{},{} 0 1,1 {},{}Z",
102 fmt(outer_start.0), fmt(outer_start.1),
103 fmt(self.outer_radius), fmt(self.outer_radius),
104 fmt(outer_mid.0), fmt(outer_mid.1),
105 fmt(self.outer_radius), fmt(self.outer_radius),
106 fmt(outer_start.0), fmt(outer_start.1),
107 )
108 }
109 }
110}
111
112fn angle_to_point(angle: f64, radius: f64) -> (f64, f64) {
115 (radius * angle.sin(), -radius * angle.cos())
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use std::f64::consts::FRAC_PI_2;
122
123 #[test]
124 fn arc_quarter_circle() {
125 let gen = ArcGenerator::new(0.0, 100.0);
126 let path = gen.generate(0.0, FRAC_PI_2);
127 assert!(path.starts_with("M"), "Path should start with M, got: {}", path);
128 assert!(path.contains("A"), "Path should contain A command, got: {}", path);
129 assert!(path.ends_with("Z"), "Path should end with Z, got: {}", path);
130 }
131
132 #[test]
133 fn arc_full_circle() {
134 let gen = ArcGenerator::new(0.0, 100.0);
135 let path = gen.generate(0.0, 2.0 * PI);
136 assert!(path.starts_with("M"), "Path should start with M, got: {}", path);
137 assert!(path.contains("A"), "Path should contain A command, got: {}", path);
138 let arc_count = path.matches("A").count();
140 assert!(arc_count >= 2, "Full circle should have at least 2 arc commands, got: {}", arc_count);
141 }
142
143 #[test]
144 fn arc_doughnut() {
145 let gen = ArcGenerator::new(50.0, 100.0);
146 let path = gen.generate(0.0, FRAC_PI_2);
147 let arc_count = path.matches("A").count();
149 assert!(arc_count >= 2, "Doughnut should have at least 2 arc commands, got: {} in path: {}", arc_count, path);
150 }
151}