ranim_core/traits/transform/
group.rs1use core::ops::{Deref, DerefMut};
25
26use glam::{DAffine3, DMat3, DQuat, DVec3};
27
28use crate::traits::Interpolatable;
29
30pub trait TransformGroup: Sized {
35 fn identity() -> Self;
37
38 fn compose(&self, inner: &Self) -> Self;
40}
41
42#[repr(transparent)]
45#[derive(Debug, Clone, Copy, PartialEq)]
46pub struct Translation(pub DVec3);
47
48impl From<DVec3> for Translation {
49 fn from(v: DVec3) -> Self {
50 Self(v)
51 }
52}
53
54impl From<Translation> for DVec3 {
55 fn from(t: Translation) -> Self {
56 t.0
57 }
58}
59
60impl Deref for Translation {
61 type Target = DVec3;
62 fn deref(&self) -> &Self::Target {
63 &self.0
64 }
65}
66
67impl DerefMut for Translation {
68 fn deref_mut(&mut self) -> &mut Self::Target {
69 &mut self.0
70 }
71}
72
73impl Interpolatable for Translation {
74 fn lerp(&self, target: &Self, t: f64) -> Self {
75 Self(self.0.lerp(target.0, t))
76 }
77}
78
79#[derive(Debug, Clone, Copy, PartialEq)]
82pub struct Rigid {
83 pub rotation: DQuat,
85 pub translation: DVec3,
87}
88
89impl Rigid {
90 pub const IDENTITY: Self = Self {
92 rotation: DQuat::IDENTITY,
93 translation: DVec3::ZERO,
94 };
95
96 pub fn from_rotation(rotation: DQuat) -> Self {
98 Self {
99 rotation,
100 translation: DVec3::ZERO,
101 }
102 }
103
104 pub fn from_translation(translation: DVec3) -> Self {
106 Self {
107 rotation: DQuat::IDENTITY,
108 translation,
109 }
110 }
111
112 pub fn from_axis_angle(axis: DVec3, angle: f64) -> Self {
114 Self::from_rotation(DQuat::from_axis_angle(axis.normalize(), angle))
115 }
116}
117
118impl Interpolatable for Rigid {
119 fn lerp(&self, target: &Self, t: f64) -> Self {
120 Self {
121 rotation: self.rotation.slerp(target.rotation, t),
122 translation: self.translation.lerp(target.translation, t),
123 }
124 }
125}
126
127#[derive(Debug, Clone, Copy, PartialEq)]
134pub struct Similarity {
135 pub scale: f64,
137 pub rotation: DQuat,
139 pub translation: DVec3,
141}
142
143impl Similarity {
144 pub const IDENTITY: Self = Self {
146 scale: 1.0,
147 rotation: DQuat::IDENTITY,
148 translation: DVec3::ZERO,
149 };
150
151 pub fn from_scale(scale: f64) -> Self {
159 assert!(
160 scale.is_finite() && scale > 0.0,
161 "similarity scale must be finite and strictly positive"
162 );
163 Self {
164 scale,
165 rotation: DQuat::IDENTITY,
166 translation: DVec3::ZERO,
167 }
168 }
169
170 pub fn transform_point(&self, p: DVec3) -> DVec3 {
172 self.scale * (self.rotation * p) + self.translation
173 }
174
175 pub fn transform_direction(&self, v: DVec3) -> DVec3 {
177 self.rotation * v
178 }
179}
180
181impl Interpolatable for Similarity {
182 fn lerp(&self, target: &Self, t: f64) -> Self {
183 Self {
184 scale: self.scale.lerp(&target.scale, t),
185 rotation: self.rotation.slerp(target.rotation, t),
186 translation: self.translation.lerp(target.translation, t),
187 }
188 }
189}
190
191#[repr(transparent)]
198#[derive(Debug, Clone, Copy, PartialEq)]
199pub struct Diag(pub DVec3);
200
201impl From<DVec3> for Diag {
202 fn from(v: DVec3) -> Self {
203 Self(v)
204 }
205}
206
207impl From<Diag> for DVec3 {
208 fn from(d: Diag) -> Self {
209 d.0
210 }
211}
212
213impl Deref for Diag {
214 type Target = DVec3;
215 fn deref(&self) -> &Self::Target {
216 &self.0
217 }
218}
219
220impl DerefMut for Diag {
221 fn deref_mut(&mut self) -> &mut Self::Target {
222 &mut self.0
223 }
224}
225
226impl Interpolatable for Diag {
227 fn lerp(&self, target: &Self, t: f64) -> Self {
228 Self(self.0.lerp(target.0, t))
229 }
230}
231
232impl TransformGroup for Translation {
234 fn identity() -> Self {
235 Self(DVec3::ZERO)
236 }
237
238 fn compose(&self, inner: &Self) -> Self {
239 Self(self.0 + inner.0)
240 }
241}
242
243impl TransformGroup for Rigid {
244 fn identity() -> Self {
245 Self::IDENTITY
246 }
247
248 fn compose(&self, inner: &Self) -> Self {
249 Self {
250 rotation: self.rotation * inner.rotation,
251 translation: self.rotation * inner.translation + self.translation,
252 }
253 }
254}
255
256impl TransformGroup for Similarity {
257 fn identity() -> Self {
258 Self::IDENTITY
259 }
260
261 fn compose(&self, inner: &Self) -> Self {
262 Self {
263 scale: self.scale * inner.scale,
264 rotation: self.rotation * inner.rotation,
265 translation: self.scale * (self.rotation * inner.translation) + self.translation,
266 }
267 }
268}
269
270impl TransformGroup for Diag {
271 fn identity() -> Self {
272 Self(DVec3::ONE)
273 }
274
275 fn compose(&self, inner: &Self) -> Self {
276 Self(self.0 * inner.0)
277 }
278}
279
280impl TransformGroup for DAffine3 {
281 fn identity() -> Self {
282 Self::IDENTITY
283 }
284
285 fn compose(&self, inner: &Self) -> Self {
286 *self * *inner
287 }
288}
289
290impl From<Translation> for Rigid {
295 fn from(t: Translation) -> Self {
296 Rigid::from_translation(t.0)
297 }
298}
299
300impl From<Translation> for Similarity {
301 fn from(t: Translation) -> Self {
302 Similarity {
303 scale: 1.0,
304 rotation: DQuat::IDENTITY,
305 translation: t.0,
306 }
307 }
308}
309
310impl From<Translation> for DAffine3 {
311 fn from(t: Translation) -> Self {
312 DAffine3::from_translation(t.0)
313 }
314}
315
316impl From<Rigid> for Similarity {
317 fn from(r: Rigid) -> Self {
318 Similarity {
319 scale: 1.0,
320 rotation: r.rotation,
321 translation: r.translation,
322 }
323 }
324}
325
326impl From<Rigid> for DAffine3 {
327 fn from(r: Rigid) -> Self {
328 DAffine3::from_rotation_translation(r.rotation, r.translation)
329 }
330}
331
332impl From<Similarity> for DAffine3 {
333 fn from(s: Similarity) -> Self {
334 DAffine3::from_scale_rotation_translation(DVec3::splat(s.scale), s.rotation, s.translation)
335 }
336}
337
338impl From<Diag> for DAffine3 {
339 fn from(d: Diag) -> Self {
340 DAffine3::from_scale(d.0)
341 }
342}
343
344#[derive(Debug, Clone, Copy, PartialEq, Eq)]
348pub struct NotSimilarity;
349
350impl core::fmt::Display for NotSimilarity {
351 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
352 f.write_str("the affine transform is not a similarity transform")
353 }
354}
355
356impl std::error::Error for NotSimilarity {}
357
358impl TryFrom<DAffine3> for Similarity {
359 type Error = NotSimilarity;
360
361 fn try_from(affine: DAffine3) -> Result<Self, Self::Error> {
365 const EPS: f64 = 1e-9;
366
367 let m = affine.matrix3;
368 if !m.is_finite() || !affine.translation.is_finite() {
369 return Err(NotSimilarity);
370 }
371 let cols = [m.x_axis, m.y_axis, m.z_axis];
372 let s = cols[0].length();
373 if s <= EPS {
374 return Err(NotSimilarity);
375 }
376 if (cols[1].length() - s).abs() > EPS * s || (cols[2].length() - s).abs() > EPS * s {
378 return Err(NotSimilarity);
379 }
380 if cols[0].dot(cols[1]).abs() > EPS * s * s
382 || cols[0].dot(cols[2]).abs() > EPS * s * s
383 || cols[1].dot(cols[2]).abs() > EPS * s * s
384 {
385 return Err(NotSimilarity);
386 }
387 if m.determinant() <= 0.0 {
389 return Err(NotSimilarity);
390 }
391 let rotation = DQuat::from_mat3(&DMat3::from_cols(cols[0] / s, cols[1] / s, cols[2] / s));
392 Ok(Self {
393 scale: s,
394 rotation,
395 translation: affine.translation,
396 })
397 }
398}
399
400#[cfg(test)]
401mod tests {
402 use super::*;
403 use glam::dvec3;
404
405 #[test]
406 fn test_similarity_try_from_accepts_similarity() {
407 let sim = Similarity {
408 scale: 2.5,
409 rotation: DQuat::from_axis_angle(DVec3::Z, 0.7),
410 translation: dvec3(1.0, 2.0, 3.0),
411 };
412 let affine = DAffine3::from(sim);
413 let back = Similarity::try_from(affine).unwrap();
414 assert!((back.scale - 2.5).abs() < 1e-9);
415 assert!(back.translation.abs_diff_eq(sim.translation, 1e-9));
416 let rotated = back.rotation * DVec3::X;
418 let expected = sim.rotation * DVec3::X;
419 assert!(rotated.abs_diff_eq(expected, 1e-9));
420 }
421
422 #[test]
423 fn test_similarity_try_from_rejects_invalid_affines() {
424 let cases = [
425 (
426 "non-uniform scale",
427 DAffine3::from_scale(dvec3(1.0, 2.0, 1.0)),
428 ),
429 ("reflection", DAffine3::from_scale(dvec3(-1.0, 1.0, 1.0))),
430 (
431 "non-finite values",
432 DAffine3::from_scale(DVec3::splat(f64::NAN)),
433 ),
434 ];
435 for (name, affine) in cases {
436 assert!(
437 Similarity::try_from(affine).is_err(),
438 "{name} must be rejected"
439 );
440 }
441 }
442
443 #[test]
444 #[should_panic(expected = "similarity scale must be finite and strictly positive")]
445 fn test_similarity_from_scale_rejects_non_positive_values() {
446 Similarity::from_scale(0.0);
447 }
448
449 fn assert_identity<G>(value: G)
450 where
451 G: TransformGroup + PartialEq + core::fmt::Debug,
452 {
453 assert_eq!(G::identity().compose(&value), value);
454 assert_eq!(value.compose(&G::identity()), value);
455 }
456
457 #[test]
458 fn composition_identity_holds_for_every_storage_family() {
459 assert_identity(Translation(dvec3(1.0, 2.0, 3.0)));
460 assert_identity(Rigid {
461 rotation: DQuat::from_rotation_z(0.4),
462 translation: DVec3::X,
463 });
464 assert_identity(Similarity {
465 scale: 2.0,
466 rotation: DQuat::from_rotation_y(0.3),
467 translation: DVec3::Y,
468 });
469 assert_identity(Diag(dvec3(2.0, 0.0, -1.0)));
470 assert_identity(DAffine3::from_scale_rotation_translation(
471 dvec3(2.0, 1.0, 3.0),
472 DQuat::from_rotation_x(0.2),
473 DVec3::Z,
474 ));
475 }
476
477 #[test]
478 fn composition_matches_affine_semantics() {
479 let outer = Rigid::from_axis_angle(DVec3::Z, core::f64::consts::FRAC_PI_2);
480 let inner = Rigid::from_translation(DVec3::X);
481 let composed = outer.compose(&inner);
482 assert!(composed.translation.abs_diff_eq(dvec3(0.0, 1.0, 0.0), 1e-9));
483 assert_eq!(composed.rotation, outer.rotation);
484
485 let outer = Similarity::from_scale(2.0);
486 let inner = Similarity::from(Translation(DVec3::X));
487 let composed = outer.compose(&inner);
488 assert_eq!(composed.scale, 2.0);
489 assert_eq!(composed.translation, dvec3(2.0, 0.0, 0.0));
490 }
491}