use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct PolarAxes {
pub cx: f64,
pub cy: f64,
pub radius: f64,
pub r_min: f64,
pub r_max: f64,
pub theta_offset: f64,
pub theta_direction: f64,
pub theta_degrees: bool,
pub r_grid_count: usize,
pub theta_grid_count: usize,
}
impl Default for PolarAxes {
fn default() -> Self {
Self {
cx: 0.0,
cy: 0.0,
radius: 100.0,
r_min: 0.0,
r_max: 1.0,
theta_offset: -PI / 2.0, theta_direction: -1.0, theta_degrees: false,
r_grid_count: 5,
theta_grid_count: 12,
}
}
}
impl PolarAxes {
pub fn new(cx: f64, cy: f64, radius: f64) -> Self {
Self {
cx,
cy,
radius,
..Default::default()
}
}
pub fn r_range(mut self, min: f64, max: f64) -> Self {
self.r_min = min;
self.r_max = max;
self
}
pub fn theta_zero_top(mut self) -> Self {
self.theta_offset = -PI / 2.0;
self
}
pub fn theta_zero_right(mut self) -> Self {
self.theta_offset = 0.0;
self
}
pub fn clockwise(mut self) -> Self {
self.theta_direction = -1.0;
self
}
pub fn counter_clockwise(mut self) -> Self {
self.theta_direction = 1.0;
self
}
pub fn degrees(mut self) -> Self {
self.theta_degrees = true;
self
}
pub fn to_cartesian(&self, theta: f64, r: f64) -> (f64, f64) {
let theta_rad = if self.theta_degrees {
theta * PI / 180.0
} else {
theta
};
let theta_adj = self.theta_offset + self.theta_direction * theta_rad;
let r_norm = if (self.r_max - self.r_min).abs() < 1e-10 {
0.0
} else {
(r - self.r_min) / (self.r_max - self.r_min) * self.radius
};
(
self.cx + r_norm * theta_adj.cos(),
self.cy + r_norm * theta_adj.sin(),
)
}
pub fn to_polar(&self, x: f64, y: f64) -> (f64, f64) {
let dx = x - self.cx;
let dy = y - self.cy;
let r_screen = (dx * dx + dy * dy).sqrt();
let r = self.r_min + (r_screen / self.radius) * (self.r_max - self.r_min);
let theta_adj = dy.atan2(dx);
let theta_rad = (theta_adj - self.theta_offset) / self.theta_direction;
let theta = if self.theta_degrees {
theta_rad * 180.0 / PI
} else {
theta_rad
};
(theta, r)
}
pub fn r_grid_circles(&self) -> Vec<(f64, f64)> {
let step = (self.r_max - self.r_min) / self.r_grid_count as f64;
(1..=self.r_grid_count)
.map(|i| {
let r = self.r_min + i as f64 * step;
let screen_r = (r - self.r_min) / (self.r_max - self.r_min) * self.radius;
(r, screen_r)
})
.collect()
}
#[allow(clippy::type_complexity)]
pub fn theta_grid_lines(&self) -> Vec<(f64, (f64, f64), (f64, f64))> {
let step = 2.0 * PI / self.theta_grid_count as f64;
(0..self.theta_grid_count)
.map(|i| {
let theta = i as f64 * step;
let theta_display = if self.theta_degrees {
theta * 180.0 / PI
} else {
theta
};
let (x_inner, y_inner) = self.to_cartesian(theta_display, self.r_min);
let (x_outer, y_outer) = self.to_cartesian(theta_display, self.r_max);
(theta_display, (x_inner, y_inner), (x_outer, y_outer))
})
.collect()
}
pub fn theta_labels(&self) -> Vec<(String, f64, f64)> {
let label_radius = self.r_max * 1.1;
self.theta_grid_lines()
.into_iter()
.map(|(theta, _, _)| {
let (x, y) = self.to_cartesian(theta, label_radius);
let label = if self.theta_degrees {
format!("{}°", theta as i32)
} else {
format!("{:.1}Ï€", theta / PI)
};
(label, x, y)
})
.collect()
}
pub fn transform_polyline(&self, theta: &[f64], r: &[f64]) -> Vec<(f64, f64)> {
theta
.iter()
.zip(r.iter())
.map(|(&t, &r)| self.to_cartesian(t, r))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_to_cartesian_basic() {
let polar = PolarAxes::new(100.0, 100.0, 50.0)
.r_range(0.0, 1.0)
.theta_zero_right()
.counter_clockwise();
let (x, y) = polar.to_cartesian(0.0, 1.0);
assert!((x - 150.0).abs() < 1e-10);
assert!((y - 100.0).abs() < 1e-10);
}
#[test]
fn test_to_cartesian_top_start() {
let polar = PolarAxes::new(100.0, 100.0, 50.0)
.r_range(0.0, 1.0)
.theta_zero_top()
.clockwise();
let (x, y) = polar.to_cartesian(0.0, 1.0);
assert!((x - 100.0).abs() < 1e-10);
assert!((y - 50.0).abs() < 1e-10);
}
#[test]
fn test_degrees_mode() {
let polar = PolarAxes::new(100.0, 100.0, 50.0)
.r_range(0.0, 1.0)
.theta_zero_right()
.counter_clockwise()
.degrees();
let (x, y) = polar.to_cartesian(90.0, 1.0);
assert!((x - 100.0).abs() < 1e-10);
assert!((y - 150.0).abs() < 1e-10);
}
#[test]
fn test_round_trip() {
let polar = PolarAxes::new(100.0, 100.0, 50.0).r_range(0.0, 2.0);
let original_theta = PI / 4.0;
let original_r = 1.5;
let (x, y) = polar.to_cartesian(original_theta, original_r);
let (theta, r) = polar.to_polar(x, y);
assert!((theta - original_theta).abs() < 1e-10);
assert!((r - original_r).abs() < 1e-10);
}
#[test]
fn test_grid_circles() {
let polar = PolarAxes::new(100.0, 100.0, 50.0).r_range(0.0, 1.0);
let circles = polar.r_grid_circles();
assert_eq!(circles.len(), 5);
assert!((circles[4].0 - 1.0).abs() < 1e-10);
assert!((circles[4].1 - 50.0).abs() < 1e-10);
}
#[test]
fn test_theta_grid() {
let polar = PolarAxes::new(100.0, 100.0, 50.0);
let lines = polar.theta_grid_lines();
assert_eq!(lines.len(), 12); }
}