Skip to main content

geometry_strategy/cartesian/
distance_projected_point.rs

1//! Point-to-segment Cartesian distance via projection onto the segment.
2//!
3//! Mirrors `boost::geometry::strategy::distance::projected_point` from
4//! `boost/geometry/strategies/cartesian/distance_projected_point.hpp`.
5//! The C++ class delegates the final point-point measurement to an
6//! inner strategy (defaulting to `pythagoras<>`) — the Rust port
7//! mirrors that with the [`PointToSegment`] generic parameter `PP`.
8//!
9//! # Algorithm
10//!
11//! For point `P` and segment `A`-`B`:
12//!
13//! 1. Compute the projection parameter
14//!    `t = ((P − A) · (B − A)) / ((B − A) · (B − A))`.
15//! 2. Clamp `t` to `[0, 1]`. When `t ∈ [0, 1]` the foot of the
16//!    perpendicular sits on the segment; otherwise the nearer endpoint
17//!    is the closest point.
18//! 3. Return the inner strategy's distance from `P` to the foot.
19//!
20//! When `A == B` (degenerate segment), `dot(B − A, B − A) == 0` and
21//! the projection step is skipped — the foot is taken as `A`. This
22//! matches the C++ implementation's behaviour through
23//! `closest_points::detail::compute_closest_point_to_segment`
24//! (`strategies/cartesian/closest_points_pt_seg.hpp`).
25
26use 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/// Distance from a point to a segment in Cartesian space.
35///
36/// Generic over the *point-point* sub-strategy `PP` so that comparable
37/// and non-comparable forms compose: `PointToSegment<Pythagoras>`
38/// returns the real distance; `PointToSegment<ComparablePythagoras>`
39/// returns the squared distance.
40///
41/// Mirrors `boost::geometry::strategy::distance::projected_point` from
42/// `boost/geometry/strategies/cartesian/distance_projected_point.hpp`.
43#[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
69// ---- Closest-point kernel --------------------------------------------
70//
71// Mirrors `closest_points::detail::compute_closest_point_to_segment`
72// from `strategies/cartesian/closest_points_pt_seg.hpp`. Split out from
73// the strategy impl so the const-recursive walks below can be reused
74// for each operation (two dot products and one foot-assembly).
75
76/// Compute the closest point on segment `seg` to point `p`.
77///
78/// Builds the segment endpoints `start`, `end` from the indexed
79/// accessors, computes the clamped projection parameter, then
80/// assembles the foot of the perpendicular as a fresh [`Point`].
81fn closest_point_on_segment<P, S>(p: &P, seg: &S) -> P
82where
83    P: PointMut + Default,
84    S: Segment<Point = P>,
85{
86    // Endpoints, reconstructed via the Point trait's `set::<D>` writer.
87    // We avoid `segment_start` / `segment_end` to keep this module
88    // free of the `S::Point: Default` re-statement those helpers would
89    // require — `P: Default` already covers it for the foot below.
90    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    // Degenerate segment: `start == end`. The foot is the start
96    // endpoint, and the distance becomes `|p − start|`. Mirrors the
97    // early-out path in `closest_points_pt_seg.hpp` where division by
98    // zero is avoided by checking `denominator <= 0`.
99    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/// Materialise endpoint `I` of segment `s` into a [`Point`] via the
115/// indexed accessors. Same shape as
116/// [`geometry_trait::segment_start`] / [`geometry_trait::segment_end`],
117/// inlined here to keep the const-generic walk co-located with the
118/// other recursions in this file.
119#[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/// Compute `(dot(p − a, b − a), dot(b − a, b − a))` in one pass.
137#[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/// Assemble the foot `foot[D] = a[D] + t · (b[D] − a[D])` for each
150/// dimension `D ∈ 0..P::DIM`.
151#[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
164// ---- Const-recursive walkers -----------------------------------------
165//
166// Same sealed-trait shape as `distance_pythagoras::SumSquares` and
167// `geometry_trait::segment::WriteDims`. Three operations live on three
168// sibling traits so the recursion bodies stay small.
169
170/// Cursor carrying a `(current, end)` pair of dimensions.
171struct Walk<const I: usize, const N: usize>;
172
173mod sealed {
174    pub trait Sealed<const I: usize, const N: usize> {}
175}
176
177/// Walk that writes endpoint `I_SEG` of a segment into `out` one
178/// dimension at a time, sourcing each coordinate from
179/// `s.get_indexed::<I_SEG, D>()`.
180trait 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
187/// Walk that accumulates `(ap · ab, ab · ab)` one dimension at a time.
188trait 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
192/// Walk that writes `out[D] = a[D] + t · (b[D] − a[D])` for each `D`.
193trait 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
197// Base cases: `I == N` — nothing left to do.
198impl<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
227/// Inductive step for one `(I, N)` pair. We cannot write `I + 1` in a
228/// generic bound on stable Rust, so the macro unrolls one impl per
229/// pair. Keep in sync with `MAX_DIM = 4` matched on above.
230macro_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
274// All `(I, N)` pairs with `0 <= I < N <= 4`.
275impl_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// ---- Tests -----------------------------------------------------------
287
288#[cfg(test)]
289mod tests {
290    //! Reference values mirror
291    //! `geometry/test/strategies/projected_point.cpp:28-31` — the four
292    //! `test_all_2d` cases. The C++ test reads WKT via the
293    //! `test_2d<P1, P2>(wkt_p, wkt_a, wkt_b, expected)` helper; here
294    //! the WKT is translated to literal coordinates.
295
296    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    /// `projected_point.cpp:28` — `POINT(1 1)` to segment
311    /// `(0 0)`-`(2 3)`. The C++ test asserts the literal
312    /// `0.27735203958327` against a `BOOST_CHECK_CLOSE(..., 0.001)`
313    /// (0.001 % relative tolerance, see
314    /// `test/strategies/test_projected_point.hpp:85`) — that literal
315    /// is a rounded value, not the exact answer. Closed form: the
316    /// projection of `(1, 1)` onto the line `(0,0)-(2,3)` has
317    /// parameter `t = 5/13`, foot `(10/13, 15/13)`, and distance
318    /// `sqrt((3/13)² + (−2/13)²) = sqrt(13/169) = 1/sqrt(13)`. We
319    /// assert that closed form to `1e-12`.
320    #[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        // Loose check against the literal in the C++ test — passes
327        // with the same 0.001 % slack the C++ side uses.
328        assert!((d - 0.277_352_039_583_27).abs() < 1e-5);
329    }
330
331    /// `projected_point.cpp:29` — `POINT(2 2)` to segment
332    /// `(1 4)`-`(4 1)` is `0.5 · sqrt(2)`.
333    #[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    /// `projected_point.cpp:30` — `POINT(6 1)` projects beyond the
341    /// `(4 1)` endpoint, so the answer is `|P − (4 1)| = 2`.
342    #[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    /// `projected_point.cpp:31` — `POINT(1 6)` projects before the
350    /// `(1 4)` endpoint, so the answer is `|P − (1 4)| = 2`.
351    #[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    /// Smoke test — perpendicular drop to a horizontal segment with
359    /// the foot strictly between the endpoints. The answer is the
360    /// vertical offset (`y = 7`).
361    #[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    /// Comparable form keeps the squared distance — `7² = 49`.
369    /// Mirrors the `comparable::pythagoras` swap on the inner strategy
370    /// described in
371    /// `distance_projected_point.hpp:64-65` ("If the Strategy is a
372    /// `comparable::pythagoras`, this strategy automatically is a
373    /// comparable `projected_point` strategy (so without sqrt)").
374    #[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    /// `comparable()` on the sqrt-paying form returns a strategy that
382    /// produces the squared distance — same value as constructing
383    /// `PointToSegment<ComparablePythagoras>` directly. Disambiguate
384    /// the trait method via the fully-qualified
385    /// `<_ as DistanceStrategy<Point, Segment>>::comparable` syntax
386    /// so the `<P, S>` projection on the trait impl resolves.
387    #[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    /// Degenerate segment `a == b`: the closest point is `a`, and the
398    /// distance reduces to `|p − a|`. Guards the division-by-zero
399    /// branch in [`super::closest_point_on_segment`].
400    #[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    /// The public strategy rejects dimensions above four while materialising
408    /// endpoints. Direct kernel checks keep the same invariant on the two
409    /// private helpers that cannot otherwise be reached after that early
410    /// rejection.
411    #[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}