pixelart/pixels/
position.rs

1use std::fmt::Display;
2
3use thiserror::Error;
4
5pub const MAIN_DIRECTIONS: [Direction; 4] = [
6    Direction::Up,
7    Direction::Right,
8    Direction::Down,
9    Direction::Left,
10];
11
12pub const MINOR_DIRECTIONS: [Direction; 4] = [
13    Direction::UpRight,
14    Direction::DownRight,
15    Direction::DownLeft,
16    Direction::UpLeft,
17];
18
19/// Interface for a pixel position.
20pub trait PixelPositionInterface {
21    /// Row number of the position starting from 0.
22    fn row(&self) -> usize;
23
24    /// Column number of the position starting from 0.
25    fn column(&self) -> usize;
26
27    /// Expand this type to a tuple of `(row, column)`.
28    fn expand(&self) -> (usize, usize) {
29        (self.row(), self.column())
30    }
31
32    /// Convert this [`PixelPosition`] to a [`PixelStrictPosition`].
33    ///
34    /// Returns [`PixelPositionOutOfBoundError`] error if provided row or column are out of bound,
35    fn bound<const H: usize, const W: usize>(&self) -> StrictPositionValidationResult<H, W> {
36        PixelStrictPosition::new(self.row(), self.column())
37    }
38
39    /// Returns a [`PixelPosition`] above this one as far as possible (0).
40    fn up(&self, amount: usize) -> PixelPosition {
41        PixelPosition::new(
42            self.row().checked_sub(amount).unwrap_or_default(),
43            self.column(),
44        )
45    }
46
47    /// Returns a [`PixelPosition`] at the left side of this one as far as possible (0).
48    fn left(&self, amount: usize) -> PixelPosition {
49        PixelPosition::new(
50            self.row(),
51            self.column().checked_sub(amount).unwrap_or_default(),
52        )
53    }
54
55    /// Returns a [`PixelPosition`] below this one as far as possible.
56    fn down(&self, amount: usize) -> PixelPosition {
57        PixelPosition::new(self.row().wrapping_add(amount), self.column())
58    }
59
60    /// Returns a [`PixelPosition`] at the right side of this one as far as possible.
61    fn right(&self, amount: usize) -> PixelPosition {
62        PixelPosition::new(self.row(), self.column().wrapping_add(amount))
63    }
64
65    /// Returns a [`PixelPosition`] at the [`Direction`] side of this one as far as possible.
66    fn direction(&self, dir: Direction, amount: usize) -> PixelPosition {
67        match dir {
68            Direction::Up => self.up(amount),
69            Direction::Right => self.right(amount),
70            Direction::Down => self.down(amount),
71            Direction::Left => self.left(amount),
72            Direction::UpRight => self.up(amount).right(amount),
73            Direction::DownRight => self.down(amount).right(amount),
74            Direction::DownLeft => self.down(amount).left(amount),
75            Direction::UpLeft => self.up(amount).left(amount),
76        }
77    }
78}
79
80/// Interface for a pixel position.
81///
82/// This type of position is limited between a H (height) and W (width).
83pub trait PixelStrictPositionInterface<const H: usize, const W: usize> {
84    /// Row number of the position starting from 0.
85    fn row(&self) -> usize;
86
87    /// Column number of the position starting from 0.
88    fn column(&self) -> usize;
89
90    /// Expand this type to a tuple of `(row, column)`.
91    fn expand(&self) -> (usize, usize) {
92        (self.row(), self.column())
93    }
94
95    /// Convert this [`PixelStrictPosition`] to a [`PixelPosition`], breaking the bounds.
96    fn unbound(&self) -> PixelPosition {
97        PixelPosition::new(self.row(), self.column())
98    }
99
100    /// Returns a [`PixelStrictPosition`] above this one **IF** its possible.
101    fn checked_up(&self, amount: usize) -> StrictPositionValidationResult<H, W> {
102        self.unbound().up(amount).bound()
103    }
104
105    /// Returns a [`PixelStrictPosition`] at the left side of this one **IF** its possible.
106    fn checked_left(&self, amount: usize) -> StrictPositionValidationResult<H, W> {
107        self.unbound().left(amount).bound()
108    }
109
110    /// Returns a [`PixelStrictPosition`] below this one **IF** its possible.
111    fn checked_down(&self, amount: usize) -> StrictPositionValidationResult<H, W> {
112        self.unbound().down(amount).bound()
113    }
114
115    /// Returns a [`PixelStrictPosition`] at the right side of this one **IF** its possible.
116    fn checked_right(&self, amount: usize) -> StrictPositionValidationResult<H, W> {
117        self.unbound().right(amount).bound()
118    }
119
120    /// Returns a [`PixelStrictPosition`] at the [`Direction`] side of this one **IF** its possible.
121    fn checked_direction(
122        &self,
123        dir: Direction,
124        amount: usize,
125    ) -> StrictPositionValidationResult<H, W> {
126        self.unbound().direction(dir, amount).bound()
127    }
128
129    /// Returns a [`PixelStrictPosition`] above this one as far as possible (0).
130    fn bounding_up(&self, amount: usize) -> PixelStrictPosition<H, W> {
131        self.checked_up(amount).unwrap_or_else(|e| e.adjust())
132    }
133
134    /// Returns a [`PixelStrictPosition`] at the left side of this one as far as possible (0).
135    fn bounding_left(&self, amount: usize) -> PixelStrictPosition<H, W> {
136        self.checked_left(amount).unwrap_or_else(|e| e.adjust())
137    }
138
139    /// Returns a [`PixelStrictPosition`] below this one as far as possible.
140    fn bounding_down(&self, amount: usize) -> PixelStrictPosition<H, W> {
141        self.checked_down(amount).unwrap_or_else(|e| e.adjust())
142    }
143
144    /// Returns a [`PixelStrictPosition`] at the right side of this one as far as possible.
145    fn bounding_right(&self, amount: usize) -> PixelStrictPosition<H, W> {
146        self.checked_right(amount).unwrap_or_else(|e| e.adjust())
147    }
148
149    /// Returns a [`PixelStrictPosition`] at the [`Direction`] side of this one as far as possible.
150    fn bounding_direction(&self, dir: Direction, amount: usize) -> PixelStrictPosition<H, W> {
151        self.checked_direction(dir, amount)
152            .unwrap_or_else(|e| e.adjust())
153    }
154}
155
156#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
157pub struct PixelPosition {
158    row: usize,
159    column: usize,
160}
161
162impl Display for PixelPosition {
163    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164        write!(f, "({}, {})", self.row, self.column)
165    }
166}
167
168impl PixelPosition {
169    pub const fn new(row: usize, column: usize) -> Self {
170        Self { row, column }
171    }
172}
173
174impl PixelPositionInterface for PixelPosition {
175    fn row(&self) -> usize {
176        self.row
177    }
178
179    fn column(&self) -> usize {
180        self.column
181    }
182}
183
184pub type StrictPositionValidationResult<const H: usize, const W: usize> =
185    Result<PixelStrictPosition<H, W>, PixelPositionOutOfBoundError<H, W>>;
186
187#[derive(Debug, Error)]
188pub enum PixelPositionOutOfBoundError<const H: usize, const W: usize> {
189    #[error("The provided row value {0:?} is equal or more that row bound ({H}).")]
190    InvalidRow(PixelPosition),
191    #[error("The provided column value {0:?} is equal or more that column bound ({W}).")]
192    InvalidColumn(PixelPosition),
193    #[error("Both provided row and column values are out of bound ({H}, {W}).")]
194    InvalidBoth(PixelPosition),
195}
196
197impl<const H: usize, const W: usize> PixelPositionOutOfBoundError<H, W> {
198    pub fn validate_position(row: usize, column: usize) -> StrictPositionValidationResult<H, W> {
199        use std::cmp::Ordering::*;
200        use PixelPositionOutOfBoundError::*;
201
202        let raw_position = PixelPosition::new(row, column);
203        match (row.cmp(&H), column.cmp(&W)) {
204            (Equal | Greater, Less) => Err(InvalidRow(raw_position)),
205            (Less, Equal | Greater) => Err(InvalidColumn(raw_position)),
206            (Equal | Greater, Equal | Greater) => Err(InvalidBoth(raw_position)),
207            _ => Ok(PixelStrictPosition { raw: raw_position }),
208        }
209    }
210
211    /// Adjusts the invalid position to be valid, reducing row or column to maximum allowed value.
212    pub fn adjust(&self) -> PixelStrictPosition<H, W> {
213        use PixelPositionOutOfBoundError::*;
214        match self {
215            InvalidRow(position) => PixelStrictPosition::new(H - 1, position.column).unwrap(),
216            InvalidColumn(position) => PixelStrictPosition::new(position.row, W - 1).unwrap(),
217            InvalidBoth(_) => PixelStrictPosition::new(H - 1, W - 1).unwrap(),
218        }
219    }
220}
221
222#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
223pub struct PixelStrictPosition<const H: usize, const W: usize> {
224    raw: PixelPosition,
225}
226
227impl<const H: usize, const W: usize> Iterator for PixelStrictPosition<H, W> {
228    type Item = PixelStrictPosition<H, W>;
229
230    fn next(&mut self) -> Option<Self::Item> {
231        // Going right by one to see what happens ...
232        let next = self.bounding_right(1);
233        if next.column() == self.column() {
234            // Reached right border, should go down by one ...
235            let next = self.bounding_down(1);
236            if next.row() == self.row() {
237                // Even moving down is not possible! nowhere no go.
238                None
239            } else {
240                // Starting the next row from the beginning (column: 0).
241                *self = PixelStrictPosition {
242                    raw: PixelPosition::new(next.row(), 0),
243                };
244                Some(self.clone())
245            }
246        } else {
247            // We can safely go right by one.
248            *self = next.clone();
249            Some(next)
250        }
251    }
252}
253
254impl<const H: usize, const W: usize> PixelStrictPosition<H, W> {
255    /// Create a new [`PixelStrictPosition`].
256    ///
257    /// Returns [`PixelPositionOutOfBoundError`] error if provided row or column are out of bound,
258    pub fn new(row: usize, column: usize) -> Result<Self, PixelPositionOutOfBoundError<H, W>> {
259        PixelPositionOutOfBoundError::validate_position(row, column)
260    }
261}
262
263impl<const H: usize, const W: usize> PixelStrictPositionInterface<H, W>
264    for PixelStrictPosition<H, W>
265{
266    fn row(&self) -> usize {
267        self.raw.row
268    }
269
270    fn column(&self) -> usize {
271        self.raw.column
272    }
273}
274
275impl<const H: usize, const W: usize> PixelStrictPositionInterface<H, W>
276    for &PixelStrictPosition<H, W>
277{
278    fn row(&self) -> usize {
279        self.raw.row
280    }
281
282    fn column(&self) -> usize {
283        self.raw.column
284    }
285}
286
287impl<const H: usize, const W: usize> PixelStrictPositionInterface<H, W>
288    for &mut PixelStrictPosition<H, W>
289{
290    fn row(&self) -> usize {
291        self.raw.row
292    }
293
294    fn column(&self) -> usize {
295        self.raw.column
296    }
297}
298
299pub trait IntoPixelStrictPosition<const H: usize, const W: usize> {
300    fn into_pixel_strict_position(self) -> PixelStrictPosition<H, W>;
301}
302
303impl<const H: usize, const W: usize, T> IntoPixelStrictPosition<H, W> for T
304where
305    T: PixelStrictPositionInterface<H, W>,
306{
307    fn into_pixel_strict_position(self) -> PixelStrictPosition<H, W> {
308        PixelStrictPosition::new(self.row(), self.column()).unwrap()
309    }
310}
311
312impl<const H: usize, const W: usize> IntoPixelStrictPosition<H, W> for (usize, usize) {
313    fn into_pixel_strict_position(self) -> PixelStrictPosition<H, W> {
314        PixelStrictPosition::new(self.0, self.1).unwrap_or_else(|e| e.adjust())
315    }
316}
317
318impl<const H: usize, const W: usize> IntoPixelStrictPosition<H, W> for [usize; 2] {
319    fn into_pixel_strict_position(self) -> PixelStrictPosition<H, W> {
320        PixelStrictPosition::new(self[0], self[1]).unwrap_or_else(|e| e.adjust())
321    }
322}
323
324/// A set of common useful [`PixelStrictPosition`]s inside the container
325/// wrapped by square from `(H - 1, 0) -> bottom-left` to `(0, W - 1) -> top-right`.
326#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
327pub enum StrictPositions {
328    /// The most top left side of the container `(0, 0)`.
329    TopLeft,
330
331    /// The most top right side of the container `(0, W - 1)`.
332    TopRight,
333
334    /// The most bottom right side of the container `(H - 1, W - 1)`.
335    BottomRight,
336
337    /// The most bottom left side of the container `(H - 1, 0)`.
338    BottomLeft,
339
340    /// The center of the square `(H / 2, W / 2)`.
341    Center,
342
343    /// The center of the top row `(0, W / 2)`.
344    TopCenter,
345
346    /// The center of the most right column `(H / 2, W - 1)`.
347    RightCenter,
348
349    /// The center of bottom row `(H - 1, W / 2)`.
350    BottomCenter,
351
352    /// The center of the most left column `(H / 2, 0)`.
353    LeftCenter,
354}
355
356impl<const H: usize, const W: usize> PixelStrictPositionInterface<H, W> for StrictPositions {
357    fn row(&self) -> usize {
358        use StrictPositions::*;
359        match self {
360            TopLeft => 0,
361            TopRight => 0,
362            BottomRight => H - 1,
363            BottomLeft => H - 1,
364            Center => H / 2,
365            TopCenter => 0,
366            RightCenter => H / 2,
367            BottomCenter => H - 1,
368            LeftCenter => H / 2,
369        }
370    }
371
372    fn column(&self) -> usize {
373        use StrictPositions::*;
374        match self {
375            TopLeft => 0,
376            TopRight => W - 1,
377            BottomRight => W - 1,
378            BottomLeft => 0,
379            Center => W / 2,
380            TopCenter => W / 2,
381            RightCenter => W - 1,
382            BottomCenter => W / 2,
383            LeftCenter => 0,
384        }
385    }
386}
387
388/// Represents a direction.
389#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
390pub enum Direction {
391    /// Going up.
392    Up,
393
394    /// Up right.
395    UpRight,
396
397    /// Going right.
398    Right,
399
400    /// Right down
401    DownRight,
402
403    /// Going down.
404    Down,
405
406    DownLeft,
407
408    /// Going left.
409    Left,
410
411    UpLeft,
412}
413
414impl Iterator for Direction {
415    type Item = Direction;
416
417    fn next(&mut self) -> Option<Self::Item> {
418        use Direction::*;
419        match self {
420            Up => UpRight,
421            UpRight => Right,
422            Right => DownRight,
423            DownRight => Down,
424            Down => DownLeft,
425            DownLeft => Left,
426            Left => UpLeft,
427            UpLeft => Up,
428        }
429        .into()
430    }
431}
432
433pub struct SingleCycle {
434    initial_dir: Direction,
435    current: Option<Direction>,
436}
437
438impl SingleCycle {
439    pub fn new(initial_dir: Direction) -> Self {
440        Self {
441            initial_dir,
442            current: None,
443        }
444    }
445}
446
447impl Iterator for SingleCycle {
448    type Item = Direction;
449
450    fn next(&mut self) -> Option<Self::Item> {
451        if let Some(mut current) = self.current {
452            let next = current.next().unwrap();
453
454            if next == self.initial_dir {
455                return None;
456            }
457
458            self.current = Some(next);
459            self.current
460        } else {
461            self.current = Some(self.initial_dir);
462            Some(self.initial_dir)
463        }
464    }
465}
466
467#[cfg(test)]
468mod tests {
469    use super::*;
470
471    #[test]
472    fn test_name() {
473        let pos = PixelStrictPosition::<5, 5>::new(0, 0).unwrap();
474
475        assert_eq!(
476            PixelStrictPosition {
477                raw: PixelPosition::new(1, 0)
478            },
479            pos.bounding_down(1)
480        );
481        assert_eq!(
482            PixelStrictPosition {
483                raw: PixelPosition::new(2, 0)
484            },
485            pos.bounding_down(2)
486        );
487        assert_eq!(
488            PixelStrictPosition {
489                raw: PixelPosition::new(3, 0)
490            },
491            pos.bounding_down(3)
492        );
493        assert_eq!(
494            PixelStrictPosition {
495                raw: PixelPosition::new(4, 0)
496            },
497            pos.bounding_down(4)
498        );
499        assert_eq!(
500            PixelStrictPosition {
501                raw: PixelPosition::new(4, 0)
502            },
503            pos.bounding_down(5)
504        );
505        assert_eq!(
506            PixelStrictPosition {
507                raw: PixelPosition::new(0, 4)
508            },
509            pos.bounding_right(5)
510        );
511    }
512
513    #[test]
514    fn test_iter() {
515        let mut pos = PixelStrictPosition::<2, 2>::new(0, 0).unwrap();
516
517        assert_eq!(
518            Some(PixelStrictPosition {
519                raw: PixelPosition::new(0, 1)
520            }),
521            pos.next()
522        );
523        assert_eq!(
524            Some(PixelStrictPosition {
525                raw: PixelPosition::new(1, 0)
526            }),
527            pos.next()
528        );
529        assert_eq!(
530            Some(PixelStrictPosition {
531                raw: PixelPosition::new(1, 1)
532            }),
533            pos.next()
534        );
535        assert_eq!(None, pos.next());
536    }
537
538    #[test]
539    fn test_direction_single_cycle() {
540        use Direction::*;
541        let mut single = SingleCycle::new(Down);
542
543        assert_eq!(Some(Down), single.next());
544        assert_eq!(Some(DownLeft), single.next());
545        assert_eq!(Some(Left), single.next());
546        assert_eq!(Some(UpLeft), single.next());
547        assert_eq!(Some(Up), single.next());
548        assert_eq!(Some(UpRight), single.next());
549        assert_eq!(Some(Right), single.next());
550        assert_eq!(Some(DownRight), single.next());
551        assert_eq!(None, single.next());
552    }
553}