Skip to main content

vello_common/
geometry.rs

1// Copyright 2025 the Vello Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Geometry utilities.
5
6use crate::kurbo::Rect;
7use bytemuck::{Pod, Zeroable};
8use core::ops::Add;
9
10/// A size represented by two 16-bit unsigned integers.
11#[repr(C)]
12#[derive(Copy, Clone, Debug, Pod, Zeroable, PartialEq, Eq)]
13pub struct SizeU16(pub [u16; 2]);
14
15impl SizeU16 {
16    /// A zero size.
17    pub const ZERO: Self = Self::new(0);
18
19    /// Create a new square size.
20    pub const fn new(size: u16) -> Self {
21        Self([size; 2])
22    }
23
24    /// Create a new size from its width and height.
25    pub const fn from_wh(width: u16, height: u16) -> Self {
26        Self([width, height])
27    }
28
29    /// The width of this size.
30    pub const fn width(self) -> u16 {
31        self.0[0]
32    }
33
34    /// The height of this size.
35    pub const fn height(self) -> u16 {
36        self.0[1]
37    }
38
39    /// Return the maximum of the two sizes.
40    pub fn max(self, other: Self) -> Self {
41        Self::from_wh(
42            self.width().max(other.width()),
43            self.height().max(other.height()),
44        )
45    }
46
47    /// Return the minimum of the two sizes.
48    pub fn min(self, other: Self) -> Self {
49        Self::from_wh(
50            self.width().min(other.width()),
51            self.height().min(other.height()),
52        )
53    }
54
55    /// Clamp both dimensions to the given range.
56    pub fn clamp(self, min: u16, max: u16) -> Self {
57        Self::from_wh(self.width().clamp(min, max), self.height().clamp(min, max))
58    }
59
60    /// Add the same value to both dimensions, returning `None` on overflow.
61    pub fn checked_add(self, value: u16) -> Option<Self> {
62        Some(Self::from_wh(
63            self.width().checked_add(value)?,
64            self.height().checked_add(value)?,
65        ))
66    }
67}
68
69impl From<[u16; 2]> for SizeU16 {
70    fn from(value: [u16; 2]) -> Self {
71        Self(value)
72    }
73}
74
75impl From<(u16, u16)> for SizeU16 {
76    fn from((width, height): (u16, u16)) -> Self {
77        Self::from_wh(width, height)
78    }
79}
80
81impl From<SizeU16> for (u16, u16) {
82    fn from(size: SizeU16) -> Self {
83        (size.width(), size.height())
84    }
85}
86
87impl Add for SizeU16 {
88    type Output = Self;
89
90    fn add(self, rhs: Self) -> Self::Output {
91        // Shouldn't overflow for our use cases.
92        Self::from_wh(
93            self.width().checked_add(rhs.width()).unwrap(),
94            self.height().checked_add(rhs.height()).unwrap(),
95        )
96    }
97}
98
99impl Add<u16> for SizeU16 {
100    type Output = Self;
101
102    fn add(self, rhs: u16) -> Self::Output {
103        self + Self::new(rhs)
104    }
105}
106
107/// Padding for the four sides of a region.
108#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
109pub struct PaddingU16 {
110    /// The left padding.
111    pub left: u16,
112    /// The top padding.
113    pub top: u16,
114    /// The right padding.
115    pub right: u16,
116    /// The bottom padding.
117    pub bottom: u16,
118}
119
120impl PaddingU16 {
121    /// Padding with all sides set to zero.
122    pub const ZERO: Self = Self::new(0, 0, 0, 0);
123
124    /// Create padding from its left, top, right, and bottom amounts.
125    pub const fn new(left: u16, top: u16, right: u16, bottom: u16) -> Self {
126        Self {
127            left,
128            top,
129            right,
130            bottom,
131        }
132    }
133}
134
135/// An axis-aligned rectangle with `u16` coordinates, stored as two corners `(x0, y0)` and
136/// `(x1, y1)`.
137///
138/// `(x0, y0)` is the top-left (minimum) corner and `(x1, y1)` is the bottom-right (maximum) corner.
139/// The rectangle is considered to be empty when `x0 >= x1` or `y0 >= y1`.
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
141pub struct RectU16 {
142    /// The minimum x coordinate (left edge).
143    pub x0: u16,
144    /// The minimum y coordinate (top edge).
145    pub y0: u16,
146    /// The maximum x coordinate (right edge, exclusive).
147    pub x1: u16,
148    /// The maximum y coordinate (bottom edge, exclusive).
149    pub y1: u16,
150}
151
152impl RectU16 {
153    /// A rectangle with all coordinates set to zero.
154    pub const ZERO: Self = Self {
155        x0: 0,
156        y0: 0,
157        x1: 0,
158        y1: 0,
159    };
160
161    /// An empty, maximally inverted rectangle, useful as a starting value for incremental union
162    /// operations.
163    ///
164    /// Has `(x0, y0) = (u16::MAX, u16::MAX)` and `(x1, y1) = (0, 0)`.
165    pub const INVERTED: Self = Self {
166        x0: u16::MAX,
167        y0: u16::MAX,
168        x1: 0,
169        y1: 0,
170    };
171
172    /// Create a new rectangle from its corner coordinates.
173    #[inline(always)]
174    pub const fn new(x0: u16, y0: u16, x1: u16, y1: u16) -> Self {
175        Self { x0, y0, x1, y1 }
176    }
177
178    /// The width of the rectangle (`x1 - x0`), saturating at zero.
179    #[inline(always)]
180    pub const fn width(self) -> u16 {
181        self.x1.saturating_sub(self.x0)
182    }
183
184    /// The height of the rectangle (`y1 - y0`), saturating at zero.
185    #[inline(always)]
186    pub const fn height(self) -> u16 {
187        self.y1.saturating_sub(self.y0)
188    }
189
190    /// Returns `true` if the rectangle has zero area (`x0 >= x1` or `y0 >= y1`).
191    #[inline(always)]
192    pub const fn is_empty(self) -> bool {
193        self.x0 >= self.x1 || self.y0 >= self.y1
194    }
195
196    /// Check if a point `(x, y)` is contained within this rectangle.
197    ///
198    /// Returns `true` if `x0 <= x < x1` and `y0 <= y < y1`.
199    #[inline(always)]
200    pub const fn contains(self, x: u16, y: u16) -> bool {
201        (x >= self.x0) & (x < self.x1) & (y >= self.y0) & (y < self.y1)
202    }
203
204    /// Compute the intersection of two rectangles.
205    ///
206    /// The result may have zero area if the rectangles do not overlap, but is never inverted.
207    #[inline(always)]
208    pub const fn intersect(self, other: Self) -> Self {
209        let x0 = const_max(self.x0, other.x0);
210        let y0 = const_max(self.y0, other.y0);
211        let x1 = const_min(self.x1, other.x1);
212        let y1 = const_min(self.y1, other.y1);
213
214        Self::new(x0, y0, const_max(x1, x0), const_max(y1, y0))
215    }
216
217    /// Expand this rectangle by the given left, top, right, and bottom padding.
218    #[inline(always)]
219    pub const fn expand(self, padding: PaddingU16) -> Self {
220        Self {
221            x0: self.x0.saturating_sub(padding.left),
222            y0: self.y0.saturating_sub(padding.top),
223            x1: self.x1.saturating_add(padding.right),
224            y1: self.y1.saturating_add(padding.bottom),
225        }
226    }
227
228    /// Return this rectangle relative to `origin`, clamping negative coordinates to zero.
229    #[inline(always)]
230    pub fn relative_to_origin(self, origin: (u16, u16)) -> Self {
231        self.shift((-(origin.0 as i32), -(origin.1 as i32)))
232    }
233
234    /// Return a shifted version of the rectangle, clamping negative coordinates to zero.
235    #[inline]
236    pub fn shift(self, shift: (i32, i32)) -> Self {
237        Self {
238            x0: (self.x0 as i32)
239                .saturating_add(shift.0)
240                .clamp(0, u16::MAX as i32) as u16,
241            y0: (self.y0 as i32)
242                .saturating_add(shift.1)
243                .clamp(0, u16::MAX as i32) as u16,
244            x1: (self.x1 as i32)
245                .saturating_add(shift.0)
246                .clamp(0, u16::MAX as i32) as u16,
247            y1: (self.y1 as i32)
248                .saturating_add(shift.1)
249                .clamp(0, u16::MAX as i32) as u16,
250        }
251    }
252
253    /// Expand this rectangle to also cover `other` (union in place).
254    ///
255    /// The union of `self` with a [`Self::INVERTED`] returns `self`.
256    #[inline(always)]
257    pub const fn union(&mut self, other: Self) {
258        self.x0 = const_min(self.x0, other.x0);
259        self.y0 = const_min(self.y0, other.y0);
260        self.x1 = const_max(self.x1, other.x1);
261        self.y1 = const_max(self.y1, other.y1);
262    }
263
264    /// Return the rect as a [`Rect`].
265    pub fn as_rect(self) -> Rect {
266        Rect::new(
267            self.x0 as f64,
268            self.y0 as f64,
269            self.x1 as f64,
270            self.y1 as f64,
271        )
272    }
273}
274
275impl From<RectU16> for SizeU16 {
276    fn from(rect: RectU16) -> Self {
277        Self::from_wh(rect.width(), rect.height())
278    }
279}
280
281#[inline(always)]
282const fn const_max(a: u16, b: u16) -> u16 {
283    if a > b { a } else { b }
284}
285
286#[inline(always)]
287const fn const_min(a: u16, b: u16) -> u16 {
288    if a < b { a } else { b }
289}
290
291#[cfg(test)]
292mod tests {
293    use super::RectU16;
294
295    #[test]
296    fn rect_u16_relative_to_origin() {
297        let rect = RectU16::new(10, 20, 30, 40);
298
299        assert_eq!(rect.relative_to_origin((5, 12)), RectU16::new(5, 8, 25, 28));
300    }
301
302    #[test]
303    fn rect_u16_relative_to_origin_clamps_to_zero() {
304        let rect = RectU16::new(10, 20, 30, 40);
305
306        assert_eq!(rect.relative_to_origin((20, 35)), RectU16::new(0, 0, 10, 5));
307    }
308
309    #[test]
310    fn disjoint_intersection_is_empty_but_not_inverted() {
311        let intersection = RectU16::new(0, 0, 4, 4).intersect(RectU16::new(8, 1, 12, 3));
312
313        assert_eq!(intersection, RectU16::new(8, 1, 8, 3));
314        assert!(intersection.is_empty());
315        assert!(intersection.x0 <= intersection.x1);
316        assert!(intersection.y0 <= intersection.y1);
317    }
318}