use nalgebra::{SVector, Vector2, Vector3, Vector4};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Circle {
pub center: Vector2<f64>,
pub radius: f64,
}
impl Circle {
pub fn new(center: Vector2<f64>, radius: f64) -> Self {
assert!(radius >= 0.0, "radius must be non-negative");
Self { center, radius }
}
pub fn power(&self, x: &Vector2<f64>) -> f64 {
(x - self.center).norm_squared() - self.radius.powi(2)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Sphere<const D: usize> {
pub center: SVector<f64, D>,
pub radius: f64,
}
impl<const D: usize> Sphere<D> {
pub fn new(center: SVector<f64, D>, radius: f64) -> Self {
assert!(radius >= 0.0, "radius must be non-negative");
Self { center, radius }
}
pub fn power(&self, x: &SVector<f64, D>) -> f64 {
(x - self.center).norm_squared() - self.radius.powi(2)
}
}
pub fn lift_circle(c: &Circle) -> Vector3<f64> {
let x = c.center.x;
let y = c.center.y;
Vector3::new(x, y, x * x + y * y - c.radius.powi(2))
}
pub fn lift_sphere_2d(s: &Sphere<2>) -> Vector3<f64> {
let x = s.center[0];
let y = s.center[1];
Vector3::new(x, y, s.center.norm_squared() - s.radius.powi(2))
}
pub fn lift_sphere_3d(s: &Sphere<3>) -> Vector4<f64> {
let mut v = Vector4::zeros();
for i in 0..3 {
v[i] = s.center[i];
}
v[3] = s.center.norm_squared() - s.radius.powi(2);
v
}
pub fn radical_line_coeffs(c1: &Circle, c2: &Circle) -> (f64, f64, f64) {
let a = 2.0 * (c2.center.x - c1.center.x);
let b = 2.0 * (c2.center.y - c1.center.y);
let c = c1.center.norm_squared()
- c1.radius.powi(2)
- c2.center.norm_squared()
+ c2.radius.powi(2);
(a, b, c)
}
pub fn lassak_normal(circles: &[Circle; 3]) -> Vector2<f64> {
let p1 = lift_circle(&circles[0]);
let p2 = lift_circle(&circles[1]);
let p3 = lift_circle(&circles[2]);
let v1 = p2 - p1;
let v2 = p3 - p1;
let normal = v1.cross(&v2);
Vector2::new(normal.x, normal.y)
}
pub fn lassak_normal_3d(spheres: &[Sphere<3>; 4]) -> Vector3<f64> {
let p1 = lift_sphere_3d(&spheres[0]);
let p2 = lift_sphere_3d(&spheres[1]);
let p3 = lift_sphere_3d(&spheres[2]);
let p4 = lift_sphere_3d(&spheres[3]);
let v1 = p2 - p1;
let v2 = p3 - p1;
let v3 = p4 - p1;
let normal_4d = Vector4::new(
det3(
(v1[1], v1[2], v1[3]),
(v2[1], v2[2], v2[3]),
(v3[1], v3[2], v3[3]),
),
-det3(
(v1[0], v1[2], v1[3]),
(v2[0], v2[2], v2[3]),
(v3[0], v3[2], v3[3]),
),
det3(
(v1[0], v1[1], v1[3]),
(v2[0], v2[1], v2[3]),
(v3[0], v3[1], v3[3]),
),
-det3(
(v1[0], v1[1], v1[2]),
(v2[0], v2[1], v2[2]),
(v3[0], v3[1], v3[2]),
),
);
Vector3::new(normal_4d[0], normal_4d[1], normal_4d[2])
}
fn det3(a: (f64, f64, f64), b: (f64, f64, f64), c: (f64, f64, f64)) -> f64 {
a.0 * (b.1 * c.2 - b.2 * c.1) - a.1 * (b.0 * c.2 - b.2 * c.0) + a.2 * (b.0 * c.1 - b.1 * c.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_circle_power() {
let c = Circle::new(Vector2::new(0.0, 0.0), 1.0);
assert!((c.power(&Vector2::new(1.0, 0.0))).abs() < 1e-12);
assert!((c.power(&Vector2::new(0.0, 0.0)) + 1.0).abs() < 1e-12);
assert!((c.power(&Vector2::new(2.0, 0.0)) - 3.0).abs() < 1e-12);
}
#[test]
fn test_lift_identity() {
let c = Circle::new(Vector2::new(3.0, 4.0), 2.0);
let p = lift_circle(&c);
assert!((p.x - 3.0).abs() < 1e-12);
assert!((p.y - 4.0).abs() < 1e-12);
assert!((p.z - 21.0).abs() < 1e-12);
}
#[test]
fn test_sphere_generic() {
let s: Sphere<3> = Sphere::new(Vector3::new(1.0, 2.0, 3.0), 5.0);
assert!((s.center.norm() - (14.0_f64).sqrt()).abs() < 1e-12);
assert!((s.radius - 5.0).abs() < 1e-12);
}
#[test]
fn test_lassak_normal_is_nonzero() {
let circles = [
Circle::new(Vector2::new(0.0, 0.0), 1.0),
Circle::new(Vector2::new(4.0, 0.0), 2.0),
Circle::new(Vector2::new(2.0, 3.0), 1.5),
];
let n = lassak_normal(&circles);
assert!(n.norm() > 1e-10, "Lassak normal should be non-zero");
}
#[test]
fn test_radical_line_coeffs_simple() {
let c1 = Circle::new(Vector2::new(0.0, 0.0), 1.0);
let c2 = Circle::new(Vector2::new(4.0, 0.0), 2.0);
let (a, b, c) = radical_line_coeffs(&c1, &c2);
assert!((a - 8.0).abs() < 1e-12);
assert!((b).abs() < 1e-12);
assert!((c + 13.0).abs() < 1e-12);
}
}