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.max() - aabb.min()) + Vector2::new (1, 1))
125      .numcast().unwrap().into();
126    Coordinates::Tile (position, dimensions)
127  }
128  pub fn pixel_from_aabb (aabb : Aabb2 <i32>) -> Self {
129    let position   = (*aabb.min()).into();
130    let dimensions = ((*aabb.max() - aabb.min()) + Vector2::new (1, 1))
131      .numcast().unwrap().into();
132    Coordinates::Pixel (position, dimensions)
133  }
134  #[inline]
135  pub const fn kind (&self) -> Kind {
136    match self {
137      Coordinates::Tile  (_, _) => Kind::Tile,
138      Coordinates::Pixel (_, _) => Kind::Pixel
139    }
140  }
141  #[inline]
142  pub fn dimensions (&self) -> Dimensions {
143    match self {
144      Coordinates::Tile  (_, dimensions) => (*dimensions).into(),
145      Coordinates::Pixel (_, dimensions) => (*dimensions).into()
146    }
147  }
148  #[inline]
149  pub fn position (&self) -> Position {
150    match self {
151      Coordinates::Tile  (position, _) => (*position).into(),
152      Coordinates::Pixel (position, _) => (*position).into()
153    }
154  }
155  #[inline]
156  pub const fn dimensions_horizontal (&self) -> u32 {
157    match self {
158      Coordinates::Tile  (_, dimensions) => dimensions.columns(),
159      Coordinates::Pixel (_, dimensions) => dimensions.width()
160    }
161  }
162  #[inline]
163  pub const fn dimensions_vertical (&self) -> u32 {
164    match self {
165      Coordinates::Tile  (_, dimensions) => dimensions.rows(),
166      Coordinates::Pixel (_, dimensions) => dimensions.height()
167    }
168  }
169  #[inline]
170  pub fn position_horizontal (&self) -> i32 {
171    match self {
172      Coordinates::Tile  (position, _) => position.column(),
173      Coordinates::Pixel (position, _) => position.0.x
174    }
175  }
176  #[inline]
177  pub fn position_vertical (&self) -> i32 {
178    match self {
179      Coordinates::Tile  (position, _) => position.row(),
180      Coordinates::Pixel (position, _) => position.0.y
181    }
182  }
183  #[inline]
184  pub fn modify_dimensions_horizontal (&mut self, d : i32) {
185    match self {
186      Coordinates::Tile  (_, dimensions) =>
187        *dimensions.columns_mut() =
188          std::cmp::max (0, dimensions.columns() as i32 + d) as u32,
189      Coordinates::Pixel (_, dimensions) =>
190        dimensions.x = std::cmp::max (0, dimensions.x as i32 + d) as u32
191    }
192  }
193  #[inline]
194  pub fn modify_dimensions_vertical (&mut self, d : i32) {
195    match self {
196      Coordinates::Tile  (_, dimensions) =>
197        *dimensions.rows_mut() =
198          std::cmp::max (0, dimensions.rows() as i32 + d) as u32,
199      Coordinates::Pixel (_, dimensions) =>
200        dimensions.y = std::cmp::max (0, dimensions.y as i32 + d) as u32
201    }
202  }
203  #[inline]
204  pub fn modify_position_horizontal (&mut self, d : i32) {
205    match self {
206      Coordinates::Tile  (position, _) => *position.column_mut() = position.column() + d,
207      Coordinates::Pixel (position, _) => position.0.x += d
208    }
209  }
210  #[inline]
211  pub fn modify_position_vertical (&mut self, d : i32) {
212    match self {
213      Coordinates::Tile  (position, _) => *position.row_mut() = position.row() + d,
214      Coordinates::Pixel (position, _) => position.0.y += d
215    }
216  }
217  #[inline]
218  /// &#9888; Position types must match
219  pub fn set_position (&mut self, position : Position) {
220    match (self, position) {
221      (Coordinates::Tile  (position, _), Position::Tile  (p)) => *position = p,
222      (Coordinates::Pixel (position, _), Position::Pixel (p)) => *position = p,
223      _ => unreachable!()
224    }
225  }
226  #[inline]
227  /// &#9888; Dimensions types must match
228  pub fn set_dimensions (&mut self, dimensions : Dimensions) {
229    match (self, dimensions) {
230      (Coordinates::Tile  (_, dimensions), Dimensions::Tile  (d)) =>
231        *dimensions = d,
232      (Coordinates::Pixel (_, dimensions), Dimensions::Pixel (d)) =>
233        *dimensions = d,
234      _ => unreachable!()
235    }
236  }
237}
238
239impl From <Coordinates> for Aabb2 <i32> {
240  fn from (coordinates : Coordinates) -> Aabb2 <i32> {
241    log::trace!("aabb2 from coordinates: {coordinates:?}");
242    // NOTE: we subtract (1, 1) below because integer aabbs are inclusive of
243    // their max endpoints; however if the dimensions are zero then we can't
244    // subtract so it will result in the same Aabb as when the dimensions are 1
245    let (min, max) = match coordinates {
246      Coordinates::Tile  (position, dimensions) => {
247        let mut max = *position + dimensions.numcast().unwrap();
248        if dimensions.x > 0 {
249          max.0.x -= 1;
250        }
251        if dimensions.y > 0 {
252          max.0.y -= 1;
253        }
254        (*position, max)
255      }
256      Coordinates::Pixel (position, dimensions) => {
257        let mut max = *position + dimensions.numcast().unwrap();
258        if dimensions.x > 0 {
259          max.0.x -= 1;
260        }
261        if dimensions.y > 0 {
262          max.0.y -= 1;
263        }
264        (*position, max)
265      }
266    };
267    log::trace!("aabb2 with min, max: {:?}", (min, max));
268    Aabb2::with_minmax (min, max)
269  }
270}
271
272impl TryFrom <(Position, Dimensions)> for Coordinates {
273  type Error = (Position, Dimensions);
274  fn try_from ((position, dimensions) : (Position, Dimensions))
275    -> Result <Coordinates, (Position, Dimensions)>
276  {
277    let coordinates = match (position, dimensions) {
278      (Position::Tile  (position), Dimensions::Tile  (dimensions)) =>
279        Coordinates::Tile  (position, dimensions),
280      (Position::Pixel (position), Dimensions::Pixel (dimensions)) =>
281        Coordinates::Pixel (position, dimensions),
282      _ => return Err ((position, dimensions))
283    };
284    Ok (coordinates)
285  }
286}
287
288pub mod position {
289  use derive_more::{From, TryInto};
290  use crate::math::Point2;
291
292  #[derive(Clone, Copy, Debug, Eq, PartialEq, From, TryInto)]
293  pub enum Position {
294    Tile  (Tile),
295    Pixel (Pixel)
296  }
297
298  #[derive(Clone, Copy, Debug, Eq, PartialEq)]
299  pub struct Tile {
300    pub position_rc : Point2 <i32>
301  }
302
303  #[derive(Clone, Copy, Debug, Eq, PartialEq)]
304  pub struct Pixel {
305    pub position_xy : Point2 <i32>
306  }
307
308  // TODO: 3d coordinates?
309
310  impl Tile {
311    #[inline]
312    pub const fn origin() -> Self {
313      Tile {
314        position_rc: Point2::new (0, 0)
315      }
316    }
317    #[inline]
318    pub fn new_rc (row : i32, column : i32) -> Self {
319      Point2::new (row, column).into()
320    }
321    #[inline]
322    pub const fn row (&self) -> i32 {
323      self.position_rc.0.x
324    }
325    #[inline]
326    pub const fn column (&self) -> i32 {
327      self.position_rc.0.y
328    }
329    #[inline]
330    pub const fn row_mut (&mut self) -> &mut i32 {
331      &mut self.position_rc.0.x
332    }
333    #[inline]
334    pub const fn column_mut (&mut self) -> &mut i32 {
335      &mut self.position_rc.0.y
336    }
337  }
338
339  impl Default for Tile {
340    fn default() -> Self {
341      Tile { position_rc: [0,0].into() }
342    }
343  }
344
345  impl From <Point2 <i32>> for Tile {
346    fn from (position_rc : Point2 <i32>) -> Self {
347      Tile { position_rc }
348    }
349  }
350
351  impl std::ops::Deref for Tile {
352    type Target = Point2 <i32>;
353    fn deref (&self) -> &Point2 <i32> {
354      &self.position_rc
355    }
356  }
357
358  impl std::ops::DerefMut for Tile {
359    fn deref_mut (&mut self) -> &mut Point2 <i32> {
360      &mut self.position_rc
361    }
362  }
363
364  impl Pixel {
365    #[inline]
366    pub const fn origin() -> Self {
367      Pixel {
368        position_xy: Point2::new (0, 0)
369      }
370    }
371    #[inline]
372    pub fn new_xy (x : i32, y : i32) -> Self {
373      Point2::new (x, y).into()
374    }
375  }
376
377  impl Default for Pixel {
378    fn default() -> Self {
379      Pixel { position_xy: [0,0].into() }
380    }
381  }
382
383  impl From <Point2 <i32>> for Pixel {
384    fn from (position_xy : Point2 <i32>) -> Self {
385      Pixel { position_xy }
386    }
387  }
388
389  impl std::ops::Deref for Pixel {
390    type Target = Point2 <i32>;
391    fn deref (&self) -> &Point2 <i32> {
392      &self.position_xy
393    }
394  }
395
396  impl std::ops::DerefMut for Pixel {
397    fn deref_mut (&mut self) -> &mut Point2 <i32> {
398      &mut self.position_xy
399    }
400  }
401}
402
403pub mod dimensions {
404  use derive_more::{From, TryInto};
405  use crate::math::Vector2;
406
407  #[derive(Clone, Copy, Debug, Eq, PartialEq, From, TryInto)]
408  pub enum Dimensions {
409    Tile  (Tile),
410    Pixel (Pixel)
411  }
412
413  #[derive(Clone, Copy, Debug, Eq, PartialEq)]
414  pub struct Tile {
415    pub dimensions_rc : Vector2 <u32>
416  }
417
418  #[derive(Clone, Copy, Debug, Eq, PartialEq)]
419  pub struct Pixel {
420    pub dimensions_xy : Vector2 <u32>
421  }
422
423  // TODO: 3d dimensions?
424
425  impl Dimensions {
426    pub const fn horizontal (&self) -> u32 {
427      match self {
428        Dimensions::Tile  (tile)  => tile.columns(),
429        Dimensions::Pixel (pixel) => pixel.width()
430      }
431    }
432
433    pub const fn vertical (&self) -> u32 {
434      match self {
435        Dimensions::Tile  (tile)  => tile.rows(),
436        Dimensions::Pixel (pixel) => pixel.height()
437      }
438    }
439
440    pub fn vec (&self) -> Vector2 <u32> {
441      match self {
442        Dimensions::Tile  (tile)  => **tile,
443        Dimensions::Pixel (pixel) => **pixel
444      }
445    }
446  }
447
448  impl Tile {
449    #[inline]
450    pub fn new_rc (rows : u32, columns : u32) -> Self {
451      Vector2::new (rows, columns).into()
452    }
453    #[inline]
454    pub const fn columns (&self) -> u32 {
455      self.dimensions_rc.y
456    }
457    #[inline]
458    pub const fn rows (&self) -> u32 {
459      self.dimensions_rc.x
460    }
461    #[inline]
462    pub const fn rows_mut (&mut self) -> &mut u32 {
463      &mut self.dimensions_rc.x
464    }
465    #[inline]
466    pub const fn columns_mut (&mut self) -> &mut u32 {
467      &mut self.dimensions_rc.y
468    }
469    #[inline]
470    pub fn to_pixel (self) -> Pixel {
471      Vector2::new (self.columns(), self.rows()).into()
472    }
473  }
474
475  impl Pixel {
476    #[inline]
477    pub fn new_wh (width : u32, height : u32) -> Self {
478      Vector2::new (width, height).into()
479    }
480    #[inline]
481    pub const fn width (&self) -> u32 {
482      self.dimensions_xy.x
483    }
484    #[inline]
485    pub const fn height (&self) -> u32 {
486      self.dimensions_xy.y
487    }
488    #[inline]
489    pub fn to_tile (self) -> Tile {
490      Vector2::new (self.height(), self.width()).into()
491    }
492  }
493
494  impl Default for Tile {
495    fn default() -> Self {
496      Tile { dimensions_rc: [0,0].into() }
497    }
498  }
499
500  impl From <Vector2 <u32>> for Tile {
501    fn from (dimensions_rc : Vector2 <u32>) -> Self {
502      Tile { dimensions_rc }
503    }
504  }
505
506  impl std::ops::Deref for Tile {
507    type Target = Vector2 <u32>;
508    fn deref (&self) -> &Self::Target {
509      &self.dimensions_rc
510    }
511  }
512
513  impl std::ops::DerefMut for Tile {
514    fn deref_mut (&mut self) -> &mut Self::Target {
515      &mut self.dimensions_rc
516    }
517  }
518
519  impl Default for Pixel {
520    fn default() -> Self {
521      Pixel { dimensions_xy: [0,0].into() }
522    }
523  }
524
525  impl From <Vector2 <u32>> for Pixel {
526    fn from (dimensions_xy : Vector2 <u32>) -> Self {
527      Pixel { dimensions_xy }
528    }
529  }
530
531  impl std::ops::Deref for Pixel {
532    type Target = Vector2 <u32>;
533    fn deref (&self) -> &Self::Target {
534      &self.dimensions_xy
535    }
536  }
537
538  impl std::ops::DerefMut for Pixel {
539    fn deref_mut (&mut self) -> &mut Self::Target {
540      &mut self.dimensions_xy
541    }
542  }
543}
544
545impl std::ops::Add <dimensions::Tile> for position::Tile {
546  type Output = Self;
547  fn add (self, rhs : dimensions::Tile) -> Self {
548    (self.position_rc + rhs.dimensions_rc.numcast().unwrap()).into()
549  }
550}
551
552impl std::ops::Add <dimensions::Pixel> for position::Pixel {
553  type Output = Self;
554  fn add (self, rhs : dimensions::Pixel) -> Self {
555    (self.position_xy + rhs.dimensions_xy.numcast().unwrap()).into()
556  }
557}
558
559impl std::ops::Sub <dimensions::Tile> for position::Tile {
560  type Output = Self;
561  fn sub (self, rhs : dimensions::Tile) -> Self {
562    (self.position_rc - rhs.dimensions_rc.numcast().unwrap()).into()
563  }
564}
565
566impl std::ops::Sub <dimensions::Pixel> for position::Pixel {
567  type Output = Self;
568  fn sub (self, rhs : dimensions::Pixel) -> Self {
569    (self.position_xy - rhs.dimensions_xy.numcast().unwrap()).into()
570  }
571}
572
573#[cfg(test)]
574mod tests {
575  use crate::geometry::integer::Aabb2;
576  use super::*;
577  /// ```text
578  /// [0,0]
579  /// (tile)    [8,100]
580  ///       +---+---------------+
581  ///       |   |               |
582  /// [0,92]+---+               |
583  ///       |    [1,1](tile)    |
584  ///       |                   | 100
585  ///       |                   |
586  ///       |                   |
587  ///       |                   |
588  ///       |                   |
589  ///       +-------------------+
590  ///   [0,0]       100
591  /// (pixel)
592  /// ```
593  ///
594  /// Sets the screen to dimensions [100,100] and tile dimensions [8,8].
595  ///
596  /// Given tile coordinates with position [0,0] and dimensions [1,1], computes an AABB
597  /// in pixel coordinates which is min = [0,92] and max = [8,100].
598  #[test]
599  fn tile_to_pixel_aabb() {
600    use super::tile_to_pixel_aabb;
601    unsafe {
602      std::env::set_var ("GOOEY_TILE_WIDTH",  "8");
603      std::env::set_var ("GOOEY_TILE_HEIGHT", "8");
604    }
605    *SCREEN_WH.write().unwrap() = [100, 100];
606    let coordinates = Coordinates::Tile (
607      position::Tile::new_rc (0, 0),
608      dimensions::Tile::new_rc (1, 1));
609    let aabb = Aabb2::from (coordinates);
610    assert!(aabb.contains (&[0, 0].into()));
611    assert!(!aabb.contains (&[1, 1].into()));
612    let aabb_pixel = tile_to_pixel_aabb (aabb);
613    assert_eq!(*aabb_pixel.min(), [0, 92].into());
614    assert_eq!(*aabb_pixel.max(), [8, 100].into());
615  }
616}