| // Copyright 2025 the Vello Authors | |
| // SPDX-License-Identifier: Apache-2.0 OR MIT | |
| //! Geometry utilities. | |
| use crate::kurbo::Rect; | |
| use bytemuck::{Pod, Zeroable}; | |
| use core::ops::Add; | |
| /// A size represented by two 16-bit unsigned integers. | |
| #[repr(C)] | |
| #[derive(Copy, Clone, Debug, Pod, Zeroable, PartialEq, Eq)] | |
| pub struct SizeU16(pub [u16; 2]); | |
| impl SizeU16 { | |
| /// A zero size. | |
| pub const ZERO: Self = Self::new(0); | |
| /// Create a new square size. | |
| pub const fn new(size: u16) -> Self { | |
| Self([size; 2]) | |
| } | |
| /// Create a new size from its width and height. | |
| pub const fn from_wh(width: u16, height: u16) -> Self { | |
| Self([width, height]) | |
| } | |
| /// The width of this size. | |
| pub const fn width(self) -> u16 { | |
| self.0[0] | |
| } | |
| /// The height of this size. | |
| pub const fn height(self) -> u16 { | |
| self.0[1] | |
| } | |
| /// Return the maximum of the two sizes. | |
| pub fn max(self, other: Self) -> Self { | |
| Self::from_wh( | |
| self.width().max(other.width()), | |
| self.height().max(other.height()), | |
| ) | |
| } | |
| /// Return the minimum of the two sizes. | |
| pub fn min(self, other: Self) -> Self { | |
| Self::from_wh( | |
| self.width().min(other.width()), | |
| self.height().min(other.height()), | |
| ) | |
| } | |
| /// Clamp both dimensions to the given range. | |
| pub fn clamp(self, min: u16, max: u16) -> Self { | |
| Self::from_wh(self.width().clamp(min, max), self.height().clamp(min, max)) | |
| } | |
| /// Add the same value to both dimensions, returning `None` on overflow. | |
| pub fn checked_add(self, value: u16) -> Option<Self> { | |
| Some(Self::from_wh( | |
| self.width().checked_add(value)?, | |
| self.height().checked_add(value)?, | |
| )) | |
| } | |
| } | |
| impl From<[u16; 2]> for SizeU16 { | |
| fn from(value: [u16; 2]) -> Self { | |
| Self(value) | |
| } | |
| } | |
| impl From<(u16, u16)> for SizeU16 { | |
| fn from((width, height): (u16, u16)) -> Self { | |
| Self::from_wh(width, height) | |
| } | |
| } | |
| impl From<SizeU16> for (u16, u16) { | |
| fn from(size: SizeU16) -> Self { | |
| (size.width(), size.height()) | |
| } | |
| } | |
| impl Add for SizeU16 { | |
| type Output = Self; | |
| fn add(self, rhs: Self) -> Self::Output { | |
| // Shouldn't overflow for our use cases. | |
| Self::from_wh( | |
| self.width().checked_add(rhs.width()).unwrap(), | |
| self.height().checked_add(rhs.height()).unwrap(), | |
| ) | |
| } | |
| } | |
| impl Add<u16> for SizeU16 { | |
| type Output = Self; | |
| fn add(self, rhs: u16) -> Self::Output { | |
| self + Self::new(rhs) | |
| } | |
| } | |
| /// Padding for the four sides of a region. | |
| #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)] | |
| pub struct PaddingU16 { | |
| /// The left padding. | |
| pub left: u16, | |
| /// The top padding. | |
| pub top: u16, | |
| /// The right padding. | |
| pub right: u16, | |
| /// The bottom padding. | |
| pub bottom: u16, | |
| } | |
| impl PaddingU16 { | |
| /// Padding with all sides set to zero. | |
| pub const ZERO: Self = Self::new(0, 0, 0, 0); | |
| /// Create padding from its left, top, right, and bottom amounts. | |
| pub const fn new(left: u16, top: u16, right: u16, bottom: u16) -> Self { | |
| Self { | |
| left, | |
| top, | |
| right, | |
| bottom, | |
| } | |
| } | |
| } | |
| /// An axis-aligned rectangle with `u16` coordinates, stored as two corners `(x0, y0)` and | |
| /// `(x1, y1)`. | |
| /// | |
| /// `(x0, y0)` is the top-left (minimum) corner and `(x1, y1)` is the bottom-right (maximum) corner. | |
| /// The rectangle is considered to be empty when `x0 >= x1` or `y0 >= y1`. | |
| #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] | |
| pub struct RectU16 { | |
| /// The minimum x coordinate (left edge). | |
| pub x0: u16, | |
| /// The minimum y coordinate (top edge). | |
| pub y0: u16, | |
| /// The maximum x coordinate (right edge, exclusive). | |
| pub x1: u16, | |
| /// The maximum y coordinate (bottom edge, exclusive). | |
| pub y1: u16, | |
| } | |
| impl RectU16 { | |
| /// A rectangle with all coordinates set to zero. | |
| pub const ZERO: Self = Self { | |
| x0: 0, | |
| y0: 0, | |
| x1: 0, | |
| y1: 0, | |
| }; | |
| /// An empty, maximally inverted rectangle, useful as a starting value for incremental union | |
| /// operations. | |
| /// | |
| /// Has `(x0, y0) = (u16::MAX, u16::MAX)` and `(x1, y1) = (0, 0)`. | |
| pub const INVERTED: Self = Self { | |
| x0: u16::MAX, | |
| y0: u16::MAX, | |
| x1: 0, | |
| y1: 0, | |
| }; | |
| /// Create a new rectangle from its corner coordinates. | |
| #[inline(always)] | |
| pub const fn new(x0: u16, y0: u16, x1: u16, y1: u16) -> Self { | |
| Self { x0, y0, x1, y1 } | |
| } | |
| /// The width of the rectangle (`x1 - x0`), saturating at zero. | |
| #[inline(always)] | |
| pub const fn width(self) -> u16 { | |
| self.x1.saturating_sub(self.x0) | |
| } | |
| /// The height of the rectangle (`y1 - y0`), saturating at zero. | |
| #[inline(always)] | |
| pub const fn height(self) -> u16 { | |
| self.y1.saturating_sub(self.y0) | |
| } | |
| /// Returns `true` if the rectangle has zero area (`x0 >= x1` or `y0 >= y1`). | |
| #[inline(always)] | |
| pub const fn is_empty(self) -> bool { | |
| self.x0 >= self.x1 || self.y0 >= self.y1 | |
| } | |
| /// Check if a point `(x, y)` is contained within this rectangle. | |
| /// | |
| /// Returns `true` if `x0 <= x < x1` and `y0 <= y < y1`. | |
| #[inline(always)] | |
| pub const fn contains(self, x: u16, y: u16) -> bool { | |
| (x >= self.x0) & (x < self.x1) & (y >= self.y0) & (y < self.y1) | |
| } | |
| /// Compute the intersection of two rectangles. | |
| /// | |
| /// The result may have zero area if the rectangles do not overlap, but is never inverted. | |
| #[inline(always)] | |
| pub const fn intersect(self, other: Self) -> Self { | |
| let x0 = const_max(self.x0, other.x0); | |
| let y0 = const_max(self.y0, other.y0); | |
| let x1 = const_min(self.x1, other.x1); | |
| let y1 = const_min(self.y1, other.y1); | |
| Self::new(x0, y0, const_max(x1, x0), const_max(y1, y0)) | |
| } | |
| /// Expand this rectangle by the given left, top, right, and bottom padding. | |
| #[inline(always)] | |
| pub const fn expand(self, padding: PaddingU16) -> Self { | |
| Self { | |
| x0: self.x0.saturating_sub(padding.left), | |
| y0: self.y0.saturating_sub(padding.top), | |
| x1: self.x1.saturating_add(padding.right), | |
| y1: self.y1.saturating_add(padding.bottom), | |
| } | |
| } | |
| /// Return this rectangle relative to `origin`, clamping negative coordinates to zero. | |
| #[inline(always)] | |
| pub fn relative_to_origin(self, origin: (u16, u16)) -> Self { | |
| self.shift((-(origin.0 as i32), -(origin.1 as i32))) | |
| } | |
| /// Return a shifted version of the rectangle, clamping negative coordinates to zero. | |
| #[inline] | |
| pub fn shift(self, shift: (i32, i32)) -> Self { | |
| Self { | |
| x0: (self.x0 as i32) | |
| .saturating_add(shift.0) | |
| .clamp(0, u16::MAX as i32) as u16, | |
| y0: (self.y0 as i32) | |
| .saturating_add(shift.1) | |
| .clamp(0, u16::MAX as i32) as u16, | |
| x1: (self.x1 as i32) | |
| .saturating_add(shift.0) | |
| .clamp(0, u16::MAX as i32) as u16, | |
| y1: (self.y1 as i32) | |
| .saturating_add(shift.1) | |
| .clamp(0, u16::MAX as i32) as u16, | |
| } | |
| } | |
| /// Expand this rectangle to also cover `other` (union in place). | |
| /// | |
| /// The union of `self` with a [`Self::INVERTED`] returns `self`. | |
| #[inline(always)] | |
| pub const fn union(&mut self, other: Self) { | |
| self.x0 = const_min(self.x0, other.x0); | |
| self.y0 = const_min(self.y0, other.y0); | |
| self.x1 = const_max(self.x1, other.x1); | |
| self.y1 = const_max(self.y1, other.y1); | |
| } | |
| /// Return the rect as a [`Rect`]. | |
| pub fn as_rect(self) -> Rect { | |
| Rect::new( | |
| self.x0 as f64, | |
| self.y0 as f64, | |
| self.x1 as f64, | |
| self.y1 as f64, | |
| ) | |
| } | |
| } | |
| impl From<RectU16> for SizeU16 { | |
| fn from(rect: RectU16) -> Self { | |
| Self::from_wh(rect.width(), rect.height()) | |
| } | |
| } | |
| #[inline(always)] | |
| const fn const_max(a: u16, b: u16) -> u16 { | |
| if a > b { a } else { b } | |
| } | |
| #[inline(always)] | |
| const fn const_min(a: u16, b: u16) -> u16 { | |
| if a < b { a } else { b } | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use super::RectU16; | |
| #[test] | |
| fn rect_u16_relative_to_origin() { | |
| let rect = RectU16::new(10, 20, 30, 40); | |
| assert_eq!(rect.relative_to_origin((5, 12)), RectU16::new(5, 8, 25, 28)); | |
| } | |
| #[test] | |
| fn rect_u16_relative_to_origin_clamps_to_zero() { | |
| let rect = RectU16::new(10, 20, 30, 40); | |
| assert_eq!(rect.relative_to_origin((20, 35)), RectU16::new(0, 0, 10, 5)); | |
| } | |
| #[test] | |
| fn disjoint_intersection_is_empty_but_not_inverted() { | |
| let intersection = RectU16::new(0, 0, 4, 4).intersect(RectU16::new(8, 1, 12, 3)); | |
| assert_eq!(intersection, RectU16::new(8, 1, 8, 3)); | |
| assert!(intersection.is_empty()); | |
| assert!(intersection.x0 <= intersection.x1); | |
| assert!(intersection.y0 <= intersection.y1); | |
| } | |
| } |