1use 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
23pub trait TransformStrategy<P: Point> {
31 type Output: PointMut + Default;
33
34 fn transform(&self, src: &P) -> Self::Output;
39}
40
41#[derive(Debug, Default, Clone, Copy)]
43pub struct Translate;
44
45impl Translate {
46 #[must_use]
48 pub fn by<T: CoordinateScalar>(x: T, y: T) -> Affine2<T> {
49 Affine2::translation(x, y)
50 }
51}
52
53#[derive(Debug, Default, Clone, Copy)]
55pub struct Scale;
56
57impl Scale {
58 #[must_use]
60 pub fn by<T: CoordinateScalar>(x: T, y: T) -> Affine2<T> {
61 Affine2::scale(x, y)
62 }
63
64 #[must_use]
66 pub fn uniform<T: CoordinateScalar>(factor: T) -> Affine2<T> {
67 Affine2::scale(factor, factor)
68 }
69}
70
71#[derive(Debug, Default, Clone, Copy)]
73pub struct Rotate;
74
75impl Rotate {
76 #[cfg(feature = "std")]
78 #[must_use]
79 pub fn radians(angle: f64) -> Affine2<f64> {
80 Affine2::rotation(angle)
81 }
82
83 #[cfg(feature = "std")]
85 #[must_use]
86 pub fn degrees(angle: f64) -> Affine2<f64> {
87 Self::radians(angle.to_radians())
88 }
89
90 #[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#[derive(Debug, Default, Clone, Copy)]
107pub struct Skew;
108
109impl Skew {
110 #[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 #[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 #[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#[derive(Debug, Clone, Copy)]
142pub struct Affine2<T: CoordinateScalar> {
143 pub m: [T; 9],
145 _t: PhantomData<T>,
146}
147
148impl<T: CoordinateScalar> Affine2<T> {
149 #[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 #[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 #[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 #[must_use]
189 pub fn then(self, other: Self) -> Self {
190 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 #[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#[derive(Debug, Clone, Copy)]
249pub struct Affine3<T: CoordinateScalar> {
250 pub m: [T; 16],
252 _t: PhantomData<T>,
253}
254
255impl<T: CoordinateScalar> Affine3<T> {
256 #[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 #[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 #[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 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 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}