use core::f64::consts::TAU;
use crate::Point;
use super::segments_intersect;
#[must_use]
pub fn normalize_angle(angle: f64) -> f64 {
let normalized = angle % TAU;
if normalized < 0.0 {
normalized + TAU
} else {
normalized
}
}
fn angle_in_span(angle: f64, start: f64, end: f64) -> bool {
if start <= end {
angle >= start && angle <= end
} else {
angle >= start || angle <= end
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct LineSegment {
pub a: Point,
pub b: Point,
}
impl LineSegment {
#[must_use]
pub const fn new(a: Point, b: Point) -> Self {
Self { a, b }
}
#[must_use]
pub fn closest_point(self, p: Point) -> Point {
let dx = self.b.x - self.a.x;
let dy = self.b.y - self.a.y;
let length_squared = dx * dx + dy * dy;
#[allow(clippy::float_cmp)]
if length_squared == 0.0 {
return self.a;
}
let t = (((p.x - self.a.x) * dx + (p.y - self.a.y) * dy) / length_squared).clamp(0.0, 1.0);
Point::new(self.a.x + t * dx, self.a.y + t * dy)
}
#[must_use]
pub fn distance_to(self, other: Self) -> f64 {
if segments_intersect(self, other) {
return 0.0;
}
let from_a = self.a.distance(other.closest_point(self.a));
let from_b = self.b.distance(other.closest_point(self.b));
let to_a = other.a.distance(self.closest_point(other.a));
let to_b = other.b.distance(self.closest_point(other.b));
from_a.min(from_b).min(to_a).min(to_b)
}
}
#[derive(Clone, Copy, Debug)]
pub struct Circle {
pub center: Point,
pub radius: f64,
}
impl Circle {
#[must_use]
pub const fn new(center: Point, radius: f64) -> Self {
Self { center, radius }
}
#[must_use]
pub fn closest_point(self, p: Point) -> Point {
let dx = p.x - self.center.x;
let dy = p.y - self.center.y;
let dist = (dx * dx + dy * dy).sqrt();
#[allow(clippy::float_cmp)]
if dist == 0.0 {
return Point::new(self.center.x + self.radius, self.center.y);
}
let scale = self.radius / dist;
Point::new(self.center.x + dx * scale, self.center.y + dy * scale)
}
#[must_use]
pub fn distance_to_segment(self, segment: LineSegment) -> f64 {
let dist_a = self.center.distance(segment.a);
let dist_b = self.center.distance(segment.b);
match (dist_a < self.radius, dist_b < self.radius) {
(true, false) | (false, true) => 0.0,
(false, false) => {
let closest = segment.closest_point(self.center);
let dist = self.center.distance(closest);
if dist < self.radius {
0.0
} else {
dist - self.radius
}
}
(true, true) => self.radius - dist_a.max(dist_b),
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct Arc {
pub center: Point,
pub radius: f64,
pub start_angle: f64,
pub end_angle: f64,
}
impl Arc {
#[must_use]
pub const fn new(center: Point, radius: f64, start_angle: f64, end_angle: f64) -> Self {
Self {
center,
radius,
start_angle,
end_angle,
}
}
#[must_use]
pub const fn circle(self) -> Circle {
Circle::new(self.center, self.radius)
}
#[must_use]
pub fn point_at(self, angle: f64) -> Point {
Point::new(
self.center.x + self.radius * angle.cos(),
self.center.y + self.radius * angle.sin(),
)
}
#[must_use]
pub fn start_point(self) -> Point {
self.point_at(self.start_angle)
}
#[must_use]
pub fn end_point(self) -> Point {
self.point_at(self.end_angle)
}
#[must_use]
pub fn contains_angle(self, angle: f64) -> bool {
angle_in_span(
normalize_angle(angle),
normalize_angle(self.start_angle),
normalize_angle(self.end_angle),
)
}
#[must_use]
pub fn closest_point(self, p: Point) -> Point {
let angle_to_point = normalize_angle((p.y - self.center.y).atan2(p.x - self.center.x));
let start = normalize_angle(self.start_angle);
let end = normalize_angle(self.end_angle);
let closest_angle = if angle_in_span(angle_to_point, start, end) {
angle_to_point
} else {
let to_start = (angle_to_point - start).abs();
let to_start = to_start.min(TAU - to_start);
let to_end = (angle_to_point - end).abs();
let to_end = to_end.min(TAU - to_end);
if to_start < to_end { start } else { end }
};
self.point_at(closest_angle)
}
#[must_use]
pub fn distance_to_segment(self, segment: LineSegment) -> f64 {
let from_a = segment.a.distance(self.closest_point(segment.a));
let from_b = segment.b.distance(self.closest_point(segment.b));
let mut minimum = from_a.min(from_b);
let arc_start = self.start_point();
minimum = minimum.min(arc_start.distance(segment.closest_point(arc_start)));
let arc_end = self.end_point();
minimum = minimum.min(arc_end.distance(segment.closest_point(arc_end)));
let closest_to_center = segment.closest_point(self.center);
let bearing =
(closest_to_center.y - self.center.y).atan2(closest_to_center.x - self.center.x);
if self.contains_angle(bearing) {
let dist_to_center = self.center.distance(closest_to_center);
minimum = minimum.min((dist_to_center - self.radius).abs());
}
minimum
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use core::f64::consts::{FRAC_PI_2, PI, TAU};
use super::{Arc, Circle, LineSegment, normalize_angle};
use crate::Point;
fn p(x: f64, y: f64) -> Point {
Point::new(x, y)
}
fn seg(ax: f64, ay: f64, bx: f64, by: f64) -> LineSegment {
LineSegment::new(p(ax, ay), p(bx, by))
}
fn near(actual: f64, expected: f64) {
assert!(
(actual - expected).abs() < 1e-12,
"expected {expected}, got {actual}"
);
}
fn near_point(actual: Point, expected: Point) {
near(actual.x, expected.x);
near(actual.y, expected.y);
}
#[test]
fn normalize_angle_lands_in_one_turn() {
near(normalize_angle(0.0), 0.0);
near(normalize_angle(-FRAC_PI_2), TAU - FRAC_PI_2);
near(normalize_angle(TAU + 1.0), 1.0);
assert!(normalize_angle(-1e-9) < TAU);
}
#[test]
fn closest_point_on_segment_projects_and_clamps() {
let segment = seg(0.0, 0.0, 10.0, 0.0);
near_point(segment.closest_point(p(5.0, 3.0)), p(5.0, 0.0));
near_point(segment.closest_point(p(-4.0, 3.0)), p(0.0, 0.0));
near_point(segment.closest_point(p(40.0, 3.0)), p(10.0, 0.0));
}
#[test]
fn a_degenerate_segment_answers_with_its_start() {
let segment = seg(2.0, 2.0, 2.0, 2.0);
near_point(segment.closest_point(p(9.0, 9.0)), p(2.0, 2.0));
}
#[test]
fn crossing_segments_are_zero_apart() {
near(
seg(-1.0, 0.0, 1.0, 0.0).distance_to(seg(0.0, -1.0, 0.0, 1.0)),
0.0,
);
}
#[test]
fn touching_segments_are_zero_apart_via_the_endpoint_sweep() {
near(
seg(0.0, 0.0, 1.0, 0.0).distance_to(seg(1.0, 0.0, 2.0, 0.0)),
0.0,
);
}
#[test]
fn parallel_segments_are_their_offset_apart() {
near(
seg(0.0, 0.0, 1.0, 0.0).distance_to(seg(0.0, 3.0, 1.0, 3.0)),
3.0,
);
}
#[test]
fn closest_point_on_circle_projects_radially() {
let circle = Circle::new(p(0.0, 0.0), 2.0);
near_point(circle.closest_point(p(5.0, 0.0)), p(2.0, 0.0));
near_point(circle.closest_point(p(0.5, 0.0)), p(2.0, 0.0));
}
#[test]
fn a_point_at_the_centre_answers_with_angle_zero() {
let circle = Circle::new(p(3.0, 4.0), 2.0);
near_point(circle.closest_point(p(3.0, 4.0)), p(5.0, 4.0));
}
#[test]
fn circle_to_segment_covers_all_three_cases() {
let circle = Circle::new(p(0.0, 0.0), 2.0);
near(circle.distance_to_segment(seg(0.0, 0.0, 5.0, 0.0)), 0.0);
near(circle.distance_to_segment(seg(-5.0, 1.0, 5.0, 1.0)), 0.0);
near(circle.distance_to_segment(seg(-5.0, 5.0, 5.0, 5.0)), 3.0);
near(circle.distance_to_segment(seg(-0.5, 0.0, 0.5, 0.0)), 1.5);
}
#[test]
fn closest_point_on_arc_projects_within_the_span() {
let arc = Arc::new(p(0.0, 0.0), 1.0, 0.0, FRAC_PI_2);
near_point(
arc.closest_point(p(5.0, 5.0)),
p(0.5_f64.sqrt(), 0.5_f64.sqrt()),
);
}
#[test]
fn closest_point_on_arc_falls_back_to_the_nearer_end() {
let arc = Arc::new(p(0.0, 0.0), 1.0, 0.0, FRAC_PI_2);
near_point(arc.closest_point(p(5.0, -5.0)), p(1.0, 0.0));
near_point(arc.closest_point(p(-5.0, 5.0)), p(0.0, 1.0));
}
#[test]
fn an_arc_span_may_cross_zero() {
let arc = Arc::new(p(0.0, 0.0), 1.0, 3.0 * FRAC_PI_2, FRAC_PI_2);
assert!(arc.contains_angle(0.0));
assert!(arc.contains_angle(-0.5));
assert!(!arc.contains_angle(PI));
near_point(arc.closest_point(p(5.0, 0.0)), p(1.0, 0.0));
}
#[test]
fn arc_to_segment_finds_the_radial_approach() {
let arc = Arc::new(p(0.0, 0.0), 1.0, 0.0, PI);
near(arc.distance_to_segment(seg(-5.0, 4.0, 5.0, 4.0)), 3.0);
}
#[test]
fn arc_to_segment_ignores_the_circle_outside_the_span() {
let arc = Arc::new(p(0.0, 0.0), 1.0, 0.0, PI);
let distance = arc.distance_to_segment(seg(-5.0, -4.0, 5.0, -4.0));
near(distance, 4.0);
}
#[test]
fn arc_touching_a_segment_is_zero() {
let arc = Arc::new(p(0.0, 0.0), 1.0, 0.0, PI);
near(arc.distance_to_segment(seg(1.0, -1.0, 1.0, 1.0)), 0.0);
}
#[test]
fn arc_to_segment_measures_candidates_not_crossings() {
let arc = Arc::new(p(0.0, 0.0), 1.0, 0.0, PI);
near(arc.distance_to_segment(seg(-5.0, 0.5, 5.0, 0.5)), 0.5);
}
}