Skip to main content

straight_skeleton/
interop.rs

1//! Optional conversions to and from other ecosystem crates.
2//!
3//! Every integration here is opt-in and off by default, so the crate keeps its
4//! zero required dependencies. Each is gated behind a feature named after the
5//! crate it bridges to.
6//!
7//! # Conversions are lossy in one direction only
8//!
9//! Going *out* of this crate — `Point -> anything` — is always exact: an `i16`
10//! fits in every target type without rounding.
11//!
12//! Coming *in* is not, because a point on some other crate's `f32`/`f64` plane
13//! need not land on the `i16` lattice at all. So inbound conversions from
14//! floating-point types are [`TryFrom`], and they fail rather than silently
15//! round or wrap. Integer inbound conversions are infallible where the width
16//! allows, and `TryFrom` where it does not.
17
18use crate::Point;
19
20/// Why a coordinate could not be brought onto the `i16` lattice.
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22#[non_exhaustive]
23pub enum CoordError {
24    /// The value lies outside `i16::MIN..=i16::MAX`.
25    OutOfRange,
26    /// The value is not an integer, so it is not on the lattice.
27    NotAnInteger,
28    /// The value is NaN or infinite.
29    NotFinite,
30}
31
32impl core::fmt::Display for CoordError {
33    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
34        match self {
35            CoordError::OutOfRange => write!(f, "coordinate is outside the i16 range"),
36            CoordError::NotAnInteger => write!(f, "coordinate is not an integer"),
37            CoordError::NotFinite => write!(f, "coordinate is not finite"),
38        }
39    }
40}
41
42#[cfg(feature = "std")]
43impl std::error::Error for CoordError {}
44
45/// Narrows one floating-point coordinate onto the lattice, exactly or not at
46/// all.
47fn lattice(v: f64) -> Result<i16, CoordError> {
48    if !v.is_finite() {
49        return Err(CoordError::NotFinite);
50    }
51    // `as i64` truncates, so comparing back catches any fractional part.
52    let t = v as i64;
53    if t as f64 != v {
54        return Err(CoordError::NotAnInteger);
55    }
56    if t < i16::MIN as i64 || t > i16::MAX as i64 {
57        return Err(CoordError::OutOfRange);
58    }
59    Ok(t as i16)
60}
61
62// --- geo-types --------------------------------------------------------------
63
64#[cfg(feature = "geo-types")]
65#[cfg_attr(docsrs, doc(cfg(feature = "geo-types")))]
66mod geo_types_impl {
67    use super::*;
68
69    impl From<Point> for geo_types::Coord<i16> {
70        #[inline]
71        fn from(p: Point) -> Self {
72            geo_types::Coord { x: p.x, y: p.y }
73        }
74    }
75
76    impl From<geo_types::Coord<i16>> for Point {
77        #[inline]
78        fn from(c: geo_types::Coord<i16>) -> Self {
79            Point::new(c.x, c.y)
80        }
81    }
82
83    impl From<Point> for geo_types::Point<i16> {
84        #[inline]
85        fn from(p: Point) -> Self {
86            geo_types::Point::new(p.x, p.y)
87        }
88    }
89
90    impl From<geo_types::Point<i16>> for Point {
91        #[inline]
92        fn from(p: geo_types::Point<i16>) -> Self {
93            Point::new(p.x(), p.y())
94        }
95    }
96
97    /// Widening to `f64` is exact, which is what makes this `From` rather than
98    /// `TryFrom`: `f64` has 53 mantissa bits and `i16` needs 16.
99    impl From<Point> for geo_types::Coord<f64> {
100        #[inline]
101        fn from(p: Point) -> Self {
102            geo_types::Coord {
103                x: p.x as f64,
104                y: p.y as f64,
105            }
106        }
107    }
108
109    impl TryFrom<geo_types::Coord<f64>> for Point {
110        type Error = CoordError;
111        #[inline]
112        fn try_from(c: geo_types::Coord<f64>) -> Result<Self, CoordError> {
113            Ok(Point::new(lattice(c.x)?, lattice(c.y)?))
114        }
115    }
116}
117
118// --- glam -------------------------------------------------------------------
119
120#[cfg(feature = "glam")]
121#[cfg_attr(docsrs, doc(cfg(feature = "glam")))]
122mod glam_impl {
123    use super::*;
124
125    impl From<Point> for glam::I16Vec2 {
126        #[inline]
127        fn from(p: Point) -> Self {
128            glam::I16Vec2::new(p.x, p.y)
129        }
130    }
131
132    impl From<glam::I16Vec2> for Point {
133        #[inline]
134        fn from(v: glam::I16Vec2) -> Self {
135            Point::new(v.x, v.y)
136        }
137    }
138
139    impl From<Point> for glam::IVec2 {
140        #[inline]
141        fn from(p: Point) -> Self {
142            glam::IVec2::new(p.x as i32, p.y as i32)
143        }
144    }
145
146    impl TryFrom<glam::IVec2> for Point {
147        type Error = CoordError;
148        #[inline]
149        fn try_from(v: glam::IVec2) -> Result<Self, CoordError> {
150            let x = i16::try_from(v.x).map_err(|_| CoordError::OutOfRange)?;
151            let y = i16::try_from(v.y).map_err(|_| CoordError::OutOfRange)?;
152            Ok(Point::new(x, y))
153        }
154    }
155
156    /// `i16 -> f32` is exact: 16 bits fits comfortably in a 24-bit mantissa.
157    impl From<Point> for glam::Vec2 {
158        #[inline]
159        fn from(p: Point) -> Self {
160            glam::Vec2::new(p.x as f32, p.y as f32)
161        }
162    }
163
164    impl TryFrom<glam::Vec2> for Point {
165        type Error = CoordError;
166        #[inline]
167        fn try_from(v: glam::Vec2) -> Result<Self, CoordError> {
168            Ok(Point::new(lattice(v.x as f64)?, lattice(v.y as f64)?))
169        }
170    }
171}
172
173// --- mint -------------------------------------------------------------------
174
175#[cfg(feature = "mint")]
176#[cfg_attr(docsrs, doc(cfg(feature = "mint")))]
177mod mint_impl {
178    use super::*;
179
180    impl From<Point> for mint::Point2<i16> {
181        #[inline]
182        fn from(p: Point) -> Self {
183            mint::Point2 { x: p.x, y: p.y }
184        }
185    }
186
187    impl From<mint::Point2<i16>> for Point {
188        #[inline]
189        fn from(p: mint::Point2<i16>) -> Self {
190            Point::new(p.x, p.y)
191        }
192    }
193
194    impl From<Point> for mint::Vector2<i16> {
195        #[inline]
196        fn from(p: Point) -> Self {
197            mint::Vector2 { x: p.x, y: p.y }
198        }
199    }
200
201    impl From<mint::Vector2<i16>> for Point {
202        #[inline]
203        fn from(v: mint::Vector2<i16>) -> Self {
204            Point::new(v.x, v.y)
205        }
206    }
207
208    impl From<Point> for mint::Point2<f32> {
209        #[inline]
210        fn from(p: Point) -> Self {
211            mint::Point2 {
212                x: p.x as f32,
213                y: p.y as f32,
214            }
215        }
216    }
217
218    impl TryFrom<mint::Point2<f32>> for Point {
219        type Error = CoordError;
220        #[inline]
221        fn try_from(p: mint::Point2<f32>) -> Result<Self, CoordError> {
222            Ok(Point::new(lattice(p.x as f64)?, lattice(p.y as f64)?))
223        }
224    }
225}
226
227// --- num-traits -------------------------------------------------------------
228
229#[cfg(feature = "num-traits")]
230#[cfg_attr(docsrs, doc(cfg(feature = "num-traits")))]
231mod num_traits_impl {
232    use super::*;
233    use num_traits::{NumCast, ToPrimitive};
234
235    impl Point {
236        /// Converts to any numeric type `num-traits` can cast to.
237        ///
238        /// Always succeeds for the usual targets, since every `i16` fits.
239        ///
240        /// # Examples
241        ///
242        /// ```
243        /// use straight_skeleton::Point;
244        ///
245        /// let p = Point::new(3, -4);
246        /// assert_eq!(p.cast::<f32>(), Some((3.0, -4.0)));
247        /// assert_eq!(p.cast::<i64>(), Some((3, -4)));
248        /// // -4 has no unsigned representation.
249        /// assert_eq!(p.cast::<u8>(), None);
250        /// ```
251        pub fn cast<T: NumCast>(self) -> Option<(T, T)> {
252            Some((T::from(self.x)?, T::from(self.y)?))
253        }
254
255        /// Builds a point from any numeric type, if both coordinates land
256        /// **exactly** on the `i16` lattice.
257        ///
258        /// Returns `None` rather than rounding. Note this is stricter than
259        /// `num_traits`' own `to_i16`, which truncates `0.5` to `0`; a silently
260        /// moved vertex is the last thing a geometry crate should hand you.
261        ///
262        /// # Examples
263        ///
264        /// ```
265        /// use straight_skeleton::Point;
266        ///
267        /// assert_eq!(Point::try_cast(3.0f64, -4.0f64), Some(Point::new(3, -4)));
268        /// assert_eq!(Point::try_cast(1e9f64, 0.0f64), None);   // out of range
269        /// assert_eq!(Point::try_cast(0.5f64, 0.0f64), None);   // off the lattice
270        /// assert_eq!(Point::try_cast(3i64, -4i64), Some(Point::new(3, -4)));
271        /// ```
272        pub fn try_cast<T: ToPrimitive>(x: T, y: T) -> Option<Point> {
273            // Via f64 rather than `to_i16`, so that `lattice` can reject a
274            // fractional value instead of truncating it. Any input too large
275            // for f64 to hold exactly is far outside i16 anyway, so it is
276            // rejected as out of range regardless.
277            Some(Point::new(
278                lattice(x.to_f64()?).ok()?,
279                lattice(y.to_f64()?).ok()?,
280            ))
281        }
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    #[test]
290    fn lattice_accepts_exact_integers() {
291        assert_eq!(lattice(0.0), Ok(0));
292        assert_eq!(lattice(-32768.0), Ok(i16::MIN));
293        assert_eq!(lattice(32767.0), Ok(i16::MAX));
294        assert_eq!(lattice(-0.0), Ok(0));
295    }
296
297    #[test]
298    fn lattice_rejects_rather_than_rounds() {
299        assert_eq!(lattice(0.5), Err(CoordError::NotAnInteger));
300        assert_eq!(lattice(-1.25), Err(CoordError::NotAnInteger));
301    }
302
303    #[test]
304    fn lattice_rejects_rather_than_wraps() {
305        assert_eq!(lattice(32768.0), Err(CoordError::OutOfRange));
306        assert_eq!(lattice(-32769.0), Err(CoordError::OutOfRange));
307        assert_eq!(lattice(1e18), Err(CoordError::OutOfRange));
308    }
309
310    #[test]
311    fn lattice_rejects_nonfinite() {
312        assert_eq!(lattice(f64::NAN), Err(CoordError::NotFinite));
313        assert_eq!(lattice(f64::INFINITY), Err(CoordError::NotFinite));
314        assert_eq!(lattice(f64::NEG_INFINITY), Err(CoordError::NotFinite));
315    }
316
317    #[cfg(feature = "geo-types")]
318    #[test]
319    fn geo_types_round_trips() {
320        let p = Point::new(-7, 12);
321        let c: geo_types::Coord<i16> = p.into();
322        assert_eq!(Point::from(c), p);
323
324        let gp: geo_types::Point<i16> = p.into();
325        assert_eq!(Point::from(gp), p);
326
327        let f: geo_types::Coord<f64> = p.into();
328        assert_eq!(Point::try_from(f), Ok(p));
329
330        assert_eq!(
331            Point::try_from(geo_types::Coord { x: 0.5f64, y: 0.0 }),
332            Err(CoordError::NotAnInteger)
333        );
334    }
335
336    #[cfg(feature = "glam")]
337    #[test]
338    fn glam_round_trips() {
339        let p = Point::new(-7, 12);
340        assert_eq!(Point::from(glam::I16Vec2::from(p)), p);
341        assert_eq!(Point::try_from(glam::IVec2::from(p)), Ok(p));
342        assert_eq!(Point::try_from(glam::Vec2::from(p)), Ok(p));
343
344        assert_eq!(
345            Point::try_from(glam::IVec2::new(70_000, 0)),
346            Err(CoordError::OutOfRange)
347        );
348        assert_eq!(
349            Point::try_from(glam::Vec2::new(0.5, 0.0)),
350            Err(CoordError::NotAnInteger)
351        );
352    }
353
354    #[cfg(feature = "mint")]
355    #[test]
356    fn mint_round_trips() {
357        let p = Point::new(-7, 12);
358        assert_eq!(Point::from(mint::Point2::from(p)), p);
359        assert_eq!(Point::from(mint::Vector2::from(p)), p);
360        let f: mint::Point2<f32> = p.into();
361        assert_eq!(Point::try_from(f), Ok(p));
362    }
363
364    #[cfg(feature = "num-traits")]
365    #[test]
366    fn num_traits_casts() {
367        let p = Point::new(3, -4);
368        assert_eq!(p.cast::<f64>(), Some((3.0, -4.0)));
369        assert_eq!(p.cast::<i32>(), Some((3, -4)));
370        assert_eq!(p.cast::<u16>(), None, "-4 is not representable");
371        assert_eq!(Point::try_cast(3i32, -4i32), Some(p));
372        assert_eq!(Point::try_cast(99_999i32, 0i32), None);
373        // Stricter than num-traits' own to_i16, which would truncate to 0.
374        assert_eq!(Point::try_cast(0.5f64, 0.0f64), None);
375        assert_eq!(Point::try_cast(-0.5f32, 0.0f32), None);
376        assert_eq!(Point::try_cast(f64::NAN, 0.0), None);
377    }
378}