1use crate::primitives::{Point2, Point3};
7use nalgebra::{
8 Isometry2, Isometry3, Point2 as NaPoint2, Point3 as NaPoint3, UnitQuaternion,
9 Vector2 as NaVector2, Vector3 as NaVector3,
10};
11
12#[derive(Debug, Clone, Copy, PartialEq)]
30#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
31pub struct Transform2D {
32 pub tx: f64,
34 pub ty: f64,
36 pub angle: f64,
38}
39
40impl Transform2D {
41 pub fn identity() -> Self {
43 Self {
44 tx: 0.0,
45 ty: 0.0,
46 angle: 0.0,
47 }
48 }
49
50 pub fn translation(tx: f64, ty: f64) -> Self {
52 Self { tx, ty, angle: 0.0 }
53 }
54
55 pub fn rotation(angle: f64) -> Self {
57 Self {
58 tx: 0.0,
59 ty: 0.0,
60 angle,
61 }
62 }
63
64 pub fn new(tx: f64, ty: f64, angle: f64) -> Self {
66 Self { tx, ty, angle }
67 }
68
69 #[inline]
71 pub fn to_isometry(&self) -> Isometry2<f64> {
72 Isometry2::new(NaVector2::new(self.tx, self.ty), self.angle)
73 }
74
75 pub fn from_isometry(iso: &Isometry2<f64>) -> Self {
77 Self {
78 tx: iso.translation.x,
79 ty: iso.translation.y,
80 angle: iso.rotation.angle(),
81 }
82 }
83
84 #[inline]
86 pub fn apply(&self, x: f64, y: f64) -> (f64, f64) {
87 let iso = self.to_isometry();
88 let p = iso.transform_point(&NaPoint2::new(x, y));
89 (p.x, p.y)
90 }
91
92 #[inline]
94 pub fn apply_point(&self, p: &Point2) -> Point2 {
95 let (x, y) = self.apply(p.x, p.y);
96 Point2::new(x, y)
97 }
98
99 pub fn apply_points(&self, points: &[(f64, f64)]) -> Vec<(f64, f64)> {
101 let iso = self.to_isometry();
102 points
103 .iter()
104 .map(|(x, y)| {
105 let p = iso.transform_point(&NaPoint2::new(*x, *y));
106 (p.x, p.y)
107 })
108 .collect()
109 }
110
111 pub fn then(&self, other: &Self) -> Self {
113 let iso1 = self.to_isometry();
114 let iso2 = other.to_isometry();
115 Self::from_isometry(&(iso1 * iso2))
116 }
117
118 pub fn inverse(&self) -> Self {
120 Self::from_isometry(&self.to_isometry().inverse())
121 }
122
123 pub fn is_identity(&self, epsilon: f64) -> bool {
125 self.tx.abs() < epsilon && self.ty.abs() < epsilon && self.angle.abs() < epsilon
126 }
127}
128
129impl Default for Transform2D {
130 fn default() -> Self {
131 Self::identity()
132 }
133}
134
135#[derive(Debug, Clone, Copy, PartialEq)]
152#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
153pub struct Transform3D {
154 pub tx: f64,
156 pub ty: f64,
158 pub tz: f64,
160 pub rx: f64,
162 pub ry: f64,
164 pub rz: f64,
166}
167
168impl Transform3D {
169 pub fn identity() -> Self {
171 Self {
172 tx: 0.0,
173 ty: 0.0,
174 tz: 0.0,
175 rx: 0.0,
176 ry: 0.0,
177 rz: 0.0,
178 }
179 }
180
181 pub fn translation(tx: f64, ty: f64, tz: f64) -> Self {
183 Self {
184 tx,
185 ty,
186 tz,
187 rx: 0.0,
188 ry: 0.0,
189 rz: 0.0,
190 }
191 }
192
193 pub fn new(tx: f64, ty: f64, tz: f64, rx: f64, ry: f64, rz: f64) -> Self {
195 Self {
196 tx,
197 ty,
198 tz,
199 rx,
200 ry,
201 rz,
202 }
203 }
204
205 #[inline]
209 pub fn to_isometry(&self) -> Isometry3<f64> {
210 let rotation = UnitQuaternion::from_euler_angles(self.rx, self.ry, self.rz);
211 Isometry3::from_parts(NaVector3::new(self.tx, self.ty, self.tz).into(), rotation)
212 }
213
214 pub fn from_isometry(iso: &Isometry3<f64>) -> Self {
216 let (rx, ry, rz) = iso.rotation.euler_angles();
217 Self {
218 tx: iso.translation.x,
219 ty: iso.translation.y,
220 tz: iso.translation.z,
221 rx,
222 ry,
223 rz,
224 }
225 }
226
227 #[inline]
229 pub fn apply(&self, x: f64, y: f64, z: f64) -> (f64, f64, f64) {
230 let iso = self.to_isometry();
231 let p = iso.transform_point(&NaPoint3::new(x, y, z));
232 (p.x, p.y, p.z)
233 }
234
235 #[inline]
237 pub fn apply_point(&self, p: &Point3) -> Point3 {
238 let (x, y, z) = self.apply(p.x, p.y, p.z);
239 Point3::new(x, y, z)
240 }
241
242 pub fn apply_points(&self, points: &[Point3]) -> Vec<Point3> {
244 let iso = self.to_isometry();
245 points
246 .iter()
247 .map(|p| {
248 let tp = iso.transform_point(&NaPoint3::new(p.x, p.y, p.z));
249 Point3::new(tp.x, tp.y, tp.z)
250 })
251 .collect()
252 }
253
254 pub fn then(&self, other: &Self) -> Self {
256 let iso1 = self.to_isometry();
257 let iso2 = other.to_isometry();
258 Self::from_isometry(&(iso1 * iso2))
259 }
260
261 pub fn inverse(&self) -> Self {
263 Self::from_isometry(&self.to_isometry().inverse())
264 }
265
266 pub fn is_identity(&self, epsilon: f64) -> bool {
268 self.tx.abs() < epsilon
269 && self.ty.abs() < epsilon
270 && self.tz.abs() < epsilon
271 && self.rx.abs() < epsilon
272 && self.ry.abs() < epsilon
273 && self.rz.abs() < epsilon
274 }
275}
276
277impl Default for Transform3D {
278 fn default() -> Self {
279 Self::identity()
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286 use std::f64::consts::PI;
287
288 #[test]
289 fn test_identity() {
290 let t = Transform2D::identity();
291 let (x, y) = t.apply(1.0, 2.0);
292 assert!((x - 1.0).abs() < 1e-10);
293 assert!((y - 2.0).abs() < 1e-10);
294 }
295
296 #[test]
297 fn test_translation() {
298 let t = Transform2D::translation(10.0, 20.0);
299 let (x, y) = t.apply(1.0, 2.0);
300 assert!((x - 11.0).abs() < 1e-10);
301 assert!((y - 22.0).abs() < 1e-10);
302 }
303
304 #[test]
305 fn test_rotation_90() {
306 let t = Transform2D::rotation(PI / 2.0);
307 let (x, y) = t.apply(1.0, 0.0);
308 assert!((x - 0.0).abs() < 1e-10);
309 assert!((y - 1.0).abs() < 1e-10);
310 }
311
312 #[test]
313 fn test_rotation_180() {
314 let t = Transform2D::rotation(PI);
315 let (x, y) = t.apply(1.0, 0.0);
316 assert!((x - (-1.0)).abs() < 1e-10);
317 assert!((y - 0.0).abs() < 1e-10);
318 }
319
320 #[test]
321 fn test_compose() {
322 let t1 = Transform2D::translation(10.0, 0.0);
323 let t2 = Transform2D::translation(0.0, 20.0);
324 let composed = t1.then(&t2);
325 let (x, y) = composed.apply(0.0, 0.0);
326 assert!((x - 10.0).abs() < 1e-10);
327 assert!((y - 20.0).abs() < 1e-10);
328 }
329
330 #[test]
331 fn test_inverse() {
332 let t = Transform2D::new(10.0, 20.0, PI / 4.0);
333 let inv = t.inverse();
334 let composed = t.then(&inv);
335 assert!(composed.is_identity(1e-10));
336 }
337
338 #[test]
339 fn test_apply_point() {
340 let t = Transform2D::translation(5.0, 3.0);
341 let p = Point2::new(1.0, 2.0);
342 let q = t.apply_point(&p);
343 assert!((q.x - 6.0).abs() < 1e-10);
344 assert!((q.y - 5.0).abs() < 1e-10);
345 }
346
347 #[test]
348 fn test_apply_points() {
349 let t = Transform2D::translation(1.0, 1.0);
350 let points = vec![(0.0, 0.0), (1.0, 0.0), (0.0, 1.0)];
351 let transformed = t.apply_points(&points);
352 assert!((transformed[0].0 - 1.0).abs() < 1e-10);
353 assert!((transformed[0].1 - 1.0).abs() < 1e-10);
354 assert!((transformed[1].0 - 2.0).abs() < 1e-10);
355 assert!((transformed[2].1 - 2.0).abs() < 1e-10);
356 }
357
358 #[test]
359 fn test_default_is_identity() {
360 let t = Transform2D::default();
361 assert!(t.is_identity(1e-15));
362 }
363
364 #[test]
367 fn test_3d_identity() {
368 let t = Transform3D::identity();
369 let (x, y, z) = t.apply(1.0, 2.0, 3.0);
370 assert!((x - 1.0).abs() < 1e-10);
371 assert!((y - 2.0).abs() < 1e-10);
372 assert!((z - 3.0).abs() < 1e-10);
373 }
374
375 #[test]
376 fn test_3d_translation() {
377 let t = Transform3D::translation(10.0, 20.0, 30.0);
378 let (x, y, z) = t.apply(1.0, 2.0, 3.0);
379 assert!((x - 11.0).abs() < 1e-10);
380 assert!((y - 22.0).abs() < 1e-10);
381 assert!((z - 33.0).abs() < 1e-10);
382 }
383
384 #[test]
385 fn test_3d_rotation_z_90() {
386 let t = Transform3D::new(0.0, 0.0, 0.0, 0.0, 0.0, PI / 2.0);
388 let (x, y, z) = t.apply(1.0, 0.0, 0.0);
389 assert!((x - 0.0).abs() < 1e-10);
390 assert!((y - 1.0).abs() < 1e-10);
391 assert!((z - 0.0).abs() < 1e-10);
392 }
393
394 #[test]
395 fn test_3d_rotation_x_90() {
396 let t = Transform3D::new(0.0, 0.0, 0.0, PI / 2.0, 0.0, 0.0);
398 let (x, y, z) = t.apply(0.0, 1.0, 0.0);
399 assert!((x - 0.0).abs() < 1e-10);
400 assert!((y - 0.0).abs() < 1e-10);
401 assert!((z - 1.0).abs() < 1e-10);
402 }
403
404 #[test]
405 fn test_3d_rotation_y_90() {
406 let t = Transform3D::new(0.0, 0.0, 0.0, 0.0, PI / 2.0, 0.0);
408 let (x, y, z) = t.apply(0.0, 0.0, 1.0);
409 assert!((x - 1.0).abs() < 1e-10);
410 assert!((y - 0.0).abs() < 1e-10);
411 assert!((z - 0.0).abs() < 1e-10);
412 }
413
414 #[test]
415 fn test_3d_compose() {
416 let t1 = Transform3D::translation(10.0, 0.0, 0.0);
417 let t2 = Transform3D::translation(0.0, 20.0, 0.0);
418 let composed = t1.then(&t2);
419 let (x, y, z) = composed.apply(0.0, 0.0, 0.0);
420 assert!((x - 10.0).abs() < 1e-10);
421 assert!((y - 20.0).abs() < 1e-10);
422 assert!((z - 0.0).abs() < 1e-10);
423 }
424
425 #[test]
426 fn test_3d_inverse() {
427 let t = Transform3D::new(10.0, 20.0, 30.0, PI / 4.0, PI / 6.0, PI / 3.0);
428 let inv = t.inverse();
429 let composed = t.then(&inv);
430 assert!(composed.is_identity(1e-10));
431 }
432
433 #[test]
434 fn test_3d_apply_point() {
435 let t = Transform3D::translation(5.0, 3.0, 1.0);
436 let p = Point3::new(1.0, 2.0, 3.0);
437 let q = t.apply_point(&p);
438 assert!((q.x - 6.0).abs() < 1e-10);
439 assert!((q.y - 5.0).abs() < 1e-10);
440 assert!((q.z - 4.0).abs() < 1e-10);
441 }
442
443 #[test]
444 fn test_3d_apply_points() {
445 let t = Transform3D::translation(1.0, 1.0, 1.0);
446 let points = vec![
447 Point3::new(0.0, 0.0, 0.0),
448 Point3::new(1.0, 0.0, 0.0),
449 Point3::new(0.0, 1.0, 0.0),
450 ];
451 let transformed = t.apply_points(&points);
452 assert!((transformed[0].x - 1.0).abs() < 1e-10);
453 assert!((transformed[1].x - 2.0).abs() < 1e-10);
454 assert!((transformed[2].y - 2.0).abs() < 1e-10);
455 }
456
457 #[test]
458 fn test_3d_default_is_identity() {
459 let t = Transform3D::default();
460 assert!(t.is_identity(1e-15));
461 }
462
463 #[cfg(feature = "serde")]
466 mod serde_tests {
467 use super::*;
468
469 #[test]
470 fn test_transform2d_roundtrip() {
471 let t = Transform2D::new(10.0, 20.0, std::f64::consts::PI / 4.0);
472 let json = serde_json::to_string(&t).unwrap();
473 let t2: Transform2D = serde_json::from_str(&json).unwrap();
474 assert_eq!(t, t2);
475 }
476
477 #[test]
478 fn test_transform3d_roundtrip() {
479 let t = Transform3D::new(1.0, 2.0, 3.0, 0.1, 0.2, 0.3);
480 let json = serde_json::to_string(&t).unwrap();
481 let t2: Transform3D = serde_json::from_str(&json).unwrap();
482 assert_eq!(t, t2);
483 }
484 }
485}