1use serde::{Deserialize, Serialize};
8
9pub const GRID_LIMIT: i32 = 1 << 20;
17
18#[derive(Debug, thiserror::Error, PartialEq, Eq)]
19pub enum OutOfExtent {
20 #[error("coordinate {0} is outside the document extent (±{GRID_LIMIT})")]
21 Coordinate(i32),
22 #[error("extent {0} exceeds the document extent ({GRID_LIMIT})")]
23 Size(u32),
24 #[error("fixed-point value {0} is outside the screen range (±{limit})", limit = FracVal::LIMIT)]
25 Frac(i64),
26}
27
28#[derive(Deserialize)]
31struct RawGridPoint {
32 x: i32,
33 y: i32,
34}
35
36#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
37#[serde(try_from = "RawGridPoint")]
38pub struct GridPoint {
39 pub x: i32,
40 pub y: i32,
41}
42
43impl TryFrom<RawGridPoint> for GridPoint {
44 type Error = OutOfExtent;
45 fn try_from(raw: RawGridPoint) -> Result<Self, Self::Error> {
46 for coordinate in [raw.x, raw.y] {
47 if !(-GRID_LIMIT..=GRID_LIMIT).contains(&coordinate) {
48 return Err(OutOfExtent::Coordinate(coordinate));
49 }
50 }
51 Ok(Self { x: raw.x, y: raw.y })
52 }
53}
54
55#[derive(Deserialize)]
56struct RawGridSize {
57 w: u32,
58 h: u32,
59}
60
61#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
62#[serde(try_from = "RawGridSize")]
63pub struct GridSize {
64 pub w: u32,
65 pub h: u32,
66}
67
68impl TryFrom<RawGridSize> for GridSize {
69 type Error = OutOfExtent;
70 fn try_from(raw: RawGridSize) -> Result<Self, Self::Error> {
71 for extent in [raw.w, raw.h] {
72 if extent > GRID_LIMIT.unsigned_abs() {
73 return Err(OutOfExtent::Size(extent));
74 }
75 }
76 Ok(Self { w: raw.w, h: raw.h })
77 }
78}
79
80#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
82pub struct GridRect {
83 pub top_left: GridPoint,
84 pub size: GridSize,
85}
86
87#[derive(Clone, Copy, PartialEq, Eq, Default, Debug, Serialize, Deserialize)]
90pub struct Waypoint {
91 pub pos: GridPoint,
92 pub locked: bool,
93}
94
95#[derive(
100 Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
101)]
102#[serde(try_from = "i64")]
103pub struct FracVal(i64);
104
105impl FracVal {
106 pub const LIMIT: i64 = 1 << 48;
109}
110
111impl TryFrom<i64> for FracVal {
112 type Error = OutOfExtent;
113 fn try_from(raw: i64) -> Result<Self, Self::Error> {
114 if (-Self::LIMIT..=Self::LIMIT).contains(&raw) {
115 Ok(Self(raw))
116 } else {
117 Err(OutOfExtent::Frac(raw))
118 }
119 }
120}
121
122impl From<f32> for FracVal {
123 fn from(val: f32) -> FracVal {
124 debug_assert!(!val.is_nan(), "NaN must not enter the document");
125 Self((val as f64 * 2.0f64.powi(24)) as i64)
126 }
127}
128impl From<FracVal> for f32 {
129 fn from(val: FracVal) -> f32 {
130 ((val.0 as f64) / 2.0f64.powi(24)) as f32
131 }
132}
133
134#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
135pub struct ScreenPoint {
136 pub x: FracVal,
137 pub y: FracVal,
138}
139
140#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
141pub struct ScreenSize {
142 pub w: FracVal,
143 pub h: FracVal,
144}
145
146#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Default)]
148pub struct ScreenRect {
149 pub top_left: ScreenPoint,
150 pub size: ScreenSize,
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156 use proptest::prelude::*;
157
158 #[test]
161 fn both_float_zeros_quantize_identically() {
162 assert_eq!(FracVal::from(0.0f32), FracVal::from(-0.0f32));
163 }
164
165 #[test]
166 #[should_panic(expected = "NaN must not enter the document")]
167 fn nan_is_refused_at_the_boundary() {
168 let _ = FracVal::from(f32::NAN);
169 }
170
171 #[test]
174 fn representable_values_round_trip_exactly() {
175 for v in [0.0f32, 1.0, -1.0, 0.5, -3.25, 1024.75, 65_536.125] {
176 assert_eq!(f32::from(FracVal::from(v)), v);
177 }
178 }
179
180 proptest! {
181 #[test]
182 fn quantization_error_is_within_one_step(v in -1.0e6f32..1.0e6) {
183 let back = f32::from(FracVal::from(v));
184 prop_assert!((f64::from(back) - f64::from(v)).abs() <= 2.0f64.powi(-24));
185 }
186
187 #[test]
190 fn ordering_is_preserved(a in -1.0e6f32..1.0e6, b in -1.0e6f32..1.0e6) {
191 let (fa, fb) = (FracVal::from(a), FracVal::from(b));
192 if a <= b {
193 prop_assert!(fa <= fb);
194 } else {
195 prop_assert!(fa >= fb);
196 }
197 }
198 }
199}