1use geometry_coords::CoordinateScalar;
27use geometry_cs::{CartesianFamily, CoordinateSystem};
28use geometry_tag::SameAs;
29use geometry_trait::{Point, PointMut, Segment};
30
31use crate::cartesian::Pythagoras;
32use crate::distance::DistanceStrategy;
33
34#[derive(Debug, Default, Clone, Copy)]
44pub struct PointToSegment<PP = Pythagoras>(pub PP);
45
46impl<P, S, PP> DistanceStrategy<P, S> for PointToSegment<PP>
47where
48 P: PointMut + Default,
49 S: Segment<Point = P>,
50 <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
51 PP: DistanceStrategy<P, P, Out = P::Scalar>,
52 PP::Comparable: DistanceStrategy<P, P, Out = P::Scalar>,
53{
54 type Out = P::Scalar;
55 type Comparable = PointToSegment<PP::Comparable>;
56
57 #[inline]
58 fn distance(&self, p: &P, s: &S) -> Self::Out {
59 let foot = closest_point_on_segment::<P, S>(p, s);
60 self.0.distance(p, &foot)
61 }
62
63 #[inline]
64 fn comparable(&self) -> Self::Comparable {
65 PointToSegment(self.0.comparable())
66 }
67}
68
69fn closest_point_on_segment<P, S>(p: &P, seg: &S) -> P
82where
83 P: PointMut + Default,
84 S: Segment<Point = P>,
85{
86 let start = endpoint::<P, S, 0>(seg);
91 let end = endpoint::<P, S, 1>(seg);
92
93 let (numerator, denominator) = dots::<P>(p, &start, &end);
94
95 if denominator <= P::Scalar::ZERO {
100 return start;
101 }
102
103 let t = numerator / denominator;
104
105 if t <= P::Scalar::ZERO {
106 start
107 } else if t >= P::Scalar::ONE {
108 end
109 } else {
110 assemble_foot::<P>(&start, &end, t)
111 }
112}
113
114#[inline]
120fn endpoint<P, S, const I: usize>(s: &S) -> P
121where
122 P: PointMut + Default,
123 S: Segment<Point = P>,
124{
125 let mut out: P = Default::default();
126 match P::DIM {
127 1 => <Walk<0, 1> as WriteEndpoint<0, 1>>::run::<P, S, I>(s, &mut out),
128 2 => <Walk<0, 2> as WriteEndpoint<0, 2>>::run::<P, S, I>(s, &mut out),
129 3 => <Walk<0, 3> as WriteEndpoint<0, 3>>::run::<P, S, I>(s, &mut out),
130 4 => <Walk<0, 4> as WriteEndpoint<0, 4>>::run::<P, S, I>(s, &mut out),
131 _ => panic!("PointToSegment: P::DIM exceeds MAX_DIM (4)"),
132 }
133 out
134}
135
136#[inline]
138fn dots<P: Point>(p: &P, a: &P, b: &P) -> (P::Scalar, P::Scalar) {
139 let init = (P::Scalar::ZERO, P::Scalar::ZERO);
140 match P::DIM {
141 1 => <Walk<0, 1> as Dots<0, 1>>::run::<P>(init, p, a, b),
142 2 => <Walk<0, 2> as Dots<0, 2>>::run::<P>(init, p, a, b),
143 3 => <Walk<0, 3> as Dots<0, 3>>::run::<P>(init, p, a, b),
144 4 => <Walk<0, 4> as Dots<0, 4>>::run::<P>(init, p, a, b),
145 _ => panic!("PointToSegment: P::DIM exceeds MAX_DIM (4)"),
146 }
147}
148
149#[inline]
152fn assemble_foot<P: PointMut + Default>(a: &P, b: &P, t: P::Scalar) -> P {
153 let mut out: P = Default::default();
154 match P::DIM {
155 1 => <Walk<0, 1> as AssembleFoot<0, 1>>::run::<P>(a, b, t, &mut out),
156 2 => <Walk<0, 2> as AssembleFoot<0, 2>>::run::<P>(a, b, t, &mut out),
157 3 => <Walk<0, 3> as AssembleFoot<0, 3>>::run::<P>(a, b, t, &mut out),
158 4 => <Walk<0, 4> as AssembleFoot<0, 4>>::run::<P>(a, b, t, &mut out),
159 _ => panic!("PointToSegment: P::DIM exceeds MAX_DIM (4)"),
160 }
161 out
162}
163
164struct Walk<const I: usize, const N: usize>;
172
173mod sealed {
174 pub trait Sealed<const I: usize, const N: usize> {}
175}
176
177trait WriteEndpoint<const I: usize, const N: usize>: sealed::Sealed<I, N> {
181 fn run<P, S, const I_SEG: usize>(s: &S, out: &mut P)
182 where
183 P: PointMut,
184 S: Segment<Point = P>;
185}
186
187trait Dots<const I: usize, const N: usize>: sealed::Sealed<I, N> {
189 fn run<P: Point>(acc: (P::Scalar, P::Scalar), p: &P, a: &P, b: &P) -> (P::Scalar, P::Scalar);
190}
191
192trait AssembleFoot<const I: usize, const N: usize>: sealed::Sealed<I, N> {
194 fn run<P: PointMut>(a: &P, b: &P, t: P::Scalar, out: &mut P);
195}
196
197impl<const N: usize> sealed::Sealed<N, N> for Walk<N, N> {}
199
200impl<const N: usize> WriteEndpoint<N, N> for Walk<N, N> {
201 #[inline]
202 fn run<P, S, const I_SEG: usize>(_s: &S, _out: &mut P)
203 where
204 P: PointMut,
205 S: Segment<Point = P>,
206 {
207 }
208}
209
210impl<const N: usize> Dots<N, N> for Walk<N, N> {
211 #[inline]
212 fn run<P: Point>(
213 acc: (P::Scalar, P::Scalar),
214 _p: &P,
215 _a: &P,
216 _b: &P,
217 ) -> (P::Scalar, P::Scalar) {
218 acc
219 }
220}
221
222impl<const N: usize> AssembleFoot<N, N> for Walk<N, N> {
223 #[inline]
224 fn run<P: PointMut>(_a: &P, _b: &P, _t: P::Scalar, _out: &mut P) {}
225}
226
227macro_rules! impl_walk {
231 ($i:expr, $n:expr) => {
232 impl sealed::Sealed<$i, $n> for Walk<$i, $n> {}
233
234 impl WriteEndpoint<$i, $n> for Walk<$i, $n> {
235 #[inline]
236 fn run<P, S, const I_SEG: usize>(s: &S, out: &mut P)
237 where
238 P: PointMut,
239 S: Segment<Point = P>,
240 {
241 let v = s.get_indexed::<I_SEG, $i>();
242 <P as PointMut>::set::<$i>(out, v);
243 <Walk<{ $i + 1 }, $n> as WriteEndpoint<{ $i + 1 }, $n>>::run::<P, S, I_SEG>(s, out);
244 }
245 }
246
247 impl Dots<$i, $n> for Walk<$i, $n> {
248 #[inline]
249 fn run<P: Point>(
250 acc: (P::Scalar, P::Scalar),
251 p: &P,
252 a: &P,
253 b: &P,
254 ) -> (P::Scalar, P::Scalar) {
255 let ap = p.get::<$i>() - a.get::<$i>();
256 let ab = b.get::<$i>() - a.get::<$i>();
257 let acc = (acc.0 + ap * ab, acc.1 + ab * ab);
258 <Walk<{ $i + 1 }, $n> as Dots<{ $i + 1 }, $n>>::run::<P>(acc, p, a, b)
259 }
260 }
261
262 impl AssembleFoot<$i, $n> for Walk<$i, $n> {
263 #[inline]
264 fn run<P: PointMut>(a: &P, b: &P, t: P::Scalar, out: &mut P) {
265 let ad = a.get::<$i>();
266 let bd = b.get::<$i>();
267 <P as PointMut>::set::<$i>(out, ad + t * (bd - ad));
268 <Walk<{ $i + 1 }, $n> as AssembleFoot<{ $i + 1 }, $n>>::run::<P>(a, b, t, out);
269 }
270 }
271 };
272}
273
274impl_walk!(0, 1);
276impl_walk!(0, 2);
277impl_walk!(1, 2);
278impl_walk!(0, 3);
279impl_walk!(1, 3);
280impl_walk!(2, 3);
281impl_walk!(0, 4);
282impl_walk!(1, 4);
283impl_walk!(2, 4);
284impl_walk!(3, 4);
285
286#[cfg(test)]
289mod tests {
290 use super::{PointToSegment, assemble_foot, dots};
297 use crate::cartesian::{ComparablePythagoras, Pythagoras};
298 use crate::distance::DistanceStrategy;
299 use geometry_cs::Cartesian;
300 use geometry_model::{Point as ModelPoint, Point2D, Segment};
301
302 fn pt(x: f64, y: f64) -> Point2D<f64, Cartesian> {
303 Point2D::<f64, Cartesian>::new(x, y)
304 }
305
306 fn seg(ax: f64, ay: f64, bx: f64, by: f64) -> Segment<Point2D<f64, Cartesian>> {
307 Segment::new(pt(ax, ay), pt(bx, by))
308 }
309
310 #[test]
321 fn proj_case_1() {
322 let d = PointToSegment::<Pythagoras>::default()
323 .distance(&pt(1.0, 1.0), &seg(0.0, 0.0, 2.0, 3.0));
324 let expected = 1.0_f64 / 13.0_f64.sqrt();
325 assert!((d - expected).abs() < 1e-12);
326 assert!((d - 0.277_352_039_583_27).abs() < 1e-5);
329 }
330
331 #[test]
334 fn proj_case_2() {
335 let d = PointToSegment::<Pythagoras>::default()
336 .distance(&pt(2.0, 2.0), &seg(1.0, 4.0, 4.0, 1.0));
337 assert!((d - 0.5 * 2.0_f64.sqrt()).abs() < 1e-12);
338 }
339
340 #[test]
343 fn proj_clamps_to_end() {
344 let d = PointToSegment::<Pythagoras>::default()
345 .distance(&pt(6.0, 1.0), &seg(1.0, 4.0, 4.0, 1.0));
346 assert!((d - 2.0).abs() < 1e-12);
347 }
348
349 #[test]
352 fn proj_clamps_to_start() {
353 let d = PointToSegment::<Pythagoras>::default()
354 .distance(&pt(1.0, 6.0), &seg(1.0, 4.0, 4.0, 1.0));
355 assert!((d - 2.0).abs() < 1e-12);
356 }
357
358 #[test]
362 fn perpendicular_distance() {
363 let d = PointToSegment::<Pythagoras>::default()
364 .distance(&pt(5.0, 7.0), &seg(0.0, 0.0, 10.0, 0.0));
365 assert!((d - 7.0).abs() < 1e-12);
366 }
367
368 #[test]
375 fn comparable_form_returns_squared_distance() {
376 let cmp = PointToSegment::<ComparablePythagoras>::default()
377 .distance(&pt(5.0, 7.0), &seg(0.0, 0.0, 10.0, 0.0));
378 assert!((cmp - 49.0).abs() < 1e-12);
379 }
380
381 #[test]
388 fn comparable_method_swaps_inner_strategy() {
389 type P = Point2D<f64, Cartesian>;
390 type S = Segment<P>;
391 let real = PointToSegment::<Pythagoras>::default();
392 let cmp = <PointToSegment<Pythagoras> as DistanceStrategy<P, S>>::comparable(&real);
393 let d = cmp.distance(&pt(5.0, 7.0), &seg(0.0, 0.0, 10.0, 0.0));
394 assert!((d - 49.0).abs() < 1e-12);
395 }
396
397 #[test]
401 fn degenerate_segment_falls_back_to_endpoint_distance() {
402 let d = PointToSegment::<Pythagoras>::default()
403 .distance(&pt(3.0, 4.0), &seg(0.0, 0.0, 0.0, 0.0));
404 assert!((d - 5.0).abs() < 1e-12);
405 }
406
407 #[test]
412 fn private_projection_helpers_reject_dimensions_above_four() {
413 type P5 = ModelPoint<f64, 5, Cartesian>;
414 let p = P5::default();
415 assert!(std::panic::catch_unwind(|| dots(&p, &p, &p)).is_err());
416 assert!(std::panic::catch_unwind(|| assemble_foot(&p, &p, 0.5)).is_err());
417 }
418}