use kuva::prelude::*;
use std::f64::consts::PI;
const CIRCLE_PTS: usize = 64;
fn circle_points() -> (Vec<f64>, Vec<f64>) {
let xs: Vec<f64> = (0..CIRCLE_PTS)
.map(|i| (2.0 * PI * i as f64 / CIRCLE_PTS as f64).cos())
.collect();
let ys: Vec<f64> = (0..CIRCLE_PTS)
.map(|i| (2.0 * PI * i as f64 / CIRCLE_PTS as f64).sin())
.collect();
(xs, ys)
}
fn render(layout: Layout) -> String {
let (xs, ys) = circle_points();
let scatter = ScatterPlot::new()
.with_data(xs.iter().copied().zip(ys.iter().copied()))
.with_color("#2196f3");
render_to_svg(vec![Plot::Scatter(scatter)], layout)
}
#[test]
fn test_without_equal_aspect_circle_is_ellipse() {
let layout = Layout::new((-1.5, 1.5), (-1.5, 1.5))
.with_width(600.0)
.with_height(300.0)
.with_title("Circle WITHOUT equal_aspect — should look like an ellipse");
let svg = render(layout);
let cx_vals = extract_attr_f64_values(&svg, "cx=");
let cy_vals = extract_attr_f64_values(&svg, "cy=");
let cx_span = range_span(&cx_vals);
let cy_span = range_span(&cy_vals);
assert!(
cx_span > cy_span * 1.5,
"Expected cx_span ({cx_span:.1}) >> cy_span ({cy_span:.1}) without equal_aspect"
);
write_test_output("equal_aspect_off_ellipse.svg", &svg);
}
#[test]
fn test_with_equal_aspect_circle_is_circular() {
let layout = Layout::new((-1.5, 1.5), (-1.5, 1.5))
.with_width(600.0)
.with_height(300.0)
.with_equal_aspect()
.with_title("Circle WITH equal_aspect — should look perfectly circular");
let svg = render(layout);
let cx_vals = extract_attr_f64_values(&svg, "cx=");
let cy_vals = extract_attr_f64_values(&svg, "cy=");
let cx_span = range_span(&cx_vals);
let cy_span = range_span(&cy_vals);
let diff = (cx_span - cy_span).abs();
assert!(
diff < 2.0,
"With equal_aspect cx_span ({cx_span:.1}) should ≈ cy_span ({cy_span:.1}), diff={diff:.2}"
);
write_test_output("equal_aspect_on_circle.svg", &svg);
}
#[test]
fn test_equal_aspect_always_equalises_spans() {
let cases: &[(f64, f64)] = &[
(400.0, 400.0),
(600.0, 400.0),
(400.0, 600.0),
(800.0, 300.0),
];
for &(w, h) in cases {
let layout = Layout::new((-1.5, 1.5), (-1.5, 1.5))
.with_width(w)
.with_height(h)
.with_equal_aspect();
let svg = render(layout);
let cx_span = range_span(&extract_attr_f64_values(&svg, "cx="));
let cy_span = range_span(&extract_attr_f64_values(&svg, "cy="));
let diff = (cx_span - cy_span).abs();
assert!(
diff < 2.0,
"Canvas {w}×{h}: cx_span ({cx_span:.1}) ≈ cy_span ({cy_span:.1}), diff={diff:.2}"
);
}
}
#[test]
fn test_equal_aspect_tall_canvas() {
let layout = Layout::new((-1.5, 1.5), (-1.5, 1.5))
.with_width(300.0)
.with_height(600.0)
.with_equal_aspect()
.with_title("Circle on tall canvas — still circular");
let svg = render(layout);
let cx_vals = extract_attr_f64_values(&svg, "cx=");
let cy_vals = extract_attr_f64_values(&svg, "cy=");
let cx_span = range_span(&cx_vals);
let cy_span = range_span(&cy_vals);
let diff = (cx_span - cy_span).abs();
assert!(
diff < 2.0,
"Tall canvas equal_aspect: cx_span ({cx_span:.1}) ≈ cy_span ({cy_span:.1}), diff={diff:.2}"
);
write_test_output("equal_aspect_tall_circle.svg", &svg);
}
#[test]
fn test_equal_aspect_builder_is_chainable() {
let layout = Layout::new((-1.0, 1.0), (-1.0, 1.0))
.with_equal_aspect()
.with_title("chained")
.with_width(400.0)
.with_height(400.0);
let svg = render(layout);
assert!(svg.contains("<svg"));
}
fn extract_attr_f64_values(haystack: &str, attr: &str) -> Vec<f64> {
let mut vals = Vec::new();
let mut rest = haystack;
while let Some(pos) = rest.find(attr) {
rest = &rest[pos + attr.len()..];
let rest2 = rest.trim_start_matches('"');
let end = rest2
.find(|c: char| !c.is_ascii_digit() && c != '.' && c != '-')
.unwrap_or(rest2.len());
if let Ok(v) = rest2[..end].parse::<f64>() {
vals.push(v);
}
}
vals
}
fn range_span(vals: &[f64]) -> f64 {
if vals.is_empty() {
return 0.0;
}
let min = vals.iter().cloned().fold(f64::INFINITY, f64::min);
let max = vals.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
max - min
}
fn write_test_output(name: &str, content: &str) {
let dir = std::path::Path::new("test_outputs");
let _ = std::fs::create_dir_all(dir);
let _ = std::fs::write(dir.join(name), content);
}