| // 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::num::TryFromIntError; |
| 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)) |
| } |
| } |
| |
| impl From<[u16; 2]> for SizeU16 { |
| fn from(value: [u16; 2]) -> Self { |
| Self(value) |
| } |
| } |
| |
| 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()) |
| } |
| } |
| |
| // TODO: Remove these types once we've completely moved to u16 everywhere in Vello Hybrid. |
| |
| /// An offset represented by two 32-bit unsigned integers. |
| #[repr(C)] |
| #[derive(Copy, Clone, Debug, Pod, Zeroable, PartialEq, Eq)] |
| pub struct OffsetU32(pub [u32; 2]); |
| |
| impl OffsetU32 { |
| /// A zero offset. |
| pub const ZERO: Self = Self::new(0); |
| |
| /// Create a new offset with equal x and y coordinates. |
| pub const fn new(offset: u32) -> Self { |
| Self([offset; 2]) |
| } |
| |
| /// Create a new offset from its x and y coordinates. |
| pub const fn from_xy(x: u32, y: u32) -> Self { |
| Self([x, y]) |
| } |
| |
| /// The x coordinate of this offset. |
| pub const fn x(self) -> u32 { |
| self.0[0] |
| } |
| |
| /// The y coordinate of this offset. |
| pub const fn y(self) -> u32 { |
| self.0[1] |
| } |
| } |
| |
| impl From<[u32; 2]> for OffsetU32 { |
| fn from(value: [u32; 2]) -> Self { |
| Self(value) |
| } |
| } |
| |
| /// A size represented by two 32-bit unsigned integers. |
| #[repr(C)] |
| #[derive(Copy, Clone, Debug, Pod, Zeroable, PartialEq, Eq)] |
| pub struct SizeU32(pub [u32; 2]); |
| |
| impl SizeU32 { |
| /// A zero size. |
| pub const ZERO: Self = Self::new(0); |
| |
| /// Create a new square size. |
| pub const fn new(size: u32) -> Self { |
| Self([size; 2]) |
| } |
| |
| /// Create a new size from its width and height. |
| pub const fn from_wh(width: u32, height: u32) -> Self { |
| Self([width, height]) |
| } |
| |
| /// The width of this size. |
| pub const fn width(self) -> u32 { |
| self.0[0] |
| } |
| |
| /// The height of this size. |
| pub const fn height(self) -> u32 { |
| 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: u32, max: u32) -> Self { |
| Self::from_wh(self.width().clamp(min, max), self.height().clamp(min, max)) |
| } |
| } |
| |
| impl From<[u32; 2]> for SizeU32 { |
| fn from(value: [u32; 2]) -> Self { |
| Self(value) |
| } |
| } |
| |
| impl From<(u32, u32)> for SizeU32 { |
| fn from((width, height): (u32, u32)) -> Self { |
| Self::from_wh(width, height) |
| } |
| } |
| |
| impl From<SizeU32> for (u32, u32) { |
| fn from(size: SizeU32) -> Self { |
| (size.width(), size.height()) |
| } |
| } |
| |
| impl From<SizeU16> for SizeU32 { |
| fn from(size: SizeU16) -> Self { |
| Self::from_wh(u32::from(size.width()), u32::from(size.height())) |
| } |
| } |
| |
| impl TryFrom<SizeU32> for SizeU16 { |
| type Error = TryFromIntError; |
| |
| fn try_from(size: SizeU32) -> Result<Self, Self::Error> { |
| Ok(Self::from_wh( |
| u16::try_from(size.width())?, |
| u16::try_from(size.height())?, |
| )) |
| } |
| } |
| |
| impl Add for SizeU32 { |
| type Output = Self; |
| |
| fn add(self, rhs: Self) -> Self::Output { |
| Self::from_wh(self.width() + rhs.width(), self.height() + rhs.height()) |
| } |
| } |
| |
| impl Add<u32> for SizeU32 { |
| type Output = Self; |
| |
| fn add(self, rhs: u32) -> Self::Output { |
| self + Self::new(rhs) |
| } |
| } |
| |
| /// An axis-aligned rectangle with `u32` coordinates. |
| #[repr(C)] |
| #[derive(Copy, Clone, Debug, Pod, Zeroable, PartialEq, Eq)] |
| pub struct RectU32 { |
| /// The minimum x coordinate. |
| pub x0: u32, |
| /// The minimum y coordinate. |
| pub y0: u32, |
| /// The exclusive maximum x coordinate. |
| pub x1: u32, |
| /// The exclusive maximum y coordinate. |
| pub y1: u32, |
| } |
| |
| impl RectU32 { |
| /// Create a new rectangle from its corner coordinates. |
| pub const fn new(x0: u32, y0: u32, x1: u32, y1: u32) -> Self { |
| Self { x0, y0, x1, y1 } |
| } |
| |
| /// The width of this rectangle. |
| pub const fn width(self) -> u32 { |
| self.x1.saturating_sub(self.x0) |
| } |
| |
| /// The height of this rectangle. |
| pub const fn height(self) -> u32 { |
| self.y1.saturating_sub(self.y0) |
| } |
| } |
| |
| #[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); |
| } |
| } |