Skip to main content

gpui_liveplot/
transform.rs

1//! Coordinate transforms between data and screen space.
2use crate::geom::{Point, ScreenPoint, ScreenRect};
3use crate::view::{Range, Viewport};
4
5const MIN_SPAN: f64 = 1e-12;
6
7/// Transform from data coordinates into screen coordinates.
8#[derive(Debug, Clone)]
9pub(crate) struct Transform {
10    viewport: Viewport,
11    screen: ScreenRect,
12    x_axis: Range,
13    y_axis: Range,
14}
15
16impl Transform {
17    /// Create a transform for the given viewport and screen rectangle.
18    pub(crate) fn new(viewport: Viewport, screen: ScreenRect) -> Option<Self> {
19        if !screen.is_valid() {
20            return None;
21        }
22        let x_axis = map_range(viewport.x.with_min_span(MIN_SPAN))?;
23        let y_axis = map_range(viewport.y.with_min_span(MIN_SPAN))?;
24        Some(Self {
25            viewport,
26            screen,
27            x_axis,
28            y_axis,
29        })
30    }
31
32    /// Access the viewport.
33    pub(crate) fn viewport(&self) -> Viewport {
34        self.viewport
35    }
36
37    /// Access the screen rectangle.
38    pub(crate) fn screen(&self) -> ScreenRect {
39        self.screen
40    }
41
42    /// Map a data point into screen space.
43    pub(crate) fn data_to_screen(&self, point: Point) -> Option<ScreenPoint> {
44        if !point.x.is_finite() || !point.y.is_finite() {
45            return None;
46        }
47        let x_norm = (point.x - self.x_axis.min) / self.x_axis.span();
48        let y_norm = (point.y - self.y_axis.min) / self.y_axis.span();
49        let sx = self.screen.min.x as f64 + x_norm * self.screen.width() as f64;
50        let sy = self.screen.max.y as f64 - y_norm * self.screen.height() as f64;
51        Some(ScreenPoint::new(sx as f32, sy as f32))
52    }
53
54    /// Map a screen point into data space.
55    pub(crate) fn screen_to_data(&self, point: ScreenPoint) -> Option<Point> {
56        let x_norm = (point.x as f64 - self.screen.min.x as f64) / self.screen.width() as f64;
57        let y_norm = (self.screen.max.y as f64 - point.y as f64) / self.screen.height() as f64;
58        let x_axis = self.x_axis.min + x_norm * self.x_axis.span();
59        let y_axis = self.y_axis.min + y_norm * self.y_axis.span();
60        Some(Point::new(x_axis, y_axis))
61    }
62}
63
64fn map_range(range: Range) -> Option<Range> {
65    if !range.is_finite() {
66        return None;
67    }
68    Some(range)
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn linear_roundtrip() {
77        let viewport = Viewport::new(Range::new(0.0, 10.0), Range::new(0.0, 10.0));
78        let screen = ScreenRect::new(ScreenPoint::new(0.0, 0.0), ScreenPoint::new(100.0, 100.0));
79        let transform = Transform::new(viewport, screen).expect("valid transform");
80        let point = Point::new(5.0, 7.5);
81        let screen_point = transform.data_to_screen(point).unwrap();
82        let roundtrip = transform.screen_to_data(screen_point).unwrap();
83        assert!((roundtrip.x - point.x).abs() < 1e-9);
84        assert!((roundtrip.y - point.y).abs() < 1e-9);
85    }
86}