thedes_geometry/
rect.rs

1use std::{
2    fmt,
3    ops::{Add, Div, Mul, Rem, Sub},
4};
5
6use num::{
7    CheckedAdd,
8    CheckedDiv,
9    CheckedMul,
10    CheckedSub,
11    One,
12    traits::CheckedRem,
13};
14use serde::{Deserialize, Serialize};
15use thiserror::Error;
16
17use crate::{
18    coords::CoordPair,
19    orientation::{Direction, DirectionVec},
20};
21
22#[derive(Debug, Error)]
23#[error("Invalid point {point} for rectangle {rect}")]
24pub struct InvalidPoint<C>
25where
26    C: fmt::Display,
27{
28    pub point: CoordPair<C>,
29    pub rect: Rect<C>,
30}
31
32#[derive(Debug, Error)]
33#[error("Invalid area {area} for partition of rectangle {rect}")]
34pub struct InvalidArea<C>
35where
36    C: fmt::Display,
37{
38    pub area: C,
39    pub rect: Rect<C>,
40}
41
42#[derive(Debug, Error)]
43pub enum HorzAreaError<C>
44where
45    C: fmt::Display,
46{
47    #[error("Point outside of rectangle defines no internal area")]
48    InvalidRectPoint(
49        #[from]
50        #[source]
51        InvalidPoint<C>,
52    ),
53    #[error("Arithmetic overflow computing area for rectangle of size {size}")]
54    Overflow { size: CoordPair<C> },
55}
56
57#[derive(
58    Debug,
59    Clone,
60    Copy,
61    PartialEq,
62    Eq,
63    PartialOrd,
64    Ord,
65    Hash,
66    Default,
67    Serialize,
68    Deserialize,
69)]
70pub struct Rect<C> {
71    pub top_left: CoordPair<C>,
72    pub size: CoordPair<C>,
73}
74
75impl<C> Rect<C> {
76    pub fn as_ref(&self) -> Rect<&C> {
77        Rect { top_left: self.top_left.as_ref(), size: self.size.as_ref() }
78    }
79
80    pub fn as_mut(&mut self) -> Rect<&mut C> {
81        Rect { top_left: self.top_left.as_mut(), size: self.size.as_mut() }
82    }
83
84    pub fn map<F, C0>(self, mut mapper: F) -> Rect<C0>
85    where
86        F: FnMut(C) -> C0,
87    {
88        Rect {
89            top_left: self.top_left.map(&mut mapper),
90            size: self.size.map(mapper),
91        }
92    }
93
94    pub fn try_map<F, C0, E>(self, mut mapper: F) -> Result<Rect<C0>, E>
95    where
96        F: FnMut(C) -> Result<C0, E>,
97    {
98        Ok(Rect {
99            top_left: self.top_left.try_map(&mut mapper)?,
100            size: self.size.try_map(mapper)?,
101        })
102    }
103
104    pub fn bottom_right<D>(self) -> CoordPair<D>
105    where
106        C: Add<Output = D>,
107    {
108        self.top_left + self.size
109    }
110
111    pub fn checked_bottom_right(&self) -> Option<CoordPair<C>>
112    where
113        C: CheckedAdd,
114    {
115        self.top_left.checked_add(&self.size)
116    }
117
118    pub fn contains_point(self, point: CoordPair<C>) -> bool
119    where
120        C: Sub<Output = C> + PartialOrd,
121    {
122        self.top_left
123            .zip3(self.size, point)
124            .all(|(start, size, coord)| coord >= start && coord - start < size)
125    }
126
127    pub fn checked_horz_area_down_to(
128        self,
129        point: CoordPair<C>,
130    ) -> Result<C, HorzAreaError<C>>
131    where
132        C: Sub<Output = C> + CheckedAdd + CheckedMul,
133        C: fmt::Display + PartialOrd + Clone,
134    {
135        if self.clone().contains_point(point.clone()) {
136            let from_origin = point - self.top_left;
137            self.size
138                .x
139                .checked_mul(&from_origin.y)
140                .and_then(|scaled| scaled.checked_add(&from_origin.x))
141                .ok_or(HorzAreaError::Overflow { size: self.size })
142        } else {
143            Err(InvalidPoint { point, rect: self })?
144        }
145    }
146
147    pub fn horz_area_down_to(self, point: CoordPair<C>) -> C
148    where
149        C: Sub<Output = C> + Mul<Output = C> + Add<Output = C>,
150        C: fmt::Display + PartialOrd + Clone,
151    {
152        let from_origin = point - self.top_left;
153        self.size.x * from_origin.y + from_origin.x
154    }
155
156    pub fn checked_bot_right_of_horz_area(
157        &self,
158        area: &C,
159    ) -> Result<CoordPair<C>, InvalidArea<C>>
160    where
161        C: CheckedAdd + CheckedDiv + CheckedRem + Clone,
162        C: fmt::Display,
163    {
164        let optional_coords = CoordPair {
165            x: area.checked_rem(&self.size.x),
166            y: area.checked_div(&self.size.x),
167        };
168
169        optional_coords
170            .transpose()
171            .and_then(|from_origin| self.top_left.checked_add(&from_origin))
172            .ok_or(InvalidArea { area: area.clone(), rect: self.clone() })
173    }
174
175    pub fn bot_right_of_horz_area(self, area: C) -> CoordPair<C>
176    where
177        C: Add<Output = C> + Div<Output = C> + Rem<Output = C> + Clone,
178    {
179        let x = area.clone() % self.size.x;
180        let y = area / self.size.y;
181        let from_origin = CoordPair { x, y };
182        self.top_left + from_origin
183    }
184
185    pub fn total_area<D>(self) -> D
186    where
187        C: Mul<Output = D>,
188    {
189        self.size.x * self.size.y
190    }
191
192    pub fn checked_total_area(&self) -> Option<C>
193    where
194        C: CheckedMul,
195    {
196        self.as_ref().checked_total_area_by_ref()
197    }
198
199    pub fn checked_move_point_unit(
200        self,
201        point: CoordPair<C>,
202        direction: Direction,
203    ) -> Result<CoordPair<C>, InvalidPoint<C>>
204    where
205        C: Add<Output = C> + Sub<Output = C> + CheckedAdd + CheckedSub + One,
206        C: Clone + PartialOrd + fmt::Display,
207    {
208        self.checked_move_point_by(
209            point,
210            DirectionVec::unit(direction).as_ref(),
211        )
212    }
213
214    pub fn checked_move_point_by(
215        self,
216        point: CoordPair<C>,
217        vector: DirectionVec<&C>,
218    ) -> Result<CoordPair<C>, InvalidPoint<C>>
219    where
220        C: Add<Output = C> + Sub<Output = C> + CheckedAdd + CheckedSub,
221        C: Clone + PartialOrd + fmt::Display,
222    {
223        if let Some(output) = point
224            .checked_move_by(vector)
225            .filter(|output| self.clone().contains_point(output.clone()))
226        {
227            Ok(output)
228        } else {
229            Err(InvalidPoint { point, rect: self })
230        }
231    }
232}
233
234impl<'a, C> Rect<&'a C> {
235    pub fn copied(self) -> Rect<C>
236    where
237        C: Copy,
238    {
239        Rect { top_left: self.top_left.copied(), size: self.size.copied() }
240    }
241
242    pub fn cloned(self) -> Rect<C>
243    where
244        C: Clone,
245    {
246        Rect { top_left: self.top_left.cloned(), size: self.size.cloned() }
247    }
248
249    pub fn checked_bottom_right_by_ref(self) -> Option<CoordPair<C>>
250    where
251        C: CheckedAdd,
252    {
253        self.top_left.checked_add_by_ref(self.size)
254    }
255
256    pub fn checked_total_area_by_ref(self) -> Option<C>
257    where
258        C: CheckedMul,
259    {
260        self.size.x.checked_mul(self.size.y)
261    }
262}
263
264impl<'a, C> Rect<&'a mut C> {
265    pub fn copied(self) -> Rect<C>
266    where
267        C: Copy,
268    {
269        Rect { top_left: self.top_left.copied(), size: self.size.copied() }
270    }
271
272    pub fn cloned(self) -> Rect<C>
273    where
274        C: Clone,
275    {
276        Rect { top_left: self.top_left.cloned(), size: self.size.cloned() }
277    }
278
279    pub fn share(self) -> Rect<&'a C> {
280        Rect { top_left: self.top_left.share(), size: self.size.share() }
281    }
282}
283
284impl<C> fmt::Display for Rect<C>
285where
286    C: fmt::Display,
287{
288    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
289        write!(f, "{}[{}]", self.top_left, self.size)
290    }
291}