1use alloc::vec::Vec;
25
26use geometry_coords::CoordinateScalar;
27use geometry_cs::{CartesianFamily, CoordinateSystem};
28use geometry_model::{Linestring, Point as ModelPoint, Segment};
29use geometry_tag::SameAs;
30use geometry_trait::{Linestring as LinestringTrait, Point, PointMut, fold_dims, ordinate};
31
32pub trait ClosestPointsStrategy<A, B> {
39 type Out: PointMut + Default;
41
42 fn closest_points(&self, a: &A, b: &B) -> (Self::Out, Self::Out);
48}
49
50#[derive(Debug, Default, Clone, Copy)]
56pub struct CartesianClosestPoints;
57
58impl<T, const D: usize, Cs> ClosestPointsStrategy<ModelPoint<T, D, Cs>, ModelPoint<T, D, Cs>>
64 for CartesianClosestPoints
65where
66 T: CoordinateScalar,
67 Cs: CoordinateSystem,
68 Cs::Family: SameAs<CartesianFamily>,
69 ModelPoint<T, D, Cs>: PointMut + Default + Copy,
70{
71 type Out = ModelPoint<T, D, Cs>;
72
73 #[inline]
74 fn closest_points(
75 &self,
76 a: &ModelPoint<T, D, Cs>,
77 b: &ModelPoint<T, D, Cs>,
78 ) -> (Self::Out, Self::Out) {
79 (*a, *b)
80 }
81}
82
83impl<P> ClosestPointsStrategy<P, Segment<P>> for CartesianClosestPoints
90where
91 P: Point<Scalar = f64> + PointMut + Default + Copy,
92 <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
93{
94 type Out = P;
95
96 #[inline]
97 fn closest_points(&self, p: &P, s: &Segment<P>) -> (Self::Out, Self::Out) {
98 (*p, foot_on_segment(p, s.start(), s.end()))
99 }
100}
101
102impl<P> ClosestPointsStrategy<Segment<P>, Segment<P>> for CartesianClosestPoints
111where
112 P: Point<Scalar = f64> + PointMut + Default + Copy,
113 <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
114{
115 type Out = P;
116
117 fn closest_points(&self, a: &Segment<P>, b: &Segment<P>) -> (Self::Out, Self::Out) {
118 segment_segment_closest(a.start(), a.end(), b.start(), b.end())
119 }
120}
121
122impl<P> ClosestPointsStrategy<Linestring<P>, Linestring<P>> for CartesianClosestPoints
131where
132 P: Point<Scalar = f64> + PointMut + Default + Copy,
133 <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
134{
135 type Out = P;
136
137 fn closest_points(&self, a: &Linestring<P>, b: &Linestring<P>) -> (Self::Out, Self::Out) {
138 let pa: Vec<&P> = a.points().collect();
139 let pb: Vec<&P> = b.points().collect();
140 assert!(
141 pa.len() >= 2 && pb.len() >= 2,
142 "empty or degenerate linestring in closest_points"
143 );
144
145 let mut best: Option<((P, P), f64)> = None;
146 for wa in pa.windows(2) {
147 for wb in pb.windows(2) {
148 let (ca, cb) = segment_segment_closest(wa[0], wa[1], wb[0], wb[1]);
149 let d = squared_distance(&ca, &cb);
150 if best.is_none_or(|(_, bd)| d < bd) {
151 best = Some(((ca, cb), d));
152 }
153 }
154 }
155 best.unwrap().0
156 }
157}
158
159fn foot_on_segment<P>(p: &P, a: &P, b: &P) -> P
166where
167 P: Point<Scalar = f64> + PointMut + Default,
168{
169 let (numerator, denominator) = dots(p, a, b);
170 if denominator <= 0.0 {
171 return copy_point(a);
172 }
173 let t = (numerator / denominator).clamp(0.0, 1.0);
174 blend(a, b, t)
175}
176
177fn segment_segment_closest<P>(a0: &P, a1: &P, b0: &P, b1: &P) -> (P, P)
179where
180 P: Point<Scalar = f64> + PointMut + Default,
181{
182 if P::DIM == 2 {
187 if let Some(pt) = segment_intersection(a0, a1, b0, b1) {
188 return (copy_point(&pt), pt);
189 }
190 }
191
192 let c1 = (copy_point(a0), foot_on_segment(a0, b0, b1));
194 let c2 = (copy_point(a1), foot_on_segment(a1, b0, b1));
195 let c3 = (foot_on_segment(b0, a0, a1), copy_point(b0));
196 let c4 = (foot_on_segment(b1, a0, a1), copy_point(b1));
197
198 let mut best = c1;
199 let mut best_d = squared_distance(&best.0, &best.1);
200 for cand in [c2, c3, c4] {
201 let d = squared_distance(&cand.0, &cand.1);
202 if d < best_d {
203 best_d = d;
204 best = cand;
205 }
206 }
207 best
208}
209
210fn segment_intersection<P>(a0: &P, a1: &P, b0: &P, b1: &P) -> Option<P>
213where
214 P: Point<Scalar = f64> + PointMut + Default,
215{
216 let (x1, y1) = (a0.get::<0>(), a0.get::<1>());
217 let (x2, y2) = (a1.get::<0>(), a1.get::<1>());
218 let (x3, y3) = (b0.get::<0>(), b0.get::<1>());
219 let (x4, y4) = (b1.get::<0>(), b1.get::<1>());
220
221 let denom = (x2 - x1) * (y4 - y3) - (y2 - y1) * (x4 - x3);
222 if denom == 0.0 {
223 return None;
224 }
225 let t = ((x3 - x1) * (y4 - y3) - (y3 - y1) * (x4 - x3)) / denom;
226 let u = ((x3 - x1) * (y2 - y1) - (y3 - y1) * (x2 - x1)) / denom;
227 if (0.0..=1.0).contains(&t) && (0.0..=1.0).contains(&u) {
228 let mut out = P::default();
229 out.set::<0>(x1 + t * (x2 - x1));
230 out.set::<1>(y1 + t * (y2 - y1));
231 Some(out)
232 } else {
233 None
234 }
235}
236
237#[inline]
239fn dots<P: Point<Scalar = f64>>(p: &P, a: &P, b: &P) -> (f64, f64) {
240 fold_dims((0.0, 0.0), p, |(ap_ab, ab_ab), p, d| {
241 let ap = ordinate(p, d) - ordinate(a, d);
242 let ab = ordinate(b, d) - ordinate(a, d);
243 (ap_ab + ap * ab, ab_ab + ab * ab)
244 })
245}
246
247#[inline]
249fn squared_distance<P: Point<Scalar = f64>>(a: &P, b: &P) -> f64 {
250 fold_dims(0.0, a, |sum, a, d| {
251 let delta = ordinate(a, d) - ordinate(b, d);
252 sum + delta * delta
253 })
254}
255
256#[inline]
258fn blend<P>(a: &P, b: &P, t: f64) -> P
259where
260 P: Point<Scalar = f64> + PointMut + Default,
261{
262 let mut out = P::default();
263 geometry_trait::fold_dims((), a, |(), _p, d| {
264 let av = get_dim(a, d);
265 let bv = get_dim(b, d);
266 set_dim(&mut out, d, av + t * (bv - av));
267 });
268 out
269}
270
271#[inline]
274fn copy_point<P>(a: &P) -> P
275where
276 P: Point<Scalar = f64> + PointMut + Default,
277{
278 let mut out = P::default();
279 geometry_trait::fold_dims((), a, |(), _p, d| {
280 set_dim(&mut out, d, get_dim(a, d));
281 });
282 out
283}
284
285#[inline]
286fn get_dim<P: Point<Scalar = f64>>(p: &P, d: usize) -> f64 {
287 match d {
288 0 => p.get::<0>(),
289 1 => p.get::<1>(),
290 2 => p.get::<2>(),
291 3 => p.get::<3>(),
292 _ => unreachable!(),
293 }
294}
295
296#[inline]
297fn set_dim<P: PointMut<Scalar = f64>>(p: &mut P, d: usize, v: f64) {
298 match d {
299 0 => p.set::<0>(v),
300 1 => p.set::<1>(v),
301 2 => p.set::<2>(v),
302 3 => p.set::<3>(v),
303 _ => unreachable!(),
304 }
305}
306
307#[cfg(test)]
308#[allow(
309 clippy::float_cmp,
310 reason = "Closest-point coordinates are exact for these inputs."
311)]
312mod tests {
313 use super::{CartesianClosestPoints, ClosestPointsStrategy};
319 use crate::cartesian::Pythagoras;
320 use crate::distance::DistanceStrategy;
321 use geometry_cs::Cartesian;
322 use geometry_model::{Point2D, Segment};
323 use geometry_trait::Point as _;
324
325 type Pt = Point2D<f64, Cartesian>;
326
327 #[test]
328 fn point_above_segment_drops_perpendicular() {
329 let p = Pt::new(0., 5.);
330 let s = Segment::new(Pt::new(0., 0.), Pt::new(10., 0.));
331 let (a, b) = CartesianClosestPoints.closest_points(&p, &s);
332 assert_eq!((a.get::<0>(), a.get::<1>()), (0., 5.));
333 assert_eq!((b.get::<0>(), b.get::<1>()), (0., 0.));
334 assert!((Pythagoras.distance(&a, &b) - 5.0).abs() < 1e-12);
335 }
336
337 #[test]
338 fn point_on_segment_returns_input() {
339 let p = Pt::new(1., 1.);
340 let s = Segment::new(Pt::new(0., 0.), Pt::new(3., 3.));
341 let (a, b) = CartesianClosestPoints.closest_points(&p, &s);
342 assert!((a.get::<0>() - 1.0).abs() < 1e-12);
343 assert!((b.get::<0>() - 1.0).abs() < 1e-12);
344 assert!(Pythagoras.distance(&a, &b) < 1e-12);
345 }
346
347 #[test]
348 fn point_beyond_segment_clamps_to_endpoint() {
349 let p = Pt::new(6., 1.);
352 let s = Segment::new(Pt::new(1., 4.), Pt::new(4., 1.));
353 let (a, b) = CartesianClosestPoints.closest_points(&p, &s);
354 assert_eq!((b.get::<0>(), b.get::<1>()), (4., 1.));
355 assert!((Pythagoras.distance(&a, &b) - 2.0).abs() < 1e-9);
356 }
357
358 #[test]
359 fn crossing_segments_share_intersection_point() {
360 let a = Segment::new(Pt::new(0., 0.), Pt::new(2., 2.));
361 let b = Segment::new(Pt::new(0., 2.), Pt::new(2., 0.));
362 let (ca, cb) = CartesianClosestPoints.closest_points(&a, &b);
363 assert!((ca.get::<0>() - 1.0).abs() < 1e-12);
364 assert!((ca.get::<1>() - 1.0).abs() < 1e-12);
365 assert!(Pythagoras.distance(&ca, &cb) < 1e-12);
366 }
367
368 #[test]
372 fn three_dimensional_point_on_vertical_segment_is_its_own_foot() {
373 use geometry_model::Point3D;
374 type P3 = Point3D<f64, Cartesian>;
375 let p = P3::new(0., 0., 5.);
376 let s = Segment::new(P3::new(0., 0., 0.), P3::new(0., 0., 10.));
377 let (a, b) = CartesianClosestPoints.closest_points(&p, &s);
378 assert_eq!((a.get::<0>(), a.get::<1>(), a.get::<2>()), (0., 0., 5.));
379 assert_eq!((b.get::<0>(), b.get::<1>(), b.get::<2>()), (0., 0., 5.));
380 let via_distance = crate::PointToSegment::<Pythagoras>::default().distance(&p, &s);
381 assert!((Pythagoras.distance(&a, &b) - via_distance).abs() < 1e-12);
382 }
383}