#![allow(clippy::cast_possible_truncation)]
use crate::axes::Axis;
use starsight_layer_1::primitives::{Point, Rect};
use std::any::Any;
pub trait Coord: Any {
fn plot_area(&self) -> Rect;
fn data_to_pixel(&self, x: f64, y: f64) -> Point;
fn as_any(&self) -> &dyn Any;
}
pub struct CartesianCoord {
pub x_axis: Axis,
pub y_axis: Axis,
pub plot_area: Rect,
}
impl CartesianCoord {
#[must_use]
pub fn map_x(&self, x: f64) -> f64 {
let nx = self.x_axis.scale.map(x);
f64::from(self.plot_area.left) + nx * f64::from(self.plot_area.width())
}
#[must_use]
pub fn map_y(&self, y: f64) -> f64 {
let ny = self.y_axis.scale.map(y);
f64::from(self.plot_area.bottom) - ny * f64::from(self.plot_area.height())
}
#[must_use]
pub fn data_to_pixel(&self, x: f64, y: f64) -> Point {
Point::new(self.map_x(x) as f32, self.map_y(y) as f32)
}
}
impl Coord for CartesianCoord {
fn plot_area(&self) -> Rect {
self.plot_area
}
fn data_to_pixel(&self, x: f64, y: f64) -> Point {
Self::data_to_pixel(self, x, y)
}
fn as_any(&self) -> &dyn Any {
self
}
}
pub struct PolarCoord {
pub theta_axis: Axis,
pub r_axis: Axis,
pub plot_area: Rect,
pub center: Point,
pub radius: f32,
}
impl PolarCoord {
#[must_use]
pub fn inscribed(theta_axis: Axis, r_axis: Axis, plot_area: Rect) -> Self {
let cx = (plot_area.left + plot_area.right) * 0.5;
let cy = (plot_area.top + plot_area.bottom) * 0.5;
let radius = plot_area.width().min(plot_area.height()) * 0.5;
Self {
theta_axis,
r_axis,
plot_area,
center: Point::new(cx, cy),
radius,
}
}
#[must_use]
pub fn with_center(mut self, center: Point) -> Self {
self.center = center;
self
}
#[must_use]
pub fn with_radius(mut self, radius: f32) -> Self {
self.radius = radius;
self
}
#[must_use]
pub fn data_to_pixel(&self, theta: f64, r: f64) -> Point {
let theta_norm = self.theta_axis.scale.map(theta);
let r_norm = self.r_axis.scale.map(r);
let angle = theta_norm * std::f64::consts::TAU;
let pixel_r = r_norm * f64::from(self.radius);
let x = f64::from(self.center.x) + pixel_r * angle.sin();
let y = f64::from(self.center.y) - pixel_r * angle.cos();
Point::new(x as f32, y as f32)
}
}
impl Coord for PolarCoord {
fn plot_area(&self) -> Rect {
self.plot_area
}
fn data_to_pixel(&self, x: f64, y: f64) -> Point {
Self::data_to_pixel(self, x, y)
}
fn as_any(&self) -> &dyn Any {
self
}
}
#[cfg(test)]
mod tests {
use super::{Coord, PolarCoord};
use crate::axes::Axis;
use crate::scales::LinearScale;
use starsight_layer_1::primitives::Rect;
fn unit_polar() -> PolarCoord {
let theta = Axis {
scale: Box::new(LinearScale {
domain_min: 0.0,
domain_max: 1.0,
}),
label: None,
tick_positions: vec![],
tick_labels: vec![],
};
let r = Axis {
scale: Box::new(LinearScale {
domain_min: 0.0,
domain_max: 1.0,
}),
label: None,
tick_positions: vec![],
tick_labels: vec![],
};
PolarCoord::inscribed(theta, r, Rect::new(0.0, 0.0, 200.0, 200.0))
}
#[test]
fn polar_inscribed_centers_in_plot_area() {
let p = unit_polar();
assert!((p.center.x - 100.0).abs() < 1e-4);
assert!((p.center.y - 100.0).abs() < 1e-4);
assert!((p.radius - 100.0).abs() < 1e-4);
}
#[test]
fn polar_theta_zero_points_up() {
let p = unit_polar();
let pt = p.data_to_pixel(0.0, 1.0);
assert!((pt.x - 100.0).abs() < 1e-3);
assert!((pt.y - 0.0).abs() < 1e-3);
}
#[test]
fn polar_quarter_turn_points_right() {
let p = unit_polar();
let pt = p.data_to_pixel(0.25, 1.0);
assert!((pt.x - 200.0).abs() < 1e-3);
assert!((pt.y - 100.0).abs() < 1e-3);
}
#[test]
fn polar_zero_radius_returns_center() {
let p = unit_polar();
let pt = p.data_to_pixel(0.5, 0.0);
assert!((pt.x - 100.0).abs() < 1e-3);
assert!((pt.y - 100.0).abs() < 1e-3);
}
#[test]
fn polar_dispatches_through_coord_trait() {
let p = unit_polar();
let dyn_coord: &dyn Coord = &p;
assert_eq!(dyn_coord.plot_area(), p.plot_area);
let pt_via_trait = dyn_coord.data_to_pixel(0.0, 1.0);
let pt_direct = p.data_to_pixel(0.0, 1.0);
assert_eq!(pt_via_trait, pt_direct);
assert!(dyn_coord.as_any().is::<PolarCoord>());
}
}