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}