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/// Named constructor for 2D translation strategies.
42#[derive(Debug, Default, Clone, Copy)]
43pub struct Translate;
44
45impl Translate {
46    /// Translate by `(x, y)`.
47    #[must_use]
48    pub fn by<T: CoordinateScalar>(x: T, y: T) -> Affine2<T> {
49        Affine2::translation(x, y)
50    }
51}
52
53/// Named constructor for 2D scale strategies.
54#[derive(Debug, Default, Clone, Copy)]
55pub struct Scale;
56
57impl Scale {
58    /// Scale the two axes independently.
59    #[must_use]
60    pub fn by<T: CoordinateScalar>(x: T, y: T) -> Affine2<T> {
61        Affine2::scale(x, y)
62    }
63
64    /// Scale both axes by the same factor.
65    #[must_use]
66    pub fn uniform<T: CoordinateScalar>(factor: T) -> Affine2<T> {
67        Affine2::scale(factor, factor)
68    }
69}
70
71/// Named constructor for 2D rotation strategies.
72#[derive(Debug, Default, Clone, Copy)]
73pub struct Rotate;
74
75impl Rotate {
76    /// Rotate counter-clockwise by radians around the coordinate origin.
77    #[cfg(feature = "std")]
78    #[must_use]
79    pub fn radians(angle: f64) -> Affine2<f64> {
80        Affine2::rotation(angle)
81    }
82
83    /// Rotate counter-clockwise by degrees around the coordinate origin.
84    #[cfg(feature = "std")]
85    #[must_use]
86    pub fn degrees(angle: f64) -> Affine2<f64> {
87        Self::radians(angle.to_radians())
88    }
89
90    /// Rotate counter-clockwise by radians around `origin`.
91    #[cfg(feature = "std")]
92    #[must_use]
93    pub fn around<P>(angle: f64, origin: &P) -> Affine2<f64>
94    where
95        P: Point<Scalar = f64>,
96    {
97        let x = origin.get::<0>();
98        let y = origin.get::<1>();
99        Affine2::translation(x, y)
100            .then(Affine2::rotation(angle))
101            .then(Affine2::translation(-x, -y))
102    }
103}
104
105/// Named constructor for 2D shear/skew strategies.
106#[derive(Debug, Default, Clone, Copy)]
107pub struct Skew;
108
109impl Skew {
110    /// Apply direct x- and y-axis shear factors.
111    ///
112    /// The resulting mapping is `x' = x + x_shear*y` and
113    /// `y' = y_shear*x + y`.
114    #[must_use]
115    pub fn by(x_shear: f64, y_shear: f64) -> Affine2<f64> {
116        Affine2 {
117            m: [1.0, x_shear, 0.0, y_shear, 1.0, 0.0, 0.0, 0.0, 1.0],
118            _t: PhantomData,
119        }
120    }
121
122    /// Apply shears specified as angles in radians.
123    #[cfg(feature = "std")]
124    #[must_use]
125    pub fn radians(x_angle: f64, y_angle: f64) -> Affine2<f64> {
126        Self::by(x_angle.tan(), y_angle.tan())
127    }
128
129    /// Apply shears specified as angles in degrees.
130    #[cfg(feature = "std")]
131    #[must_use]
132    pub fn degrees(x_angle: f64, y_angle: f64) -> Affine2<f64> {
133        Self::radians(x_angle.to_radians(), y_angle.to_radians())
134    }
135}
136
137/// 3×3 affine matrix in homogeneous 2D coordinates.
138///
139/// Mirrors `boost::geometry::strategy::transform::matrix_transformer<T, 2, 2>`
140/// from `matrix_transformers.hpp`.
141#[derive(Debug, Clone, Copy)]
142pub struct Affine2<T: CoordinateScalar> {
143    /// Row-major: `[a, b, tx, c, d, ty, 0, 0, 1]`.
144    pub m: [T; 9],
145    _t: PhantomData<T>,
146}
147
148impl<T: CoordinateScalar> Affine2<T> {
149    /// The identity transform.
150    #[must_use]
151    pub fn identity() -> Self {
152        Self {
153            m: [
154                T::ONE,
155                T::ZERO,
156                T::ZERO,
157                T::ZERO,
158                T::ONE,
159                T::ZERO,
160                T::ZERO,
161                T::ZERO,
162                T::ONE,
163            ],
164            _t: PhantomData,
165        }
166    }
167
168    /// Translation by `(tx, ty)`.
169    #[must_use]
170    pub fn translation(tx: T, ty: T) -> Self {
171        let mut m = Self::identity();
172        m.m[2] = tx;
173        m.m[5] = ty;
174        m
175    }
176
177    /// Non-uniform scale by `(sx, sy)`.
178    #[must_use]
179    pub fn scale(sx: T, sy: T) -> Self {
180        let mut m = Self::identity();
181        m.m[0] = sx;
182        m.m[4] = sy;
183        m
184    }
185
186    /// `self ∘ other` — apply `other` first, then `self`
187    /// (standard matrix composition `self * other`).
188    #[must_use]
189    pub fn then(self, other: Self) -> Self {
190        // The point is transformed by `self` after `other`, i.e.
191        // `result = self * other` as 3×3 matrices.
192        let a = &self.m;
193        let b = &other.m;
194        let mut out = [T::ZERO; 9];
195        for (r, row) in out.chunks_mut(3).enumerate() {
196            for (c, cell) in row.iter_mut().enumerate() {
197                *cell = a[r * 3] * b[c] + a[r * 3 + 1] * b[3 + c] + a[r * 3 + 2] * b[6 + c];
198            }
199        }
200        Self {
201            m: out,
202            _t: PhantomData,
203        }
204    }
205}
206
207impl Affine2<f64> {
208    /// Rotation by `angle_rad` radians counter-clockwise. Only
209    /// available for `f64` (needs `sin`/`cos`, which the scalar trait
210    /// does not yet expose generically).
211    ///
212    /// `std`-gated: `f64::sin_cos` is not shimmed by `geometry-coords`
213    /// under `libm` (only `sqrt`/`abs` are), so this constructor is
214    /// unavailable in a `no_std` build.
215    #[cfg(feature = "std")]
216    #[must_use]
217    pub fn rotation(angle_rad: f64) -> Self {
218        let (s, c) = angle_rad.sin_cos();
219        let mut m = Self::identity();
220        m.m[0] = c;
221        m.m[1] = -s;
222        m.m[3] = s;
223        m.m[4] = c;
224        m
225    }
226}
227
228impl<T, P> TransformStrategy<P> for Affine2<T>
229where
230    T: CoordinateScalar,
231    P: Point<Scalar = T>,
232    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
233{
234    type Output = Point2D<T, Cartesian>;
235
236    #[inline]
237    fn transform(&self, src: &P) -> Self::Output {
238        let x = src.get::<0>();
239        let y = src.get::<1>();
240        let nx = self.m[0] * x + self.m[1] * y + self.m[2];
241        let ny = self.m[3] * x + self.m[4] * y + self.m[5];
242        Point2D::new(nx, ny)
243    }
244}
245
246/// 4×4 affine matrix in homogeneous 3D coordinates. Same shape as
247/// [`Affine2`], one dimension up.
248#[derive(Debug, Clone, Copy)]
249pub struct Affine3<T: CoordinateScalar> {
250    /// Row-major 4×4.
251    pub m: [T; 16],
252    _t: PhantomData<T>,
253}
254
255impl<T: CoordinateScalar> Affine3<T> {
256    /// The identity transform.
257    #[must_use]
258    pub fn identity() -> Self {
259        let mut m = [T::ZERO; 16];
260        m[0] = T::ONE;
261        m[5] = T::ONE;
262        m[10] = T::ONE;
263        m[15] = T::ONE;
264        Self { m, _t: PhantomData }
265    }
266
267    /// Translation by `(tx, ty, tz)`.
268    #[must_use]
269    pub fn translation(tx: T, ty: T, tz: T) -> Self {
270        let mut m = Self::identity();
271        m.m[3] = tx;
272        m.m[7] = ty;
273        m.m[11] = tz;
274        m
275    }
276
277    /// Non-uniform scale by `(sx, sy, sz)`.
278    #[must_use]
279    pub fn scale(sx: T, sy: T, sz: T) -> Self {
280        let mut m = Self::identity();
281        m.m[0] = sx;
282        m.m[5] = sy;
283        m.m[10] = sz;
284        m
285    }
286}
287
288impl<T, P> TransformStrategy<P> for Affine3<T>
289where
290    T: CoordinateScalar,
291    P: Point<Scalar = T>,
292    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
293{
294    type Output = Point3D<T, Cartesian>;
295
296    #[inline]
297    fn transform(&self, src: &P) -> Self::Output {
298        let x = src.get::<0>();
299        let y = src.get::<1>();
300        let z = src.get::<2>();
301        let m = &self.m;
302        let nx = m[0] * x + m[1] * y + m[2] * z + m[3];
303        let ny = m[4] * x + m[5] * y + m[6] * z + m[7];
304        let nz = m[8] * x + m[9] * y + m[10] * z + m[11];
305        let mut out = Point3D::<T, Cartesian>::default();
306        out.set::<0>(nx);
307        out.set::<1>(ny);
308        out.set::<2>(nz);
309        out
310    }
311}
312
313#[cfg(test)]
314#[allow(
315    clippy::float_cmp,
316    reason = "Affine outputs of integer inputs are exact."
317)]
318mod tests {
319    //! Reference behaviour from
320    //! `boost/geometry/test/algorithms/transform.cpp`: translation and
321    //! scale map a point to the expected coordinates.
322
323    use super::{Affine2, Rotate, Scale, Skew, TransformStrategy, Translate};
324    use geometry_cs::Cartesian;
325    use geometry_model::Point2D;
326    use geometry_trait::Point as _;
327
328    type P = Point2D<f64, Cartesian>;
329
330    #[test]
331    fn translation_shifts_point() {
332        let t = Affine2::translation(3.0, 4.0);
333        let out = t.transform(&P::new(1.0, 1.0));
334        assert_eq!(out.get::<0>(), 4.0);
335        assert_eq!(out.get::<1>(), 5.0);
336    }
337
338    #[test]
339    fn scale_multiplies_point() {
340        let s = Affine2::scale(2.0, 3.0);
341        let out = s.transform(&P::new(1.0, 1.0));
342        assert_eq!(out.get::<0>(), 2.0);
343        assert_eq!(out.get::<1>(), 3.0);
344    }
345
346    #[test]
347    fn rotation_by_half_pi_maps_x_to_y() {
348        let r = Affine2::rotation(core::f64::consts::FRAC_PI_2);
349        let out = r.transform(&P::new(1.0, 0.0));
350        assert!((out.get::<0>() - 0.0).abs() < 1e-12);
351        assert!((out.get::<1>() - 1.0).abs() < 1e-12);
352    }
353
354    #[test]
355    fn then_composes_translate_after_scale() {
356        // Apply scale first, then translate: (1,1) → scale×2 → (2,2)
357        // → translate(1,1) → (3,3).
358        let combined = Affine2::translation(1.0, 1.0).then(Affine2::scale(2.0, 2.0));
359        let out = combined.transform(&P::new(1.0, 1.0));
360        assert_eq!(out.get::<0>(), 3.0);
361        assert_eq!(out.get::<1>(), 3.0);
362    }
363
364    #[test]
365    fn named_constructors_build_affine_strategies() {
366        assert_eq!(
367            Translate::by(2.0, 3.0).transform(&P::new(1.0, 1.0)),
368            P::new(3.0, 4.0)
369        );
370        assert_eq!(
371            Scale::uniform(2.0).transform(&P::new(1.0, 3.0)),
372            P::new(2.0, 6.0)
373        );
374        assert_eq!(
375            Skew::by(2.0, 0.0).transform(&P::new(1.0, 3.0)),
376            P::new(7.0, 3.0)
377        );
378        let rotated = Rotate::around(core::f64::consts::FRAC_PI_2, &P::new(1.0, 1.0))
379            .transform(&P::new(2.0, 1.0));
380        assert!((rotated.get::<0>() - 1.0).abs() < 1e-12);
381        assert!((rotated.get::<1>() - 2.0).abs() < 1e-12);
382    }
383}