Skip to main content

geometry_trait/
point.rs

1//! The [`Point`] concept and the [`fold_dims`] dimension iterator.
2//!
3//! Mirrors `boost/geometry/geometries/concepts/point_concept.hpp`
4//! together with the per-dimension `traits::access<P, D>::{get,set}`
5//! free-function pair declared in
6//! `boost/geometry/core/access.hpp:270-313`.
7//!
8//! Design notes:
9//! * The five C++ trait specialisations collapse to four associated
10//!   items on one trait plus the super-bound on [`Geometry`].
11//! * `get` and `set` take the dimension as a *const generic*, the
12//!   Rust analogue of Boost's `template <std::size_t Dim>` parameter
13//!   on `traits::access`. Out-of-range access becomes a compile
14//!   error (cf. `tests/ui/get_out_of_range.rs`).
15//! * Dimension-by-dimension recursion (the C++ idiom in
16//!   `strategies/cartesian/distance_pythagoras.hpp:44-66`) is
17//!   expressed by [`fold_dims`], which descends through a private
18//!   sealed [`Recurse`] trait.
19//!
20//! Phase 01 split (KC1.T1): the v1 `Point` trait fused `Point` and
21//! `ConstPoint` from the C++ side. It now carries only the *read*
22//! surface; the *write* surface (`set::<D>`) lives on a
23//! [`PointMut`]`: Point` subtrait. Mirrors the `Point` / `ConstPoint`
24//! concept pair in
25//! `boost/geometry/geometries/concepts/point_concept.hpp:82-133`.
26
27use geometry_coords::CoordinateScalar;
28use geometry_cs::CoordinateSystem;
29use geometry_tag::PointTag;
30
31use crate::geometry::Geometry;
32
33/// The Point concept.
34///
35/// Models `doc/concept/point.qbk` — equivalent in C++ to the five
36/// trait specialisations spelled out in
37/// `boost/geometry/geometries/concepts/point_concept.hpp` and the
38/// per-dimension accessor declared in
39/// `boost/geometry/core/access.hpp:270-313`. The five C++ pieces
40/// collapse here to four associated items plus the super-bound:
41///
42/// | C++ trait | Rust counterpart |
43/// |---|---|
44/// | `traits::tag<P>` (`core/tag.hpp`) | `Geometry<Kind = PointTag>` |
45/// | `traits::dimension<P>` (`core/coordinate_dimension.hpp`) | `const DIM: usize` |
46/// | `traits::coordinate_type<P>` (`core/coordinate_type.hpp`) | `type Scalar` |
47/// | `traits::coordinate_system<P>` (`core/coordinate_system.hpp`) | `type Cs` |
48/// | `traits::access<P, D>::get` (`core/access.hpp:270-313`) | `fn get::<D>` |
49///
50/// The read-write half — `traits::access<P, D>::set` — lives on the
51/// [`PointMut`] subtrait, mirroring the way Boost's mutating `Point`
52/// concept sits above the read-only `ConstPoint`.
53///
54/// The `Point = Self` projection on the [`Geometry`] super-trait is
55/// the Rust spelling of Boost's "a Point is its own point type"
56/// invariant — `point_type<P>::type = P` whenever
57/// `tag<P>::type = point_tag`.
58///
59/// # Examples
60///
61/// ```
62/// use geometry_cs::Cartesian;
63/// use geometry_tag::PointTag;
64/// use geometry_trait::{Geometry, Point};
65///
66/// #[derive(Default)]
67/// struct Xy { x: f64, y: f64 }
68///
69/// impl Geometry for Xy { type Kind = PointTag; type Point = Self; }
70///
71/// impl Point for Xy {
72///     type Scalar = f64;
73///     type Cs = Cartesian;
74///     const DIM: usize = 2;
75///     fn get<const D: usize>(&self) -> f64 { if D == 0 { self.x } else { self.y } }
76/// }
77///
78/// let p = Xy { x: 3.0, y: 4.0 };
79/// assert_eq!(p.get::<0>(), 3.0);
80/// ```
81#[diagnostic::on_unimplemented(
82    message = "`{Self}` does not implement the Point concept",
83    label = "implement `Point` for this type, or wrap it in `Adapt` / `WithCs`",
84    note = "see `geometry_trait::Point` — four items are required \
85            (Kind = PointTag, Point = Self, Scalar, Cs, DIM, get::<D>). \
86            If you also need to *write* coordinates, additionally \
87            implement `PointMut`."
88)]
89pub trait Point: Geometry<Kind = PointTag, Point = Self> + Sized {
90    /// The scalar coordinate type.
91    ///
92    /// Mirrors `boost::geometry::traits::coordinate_type<P>::type`
93    /// (`boost/geometry/core/coordinate_type.hpp`).
94    type Scalar: CoordinateScalar;
95
96    /// The coordinate system this point lives in.
97    ///
98    /// Mirrors `boost::geometry::traits::coordinate_system<P>::type`
99    /// (`boost/geometry/core/coordinate_system.hpp`).
100    type Cs: CoordinateSystem;
101
102    /// The number of dimensions.
103    ///
104    /// Mirrors `boost::geometry::traits::dimension<P>::value`
105    /// (`boost/geometry/core/coordinate_dimension.hpp`).
106    const DIM: usize;
107
108    /// Read coordinate `D`.
109    ///
110    /// Mirrors `boost::geometry::traits::access<P, D>::get(p)`
111    /// (`boost/geometry/core/access.hpp:270-313`). `D` is a const
112    /// generic: passing an out-of-range index is rejected at the
113    /// call site rather than at runtime.
114    fn get<const D: usize>(&self) -> Self::Scalar;
115    // The read-write half — `set::<D>` — lives on [`PointMut`] below.
116}
117
118/// The *mutating* half of the Point concept.
119///
120/// Mirrors Boost's read-write `Point` concept sitting above the
121/// read-only `ConstPoint` in
122/// `boost/geometry/geometries/concepts/point_concept.hpp:82-133`.
123/// Algorithms that need to *construct* a Point — e.g. the
124/// [`segment_start`](crate::segment_start) /
125/// [`segment_end`](crate::segment_end) materialisers, the
126/// [`box_min`](crate::box_min) / [`box_max`](crate::box_max) helpers,
127/// the envelope output builder in `geometry-strategy::envelope` —
128/// bound on `PointMut`; everything
129/// else (`distance`, `length`, `area`, `within`, `intersects`,
130/// `equals`, `comparable_distance`) bounds on the read-only
131/// [`Point`] only.
132///
133/// Implementers that already provide [`Point`] get `PointMut` for
134/// almost free: add `impl PointMut for MyPoint { fn set::<D>(…) }`
135/// next to the existing `impl Point`. Every kernel concrete `Point`
136/// impl (model, derive, `Adapt` array/tuple, `WithCs`) ships the
137/// matching `PointMut` impl.
138///
139/// # Examples
140///
141/// ```
142/// use geometry_cs::Cartesian;
143/// use geometry_tag::PointTag;
144/// use geometry_trait::{Geometry, Point, PointMut};
145///
146/// #[derive(Default)]
147/// struct Xy { x: f64, y: f64 }
148/// impl Geometry for Xy { type Kind = PointTag; type Point = Self; }
149/// impl Point for Xy {
150///     type Scalar = f64;
151///     type Cs = Cartesian;
152///     const DIM: usize = 2;
153///     fn get<const D: usize>(&self) -> f64 { if D == 0 { self.x } else { self.y } }
154/// }
155/// impl PointMut for Xy {
156///     fn set<const D: usize>(&mut self, v: f64) { if D == 0 { self.x = v } else { self.y = v } }
157/// }
158///
159/// let mut p = Xy::default();
160/// p.set::<0>(3.0);
161/// assert_eq!(p.get::<0>(), 3.0);
162/// ```
163#[diagnostic::on_unimplemented(
164    message = "`{Self}` implements `Point` but not `PointMut`, and this algorithm needs to construct a point",
165    label = "implement `PointMut` for this type, or feed the algorithm a mutable point type instead",
166    note = "see `geometry_trait::PointMut` — adds `set::<D>` next to the inherited `get::<D>`. \
167            Algorithms that only read coordinates (distance, length, area, within, intersects, \
168            equals, comparable_distance) require `Point` only — this bound is reached when the \
169            algorithm needs to *output* a Point (envelope, segment_start, box_min, …)."
170)]
171pub trait PointMut: Point {
172    /// Write coordinate `D`.
173    ///
174    /// Mirrors `boost::geometry::traits::access<P, D>::set(p, v)`
175    /// (`boost/geometry/core/access.hpp:270-313`) — the read-write
176    /// half of `traits::access` only available on `Point` (not
177    /// `ConstPoint`) in
178    /// `boost/geometry/geometries/concepts/point_concept.hpp:82-133`.
179    fn set<const D: usize>(&mut self, value: Self::Scalar);
180}
181
182/// Fold a closure over the dimensions `0, 1, …, P::DIM - 1` of a
183/// [`Point`] at compile time.
184///
185/// Used by Pythagoras, equality, transforms — anything that needs
186/// to iterate over coordinates without paying a runtime bounds
187/// check. Mirrors the C++ template-recursion pattern in
188/// `boost/geometry/strategies/cartesian/distance_pythagoras.hpp:44-66`
189/// (the `detail::compute_pythagoras<I, T>` recursion).
190///
191/// The closure is invoked once per dimension, in ascending order.
192/// It receives the running accumulator, the point, and the
193/// dimension index (as a `usize`, only as a label — `get::<D>`
194/// itself must still be called with a const-generic `D`, which is
195/// what the `Recurse` sealed trait below provides for the
196/// dimensions kernel code actually needs).
197///
198/// # Supported dimensions
199///
200/// Const-generic recursion via `I + 1` requires the unstable
201/// `generic_const_exprs` feature. To stay on stable, the
202/// recursive descent is unrolled by `Recurse` for the dimensions
203/// the kernel needs (1‥=`MAX_DIM`). `Point` impls with
204/// `DIM > MAX_DIM` are a compile-time error at the call site —
205/// raise `MAX_DIM` and add the matching `impl_recurse!` row.
206///
207/// # Examples
208///
209/// ```
210/// use geometry_cs::Cartesian;
211/// use geometry_tag::PointTag;
212/// use geometry_trait::{fold_dims, Geometry, Point};
213///
214/// #[derive(Default)]
215/// struct Xy { x: f64, y: f64 }
216/// impl Geometry for Xy { type Kind = PointTag; type Point = Self; }
217/// impl Point for Xy {
218///     type Scalar = f64;
219///     type Cs = Cartesian;
220///     const DIM: usize = 2;
221///     fn get<const D: usize>(&self) -> f64 { if D == 0 { self.x } else { self.y } }
222/// }
223///
224/// let p = Xy { x: 3.0, y: 4.0 };
225/// // Fold the closure over each dimension; the recursion supplies the
226/// // dimension index, the closure does its own per-D dispatch.
227/// let sum = fold_dims(0.0_f64, &p, |acc, p, i| {
228///     let v = match i { 0 => p.get::<0>(), 1 => p.get::<1>(), _ => 0.0 };
229///     acc + v
230/// });
231/// assert_eq!(sum, 7.0);
232/// ```
233///
234/// In practice algorithm code uses [`fold_dims`] indirectly via
235/// the `Recurse` machinery already wired below: the closure can
236/// rely on the dimension index being supplied to it, but
237/// `Point::get::<D>` calls inside the closure are issued with
238/// hard-coded `D`s by the recursion impls themselves.
239///
240/// # Panics
241///
242/// Panics if `P::DIM` exceeds `MAX_DIM`. Raise `MAX_DIM` and
243/// add the matching `impl_recurse!` row to support a wider point.
244pub fn fold_dims<P, T, F>(init: T, point: &P, mut f: F) -> T
245where
246    P: Point,
247    F: FnMut(T, &P, usize) -> T,
248{
249    // `P::DIM` is monomorphisation-time constant but cannot appear in
250    // a const-generic position on stable Rust. We dispatch via a
251    // `match` to the right `Cursor<0, N>` impl; the unreachable arms
252    // collapse to dead code once the compiler sees the concrete `DIM`.
253    match P::DIM {
254        0 => <Cursor<0, 0> as Recurse<0, 0>>::step(init, point, &mut f),
255        1 => <Cursor<0, 1> as Recurse<0, 1>>::step(init, point, &mut f),
256        2 => <Cursor<0, 2> as Recurse<0, 2>>::step(init, point, &mut f),
257        3 => <Cursor<0, 3> as Recurse<0, 3>>::step(init, point, &mut f),
258        4 => <Cursor<0, 4> as Recurse<0, 4>>::step(init, point, &mut f),
259        _ => panic!("fold_dims: DIM exceeds MAX_DIM ({MAX_DIM})"),
260    }
261}
262
263/// Largest `DIM` the const-recursive [`fold_dims`] supports on
264/// stable Rust. Raise this constant and add the corresponding
265/// `impl_recurse!` row below when a strategy needs more.
266///
267/// Three is the working assumption (XYZ); four is included so
268/// future homogeneous-coordinate kernels do not stall waiting for
269/// `generic_const_exprs` to stabilise.
270pub const MAX_DIM: usize = 4;
271
272/// Marker carrying a `(cursor, end)` pair of dimensions to descend
273/// over. Private; reachable only via [`fold_dims`].
274struct Cursor<const I: usize, const N: usize>;
275
276/// Sealed const-recursive iterator over the dimensions of a
277/// [`Point`]. The two const parameters are the *current* dimension
278/// `I` and the *end* dimension `N` (= `P::DIM`).
279///
280/// Mirrors the `template <size_t I, typename T> compute_pythagoras`
281/// recursion in
282/// `boost/geometry/strategies/cartesian/distance_pythagoras.hpp:44-66`,
283/// except the C++ side counts *down* from `I = DIM` to `0` while
284/// we count *up* — same shape, more natural Rust read order.
285///
286/// On nightly with `generic_const_exprs` this trait would collapse
287/// to a single blanket impl
288/// `impl<const I: usize, const N: usize> Recurse<I, N> for Cursor<I, N>
289///   where Cursor<{I + 1}, N>: Recurse<{I + 1}, N>`. On stable we
290/// unroll the recursion via the [`impl_recurse!`] macro below for
291/// every dimension up to [`MAX_DIM`].
292trait Recurse<const I: usize, const N: usize>: sealed::Sealed<I, N> {
293    fn step<P, T, F>(acc: T, point: &P, f: &mut F) -> T
294    where
295        P: Point,
296        F: FnMut(T, &P, usize) -> T;
297}
298
299mod sealed {
300    pub trait Sealed<const I: usize, const N: usize> {}
301}
302
303/// Base case: `I == N`, nothing left to visit, return the
304/// accumulator. Matches the partial specialisation
305/// `compute_pythagoras<0, T>` in the C++ header.
306impl<const N: usize> sealed::Sealed<N, N> for Cursor<N, N> {}
307impl<const N: usize> Recurse<N, N> for Cursor<N, N> {
308    #[inline]
309    fn step<P, T, F>(acc: T, _point: &P, _f: &mut F) -> T
310    where
311        P: Point,
312        F: FnMut(T, &P, usize) -> T,
313    {
314        acc
315    }
316}
317
318/// Inductive step for one fixed `(I, N)` pair. Calls the closure
319/// with dimension `I`, then descends to `Cursor<I + 1, N>`. We
320/// cannot write `I + 1` in a generic bound on stable, so the
321/// macro unrolls one impl per pair we want to support.
322macro_rules! impl_recurse {
323    ($i:expr, $n:expr) => {
324        impl sealed::Sealed<$i, $n> for Cursor<$i, $n> {}
325        impl Recurse<$i, $n> for Cursor<$i, $n> {
326            #[inline]
327            fn step<P, T, F>(acc: T, point: &P, f: &mut F) -> T
328            where
329                P: Point,
330                F: FnMut(T, &P, usize) -> T,
331            {
332                let acc = f(acc, point, $i);
333                <Cursor<{ $i + 1 }, $n> as Recurse<{ $i + 1 }, $n>>::step(acc, point, f)
334            }
335        }
336    };
337}
338
339// All (I, N) pairs with 0 <= I < N <= MAX_DIM. Keep `MAX_DIM` and
340// this block in sync.
341impl_recurse!(0, 1);
342impl_recurse!(0, 2);
343impl_recurse!(1, 2);
344impl_recurse!(0, 3);
345impl_recurse!(1, 3);
346impl_recurse!(2, 3);
347impl_recurse!(0, 4);
348impl_recurse!(1, 4);
349impl_recurse!(2, 4);
350impl_recurse!(3, 4);