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}