Skip to main content

straight_skeleton/
point.rs

1//! The integer point type that forms the crate's public boundary.
2
3use crate::math::Vec2;
4
5/// A point on the integer lattice.
6///
7/// `i16` is the crate's coordinate type for **both input and output**.
8///
9/// # The coordinate cap
10///
11/// Coordinates are restricted to [`Point::MIN_COORD`]`..=`[`Point::MAX_COORD`],
12/// i.e. `-16384..=16383` — half of what `i16` could hold. [`Polygon`] rejects
13/// anything outside it.
14///
15/// That one bit is what pays for everything else. It is exactly the bit that
16/// lets the crate compute the whole skeleton in `i32` and `f32`, with no `f64`
17/// and no `i64` in the algorithm:
18///
19/// - The orientation determinant of three points needs `2 * d^2` where `d` is
20///   the largest coordinate difference. At the full `i16` range that is
21///   `8_589_672_450` — it overflows `i32` and reports the *wrong side*. Capped,
22///   it is `2_147_352_578`, which fits `i32` with 131_069 to spare, making
23///   every predicate exact. See [`crate::predicates`].
24/// - `f32` resolves `0.002` at the cap, against `0.004` at the full range,
25///   which is what leaves the simulation enough room to work in.
26///
27/// [`Polygon`]: crate::Polygon
28///
29/// # Rounding
30///
31/// A straight skeleton's interior nodes generally land on *irrational*
32/// coordinates even when every input vertex is an integer, so there is no
33/// lattice to compute *on*. The algorithm works internally in `f32` and rounds
34/// only at the boundary, so a [`Node`]'s `position` is the nearest lattice
35/// point to its true location. When you need the unrounded value, every node
36/// also carries [`Node::exact`].
37///
38/// [`Node`]: crate::Node
39/// [`Node::exact`]: crate::Node::exact
40///
41/// # Examples
42///
43/// ```
44/// use straight_skeleton::Point;
45///
46/// let p = Point::new(3, 4);
47/// assert_eq!(p.x, 3);
48/// assert_eq!(p.y, 4);
49///
50/// // Points convert from the obvious tuple and array forms.
51/// assert_eq!(Point::from((3, 4)), p);
52/// assert_eq!(Point::from([3, 4]), p);
53/// ```
54#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
55#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
56pub struct Point {
57    /// Horizontal coordinate.
58    pub x: i16,
59    /// Vertical coordinate.
60    pub y: i16,
61}
62
63impl Point {
64    /// The origin, `(0, 0)`.
65    pub const ORIGIN: Point = Point { x: 0, y: 0 };
66
67    /// The most negative coordinate a [`Polygon`] may use, `-16384`.
68    ///
69    /// See [`Point`] for why the range is capped below what `i16` could hold.
70    ///
71    /// [`Polygon`]: crate::Polygon
72    pub const MIN_COORD: i16 = -16384;
73
74    /// The largest coordinate a [`Polygon`](crate::Polygon) may use, `16383`.
75    ///
76    /// See [`Point`] for why the range is capped below what `i16` could hold.
77    pub const MAX_COORD: i16 = 16383;
78
79    /// Whether both coordinates are within
80    /// [`MIN_COORD`](Point::MIN_COORD)`..=`[`MAX_COORD`](Point::MAX_COORD).
81    ///
82    /// # Examples
83    ///
84    /// ```
85    /// use straight_skeleton::Point;
86    ///
87    /// assert!(Point::new(16383, -16384).in_range());
88    /// assert!(!Point::new(16384, 0).in_range());
89    /// assert!(!Point::new(0, i16::MIN).in_range());
90    /// ```
91    #[inline]
92    pub const fn in_range(self) -> bool {
93        self.x >= Self::MIN_COORD
94            && self.x <= Self::MAX_COORD
95            && self.y >= Self::MIN_COORD
96            && self.y <= Self::MAX_COORD
97    }
98
99    /// Constructs a point from its coordinates.
100    #[inline]
101    pub const fn new(x: i16, y: i16) -> Self {
102        Point { x, y }
103    }
104
105    /// Widens to the internal `f32` working space.
106    ///
107    /// Exact: `i16` needs 16 bits and `f32`'s mantissa holds 24.
108    #[inline]
109    pub(crate) fn to_vec2(self) -> Vec2 {
110        Vec2::new(self.x as f32, self.y as f32)
111    }
112
113    /// Rounds an internal `f32` position back to the lattice, saturating at the
114    /// `i16` bounds rather than wrapping.
115    ///
116    /// Saturation is a safety net, not an expected path: a straight skeleton
117    /// lies inside the convex hull of its input, so a node can only exceed the
118    /// input's range through floating-point error at the very edge of the
119    /// coordinate space.
120    #[inline]
121    pub(crate) fn from_vec2_rounded(v: Vec2) -> Self {
122        Point {
123            x: round_to_i16(v.x),
124            y: round_to_i16(v.y),
125        }
126    }
127
128    /// Squared distance to `other`, computed exactly in `i32`.
129    ///
130    /// Exact for every in-range pair: the largest possible value is
131    /// `2 * 32767^2 = 2_147_352_578`, which fits comfortably.
132    ///
133    /// Points outside the [coordinate cap](Point::MAX_COORD) saturate rather
134    /// than wrap, which keeps comparisons monotone; a [`Polygon`] cannot
135    /// contain such a point anyway.
136    ///
137    /// [`Polygon`]: crate::Polygon
138    ///
139    /// # Examples
140    ///
141    /// ```
142    /// use straight_skeleton::Point;
143    ///
144    /// assert_eq!(Point::new(0, 0).distance_squared(Point::new(3, 4)), 25);
145    /// ```
146    #[inline]
147    pub fn distance_squared(self, other: Point) -> u32 {
148        let dx = (self.x as i32 - other.x as i32).unsigned_abs();
149        let dy = (self.y as i32 - other.y as i32).unsigned_abs();
150        dx.saturating_mul(dx).saturating_add(dy.saturating_mul(dy))
151    }
152}
153
154/// Rounds half-away-from-zero and saturates into `i16`.
155#[inline]
156fn round_to_i16(v: f32) -> i16 {
157    if v.is_nan() {
158        return 0;
159    }
160    let r = round_half_away_from_zero(v);
161    if r <= i16::MIN as f32 {
162        i16::MIN
163    } else if r >= i16::MAX as f32 {
164        i16::MAX
165    } else {
166        r as i16
167    }
168}
169
170/// `f32::round` is unavailable in `no_std`, so we spell it out.
171#[inline]
172pub(crate) fn round_half_away_from_zero(v: f32) -> f32 {
173    // `as i32` truncates toward zero; nudging by 0.5 in the sign direction
174    // turns that into round-half-away-from-zero for the magnitudes we see.
175    if v >= 0.0 {
176        let t = (v + 0.5) as i32 as f32;
177        // Guard the exact-half-below case introduced by the nudge.
178        if t - v > 0.5 {
179            t - 1.0
180        } else {
181            t
182        }
183    } else {
184        let t = (v - 0.5) as i32 as f32;
185        if v - t > 0.5 {
186            t + 1.0
187        } else {
188            t
189        }
190    }
191}
192
193impl From<(i16, i16)> for Point {
194    #[inline]
195    fn from((x, y): (i16, i16)) -> Self {
196        Point::new(x, y)
197    }
198}
199
200impl From<[i16; 2]> for Point {
201    #[inline]
202    fn from([x, y]: [i16; 2]) -> Self {
203        Point::new(x, y)
204    }
205}
206
207impl From<Point> for (i16, i16) {
208    #[inline]
209    fn from(p: Point) -> Self {
210        (p.x, p.y)
211    }
212}
213
214impl From<Point> for [i16; 2] {
215    #[inline]
216    fn from(p: Point) -> Self {
217        [p.x, p.y]
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    #[test]
226    fn rounds_to_nearest() {
227        assert_eq!(round_to_i16(0.4), 0);
228        assert_eq!(round_to_i16(0.5), 1);
229        assert_eq!(round_to_i16(0.6), 1);
230        assert_eq!(round_to_i16(-0.4), 0);
231        assert_eq!(round_to_i16(-0.5), -1);
232        assert_eq!(round_to_i16(-0.6), -1);
233        assert_eq!(round_to_i16(1.5), 2);
234        assert_eq!(round_to_i16(2.5), 3);
235        assert_eq!(round_to_i16(-2.5), -3);
236    }
237
238    #[test]
239    fn rounding_saturates_instead_of_wrapping() {
240        assert_eq!(round_to_i16(1e9), i16::MAX);
241        assert_eq!(round_to_i16(-1e9), i16::MIN);
242        assert_eq!(round_to_i16(f32::INFINITY), i16::MAX);
243        assert_eq!(round_to_i16(f32::NEG_INFINITY), i16::MIN);
244        assert_eq!(round_to_i16(f32::NAN), 0);
245        assert_eq!(round_to_i16(32767.4), i16::MAX);
246        assert_eq!(round_to_i16(-32768.4), i16::MIN);
247    }
248
249    #[test]
250    fn coordinate_cap_is_where_the_arithmetic_says() {
251        // One bit below i16, which is what makes i32 predicates exact.
252        assert_eq!(Point::MIN_COORD, -16384);
253        assert_eq!(Point::MAX_COORD, 16383);
254
255        assert!(Point::new(0, 0).in_range());
256        assert!(Point::new(16383, -16384).in_range());
257        assert!(!Point::new(16384, 0).in_range());
258        assert!(!Point::new(0, -16385).in_range());
259        assert!(!Point::new(i16::MAX, i16::MIN).in_range());
260    }
261
262    #[test]
263    fn distance_squared_is_exact_across_the_whole_capped_range() {
264        let d = Point::new(Point::MIN_COORD, Point::MIN_COORD)
265            .distance_squared(Point::new(Point::MAX_COORD, Point::MAX_COORD));
266        // 2 * 32767^2, exact rather than saturated.
267        assert_eq!(d, 2 * 32767 * 32767);
268    }
269
270    #[test]
271    fn distance_squared_is_exact_in_range() {
272        assert_eq!(Point::new(0, 0).distance_squared(Point::new(3, 4)), 25);
273        assert_eq!(Point::new(-3, -4).distance_squared(Point::new(0, 0)), 25);
274        assert_eq!(Point::new(5, 5).distance_squared(Point::new(5, 5)), 0);
275    }
276
277    #[test]
278    fn distance_squared_saturates_beyond_the_cap_rather_than_wrapping() {
279        // The full i16 diagonal needs 2 * 65535^2 = 8_589_672_450, which does
280        // not fit u32. Such points cannot reach a Polygon, but must saturate
281        // rather than wrap if handed here directly.
282        let d = Point::new(i16::MIN, i16::MIN).distance_squared(Point::new(i16::MAX, i16::MAX));
283        assert_eq!(d, u32::MAX);
284        assert!(2u64 * 65535 * 65535 > u32::MAX as u64);
285    }
286
287    #[test]
288    fn conversions_round_trip() {
289        let p = Point::new(-7, 12);
290        assert_eq!(Point::from(<(i16, i16)>::from(p)), p);
291        assert_eq!(Point::from(<[i16; 2]>::from(p)), p);
292    }
293
294    #[test]
295    fn to_vec2_is_lossless() {
296        // i16 needs 16 bits; f32's mantissa holds 24, so this cannot round.
297        for v in [i16::MIN, -1, 0, 1, i16::MAX] {
298            let p = Point::new(v, v);
299            assert_eq!(Point::from_vec2_rounded(p.to_vec2()), p);
300        }
301    }
302}