#![forbid(unsafe_code)]
use axiolid_core::{Frame2, Point2, Scalar, Tolerance, Vec2};
use axiolid_curve::Circle2;
use axiolid_linear::Line2;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Tangent {
Point(Point2),
Line(Line2),
Circle(Circle2),
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub enum TangencyError {
NonFinite,
InvalidRadius {
index: usize,
radius: Scalar,
},
ZeroDirection {
index: usize,
},
Degenerate,
}
impl core::fmt::Display for TangencyError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::NonFinite => f.write_str("a coordinate, direction or radius is not finite"),
Self::InvalidRadius { index, radius } => {
write!(f, "constraint {index} has non-positive radius {radius}")
}
Self::ZeroDirection { index } => {
write!(f, "constraint {index} is a line with no direction")
}
Self::Degenerate => f.write_str("no sign choice pins down a family of tangent circles"),
}
}
}
impl core::error::Error for TangencyError {}
#[derive(Debug, Clone, Copy)]
struct Equation {
a: Scalar,
b: Scalar,
c: Scalar,
d: Scalar,
f: Scalar,
}
fn equation(constraint: &Tangent, sign: Scalar) -> Equation {
match *constraint {
Tangent::Point(p) => Equation {
a: 1.0,
b: -2.0 * p.x,
c: -2.0 * p.y,
d: 0.0,
f: p.x * p.x + p.y * p.y,
},
Tangent::Circle(circle) => {
let o = circle.frame.origin;
let rho = circle.radius;
Equation {
a: 1.0,
b: -2.0 * o.x,
c: -2.0 * o.y,
d: -2.0 * sign * rho,
f: o.x * o.x + o.y * o.y - rho * rho,
}
}
Tangent::Line(line) => {
let n = line.direction.perp().normalize();
Equation {
a: 0.0,
b: n.x,
c: n.y,
d: -sign,
f: -(n.dot(line.origin)),
}
}
}
}
fn sub(lhs: &Equation, rhs: &Equation) -> Equation {
Equation {
a: lhs.a - rhs.a,
b: lhs.b - rhs.b,
c: lhs.c - rhs.c,
d: lhs.d - rhs.d,
f: lhs.f - rhs.f,
}
}
pub fn tangent_circles(
constraints: [Tangent; 3],
tolerance: Tolerance,
) -> Result<Vec<Circle2>, TangencyError> {
validate(&constraints, tolerance)?;
let linear = tolerance.linear();
let mut candidates: Vec<(Point2, Scalar)> = Vec::new();
let mut any_solvable = false;
for bits in 0u8..8 {
let signs = [
if bits & 1 == 0 { -1.0 } else { 1.0 },
if bits & 2 == 0 { -1.0 } else { 1.0 },
if bits & 4 == 0 { -1.0 } else { 1.0 },
];
let eqs = [
equation(&constraints[0], signs[0]),
equation(&constraints[1], signs[1]),
equation(&constraints[2], signs[2]),
];
if let Some(roots) = solve_system(&eqs) {
any_solvable = true;
for (cx, cy, r) in roots {
if r.is_finite() && cx.is_finite() && cy.is_finite() && r > linear {
candidates.push((Point2::new(cx, cy), r));
}
}
}
}
if !any_solvable {
return Err(TangencyError::Degenerate);
}
candidates.sort_by(|a, b| {
a.0.x
.total_cmp(&b.0.x)
.then_with(|| a.0.y.total_cmp(&b.0.y))
.then_with(|| a.1.total_cmp(&b.1))
});
let mut deduped: Vec<(Point2, Scalar)> = Vec::new();
for (centre, radius) in candidates {
let same = deduped
.iter()
.any(|&(c, r)| (c - centre).length() <= linear && (r - radius).abs() <= linear);
if !same {
deduped.push((centre, radius));
}
}
Ok(deduped
.into_iter()
.map(|(centre, radius)| Circle2 {
frame: Frame2 {
origin: centre,
x: Vec2::X,
y: Vec2::Y,
},
radius,
})
.collect())
}
fn validate(constraints: &[Tangent; 3], tolerance: Tolerance) -> Result<(), TangencyError> {
let linear = tolerance.linear();
for (index, constraint) in constraints.iter().enumerate() {
match *constraint {
Tangent::Point(p) => {
if !p.is_finite() {
return Err(TangencyError::NonFinite);
}
}
Tangent::Line(line) => {
if !line.origin.is_finite() || !line.direction.is_finite() {
return Err(TangencyError::NonFinite);
}
if line.direction.length() <= linear {
return Err(TangencyError::ZeroDirection { index });
}
}
Tangent::Circle(circle) => {
if !circle.frame.origin.is_finite() || !circle.radius.is_finite() {
return Err(TangencyError::NonFinite);
}
if circle.radius <= 0.0 {
return Err(TangencyError::InvalidRadius {
index,
radius: circle.radius,
});
}
}
}
}
Ok(())
}
fn solve_system(eqs: &[Equation; 3]) -> Option<Vec<(Scalar, Scalar, Scalar)>> {
let quad: Vec<usize> = (0..3).filter(|&i| eqs[i].a != 0.0).collect();
if quad.is_empty() {
return solve_linear3(eqs).map(|root| vec![root]);
}
let base = quad[0];
let mut lines: Vec<Equation> = Vec::with_capacity(2);
for i in 0..3 {
if i == base {
continue;
}
lines.push(if eqs[i].a != 0.0 {
sub(&eqs[base], &eqs[i])
} else {
eqs[i]
});
}
let (m1, n1, m2, n2) = solve_cxy_in_r(&lines[0], &lines[1])?;
let e = &eqs[base];
let a_coef = m1 * m1 + m2 * m2 - 1.0;
let b_coef = 2.0 * (m1 * n1 + m2 * n2) + e.b * m1 + e.c * m2 + e.d;
let c_coef = n1 * n1 + n2 * n2 + e.b * n1 + e.c * n2 + e.f;
Some(
solve_quadratic(a_coef, b_coef, c_coef)
.into_iter()
.map(|r| (m1 * r + n1, m2 * r + n2, r))
.collect(),
)
}
fn solve_cxy_in_r(l0: &Equation, l1: &Equation) -> Option<(Scalar, Scalar, Scalar, Scalar)> {
let det = l0.b * l1.c - l1.b * l0.c;
let scale = (l0.b.hypot(l0.c) * l1.b.hypot(l1.c)).max(1.0);
if det.abs() <= Scalar::EPSILON * 1e8 * scale {
return None;
}
let m1 = (l1.d * l0.c - l0.d * l1.c) / det;
let n1 = (l1.f * l0.c - l0.f * l1.c) / det;
let m2 = (l1.b * l0.d - l0.b * l1.d) / det;
let n2 = (l1.b * l0.f - l0.b * l1.f) / det;
Some((m1, n1, m2, n2))
}
fn solve_linear3(eqs: &[Equation; 3]) -> Option<(Scalar, Scalar, Scalar)> {
let rows = [
[eqs[0].b, eqs[0].c, eqs[0].d],
[eqs[1].b, eqs[1].c, eqs[1].d],
[eqs[2].b, eqs[2].c, eqs[2].d],
];
let rhs = [-eqs[0].f, -eqs[1].f, -eqs[2].f];
let det = det3(rows);
let scale = rows
.iter()
.map(|row| row[0].hypot(row[1]).hypot(row[2]))
.fold(1.0, Scalar::max);
if det.abs() <= Scalar::EPSILON * 1e8 * scale * scale * scale {
return None;
}
let cx = det3(replace_col(rows, 0, rhs)) / det;
let cy = det3(replace_col(rows, 1, rhs)) / det;
let r = det3(replace_col(rows, 2, rhs)) / det;
Some((cx, cy, r))
}
fn det3(m: [[Scalar; 3]; 3]) -> Scalar {
m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
- m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
+ m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0])
}
fn replace_col(mut m: [[Scalar; 3]; 3], col: usize, values: [Scalar; 3]) -> [[Scalar; 3]; 3] {
for row in 0..3 {
m[row][col] = values[row];
}
m
}
fn solve_quadratic(a: Scalar, b: Scalar, c: Scalar) -> Vec<Scalar> {
let scale = a.abs().max(b.abs()).max(c.abs()).max(1.0);
if a.abs() <= Scalar::EPSILON * 1e8 * scale {
if b.abs() <= Scalar::EPSILON * 1e8 * scale {
return Vec::new();
}
return vec![-c / b];
}
let discriminant = b * b - 4.0 * a * c;
if discriminant < 0.0 {
return Vec::new();
}
if discriminant <= Scalar::EPSILON * 1e8 * scale * scale {
return vec![-b / (2.0 * a)];
}
let sqrt_d = discriminant.sqrt();
let q = if b >= 0.0 {
-0.5 * (b + sqrt_d)
} else {
-0.5 * (b - sqrt_d)
};
if q.abs() <= Scalar::EPSILON * 1e8 * scale {
return vec![-b / (2.0 * a)];
}
let mut roots = vec![q / a, c / q];
roots.sort_by(Scalar::total_cmp);
roots
}