Skip to main content

geometry_adapt/
with_cs.rs

1//! The coordinate-system re-tagging wrapper.
2//!
3//! Mirrors no single Boost header — C++ would simply specialise a
4//! different `traits::coordinate_system` for a different adapter
5//! type. In Rust, with the orphan rule and no specialisation, we
6//! lift the choice of coordinate system into its own zero-cost
7//! newtype that composes on top of any [`Point`].
8//!
9//! See proposal §3.7 ("Adapting foreign types: the coherence story")
10//! for the design rationale: `Adapt<T>` answers *"how do I read
11//! coordinates out of this foreign data layout?"* while
12//! [`WithCs`] answers *"what does that coordinate pair mean?"*.
13//! The two decisions are orthogonal, so they get independent
14//! wrappers.
15
16use core::marker::PhantomData;
17
18use geometry_cs::CoordinateSystem;
19use geometry_tag::PointTag;
20use geometry_trait::{Geometry, Point, PointMut};
21
22/// Re-tag any [`Point`] with a different coordinate system, without
23/// changing its storage layout.
24///
25/// Zero-cost: `#[repr(transparent)]` plus pure forwarding impls
26/// means `&WithCs<T, Cs>` is layout-compatible with `&T` and
27/// monomorphises to identical code. Only the `Cs` associated type
28/// of the [`Point`] impl changes — `Scalar`, `DIM`, `get`, and
29/// `set` all forward to the inner point.
30///
31/// See proposal §3.7 for the design rationale (orthogonality of
32/// shape vs. coordinate system, and the silent-Cartesian risk
33/// mitigation that lives in T31).
34///
35/// # Example
36///
37/// ```
38/// use geometry_adapt::{Adapt, WithCs};
39/// use geometry_cs::{Degree, Geographic};
40/// use geometry_trait::Point;
41///
42/// // An [f64; 2] adapted as a Cartesian point, then re-tagged as
43/// // a geographic lat/lon pair in degrees.
44/// let p: WithCs<Adapt<[f64; 2]>, Geographic<Degree>> =
45///     WithCs::new(Adapt([4.9, 52.4]));
46/// assert_eq!(p.get::<0>(), 4.9);
47/// assert_eq!(p.get::<1>(), 52.4);
48/// ```
49#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
50#[repr(transparent)]
51pub struct WithCs<T, Cs: CoordinateSystem> {
52    /// The wrapped inner point. Public so call sites can read or
53    /// mutate the foreign storage directly when convenient.
54    pub inner: T,
55    _cs: PhantomData<Cs>,
56}
57
58impl<T, Cs: CoordinateSystem> WithCs<T, Cs> {
59    /// Wrap an inner point and tag it with coordinate system `Cs`.
60    #[inline]
61    #[must_use]
62    pub const fn new(inner: T) -> Self {
63        Self {
64            inner,
65            _cs: PhantomData,
66        }
67    }
68
69    /// Unwrap, returning the inner point.
70    #[inline]
71    #[must_use]
72    pub fn into_inner(self) -> T {
73        self.inner
74    }
75}
76
77impl<P: Point, Cs: CoordinateSystem> Geometry for WithCs<P, Cs> {
78    type Kind = PointTag;
79    type Point = Self;
80}
81
82impl<P: Point, Cs: CoordinateSystem> Point for WithCs<P, Cs> {
83    type Scalar = P::Scalar;
84    type Cs = Cs;
85    const DIM: usize = P::DIM;
86
87    #[inline]
88    fn get<const D: usize>(&self) -> P::Scalar {
89        self.inner.get::<D>()
90    }
91}
92
93impl<P: PointMut, Cs: CoordinateSystem> PointMut for WithCs<P, Cs> {
94    #[inline]
95    fn set<const D: usize>(&mut self, value: P::Scalar) {
96        self.inner.set::<D>(value);
97    }
98}
99
100#[cfg(test)]
101#[allow(
102    clippy::float_cmp,
103    reason = "Every assertion here reads back a value just written, \
104              with no arithmetic in between, so strict equality is exact."
105)]
106mod tests {
107    use super::*;
108    use crate::Adapt;
109    use geometry_cs::{Cartesian, CoordinateSystem, Degree, Geographic, Spherical};
110    use geometry_trait::{Point, PointMut};
111
112    // Coordinates are passed through unchanged.
113    #[test]
114    fn re_tag_array_as_geographic_preserves_values() {
115        let p: WithCs<Adapt<[f64; 2]>, Geographic<Degree>> = WithCs::new(Adapt([4.9, 52.4]));
116        assert_eq!(p.get::<0>(), 4.9);
117        assert_eq!(p.get::<1>(), 52.4);
118        assert_eq!(
119            <WithCs<Adapt<[f64; 2]>, Geographic<Degree>> as Point>::DIM,
120            2
121        );
122    }
123
124    // The new Cs *is* in the type — the family witness changes.
125    #[test]
126    fn re_tag_changes_family() {
127        fn family<P: Point>() -> &'static str
128        where
129            <P::Cs as CoordinateSystem>::Family: 'static,
130        {
131            core::any::type_name::<<P::Cs as CoordinateSystem>::Family>()
132        }
133        // Before: Cartesian.
134        assert!(family::<Adapt<[f64; 2]>>().contains("CartesianFamily"));
135        // After WithCs<Geographic>: Geographic.
136        assert!(
137            family::<WithCs<Adapt<[f64; 2]>, Geographic<Degree>>>().contains("GeographicFamily")
138        );
139        // And Spherical works just as well.
140        assert!(family::<WithCs<Adapt<[f64; 2]>, Spherical<Degree>>>().contains("SphericalFamily"));
141    }
142
143    // Stacking: re-tag a user-owned Cartesian point as Spherical.
144    //
145    // Ordinates live in an array rather than two fields so `get`/`set`
146    // can index by `D` the way `geometry_model::Point` does. The
147    // earlier `if D == 0 { .. } else { .. }` spelling silently aliased
148    // every out-of-range `D` onto the y ordinate; indexing panics
149    // instead, which is what a stand-in for a real point should do.
150    struct MyXy([f64; 2]);
151
152    impl Geometry for MyXy {
153        type Kind = PointTag;
154        type Point = Self;
155    }
156
157    impl Point for MyXy {
158        type Scalar = f64;
159        type Cs = Cartesian;
160        const DIM: usize = 2;
161
162        fn get<const D: usize>(&self) -> f64 {
163            self.0[D]
164        }
165    }
166
167    impl PointMut for MyXy {
168        fn set<const D: usize>(&mut self, v: f64) {
169            self.0[D] = v;
170        }
171    }
172
173    #[test]
174    fn re_tag_user_point() {
175        let p: WithCs<MyXy, Spherical<Degree>> = WithCs::new(MyXy([1.0, 2.0]));
176        assert_eq!(p.get::<0>(), 1.0);
177        assert_eq!(p.get::<1>(), 2.0);
178    }
179
180    /// `PointMut::set` on a `WithCs` writes through to the inner point's
181    /// storage; re-reading through `get` returns the written value.
182    #[test]
183    fn set_writes_through_to_inner() {
184        let mut p: WithCs<MyXy, Spherical<Degree>> = WithCs::new(MyXy([0.0, 0.0]));
185        p.set::<0>(7.0);
186        p.set::<1>(9.0);
187        assert_eq!(p.get::<0>(), 7.0);
188        assert_eq!(p.get::<1>(), 9.0);
189        // The change landed in the wrapped MyXy, visible after unwrap.
190        let inner = p.into_inner();
191        assert_eq!(inner.0, [7.0, 9.0]);
192    }
193
194    // `#[repr(transparent)]` guarantees layout-compat; this is a
195    // sanity check rather than a real test, but it pins the
196    // assumption that adapter code depends on.
197    #[test]
198    fn size_is_inner_size() {
199        assert_eq!(
200            core::mem::size_of::<WithCs<Adapt<[f64; 2]>, Geographic<Degree>>>(),
201            core::mem::size_of::<[f64; 2]>(),
202        );
203    }
204}