1use crate::{Mat3, Mat6, Vec3, Vec6, skew};
8use nalgebra as na;
9
10#[derive(Debug, Clone, Copy)]
12pub struct SpatialVec {
13 pub data: Vec6,
15}
16
17impl SpatialVec {
18 #[inline]
20 pub fn new(angular: Vec3, linear: Vec3) -> Self {
21 Self {
22 data: Vec6::new(
23 angular.x, angular.y, angular.z, linear.x, linear.y, linear.z,
24 ),
25 }
26 }
27
28 #[inline]
30 pub fn zero() -> Self {
31 Self {
32 data: Vec6::zeros(),
33 }
34 }
35
36 #[inline]
38 pub fn angular(&self) -> Vec3 {
39 Vec3::new(self.data[0], self.data[1], self.data[2])
40 }
41
42 #[inline]
44 pub fn linear(&self) -> Vec3 {
45 Vec3::new(self.data[3], self.data[4], self.data[5])
46 }
47
48 pub fn cross_motion(&self, other: &SpatialVec) -> SpatialVec {
51 let w = self.angular();
52 let v = self.linear();
53 let w2 = other.angular();
54 let v2 = other.linear();
55 SpatialVec::new(w.cross(&w2), w.cross(&v2) + v.cross(&w2))
56 }
57
58 pub fn cross_force(&self, other: &SpatialVec) -> SpatialVec {
61 let w = self.angular();
62 let v = self.linear();
63 let t = other.angular();
64 let f = other.linear();
65 SpatialVec::new(w.cross(&t) + v.cross(&f), w.cross(&f))
66 }
67
68 #[inline]
70 pub fn dot(&self, other: &SpatialVec) -> f64 {
71 self.data.dot(&other.data)
72 }
73
74 #[inline]
76 pub fn from_linear_angular(linear: Vec3, angular: Vec3) -> Self {
77 Self::new(angular, linear)
78 }
79}
80
81impl std::ops::Add for SpatialVec {
82 type Output = SpatialVec;
83 #[inline]
84 fn add(self, rhs: SpatialVec) -> SpatialVec {
85 SpatialVec {
86 data: self.data + rhs.data,
87 }
88 }
89}
90
91impl std::ops::Sub for SpatialVec {
92 type Output = SpatialVec;
93 #[inline]
94 fn sub(self, rhs: SpatialVec) -> SpatialVec {
95 SpatialVec {
96 data: self.data - rhs.data,
97 }
98 }
99}
100
101impl std::ops::Mul<f64> for SpatialVec {
102 type Output = SpatialVec;
103 #[inline]
104 fn mul(self, rhs: f64) -> SpatialVec {
105 SpatialVec {
106 data: self.data * rhs,
107 }
108 }
109}
110
111impl std::ops::Neg for SpatialVec {
112 type Output = SpatialVec;
113 #[inline]
114 fn neg(self) -> SpatialVec {
115 SpatialVec { data: -self.data }
116 }
117}
118
119#[derive(Debug, Clone, Copy)]
121pub struct SpatialMat {
122 pub data: Mat6,
123}
124
125impl SpatialMat {
126 #[inline]
128 pub fn from_mat6(data: Mat6) -> Self {
129 Self { data }
130 }
131
132 #[inline]
134 pub fn zero() -> Self {
135 Self {
136 data: Mat6::zeros(),
137 }
138 }
139
140 #[inline]
142 pub fn identity() -> Self {
143 Self {
144 data: Mat6::identity(),
145 }
146 }
147
148 #[inline]
150 pub fn mul_vec(&self, v: &SpatialVec) -> SpatialVec {
151 SpatialVec {
152 data: self.data * v.data,
153 }
154 }
155
156 #[inline]
158 pub fn mul_mat(&self, other: &SpatialMat) -> SpatialMat {
159 SpatialMat {
160 data: self.data * other.data,
161 }
162 }
163
164 #[inline]
166 pub fn transpose(&self) -> SpatialMat {
167 SpatialMat {
168 data: self.data.transpose(),
169 }
170 }
171}
172
173impl std::ops::Add for SpatialMat {
174 type Output = SpatialMat;
175 #[inline]
176 fn add(self, rhs: SpatialMat) -> SpatialMat {
177 SpatialMat {
178 data: self.data + rhs.data,
179 }
180 }
181}
182
183impl std::ops::Sub for SpatialMat {
184 type Output = SpatialMat;
185 #[inline]
186 fn sub(self, rhs: SpatialMat) -> SpatialMat {
187 SpatialMat {
188 data: self.data - rhs.data,
189 }
190 }
191}
192
193#[derive(Debug, Clone, Copy)]
198pub struct SpatialTransform {
199 pub rot: Mat3,
201 pub pos: Vec3,
203}
204
205impl SpatialTransform {
206 pub fn new(rot: Mat3, pos: Vec3) -> Self {
208 Self { rot, pos }
209 }
210
211 pub fn identity() -> Self {
213 Self {
214 rot: Mat3::identity(),
215 pos: Vec3::zeros(),
216 }
217 }
218
219 pub fn rot_x(angle: f64) -> Self {
221 let (s, c) = angle.sin_cos();
222 Self {
223 rot: Mat3::new(1.0, 0.0, 0.0, 0.0, c, -s, 0.0, s, c),
224 pos: Vec3::zeros(),
225 }
226 }
227
228 pub fn rot_y(angle: f64) -> Self {
230 let (s, c) = angle.sin_cos();
231 Self {
232 rot: Mat3::new(c, 0.0, s, 0.0, 1.0, 0.0, -s, 0.0, c),
233 pos: Vec3::zeros(),
234 }
235 }
236
237 pub fn rot_z(angle: f64) -> Self {
239 let (s, c) = angle.sin_cos();
240 Self {
241 rot: Mat3::new(c, -s, 0.0, s, c, 0.0, 0.0, 0.0, 1.0),
242 pos: Vec3::zeros(),
243 }
244 }
245
246 pub fn translation(pos: Vec3) -> Self {
248 Self {
249 rot: Mat3::identity(),
250 pos,
251 }
252 }
253
254 pub fn rot_axis(axis: &na::Unit<Vec3>, angle: f64) -> Self {
256 let rot = na::Rotation3::from_axis_angle(axis, angle);
257 Self {
258 rot: *rot.matrix(),
259 pos: Vec3::zeros(),
260 }
261 }
262
263 pub fn to_motion_matrix(&self) -> Mat6 {
270 let r = self.rot;
271 let px = skew(&self.pos);
272 let neg_rpx = -r * px;
273
274 let mut m = Mat6::zeros();
275 m.fixed_view_mut::<3, 3>(0, 0).copy_from(&r);
277 m.fixed_view_mut::<3, 3>(3, 0).copy_from(&neg_rpx);
279 m.fixed_view_mut::<3, 3>(3, 3).copy_from(&r);
281 m
282 }
283
284 pub fn to_force_matrix(&self) -> Mat6 {
291 let r = self.rot;
292 let px = skew(&self.pos);
293 let neg_rpx = -r * px;
294
295 let mut m = Mat6::zeros();
296 m.fixed_view_mut::<3, 3>(0, 0).copy_from(&r);
298 m.fixed_view_mut::<3, 3>(0, 3).copy_from(&neg_rpx);
300 m.fixed_view_mut::<3, 3>(3, 3).copy_from(&r);
302 m
303 }
304
305 pub fn apply_motion(&self, v: &SpatialVec) -> SpatialVec {
307 let w = v.angular();
308 let vel = v.linear();
309 let new_w = self.rot * w;
310 let new_v = self.rot * (vel - self.pos.cross(&w));
311 SpatialVec::new(new_w, new_v)
312 }
313
314 pub fn apply_force(&self, f: &SpatialVec) -> SpatialVec {
316 let tau = f.angular();
317 let force = f.linear();
318 let new_f = self.rot * force;
319 let new_tau = self.rot * (tau - self.pos.cross(&force));
320 SpatialVec::new(new_tau, new_f)
321 }
322
323 pub fn inv_apply_motion(&self, v: &SpatialVec) -> SpatialVec {
325 let rt = self.rot.transpose();
326 let w = v.angular();
327 let vel = v.linear();
328 let new_w = rt * w;
329 let new_v = rt * vel + self.pos.cross(&(rt * w));
330 SpatialVec::new(new_w, new_v)
331 }
332
333 pub fn inv_apply_force(&self, f: &SpatialVec) -> SpatialVec {
335 let rt = self.rot.transpose();
336 let tau = f.angular();
337 let force = f.linear();
338 let new_f = rt * force;
339 let new_tau = rt * tau + self.pos.cross(&(rt * force));
340 SpatialVec::new(new_tau, new_f)
341 }
342
343 pub fn compose(&self, other: &SpatialTransform) -> SpatialTransform {
345 SpatialTransform {
346 rot: self.rot * other.rot,
347 pos: other.pos + other.rot.transpose() * self.pos,
348 }
349 }
350
351 pub fn inverse(&self) -> SpatialTransform {
353 let rt = self.rot.transpose();
354 SpatialTransform {
355 rot: rt,
356 pos: -(self.rot * self.pos),
357 }
358 }
359
360 pub fn translation_vector(&self) -> Vec3 {
362 self.pos
363 }
364
365 pub fn rotation_matrix(&self) -> Mat3 {
367 self.rot
368 }
369}
370
371#[derive(Debug, Clone, Copy)]
376pub struct SpatialInertia {
377 pub mass: f64,
379 pub com: Vec3,
381 pub inertia: Mat3,
383}
384
385impl SpatialInertia {
386 pub fn new(mass: f64, com: Vec3, inertia: Mat3) -> Self {
388 Self { mass, com, inertia }
389 }
390
391 pub fn point_mass(mass: f64, pos: Vec3) -> Self {
393 Self {
394 mass,
395 com: pos,
396 inertia: Mat3::zeros(),
397 }
398 }
399
400 pub fn rod(mass: f64, length: f64) -> Self {
403 let i = mass * length * length / 12.0;
404 Self {
405 mass,
406 com: Vec3::zeros(),
407 inertia: Mat3::new(i, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, i),
408 }
409 }
410
411 pub fn sphere(mass: f64, radius: f64) -> Self {
413 let i = 2.0 / 5.0 * mass * radius * radius;
414 Self {
415 mass,
416 com: Vec3::zeros(),
417 inertia: Mat3::from_diagonal(&Vec3::new(i, i, i)),
418 }
419 }
420
421 pub fn to_matrix(&self) -> SpatialMat {
426 let cx = skew(&self.com);
427 let m = self.mass;
428 let i3 = Mat3::identity() * m;
429
430 let mut mat = Mat6::zeros();
431 let top_left = self.inertia + cx * cx.transpose() * m;
433 mat.fixed_view_mut::<3, 3>(0, 0).copy_from(&top_left);
434 let mcx = cx * m;
436 mat.fixed_view_mut::<3, 3>(0, 3).copy_from(&mcx);
437 mat.fixed_view_mut::<3, 3>(3, 0).copy_from(&mcx.transpose());
439 mat.fixed_view_mut::<3, 3>(3, 3).copy_from(&i3);
441
442 SpatialMat::from_mat6(mat)
443 }
444
445 pub fn transform(&self, xform: &SpatialTransform) -> SpatialInertia {
447 let x_force = SpatialMat::from_mat6(xform.to_force_matrix());
450 let i_mat = self.to_matrix();
451 let transformed = x_force
452 .mul_mat(&i_mat)
453 .mul_mat(&SpatialMat::from_mat6(xform.to_motion_matrix()).transpose());
454
455 let mass = transformed.data[(3, 3)];
457 let com = if mass.abs() > 1e-12 {
458 Vec3::new(
459 transformed.data[(2, 3)] / mass,
460 -transformed.data[(0, 3)] / mass, transformed.data[(1, 3)] / mass, )
463 } else {
464 Vec3::zeros()
465 };
466 let cx = skew(&com);
468 let inertia_with_com = transformed.data.fixed_view::<3, 3>(0, 0).into_owned();
469 let inertia = inertia_with_com - cx * cx.transpose() * mass;
470
471 SpatialInertia { mass, com, inertia }
472 }
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478 use approx::assert_relative_eq;
479
480 #[test]
481 fn test_spatial_vec_cross_motion() {
482 let v1 = SpatialVec::new(Vec3::new(0.0, 0.0, 1.0), Vec3::zeros());
483 let v2 = SpatialVec::new(Vec3::new(1.0, 0.0, 0.0), Vec3::zeros());
484 let result = v1.cross_motion(&v2);
485 assert_relative_eq!(result.angular().y, 1.0, epsilon = 1e-10);
487 }
488
489 #[test]
490 fn test_transform_identity() {
491 let xf = SpatialTransform::identity();
492 let v = SpatialVec::new(Vec3::new(1.0, 2.0, 3.0), Vec3::new(4.0, 5.0, 6.0));
493 let result = xf.apply_motion(&v);
494 assert_relative_eq!(result.data, v.data, epsilon = 1e-10);
495 }
496
497 #[test]
498 fn test_transform_inverse_roundtrip() {
499 let xf = SpatialTransform::new(
500 *na::Rotation3::from_axis_angle(&na::Vector3::z_axis(), 0.5).matrix(),
501 Vec3::new(1.0, 2.0, 3.0),
502 );
503 let v = SpatialVec::new(Vec3::new(1.0, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0));
504
505 let forward = xf.apply_motion(&v);
506 let back = xf.inv_apply_motion(&forward);
507 assert_relative_eq!(back.data, v.data, epsilon = 1e-10);
508 }
509
510 #[test]
511 fn test_compose_transforms() {
512 let xf1 = SpatialTransform::translation(Vec3::new(1.0, 0.0, 0.0));
513 let xf2 = SpatialTransform::translation(Vec3::new(0.0, 2.0, 0.0));
514 let composed = xf1.compose(&xf2);
515 assert_relative_eq!(composed.pos, Vec3::new(1.0, 2.0, 0.0), epsilon = 1e-10);
516 }
517
518 #[test]
519 fn test_spatial_inertia_point_mass() {
520 let si = SpatialInertia::point_mass(2.0, Vec3::new(0.0, 1.0, 0.0));
521 let mat = si.to_matrix();
522 assert_relative_eq!(mat.data[(3, 3)], 2.0, epsilon = 1e-10);
525 assert_relative_eq!(mat.data[(4, 4)], 2.0, epsilon = 1e-10);
526 assert_relative_eq!(mat.data[(5, 5)], 2.0, epsilon = 1e-10);
527 }
528
529 #[test]
530 fn test_spatial_inertia_sphere() {
531 let si = SpatialInertia::sphere(5.0, 0.1);
532 let expected_i = 2.0 / 5.0 * 5.0 * 0.01;
533 assert_relative_eq!(si.inertia[(0, 0)], expected_i, epsilon = 1e-10);
534 assert_relative_eq!(si.inertia[(1, 1)], expected_i, epsilon = 1e-10);
535 assert_relative_eq!(si.inertia[(2, 2)], expected_i, epsilon = 1e-10);
536 }
537}
538
539#[cfg(test)]
540mod prop_tests {
541 use super::*;
542 use crate::quaternion::Quat;
543 use proptest::prelude::*;
544
545 const EPS: f64 = 1e-9;
546
547 fn arb_pos() -> impl Strategy<Value = Vec3> {
548 (-10.0..10.0_f64, -10.0..10.0_f64, -10.0..10.0_f64)
549 .prop_map(|(x, y, z)| Vec3::new(x, y, z))
550 }
551
552 fn arb_angle() -> impl Strategy<Value = f64> {
553 -std::f64::consts::PI..std::f64::consts::PI
554 }
555
556 fn arb_unit_axis() -> impl Strategy<Value = na::Unit<Vec3>> {
557 (-1.0..1.0_f64, -1.0..1.0_f64, -1.0..1.0_f64)
559 .prop_filter("non-zero axis", |(x, y, z)| x * x + y * y + z * z > 0.01)
560 .prop_map(|(x, y, z)| na::Unit::new_normalize(Vec3::new(x, y, z)))
561 }
562
563 fn arb_transform() -> impl Strategy<Value = SpatialTransform> {
564 (arb_unit_axis(), arb_angle(), arb_pos()).prop_map(|(axis, angle, pos)| {
565 let rot = na::Rotation3::from_axis_angle(&axis, angle);
566 SpatialTransform::new(*rot.matrix(), pos)
567 })
568 }
569
570 fn arb_spatial_vec() -> impl Strategy<Value = SpatialVec> {
571 (arb_pos(), arb_pos()).prop_map(|(a, l)| SpatialVec::new(a, l))
572 }
573
574 proptest! {
575 #[test]
576 fn compose_with_inverse_is_identity(xf in arb_transform()) {
577 let result = xf.compose(&xf.inverse());
578 let id = SpatialTransform::identity();
579 for i in 0..3 {
580 for j in 0..3 {
581 prop_assert!((result.rot[(i, j)] - id.rot[(i, j)]).abs() < EPS,
582 "rot[{},{}]: {} vs {}", i, j, result.rot[(i, j)], id.rot[(i, j)]);
583 }
584 }
585 for i in 0..3 {
586 prop_assert!((result.pos[i] - id.pos[i]).abs() < EPS,
587 "pos[{}]: {} vs {}", i, result.pos[i], id.pos[i]);
588 }
589 }
590
591 #[test]
592 fn compose_is_associative(
593 a in arb_transform(),
594 b in arb_transform(),
595 c in arb_transform(),
596 ) {
597 let ab_c = a.compose(&b).compose(&c);
598 let a_bc = a.compose(&b.compose(&c));
599 for i in 0..3 {
600 for j in 0..3 {
601 prop_assert!((ab_c.rot[(i, j)] - a_bc.rot[(i, j)]).abs() < EPS,
602 "rot[{},{}]: {} vs {}", i, j, ab_c.rot[(i, j)], a_bc.rot[(i, j)]);
603 }
604 }
605 for i in 0..3 {
606 prop_assert!((ab_c.pos[i] - a_bc.pos[i]).abs() < EPS,
607 "pos[{}]: {} vs {}", i, ab_c.pos[i], a_bc.pos[i]);
608 }
609 }
610
611 #[test]
612 fn apply_force_matches_matrix(xf in arb_transform(), f in arb_spatial_vec()) {
613 let applied = xf.apply_force(&f);
614 let mat_result = SpatialMat::from_mat6(xf.to_force_matrix()).mul_vec(&f);
615 for i in 0..6 {
616 prop_assert!((applied.data[i] - mat_result.data[i]).abs() < EPS,
617 "component {}: {} vs {}", i, applied.data[i], mat_result.data[i]);
618 }
619 }
620
621 #[test]
622 fn apply_motion_matches_matrix(xf in arb_transform(), v in arb_spatial_vec()) {
623 let applied = xf.apply_motion(&v);
624 let mat_result = SpatialMat::from_mat6(xf.to_motion_matrix()).mul_vec(&v);
625 for i in 0..6 {
626 prop_assert!((applied.data[i] - mat_result.data[i]).abs() < EPS,
627 "component {}: {} vs {}", i, applied.data[i], mat_result.data[i]);
628 }
629 }
630
631 #[test]
632 fn sphere_inertia_matrix_is_symmetric(
633 mass in 0.1..100.0_f64,
634 radius in 0.01..10.0_f64,
635 ) {
636 let si = SpatialInertia::sphere(mass, radius);
637 let mat = si.to_matrix().data;
638 for i in 0..6 {
639 for j in 0..6 {
640 prop_assert!((mat[(i, j)] - mat[(j, i)]).abs() < EPS,
641 "not symmetric at ({},{}): {} vs {}", i, j, mat[(i, j)], mat[(j, i)]);
642 }
643 }
644 }
645
646 #[test]
647 fn quat_to_matrix_is_rotation(
648 axis in arb_unit_axis(),
649 angle in arb_angle(),
650 ) {
651 let q = Quat::from_axis_angle(&axis.into_inner(), angle).normalize();
652 let m = q.to_matrix();
653 let det = m.determinant();
655 prop_assert!((det - 1.0).abs() < EPS, "det = {}", det);
656 let rrt = m * m.transpose();
658 let id = Mat3::identity();
659 for i in 0..3 {
660 for j in 0..3 {
661 prop_assert!((rrt[(i, j)] - id[(i, j)]).abs() < EPS,
662 "R*R^T[{},{}]: {} vs {}", i, j, rrt[(i, j)], id[(i, j)]);
663 }
664 }
665 }
666
667 #[test]
668 fn quat_matrix_roundtrip(
669 axis in arb_unit_axis(),
670 angle in arb_angle(),
671 ) {
672 let q = Quat::from_axis_angle(&axis.into_inner(), angle).normalize();
673 let m = q.to_matrix();
674 let q2 = Quat::from_matrix(&m).normalize();
675 let same = (q.w - q2.w).abs() < EPS
677 && (q.v.x - q2.v.x).abs() < EPS
678 && (q.v.y - q2.v.y).abs() < EPS
679 && (q.v.z - q2.v.z).abs() < EPS;
680 let neg = (q.w + q2.w).abs() < EPS
681 && (q.v.x + q2.v.x).abs() < EPS
682 && (q.v.y + q2.v.y).abs() < EPS
683 && (q.v.z + q2.v.z).abs() < EPS;
684 prop_assert!(same || neg,
685 "roundtrip failed: q={:?}, q2={:?}", q, q2);
686 }
687 }
688}