Skip to main content

ravenrs_core/
geometry.rs

1//! Geometry in logical pixels. Conversion to physical pixels happens only in the
2//! renderer and platform layers, using the window's scale factor.
3
4use std::ops::{Add, Sub};
5
6/// A point in logical pixels, origin at the top-left corner.
7#[derive(Clone, Copy, Debug, Default, PartialEq)]
8#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
9pub struct Point {
10    /// Horizontal coordinate in logical pixels.
11    pub x: f32,
12    /// Vertical coordinate in logical pixels, growing downward.
13    pub y: f32,
14}
15
16#[cfg_attr(feature = "live", ravenrs_live::expose)]
17impl Point {
18    /// The origin, `(0, 0)`.
19    pub const ZERO: Self = Self { x: 0.0, y: 0.0 };
20
21    /// Creates a point from logical-pixel coordinates.
22    pub const fn new(x: f32, y: f32) -> Self {
23        Self { x, y }
24    }
25}
26
27impl Add for Point {
28    type Output = Self;
29    fn add(self, rhs: Self) -> Self {
30        Self::new(self.x + rhs.x, self.y + rhs.y)
31    }
32}
33
34impl Sub for Point {
35    type Output = Self;
36    fn sub(self, rhs: Self) -> Self {
37        Self::new(self.x - rhs.x, self.y - rhs.y)
38    }
39}
40
41/// A size in logical pixels.
42#[derive(Clone, Copy, Debug, Default, PartialEq)]
43#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
44pub struct Size {
45    /// Width in logical pixels.
46    pub width: f32,
47    /// Height in logical pixels.
48    pub height: f32,
49}
50
51#[cfg_attr(feature = "live", ravenrs_live::expose)]
52impl Size {
53    /// A zero-area size.
54    pub const ZERO: Self = Self { width: 0.0, height: 0.0 };
55
56    /// Creates a size from logical-pixel dimensions.
57    pub const fn new(width: f32, height: f32) -> Self {
58        Self { width, height }
59    }
60
61    /// Scales both dimensions, e.g. logical to physical pixels.
62    pub fn scale(self, factor: f32) -> Self {
63        Self::new(self.width * factor, self.height * factor)
64    }
65}
66
67/// An axis-aligned rectangle in logical pixels.
68#[derive(Clone, Copy, Debug, Default, PartialEq)]
69#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
70pub struct Rect {
71    /// Top-left corner.
72    pub origin: Point,
73    /// Width and height.
74    pub size: Size,
75}
76
77#[cfg_attr(feature = "live", ravenrs_live::expose)]
78impl Rect {
79    /// An empty rectangle at the origin.
80    pub const ZERO: Self = Self { origin: Point::ZERO, size: Size::ZERO };
81
82    /// Creates a rectangle from its top-left corner and dimensions, in logical pixels.
83    pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
84        Self { origin: Point::new(x, y), size: Size::new(width, height) }
85    }
86
87    /// Creates a rectangle from a top-left corner and a size.
88    pub const fn from_origin_size(origin: Point, size: Size) -> Self {
89        Self { origin, size }
90    }
91
92    /// Left edge.
93    pub fn x(&self) -> f32 {
94        self.origin.x
95    }
96
97    /// Top edge.
98    pub fn y(&self) -> f32 {
99        self.origin.y
100    }
101
102    /// Horizontal extent.
103    pub fn width(&self) -> f32 {
104        self.size.width
105    }
106
107    /// Vertical extent.
108    pub fn height(&self) -> f32 {
109        self.size.height
110    }
111
112    /// Right edge (`x + width`).
113    pub fn max_x(&self) -> f32 {
114        self.origin.x + self.size.width
115    }
116
117    /// Bottom edge (`y + height`).
118    pub fn max_y(&self) -> f32 {
119        self.origin.y + self.size.height
120    }
121
122    /// Center point.
123    pub fn center(&self) -> Point {
124        Point::new(self.origin.x + self.size.width * 0.5, self.origin.y + self.size.height * 0.5)
125    }
126
127    /// Half-open containment: the left/top edges are inside, the right/bottom edges are not.
128    pub fn contains(&self, p: Point) -> bool {
129        p.x >= self.origin.x && p.x < self.max_x() && p.y >= self.origin.y && p.y < self.max_y()
130    }
131
132    /// Grows the rectangle by `amount` on every side (shrinks for negative values).
133    pub fn inflate(&self, amount: f32) -> Self {
134        Self::new(
135            self.origin.x - amount,
136            self.origin.y - amount,
137            (self.size.width + amount * 2.0).max(0.0),
138            (self.size.height + amount * 2.0).max(0.0),
139        )
140    }
141
142    /// The overlapping area, or an empty rectangle at the origin of `self` when
143    /// the two do not overlap.
144    pub fn intersect(&self, other: &Rect) -> Self {
145        let x0 = self.x().max(other.x());
146        let y0 = self.y().max(other.y());
147        let x1 = self.max_x().min(other.max_x());
148        let y1 = self.max_y().min(other.max_y());
149        if x1 <= x0 || y1 <= y0 {
150            return Self::from_origin_size(self.origin, Size::ZERO);
151        }
152        Self::new(x0, y0, x1 - x0, y1 - y0)
153    }
154
155    /// Returns `true` when the width or height is zero or negative.
156    pub fn is_empty(&self) -> bool {
157        self.size.width <= 0.0 || self.size.height <= 0.0
158    }
159
160    /// Returns the rectangle moved by `offset`, keeping its size.
161    pub fn translate(&self, offset: Point) -> Self {
162        Self::from_origin_size(self.origin + offset, self.size)
163    }
164}
165
166/// Distances from each edge, used for padding.
167#[derive(Clone, Copy, Debug, Default, PartialEq)]
168#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
169pub struct Insets {
170    /// Top inset in logical pixels.
171    pub top: f32,
172    /// Right inset in logical pixels.
173    pub right: f32,
174    /// Bottom inset in logical pixels.
175    pub bottom: f32,
176    /// Left inset in logical pixels.
177    pub left: f32,
178}
179
180#[cfg_attr(feature = "live", ravenrs_live::expose)]
181impl Insets {
182    /// No inset on any side.
183    pub const ZERO: Self = Self::uniform(0.0);
184
185    /// Same inset `v` on all four sides.
186    pub const fn uniform(v: f32) -> Self {
187        Self { top: v, right: v, bottom: v, left: v }
188    }
189
190    /// `vertical` on top and bottom, `horizontal` on left and right.
191    pub const fn symmetric(vertical: f32, horizontal: f32) -> Self {
192        Self { top: vertical, right: horizontal, bottom: vertical, left: horizontal }
193    }
194}
195
196/// `16.0`, `(8.0, 12.0)` or an `Insets` already made, in the playground's interpreter.
197#[cfg(feature = "live")]
198impl ravenrs_live::FromLive for Insets {
199    fn from_live(value: ravenrs_live::Value) -> Result<Self, String> {
200        match value.read() {
201            ravenrs_live::Value::Host(host) => host.take_as::<Insets>(),
202            ravenrs_live::Value::Tuple(items) if items.len() == 2 => {
203                let (a, b) =
204                    <(f32, f32) as ravenrs_live::FromLive>::from_live(ravenrs_live::Value::Tuple(items))?;
205                Ok(Self::from((a, b)))
206            }
207            other => <f32 as ravenrs_live::FromLive>::from_live(other).map(Self::from),
208        }
209    }
210}
211
212impl From<f32> for Insets {
213    fn from(v: f32) -> Self {
214        Self::uniform(v)
215    }
216}
217
218/// `(vertical, horizontal)`, like the two-value CSS `padding`.
219impl From<(f32, f32)> for Insets {
220    fn from((vertical, horizontal): (f32, f32)) -> Self {
221        Self::symmetric(vertical, horizontal)
222    }
223}
224
225/// The slope at each of `points` (in order, with growing `x`) of a curve that never swings
226/// past them: Fritsch-Carlson, with a harmonic mean. Charts and curve editors share it so
227/// that a curve reads the same in both. Fewer than two points have no slope to speak of, so
228/// they get zeros.
229pub fn monotone_slopes(points: &[Point]) -> Vec<f32> {
230    let secant = |a: Point, b: Point| {
231        let run = b.x - a.x;
232        if run.abs() < f32::EPSILON { 0.0 } else { (b.y - a.y) / run }
233    };
234    let n = points.len();
235    let mut slopes = vec![0.0; n];
236    if n < 2 {
237        return slopes;
238    }
239    slopes[0] = secant(points[0], points[1]);
240    slopes[n - 1] = secant(points[n - 2], points[n - 1]);
241    for i in 1..n - 1 {
242        let (before, after) = (secant(points[i - 1], points[i]), secant(points[i], points[i + 1]));
243        slopes[i] = if before * after <= 0.0 { 0.0 } else { 2.0 * before * after / (before + after) };
244    }
245    slopes
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn monotone_slopes_flatten_at_a_turn_and_keep_a_straight_line() {
254        let line = [Point::new(0.0, 0.0), Point::new(10.0, 5.0), Point::new(20.0, 10.0)];
255        assert_eq!(monotone_slopes(&line), [0.5, 0.5, 0.5]);
256        let peak = [Point::new(0.0, 0.0), Point::new(10.0, 10.0), Point::new(20.0, 0.0)];
257        assert_eq!(monotone_slopes(&peak), [1.0, 0.0, -1.0]);
258        let uneven = [Point::new(0.0, 0.0), Point::new(10.0, 10.0), Point::new(30.0, 20.0)];
259        assert_eq!(monotone_slopes(&uneven)[1], 2.0 * 1.0 * 0.5 / 1.5);
260        assert_eq!(monotone_slopes(&[Point::ZERO]), [0.0]);
261        assert!(monotone_slopes(&[]).is_empty());
262    }
263
264    #[test]
265    fn contains_is_half_open() {
266        let r = Rect::new(10.0, 10.0, 20.0, 10.0);
267        assert!(r.contains(Point::new(10.0, 10.0)));
268        assert!(r.contains(Point::new(29.9, 19.9)));
269        assert!(!r.contains(Point::new(30.0, 15.0)));
270        assert!(!r.contains(Point::new(15.0, 20.0)));
271    }
272
273    #[test]
274    fn intersection() {
275        let a = Rect::new(0.0, 0.0, 10.0, 10.0);
276        assert_eq!(a.intersect(&Rect::new(5.0, 5.0, 10.0, 10.0)), Rect::new(5.0, 5.0, 5.0, 5.0));
277        assert!(a.intersect(&Rect::new(20.0, 0.0, 5.0, 5.0)).is_empty());
278    }
279
280    #[test]
281    fn inflate_never_goes_negative() {
282        let r = Rect::new(0.0, 0.0, 4.0, 4.0).inflate(-5.0);
283        assert_eq!(r.size, Size::ZERO);
284        let g = Rect::new(10.0, 10.0, 4.0, 4.0).inflate(3.0);
285        assert_eq!(g, Rect::new(7.0, 7.0, 10.0, 10.0));
286    }
287}