sark_grids/
pivot.rs

1//! A pivot point on a 2d grid.
2use std::ops::Sub;
3
4use enum_ordinalize::Ordinalize;
5use glam::{IVec2, Vec2};
6
7use crate::GridPoint;
8
9use super::GridSize;
10
11/// A pivot on a 2d sized grid. Can be used to set positions relative to a given
12/// pivot. Each pivot has it's own coordinate space it uses to calculate
13/// the final adjusted position.
14#[derive(Eq, PartialEq, Clone, Copy, Debug, Ordinalize)]
15pub enum Pivot {
16    /// Coordinate space: X increases to the right, Y increases downwards.
17    TopLeft,
18    /// Coordinate space: X increases to the right, Y increases downwards.
19    TopCenter,
20    /// Coordinate space: X increases to the left, Y increases downwards.
21    TopRight,
22    /// Coordinate space: X increases to the right, Y increases upwards.
23    LeftCenter,
24    /// Coordinate space: X increases to the left, Y increases upwards.
25    RightCenter,
26    /// Coordinate space: X increases to the right, Y increases upwards.
27    BottomLeft,
28    /// Coordinate space: X increases to the right, Y increases upwards.
29    BottomCenter,
30    /// Coordinate space: X increases to the left, Y increases upwards.
31    BottomRight,
32    /// Coordinate space: X increases to the right, Y increases upwards.
33    Center,
34}
35
36impl Pivot {
37    /// Coordinate axis for each pivot, used when transforming a point into
38    /// the pivot's coordinate space.
39    #[inline]
40    pub fn axis(&self) -> IVec2 {
41        match self {
42            Pivot::TopLeft => IVec2::new(1, -1),
43            Pivot::TopRight => IVec2::new(-1, -1),
44            Pivot::Center => IVec2::new(1, 1),
45            Pivot::BottomLeft => IVec2::new(1, 1),
46            Pivot::BottomRight => IVec2::new(-1, 1),
47            Pivot::TopCenter => IVec2::new(1, -1),
48            Pivot::LeftCenter => IVec2::new(1, 1),
49            Pivot::RightCenter => IVec2::new(-1, 1),
50            Pivot::BottomCenter => IVec2::new(1, 1),
51        }
52    }
53
54    /// The normalized value of this pivot in default coordinate space where
55    /// `[0.0, 0.0]` is the bottom left and `[1.0, 1.0]` is the top right.
56    #[inline]
57    pub fn normalized(&self) -> Vec2 {
58        match self {
59            Pivot::TopLeft => Vec2::new(0.0, 1.0),
60            Pivot::TopRight => Vec2::new(1.0, 1.0),
61            Pivot::Center => Vec2::new(0.5, 0.5),
62            Pivot::BottomLeft => Vec2::new(0.0, 0.0),
63            Pivot::BottomRight => Vec2::new(1.0, 0.0),
64            Pivot::TopCenter => Vec2::new(0.5, 1.0),
65            Pivot::LeftCenter => Vec2::new(0.0, 0.5),
66            Pivot::RightCenter => Vec2::new(1.0, 0.5),
67            Pivot::BottomCenter => Vec2::new(0.5, 0.0),
68        }
69    }
70
71    /// Transform a point into the pivot's coordinate space.
72    #[inline]
73    pub fn transform_point(&self, grid_point: impl GridPoint) -> IVec2 {
74        grid_point.to_ivec2() * self.axis()
75    }
76
77    /// Calculate the position of a pivot on a sized grid.
78    #[inline]
79    pub fn pivot_position(&self, grid_size: impl GridSize) -> IVec2 {
80        (grid_size.to_vec2().sub(1.0) * self.normalized())
81            .round()
82            .as_ivec2()
83    }
84}
85
86/// A grid point that may optionally have a pivot applied to it.
87#[derive(Debug, Clone, Copy, Eq, PartialEq)]
88pub struct PivotedPoint {
89    pub point: IVec2,
90    pub pivot: Option<Pivot>,
91}
92
93impl PivotedPoint {
94    pub fn new(xy: impl GridPoint, pivot: Pivot) -> Self {
95        Self {
96            point: xy.to_ivec2(),
97            pivot: Some(pivot),
98        }
99    }
100
101    /// Calculate the final pivoted position on a sized grid.
102    ///
103    /// Transforms into the pivot's coordinate space if a pivot is applied,
104    /// returns the original point if no pivot is applied.
105    pub fn calculate(&self, grid_size: impl GridSize) -> IVec2 {
106        if let Some(pivot) = self.pivot {
107            pivot.pivot_position(grid_size) + pivot.transform_point(self.point)
108        } else {
109            self.point
110        }
111    }
112
113    /// Returns a new PivotedPoint with this point's pivot or a default applied
114    /// to it if this point doesn't have one.
115    pub fn with_default_pivot(&self, default_pivot: Pivot) -> PivotedPoint {
116        Self {
117            point: self.point,
118            pivot: Some(self.pivot.unwrap_or(default_pivot)),
119        }
120    }
121}
122
123impl<T: GridPoint> From<T> for PivotedPoint {
124    fn from(value: T) -> Self {
125        Self {
126            point: value.to_ivec2(),
127            pivot: None,
128        }
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn grid_pivot_size_offset() {
138        assert_eq!([4, 4], Pivot::TopRight.pivot_position([5, 5]).to_array());
139        assert_eq!([2, 2], Pivot::Center.pivot_position([5, 5]).to_array());
140        assert_eq!([3, 3], Pivot::TopRight.pivot_position([4, 4]).to_array());
141        assert_eq!([2, 2], Pivot::Center.pivot_position([4, 4]).to_array());
142    }
143
144    #[test]
145    fn pivoted_point() {
146        let pp = [1, 1].pivot(Pivot::TopLeft);
147        assert_eq!([1, 3], pp.calculate([5, 5]).to_array());
148
149        let pp = [1, 1].pivot(Pivot::TopRight);
150        assert_eq!([3, 3], pp.calculate([5, 5]).to_array());
151
152        let pp = [1, 1].pivot(Pivot::TopRight);
153        assert_eq!([4, 4], pp.calculate([6, 6]).to_array());
154
155        let pp = [1, 1].pivot(Pivot::Center);
156        assert_eq!([4, 4], pp.calculate([6, 6]).to_array());
157
158        let pp = [1, 1].pivot(Pivot::Center);
159        assert_eq!([3, 3], pp.calculate([5, 5]).to_array());
160
161        let pp = [0, 0].pivot(Pivot::BottomRight);
162        assert_eq!([8, 0], pp.calculate([9, 9]).to_array());
163    }
164
165    #[test]
166    fn center_negative() {
167        let p = [-5, -5].pivot(Pivot::Center);
168        assert_eq!([0, 0], p.calculate([10, 10]).to_array());
169    }
170}