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};
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 let Some(pt) = segment_intersection(a0, a1, b0, b1) {
185 return (copy_point(&pt), pt);
186 }
187
188 let c1 = (copy_point(a0), foot_on_segment(a0, b0, b1));
190 let c2 = (copy_point(a1), foot_on_segment(a1, b0, b1));
191 let c3 = (foot_on_segment(b0, a0, a1), copy_point(b0));
192 let c4 = (foot_on_segment(b1, a0, a1), copy_point(b1));
193
194 let mut best = c1;
195 let mut best_d = squared_distance(&best.0, &best.1);
196 for cand in [c2, c3, c4] {
197 let d = squared_distance(&cand.0, &cand.1);
198 if d < best_d {
199 best_d = d;
200 best = cand;
201 }
202 }
203 best
204}
205
206fn segment_intersection<P>(a0: &P, a1: &P, b0: &P, b1: &P) -> Option<P>
209where
210 P: Point<Scalar = f64> + PointMut + Default,
211{
212 let (x1, y1) = (a0.get::<0>(), a0.get::<1>());
213 let (x2, y2) = (a1.get::<0>(), a1.get::<1>());
214 let (x3, y3) = (b0.get::<0>(), b0.get::<1>());
215 let (x4, y4) = (b1.get::<0>(), b1.get::<1>());
216
217 let denom = (x2 - x1) * (y4 - y3) - (y2 - y1) * (x4 - x3);
218 if denom == 0.0 {
219 return None;
220 }
221 let t = ((x3 - x1) * (y4 - y3) - (y3 - y1) * (x4 - x3)) / denom;
222 let u = ((x3 - x1) * (y2 - y1) - (y3 - y1) * (x2 - x1)) / denom;
223 if (0.0..=1.0).contains(&t) && (0.0..=1.0).contains(&u) {
224 let mut out = P::default();
225 out.set::<0>(x1 + t * (x2 - x1));
226 out.set::<1>(y1 + t * (y2 - y1));
227 Some(out)
228 } else {
229 None
230 }
231}
232
233#[inline]
235fn dots<P: Point<Scalar = f64>>(p: &P, a: &P, b: &P) -> (f64, f64) {
236 let apx = p.get::<0>() - a.get::<0>();
237 let apy = p.get::<1>() - a.get::<1>();
238 let abx = b.get::<0>() - a.get::<0>();
239 let aby = b.get::<1>() - a.get::<1>();
240 (apx * abx + apy * aby, abx * abx + aby * aby)
241}
242
243#[inline]
245fn squared_distance<P: Point<Scalar = f64>>(a: &P, b: &P) -> f64 {
246 let dx = a.get::<0>() - b.get::<0>();
247 let dy = a.get::<1>() - b.get::<1>();
248 dx * dx + dy * dy
249}
250
251#[inline]
253fn blend<P>(a: &P, b: &P, t: f64) -> P
254where
255 P: Point<Scalar = f64> + PointMut + Default,
256{
257 let mut out = P::default();
258 geometry_trait::fold_dims((), a, |(), _p, d| {
259 let av = get_dim(a, d);
260 let bv = get_dim(b, d);
261 set_dim(&mut out, d, av + t * (bv - av));
262 });
263 out
264}
265
266#[inline]
269fn copy_point<P>(a: &P) -> P
270where
271 P: Point<Scalar = f64> + PointMut + Default,
272{
273 let mut out = P::default();
274 geometry_trait::fold_dims((), a, |(), _p, d| {
275 set_dim(&mut out, d, get_dim(a, d));
276 });
277 out
278}
279
280#[inline]
281fn get_dim<P: Point<Scalar = f64>>(p: &P, d: usize) -> f64 {
282 match d {
283 0 => p.get::<0>(),
284 1 => p.get::<1>(),
285 2 => p.get::<2>(),
286 3 => p.get::<3>(),
287 _ => unreachable!(),
288 }
289}
290
291#[inline]
292fn set_dim<P: PointMut<Scalar = f64>>(p: &mut P, d: usize, v: f64) {
293 match d {
294 0 => p.set::<0>(v),
295 1 => p.set::<1>(v),
296 2 => p.set::<2>(v),
297 3 => p.set::<3>(v),
298 _ => unreachable!(),
299 }
300}
301
302#[cfg(test)]
303#[allow(
304 clippy::float_cmp,
305 reason = "Closest-point coordinates are exact for these inputs."
306)]
307mod tests {
308 use super::{CartesianClosestPoints, ClosestPointsStrategy};
314 use crate::cartesian::Pythagoras;
315 use crate::distance::DistanceStrategy;
316 use geometry_cs::Cartesian;
317 use geometry_model::{Point2D, Segment};
318 use geometry_trait::Point as _;
319
320 type Pt = Point2D<f64, Cartesian>;
321
322 #[test]
323 fn point_above_segment_drops_perpendicular() {
324 let p = Pt::new(0., 5.);
325 let s = Segment::new(Pt::new(0., 0.), Pt::new(10., 0.));
326 let (a, b) = CartesianClosestPoints.closest_points(&p, &s);
327 assert_eq!((a.get::<0>(), a.get::<1>()), (0., 5.));
328 assert_eq!((b.get::<0>(), b.get::<1>()), (0., 0.));
329 assert!((Pythagoras.distance(&a, &b) - 5.0).abs() < 1e-12);
330 }
331
332 #[test]
333 fn point_on_segment_returns_input() {
334 let p = Pt::new(1., 1.);
335 let s = Segment::new(Pt::new(0., 0.), Pt::new(3., 3.));
336 let (a, b) = CartesianClosestPoints.closest_points(&p, &s);
337 assert!((a.get::<0>() - 1.0).abs() < 1e-12);
338 assert!((b.get::<0>() - 1.0).abs() < 1e-12);
339 assert!(Pythagoras.distance(&a, &b) < 1e-12);
340 }
341
342 #[test]
343 fn point_beyond_segment_clamps_to_endpoint() {
344 let p = Pt::new(6., 1.);
347 let s = Segment::new(Pt::new(1., 4.), Pt::new(4., 1.));
348 let (a, b) = CartesianClosestPoints.closest_points(&p, &s);
349 assert_eq!((b.get::<0>(), b.get::<1>()), (4., 1.));
350 assert!((Pythagoras.distance(&a, &b) - 2.0).abs() < 1e-9);
351 }
352
353 #[test]
354 fn crossing_segments_share_intersection_point() {
355 let a = Segment::new(Pt::new(0., 0.), Pt::new(2., 2.));
356 let b = Segment::new(Pt::new(0., 2.), Pt::new(2., 0.));
357 let (ca, cb) = CartesianClosestPoints.closest_points(&a, &b);
358 assert!((ca.get::<0>() - 1.0).abs() < 1e-12);
359 assert!((ca.get::<1>() - 1.0).abs() < 1e-12);
360 assert!(Pythagoras.distance(&ca, &cb) < 1e-12);
361 }
362}