#![allow(clippy::unwrap_used, clippy::disallowed_methods)]
use serde::{Deserialize, Serialize};
use std::f64::consts::PI;
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
pub struct Point2D {
pub x: f64,
pub y: f64,
}
impl Point2D {
#[must_use]
pub const fn new(x: f64, y: f64) -> Self {
Self { x, y }
}
pub const ORIGIN: Self = Self { x: 0.0, y: 0.0 };
#[must_use]
pub fn distance(&self, other: &Self) -> f64 {
let dx = self.x - other.x;
let dy = self.y - other.y;
dx.hypot(dy)
}
#[must_use]
pub fn lerp(&self, other: &Self, t: f64) -> Self {
Self {
x: (other.x - self.x).mul_add(t, self.x),
y: (other.y - self.y).mul_add(t, self.y),
}
}
}
impl std::ops::Add for Point2D {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self {
x: self.x + rhs.x,
y: self.y + rhs.y,
}
}
}
impl std::ops::Sub for Point2D {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
Self {
x: self.x - rhs.x,
y: self.y - rhs.y,
}
}
}
impl std::ops::Mul<f64> for Point2D {
type Output = Self;
fn mul(self, rhs: f64) -> Self::Output {
Self {
x: self.x * rhs,
y: self.y * rhs,
}
}
}
pub trait Interpolator {
fn interpolate(&self, x: f64) -> f64;
fn sample(&self, start: f64, end: f64, num_points: usize) -> Vec<Point2D> {
if num_points < 2 {
return vec![];
}
let step = (end - start) / (num_points - 1) as f64;
(0..num_points)
.map(|i| {
let x = (i as f64).mul_add(step, start);
Point2D::new(x, self.interpolate(x))
})
.collect()
}
}
#[derive(Debug, Clone)]
pub struct LinearInterpolator {
points: Vec<Point2D>,
}
impl LinearInterpolator {
#[must_use]
pub fn from_points(points: &[Point2D]) -> Self {
let mut sorted = points.to_vec();
sorted.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap_or(std::cmp::Ordering::Equal));
Self { points: sorted }
}
#[must_use]
pub fn from_xy(xs: &[f64], ys: &[f64]) -> Self {
let points: Vec<_> = xs
.iter()
.zip(ys.iter())
.map(|(&x, &y)| Point2D::new(x, y))
.collect();
Self::from_points(&points)
}
#[must_use]
pub fn points(&self) -> &[Point2D] {
&self.points
}
fn find_segment(&self, x: f64) -> Option<(usize, f64)> {
if self.points.len() < 2 {
return None;
}
for i in 0..self.points.len() - 1 {
let p1 = &self.points[i];
let p2 = &self.points[i + 1];
if x >= p1.x && x <= p2.x {
let t = if (p2.x - p1.x).abs() < 1e-10 {
0.0
} else {
(x - p1.x) / (p2.x - p1.x)
};
return Some((i, t));
}
}
if x < self.points[0].x {
Some((
0,
(x - self.points[0].x) / (self.points[1].x - self.points[0].x),
))
} else {
let n = self.points.len();
Some((
n - 2,
(x - self.points[n - 2].x) / (self.points[n - 1].x - self.points[n - 2].x),
))
}
}
}
impl Interpolator for LinearInterpolator {
fn interpolate(&self, x: f64) -> f64 {
if self.points.is_empty() {
return 0.0;
}
if self.points.len() == 1 {
return self.points[0].y;
}
if let Some((i, t)) = self.find_segment(x) {
let p1 = &self.points[i];
let p2 = &self.points[i + 1];
(p2.y - p1.y).mul_add(t, p1.y)
} else {
0.0
}
}
}
#[derive(Debug, Clone)]
pub struct CubicSpline {
points: Vec<Point2D>,
y2: Vec<f64>,
}
impl CubicSpline {
#[must_use]
pub fn from_points(points: &[Point2D]) -> Self {
let mut sorted = points.to_vec();
sorted.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap_or(std::cmp::Ordering::Equal));
let n = sorted.len();
if n < 3 {
return Self {
points: sorted,
y2: vec![0.0; n],
};
}
let mut y2 = vec![0.0; n];
let mut u = vec![0.0; n];
for i in 1..n - 1 {
let h_prev = sorted[i].x - sorted[i - 1].x;
let h_next = sorted[i + 1].x - sorted[i].x;
if h_prev.abs() < 1e-10 || h_next.abs() < 1e-10 {
continue;
}
let sig = h_prev / (h_prev + h_next);
let p = sig.mul_add(y2[i - 1], 2.0);
y2[i] = (sig - 1.0) / p;
u[i] =
(sorted[i + 1].y - sorted[i].y) / h_next - (sorted[i].y - sorted[i - 1].y) / h_prev;
u[i] = sig.mul_add(-u[i - 1], 6.0 * u[i] / (h_prev + h_next)) / p;
}
for k in (0..n - 1).rev() {
y2[k] = y2[k].mul_add(y2[k + 1], u[k]);
}
Self { points: sorted, y2 }
}
#[must_use]
pub fn from_xy(xs: &[f64], ys: &[f64]) -> Self {
let points: Vec<_> = xs
.iter()
.zip(ys.iter())
.map(|(&x, &y)| Point2D::new(x, y))
.collect();
Self::from_points(&points)
}
#[must_use]
pub fn points(&self) -> &[Point2D] {
&self.points
}
}
impl Interpolator for CubicSpline {
fn interpolate(&self, x: f64) -> f64 {
let n = self.points.len();
if n == 0 {
return 0.0;
}
if n == 1 {
return self.points[0].y;
}
if n == 2 {
let t = (x - self.points[0].x) / (self.points[1].x - self.points[0].x);
return (self.points[1].y - self.points[0].y).mul_add(t, self.points[0].y);
}
let mut lo = 0;
let mut hi = n - 1;
while hi - lo > 1 {
let mid = (hi + lo) / 2;
if self.points[mid].x > x {
hi = mid;
} else {
lo = mid;
}
}
let h = self.points[hi].x - self.points[lo].x;
if h.abs() < 1e-10 {
return self.points[lo].y;
}
let a = (self.points[hi].x - x) / h;
let b = (x - self.points[lo].x) / h;
a.mul_add(self.points[lo].y, b * self.points[hi].y)
+ (a * a)
.mul_add(a, -a)
.mul_add(self.y2[lo], (b * b).mul_add(b, -b) * self.y2[hi])
* h
* h
/ 6.0
}
}
#[derive(Debug, Clone)]
pub struct CatmullRom {
points: Vec<Point2D>,
tension: f64,
}
impl CatmullRom {
#[must_use]
pub fn from_points(points: &[Point2D]) -> Self {
Self::with_tension(points, 0.5)
}
#[must_use]
pub fn with_tension(points: &[Point2D], tension: f64) -> Self {
let mut sorted = points.to_vec();
sorted.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap_or(std::cmp::Ordering::Equal));
Self {
points: sorted,
tension: tension.clamp(0.0, 1.0),
}
}
#[must_use]
pub fn points(&self) -> &[Point2D] {
&self.points
}
#[must_use]
pub fn to_path(&self, segments_per_span: usize) -> Vec<Point2D> {
if self.points.len() < 2 {
return self.points.clone();
}
if self.points.len() == 2 {
return self.points.clone();
}
let mut path = Vec::new();
let n = self.points.len();
for i in 0..n - 1 {
let p0 = if i == 0 {
self.points[0]
} else {
self.points[i - 1]
};
let p1 = self.points[i];
let p2 = self.points[i + 1];
let p3 = if i + 2 < n {
self.points[i + 2]
} else {
self.points[n - 1]
};
for j in 0..segments_per_span {
let t = j as f64 / segments_per_span as f64;
let point = self.catmull_rom_point(p0, p1, p2, p3, t);
path.push(point);
}
}
path.push(self.points[n - 1]);
path
}
fn catmull_rom_point(
&self,
p0: Point2D,
p1: Point2D,
p2: Point2D,
p3: Point2D,
t: f64,
) -> Point2D {
let t2 = t * t;
let t3 = t2 * t;
let tau = self.tension;
let c0 = tau.mul_add(-t, (-tau).mul_add(t3, 2.0 * tau * t2));
let c1 = (2.0 - tau).mul_add(t3, (tau - 3.0) * t2) + 1.0;
let c2 = tau.mul_add(t, (tau - 2.0).mul_add(t3, 2.0f64.mul_add(-tau, 3.0) * t2));
let c3 = tau.mul_add(t3, -(tau * t2));
Point2D::new(
c3.mul_add(p3.x, c0 * p0.x + c1 * p1.x + c2 * p2.x),
c3.mul_add(p3.y, c0 * p0.y + c1 * p1.y + c2 * p2.y),
)
}
}
impl Interpolator for CatmullRom {
fn interpolate(&self, x: f64) -> f64 {
if self.points.is_empty() {
return 0.0;
}
if self.points.len() == 1 {
return self.points[0].y;
}
let mut idx = 0;
for i in 0..self.points.len() - 1 {
if x >= self.points[i].x && x <= self.points[i + 1].x {
idx = i;
break;
}
if x < self.points[i].x {
idx = i.saturating_sub(1);
break;
}
idx = i;
}
let p1 = &self.points[idx];
let p2 = &self.points[(idx + 1).min(self.points.len() - 1)];
let t = if (p2.x - p1.x).abs() < 1e-10 {
0.0
} else {
((x - p1.x) / (p2.x - p1.x)).clamp(0.0, 1.0)
};
let p0 = if idx == 0 { *p1 } else { self.points[idx - 1] };
let p3 = if idx + 2 < self.points.len() {
self.points[idx + 2]
} else {
*p2
};
self.catmull_rom_point(p0, *p1, *p2, p3, t).y
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CubicBezier {
pub p0: Point2D,
pub p1: Point2D,
pub p2: Point2D,
pub p3: Point2D,
}
impl CubicBezier {
#[must_use]
pub const fn new(p0: Point2D, p1: Point2D, p2: Point2D, p3: Point2D) -> Self {
Self { p0, p1, p2, p3 }
}
#[must_use]
pub fn evaluate(&self, t: f64) -> Point2D {
let t = t.clamp(0.0, 1.0);
let mt = 1.0 - t;
let mt2 = mt * mt;
let mt3 = mt2 * mt;
let t2 = t * t;
let t3 = t2 * t;
Point2D::new(
(3.0 * mt * t2).mul_add(self.p2.x, mt3 * self.p0.x + 3.0 * mt2 * t * self.p1.x)
+ t3 * self.p3.x,
(3.0 * mt * t2).mul_add(self.p2.y, mt3 * self.p0.y + 3.0 * mt2 * t * self.p1.y)
+ t3 * self.p3.y,
)
}
#[must_use]
pub fn to_polyline(&self, segments: usize) -> Vec<Point2D> {
let segments = segments.max(1);
(0..=segments)
.map(|i| self.evaluate(i as f64 / segments as f64))
.collect()
}
#[must_use]
pub fn arc_length(&self, segments: usize) -> f64 {
let points = self.to_polyline(segments);
points.windows(2).map(|w| w[0].distance(&w[1])).sum()
}
#[must_use]
pub fn split(&self, t: f64) -> (Self, Self) {
let t = t.clamp(0.0, 1.0);
let p01 = self.p0.lerp(&self.p1, t);
let p12 = self.p1.lerp(&self.p2, t);
let p23 = self.p2.lerp(&self.p3, t);
let p012 = p01.lerp(&p12, t);
let p123 = p12.lerp(&p23, t);
let p0123 = p012.lerp(&p123, t);
let left = Self::new(self.p0, p01, p012, p0123);
let right = Self::new(p0123, p123, p23, self.p3);
(left, right)
}
}
#[derive(Debug, Clone)]
pub struct HistogramBins {
pub edges: Vec<f64>,
pub counts: Vec<usize>,
pub densities: Vec<f64>,
}
impl HistogramBins {
#[must_use]
pub fn from_data(data: &[f64], num_bins: usize) -> Self {
if data.is_empty() || num_bins == 0 {
return Self {
edges: vec![],
counts: vec![],
densities: vec![],
};
}
let num_bins = num_bins.max(1);
let min = data.iter().copied().fold(f64::INFINITY, f64::min);
let max = data.iter().copied().fold(f64::NEG_INFINITY, f64::max);
Self::from_data_range(data, num_bins, min, max)
}
#[must_use]
pub fn from_data_range(data: &[f64], num_bins: usize, min: f64, max: f64) -> Self {
let num_bins = num_bins.max(1);
let range = (max - min).max(1e-10);
let bin_width = range / num_bins as f64;
let edges: Vec<f64> = (0..=num_bins)
.map(|i| (i as f64).mul_add(bin_width, min))
.collect();
let mut counts = vec![0usize; num_bins];
for &value in data {
let bin = ((value - min) / bin_width).floor() as usize;
let bin = bin.min(num_bins - 1); counts[bin] += 1;
}
let total = data.len() as f64;
let densities: Vec<f64> = counts
.iter()
.map(|&c| (c as f64) / (total * bin_width))
.collect();
Self {
edges,
counts,
densities,
}
}
#[must_use]
pub fn num_bins(&self) -> usize {
self.counts.len()
}
#[must_use]
pub fn bin_width(&self) -> f64 {
if self.edges.len() < 2 {
return 0.0;
}
self.edges[1] - self.edges[0]
}
#[must_use]
pub fn bin_center(&self, index: usize) -> Option<f64> {
if index >= self.counts.len() {
return None;
}
Some((self.edges[index] + self.edges[index + 1]) / 2.0)
}
#[must_use]
pub fn bin_range(&self, index: usize) -> Option<(f64, f64)> {
if index >= self.counts.len() {
return None;
}
Some((self.edges[index], self.edges[index + 1]))
}
#[must_use]
pub fn total_count(&self) -> usize {
self.counts.iter().sum()
}
#[must_use]
pub fn max_count(&self) -> usize {
self.counts.iter().copied().max().unwrap_or(0)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ArcGeometry {
pub center: Point2D,
pub radius: f64,
pub start_angle: f64,
pub end_angle: f64,
}
impl ArcGeometry {
#[must_use]
pub const fn new(center: Point2D, radius: f64, start_angle: f64, end_angle: f64) -> Self {
Self {
center,
radius,
start_angle,
end_angle,
}
}
#[must_use]
pub fn circle(center: Point2D, radius: f64) -> Self {
Self::new(center, radius, 0.0, 2.0 * PI)
}
#[must_use]
pub fn sweep(&self) -> f64 {
self.end_angle - self.start_angle
}
#[must_use]
pub fn point_at_angle(&self, angle: f64) -> Point2D {
Point2D::new(
self.radius.mul_add(angle.cos(), self.center.x),
self.radius.mul_add(angle.sin(), self.center.y),
)
}
#[must_use]
pub fn start_point(&self) -> Point2D {
self.point_at_angle(self.start_angle)
}
#[must_use]
pub fn end_point(&self) -> Point2D {
self.point_at_angle(self.end_angle)
}
#[must_use]
pub fn mid_point(&self) -> Point2D {
let mid_angle = (self.start_angle + self.end_angle) / 2.0;
self.point_at_angle(mid_angle)
}
#[must_use]
pub fn arc_length(&self) -> f64 {
self.radius * self.sweep().abs()
}
#[must_use]
pub fn to_polyline(&self, segments: usize) -> Vec<Point2D> {
let segments = segments.max(1);
let sweep = self.sweep();
(0..=segments)
.map(|i| {
let t = i as f64 / segments as f64;
let angle = self.start_angle + t * sweep;
self.point_at_angle(angle)
})
.collect()
}
#[must_use]
pub fn to_pie_slice(&self, segments: usize) -> Vec<Point2D> {
let mut points = vec![self.center];
points.extend(self.to_polyline(segments));
points.push(self.center);
points
}
#[must_use]
pub fn contains_angle(&self, angle: f64) -> bool {
let normalized = Self::normalize_angle(angle);
let start = Self::normalize_angle(self.start_angle);
let end = Self::normalize_angle(self.end_angle);
if start <= end {
normalized >= start && normalized <= end
} else {
normalized >= start || normalized <= end
}
}
fn normalize_angle(angle: f64) -> f64 {
let mut a = angle % (2.0 * PI);
if a < 0.0 {
a += 2.0 * PI;
}
a
}
}
#[derive(Debug, Clone, Copy)]
pub struct DataNormalizer {
pub min: f64,
pub max: f64,
}
impl DataNormalizer {
#[must_use]
pub fn new(min: f64, max: f64) -> Self {
Self { min, max }
}
#[must_use]
pub fn from_data(data: &[f64]) -> Self {
if data.is_empty() {
return Self::new(0.0, 1.0);
}
let min = data.iter().copied().fold(f64::INFINITY, f64::min);
let max = data.iter().copied().fold(f64::NEG_INFINITY, f64::max);
Self::new(min, max)
}
#[must_use]
pub fn normalize(&self, value: f64) -> f64 {
let range = self.max - self.min;
if range.abs() < 1e-10 {
return 0.5;
}
(value - self.min) / range
}
#[must_use]
pub fn denormalize(&self, normalized: f64) -> f64 {
normalized.mul_add(self.max - self.min, self.min)
}
#[must_use]
pub fn normalize_all(&self, data: &[f64]) -> Vec<f64> {
data.iter().map(|&v| self.normalize(v)).collect()
}
#[must_use]
pub fn nice_bounds(&self) -> (f64, f64) {
let range = self.max - self.min;
if range.abs() < 1e-10 {
return (self.min - 1.0, self.max + 1.0);
}
let magnitude = 10.0_f64.powf(range.log10().floor());
let nice_min = (self.min / magnitude).floor() * magnitude;
let nice_max = (self.max / magnitude).ceil() * magnitude;
(nice_min, nice_max)
}
}
#[derive(Debug, Clone, Default)]
pub struct PathTessellator {
pub tolerance: f64,
pub vertices: Vec<[f32; 2]>,
pub indices: Vec<u32>,
}
impl PathTessellator {
#[must_use]
pub fn new(tolerance: f64) -> Self {
Self {
tolerance: tolerance.max(0.001),
vertices: Vec::new(),
indices: Vec::new(),
}
}
#[must_use]
pub fn with_default_tolerance() -> Self {
Self::new(0.25)
}
pub fn clear(&mut self) {
self.vertices.clear();
self.indices.clear();
}
pub fn tessellate_polygon(&mut self, points: &[Point2D]) {
if points.len() < 3 {
return;
}
let base_idx = self.vertices.len() as u32;
for p in points {
self.vertices.push([p.x as f32, p.y as f32]);
}
for i in 1..points.len() as u32 - 1 {
self.indices.push(base_idx);
self.indices.push(base_idx + i);
self.indices.push(base_idx + i + 1);
}
}
pub fn tessellate_stroke(&mut self, points: &[Point2D], width: f64) {
if points.len() < 2 {
return;
}
let half_width = width / 2.0;
for window in points.windows(2) {
let p1 = window[0];
let p2 = window[1];
let dx = p2.x - p1.x;
let dy = p2.y - p1.y;
let len = dx.hypot(dy);
if len < 1e-10 {
continue;
}
let nx = -dy / len * half_width;
let ny = dx / len * half_width;
let base_idx = self.vertices.len() as u32;
self.vertices.push([(p1.x + nx) as f32, (p1.y + ny) as f32]);
self.vertices.push([(p1.x - nx) as f32, (p1.y - ny) as f32]);
self.vertices.push([(p2.x + nx) as f32, (p2.y + ny) as f32]);
self.vertices.push([(p2.x - nx) as f32, (p2.y - ny) as f32]);
self.indices.push(base_idx);
self.indices.push(base_idx + 1);
self.indices.push(base_idx + 2);
self.indices.push(base_idx + 1);
self.indices.push(base_idx + 3);
self.indices.push(base_idx + 2);
}
}
pub fn tessellate_circle(&mut self, center: Point2D, radius: f64, segments: usize) {
let segments = segments.max(8);
let base_idx = self.vertices.len() as u32;
self.vertices.push([center.x as f32, center.y as f32]);
for i in 0..segments {
let angle = 2.0 * PI * i as f64 / segments as f64;
let x = radius.mul_add(angle.cos(), center.x);
let y = radius.mul_add(angle.sin(), center.y);
self.vertices.push([x as f32, y as f32]);
}
for i in 0..segments as u32 {
self.indices.push(base_idx); self.indices.push(base_idx + 1 + i);
self.indices.push(base_idx + 1 + (i + 1) % segments as u32);
}
}
pub fn tessellate_rect(&mut self, x: f64, y: f64, width: f64, height: f64) {
let base_idx = self.vertices.len() as u32;
self.vertices.push([x as f32, y as f32]);
self.vertices.push([(x + width) as f32, y as f32]);
self.vertices
.push([(x + width) as f32, (y + height) as f32]);
self.vertices.push([x as f32, (y + height) as f32]);
self.indices.push(base_idx);
self.indices.push(base_idx + 1);
self.indices.push(base_idx + 2);
self.indices.push(base_idx);
self.indices.push(base_idx + 2);
self.indices.push(base_idx + 3);
}
#[must_use]
pub fn vertex_count(&self) -> usize {
self.vertices.len()
}
#[must_use]
pub fn index_count(&self) -> usize {
self.indices.len()
}
#[must_use]
pub fn triangle_count(&self) -> usize {
self.indices.len() / 3
}
}
#[derive(Debug, Clone, Default)]
pub struct DrawBatch {
pub circles: Vec<[f32; 7]>,
pub rects: Vec<[f32; 8]>,
pub lines: Vec<[f32; 9]>,
}
impl DrawBatch {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn add_circle(&mut self, x: f32, y: f32, radius: f32, r: f32, g: f32, b: f32, a: f32) {
self.circles.push([x, y, radius, r, g, b, a]);
}
#[allow(clippy::too_many_arguments)]
pub fn add_rect(&mut self, x: f32, y: f32, w: f32, h: f32, r: f32, g: f32, b: f32, a: f32) {
self.rects.push([x, y, w, h, r, g, b, a]);
}
#[allow(clippy::too_many_arguments)]
pub fn add_line(
&mut self,
x1: f32,
y1: f32,
x2: f32,
y2: f32,
width: f32,
r: f32,
g: f32,
b: f32,
a: f32,
) {
self.lines.push([x1, y1, x2, y2, width, r, g, b, a]);
}
pub fn clear(&mut self) {
self.circles.clear();
self.rects.clear();
self.lines.clear();
}
#[must_use]
pub fn unbatched_draw_calls(&self) -> usize {
self.circles.len() + self.rects.len() + self.lines.len()
}
#[must_use]
pub fn batched_draw_calls(&self) -> usize {
let mut calls = 0;
if !self.circles.is_empty() {
calls += 1;
}
if !self.rects.is_empty() {
calls += 1;
}
if !self.lines.is_empty() {
calls += 1;
}
calls
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_point2d_new() {
let p = Point2D::new(1.0, 2.0);
assert_eq!(p.x, 1.0);
assert_eq!(p.y, 2.0);
}
#[test]
fn test_point2d_origin() {
assert_eq!(Point2D::ORIGIN, Point2D::new(0.0, 0.0));
}
#[test]
fn test_point2d_distance() {
let p1 = Point2D::new(0.0, 0.0);
let p2 = Point2D::new(3.0, 4.0);
assert!((p1.distance(&p2) - 5.0).abs() < 1e-10);
}
#[test]
fn test_point2d_lerp() {
let p1 = Point2D::new(0.0, 0.0);
let p2 = Point2D::new(10.0, 20.0);
let mid = p1.lerp(&p2, 0.5);
assert!((mid.x - 5.0).abs() < 1e-10);
assert!((mid.y - 10.0).abs() < 1e-10);
}
#[test]
fn test_point2d_add() {
let p1 = Point2D::new(1.0, 2.0);
let p2 = Point2D::new(3.0, 4.0);
let sum = p1 + p2;
assert_eq!(sum, Point2D::new(4.0, 6.0));
}
#[test]
fn test_point2d_sub() {
let p1 = Point2D::new(5.0, 7.0);
let p2 = Point2D::new(2.0, 3.0);
let diff = p1 - p2;
assert_eq!(diff, Point2D::new(3.0, 4.0));
}
#[test]
fn test_point2d_mul() {
let p = Point2D::new(2.0, 3.0);
let scaled = p * 2.0;
assert_eq!(scaled, Point2D::new(4.0, 6.0));
}
#[test]
fn test_linear_empty() {
let interp = LinearInterpolator::from_points(&[]);
assert_eq!(interp.interpolate(0.0), 0.0);
}
#[test]
fn test_linear_single_point() {
let interp = LinearInterpolator::from_points(&[Point2D::new(1.0, 5.0)]);
assert_eq!(interp.interpolate(0.0), 5.0);
assert_eq!(interp.interpolate(2.0), 5.0);
}
#[test]
fn test_linear_two_points() {
let interp =
LinearInterpolator::from_points(&[Point2D::new(0.0, 0.0), Point2D::new(10.0, 20.0)]);
assert!((interp.interpolate(0.0) - 0.0).abs() < 1e-10);
assert!((interp.interpolate(5.0) - 10.0).abs() < 1e-10);
assert!((interp.interpolate(10.0) - 20.0).abs() < 1e-10);
}
#[test]
fn test_linear_multiple_points() {
let interp = LinearInterpolator::from_points(&[
Point2D::new(0.0, 0.0),
Point2D::new(1.0, 2.0),
Point2D::new(2.0, 1.0),
Point2D::new(3.0, 3.0),
]);
assert!((interp.interpolate(0.0) - 0.0).abs() < 1e-10);
assert!((interp.interpolate(1.0) - 2.0).abs() < 1e-10);
assert!((interp.interpolate(2.0) - 1.0).abs() < 1e-10);
assert!((interp.interpolate(3.0) - 3.0).abs() < 1e-10);
assert!((interp.interpolate(0.5) - 1.0).abs() < 1e-10);
assert!((interp.interpolate(1.5) - 1.5).abs() < 1e-10);
}
#[test]
fn test_linear_from_xy() {
let xs = [0.0, 1.0, 2.0];
let ys = [0.0, 10.0, 20.0];
let interp = LinearInterpolator::from_xy(&xs, &ys);
assert!((interp.interpolate(1.5) - 15.0).abs() < 1e-10);
}
#[test]
fn test_linear_sample() {
let interp =
LinearInterpolator::from_points(&[Point2D::new(0.0, 0.0), Point2D::new(10.0, 10.0)]);
let samples = interp.sample(0.0, 10.0, 11);
assert_eq!(samples.len(), 11);
assert!((samples[0].x - 0.0).abs() < 1e-10);
assert!((samples[10].x - 10.0).abs() < 1e-10);
}
#[test]
fn test_spline_empty() {
let spline = CubicSpline::from_points(&[]);
assert_eq!(spline.interpolate(0.0), 0.0);
}
#[test]
fn test_spline_single_point() {
let spline = CubicSpline::from_points(&[Point2D::new(1.0, 5.0)]);
assert_eq!(spline.interpolate(0.0), 5.0);
}
#[test]
fn test_spline_two_points() {
let spline = CubicSpline::from_points(&[Point2D::new(0.0, 0.0), Point2D::new(10.0, 20.0)]);
assert!((spline.interpolate(5.0) - 10.0).abs() < 1e-10);
}
#[test]
fn test_spline_passes_through_points() {
let points = vec![
Point2D::new(0.0, 0.0),
Point2D::new(1.0, 2.0),
Point2D::new(2.0, 1.5),
Point2D::new(3.0, 3.0),
];
let spline = CubicSpline::from_points(&points);
for p in &points {
assert!(
(spline.interpolate(p.x) - p.y).abs() < 0.01,
"Spline should pass through control points"
);
}
}
#[test]
fn test_spline_smooth() {
let points = vec![
Point2D::new(0.0, 0.0),
Point2D::new(1.0, 1.0),
Point2D::new(2.0, 0.0),
];
let spline = CubicSpline::from_points(&points);
let samples = spline.sample(0.0, 2.0, 100);
for w in samples.windows(3) {
let dy1 = (w[1].y - w[0].y).abs();
let dy2 = (w[2].y - w[1].y).abs();
assert!(dy1 < 0.5 && dy2 < 0.5, "Spline should be smooth");
}
}
#[test]
fn test_catmull_rom_empty() {
let cr = CatmullRom::from_points(&[]);
assert_eq!(cr.interpolate(0.0), 0.0);
}
#[test]
fn test_catmull_rom_single() {
let cr = CatmullRom::from_points(&[Point2D::new(1.0, 5.0)]);
assert_eq!(cr.interpolate(0.0), 5.0);
}
#[test]
fn test_catmull_rom_passes_through() {
let points = vec![
Point2D::new(0.0, 0.0),
Point2D::new(1.0, 2.0),
Point2D::new(2.0, 1.0),
Point2D::new(3.0, 3.0),
];
let cr = CatmullRom::from_points(&points);
for p in &points {
let y = cr.interpolate(p.x);
assert!(
(y - p.y).abs() < 0.1,
"Catmull-Rom should pass through points: expected {} at x={}, got {}",
p.y,
p.x,
y
);
}
}
#[test]
fn test_catmull_rom_to_path() {
let points = vec![
Point2D::new(0.0, 0.0),
Point2D::new(1.0, 1.0),
Point2D::new(2.0, 0.0),
];
let cr = CatmullRom::from_points(&points);
let path = cr.to_path(10);
assert!(path.len() > points.len());
assert_eq!(path.first().unwrap().x, 0.0);
assert_eq!(path.last().unwrap().x, 2.0);
}
#[test]
fn test_catmull_rom_tension() {
let points = vec![
Point2D::new(0.0, 0.0),
Point2D::new(1.0, 1.0),
Point2D::new(2.0, 0.0),
];
let low_tension = CatmullRom::with_tension(&points, 0.0);
let high_tension = CatmullRom::with_tension(&points, 1.0);
let y_low = low_tension.interpolate(0.5);
let y_high = high_tension.interpolate(0.5);
assert!((y_low - y_high).abs() > 0.01 || (y_low - y_high).abs() < 0.5);
}
#[test]
fn test_bezier_endpoints() {
let bezier = CubicBezier::new(
Point2D::new(0.0, 0.0),
Point2D::new(1.0, 2.0),
Point2D::new(2.0, 2.0),
Point2D::new(3.0, 0.0),
);
let start = bezier.evaluate(0.0);
let end = bezier.evaluate(1.0);
assert!((start.x - 0.0).abs() < 1e-10);
assert!((start.y - 0.0).abs() < 1e-10);
assert!((end.x - 3.0).abs() < 1e-10);
assert!((end.y - 0.0).abs() < 1e-10);
}
#[test]
fn test_bezier_midpoint() {
let bezier = CubicBezier::new(
Point2D::new(0.0, 0.0),
Point2D::new(0.0, 1.0),
Point2D::new(1.0, 1.0),
Point2D::new(1.0, 0.0),
);
let mid = bezier.evaluate(0.5);
assert!(mid.x > 0.0 && mid.x < 1.0);
assert!(mid.y > 0.0 && mid.y < 1.0);
}
#[test]
fn test_bezier_to_polyline() {
let bezier = CubicBezier::new(
Point2D::new(0.0, 0.0),
Point2D::new(1.0, 2.0),
Point2D::new(2.0, 2.0),
Point2D::new(3.0, 0.0),
);
let polyline = bezier.to_polyline(10);
assert_eq!(polyline.len(), 11);
assert_eq!(polyline[0], bezier.evaluate(0.0));
assert_eq!(polyline[10], bezier.evaluate(1.0));
}
#[test]
fn test_bezier_arc_length() {
let line = CubicBezier::new(
Point2D::new(0.0, 0.0),
Point2D::new(1.0, 0.0),
Point2D::new(2.0, 0.0),
Point2D::new(3.0, 0.0),
);
let length = line.arc_length(100);
assert!((length - 3.0).abs() < 0.01);
}
#[test]
fn test_bezier_split() {
let bezier = CubicBezier::new(
Point2D::new(0.0, 0.0),
Point2D::new(1.0, 2.0),
Point2D::new(2.0, 2.0),
Point2D::new(3.0, 0.0),
);
let (left, right) = bezier.split(0.5);
assert_eq!(left.p0, bezier.p0);
assert_eq!(right.p3, bezier.p3);
assert!((left.p3.x - right.p0.x).abs() < 1e-10);
assert!((left.p3.y - right.p0.y).abs() < 1e-10);
}
#[test]
fn test_histogram_empty() {
let hist = HistogramBins::from_data(&[], 10);
assert_eq!(hist.num_bins(), 0);
}
#[test]
fn test_histogram_single_value() {
let hist = HistogramBins::from_data(&[5.0], 10);
assert_eq!(hist.total_count(), 1);
}
#[test]
fn test_histogram_uniform() {
let data: Vec<f64> = (0..100).map(|i| i as f64).collect();
let hist = HistogramBins::from_data(&data, 10);
assert_eq!(hist.num_bins(), 10);
assert_eq!(hist.total_count(), 100);
for &count in &hist.counts {
assert!(count >= 9 && count <= 11);
}
}
#[test]
fn test_histogram_bin_width() {
let hist = HistogramBins::from_data_range(&[0.0], 5, 0.0, 10.0);
assert!((hist.bin_width() - 2.0).abs() < 1e-10);
}
#[test]
fn test_histogram_bin_center() {
let hist = HistogramBins::from_data_range(&[0.0], 4, 0.0, 8.0);
assert_eq!(hist.bin_center(0), Some(1.0));
assert_eq!(hist.bin_center(1), Some(3.0));
assert_eq!(hist.bin_center(4), None);
}
#[test]
fn test_histogram_bin_range() {
let hist = HistogramBins::from_data_range(&[0.0], 4, 0.0, 8.0);
assert_eq!(hist.bin_range(0), Some((0.0, 2.0)));
assert_eq!(hist.bin_range(3), Some((6.0, 8.0)));
}
#[test]
fn test_histogram_densities() {
let data = vec![0.5, 1.5, 1.5, 2.5, 2.5, 2.5];
let hist = HistogramBins::from_data_range(&data, 3, 0.0, 3.0);
let total_density: f64 = hist.densities.iter().map(|d| d * hist.bin_width()).sum();
assert!((total_density - 1.0).abs() < 1e-10);
}
#[test]
fn test_histogram_max_count() {
let data = vec![1.0, 1.0, 1.0, 2.0];
let hist = HistogramBins::from_data_range(&data, 2, 0.0, 4.0);
assert_eq!(hist.max_count(), 3);
}
#[test]
fn test_arc_new() {
let arc = ArcGeometry::new(Point2D::new(0.0, 0.0), 10.0, 0.0, PI);
assert_eq!(arc.center, Point2D::ORIGIN);
assert_eq!(arc.radius, 10.0);
}
#[test]
fn test_arc_circle() {
let circle = ArcGeometry::circle(Point2D::new(5.0, 5.0), 3.0);
assert!((circle.sweep() - 2.0 * PI).abs() < 1e-10);
}
#[test]
fn test_arc_sweep() {
let arc = ArcGeometry::new(Point2D::ORIGIN, 1.0, 0.0, PI / 2.0);
assert!((arc.sweep() - PI / 2.0).abs() < 1e-10);
}
#[test]
fn test_arc_point_at_angle() {
let arc = ArcGeometry::circle(Point2D::ORIGIN, 1.0);
let p0 = arc.point_at_angle(0.0);
assert!((p0.x - 1.0).abs() < 1e-10);
assert!((p0.y - 0.0).abs() < 1e-10);
let p90 = arc.point_at_angle(PI / 2.0);
assert!((p90.x - 0.0).abs() < 1e-10);
assert!((p90.y - 1.0).abs() < 1e-10);
}
#[test]
fn test_arc_start_end_points() {
let arc = ArcGeometry::new(Point2D::ORIGIN, 1.0, 0.0, PI);
let start = arc.start_point();
assert!((start.x - 1.0).abs() < 1e-10);
let end = arc.end_point();
assert!((end.x - (-1.0)).abs() < 1e-10);
}
#[test]
fn test_arc_mid_point() {
let arc = ArcGeometry::new(Point2D::ORIGIN, 1.0, 0.0, PI);
let mid = arc.mid_point();
assert!((mid.y - 1.0).abs() < 1e-10);
}
#[test]
fn test_arc_length() {
let semicircle = ArcGeometry::new(Point2D::ORIGIN, 1.0, 0.0, PI);
assert!((semicircle.arc_length() - PI).abs() < 1e-10);
let full = ArcGeometry::circle(Point2D::ORIGIN, 1.0);
assert!((full.arc_length() - 2.0 * PI).abs() < 1e-10);
}
#[test]
fn test_arc_to_polyline() {
let arc = ArcGeometry::new(Point2D::ORIGIN, 1.0, 0.0, PI);
let poly = arc.to_polyline(4);
assert_eq!(poly.len(), 5);
assert!((poly[0].x - 1.0).abs() < 1e-10); assert!((poly[4].x - (-1.0)).abs() < 1e-10); }
#[test]
fn test_arc_to_pie_slice() {
let arc = ArcGeometry::new(Point2D::ORIGIN, 1.0, 0.0, PI / 2.0);
let slice = arc.to_pie_slice(4);
assert_eq!(slice[0], Point2D::ORIGIN);
assert_eq!(slice[slice.len() - 1], Point2D::ORIGIN);
}
#[test]
fn test_arc_contains_angle() {
let arc = ArcGeometry::new(Point2D::ORIGIN, 1.0, 0.0, PI);
assert!(arc.contains_angle(PI / 2.0));
assert!(arc.contains_angle(0.0));
assert!(!arc.contains_angle(3.0 * PI / 2.0));
}
#[test]
fn test_normalizer_new() {
let norm = DataNormalizer::new(0.0, 100.0);
assert_eq!(norm.min, 0.0);
assert_eq!(norm.max, 100.0);
}
#[test]
fn test_normalizer_from_data() {
let data = vec![10.0, 20.0, 30.0, 40.0, 50.0];
let norm = DataNormalizer::from_data(&data);
assert_eq!(norm.min, 10.0);
assert_eq!(norm.max, 50.0);
}
#[test]
fn test_normalizer_from_empty() {
let norm = DataNormalizer::from_data(&[]);
assert_eq!(norm.min, 0.0);
assert_eq!(norm.max, 1.0);
}
#[test]
fn test_normalizer_normalize() {
let norm = DataNormalizer::new(0.0, 100.0);
assert!((norm.normalize(0.0) - 0.0).abs() < 1e-10);
assert!((norm.normalize(50.0) - 0.5).abs() < 1e-10);
assert!((norm.normalize(100.0) - 1.0).abs() < 1e-10);
}
#[test]
fn test_normalizer_denormalize() {
let norm = DataNormalizer::new(0.0, 100.0);
assert!((norm.denormalize(0.0) - 0.0).abs() < 1e-10);
assert!((norm.denormalize(0.5) - 50.0).abs() < 1e-10);
assert!((norm.denormalize(1.0) - 100.0).abs() < 1e-10);
}
#[test]
fn test_normalizer_roundtrip() {
let norm = DataNormalizer::new(-50.0, 150.0);
let values = vec![-50.0, 0.0, 50.0, 100.0, 150.0];
for &v in &values {
let normalized = norm.normalize(v);
let denormalized = norm.denormalize(normalized);
assert!((v - denormalized).abs() < 1e-10);
}
}
#[test]
fn test_normalizer_normalize_all() {
let norm = DataNormalizer::new(0.0, 10.0);
let data = vec![0.0, 5.0, 10.0];
let normalized = norm.normalize_all(&data);
assert_eq!(normalized, vec![0.0, 0.5, 1.0]);
}
#[test]
fn test_normalizer_nice_bounds() {
let norm = DataNormalizer::new(3.2, 97.8);
let (nice_min, nice_max) = norm.nice_bounds();
assert!(nice_min <= 3.2);
assert!(nice_max >= 97.8);
assert!((nice_min * 10.0).round() == nice_min * 10.0);
}
#[test]
fn test_tessellator_new() {
let tess = PathTessellator::new(0.5);
assert!((tess.tolerance - 0.5).abs() < 1e-10);
assert!(tess.vertices.is_empty());
assert!(tess.indices.is_empty());
}
#[test]
fn test_tessellator_default() {
let tess = PathTessellator::with_default_tolerance();
assert!((tess.tolerance - 0.25).abs() < 1e-10);
}
#[test]
fn test_tessellator_polygon() {
let mut tess = PathTessellator::new(0.5);
let triangle = vec![
Point2D::new(0.0, 0.0),
Point2D::new(1.0, 0.0),
Point2D::new(0.5, 1.0),
];
tess.tessellate_polygon(&triangle);
assert_eq!(tess.vertex_count(), 3);
assert_eq!(tess.index_count(), 3);
assert_eq!(tess.triangle_count(), 1);
}
#[test]
fn test_tessellator_quad() {
let mut tess = PathTessellator::new(0.5);
let quad = vec![
Point2D::new(0.0, 0.0),
Point2D::new(1.0, 0.0),
Point2D::new(1.0, 1.0),
Point2D::new(0.0, 1.0),
];
tess.tessellate_polygon(&quad);
assert_eq!(tess.vertex_count(), 4);
assert_eq!(tess.triangle_count(), 2);
}
#[test]
fn test_tessellator_stroke() {
let mut tess = PathTessellator::new(0.5);
let line = vec![Point2D::new(0.0, 0.0), Point2D::new(10.0, 0.0)];
tess.tessellate_stroke(&line, 2.0);
assert_eq!(tess.vertex_count(), 4);
assert_eq!(tess.triangle_count(), 2);
}
#[test]
fn test_tessellator_multi_segment_stroke() {
let mut tess = PathTessellator::new(0.5);
let path = vec![
Point2D::new(0.0, 0.0),
Point2D::new(10.0, 0.0),
Point2D::new(10.0, 10.0),
];
tess.tessellate_stroke(&path, 1.0);
assert_eq!(tess.vertex_count(), 8);
assert_eq!(tess.triangle_count(), 4);
}
#[test]
fn test_tessellator_circle() {
let mut tess = PathTessellator::new(0.5);
tess.tessellate_circle(Point2D::ORIGIN, 1.0, 16);
assert_eq!(tess.vertex_count(), 17);
assert_eq!(tess.triangle_count(), 16);
}
#[test]
fn test_tessellator_rect() {
let mut tess = PathTessellator::new(0.5);
tess.tessellate_rect(0.0, 0.0, 10.0, 5.0);
assert_eq!(tess.vertex_count(), 4);
assert_eq!(tess.triangle_count(), 2);
}
#[test]
fn test_tessellator_clear() {
let mut tess = PathTessellator::new(0.5);
tess.tessellate_rect(0.0, 0.0, 10.0, 5.0);
assert!(!tess.vertices.is_empty());
tess.clear();
assert!(tess.vertices.is_empty());
assert!(tess.indices.is_empty());
}
#[test]
fn test_tessellator_multiple_shapes() {
let mut tess = PathTessellator::new(0.5);
tess.tessellate_rect(0.0, 0.0, 10.0, 10.0);
tess.tessellate_circle(Point2D::new(20.0, 5.0), 3.0, 8);
assert_eq!(tess.vertex_count(), 4 + 9); assert_eq!(tess.triangle_count(), 2 + 8);
}
#[test]
fn test_batch_new() {
let batch = DrawBatch::new();
assert!(batch.circles.is_empty());
assert!(batch.rects.is_empty());
assert!(batch.lines.is_empty());
}
#[test]
fn test_batch_add_circle() {
let mut batch = DrawBatch::new();
batch.add_circle(10.0, 20.0, 5.0, 1.0, 0.0, 0.0, 1.0);
assert_eq!(batch.circles.len(), 1);
assert_eq!(batch.circles[0][0], 10.0);
assert_eq!(batch.circles[0][1], 20.0);
assert_eq!(batch.circles[0][2], 5.0);
}
#[test]
fn test_batch_add_rect() {
let mut batch = DrawBatch::new();
batch.add_rect(0.0, 0.0, 100.0, 50.0, 0.0, 1.0, 0.0, 1.0);
assert_eq!(batch.rects.len(), 1);
assert_eq!(batch.rects[0][2], 100.0);
assert_eq!(batch.rects[0][3], 50.0);
}
#[test]
fn test_batch_add_line() {
let mut batch = DrawBatch::new();
batch.add_line(0.0, 0.0, 100.0, 100.0, 2.0, 0.0, 0.0, 1.0, 1.0);
assert_eq!(batch.lines.len(), 1);
assert_eq!(batch.lines[0][4], 2.0); }
#[test]
fn test_batch_clear() {
let mut batch = DrawBatch::new();
batch.add_circle(0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0);
batch.add_rect(0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0);
batch.add_line(0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0);
batch.clear();
assert!(batch.circles.is_empty());
assert!(batch.rects.is_empty());
assert!(batch.lines.is_empty());
}
#[test]
fn test_batch_draw_call_counts() {
let mut batch = DrawBatch::new();
assert_eq!(batch.unbatched_draw_calls(), 0);
assert_eq!(batch.batched_draw_calls(), 0);
for i in 0..100 {
batch.add_circle(i as f32, 0.0, 5.0, 1.0, 0.0, 0.0, 1.0);
}
for i in 0..50 {
batch.add_rect(i as f32 * 10.0, 0.0, 10.0, 10.0, 0.0, 1.0, 0.0, 1.0);
}
assert_eq!(batch.unbatched_draw_calls(), 150);
assert_eq!(batch.batched_draw_calls(), 2);
}
#[test]
fn test_batch_efficiency() {
let mut batch = DrawBatch::new();
for i in 0..1000 {
batch.add_circle(i as f32, (i as f32).sin() * 100.0, 3.0, 0.2, 0.5, 0.9, 1.0);
}
for i in 0..10 {
batch.add_rect(
i as f32 * 50.0,
0.0,
40.0,
(i as f32 + 1.0) * 20.0,
0.9,
0.3,
0.3,
1.0,
);
}
let reduction = batch.unbatched_draw_calls() as f64 / batch.batched_draw_calls() as f64;
assert!(reduction > 500.0);
}
#[test]
fn test_point2d_default() {
let p: Point2D = Default::default();
assert_eq!(p, Point2D::ORIGIN);
}
#[test]
fn test_point2d_clone() {
let p1 = Point2D::new(3.14, 2.71);
let p2 = p1;
assert_eq!(p1, p2);
}
#[test]
fn test_point2d_debug() {
let p = Point2D::new(1.0, 2.0);
let debug = format!("{p:?}");
assert!(debug.contains("Point2D"));
}
#[test]
fn test_point2d_distance_to_self() {
let p = Point2D::new(5.0, 10.0);
assert_eq!(p.distance(&p), 0.0);
}
#[test]
fn test_point2d_lerp_boundaries() {
let p1 = Point2D::new(0.0, 0.0);
let p2 = Point2D::new(10.0, 10.0);
let at_start = p1.lerp(&p2, 0.0);
assert_eq!(at_start, p1);
let at_end = p1.lerp(&p2, 1.0);
assert_eq!(at_end, p2);
}
#[test]
fn test_point2d_lerp_extrapolate() {
let p1 = Point2D::new(0.0, 0.0);
let p2 = Point2D::new(10.0, 10.0);
let beyond = p1.lerp(&p2, 2.0);
assert!((beyond.x - 20.0).abs() < 1e-10);
assert!((beyond.y - 20.0).abs() < 1e-10);
}
#[test]
fn test_point2d_mul_zero() {
let p = Point2D::new(5.0, 10.0);
let scaled = p * 0.0;
assert_eq!(scaled, Point2D::ORIGIN);
}
#[test]
fn test_point2d_mul_negative() {
let p = Point2D::new(5.0, 10.0);
let scaled = p * -1.0;
assert_eq!(scaled, Point2D::new(-5.0, -10.0));
}
#[test]
fn test_linear_extrapolate_left() {
let interp =
LinearInterpolator::from_points(&[Point2D::new(0.0, 0.0), Point2D::new(10.0, 10.0)]);
let y = interp.interpolate(-5.0);
assert!((y - (-5.0)).abs() < 1e-10);
}
#[test]
fn test_linear_extrapolate_right() {
let interp =
LinearInterpolator::from_points(&[Point2D::new(0.0, 0.0), Point2D::new(10.0, 10.0)]);
let y = interp.interpolate(15.0);
assert!((y - 15.0).abs() < 1e-10);
}
#[test]
fn test_linear_unsorted_input() {
let interp = LinearInterpolator::from_points(&[
Point2D::new(3.0, 30.0),
Point2D::new(1.0, 10.0),
Point2D::new(2.0, 20.0),
]);
assert!((interp.interpolate(1.5) - 15.0).abs() < 1e-10);
}
#[test]
fn test_linear_points_getter() {
let interp =
LinearInterpolator::from_points(&[Point2D::new(0.0, 0.0), Point2D::new(1.0, 1.0)]);
assert_eq!(interp.points().len(), 2);
}
#[test]
fn test_linear_sample_single_point() {
let interp = LinearInterpolator::from_points(&[Point2D::new(0.0, 5.0)]);
let samples = interp.sample(0.0, 10.0, 5);
for s in &samples {
assert_eq!(s.y, 5.0);
}
}
#[test]
fn test_linear_sample_too_few() {
let interp =
LinearInterpolator::from_points(&[Point2D::new(0.0, 0.0), Point2D::new(10.0, 10.0)]);
let samples = interp.sample(0.0, 10.0, 1);
assert!(samples.is_empty());
}
#[test]
fn test_linear_vertical_segment() {
let interp = LinearInterpolator::from_points(&[
Point2D::new(5.0, 0.0),
Point2D::new(5.0, 10.0), Point2D::new(10.0, 10.0),
]);
let y = interp.interpolate(5.0);
assert!(y.is_finite());
}
#[test]
fn test_spline_from_xy() {
let xs = [0.0, 1.0, 2.0, 3.0];
let ys = [0.0, 1.0, 0.0, 1.0];
let spline = CubicSpline::from_xy(&xs, &ys);
assert_eq!(spline.points().len(), 4);
}
#[test]
fn test_spline_points_getter() {
let points = vec![
Point2D::new(0.0, 0.0),
Point2D::new(1.0, 1.0),
Point2D::new(2.0, 0.0),
];
let spline = CubicSpline::from_points(&points);
assert_eq!(spline.points().len(), 3);
}
#[test]
fn test_spline_identical_x() {
let points = vec![
Point2D::new(1.0, 0.0),
Point2D::new(1.0, 10.0),
Point2D::new(2.0, 5.0),
];
let spline = CubicSpline::from_points(&points);
let y = spline.interpolate(1.0);
assert!(y.is_finite());
}
#[test]
fn test_spline_extrapolate() {
let points = vec![
Point2D::new(0.0, 0.0),
Point2D::new(1.0, 1.0),
Point2D::new(2.0, 0.0),
];
let spline = CubicSpline::from_points(&points);
let y_before = spline.interpolate(-1.0);
let y_after = spline.interpolate(3.0);
assert!(y_before.is_finite());
assert!(y_after.is_finite());
}
#[test]
fn test_catmull_rom_points_getter() {
let points = vec![Point2D::new(0.0, 0.0), Point2D::new(1.0, 1.0)];
let cr = CatmullRom::from_points(&points);
assert_eq!(cr.points().len(), 2);
}
#[test]
fn test_catmull_rom_to_path_two_points() {
let points = vec![Point2D::new(0.0, 0.0), Point2D::new(1.0, 1.0)];
let cr = CatmullRom::from_points(&points);
let path = cr.to_path(10);
assert_eq!(path.len(), 2); }
#[test]
fn test_catmull_rom_to_path_single() {
let points = vec![Point2D::new(0.0, 0.0)];
let cr = CatmullRom::from_points(&points);
let path = cr.to_path(10);
assert_eq!(path.len(), 1);
}
#[test]
fn test_catmull_rom_tension_clamp() {
let points = vec![
Point2D::new(0.0, 0.0),
Point2D::new(1.0, 1.0),
Point2D::new(2.0, 0.0),
];
let cr_low = CatmullRom::with_tension(&points, -1.0);
let cr_high = CatmullRom::with_tension(&points, 2.0);
let _ = cr_low.interpolate(0.5);
let _ = cr_high.interpolate(0.5);
}
#[test]
fn test_bezier_clamp_t() {
let bezier = CubicBezier::new(
Point2D::new(0.0, 0.0),
Point2D::new(1.0, 2.0),
Point2D::new(2.0, 2.0),
Point2D::new(3.0, 0.0),
);
let p_neg = bezier.evaluate(-0.5);
let p_over = bezier.evaluate(1.5);
assert_eq!(p_neg, bezier.evaluate(0.0));
assert_eq!(p_over, bezier.evaluate(1.0));
}
#[test]
fn test_bezier_polyline_min_segments() {
let bezier = CubicBezier::new(
Point2D::new(0.0, 0.0),
Point2D::new(1.0, 1.0),
Point2D::new(2.0, 1.0),
Point2D::new(3.0, 0.0),
);
let polyline = bezier.to_polyline(0);
assert!(polyline.len() >= 2);
}
#[test]
fn test_bezier_split_at_zero() {
let bezier = CubicBezier::new(
Point2D::new(0.0, 0.0),
Point2D::new(1.0, 1.0),
Point2D::new(2.0, 1.0),
Point2D::new(3.0, 0.0),
);
let (left, right) = bezier.split(0.0);
assert_eq!(left.p0, bezier.p0);
assert_eq!(right.p3, bezier.p3);
}
#[test]
fn test_bezier_split_at_one() {
let bezier = CubicBezier::new(
Point2D::new(0.0, 0.0),
Point2D::new(1.0, 1.0),
Point2D::new(2.0, 1.0),
Point2D::new(3.0, 0.0),
);
let (left, _right) = bezier.split(1.0);
assert_eq!(left.p3, bezier.p3);
}
#[test]
fn test_bezier_arc_length_zero_segments() {
let bezier = CubicBezier::new(
Point2D::new(0.0, 0.0),
Point2D::new(1.0, 0.0),
Point2D::new(2.0, 0.0),
Point2D::new(3.0, 0.0),
);
let length = bezier.arc_length(0);
assert!(length.is_finite());
}
#[test]
fn test_histogram_zero_bins() {
let hist = HistogramBins::from_data(&[1.0, 2.0, 3.0], 0);
assert_eq!(hist.num_bins(), 0);
}
#[test]
fn test_histogram_all_same_value() {
let data = vec![5.0, 5.0, 5.0, 5.0, 5.0];
let hist = HistogramBins::from_data(&data, 5);
assert_eq!(hist.total_count(), 5);
}
#[test]
fn test_histogram_negative_values() {
let data = vec![-10.0, -5.0, 0.0, 5.0, 10.0];
let hist = HistogramBins::from_data(&data, 4);
assert_eq!(hist.num_bins(), 4);
assert_eq!(hist.total_count(), 5);
}
#[test]
fn test_histogram_bin_range_out_of_bounds() {
let hist = HistogramBins::from_data(&[1.0, 2.0, 3.0], 3);
assert_eq!(hist.bin_range(100), None);
}
#[test]
fn test_histogram_bin_center_out_of_bounds() {
let hist = HistogramBins::from_data(&[1.0, 2.0, 3.0], 3);
assert_eq!(hist.bin_center(100), None);
}
#[test]
fn test_histogram_edge_case_max_value() {
let hist = HistogramBins::from_data_range(&[0.0, 5.0, 10.0], 2, 0.0, 10.0);
assert_eq!(hist.total_count(), 3);
}
#[test]
fn test_arc_negative_angles() {
let arc = ArcGeometry::new(Point2D::ORIGIN, 1.0, -PI / 2.0, PI / 2.0);
assert!((arc.sweep() - PI).abs() < 1e-10);
}
#[test]
fn test_arc_large_angles() {
let arc = ArcGeometry::new(Point2D::ORIGIN, 1.0, 0.0, 4.0 * PI);
let poly = arc.to_polyline(10);
assert_eq!(poly.len(), 11);
}
#[test]
fn test_arc_zero_radius() {
let arc = ArcGeometry::new(Point2D::new(5.0, 5.0), 0.0, 0.0, PI);
let start = arc.start_point();
assert_eq!(start, arc.center);
}
#[test]
fn test_arc_contains_angle_wrap() {
let arc = ArcGeometry::new(Point2D::ORIGIN, 1.0, 3.0 * PI / 2.0, PI / 2.0 + 2.0 * PI);
assert!(arc.contains_angle(0.0));
}
#[test]
fn test_arc_pie_slice_segments() {
let arc = ArcGeometry::new(Point2D::ORIGIN, 1.0, 0.0, PI / 2.0);
let slice = arc.to_pie_slice(8);
assert_eq!(slice.len(), 11);
}
#[test]
fn test_normalizer_zero_range() {
let norm = DataNormalizer::new(5.0, 5.0);
assert_eq!(norm.normalize(5.0), 0.5);
assert_eq!(norm.normalize(10.0), 0.5);
}
#[test]
fn test_normalizer_negative_range() {
let norm = DataNormalizer::new(-100.0, -50.0);
assert!((norm.normalize(-100.0) - 0.0).abs() < 1e-10);
assert!((norm.normalize(-75.0) - 0.5).abs() < 1e-10);
assert!((norm.normalize(-50.0) - 1.0).abs() < 1e-10);
}
#[test]
fn test_normalizer_nice_bounds_small_range() {
let norm = DataNormalizer::new(0.001, 0.002);
let (nice_min, nice_max) = norm.nice_bounds();
assert!(nice_min <= 0.001);
assert!(nice_max >= 0.002);
}
#[test]
fn test_normalizer_nice_bounds_large_range() {
let norm = DataNormalizer::new(0.0, 1_000_000.0);
let (nice_min, nice_max) = norm.nice_bounds();
assert!(nice_min <= 0.0);
assert!(nice_max >= 1_000_000.0);
}
#[test]
fn test_normalizer_from_single_value() {
let norm = DataNormalizer::from_data(&[42.0]);
assert_eq!(norm.min, 42.0);
assert_eq!(norm.max, 42.0);
}
#[test]
fn test_tessellator_tolerance_minimum() {
let tess = PathTessellator::new(0.0001);
assert!(tess.tolerance >= 0.001);
}
#[test]
fn test_tessellator_polygon_too_small() {
let mut tess = PathTessellator::new(0.5);
tess.tessellate_polygon(&[Point2D::new(0.0, 0.0)]);
assert!(tess.vertices.is_empty());
tess.tessellate_polygon(&[Point2D::new(0.0, 0.0), Point2D::new(1.0, 0.0)]);
assert!(tess.vertices.is_empty());
}
#[test]
fn test_tessellator_stroke_too_short() {
let mut tess = PathTessellator::new(0.5);
tess.tessellate_stroke(&[Point2D::new(0.0, 0.0)], 1.0);
assert!(tess.vertices.is_empty());
}
#[test]
fn test_tessellator_stroke_zero_length_segment() {
let mut tess = PathTessellator::new(0.5);
tess.tessellate_stroke(&[Point2D::new(5.0, 5.0), Point2D::new(5.0, 5.0)], 1.0);
assert!(tess.vertices.is_empty());
}
#[test]
fn test_tessellator_circle_min_segments() {
let mut tess = PathTessellator::new(0.5);
tess.tessellate_circle(Point2D::ORIGIN, 1.0, 3);
assert!(tess.vertex_count() >= 9); }
#[test]
fn test_tessellator_default_trait() {
let tess: PathTessellator = Default::default();
assert!(tess.vertices.is_empty());
}
#[test]
fn test_batch_default_trait() {
let batch: DrawBatch = Default::default();
assert!(batch.circles.is_empty());
assert!(batch.rects.is_empty());
assert!(batch.lines.is_empty());
}
#[test]
fn test_batch_only_circles() {
let mut batch = DrawBatch::new();
batch.add_circle(0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0);
assert_eq!(batch.batched_draw_calls(), 1);
}
#[test]
fn test_batch_only_rects() {
let mut batch = DrawBatch::new();
batch.add_rect(0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0);
assert_eq!(batch.batched_draw_calls(), 1);
}
#[test]
fn test_batch_only_lines() {
let mut batch = DrawBatch::new();
batch.add_line(0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0);
assert_eq!(batch.batched_draw_calls(), 1);
}
#[test]
fn test_batch_all_types() {
let mut batch = DrawBatch::new();
batch.add_circle(0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0);
batch.add_rect(0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0);
batch.add_line(0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0);
assert_eq!(batch.batched_draw_calls(), 3);
assert_eq!(batch.unbatched_draw_calls(), 3);
}
#[test]
fn test_batch_debug() {
let batch = DrawBatch::new();
let debug = format!("{batch:?}");
assert!(debug.contains("DrawBatch"));
}
#[test]
fn test_batch_clone() {
let mut batch = DrawBatch::new();
batch.add_circle(1.0, 2.0, 3.0, 1.0, 0.0, 0.0, 1.0);
let cloned = batch.clone();
assert_eq!(cloned.circles.len(), 1);
assert_eq!(cloned.circles[0][0], 1.0);
}
}