1use serde::{Deserialize, Serialize};
8
9use crate::values::PinSide;
10
11pub const GRID_LIMIT: i32 = 1 << 20;
19
20#[derive(Debug, thiserror::Error, PartialEq, Eq)]
21pub enum OutOfExtent {
22 #[error("coordinate {0} is outside the document extent (±{GRID_LIMIT})")]
23 Coordinate(i32),
24 #[error("extent {0} exceeds the document extent ({GRID_LIMIT})")]
25 Size(u32),
26 #[error("pin slot {0} is outside the document extent ({GRID_LIMIT})")]
27 Slot(u32),
28 #[error("fixed-point value {0} is outside the screen range (±{limit})", limit = FracVal::LIMIT)]
29 Frac(i64),
30}
31
32#[derive(Deserialize)]
35struct RawGridPoint {
36 #[serde(default)]
37 x: i32,
38 #[serde(default)]
39 y: i32,
40}
41
42#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
43#[serde(try_from = "RawGridPoint")]
44pub struct GridPoint {
45 #[serde(default, skip_serializing_if = "crate::entity::is_default")]
46 pub x: i32,
47 #[serde(default, skip_serializing_if = "crate::entity::is_default")]
48 pub y: i32,
49}
50
51impl TryFrom<RawGridPoint> for GridPoint {
52 type Error = OutOfExtent;
53 fn try_from(raw: RawGridPoint) -> Result<Self, Self::Error> {
54 for coordinate in [raw.x, raw.y] {
55 if !(-GRID_LIMIT..=GRID_LIMIT).contains(&coordinate) {
56 return Err(OutOfExtent::Coordinate(coordinate));
57 }
58 }
59 Ok(Self { x: raw.x, y: raw.y })
60 }
61}
62
63#[derive(Deserialize)]
64struct RawGridSize {
65 #[serde(default)]
66 w: u32,
67 #[serde(default)]
68 h: u32,
69}
70
71#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
72#[serde(try_from = "RawGridSize")]
73pub struct GridSize {
74 #[serde(default, skip_serializing_if = "crate::entity::is_default")]
75 pub w: u32,
76 #[serde(default, skip_serializing_if = "crate::entity::is_default")]
77 pub h: u32,
78}
79
80impl TryFrom<RawGridSize> for GridSize {
81 type Error = OutOfExtent;
82 fn try_from(raw: RawGridSize) -> Result<Self, Self::Error> {
83 for extent in [raw.w, raw.h] {
84 if extent > GRID_LIMIT.unsigned_abs() {
85 return Err(OutOfExtent::Size(extent));
86 }
87 }
88 Ok(Self { w: raw.w, h: raw.h })
89 }
90}
91
92#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)]
95pub struct GridVec {
96 pub dx: i32,
97 pub dy: i32,
98}
99
100impl GridVec {
101 pub const ZERO: GridVec = GridVec { dx: 0, dy: 0 };
102
103 pub const fn new(dx: i32, dy: i32) -> Self {
104 Self { dx, dy }
105 }
106}
107
108impl std::ops::Add<GridVec> for GridPoint {
109 type Output = GridPoint;
110 fn add(self, rhs: GridVec) -> GridPoint {
111 GridPoint {
112 x: self.x + rhs.dx,
113 y: self.y + rhs.dy,
114 }
115 }
116}
117
118impl std::ops::Sub<GridPoint> for GridPoint {
119 type Output = GridVec;
120 fn sub(self, rhs: GridPoint) -> GridVec {
121 GridVec::new(self.x - rhs.x, self.y - rhs.y)
122 }
123}
124
125#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
127pub struct GridRect {
128 #[serde(default, skip_serializing_if = "crate::entity::is_default")]
129 pub top_left: GridPoint,
130 #[serde(default, skip_serializing_if = "crate::entity::is_default")]
131 pub size: GridSize,
132}
133
134impl GridRect {
139 pub fn from_two_pos(a: GridPoint, b: GridPoint) -> Self {
140 let min_x = a.x.min(b.x);
141 let min_y = a.y.min(b.y);
142 Self {
143 top_left: GridPoint { x: min_x, y: min_y },
144 size: GridSize {
145 w: a.x.abs_diff(b.x),
146 h: a.y.abs_diff(b.y),
147 },
148 }
149 }
150
151 pub fn max(self) -> GridPoint {
152 GridPoint {
153 x: self.right(),
154 y: self.bottom(),
155 }
156 }
157
158 pub fn left(self) -> i32 {
159 self.top_left.x
160 }
161
162 pub fn top(self) -> i32 {
163 self.top_left.y
164 }
165
166 pub fn right(self) -> i32 {
167 self.top_left.x + self.size.w as i32
168 }
169
170 pub fn bottom(self) -> i32 {
171 self.top_left.y + self.size.h as i32
172 }
173
174 pub fn contains(self, p: GridPoint) -> bool {
175 p.x >= self.left() && p.x <= self.right() && p.y >= self.top() && p.y <= self.bottom()
176 }
177
178 pub fn intersects(self, other: GridRect) -> bool {
179 self.left() < other.right()
180 && other.left() < self.right()
181 && self.top() < other.bottom()
182 && other.top() < self.bottom()
183 }
184
185 #[must_use]
188 pub fn intersection(self, other: GridRect) -> Option<Self> {
189 if !self.intersects(other) {
190 return None;
191 }
192 let left = self.left().max(other.left());
193 let top = self.top().max(other.top());
194 let right = self.right().min(other.right());
195 let bottom = self.bottom().min(other.bottom());
196 Some(Self {
197 top_left: GridPoint { x: left, y: top },
198 size: GridSize {
199 w: (right - left).unsigned_abs(),
200 h: (bottom - top).unsigned_abs(),
201 },
202 })
203 }
204
205 #[must_use]
206 pub fn translate(self, delta: GridVec) -> Self {
207 Self {
208 top_left: self.top_left + delta,
209 size: self.size,
210 }
211 }
212}
213
214#[derive(Deserialize)]
215struct RawPinSlot {
216 #[serde(default)]
217 side: PinSide,
218 #[serde(default)]
219 offset: u32,
220}
221
222#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
227#[serde(try_from = "RawPinSlot")]
228pub struct PinSlot {
229 #[serde(default, skip_serializing_if = "crate::entity::is_default")]
230 pub side: PinSide,
231 #[serde(default, skip_serializing_if = "crate::entity::is_default")]
232 pub offset: u32,
233}
234
235impl TryFrom<RawPinSlot> for PinSlot {
236 type Error = OutOfExtent;
237 fn try_from(raw: RawPinSlot) -> Result<Self, Self::Error> {
238 if raw.offset > GRID_LIMIT.unsigned_abs() {
239 return Err(OutOfExtent::Slot(raw.offset));
240 }
241 Ok(Self {
242 side: raw.side,
243 offset: raw.offset,
244 })
245 }
246}
247
248#[derive(Clone, Copy, PartialEq, Eq, Default, Debug, Serialize, Deserialize)]
251pub struct Waypoint {
252 #[serde(default, skip_serializing_if = "crate::entity::is_default")]
253 pub pos: GridPoint,
254 #[serde(default, skip_serializing_if = "crate::entity::is_default")]
255 pub locked: bool,
256}
257
258#[derive(
263 Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
264)]
265#[serde(try_from = "i64")]
266pub struct FracVal(i64);
267
268impl FracVal {
269 pub const LIMIT: i64 = 1 << 48;
272}
273
274impl TryFrom<i64> for FracVal {
275 type Error = OutOfExtent;
276 fn try_from(raw: i64) -> Result<Self, Self::Error> {
277 if (-Self::LIMIT..=Self::LIMIT).contains(&raw) {
278 Ok(Self(raw))
279 } else {
280 Err(OutOfExtent::Frac(raw))
281 }
282 }
283}
284
285impl From<f32> for FracVal {
286 fn from(val: f32) -> FracVal {
287 debug_assert!(!val.is_nan(), "NaN must not enter the document");
288 Self((val as f64 * 2.0f64.powi(24)) as i64)
289 }
290}
291impl From<FracVal> for f32 {
292 fn from(val: FracVal) -> f32 {
293 ((val.0 as f64) / 2.0f64.powi(24)) as f32
294 }
295}
296
297#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
298pub struct ScreenPoint {
299 #[serde(default, skip_serializing_if = "crate::entity::is_default")]
300 pub x: FracVal,
301 #[serde(default, skip_serializing_if = "crate::entity::is_default")]
302 pub y: FracVal,
303}
304
305#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
306pub struct ScreenSize {
307 #[serde(default, skip_serializing_if = "crate::entity::is_default")]
308 pub w: FracVal,
309 #[serde(default, skip_serializing_if = "crate::entity::is_default")]
310 pub h: FracVal,
311}
312
313#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Default)]
315pub struct ScreenRect {
316 #[serde(default, skip_serializing_if = "crate::entity::is_default")]
317 pub top_left: ScreenPoint,
318 #[serde(default, skip_serializing_if = "crate::entity::is_default")]
319 pub size: ScreenSize,
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325 use proptest::prelude::*;
326
327 #[test]
330 fn intersects_is_open_on_the_right() {
331 let rect = |x, y, w, h| GridRect {
332 top_left: GridPoint { x, y },
333 size: GridSize { w, h },
334 };
335 let a = rect(0, 0, 2, 2);
336 assert!(!a.intersects(rect(2, 0, 2, 2)), "touching edges are apart");
337 assert!(a.intersects(rect(1, 0, 2, 2)));
338 assert!(!a.intersects(rect(0, 2, 2, 2)), "touching edges are apart");
339 assert!(a.intersects(rect(0, 1, 2, 2)));
340 }
341
342 #[test]
346 fn contains_is_closed_on_every_edge() {
347 let rect = GridRect {
348 top_left: GridPoint { x: 0, y: 0 },
349 size: GridSize { w: 2, h: 2 },
350 };
351 for corner in [
352 GridPoint { x: 0, y: 0 },
353 GridPoint { x: 2, y: 2 },
354 GridPoint { x: 0, y: 2 },
355 GridPoint { x: 2, y: 0 },
356 ] {
357 assert!(rect.contains(corner), "{corner:?} is on the boundary");
358 }
359 assert!(!rect.contains(GridPoint { x: 3, y: 0 }));
360 }
361
362 #[test]
363 fn from_two_pos_normalizes_any_corner_pair() {
364 let a = GridPoint { x: 5, y: -1 };
365 let b = GridPoint { x: 2, y: 3 };
366 let expected = GridRect {
367 top_left: GridPoint { x: 2, y: -1 },
368 size: GridSize { w: 3, h: 4 },
369 };
370 assert_eq!(GridRect::from_two_pos(a, b), expected);
371 assert_eq!(GridRect::from_two_pos(b, a), expected);
372 assert_eq!(expected.max(), GridPoint { x: 5, y: 3 });
373 }
374
375 #[test]
376 fn translation_moves_the_corner_and_keeps_the_size() {
377 let rect = GridRect {
378 top_left: GridPoint { x: 1, y: 1 },
379 size: GridSize { w: 2, h: 3 },
380 };
381 let moved = rect.translate(GridVec::new(3, -2));
382 assert_eq!(moved.top_left, GridPoint { x: 4, y: -1 });
383 assert_eq!(moved.size, rect.size);
384 assert_eq!(moved.top_left - rect.top_left, GridVec::new(3, -2));
385 }
386
387 #[test]
390 fn both_float_zeros_quantize_identically() {
391 assert_eq!(FracVal::from(0.0f32), FracVal::from(-0.0f32));
392 }
393
394 #[test]
395 #[should_panic(expected = "NaN must not enter the document")]
396 fn nan_is_refused_at_the_boundary() {
397 let _ = FracVal::from(f32::NAN);
398 }
399
400 #[test]
403 fn representable_values_round_trip_exactly() {
404 for v in [0.0f32, 1.0, -1.0, 0.5, -3.25, 1024.75, 65_536.125] {
405 assert_eq!(f32::from(FracVal::from(v)), v);
406 }
407 }
408
409 proptest! {
410 #[test]
411 fn quantization_error_is_within_one_step(v in -1.0e6f32..1.0e6) {
412 let back = f32::from(FracVal::from(v));
413 prop_assert!((f64::from(back) - f64::from(v)).abs() <= 2.0f64.powi(-24));
414 }
415
416 #[test]
419 fn ordering_is_preserved(a in -1.0e6f32..1.0e6, b in -1.0e6f32..1.0e6) {
420 let (fa, fb) = (FracVal::from(a), FracVal::from(b));
421 if a <= b {
422 prop_assert!(fa <= fb);
423 } else {
424 prop_assert!(fa >= fb);
425 }
426 }
427 }
428}