Skip to main content

sim_lib_interference_core/
geometry.rs

1//! Finite three-dimensional points and checked directions.
2
3use crate::{InterferenceError, Metres};
4
5fn canonical_zero(value: f64) -> f64 {
6    if value == 0.0 { 0.0 } else { value }
7}
8
9/// A point in three-dimensional Cartesian space measured in metres.
10///
11/// Each coordinate is a [`Metres`], so a `Point3M` cannot contain NaN or an
12/// infinity.
13#[derive(Clone, Copy, Debug, PartialEq)]
14pub struct Point3M {
15    x: Metres,
16    y: Metres,
17    z: Metres,
18}
19
20impl Point3M {
21    /// Constructs a point from already checked coordinates.
22    pub fn new(x: Metres, y: Metres, z: Metres) -> Self {
23        Self { x, y, z }
24    }
25
26    /// Checks three raw coordinates and constructs a point.
27    pub fn from_metres(x: f64, y: f64, z: f64) -> Result<Self, InterferenceError> {
28        Ok(Self::new(Metres::new(x)?, Metres::new(y)?, Metres::new(z)?))
29    }
30
31    /// Returns the x coordinate.
32    pub fn x(self) -> Metres {
33        self.x
34    }
35
36    /// Returns the y coordinate.
37    pub fn y(self) -> Metres {
38        self.y
39    }
40
41    /// Returns the z coordinate.
42    pub fn z(self) -> Metres {
43        self.z
44    }
45
46    /// Returns the Cartesian coordinates as metres.
47    pub fn coordinates_metres(self) -> [f64; 3] {
48        [self.x.get(), self.y.get(), self.z.get()]
49    }
50
51    /// Returns the Euclidean distance to another point in metres.
52    ///
53    /// Extremely separated finite coordinates can have a distance beyond the
54    /// finite `f64` range. Propagation entry points reject that derived value.
55    pub fn distance_to(self, other: Self) -> f64 {
56        let [x, y, z] = self.coordinates_metres();
57        let [other_x, other_y, other_z] = other.coordinates_metres();
58        (x - other_x).hypot(y - other_y).hypot(z - other_z)
59    }
60}
61
62/// A finite, normalized direction in three-dimensional Cartesian space.
63///
64/// Construction accepts any finite non-zero vector and normalizes it with a
65/// scaled norm, avoiding intermediate overflow for large finite components.
66#[derive(Clone, Copy, Debug, PartialEq)]
67pub struct UnitVector3 {
68    x: f64,
69    y: f64,
70    z: f64,
71}
72
73impl UnitVector3 {
74    /// Checks and normalizes a direction.
75    pub fn new(x: f64, y: f64, z: f64) -> Result<Self, InterferenceError> {
76        if !x.is_finite() || !y.is_finite() || !z.is_finite() {
77            return Err(InterferenceError::InvalidDirection { x, y, z });
78        }
79        let scale = x.abs().max(y.abs()).max(z.abs());
80        if scale == 0.0 {
81            return Err(InterferenceError::InvalidDirection { x, y, z });
82        }
83
84        let scaled_x = x / scale;
85        let scaled_y = y / scale;
86        let scaled_z = z / scale;
87        let norm = scaled_x.hypot(scaled_y).hypot(scaled_z);
88
89        Ok(Self {
90            x: canonical_zero(scaled_x / norm),
91            y: canonical_zero(scaled_y / norm),
92            z: canonical_zero(scaled_z / norm),
93        })
94    }
95
96    /// Returns the normalized Cartesian components.
97    pub fn components(self) -> [f64; 3] {
98        [self.x, self.y, self.z]
99    }
100
101    /// Projects the displacement from `from` to `to` onto this direction.
102    ///
103    /// The result is a signed distance in metres. Propagation entry points
104    /// reject a non-finite derived value and negative forward-plane distances.
105    pub fn signed_distance_metres(self, from: Point3M, to: Point3M) -> f64 {
106        let [from_x, from_y, from_z] = from.coordinates_metres();
107        let [to_x, to_y, to_z] = to.coordinates_metres();
108        self.x * (to_x - from_x) + self.y * (to_y - from_y) + self.z * (to_z - from_z)
109    }
110}