use core::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign};
use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Vector {
pub x: f64,
pub y: f64,
pub z: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Vector2 {
pub x: f64,
pub y: f64,
}
impl Vector {
pub const ZERO: Self = Self::new(0.0, 0.0, 0.0);
pub const X: Self = Self::new(1.0, 0.0, 0.0);
pub const Y: Self = Self::new(0.0, 1.0, 0.0);
pub const Z: Self = Self::new(0.0, 0.0, 1.0);
#[must_use]
pub const fn new(x: f64, y: f64, z: f64) -> Self {
Self { x, y, z }
}
#[must_use]
pub const fn splat(v: f64) -> Self {
Self::new(v, v, v)
}
#[must_use]
pub const fn to_array(self) -> [f64; 3] {
[self.x, self.y, self.z]
}
#[must_use]
pub const fn from_array([x, y, z]: [f64; 3]) -> Self {
Self::new(x, y, z)
}
pub fn coord(self, index: usize) -> OgeomResult<f64> {
match index {
0 => Ok(self.x),
1 => Ok(self.y),
2 => Ok(self.z),
_ => ogeom_bail!(Range, "vector component {index} of 3"),
}
}
#[must_use]
pub fn dot(self, other: Self) -> f64 {
self.x
.mul_add(other.x, self.y.mul_add(other.y, self.z * other.z))
}
#[must_use]
pub fn cross(self, other: Self) -> Self {
Self::new(
self.y * other.z - self.z * other.y,
self.z * other.x - self.x * other.z,
self.x * other.y - self.y * other.x,
)
}
#[must_use]
pub fn triple(self, a: Self, b: Self) -> f64 {
self.dot(a.cross(b))
}
#[must_use]
pub fn square_magnitude(self) -> f64 {
self.dot(self)
}
#[must_use]
pub fn magnitude(self) -> f64 {
self.square_magnitude().sqrt()
}
pub fn normalized(self, tol: Tolerances) -> OgeomResult<Self> {
if !self.is_finite() {
ogeom_bail!(Construction, "cannot normalize a non-finite vector");
}
let m = self.magnitude();
if m <= tol.confusion() {
ogeom_bail!(Construction, "cannot normalize a vector of magnitude {m}");
}
Ok(self / m)
}
#[must_use]
pub fn is_finite(self) -> bool {
self.x.is_finite() && self.y.is_finite() && self.z.is_finite()
}
#[must_use]
pub fn is_zero(self, tol: Tolerances) -> bool {
self.magnitude() <= tol.confusion()
}
#[must_use]
pub fn is_equal(self, other: Self, tol: Tolerances) -> bool {
(self - other).magnitude() <= tol.confusion()
}
pub fn angle(self, other: Self, tol: Tolerances) -> OgeomResult<f64> {
if self.is_zero(tol) || other.is_zero(tol) {
ogeom_bail!(Construction, "angle is undefined for a null vector");
}
Ok(self.cross(other).magnitude().atan2(self.dot(other)))
}
pub fn signed_angle(self, other: Self, reference: Self, tol: Tolerances) -> OgeomResult<f64> {
let unsigned = self.angle(other, tol)?;
let normal = self.cross(other);
if normal.is_zero(tol) {
return Ok(unsigned);
}
if reference.is_zero(tol) {
ogeom_bail!(Construction, "reference vector is null");
}
Ok(if normal.dot(reference) < 0.0 {
-unsigned
} else {
unsigned
})
}
#[must_use]
pub fn is_parallel(self, other: Self, tol: Tolerances) -> bool {
self.angle(other, tol).is_ok_and(|a| a <= tol.angular())
}
#[must_use]
pub fn is_collinear(self, other: Self, tol: Tolerances) -> bool {
self.angle(other, tol)
.is_ok_and(|a| a <= tol.angular() || (core::f64::consts::PI - a) <= tol.angular())
}
#[must_use]
pub fn is_normal(self, other: Self, tol: Tolerances) -> bool {
self.angle(other, tol)
.is_ok_and(|a| (core::f64::consts::FRAC_PI_2 - a).abs() <= tol.angular())
}
pub fn projected_onto(self, other: Self, tol: Tolerances) -> OgeomResult<Self> {
let square = other.square_magnitude();
if square <= tol.confusion() * tol.confusion() {
ogeom_bail!(Construction, "cannot project onto a null vector");
}
Ok(other * (self.dot(other) / square))
}
#[must_use]
pub fn lerp(self, other: Self, t: f64) -> Self {
self + (other - self) * t
}
#[must_use]
pub fn min(self, other: Self) -> Self {
Self::new(
self.x.min(other.x),
self.y.min(other.y),
self.z.min(other.z),
)
}
#[must_use]
pub fn max(self, other: Self) -> Self {
Self::new(
self.x.max(other.x),
self.y.max(other.y),
self.z.max(other.z),
)
}
#[must_use]
pub const fn xy(self) -> Vector2 {
Vector2::new(self.x, self.y)
}
}
impl Vector2 {
pub const ZERO: Self = Self::new(0.0, 0.0);
pub const X: Self = Self::new(1.0, 0.0);
pub const Y: Self = Self::new(0.0, 1.0);
#[must_use]
pub const fn new(x: f64, y: f64) -> Self {
Self { x, y }
}
#[must_use]
pub const fn to_array(self) -> [f64; 2] {
[self.x, self.y]
}
#[must_use]
pub const fn from_array([x, y]: [f64; 2]) -> Self {
Self::new(x, y)
}
#[must_use]
pub fn dot(self, other: Self) -> f64 {
self.x * other.x + self.y * other.y
}
#[must_use]
pub fn cross(self, other: Self) -> f64 {
self.x * other.y - self.y * other.x
}
#[must_use]
pub fn square_magnitude(self) -> f64 {
self.dot(self)
}
#[must_use]
pub fn magnitude(self) -> f64 {
self.square_magnitude().sqrt()
}
#[must_use]
pub const fn perpendicular(self) -> Self {
Self::new(-self.y, self.x)
}
pub fn normalized(self, tol: Tolerances) -> OgeomResult<Self> {
if !self.is_finite() {
ogeom_bail!(Construction, "cannot normalize a non-finite vector");
}
let m = self.magnitude();
if m <= tol.confusion() {
ogeom_bail!(Construction, "cannot normalize a vector of magnitude {m}");
}
Ok(self / m)
}
#[must_use]
pub fn is_finite(self) -> bool {
self.x.is_finite() && self.y.is_finite()
}
#[must_use]
pub fn is_zero(self, tol: Tolerances) -> bool {
self.magnitude() <= tol.confusion()
}
#[must_use]
pub fn is_equal(self, other: Self, tol: Tolerances) -> bool {
(self - other).magnitude() <= tol.confusion()
}
pub fn angle(self, other: Self, tol: Tolerances) -> OgeomResult<f64> {
if self.is_zero(tol) || other.is_zero(tol) {
ogeom_bail!(Construction, "angle is undefined for a null vector");
}
Ok(self.cross(other).atan2(self.dot(other)))
}
#[must_use]
pub fn lerp(self, other: Self, t: f64) -> Self {
self + (other - self) * t
}
#[must_use]
pub const fn to_3d(self) -> Vector {
Vector::new(self.x, self.y, 0.0)
}
}
macro_rules! impl_vector_ops {
($t:ty, $($f:ident),+) => {
impl Add for $t {
type Output = Self;
fn add(self, o: Self) -> Self { Self { $($f: self.$f + o.$f),+ } }
}
impl Sub for $t {
type Output = Self;
fn sub(self, o: Self) -> Self { Self { $($f: self.$f - o.$f),+ } }
}
impl Neg for $t {
type Output = Self;
fn neg(self) -> Self { Self { $($f: -self.$f),+ } }
}
impl Mul<f64> for $t {
type Output = Self;
fn mul(self, s: f64) -> Self { Self { $($f: self.$f * s),+ } }
}
impl Mul<$t> for f64 {
type Output = $t;
fn mul(self, v: $t) -> $t { v * self }
}
impl Div<f64> for $t {
type Output = Self;
fn div(self, s: f64) -> Self { Self { $($f: self.$f / s),+ } }
}
impl AddAssign for $t {
fn add_assign(&mut self, o: Self) { *self = *self + o; }
}
impl SubAssign for $t {
fn sub_assign(&mut self, o: Self) { *self = *self - o; }
}
impl MulAssign<f64> for $t {
fn mul_assign(&mut self, s: f64) { *self = *self * s; }
}
impl DivAssign<f64> for $t {
fn div_assign(&mut self, s: f64) { *self = *self / s; }
}
};
}
impl_vector_ops!(Vector, x, y, z);
impl_vector_ops!(Vector2, x, y);
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use approx::assert_relative_eq;
const T: Tolerances = Tolerances::millimetres();
#[test]
fn cross_product_is_right_handed() {
assert_eq!(Vector::X.cross(Vector::Y), Vector::Z);
assert_eq!(Vector::Y.cross(Vector::Z), Vector::X);
assert_eq!(Vector::Z.cross(Vector::X), Vector::Y);
assert_eq!(Vector::Y.cross(Vector::X), -Vector::Z);
}
#[test]
fn normalizing_a_null_vector_is_refused_not_approximated() {
assert!(Vector::ZERO.normalized(T).is_err());
assert!(Vector::new(1e-12, 0.0, 0.0).normalized(T).is_err());
assert!(Vector::new(f64::NAN, 0.0, 0.0).normalized(T).is_err());
assert!(Vector::new(f64::INFINITY, 0.0, 0.0).normalized(T).is_err());
assert!(Vector::new(3.0, 4.0, 0.0).normalized(T).is_ok());
}
#[test]
fn normalized_has_unit_magnitude() {
let v = Vector::new(3.0, 4.0, 12.0).normalized(T).unwrap();
assert_relative_eq!(v.magnitude(), 1.0, epsilon = 1e-15);
}
#[test]
fn angle_is_accurate_for_nearly_parallel_vectors() {
let tiny: f64 = 1e-9;
let a = Vector::X;
let b = Vector::new(tiny.cos(), tiny.sin(), 0.0);
assert_relative_eq!(a.angle(b, T).unwrap(), tiny, max_relative = 1e-9);
}
#[test]
fn angle_endpoints() {
assert_relative_eq!(Vector::X.angle(Vector::X, T).unwrap(), 0.0);
assert_relative_eq!(
Vector::X.angle(-Vector::X, T).unwrap(),
core::f64::consts::PI
);
assert_relative_eq!(
Vector::X.angle(Vector::Y, T).unwrap(),
core::f64::consts::FRAC_PI_2
);
assert!(Vector::X.angle(Vector::ZERO, T).is_err());
}
#[test]
fn signed_angle_respects_the_reference_direction() {
let a = Vector::X;
let b = Vector::Y;
let quarter = core::f64::consts::FRAC_PI_2;
assert_relative_eq!(a.signed_angle(b, Vector::Z, T).unwrap(), quarter);
assert_relative_eq!(a.signed_angle(b, -Vector::Z, T).unwrap(), -quarter);
assert_relative_eq!(
a.signed_angle(-a, Vector::Z, T).unwrap(),
core::f64::consts::PI
);
}
#[test]
fn parallel_collinear_and_normal() {
let a = Vector::new(1.0, 2.0, 3.0);
assert!(a.is_parallel(a * 5.0, T));
assert!(!a.is_parallel(a * -5.0, T), "antiparallel is not parallel");
assert!(a.is_collinear(a * -5.0, T), "but it is collinear");
assert!(Vector::X.is_normal(Vector::Y, T));
assert!(!Vector::X.is_normal(Vector::X, T));
}
#[test]
fn projection_onto_an_axis() {
let v = Vector::new(3.0, 4.0, 5.0);
let p = v.projected_onto(Vector::X, T).unwrap();
assert_eq!(p, Vector::new(3.0, 0.0, 0.0));
assert_relative_eq!((v - p).dot(Vector::X), 0.0, epsilon = 1e-15);
assert!(v.projected_onto(Vector::ZERO, T).is_err());
}
#[test]
fn triple_product_is_the_signed_volume() {
assert_relative_eq!(Vector::X.triple(Vector::Y, Vector::Z), 1.0);
assert_relative_eq!(Vector::X.triple(Vector::Z, Vector::Y), -1.0);
assert_relative_eq!(Vector::X.triple(Vector::Y, Vector::new(1.0, 1.0, 0.0)), 0.0);
}
#[test]
fn component_access_is_bounds_checked() {
let v = Vector::new(1.0, 2.0, 3.0);
assert_eq!(v.coord(0).unwrap(), 1.0);
assert_eq!(v.coord(2).unwrap(), 3.0);
assert!(v.coord(3).is_err());
}
#[test]
fn vector2_cross_is_the_signed_area() {
assert_relative_eq!(Vector2::X.cross(Vector2::Y), 1.0);
assert_relative_eq!(Vector2::Y.cross(Vector2::X), -1.0);
assert_relative_eq!(Vector2::X.cross(Vector2::X), 0.0);
}
#[test]
fn vector2_perpendicular_is_an_exact_quarter_turn() {
let v = Vector2::new(0.1, 0.7);
let p = v.perpendicular();
assert_eq!(p, Vector2::new(-0.7, 0.1));
assert_eq!(p.dot(v), 0.0, "exactly zero, not merely small");
assert_eq!(p.perpendicular().perpendicular().perpendicular(), v);
}
#[test]
fn vector2_angle_is_signed() {
let quarter = core::f64::consts::FRAC_PI_2;
assert_relative_eq!(Vector2::X.angle(Vector2::Y, T).unwrap(), quarter);
assert_relative_eq!(Vector2::Y.angle(Vector2::X, T).unwrap(), -quarter);
}
#[test]
fn arithmetic_operators() {
let a = Vector::new(1.0, 2.0, 3.0);
let b = Vector::new(4.0, 5.0, 6.0);
assert_eq!(a + b, Vector::new(5.0, 7.0, 9.0));
assert_eq!(b - a, Vector::splat(3.0));
assert_eq!(a * 2.0, Vector::new(2.0, 4.0, 6.0));
assert_eq!(2.0 * a, a * 2.0);
assert_eq!(a / 2.0, Vector::new(0.5, 1.0, 1.5));
let mut c = a;
c += b;
c -= b;
assert_eq!(c, a);
}
#[test]
fn lerp_hits_both_endpoints() {
let a = Vector::new(1.0, 0.0, 0.0);
let b = Vector::new(3.0, 4.0, 0.0);
assert_eq!(a.lerp(b, 0.0), a);
assert_eq!(a.lerp(b, 1.0), b);
assert_eq!(a.lerp(b, 0.5), Vector::new(2.0, 2.0, 0.0));
}
}