spatial_math/
spatial_vec.rs

1// Copyright (C) 2020-2025 spatial-math authors. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::fmt::Display;
16use std::ops::{Add, AddAssign, Mul, Neg, Sub, SubAssign};
17
18use super::traits::SpatialVec;
19use super::{SVector, VEC3_ZERO, Vec3};
20use crate::exts::MatrixExt;
21use crate::{Real, Vec6};
22
23/// Type marker for spatial motion vectors.
24///
25/// This zero-sized type is used at the type level to distinguish
26/// motion vectors from force vectors, enabling compile-time type safety.
27#[derive(Clone, Copy, Debug, Default, PartialEq)]
28pub struct Motion;
29
30/// Type marker for spatial force vectors.
31///
32/// This zero-sized type is used at the type level to distinguish
33/// force vectors from motion vectors, enabling compile-time type safety.
34#[derive(Clone, Copy, Debug, Default, PartialEq)]
35pub struct Force;
36
37/// A 6D spatial vector representing either motion or force.
38#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
39#[derive(Clone, Copy, Debug, Default, PartialEq)]
40pub struct SpatialVector<T = ()> {
41    /// The top (angular) component of the spatial vector.
42    ///
43    /// - For motion vectors: angular velocity ω
44    /// - For force vectors: moment/torque n
45    pub top: Vec3,
46
47    /// The bottom (linear) component of the spatial vector.
48    ///
49    /// - For motion vectors: linear velocity v
50    /// - For force vectors: force f
51    pub bottom: Vec3,
52
53    /// Phantom data for type-level distinction between motion and force vectors.
54    /// This ensures that `SpatialMotionVector` and `SpatialForceVector` are
55    /// different types at compile time, preventing misuse.
56    _phantom: std::marker::PhantomData<T>,
57}
58
59impl<T> Display for SpatialVector<T> {
60    #[inline]
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        write!(
63            f,
64            "[{},{},{}, {},{},{}]",
65            self.top[0], self.top[1], self.top[2], self.bottom[0], self.bottom[1], self.bottom[2]
66        )
67    }
68}
69
70impl<T> From<SpatialVector<T>> for Vec6 {
71    #[inline]
72    fn from(v: SpatialVector<T>) -> Self {
73        v.into_vec6()
74    }
75}
76
77impl<T> SpatialVector<T> {
78    /// Zero spatial vector (both angular and linear components are zero).
79    ///
80    /// This is useful for initialization and as the identity element
81    /// for vector addition operations.
82    pub const ZERO: Self = Self {
83        top: VEC3_ZERO,
84        bottom: VEC3_ZERO,
85        _phantom: std::marker::PhantomData,
86    };
87
88    /// Create a spatial vector from a 6-element array.
89    ///
90    /// # Array Format
91    ///
92    /// - **Motion vectors**: [ωx, ωy, ωz, vx, vy, vz] where ω is angular velocity and v is linear
93    ///   velocity
94    /// - **Force vectors**: [nx, ny, nz, fx, fy, fz] where n is moment/torque and f is force
95    ///
96    /// # Example
97    ///
98    /// ```rust
99    /// use spatial_math::SpatialMotionVector;
100    ///
101    /// let velocity = SpatialMotionVector::from_array([1.0, 0.0, 0.0, 0.0, 1.0, 0.0]);
102    /// // ω = [1, 0, 0], v = [0, 1, 0]
103    /// ```
104    #[inline]
105    pub const fn from_array(array: [Real; 6]) -> Self {
106        Self {
107            top: Vec3::new(array[0], array[1], array[2]),
108            bottom: Vec3::new(array[3], array[4], array[5]),
109            _phantom: std::marker::PhantomData,
110        }
111    }
112
113    /// Create a spatial vector from a 6D static vector.
114    ///
115    /// This is useful when converting from other linear algebra
116    /// representations that use `SVector<6>`.
117    #[inline]
118    pub fn from_vec6(vec: SVector<6>) -> Self {
119        Self {
120            top: Vec3::new(vec[0], vec[1], vec[2]),
121            bottom: Vec3::new(vec[3], vec[4], vec[5]),
122            _phantom: std::marker::PhantomData,
123        }
124    }
125
126    /// Create a spatial vector from separate 3D top and bottom components.
127    ///
128    /// This is often used when you have separate angular and linear quantities
129    /// that need to be combined into a spatial vector.
130    ///
131    /// # Arguments
132    ///
133    /// * `top` - The angular component (ω for motion, n for force)
134    /// * `bottom` - The linear component (v for motion, f for force)
135    #[inline]
136    pub const fn from_pair(top: Vec3, bottom: Vec3) -> Self {
137        Self {
138            top,
139            bottom,
140            _phantom: std::marker::PhantomData,
141        }
142    }
143
144    /// Convert the spatial vector to a 6-element array.
145    ///
146    /// Returns the components in the same format as expected by `from_array()`:
147    /// [top.x, top.y, top.z, bottom.x, bottom.y, bottom.z]
148    #[inline]
149    pub fn into_array(self) -> [Real; 6] {
150        [
151            self.top.x,
152            self.top.y,
153            self.top.z,
154            self.bottom.x,
155            self.bottom.y,
156            self.bottom.z,
157        ]
158    }
159
160    #[inline]
161    pub fn any_nan(&self) -> bool {
162        self.top.any_nan() || self.bottom.any_nan()
163    }
164
165    #[inline]
166    #[must_use]
167    pub fn add(&self, rhs: &Self) -> Self {
168        Self::from_pair(self.top + rhs.top, self.bottom + rhs.bottom)
169    }
170
171    #[inline]
172    #[must_use]
173    pub fn sub(&self, rhs: &Self) -> Self {
174        Self::from_pair(self.top - rhs.top, self.bottom - rhs.bottom)
175    }
176
177    #[inline]
178    #[must_use]
179    pub fn neg(&self) -> Self {
180        Self::from_pair(-self.top, -self.bottom)
181    }
182
183    #[inline]
184    #[must_use]
185    pub fn scale(&self, scalar: Real) -> Self {
186        Self::from_pair(self.top * scalar, self.bottom * scalar)
187    }
188
189    #[inline]
190    #[must_use]
191    pub fn cross(&self, rhs: &Self) -> Self {
192        Self::from_pair(
193            self.top.cross(&rhs.top),
194            self.top.cross(&rhs.bottom) + self.bottom.cross(&rhs.top),
195        )
196    }
197
198    #[inline]
199    pub fn into_vec6(self) -> SVector<6> {
200        SVector::from_iterator([
201            self.top.x,
202            self.top.y,
203            self.top.z,
204            self.bottom.x,
205            self.bottom.y,
206            self.bottom.z,
207        ])
208    }
209}
210
211impl<T> Add<Self> for SpatialVector<T> {
212    type Output = Self;
213
214    #[inline]
215    fn add(self, rhs: Self) -> Self::Output {
216        SpatialVector::add(&self, &rhs)
217    }
218}
219
220impl<T> AddAssign<Self> for SpatialVector<T> {
221    #[inline]
222    fn add_assign(&mut self, rhs: Self) {
223        *self = SpatialVector::add(&*self, &rhs);
224    }
225}
226
227impl<T> Sub<Self> for SpatialVector<T> {
228    type Output = Self;
229
230    #[inline]
231    fn sub(self, rhs: Self) -> Self::Output {
232        SpatialVector::sub(&self, &rhs)
233    }
234}
235
236impl<T> SubAssign<Self> for SpatialVector<T> {
237    #[inline]
238    fn sub_assign(&mut self, rhs: Self) {
239        *self = SpatialVector::sub(&*self, &rhs);
240    }
241}
242
243impl<T> Mul<Real> for SpatialVector<T> {
244    type Output = Self;
245
246    #[inline]
247    fn mul(self, rhs: Real) -> Self::Output {
248        self.scale(rhs)
249    }
250}
251
252impl<T> Neg for SpatialVector<T> {
253    type Output = Self;
254
255    #[inline]
256    fn neg(self) -> Self::Output {
257        Self::neg(&self)
258    }
259}
260
261pub type SpatialMotionVector = SpatialVector<Motion>;
262pub type SpatialForceVector = SpatialVector<Force>;
263
264impl SpatialVec for SpatialMotionVector {
265    type DualType = SpatialForceVector;
266
267    #[inline]
268    fn from_pair(top: Vec3, bottom: Vec3) -> Self {
269        Self::from_pair(top, bottom)
270    }
271
272    #[inline]
273    fn top(&self) -> Vec3 {
274        self.top
275    }
276
277    #[inline]
278    fn bottom(&self) -> Vec3 {
279        self.bottom
280    }
281}
282
283impl SpatialVec for SpatialForceVector {
284    type DualType = SpatialMotionVector;
285
286    #[inline]
287    fn from_pair(top: Vec3, bottom: Vec3) -> Self {
288        Self::from_pair(top, bottom)
289    }
290
291    #[inline]
292    fn top(&self) -> Vec3 {
293        self.top
294    }
295
296    #[inline]
297    fn bottom(&self) -> Vec3 {
298        self.bottom
299    }
300}
301
302#[cfg(feature = "approx")]
303mod approx_eq {
304    use crate::Real;
305
306    impl<T> approx::AbsDiffEq for super::SpatialVector<T>
307    where
308        T: PartialEq,
309    {
310        type Epsilon = Real;
311
312        fn default_epsilon() -> Self::Epsilon {
313            Real::EPSILON
314        }
315
316        fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool {
317            self.top.abs_diff_eq(&other.top, epsilon)
318                && self.bottom.abs_diff_eq(&other.bottom, epsilon)
319        }
320    }
321
322    impl<T> approx::RelativeEq for super::SpatialVector<T>
323    where
324        T: PartialEq,
325    {
326        fn default_max_relative() -> Self::Epsilon {
327            Real::EPSILON
328        }
329
330        fn relative_eq(
331            &self,
332            other: &Self,
333            epsilon: Self::Epsilon,
334            max_relative: Self::Epsilon,
335        ) -> bool {
336            self.top.relative_eq(&other.top, epsilon, max_relative)
337                && self
338                    .bottom
339                    .relative_eq(&other.bottom, epsilon, max_relative)
340        }
341    }
342}
343
344#[cfg(test)]
345mod tests {
346
347    use approx::assert_relative_eq;
348
349    use super::*;
350    use crate::vec3;
351
352    #[test]
353    fn test_construct() {
354        let v = SpatialVector::<()>::from_array([1., 2., 3., 4., 5., 6.]);
355        assert_eq!(v.top, vec3(1., 2., 3.));
356        assert_eq!(v.bottom, vec3(4., 5., 6.));
357
358        let v = SpatialVector::<()>::from_vec6(SVector::from_iterator([1., 2., 3., 4., 5., 6.]));
359        assert_eq!(v.top, vec3(1., 2., 3.));
360        assert_eq!(v.bottom, vec3(4., 5., 6.));
361    }
362
363    #[test]
364    fn test_add() {
365        let v1 = SpatialVector::<()>::from_array([1., 2., 3., 4., 5., 6.]);
366        let v2 = SpatialVector::<()>::from_array([7., 8., 9., 10., 11., 12.]);
367
368        let result = v1 + v2;
369
370        assert_eq!(result.top, vec3(8., 10., 12.));
371        assert_eq!(result.bottom, vec3(14., 16., 18.));
372    }
373
374    #[test]
375    fn test_add_assign() {
376        let mut v1 = SpatialVector::<()>::from_array([1., 2., 3., 4., 5., 6.]);
377        let v2 = SpatialVector::<()>::from_array([7., 8., 9., 10., 11., 12.]);
378
379        v1 += v2;
380
381        assert_eq!(v1.top, vec3(8., 10., 12.));
382        assert_eq!(v1.bottom, vec3(14., 16., 18.));
383    }
384
385    #[test]
386    fn test_sub() {
387        let v1 = SpatialVector::<()>::from_array([1., 2., 3., 4., 5., 6.]);
388        let v2 = SpatialVector::<()>::from_array([7., 8., 9., 10., 11., 12.]);
389
390        let result = v1 - v2;
391
392        assert_eq!(result.top, vec3(-6., -6., -6.));
393        assert_eq!(result.bottom, vec3(-6., -6., -6.));
394    }
395
396    #[test]
397    fn test_sub_assign() {
398        let mut v1 = SpatialVector::<()>::from_array([1., 2., 3., 4., 5., 6.]);
399        let v2 = SpatialVector::<()>::from_array([7., 8., 9., 10., 11., 12.]);
400
401        v1 -= v2;
402
403        assert_eq!(v1.top, vec3(-6., -6., -6.));
404        assert_eq!(v1.bottom, vec3(-6., -6., -6.));
405    }
406
407    #[test]
408    fn test_mul() {
409        let v = SpatialVector::<()>::from_array([1., 2., 3., 4., 5., 6.]);
410        let scalar = 2.0;
411
412        let result = v * scalar;
413
414        assert_eq!(result.top, vec3(2., 4., 6.));
415        assert_eq!(result.bottom, vec3(8., 10., 12.));
416    }
417
418    #[test]
419    fn test_neg() {
420        let v = SpatialVector::<()>::from_array([1., 2., 3., 4., 5., 6.]);
421
422        let result = v.neg();
423
424        assert_eq!(result.top, vec3(-1., -2., -3.));
425        assert_eq!(result.bottom, vec3(-4., -5., -6.));
426    }
427
428    #[test]
429    fn test_cross() {
430        let spatial_v1 = SpatialMotionVector::from_array([1., 2., 3., 4., 5., 6.]);
431        let spatial_v2 = SpatialMotionVector::from_array([7., 8., 9., 10., 11., 12.]);
432
433        let result = spatial_v1.cross(&spatial_v2);
434
435        let w1 = spatial_v1.top;
436        let v1 = spatial_v1.bottom;
437        let w2 = spatial_v2.top;
438        let v2 = spatial_v2.bottom;
439
440        assert_relative_eq!(result.top, w1.cross(&w2));
441        assert_relative_eq!(result.bottom, w1.cross(&v2) + v1.cross(&w2));
442    }
443}