use crate::prelude::*;
#[must_use]
pub fn tangent_point_to_circle(ext_point: Point, circle: Circle) -> Option<(Point, Point)> {
if circle.r <= 0.0 {
return None;
}
let pc = circle.c - ext_point;
let d_sq = pc.dot(pc);
let r_sq = circle.r * circle.r;
if d_sq <= r_sq {
return None; }
let d = d_sq.sqrt();
if d < 1e-10 {
return None; }
let pt_dist = (d_sq - r_sq).sqrt();
let pc_norm = pc / d;
let perp = point(-pc_norm.y, pc_norm.x);
let proj_dist = r_sq / d;
let perp_dist = circle.r * pt_dist / d;
let base = circle.c - pc_norm * proj_dist;
let t1 = base + perp * perp_dist; let t2 = base - perp * perp_dist;
Some((t1, t2))
}
#[must_use]
pub fn external_tangents_between_circles(
c1: Circle,
c2: Circle,
) -> Option<(Point, Point, Point, Point)> {
if c1.r <= 0.0 || c2.r <= 0.0 {
return None;
}
let c1c2 = c2.c - c1.c;
let d_sq = c1c2.dot(c1c2);
let d = d_sq.sqrt();
if d < 1e-10 {
return None; }
let r_diff = (c1.r - c2.r).abs();
if d < r_diff {
return None; }
if (c1.r - c2.r).abs() < 1e-10 {
let c1c2_norm = c1c2 / d;
let perp = Point::new(-c1c2_norm.y, c1c2_norm.x);
let t1_c1 = c1.c + perp * c1.r;
let t1_c2 = c2.c + perp * c2.r;
let t2_c1 = c1.c - perp * c1.r;
let t2_c2 = c2.c - perp * c2.r;
return Some((t1_c1, t1_c2, t2_c1, t2_c2));
}
let r_diff_signed = c2.r - c1.r;
let h = (c2.c * c1.r - c1.c * c2.r) / r_diff_signed;
let helper_circle = circle(c1.c, r_diff.abs());
let tangents_to_helper = tangent_point_to_circle(h, helper_circle)?;
let (helper_t1, helper_t2) = tangents_to_helper;
let (dir1, _) = (helper_t1 - h).normalize(false);
let (dir2, _) = (helper_t2 - h).normalize(false);
let perp1 = point(-dir1.y, dir1.x);
let perp2 = point(-dir2.y, dir2.x);
let sign1_c1 = if (c1.c - h).perp(dir1) > 0.0 { 1.0 } else { -1.0 };
let sign1_c2 = if (c2.c - h).perp(dir1) > 0.0 { 1.0 } else { -1.0 };
let sign2_c1 = if (c1.c - h).perp(dir2) > 0.0 { 1.0 } else { -1.0 };
let sign2_c2 = if (c2.c - h).perp(dir2) > 0.0 { 1.0 } else { -1.0 };
let t1_c1 = c1.c + perp1 * (c1.r * sign1_c1);
let t1_c2 = c2.c + perp1 * (c2.r * sign1_c2);
let t2_c1 = c1.c + perp2 * (c1.r * sign2_c1);
let t2_c2 = c2.c + perp2 * (c2.r * sign2_c2);
Some((t1_c1, t1_c2, t2_c1, t2_c2))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tangent_point_to_circle_basic() {
let p = point(3.0, 0.0);
let c = circle(point(0.0, 0.0), 1.0);
let result = tangent_point_to_circle(p, c);
assert!(result.is_some());
let (t1, t2) = result.unwrap();
let dist1 = (t1 - c.c).norm();
let dist2 = (t2 - c.c).norm();
assert!((dist1 - c.r).abs() < 1e-10, "t1 not on circle: dist={}", dist1);
assert!((dist2 - c.r).abs() < 1e-10, "t2 not on circle: dist={}", dist2);
let radius1 = t1 - c.c;
let tangent1 = t1 - p;
let dot1 = radius1.dot(tangent1);
assert!(dot1.abs() < 1e-9, "Tangent 1 not perpendicular: dot={}", dot1);
let radius2 = t2 - c.c;
let tangent2 = t2 - p;
let dot2 = radius2.dot(tangent2);
assert!(dot2.abs() < 1e-9, "Tangent 2 not perpendicular: dot={}", dot2);
}
#[test]
fn test_tangent_point_to_circle_point_inside() {
let p = point(0.5, 0.0);
let c = circle(point(0.0, 0.0), 1.0);
let result = tangent_point_to_circle(p, c);
assert!(result.is_none());
}
#[test]
fn test_tangent_point_to_circle_point_on_circle() {
let p = point(1.0, 0.0);
let c = circle(point(0.0, 0.0), 1.0);
let result = tangent_point_to_circle(p, c);
assert!(result.is_none());
}
#[test]
fn test_tangent_point_to_circle_point_at_center() {
let p = point(0.0, 0.0);
let c = circle(point(0.0, 0.0), 1.0);
let result = tangent_point_to_circle(p, c);
assert!(result.is_none());
}
#[test]
fn test_external_tangents_equal_radii() {
let c1 = circle(point(0.0, 0.0), 1.0);
let c2 = circle(point(5.0, 0.0), 1.0);
let result = external_tangents_between_circles(c1, c2);
assert!(result.is_some());
let (t1_c1, t1_c2, t2_c1, t2_c2) = result.unwrap();
assert!(((t1_c1 - c1.c).norm() - c1.r).abs() < 1e-10);
assert!(((t1_c2 - c2.c).norm() - c2.r).abs() < 1e-10);
assert!(((t2_c1 - c1.c).norm() - c1.r).abs() < 1e-10);
assert!(((t2_c2 - c2.c).norm() - c2.r).abs() < 1e-10);
let (tangent_dir1, _) = (t1_c2 - t1_c1).normalize(false);
let (tangent_dir2, _) = (t2_c2 - t2_c1).normalize(false);
let cross = tangent_dir1.perp(tangent_dir2);
assert!(cross.abs() < 1e-9, "Tangents not parallel for equal radii");
}
#[test]
fn test_external_tangents_different_radii() {
let c1 = circle(point(0.0, 0.0), 1.0);
let c2 = circle(point(10.0, 0.0), 2.0);
let result = external_tangents_between_circles(c1, c2);
assert!(result.is_some());
let (t1_c1, t1_c2, t2_c1, t2_c2) = result.unwrap();
assert!(((t1_c1 - c1.c).norm() - c1.r).abs() < 1e-9);
assert!(((t1_c2 - c2.c).norm() - c2.r).abs() < 1e-9);
assert!(((t2_c1 - c1.c).norm() - c1.r).abs() < 1e-9);
assert!(((t2_c2 - c2.c).norm() - c2.r).abs() < 1e-9);
}
#[test]
fn test_external_tangents_concentric_circles() {
let c1 = circle(point(0.0, 0.0), 1.0);
let c2 = circle(point(0.0, 0.0), 2.0);
let result = external_tangents_between_circles(c1, c2);
assert!(result.is_none());
}
#[test]
fn test_external_tangents_one_contains_other() {
let c1 = circle(point(0.0, 0.0), 5.0);
let c2 = circle(point(1.0, 0.0), 1.0);
let result = external_tangents_between_circles(c1, c2);
assert!(result.is_none());
}
}