maelstrom_plot/
transform.rs

1use crate::PlotPoint;
2use egui::{pos2, remap, Pos2, Rect, Vec2};
3use std::ops::RangeInclusive;
4
5/// 2D bounding box of f64 precision.
6/// The range of data values we show.
7#[derive(Clone, Copy, PartialEq, Debug)]
8pub struct PlotBounds {
9    pub(crate) min: [f64; 2],
10    pub(crate) max: [f64; 2],
11}
12
13impl PlotBounds {
14    pub const NOTHING: Self = Self {
15        min: [f64::INFINITY; 2],
16        max: [-f64::INFINITY; 2],
17    };
18
19    pub fn from_min_max(min: [f64; 2], max: [f64; 2]) -> Self {
20        Self { min, max }
21    }
22
23    pub fn min(&self) -> [f64; 2] {
24        self.min
25    }
26
27    pub fn max(&self) -> [f64; 2] {
28        self.max
29    }
30
31    pub(crate) fn new_symmetrical(half_extent: f64) -> Self {
32        Self {
33            min: [-half_extent; 2],
34            max: [half_extent; 2],
35        }
36    }
37
38    pub fn is_finite(&self) -> bool {
39        self.min[0].is_finite()
40            && self.min[1].is_finite()
41            && self.max[0].is_finite()
42            && self.max[1].is_finite()
43    }
44
45    pub fn is_finite_x(&self) -> bool {
46        self.min[0].is_finite() && self.max[0].is_finite()
47    }
48
49    pub fn is_finite_y(&self) -> bool {
50        self.min[1].is_finite() && self.max[1].is_finite()
51    }
52
53    pub fn is_valid(&self) -> bool {
54        self.is_finite() && self.width() > 0.0 && self.height() > 0.0
55    }
56
57    pub fn is_valid_x(&self) -> bool {
58        self.is_finite_x() && self.width() > 0.0
59    }
60
61    pub fn is_valid_y(&self) -> bool {
62        self.is_finite_y() && self.height() > 0.0
63    }
64
65    pub fn width(&self) -> f64 {
66        self.max[0] - self.min[0]
67    }
68
69    pub fn height(&self) -> f64 {
70        self.max[1] - self.min[1]
71    }
72
73    pub fn center(&self) -> PlotPoint {
74        [
75            (self.min[0] + self.max[0]) / 2.0,
76            (self.min[1] + self.max[1]) / 2.0,
77        ]
78        .into()
79    }
80
81    /// Expand to include the given (x,y) value
82    pub(crate) fn extend_with(&mut self, value: &PlotPoint) {
83        self.extend_with_x(value.x);
84        self.extend_with_y(value.y);
85    }
86
87    /// Expand to include the given x coordinate
88    pub(crate) fn extend_with_x(&mut self, x: f64) {
89        self.min[0] = self.min[0].min(x);
90        self.max[0] = self.max[0].max(x);
91    }
92
93    /// Expand to include the given y coordinate
94    pub(crate) fn extend_with_y(&mut self, y: f64) {
95        self.min[1] = self.min[1].min(y);
96        self.max[1] = self.max[1].max(y);
97    }
98
99    pub(crate) fn expand_x(&mut self, pad: f64) {
100        self.min[0] -= pad;
101        self.max[0] += pad;
102    }
103
104    pub(crate) fn expand_y(&mut self, pad: f64) {
105        self.min[1] -= pad;
106        self.max[1] += pad;
107    }
108
109    pub(crate) fn merge_x(&mut self, other: &PlotBounds) {
110        self.min[0] = self.min[0].min(other.min[0]);
111        self.max[0] = self.max[0].max(other.max[0]);
112    }
113
114    pub(crate) fn merge_y(&mut self, other: &PlotBounds) {
115        self.min[1] = self.min[1].min(other.min[1]);
116        self.max[1] = self.max[1].max(other.max[1]);
117    }
118
119    pub(crate) fn set_x(&mut self, other: &PlotBounds) {
120        self.min[0] = other.min[0];
121        self.max[0] = other.max[0];
122    }
123
124    pub(crate) fn set_y(&mut self, other: &PlotBounds) {
125        self.min[1] = other.min[1];
126        self.max[1] = other.max[1];
127    }
128
129    pub(crate) fn translate_x(&mut self, delta: f64) {
130        self.min[0] += delta;
131        self.max[0] += delta;
132    }
133
134    pub(crate) fn translate_y(&mut self, delta: f64) {
135        self.min[1] += delta;
136        self.max[1] += delta;
137    }
138
139    pub(crate) fn translate(&mut self, delta: Vec2) {
140        self.translate_x(delta.x as f64);
141        self.translate_y(delta.y as f64);
142    }
143
144    pub(crate) fn add_relative_margin_x(&mut self, margin_fraction: Vec2) {
145        let width = self.width().max(0.0);
146        self.expand_x(margin_fraction.x as f64 * width);
147    }
148
149    pub(crate) fn add_relative_margin_y(&mut self, margin_fraction: Vec2) {
150        let height = self.height().max(0.0);
151        self.expand_y(margin_fraction.y as f64 * height);
152    }
153
154    pub(crate) fn range_x(&self) -> RangeInclusive<f64> {
155        self.min[0]..=self.max[0]
156    }
157
158    pub(crate) fn range_y(&self) -> RangeInclusive<f64> {
159        self.min[1]..=self.max[1]
160    }
161
162    pub(crate) fn make_x_symmetrical(&mut self) {
163        let x_abs = self.min[0].abs().max(self.max[0].abs());
164        self.min[0] = -x_abs;
165        self.max[0] = x_abs;
166    }
167
168    pub(crate) fn make_y_symmetrical(&mut self) {
169        let y_abs = self.min[1].abs().max(self.max[1].abs());
170        self.min[1] = -y_abs;
171        self.max[1] = y_abs;
172    }
173}
174
175/// Contains the screen rectangle and the plot bounds and provides methods to transform between them.
176#[derive(Clone, Copy, Debug)]
177pub struct PlotTransform {
178    /// The screen rectangle.
179    frame: Rect,
180
181    /// The plot bounds.
182    bounds: PlotBounds,
183
184    /// Whether to always center the x-range of the bounds.
185    x_centered: bool,
186
187    /// Whether to always center the y-range of the bounds.
188    y_centered: bool,
189}
190
191impl PlotTransform {
192    pub fn new(frame: Rect, mut bounds: PlotBounds, x_centered: bool, y_centered: bool) -> Self {
193        // Make sure they are not empty.
194        if !bounds.is_valid_x() {
195            bounds.set_x(&PlotBounds::new_symmetrical(1.0));
196        }
197        if !bounds.is_valid_y() {
198            bounds.set_y(&PlotBounds::new_symmetrical(1.0));
199        }
200
201        // Scale axes so that the origin is in the center.
202        if x_centered {
203            bounds.make_x_symmetrical();
204        };
205        if y_centered {
206            bounds.make_y_symmetrical();
207        };
208
209        Self {
210            frame,
211            bounds,
212            x_centered,
213            y_centered,
214        }
215    }
216
217    /// ui-space rectangle.
218    pub fn frame(&self) -> &Rect {
219        &self.frame
220    }
221
222    /// Plot-space bounds.
223    pub fn bounds(&self) -> &PlotBounds {
224        &self.bounds
225    }
226
227    pub(crate) fn set_bounds(&mut self, bounds: PlotBounds) {
228        self.bounds = bounds;
229    }
230
231    pub(crate) fn translate_bounds(&mut self, mut delta_pos: Vec2) {
232        if self.x_centered {
233            delta_pos.x = 0.;
234        }
235        if self.y_centered {
236            delta_pos.y = 0.;
237        }
238        delta_pos.x *= self.dvalue_dpos()[0] as f32;
239        delta_pos.y *= self.dvalue_dpos()[1] as f32;
240        self.bounds.translate(delta_pos);
241    }
242
243    /// Zoom by a relative factor with the given screen position as center.
244    pub(crate) fn zoom(&mut self, zoom_factor: Vec2, center: Pos2) {
245        let center = self.value_from_position(center);
246
247        let mut new_bounds = self.bounds;
248        new_bounds.min[0] = center.x + (new_bounds.min[0] - center.x) / (zoom_factor.x as f64);
249        new_bounds.max[0] = center.x + (new_bounds.max[0] - center.x) / (zoom_factor.x as f64);
250        new_bounds.min[1] = center.y + (new_bounds.min[1] - center.y) / (zoom_factor.y as f64);
251        new_bounds.max[1] = center.y + (new_bounds.max[1] - center.y) / (zoom_factor.y as f64);
252
253        if new_bounds.is_valid() {
254            self.bounds = new_bounds;
255        }
256    }
257
258    pub fn position_from_point_x(&self, value: f64) -> f32 {
259        remap(
260            value,
261            self.bounds.min[0]..=self.bounds.max[0],
262            (self.frame.left() as f64)..=(self.frame.right() as f64),
263        ) as f32
264    }
265
266    pub fn position_from_point_y(&self, value: f64) -> f32 {
267        remap(
268            value,
269            self.bounds.min[1]..=self.bounds.max[1],
270            (self.frame.bottom() as f64)..=(self.frame.top() as f64), // negated y axis!
271        ) as f32
272    }
273
274    /// Screen/ui position from point on plot.
275    pub fn position_from_point(&self, value: &PlotPoint) -> Pos2 {
276        pos2(
277            self.position_from_point_x(value.x),
278            self.position_from_point_y(value.y),
279        )
280    }
281
282    /// Plot point from screen/ui position.
283    pub fn value_from_position(&self, pos: Pos2) -> PlotPoint {
284        let x = remap(
285            pos.x as f64,
286            (self.frame.left() as f64)..=(self.frame.right() as f64),
287            self.bounds.min[0]..=self.bounds.max[0],
288        );
289        let y = remap(
290            pos.y as f64,
291            (self.frame.bottom() as f64)..=(self.frame.top() as f64), // negated y axis!
292            self.bounds.min[1]..=self.bounds.max[1],
293        );
294        PlotPoint::new(x, y)
295    }
296
297    /// Transform a rectangle of plot values to a screen-coordinate rectangle.
298    ///
299    /// This typically means that the rect is mirrored vertically (top becomes bottom and vice versa),
300    /// since the plot's coordinate system has +Y up, while egui has +Y down.
301    pub fn rect_from_values(&self, value1: &PlotPoint, value2: &PlotPoint) -> Rect {
302        let pos1 = self.position_from_point(value1);
303        let pos2 = self.position_from_point(value2);
304
305        let mut rect = Rect::NOTHING;
306        rect.extend_with(pos1);
307        rect.extend_with(pos2);
308        rect
309    }
310
311    /// delta position / delta value
312    pub fn dpos_dvalue_x(&self) -> f64 {
313        self.frame.width() as f64 / self.bounds.width()
314    }
315
316    /// delta position / delta value
317    pub fn dpos_dvalue_y(&self) -> f64 {
318        -self.frame.height() as f64 / self.bounds.height() // negated y axis!
319    }
320
321    /// delta position / delta value
322    pub fn dpos_dvalue(&self) -> [f64; 2] {
323        [self.dpos_dvalue_x(), self.dpos_dvalue_y()]
324    }
325
326    /// delta value / delta position
327    pub fn dvalue_dpos(&self) -> [f64; 2] {
328        [1.0 / self.dpos_dvalue_x(), 1.0 / self.dpos_dvalue_y()]
329    }
330
331    /// width / height aspect ratio
332    fn aspect(&self) -> f64 {
333        let rw = self.frame.width() as f64;
334        let rh = self.frame.height() as f64;
335        (self.bounds.width() / rw) / (self.bounds.height() / rh)
336    }
337
338    /// Sets the aspect ratio by expanding the x- or y-axis.
339    ///
340    /// This never contracts, so we don't miss out on any data.
341    pub(crate) fn set_aspect_by_expanding(&mut self, aspect: f64) {
342        let current_aspect = self.aspect();
343
344        let epsilon = 1e-5;
345        if (current_aspect - aspect).abs() < epsilon {
346            // Don't make any changes when the aspect is already almost correct.
347            return;
348        }
349
350        if current_aspect < aspect {
351            self.bounds
352                .expand_x((aspect / current_aspect - 1.0) * self.bounds.width() * 0.5);
353        } else {
354            self.bounds
355                .expand_y((current_aspect / aspect - 1.0) * self.bounds.height() * 0.5);
356        }
357    }
358
359    /// Sets the aspect ratio by changing either the X or Y axis (callers choice).
360    pub(crate) fn set_aspect_by_changing_axis(&mut self, aspect: f64, change_x: bool) {
361        let current_aspect = self.aspect();
362
363        let epsilon = 1e-5;
364        if (current_aspect - aspect).abs() < epsilon {
365            // Don't make any changes when the aspect is already almost correct.
366            return;
367        }
368
369        if change_x {
370            self.bounds
371                .expand_x((aspect / current_aspect - 1.0) * self.bounds.width() * 0.5);
372        } else {
373            self.bounds
374                .expand_y((current_aspect / aspect - 1.0) * self.bounds.height() * 0.5);
375        }
376    }
377}