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    struct MyXy(f64, f64);
145
146    impl Geometry for MyXy {
147        type Kind = PointTag;
148        type Point = Self;
149    }
150
151    impl Point for MyXy {
152        type Scalar = f64;
153        type Cs = Cartesian;
154        const DIM: usize = 2;
155
156        fn get<const D: usize>(&self) -> f64 {
157            if D == 0 { self.0 } else { self.1 }
158        }
159    }
160
161    impl PointMut for MyXy {
162        fn set<const D: usize>(&mut self, v: f64) {
163            if D == 0 {
164                self.0 = v;
165            } else {
166                self.1 = v;
167            }
168        }
169    }
170
171    #[test]
172    fn re_tag_user_point() {
173        let p: WithCs<MyXy, Spherical<Degree>> = WithCs::new(MyXy(1.0, 2.0));
174        assert_eq!(p.get::<0>(), 1.0);
175        assert_eq!(p.get::<1>(), 2.0);
176    }
177
178    /// `PointMut::set` on a `WithCs` writes through to the inner point's
179    /// storage; re-reading through `get` returns the written value.
180    #[test]
181    fn set_writes_through_to_inner() {
182        let mut p: WithCs<MyXy, Spherical<Degree>> = WithCs::new(MyXy(0.0, 0.0));
183        p.set::<0>(7.0);
184        p.set::<1>(9.0);
185        assert_eq!(p.get::<0>(), 7.0);
186        assert_eq!(p.get::<1>(), 9.0);
187        // The change landed in the wrapped MyXy, visible after unwrap.
188        let inner = p.into_inner();
189        assert_eq!(inner.0, 7.0);
190        assert_eq!(inner.1, 9.0);
191    }
192
193    // `#[repr(transparent)]` guarantees layout-compat; this is a
194    // sanity check rather than a real test, but it pins the
195    // assumption that adapter code depends on.
196    #[test]
197    fn size_is_inner_size() {
198        assert_eq!(
199            core::mem::size_of::<WithCs<Adapt<[f64; 2]>, Geographic<Degree>>>(),
200            core::mem::size_of::<[f64; 2]>(),
201        );
202    }
203}