use core::ops::{Add, AddAssign, Sub, SubAssign};
use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
use crate::{Vector, Vector2};
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Point {
pub x: f64,
pub y: f64,
pub z: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Point2 {
pub x: f64,
pub y: f64,
}
impl Point {
pub const ORIGIN: Self = Self::new(0.0, 0.0, 0.0);
#[must_use]
pub const fn new(x: f64, y: f64, z: f64) -> Self {
Self { x, y, z }
}
#[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)
}
#[must_use]
pub const fn to_vector(self) -> Vector {
Vector::new(self.x, self.y, self.z)
}
#[must_use]
pub const fn from_vector(v: Vector) -> Self {
Self::new(v.x, v.y, v.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, "point coordinate {index} of 3"),
}
}
#[must_use]
pub fn square_distance(self, other: Self) -> f64 {
(other - self).square_magnitude()
}
#[must_use]
pub fn distance(self, other: Self) -> f64 {
self.square_distance(other).sqrt()
}
#[must_use]
pub fn is_equal(self, other: Self, tol: Tolerances) -> bool {
self.square_distance(other) <= tol.confusion() * tol.confusion()
}
#[must_use]
pub fn is_within(self, other: Self, distance: f64) -> bool {
self.square_distance(other) <= distance * distance
}
#[must_use]
pub fn is_finite(self) -> bool {
self.x.is_finite() && self.y.is_finite() && self.z.is_finite()
}
#[must_use]
pub fn midpoint(self, other: Self) -> Self {
Self::new(
f64::midpoint(self.x, other.x),
f64::midpoint(self.y, other.y),
f64::midpoint(self.z, other.z),
)
}
#[must_use]
pub fn lerp(self, other: Self, t: f64) -> Self {
self + (other - self) * t
}
pub fn centroid(points: &[Self]) -> OgeomResult<Self> {
let Some((first, rest)) = points.split_first() else {
ogeom_bail!(Construction, "centroid of an empty point set");
};
let mut sum = Vector::ZERO;
for p in rest {
sum += *p - *first;
}
#[allow(clippy::cast_precision_loss)]
Ok(*first + sum / points.len() as f64)
}
#[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) -> Point2 {
Point2::new(self.x, self.y)
}
}
impl Point2 {
pub const ORIGIN: Self = Self::new(0.0, 0.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 const fn to_vector(self) -> Vector2 {
Vector2::new(self.x, self.y)
}
#[must_use]
pub const fn from_vector(v: Vector2) -> Self {
Self::new(v.x, v.y)
}
#[must_use]
pub fn square_distance(self, other: Self) -> f64 {
(other - self).square_magnitude()
}
#[must_use]
pub fn distance(self, other: Self) -> f64 {
self.square_distance(other).sqrt()
}
#[must_use]
pub fn is_equal(self, other: Self, tol: Tolerances) -> bool {
self.square_distance(other) <= tol.confusion() * tol.confusion()
}
#[must_use]
pub fn is_finite(self) -> bool {
self.x.is_finite() && self.y.is_finite()
}
#[must_use]
pub fn midpoint(self, other: Self) -> Self {
Self::new(
f64::midpoint(self.x, other.x),
f64::midpoint(self.y, other.y),
)
}
#[must_use]
pub fn lerp(self, other: Self, t: f64) -> Self {
self + (other - self) * t
}
#[must_use]
pub const fn to_3d(self) -> Point {
Point::new(self.x, self.y, 0.0)
}
}
impl Add<Vector> for Point {
type Output = Self;
fn add(self, v: Vector) -> Self {
Self::new(self.x + v.x, self.y + v.y, self.z + v.z)
}
}
impl Sub<Vector> for Point {
type Output = Self;
fn sub(self, v: Vector) -> Self {
Self::new(self.x - v.x, self.y - v.y, self.z - v.z)
}
}
impl Sub for Point {
type Output = Vector;
fn sub(self, other: Self) -> Vector {
Vector::new(self.x - other.x, self.y - other.y, self.z - other.z)
}
}
impl AddAssign<Vector> for Point {
fn add_assign(&mut self, v: Vector) {
*self = *self + v;
}
}
impl SubAssign<Vector> for Point {
fn sub_assign(&mut self, v: Vector) {
*self = *self - v;
}
}
impl Add<Vector2> for Point2 {
type Output = Self;
fn add(self, v: Vector2) -> Self {
Self::new(self.x + v.x, self.y + v.y)
}
}
impl Sub<Vector2> for Point2 {
type Output = Self;
fn sub(self, v: Vector2) -> Self {
Self::new(self.x - v.x, self.y - v.y)
}
}
impl Sub for Point2 {
type Output = Vector2;
fn sub(self, other: Self) -> Vector2 {
Vector2::new(self.x - other.x, self.y - other.y)
}
}
impl AddAssign<Vector2> for Point2 {
fn add_assign(&mut self, v: Vector2) {
*self = *self + v;
}
}
impl SubAssign<Vector2> for Point2 {
fn sub_assign(&mut self, v: Vector2) {
*self = *self - v;
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use approx::assert_relative_eq;
const T: Tolerances = Tolerances::millimetres();
#[test]
fn affine_algebra() {
let a = Point::new(1.0, 2.0, 3.0);
let b = Point::new(4.0, 6.0, 3.0);
let d: Vector = b - a;
assert_eq!(d, Vector::new(3.0, 4.0, 0.0));
assert_eq!(a + d, b);
assert_eq!(b - d, a);
assert_relative_eq!(a.distance(b), 5.0);
assert_relative_eq!(a.square_distance(b), 25.0);
}
#[test]
fn midpoint_does_not_overflow_for_extreme_coordinates() {
let a = Point::new(f64::MAX, 0.0, 0.0);
let b = Point::new(f64::MAX, 0.0, 0.0);
assert!(a.midpoint(b).is_finite());
assert_eq!(a.midpoint(b).x, f64::MAX);
}
#[test]
fn midpoint_and_lerp_agree() {
let a = Point::new(-1.0, 5.0, 2.0);
let b = Point::new(3.0, 1.0, -4.0);
assert_eq!(a.midpoint(b), a.lerp(b, 0.5));
assert_eq!(a.lerp(b, 0.0), a);
assert_eq!(a.lerp(b, 1.0), b);
}
#[test]
fn centroid_keeps_precision_far_from_the_origin() {
let base = 1.0e9;
let pts = [
Point::new(base, base, base),
Point::new(base + 2.0, base, base),
Point::new(base + 1.0, base + 3.0, base),
];
let c = Point::centroid(&pts).unwrap();
assert_relative_eq!(c.x, base + 1.0, epsilon = 1e-9);
assert_relative_eq!(c.y, base + 1.0, epsilon = 1e-9);
assert_relative_eq!(c.z, base, epsilon = 1e-9);
}
#[test]
fn centroid_of_nothing_is_an_error_not_the_origin() {
assert!(Point::centroid(&[]).is_err());
let single = Point::new(1.0, 2.0, 3.0);
assert_eq!(Point::centroid(&[single]).unwrap(), single);
}
#[test]
fn equality_uses_tolerance() {
let a = Point::new(1.0, 1.0, 1.0);
let near = Point::new(1.0 + 1e-9, 1.0, 1.0);
let far = Point::new(1.0 + 1e-3, 1.0, 1.0);
assert!(a.is_equal(near, T));
assert!(!a.is_equal(far, T));
assert!(a.is_within(far, 1e-2));
}
#[test]
fn coordinate_access_is_bounds_checked() {
let p = Point::new(7.0, 8.0, 9.0);
assert_eq!(p.coord(1).unwrap(), 8.0);
assert!(p.coord(3).is_err());
}
#[test]
fn dimension_round_trip() {
let p = Point::new(1.0, 2.0, 3.0);
assert_eq!(p.xy(), Point2::new(1.0, 2.0));
assert_eq!(p.xy().to_3d(), Point::new(1.0, 2.0, 0.0));
}
#[test]
fn point2_affine_algebra() {
let a = Point2::new(1.0, 2.0);
let b = Point2::new(4.0, 6.0);
assert_eq!(b - a, Vector2::new(3.0, 4.0));
assert_relative_eq!(a.distance(b), 5.0);
assert_eq!(a + (b - a), b);
}
}