fidget_core/render/
region.rs

1use nalgebra::{
2    Const, DefaultAllocator, DimNameAdd, DimNameSub, DimNameSum, OMatrix,
3    OVector, Point2, Point3, U1, Vector2, Vector3, allocator::Allocator,
4};
5
6/// Image size in pixels, used to generate a screen-to-world matrix
7///
8/// The screen coordinate space is the following:
9///
10/// ```text
11///        0 ------------> width
12///        |             |
13///        |             |
14///        |             |
15///        V--------------
16///   height
17/// ```
18///
19///
20/// The map from screen to world coordinates (generated by
21/// [`screen_to_world`](RegionSize::screen_to_world)) is as following:
22///
23/// ```text
24///       -1           y = +1
25///        0-------------^-------------> width
26///        |             |             |
27///        |             |             |
28///        |             |             |
29///   x = -1 <-----------0-------------> x = +1
30///        |             |             |
31///        |             |             |
32///        |             V             |
33///        V---------- y = -1 ---------
34///   height
35/// ```
36///
37/// (with `+z` pointing out of the screen)
38///
39/// Note that the Y axis is reversed between screen and world coordinates:
40/// screen coordinates have `+y` pointing down, but world coordinates have it
41/// pointing up.  For both X and Y coordinates, the `+1` value is located one
42/// pixel beyond the edge of the screen region (off the right edge for X, and
43/// off the top edge for Y).
44///
45/// If the render region is not square, then the shorter axis is clamped to ±1
46/// and the longer axis will exceed that value.
47#[derive(Copy, Clone, Debug, Eq, PartialEq)]
48pub struct RegionSize<const N: usize>
49where
50    Const<N>: DimNameAdd<U1>,
51    DefaultAllocator: Allocator<DimNameSum<Const<N>, U1>, DimNameSum<Const<N>, U1>>,
52    DefaultAllocator: Allocator<<<Const<N> as DimNameAdd<Const<1>>>::Output as DimNameSub<Const<1>>>::Output>,
53    <Const<N> as DimNameAdd<Const<1>>>::Output: DimNameSub<Const<1>>,
54    OVector<u32, <<Const<N> as DimNameAdd<Const<1>>>::Output as DimNameSub<Const<1>>>::Output>: Copy,
55{
56    size: OVector<u32, <<Const<N> as DimNameAdd<Const<1>>>::Output as DimNameSub<Const<1>>>::Output>,
57}
58
59impl<const N: usize> Default for RegionSize<N>
60where
61    Const<N>: DimNameAdd<U1>,
62    DefaultAllocator: Allocator<DimNameSum<Const<N>, U1>, DimNameSum<Const<N>, U1>>,
63    DefaultAllocator: Allocator<<<Const<N> as DimNameAdd<Const<1>>>::Output as DimNameSub<Const<1>>>::Output>,
64    <Const<N> as DimNameAdd<Const<1>>>::Output: DimNameSub<Const<1>>,
65    OVector<u32, <<Const<N> as DimNameAdd<Const<1>>>::Output as DimNameSub<Const<1>>>::Output>: Copy,
66{
67    fn default() -> Self {
68        Self {
69            size: OVector::<u32, <<Const<N> as DimNameAdd<Const<1>>>::Output as DimNameSub<Const<1>>>::Output>::from_element(0)
70        }
71    }
72}
73
74/// Apologies for the terrible trait bounds; they're necessary to persuade the
75/// internals to type-check, but shouldn't be noticeable to library users.
76impl<const N: usize> RegionSize<N>
77where
78    Const<N>: DimNameAdd<U1>,
79    DefaultAllocator: Allocator<DimNameSum<Const<N>, U1>, DimNameSum<Const<N>, U1>>,
80    DefaultAllocator: Allocator<<<Const<N> as DimNameAdd<Const<1>>>::Output as DimNameSub<Const<1>>>::Output>,
81    <Const<N> as DimNameAdd<Const<1>>>::Output: DimNameSub<Const<1>>,
82    <DefaultAllocator as nalgebra::allocator::Allocator<<<Const<N> as DimNameAdd<Const<1>>>::Output as DimNameSub<Const<1>>>::Output>>::Buffer<u32>: std::marker::Copy,
83{
84    /// Builds a matrix that converts from screen to world coordinates
85    ///
86    /// See the [`struct` docstring](RegionSize) for a diagram of this mapping.
87    pub fn screen_to_world(
88        &self,
89    ) -> OMatrix<
90        f32,
91        <Const<N> as DimNameAdd<Const<1>>>::Output,
92        <Const<N> as DimNameAdd<Const<1>>>::Output,
93    > {
94        let mut center = self.size.cast::<f32>() / 2.0;
95        center[1] -= 1.0;
96        let scale = 2.0 / self.size.min() as f32;
97
98        let mut out = OMatrix::<
99            f32,
100            <Const<N> as DimNameAdd<Const<1>>>::Output,
101            <Const<N> as DimNameAdd<Const<1>>>::Output,
102        >::identity();
103        out.append_translation_mut(&(-center));
104        let mut scale = OVector::<f32, _>::from_element(scale);
105        scale[1] *= -1.0;
106        out.append_nonuniform_scaling_mut(&scale);
107        out
108    }
109
110    /// Returns the width of the image (in pixels or voxels)
111    pub fn width(&self) -> u32 {
112        self.size[0]
113    }
114
115    /// Returns the height of the image (in pixels or voxels)
116    pub fn height(&self) -> u32 {
117        self.size[1]
118    }
119}
120
121/// Builds a `RegionSize` with the same dimension on all axes
122impl<const N: usize> From<u32> for RegionSize<N>
123where
124    Const<N>: DimNameAdd<U1>,
125    DefaultAllocator: Allocator<DimNameSum<Const<N>, U1>, DimNameSum<Const<N>, U1>>,
126    DefaultAllocator: Allocator<<<Const<N> as DimNameAdd<Const<1>>>::Output as DimNameSub<Const<1>>>::Output>,
127    <Const<N> as DimNameAdd<Const<1>>>::Output: DimNameSub<Const<1>>,
128    <DefaultAllocator as nalgebra::allocator::Allocator<<<Const<N> as DimNameAdd<Const<1>>>::Output as DimNameSub<Const<1>>>::Output>>::Buffer<u32>: std::marker::Copy,
129{
130    fn from(v: u32) -> Self {
131        Self {
132            size: OVector::<
133                u32,
134                <<Const<N> as DimNameAdd<Const<1>>>::Output as DimNameSub<
135                    Const<1>,
136                >>::Output,
137            >::from_element(v)
138        }
139    }
140}
141
142/// Size for 2D rendering of an image
143pub type ImageSize = RegionSize<2>;
144impl ImageSize {
145    /// Builds a new `ImageSize` object from width and height in pixels
146    pub fn new(width: u32, height: u32) -> Self {
147        Self {
148            size: Vector2::new(width, height),
149        }
150    }
151
152    /// Transforms a point from screen to world coordinates
153    pub fn transform_point(&self, p: Point2<i32>) -> Point2<f32> {
154        self.screen_to_world().transform_point(&p.cast())
155    }
156}
157
158/// Size for 3D rendering of an image
159pub type VoxelSize = RegionSize<3>;
160impl VoxelSize {
161    /// Builds a new `VoxelSize` object from width, height, and depth in voxels
162    pub fn new(width: u32, height: u32, depth: u32) -> Self {
163        Self {
164            size: Vector3::new(width, height, depth),
165        }
166    }
167
168    /// Returns the depth of the image (in voxels)
169    pub fn depth(&self) -> u32 {
170        self.size.z
171    }
172
173    /// Transforms a point from screen to world coordinates
174    pub fn transform_point(&self, p: Point3<i32>) -> Point3<f32> {
175        self.screen_to_world().transform_point(&p.cast())
176    }
177}
178
179impl<const N: usize> std::ops::Index<usize> for RegionSize<N>
180where
181    Const<N>: DimNameAdd<U1>,
182    DefaultAllocator: Allocator<DimNameSum<Const<N>, U1>, DimNameSum<Const<N>, U1>>,
183    DefaultAllocator: Allocator<<<Const<N> as DimNameAdd<Const<1>>>::Output as DimNameSub<Const<1>>>::Output>,
184    <Const<N> as DimNameAdd<Const<1>>>::Output: DimNameSub<Const<1>>,
185    OVector<u32, <<Const<N> as DimNameAdd<Const<1>>>::Output as DimNameSub<Const<1>>>::Output>: Copy,
186{
187    type Output = u32;
188    fn index(&self, i: usize) -> &Self::Output {
189        &self.size[i]
190    }
191}
192
193#[cfg(test)]
194mod test {
195    use super::*;
196    use nalgebra::Point2;
197
198    #[test]
199    fn test_screen_size() {
200        let image_size = ImageSize::new(1000, 500);
201        let mat = image_size.screen_to_world();
202
203        let pt = mat.transform_point(&Point2::new(500.0, 249.0));
204        assert_eq!(pt.x, 0.0);
205        assert_eq!(pt.y, 0.0);
206
207        let pt = mat.transform_point(&Point2::new(500.0, -1.0));
208        assert_eq!(pt.x, 0.0);
209        assert_eq!(pt.y, 1.0);
210
211        let pt = mat.transform_point(&Point2::new(500.0, 499.0));
212        assert_eq!(pt.x, 0.0);
213        assert_eq!(pt.y, -1.0);
214
215        let pt = mat.transform_point(&Point2::new(0.0, 249.0));
216        assert_eq!(pt.x, -2.0);
217        assert_eq!(pt.y, 0.0);
218
219        let pt = mat.transform_point(&Point2::new(1000.0, 249.0));
220        assert_eq!(pt.x, 2.0);
221        assert_eq!(pt.y, 0.0);
222    }
223}