Skip to main content

use_coordinate/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::fmt;
5use std::error::Error;
6
7/// Errors returned by validated geometry constructors and tolerance-aware helpers.
8#[derive(Debug, Clone, PartialEq)]
9pub enum GeometryError {
10    /// A geometry component must be finite.
11    ///
12    /// The type name, component name, and invalid value are included for
13    /// diagnostics.
14    NonFiniteComponent {
15        /// The type that rejected the invalid component.
16        type_name: &'static str,
17        /// The component that rejected the invalid value.
18        component: &'static str,
19        /// The invalid component value.
20        value: f64,
21    },
22    /// A circle radius cannot be negative.
23    NegativeRadius(f64),
24    /// A circle radius must be finite.
25    NonFiniteRadius(f64),
26    /// A tolerance must be finite.
27    NonFiniteTolerance(f64),
28    /// A tolerance cannot be negative.
29    NegativeTolerance(f64),
30    /// Distinct points were required but identical points were supplied.
31    IdenticalPoints,
32    /// A non-zero direction vector was required.
33    ZeroDirectionVector,
34    /// An axis-aligned bounding box corner ordering was invalid.
35    InvalidBounds {
36        /// The minimum x coordinate.
37        min_x: f64,
38        /// The minimum y coordinate.
39        min_y: f64,
40        /// The maximum x coordinate.
41        max_x: f64,
42        /// The maximum y coordinate.
43        max_y: f64,
44    },
45}
46
47impl GeometryError {
48    /// Builds a non-finite component error for a named public type and field.
49    #[must_use]
50    pub const fn non_finite_component(
51        type_name: &'static str,
52        component: &'static str,
53        value: f64,
54    ) -> Self {
55        Self::NonFiniteComponent {
56            type_name,
57            component,
58            value,
59        }
60    }
61
62    /// Validates a tolerance used by geometry APIs.
63    ///
64    /// # Errors
65    ///
66    /// Returns [`GeometryError::NonFiniteTolerance`] when `tolerance` is `NaN`
67    /// or infinite.
68    ///
69    /// Returns [`GeometryError::NegativeTolerance`] when `tolerance` is negative.
70    pub const fn validate_tolerance(tolerance: f64) -> Result<f64, Self> {
71        if !tolerance.is_finite() {
72            return Err(Self::NonFiniteTolerance(tolerance));
73        }
74
75        if tolerance < 0.0 {
76            return Err(Self::NegativeTolerance(tolerance));
77        }
78
79        Ok(tolerance)
80    }
81}
82
83impl fmt::Display for GeometryError {
84    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
85        match self {
86            Self::NonFiniteComponent {
87                type_name,
88                component,
89                value,
90            } => write!(
91                formatter,
92                "{type_name} {component} component must be finite, got {value}"
93            ),
94            Self::NegativeRadius(value) => {
95                write!(formatter, "circle radius must be non-negative, got {value}")
96            }
97            Self::NonFiniteRadius(value) => {
98                write!(formatter, "circle radius must be finite, got {value}")
99            }
100            Self::NonFiniteTolerance(value) => {
101                write!(formatter, "tolerance must be finite, got {value}")
102            }
103            Self::NegativeTolerance(value) => {
104                write!(formatter, "tolerance must be non-negative, got {value}")
105            }
106            Self::IdenticalPoints => write!(formatter, "points must be distinct"),
107            Self::ZeroDirectionVector => write!(formatter, "direction vector must be non-zero"),
108            Self::InvalidBounds {
109                min_x,
110                min_y,
111                max_x,
112                max_y,
113            } => write!(
114                formatter,
115                "aabb min must not exceed max, got min=({min_x}, {min_y}) and max=({max_x}, {max_y})"
116            ),
117        }
118    }
119}
120
121impl Error for GeometryError {}
122
123/// Axis labels for two-dimensional coordinates.
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub enum Axis2 {
126    /// Horizontal axis.
127    X,
128    /// Vertical axis.
129    Y,
130}
131
132/// Axis labels for three-dimensional coordinates.
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub enum Axis3 {
135    /// X axis.
136    X,
137    /// Y axis.
138    Y,
139    /// Z axis.
140    Z,
141}
142
143/// A raw two-dimensional coordinate pair.
144#[derive(Debug, Clone, Copy, PartialEq)]
145pub struct Coordinate2 {
146    x: f64,
147    y: f64,
148}
149
150impl Coordinate2 {
151    /// Creates a two-dimensional coordinate pair.
152    #[must_use]
153    pub const fn new(x: f64, y: f64) -> Self {
154        Self { x, y }
155    }
156
157    /// Returns the origin coordinate.
158    #[must_use]
159    pub const fn origin() -> Self {
160        Self::new(0.0, 0.0)
161    }
162
163    /// Returns the x component.
164    #[must_use]
165    pub const fn x(self) -> f64 {
166        self.x
167    }
168
169    /// Returns the y component.
170    #[must_use]
171    pub const fn y(self) -> f64 {
172        self.y
173    }
174
175    /// Returns the component selected by `axis`.
176    #[must_use]
177    pub const fn component(self, axis: Axis2) -> f64 {
178        match axis {
179            Axis2::X => self.x,
180            Axis2::Y => self.y,
181        }
182    }
183
184    /// Returns `(x, y)`.
185    #[must_use]
186    pub const fn as_tuple(self) -> (f64, f64) {
187        (self.x, self.y)
188    }
189
190    /// Returns `true` when both components are finite.
191    #[must_use]
192    pub const fn is_finite(self) -> bool {
193        self.x.is_finite() && self.y.is_finite()
194    }
195}
196
197/// A raw three-dimensional coordinate triple.
198#[derive(Debug, Clone, Copy, PartialEq)]
199pub struct Coordinate3 {
200    x: f64,
201    y: f64,
202    z: f64,
203}
204
205impl Coordinate3 {
206    /// Creates a three-dimensional coordinate triple.
207    #[must_use]
208    pub const fn new(x: f64, y: f64, z: f64) -> Self {
209        Self { x, y, z }
210    }
211
212    /// Returns the origin coordinate.
213    #[must_use]
214    pub const fn origin() -> Self {
215        Self::new(0.0, 0.0, 0.0)
216    }
217
218    /// Returns the x component.
219    #[must_use]
220    pub const fn x(self) -> f64 {
221        self.x
222    }
223
224    /// Returns the y component.
225    #[must_use]
226    pub const fn y(self) -> f64 {
227        self.y
228    }
229
230    /// Returns the z component.
231    #[must_use]
232    pub const fn z(self) -> f64 {
233        self.z
234    }
235
236    /// Returns the component selected by `axis`.
237    #[must_use]
238    pub const fn component(self, axis: Axis3) -> f64 {
239        match axis {
240            Axis3::X => self.x,
241            Axis3::Y => self.y,
242            Axis3::Z => self.z,
243        }
244    }
245
246    /// Returns `(x, y, z)`.
247    #[must_use]
248    pub const fn as_tuple(self) -> (f64, f64, f64) {
249        (self.x, self.y, self.z)
250    }
251
252    /// Returns `true` when all components are finite.
253    #[must_use]
254    pub const fn is_finite(self) -> bool {
255        self.x.is_finite() && self.y.is_finite() && self.z.is_finite()
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use super::{Axis2, Axis3, Coordinate2, Coordinate3, GeometryError};
262
263    #[test]
264    fn exposes_coordinate_components() {
265        let coordinate = Coordinate2::new(2.0, 3.0);
266
267        assert_eq!(coordinate.component(Axis2::X), 2.0);
268        assert_eq!(coordinate.component(Axis2::Y), 3.0);
269        assert_eq!(coordinate.as_tuple(), (2.0, 3.0));
270        assert!(coordinate.is_finite());
271        assert_eq!(Coordinate2::origin(), Coordinate2::new(0.0, 0.0));
272    }
273
274    #[test]
275    fn exposes_three_dimensional_components() {
276        let coordinate = Coordinate3::new(2.0, 3.0, 5.0);
277
278        assert_eq!(coordinate.component(Axis3::Z), 5.0);
279        assert_eq!(coordinate.as_tuple(), (2.0, 3.0, 5.0));
280        assert!(coordinate.is_finite());
281        assert_eq!(Coordinate3::origin(), Coordinate3::new(0.0, 0.0, 0.0));
282    }
283
284    #[test]
285    fn validates_tolerance_values() {
286        assert_eq!(GeometryError::validate_tolerance(0.25), Ok(0.25));
287        assert_eq!(
288            GeometryError::validate_tolerance(-0.25),
289            Err(GeometryError::NegativeTolerance(-0.25))
290        );
291        assert!(matches!(
292            GeometryError::validate_tolerance(f64::NAN),
293            Err(GeometryError::NonFiniteTolerance(value)) if value.is_nan()
294        ));
295    }
296}