Skip to main content

incular_rendering/
geometry.rs

1use incular_core::finite_non_negative;
2use incular_core::{Offset, Rect, Size};
3
4/// Per-corner radii in logical pixels, ordered clockwise from the top left.
5#[derive(Clone, Copy, Debug, Default, PartialEq)]
6pub struct CornerRadii {
7    pub top_left: f32,
8    pub top_right: f32,
9    pub bottom_right: f32,
10    pub bottom_left: f32,
11}
12impl CornerRadii {
13    pub const ZERO: Self = Self::uniform(0.0);
14
15    #[must_use]
16    pub const fn uniform(radius: f32) -> Self {
17        Self {
18            top_left: radius,
19            top_right: radius,
20            bottom_right: radius,
21            bottom_left: radius,
22        }
23    }
24
25    #[must_use]
26    pub fn is_zero(&self) -> bool {
27        self.top_left <= 0.0
28            && self.top_right <= 0.0
29            && self.bottom_right <= 0.0
30            && self.bottom_left <= 0.0
31    }
32
33    /// Scales every corner uniformly, keeping mirrored (negative) scales
34    /// non-negative. Uniform world scales resolve rounded corners through
35    /// here instead of a path fallback.
36    #[must_use]
37    pub fn scaled(self, scale: f32) -> Self {
38        let scale = scale.abs();
39        Self {
40            top_left: self.top_left * scale,
41            top_right: self.top_right * scale,
42            bottom_right: self.bottom_right * scale,
43            bottom_left: self.bottom_left * scale,
44        }
45    }
46
47    /// CSS-compatible normalization: invalid values become zero and all four
48    /// radii scale together when an opposing pair exceeds an edge.
49    #[must_use]
50    pub fn normalized(self, size: incular_core::Size) -> Self {
51        let clean = |v: f32| finite_non_negative(v);
52        let mut r = Self {
53            top_left: clean(self.top_left),
54            top_right: clean(self.top_right),
55            bottom_right: clean(self.bottom_right),
56            bottom_left: clean(self.bottom_left),
57        };
58        let ratio = |edge: f32, sum: f32| if sum > 0. { edge / sum } else { 1. };
59        let scale = ratio(size.width, r.top_left + r.top_right)
60            .min(ratio(size.width, r.bottom_left + r.bottom_right))
61            .min(ratio(size.height, r.top_left + r.bottom_left))
62            .min(ratio(size.height, r.top_right + r.bottom_right))
63            .min(1.);
64        r.top_left *= scale;
65        r.top_right *= scale;
66        r.bottom_right *= scale;
67        r.bottom_left *= scale;
68        r
69    }
70    #[must_use]
71    pub fn inset(self, amount: f32) -> Self {
72        Self {
73            top_left: (self.top_left - amount).max(0.),
74            top_right: (self.top_right - amount).max(0.),
75            bottom_right: (self.bottom_right - amount).max(0.),
76            bottom_left: (self.bottom_left - amount).max(0.),
77        }
78    }
79}
80
81impl From<f32> for CornerRadii {
82    fn from(v: f32) -> Self {
83        Self::uniform(v)
84    }
85}
86
87#[derive(Clone, Copy, Debug, PartialEq)]
88pub struct RRect {
89    pub rect: Rect,
90    pub radii: CornerRadii,
91}
92impl RRect {
93    #[must_use]
94    pub fn new(rect: Rect, radii: CornerRadii) -> Self {
95        Self {
96            rect,
97            radii: radii.normalized(rect.size),
98        }
99    }
100    #[must_use]
101    pub fn uniform(rect: Rect, radius: f32) -> Self {
102        Self::new(rect, CornerRadii::uniform(radius))
103    }
104    #[must_use]
105    pub fn inset(self, amount: f32) -> Self {
106        let rect = Rect::from_origin_size(
107            Offset::new(self.rect.origin.x + amount, self.rect.origin.y + amount),
108            incular_core::Size::new(
109                (self.rect.size.width - 2. * amount).max(0.),
110                (self.rect.size.height - 2. * amount).max(0.),
111            ),
112        );
113        Self::new(rect, self.radii.inset(amount))
114    }
115    /// Exact containment for the same normalized corner geometry used by the
116    /// analytic renderer. Boundary points are included.
117    #[must_use]
118    pub fn contains(self, point: Offset) -> bool {
119        if !self.rect.contains(point) {
120            return false;
121        }
122        let local = point - self.rect.origin;
123        let size = self.rect.size;
124        let corner = if local.x < self.radii.top_left && local.y < self.radii.top_left {
125            Some((
126                self.radii.top_left,
127                self.radii.top_left,
128                self.radii.top_left,
129            ))
130        } else if local.x > size.width - self.radii.top_right && local.y < self.radii.top_right {
131            Some((
132                size.width - self.radii.top_right,
133                self.radii.top_right,
134                self.radii.top_right,
135            ))
136        } else if local.x > size.width - self.radii.bottom_right
137            && local.y > size.height - self.radii.bottom_right
138        {
139            Some((
140                size.width - self.radii.bottom_right,
141                size.height - self.radii.bottom_right,
142                self.radii.bottom_right,
143            ))
144        } else if local.x < self.radii.bottom_left && local.y > size.height - self.radii.bottom_left
145        {
146            Some((
147                self.radii.bottom_left,
148                size.height - self.radii.bottom_left,
149                self.radii.bottom_left,
150            ))
151        } else {
152            None
153        };
154        corner.is_none_or(|(cx, cy, radius)| {
155            radius <= 0.
156                || (local.x - cx).mul_add(local.x - cx, (local.y - cy) * (local.y - cy))
157                    <= radius * radius
158        })
159    }
160}
161
162pub(crate) fn union_rect(left: Rect, right: Rect) -> Rect {
163    let x0 = left.origin.x.min(right.origin.x);
164    let y0 = left.origin.y.min(right.origin.y);
165    let x1 = (left.origin.x + left.size.width).max(right.origin.x + right.size.width);
166    let y1 = (left.origin.y + left.size.height).max(right.origin.y + right.size.height);
167    Rect::from_origin_size(Offset::new(x0, y0), Size::new(x1 - x0, y1 - y0))
168}