Skip to main content

geometry_strategy/
transform.rs

1//! `TransformStrategy<P>` and affine implementations.
2//!
3//! The trait mirrors the per-CS transform-strategy concept from
4//! `boost/geometry/strategies/transform/services.hpp`; the affine
5//! matrices mirror
6//! `boost::geometry::strategy::transform::matrix_transformer` and its
7//! `translate_/rotate_/scale_transformer` siblings from
8//! `boost/geometry/strategies/transform/matrix_transformers.hpp`.
9//!
10//! Boost's `apply(src, dst)` mutates the destination through an
11//! out-parameter; the Rust port returns the destination by value,
12//! which makes the output type explicit and lines up with the KC1
13//! `PointMut` bound used to build it.
14
15use core::marker::PhantomData;
16
17use geometry_coords::CoordinateScalar;
18use geometry_cs::{Cartesian, CartesianFamily, CoordinateSystem};
19use geometry_model::{Point2D, Point3D};
20use geometry_tag::SameAs;
21use geometry_trait::{Point, PointMut};
22
23/// A function from one [`Point`] to another, used by
24/// [`transform`](../../geometry_algorithm/fn.transform.html).
25///
26/// The output may have a different scalar type or coordinate system
27/// than the input. [`Self::Output`] must be
28/// `PointMut + Default` because the strategy builds it via
29/// `Default + set::<D>`.
30pub trait TransformStrategy<P: Point> {
31    /// The destination point type.
32    type Output: PointMut + Default;
33
34    /// Apply the transform to `src` and return a fresh destination
35    /// point.
36    ///
37    /// Mirrors Boost's `apply(src, dst)`, returning `dst` by value.
38    fn transform(&self, src: &P) -> Self::Output;
39}
40
41/// 3×3 affine matrix in homogeneous 2D coordinates.
42///
43/// Mirrors `boost::geometry::strategy::transform::matrix_transformer<T, 2, 2>`
44/// from `matrix_transformers.hpp`.
45#[derive(Debug, Clone, Copy)]
46pub struct Affine2<T: CoordinateScalar> {
47    /// Row-major: `[a, b, tx, c, d, ty, 0, 0, 1]`.
48    pub m: [T; 9],
49    _t: PhantomData<T>,
50}
51
52impl<T: CoordinateScalar> Affine2<T> {
53    /// The identity transform.
54    #[must_use]
55    pub fn identity() -> Self {
56        Self {
57            m: [
58                T::ONE,
59                T::ZERO,
60                T::ZERO,
61                T::ZERO,
62                T::ONE,
63                T::ZERO,
64                T::ZERO,
65                T::ZERO,
66                T::ONE,
67            ],
68            _t: PhantomData,
69        }
70    }
71
72    /// Translation by `(tx, ty)`.
73    #[must_use]
74    pub fn translation(tx: T, ty: T) -> Self {
75        let mut m = Self::identity();
76        m.m[2] = tx;
77        m.m[5] = ty;
78        m
79    }
80
81    /// Non-uniform scale by `(sx, sy)`.
82    #[must_use]
83    pub fn scale(sx: T, sy: T) -> Self {
84        let mut m = Self::identity();
85        m.m[0] = sx;
86        m.m[4] = sy;
87        m
88    }
89
90    /// `self ∘ other` — apply `other` first, then `self`
91    /// (standard matrix composition `self * other`).
92    #[must_use]
93    pub fn then(self, other: Self) -> Self {
94        // The point is transformed by `self` after `other`, i.e.
95        // `result = self * other` as 3×3 matrices.
96        let a = &self.m;
97        let b = &other.m;
98        let mut out = [T::ZERO; 9];
99        for (r, row) in out.chunks_mut(3).enumerate() {
100            for (c, cell) in row.iter_mut().enumerate() {
101                *cell = a[r * 3] * b[c] + a[r * 3 + 1] * b[3 + c] + a[r * 3 + 2] * b[6 + c];
102            }
103        }
104        Self {
105            m: out,
106            _t: PhantomData,
107        }
108    }
109}
110
111impl Affine2<f64> {
112    /// Rotation by `angle_rad` radians counter-clockwise. Only
113    /// available for `f64` (needs `sin`/`cos`, which the scalar trait
114    /// does not yet expose generically).
115    ///
116    /// `std`-gated: `f64::sin_cos` is not shimmed by `geometry-coords`
117    /// under `libm` (only `sqrt`/`abs` are), so this constructor is
118    /// unavailable in a `no_std` build.
119    #[cfg(feature = "std")]
120    #[must_use]
121    pub fn rotation(angle_rad: f64) -> Self {
122        let (s, c) = angle_rad.sin_cos();
123        let mut m = Self::identity();
124        m.m[0] = c;
125        m.m[1] = -s;
126        m.m[3] = s;
127        m.m[4] = c;
128        m
129    }
130}
131
132impl<T, P> TransformStrategy<P> for Affine2<T>
133where
134    T: CoordinateScalar,
135    P: Point<Scalar = T>,
136    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
137{
138    type Output = Point2D<T, Cartesian>;
139
140    #[inline]
141    fn transform(&self, src: &P) -> Self::Output {
142        let x = src.get::<0>();
143        let y = src.get::<1>();
144        let nx = self.m[0] * x + self.m[1] * y + self.m[2];
145        let ny = self.m[3] * x + self.m[4] * y + self.m[5];
146        Point2D::new(nx, ny)
147    }
148}
149
150/// 4×4 affine matrix in homogeneous 3D coordinates. Same shape as
151/// [`Affine2`], one dimension up.
152#[derive(Debug, Clone, Copy)]
153pub struct Affine3<T: CoordinateScalar> {
154    /// Row-major 4×4.
155    pub m: [T; 16],
156    _t: PhantomData<T>,
157}
158
159impl<T: CoordinateScalar> Affine3<T> {
160    /// The identity transform.
161    #[must_use]
162    pub fn identity() -> Self {
163        let mut m = [T::ZERO; 16];
164        m[0] = T::ONE;
165        m[5] = T::ONE;
166        m[10] = T::ONE;
167        m[15] = T::ONE;
168        Self { m, _t: PhantomData }
169    }
170
171    /// Translation by `(tx, ty, tz)`.
172    #[must_use]
173    pub fn translation(tx: T, ty: T, tz: T) -> Self {
174        let mut m = Self::identity();
175        m.m[3] = tx;
176        m.m[7] = ty;
177        m.m[11] = tz;
178        m
179    }
180
181    /// Non-uniform scale by `(sx, sy, sz)`.
182    #[must_use]
183    pub fn scale(sx: T, sy: T, sz: T) -> Self {
184        let mut m = Self::identity();
185        m.m[0] = sx;
186        m.m[5] = sy;
187        m.m[10] = sz;
188        m
189    }
190}
191
192impl<T, P> TransformStrategy<P> for Affine3<T>
193where
194    T: CoordinateScalar,
195    P: Point<Scalar = T>,
196    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
197{
198    type Output = Point3D<T, Cartesian>;
199
200    #[inline]
201    fn transform(&self, src: &P) -> Self::Output {
202        let x = src.get::<0>();
203        let y = src.get::<1>();
204        let z = src.get::<2>();
205        let m = &self.m;
206        let nx = m[0] * x + m[1] * y + m[2] * z + m[3];
207        let ny = m[4] * x + m[5] * y + m[6] * z + m[7];
208        let nz = m[8] * x + m[9] * y + m[10] * z + m[11];
209        let mut out = Point3D::<T, Cartesian>::default();
210        out.set::<0>(nx);
211        out.set::<1>(ny);
212        out.set::<2>(nz);
213        out
214    }
215}
216
217#[cfg(test)]
218#[allow(
219    clippy::float_cmp,
220    reason = "Affine outputs of integer inputs are exact."
221)]
222mod tests {
223    //! Reference behaviour from
224    //! `boost/geometry/test/algorithms/transform.cpp`: translation and
225    //! scale map a point to the expected coordinates.
226
227    use super::{Affine2, TransformStrategy};
228    use geometry_cs::Cartesian;
229    use geometry_model::Point2D;
230    use geometry_trait::Point as _;
231
232    type P = Point2D<f64, Cartesian>;
233
234    #[test]
235    fn translation_shifts_point() {
236        let t = Affine2::translation(3.0, 4.0);
237        let out = t.transform(&P::new(1.0, 1.0));
238        assert_eq!(out.get::<0>(), 4.0);
239        assert_eq!(out.get::<1>(), 5.0);
240    }
241
242    #[test]
243    fn scale_multiplies_point() {
244        let s = Affine2::scale(2.0, 3.0);
245        let out = s.transform(&P::new(1.0, 1.0));
246        assert_eq!(out.get::<0>(), 2.0);
247        assert_eq!(out.get::<1>(), 3.0);
248    }
249
250    #[test]
251    fn rotation_by_half_pi_maps_x_to_y() {
252        let r = Affine2::rotation(core::f64::consts::FRAC_PI_2);
253        let out = r.transform(&P::new(1.0, 0.0));
254        assert!((out.get::<0>() - 0.0).abs() < 1e-12);
255        assert!((out.get::<1>() - 1.0).abs() < 1e-12);
256    }
257
258    #[test]
259    fn then_composes_translate_after_scale() {
260        // Apply scale first, then translate: (1,1) → scale×2 → (2,2)
261        // → translate(1,1) → (3,3).
262        let combined = Affine2::translation(1.0, 1.0).then(Affine2::scale(2.0, 2.0));
263        let out = combined.transform(&P::new(1.0, 1.0));
264        assert_eq!(out.get::<0>(), 3.0);
265        assert_eq!(out.get::<1>(), 3.0);
266    }
267}