Skip to main content

gooey/interface/view/
coordinates.rs

1//! Tile and pixel positions and dimensions.
2//!
3//! # Coordinate systems
4//!
5//! The interface works with two types of coordinates: tile and pixel.
6//!
7//! Tile coordinates are given in (row,column) pairs, while pixel coordinates
8//! are defined as (x,y) pairs, where $x$ is understood to be the horizontal
9//! component and $y$ the vertical component.
10//!
11//! Both coordinate systems are left-handed, however they are oriented
12//! differently:
13//!
14//! - The tile coordinate origin is taken to be the upper-left, with
15//!   rows increasing towards the bottom and columns increasing towards the
16//!   right.
17//! - The pixel coordinate origin is taken to be the bottom-left, with $x$
18//!   increasing towards the right and $y$ increasing towards the top.
19//!
20//! The main layout component is the view *`Canvas`* component which contains a
21//! coordinates field.
22//!
23//! ## Presentation backends
24//!
25//! Different presentation backends may have their own coordinate systems.
26//!
27//! **Curses**
28//!
29//! TODO
30//!
31//! **OpenGL**
32//!
33//! The OpenGL rendering backend supports both pixel-based and tile-based
34//! rendering.
35//!
36//! By default the 2D camera is centered with (0,0) in the center of the screen.
37//! In the presentation implementation, the camera is moved so that (0,0) is at
38//! the bottom-left of the screen.
39//!
40//! TODO: more
41
42use std;
43use std::sync::LazyLock;
44use derive_more::{From, TryInto};
45
46// TODO: use tagged coordinates?
47
48use crate::math::Vector2;
49use crate::geometry::integer::Aabb2;
50
51pub use self::position::Position;
52pub use self::dimensions::Dimensions;
53
54pub (crate) static TILE_WH : LazyLock <[u32; 2]> = LazyLock::new (||{
55  let width  = std::env::var ("GOOEY_TILE_WIDTH").unwrap().parse().unwrap();
56  let height = std::env::var ("GOOEY_TILE_HEIGHT").unwrap().parse().unwrap();
57  [width, height]
58});
59pub (crate) static SCREEN_WH : LazyLock <std::sync::RwLock <[u32; 2]>> = LazyLock::new (
60  || std::sync::RwLock::new ([0, 0]));
61
62#[derive(Clone, Copy, Debug, Eq, PartialEq, From, TryInto)]
63pub enum Coordinates {
64  Tile  (position::Tile,  dimensions::Tile),
65  Pixel (position::Pixel, dimensions::Pixel)
66}
67
68#[derive(Clone, Copy, Debug, Eq, PartialEq)]
69pub enum Kind {
70  Tile, Pixel
71}
72
73/// Convert a screen pixel (x, y) coordinate to a tile (row, column) coordinate
74pub fn pixel_to_tile (x : i32, y : i32) -> [i32; 2] {
75  let [tile_w,   tile_h]    = *TILE_WH;
76  let [_screen_w, screen_h] = *SCREEN_WH.read().unwrap();
77  let column = x / tile_w as i32;
78  let row    = (screen_h as i32 - y) / tile_h as i32;
79  [row, column]
80}
81
82/// Convert a screen pixel (x, y) AABB to tile (row, column) coordinate AABB
83pub fn pixel_to_tile_aabb (aabb : Aabb2 <i32>) -> Aabb2 <i32> {
84  let [min_x, min_y] = aabb.min().0.into_array();
85  let [max_x, max_y] = aabb.max().0.into_array();
86  let min = pixel_to_tile (min_x, max_y).into();
87  let max = pixel_to_tile (max_x, min_y).into();
88  Aabb2::with_minmax (min, max)
89}
90
91/// Convert a (row, column) coordinate to a screen pixel (x, y) coordinate
92pub fn tile_to_pixel (row : i32, column : i32) -> [i32; 2] {
93  let [tile_w,   tile_h]    = *TILE_WH;
94  let [_screen_w, screen_h] = *SCREEN_WH.read().unwrap();
95  let x = column * tile_w as i32;
96  let y = screen_h as i32 - row * tile_h as i32;
97  [x, y]
98}
99
100/// Convert a (row, column) AABB to a screen pixel (x, y) coordinate AABB
101pub fn tile_to_pixel_aabb (aabb : Aabb2 <i32>) -> Aabb2 <i32> {
102  let aabb = Aabb2::with_minmax (
103    aabb.min(),
104    aabb.max() + Vector2::new (1, 1)
105  );
106  let [min_row, min_col] = aabb.min().0.into_array();
107  let [max_row, max_col] = aabb.max().0.into_array();
108  let min = tile_to_pixel (max_row, min_col).into();
109  let max = tile_to_pixel (min_row, max_col).into();
110  Aabb2::with_minmax (min, max)
111}
112
113impl Coordinates {
114  #[inline]
115  pub fn default_tile() -> Self {
116    (position::Tile::default(), dimensions::Tile::default()).into()
117  }
118  #[inline]
119  pub fn default_pixel() -> Self {
120    (position::Pixel::default(), dimensions::Pixel::default()).into()
121  }
122  pub fn tile_from_aabb (aabb : Aabb2 <i32>) -> Self {
123    let position   = (aabb.min()).into();
124    let dimensions = aabb.dimensions().numcast().unwrap().into();
125    Coordinates::Tile (position, dimensions)
126  }
127  pub fn pixel_from_aabb (aabb : Aabb2 <i32>) -> Self {
128    let position   = aabb.min().into();
129    let dimensions = aabb.dimensions().numcast().unwrap().into();
130    Coordinates::Pixel (position, dimensions)
131  }
132  #[inline]
133  pub const fn kind (&self) -> Kind {
134    match self {
135      Coordinates::Tile  (_, _) => Kind::Tile,
136      Coordinates::Pixel (_, _) => Kind::Pixel
137    }
138  }
139  #[inline]
140  pub fn dimensions (&self) -> Dimensions {
141    match self {
142      Coordinates::Tile  (_, dimensions) => (*dimensions).into(),
143      Coordinates::Pixel (_, dimensions) => (*dimensions).into()
144    }
145  }
146  #[inline]
147  pub fn position (&self) -> Position {
148    match self {
149      Coordinates::Tile  (position, _) => (*position).into(),
150      Coordinates::Pixel (position, _) => (*position).into()
151    }
152  }
153  #[inline]
154  pub const fn dimensions_horizontal (&self) -> u32 {
155    match self {
156      Coordinates::Tile  (_, dimensions) => dimensions.columns(),
157      Coordinates::Pixel (_, dimensions) => dimensions.width()
158    }
159  }
160  #[inline]
161  pub const fn dimensions_vertical (&self) -> u32 {
162    match self {
163      Coordinates::Tile  (_, dimensions) => dimensions.rows(),
164      Coordinates::Pixel (_, dimensions) => dimensions.height()
165    }
166  }
167  #[inline]
168  pub fn position_horizontal (&self) -> i32 {
169    match self {
170      Coordinates::Tile  (position, _) => position.column(),
171      Coordinates::Pixel (position, _) => position.0.x
172    }
173  }
174  #[inline]
175  pub fn position_vertical (&self) -> i32 {
176    match self {
177      Coordinates::Tile  (position, _) => position.row(),
178      Coordinates::Pixel (position, _) => position.0.y
179    }
180  }
181  #[inline]
182  pub fn modify_dimensions_horizontal (&mut self, d : i32) {
183    match self {
184      Coordinates::Tile  (_, dimensions) =>
185        *dimensions.columns_mut() =
186          std::cmp::max (0, dimensions.columns() as i32 + d) as u32,
187      Coordinates::Pixel (_, dimensions) =>
188        dimensions.x = std::cmp::max (0, dimensions.x as i32 + d) as u32
189    }
190  }
191  #[inline]
192  pub fn modify_dimensions_vertical (&mut self, d : i32) {
193    match self {
194      Coordinates::Tile  (_, dimensions) =>
195        *dimensions.rows_mut() =
196          std::cmp::max (0, dimensions.rows() as i32 + d) as u32,
197      Coordinates::Pixel (_, dimensions) =>
198        dimensions.y = std::cmp::max (0, dimensions.y as i32 + d) as u32
199    }
200  }
201  #[inline]
202  pub fn modify_position_horizontal (&mut self, d : i32) {
203    match self {
204      Coordinates::Tile  (position, _) => *position.column_mut() = position.column() + d,
205      Coordinates::Pixel (position, _) => position.0.x += d
206    }
207  }
208  #[inline]
209  pub fn modify_position_vertical (&mut self, d : i32) {
210    match self {
211      Coordinates::Tile  (position, _) => *position.row_mut() = position.row() + d,
212      Coordinates::Pixel (position, _) => position.0.y += d
213    }
214  }
215  #[inline]
216  /// &#9888; Position types must match
217  pub fn set_position (&mut self, position : Position) {
218    match (self, position) {
219      (Coordinates::Tile  (position, _), Position::Tile  (p)) => *position = p,
220      (Coordinates::Pixel (position, _), Position::Pixel (p)) => *position = p,
221      _ => unreachable!()
222    }
223  }
224  #[inline]
225  /// &#9888; Dimensions types must match
226  pub fn set_dimensions (&mut self, dimensions : Dimensions) {
227    match (self, dimensions) {
228      (Coordinates::Tile  (_, dimensions), Dimensions::Tile  (d)) =>
229        *dimensions = d,
230      (Coordinates::Pixel (_, dimensions), Dimensions::Pixel (d)) =>
231        *dimensions = d,
232      _ => unreachable!()
233    }
234  }
235}
236
237impl From <Coordinates> for Aabb2 <i32> {
238  fn from (coordinates : Coordinates) -> Aabb2 <i32> {
239    log::trace!("aabb2 from coordinates: {coordinates:?}");
240    // NOTE: we subtract (1, 1) below because integer aabbs are inclusive of
241    // their max endpoints; however if the dimensions are zero then we can't
242    // subtract so it will result in the same Aabb as when the dimensions are 1
243    let (min, max) = match coordinates {
244      Coordinates::Tile  (position, dimensions) => {
245        let mut max = *position + dimensions.numcast().unwrap();
246        if dimensions.x > 0 {
247          max.0.x -= 1;
248        }
249        if dimensions.y > 0 {
250          max.0.y -= 1;
251        }
252        (*position, max)
253      }
254      Coordinates::Pixel (position, dimensions) => {
255        let mut max = *position + dimensions.numcast().unwrap();
256        if dimensions.x > 0 {
257          max.0.x -= 1;
258        }
259        if dimensions.y > 0 {
260          max.0.y -= 1;
261        }
262        (*position, max)
263      }
264    };
265    log::trace!("aabb2 with min, max: {:?}", (min, max));
266    Aabb2::with_minmax (min, max)
267  }
268}
269
270impl TryFrom <(Position, Dimensions)> for Coordinates {
271  type Error = (Position, Dimensions);
272  fn try_from ((position, dimensions) : (Position, Dimensions))
273    -> Result <Coordinates, (Position, Dimensions)>
274  {
275    let coordinates = match (position, dimensions) {
276      (Position::Tile  (position), Dimensions::Tile  (dimensions)) =>
277        Coordinates::Tile  (position, dimensions),
278      (Position::Pixel (position), Dimensions::Pixel (dimensions)) =>
279        Coordinates::Pixel (position, dimensions),
280      _ => return Err ((position, dimensions))
281    };
282    Ok (coordinates)
283  }
284}
285
286pub mod position {
287  use derive_more::{From, TryInto};
288  use crate::math::Point2;
289
290  #[derive(Clone, Copy, Debug, Eq, PartialEq, From, TryInto)]
291  pub enum Position {
292    Tile  (Tile),
293    Pixel (Pixel)
294  }
295
296  #[derive(Clone, Copy, Debug, Eq, PartialEq)]
297  pub struct Tile {
298    pub position_rc : Point2 <i32>
299  }
300
301  #[derive(Clone, Copy, Debug, Eq, PartialEq)]
302  pub struct Pixel {
303    pub position_xy : Point2 <i32>
304  }
305
306  // TODO: 3d coordinates?
307
308  impl Tile {
309    #[inline]
310    pub const fn origin() -> Self {
311      Tile {
312        position_rc: Point2::new (0, 0)
313      }
314    }
315    #[inline]
316    pub fn new_rc (row : i32, column : i32) -> Self {
317      Point2::new (row, column).into()
318    }
319    #[inline]
320    pub const fn row (&self) -> i32 {
321      self.position_rc.0.x
322    }
323    #[inline]
324    pub const fn column (&self) -> i32 {
325      self.position_rc.0.y
326    }
327    #[inline]
328    pub const fn row_mut (&mut self) -> &mut i32 {
329      &mut self.position_rc.0.x
330    }
331    #[inline]
332    pub const fn column_mut (&mut self) -> &mut i32 {
333      &mut self.position_rc.0.y
334    }
335  }
336
337  impl Default for Tile {
338    fn default() -> Self {
339      Tile { position_rc: [0,0].into() }
340    }
341  }
342
343  impl From <Point2 <i32>> for Tile {
344    fn from (position_rc : Point2 <i32>) -> Self {
345      Tile { position_rc }
346    }
347  }
348
349  impl std::ops::Deref for Tile {
350    type Target = Point2 <i32>;
351    fn deref (&self) -> &Point2 <i32> {
352      &self.position_rc
353    }
354  }
355
356  impl std::ops::DerefMut for Tile {
357    fn deref_mut (&mut self) -> &mut Point2 <i32> {
358      &mut self.position_rc
359    }
360  }
361
362  impl Pixel {
363    #[inline]
364    pub const fn origin() -> Self {
365      Pixel {
366        position_xy: Point2::new (0, 0)
367      }
368    }
369    #[inline]
370    pub fn new_xy (x : i32, y : i32) -> Self {
371      Point2::new (x, y).into()
372    }
373  }
374
375  impl Default for Pixel {
376    fn default() -> Self {
377      Pixel { position_xy: [0,0].into() }
378    }
379  }
380
381  impl From <Point2 <i32>> for Pixel {
382    fn from (position_xy : Point2 <i32>) -> Self {
383      Pixel { position_xy }
384    }
385  }
386
387  impl std::ops::Deref for Pixel {
388    type Target = Point2 <i32>;
389    fn deref (&self) -> &Point2 <i32> {
390      &self.position_xy
391    }
392  }
393
394  impl std::ops::DerefMut for Pixel {
395    fn deref_mut (&mut self) -> &mut Point2 <i32> {
396      &mut self.position_xy
397    }
398  }
399}
400
401pub mod dimensions {
402  use derive_more::{From, TryInto};
403  use crate::math::Vector2;
404
405  #[derive(Clone, Copy, Debug, Eq, PartialEq, From, TryInto)]
406  pub enum Dimensions {
407    Tile  (Tile),
408    Pixel (Pixel)
409  }
410
411  #[derive(Clone, Copy, Debug, Eq, PartialEq)]
412  pub struct Tile {
413    pub dimensions_rc : Vector2 <u32>
414  }
415
416  #[derive(Clone, Copy, Debug, Eq, PartialEq)]
417  pub struct Pixel {
418    pub dimensions_xy : Vector2 <u32>
419  }
420
421  // TODO: 3d dimensions?
422
423  impl Dimensions {
424    pub const fn horizontal (&self) -> u32 {
425      match self {
426        Dimensions::Tile  (tile)  => tile.columns(),
427        Dimensions::Pixel (pixel) => pixel.width()
428      }
429    }
430
431    pub const fn vertical (&self) -> u32 {
432      match self {
433        Dimensions::Tile  (tile)  => tile.rows(),
434        Dimensions::Pixel (pixel) => pixel.height()
435      }
436    }
437
438    pub fn vec (&self) -> Vector2 <u32> {
439      match self {
440        Dimensions::Tile  (tile)  => **tile,
441        Dimensions::Pixel (pixel) => **pixel
442      }
443    }
444  }
445
446  impl Tile {
447    #[inline]
448    pub fn new_rc (rows : u32, columns : u32) -> Self {
449      Vector2::new (rows, columns).into()
450    }
451    #[inline]
452    pub const fn columns (&self) -> u32 {
453      self.dimensions_rc.y
454    }
455    #[inline]
456    pub const fn rows (&self) -> u32 {
457      self.dimensions_rc.x
458    }
459    #[inline]
460    pub const fn rows_mut (&mut self) -> &mut u32 {
461      &mut self.dimensions_rc.x
462    }
463    #[inline]
464    pub const fn columns_mut (&mut self) -> &mut u32 {
465      &mut self.dimensions_rc.y
466    }
467    #[inline]
468    pub fn to_pixel (self) -> Pixel {
469      Vector2::new (self.columns(), self.rows()).into()
470    }
471  }
472
473  impl Pixel {
474    #[inline]
475    pub fn new_wh (width : u32, height : u32) -> Self {
476      Vector2::new (width, height).into()
477    }
478    #[inline]
479    pub const fn width (&self) -> u32 {
480      self.dimensions_xy.x
481    }
482    #[inline]
483    pub const fn height (&self) -> u32 {
484      self.dimensions_xy.y
485    }
486    #[inline]
487    pub fn to_tile (self) -> Tile {
488      Vector2::new (self.height(), self.width()).into()
489    }
490  }
491
492  impl Default for Tile {
493    fn default() -> Self {
494      Tile { dimensions_rc: [0,0].into() }
495    }
496  }
497
498  impl From <Vector2 <u32>> for Tile {
499    fn from (dimensions_rc : Vector2 <u32>) -> Self {
500      Tile { dimensions_rc }
501    }
502  }
503
504  impl std::ops::Deref for Tile {
505    type Target = Vector2 <u32>;
506    fn deref (&self) -> &Self::Target {
507      &self.dimensions_rc
508    }
509  }
510
511  impl std::ops::DerefMut for Tile {
512    fn deref_mut (&mut self) -> &mut Self::Target {
513      &mut self.dimensions_rc
514    }
515  }
516
517  impl Default for Pixel {
518    fn default() -> Self {
519      Pixel { dimensions_xy: [0,0].into() }
520    }
521  }
522
523  impl From <Vector2 <u32>> for Pixel {
524    fn from (dimensions_xy : Vector2 <u32>) -> Self {
525      Pixel { dimensions_xy }
526    }
527  }
528
529  impl std::ops::Deref for Pixel {
530    type Target = Vector2 <u32>;
531    fn deref (&self) -> &Self::Target {
532      &self.dimensions_xy
533    }
534  }
535
536  impl std::ops::DerefMut for Pixel {
537    fn deref_mut (&mut self) -> &mut Self::Target {
538      &mut self.dimensions_xy
539    }
540  }
541}
542
543impl std::ops::Add <dimensions::Tile> for position::Tile {
544  type Output = Self;
545  fn add (self, rhs : dimensions::Tile) -> Self {
546    (self.position_rc + rhs.dimensions_rc.numcast().unwrap()).into()
547  }
548}
549
550impl std::ops::Add <dimensions::Pixel> for position::Pixel {
551  type Output = Self;
552  fn add (self, rhs : dimensions::Pixel) -> Self {
553    (self.position_xy + rhs.dimensions_xy.numcast().unwrap()).into()
554  }
555}
556
557impl std::ops::Sub <dimensions::Tile> for position::Tile {
558  type Output = Self;
559  fn sub (self, rhs : dimensions::Tile) -> Self {
560    (self.position_rc - rhs.dimensions_rc.numcast().unwrap()).into()
561  }
562}
563
564impl std::ops::Sub <dimensions::Pixel> for position::Pixel {
565  type Output = Self;
566  fn sub (self, rhs : dimensions::Pixel) -> Self {
567    (self.position_xy - rhs.dimensions_xy.numcast().unwrap()).into()
568  }
569}
570
571#[cfg(test)]
572mod tests {
573  use crate::geometry::integer::Aabb2;
574  use super::*;
575  /// ```text
576  /// [0,0]
577  /// (tile)    [8,100]
578  ///       +---+---------------+
579  ///       |   |               |
580  /// [0,92]+---+               |
581  ///       |    [1,1](tile)    |
582  ///       |                   | 100
583  ///       |                   |
584  ///       |                   |
585  ///       |                   |
586  ///       |                   |
587  ///       +-------------------+
588  ///   [0,0]       100
589  /// (pixel)
590  /// ```
591  ///
592  /// Sets the screen to dimensions [100,100] and tile dimensions [8,8].
593  ///
594  /// Given tile coordinates with position [0,0] and dimensions [1,1], computes an AABB
595  /// in pixel coordinates which is min = [0,92] and max = [8,100].
596  #[test]
597  fn tile_to_pixel_aabb() {
598    use super::tile_to_pixel_aabb;
599    unsafe {
600      std::env::set_var ("GOOEY_TILE_WIDTH",  "8");
601      std::env::set_var ("GOOEY_TILE_HEIGHT", "8");
602    }
603    *SCREEN_WH.write().unwrap() = [100, 100];
604    let coordinates = Coordinates::Tile (
605      position::Tile::new_rc (0, 0),
606      dimensions::Tile::new_rc (1, 1));
607    let aabb = Aabb2::from (coordinates);
608    assert!(aabb.contains ([0, 0].into()));
609    assert!(!aabb.contains ([1, 1].into()));
610    let aabb_pixel = tile_to_pixel_aabb (aabb);
611    assert_eq!(aabb_pixel.min(), [0, 92].into());
612    assert_eq!(aabb_pixel.max(), [8, 100].into());
613  }
614}