Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-core/src/validated_rect.rs
StratoSDK / crates / strato-core / src / validated_rect.rs
1//! Validated rectangle type with GPU-safe coordinate guarantees.
2//!
3//! # Safety Guarantees
4//!
5//! `ValidatedRect` provides the following invariants:
6//! - All coordinates are finite (no NaN, no Infinity)
7//! - Width and height are non-negative
8//! - All values are clamped to prevent GPU overflow
9//!
10//! # Example
11//!
12//! ```rust
13//! use strato_core::validated_rect::ValidatedRect;
14//!
15//! // Valid construction
16//! let rect = ValidatedRect::new(10.0, 20.0, 100.0, 50.0).unwrap();
17//!
18//! // Invalid construction (NaN)
19//! assert!(ValidatedRect::new(f32::NAN, 0.0, 10.0, 10.0).is_err());
20//! ```
21 
22use crate::error::TaffyValidationError;
23 
24/// Maximum coordinate value to prevent GPU overflow.
25///
26/// @note This is a conservative limit that all major GPUs can handle.
27/// @rationale Prevents floating-point precision issues at extreme values.
28const MAX_COORD: f32 = 1_000_000.0;
29 
30/// Rectangle with GUARANTEED validity of all coordinates.
31///
32/// # Invariants
33///
34/// All instances of `ValidatedRect` satisfy:
35/// - `x`, `y` are finite and in range `[-MAX_COORD, MAX_COORD]`
36/// - `width`, `height` are finite, non-negative, and `<= MAX_COORD`
37///
38/// These invariants are enforced at construction time and cannot be violated
39/// through safe Rust code.
40#[derive(Debug, Clone, Copy, PartialEq)]
41pub struct ValidatedRect {
42 x: f32,
43 y: f32,
44 width: f32,
45 height: f32,
46}
47 
48impl ValidatedRect {
49 /// Create a new validated rectangle.
50 ///
51 /// # Arguments
52 ///
53 /// * `x` - X position (must be finite)
54 /// * `y` - Y position (must be finite)
55 /// * `width` - Width (must be finite and >= 0.0)
56 /// * `height` - Height (must be finite and >= 0.0)
57 ///
58 /// # Returns
59 ///
60 /// * `Ok(ValidatedRect)` - If all values are valid
61 /// * `Err(TaffyValidationError)` - If any validation fails
62 ///
63 /// # Errors
64 ///
65 /// * `TaffyValidationError::NonFiniteValue` - If any value is NaN or Infinity
66 /// * `TaffyValidationError::NegativeDimension` - If width or height < 0.0
67 ///
68 /// # Safety
69 ///
70 /// Thread-safety: This function is thread-safe.
71 ///
72 /// # Performance
73 ///
74 /// WCET: O(1), ~10 floating-point comparisons
75 pub fn new(x: f32, y: f32, width: f32, height: f32) -> Result<Self, TaffyValidationError> {
76 // STEP 1: Check is_finite() for all 4 values
77 if !x.is_finite() || !y.is_finite() || !width.is_finite() || !height.is_finite() {
78 return Err(TaffyValidationError::NonFiniteValue);
79 }
80 
81 // STEP 2: Check width >= 0.0 && height >= 0.0
82 if width < 0.0 || height < 0.0 {
83 return Err(TaffyValidationError::NegativeDimension { width, height });
84 }
85 
86 // STEP 3: Clamp all values to safe range
87 let x_clamped = x.clamp(-MAX_COORD, MAX_COORD);
88 let y_clamped = y.clamp(-MAX_COORD, MAX_COORD);
89 let width_clamped = width.min(MAX_COORD);
90 let height_clamped = height.min(MAX_COORD);
91 
92 Ok(Self {
93 x: x_clamped,
94 y: y_clamped,
95 width: width_clamped,
96 height: height_clamped,
97 })
98 }
99 
100 /// Create a validated rectangle from Taffy layout output.
101 ///
102 /// # Arguments
103 ///
104 /// * `layout` - Taffy layout result
105 ///
106 /// # Returns
107 ///
108 /// * `Ok(ValidatedRect)` - If Taffy output is valid
109 /// * `Err(TaffyValidationError)` - If Taffy produced invalid coordinates
110 ///
111 /// # Note
112 ///
113 /// Taffy COULD potentially produce invalid values in edge cases
114 /// (e.g., overflow from extreme percentages). This function catches those.
115 pub fn from_taffy(layout: &taffy::Layout) -> Result<Self, TaffyValidationError> {
116 Self::new(
117 layout.location.x,
118 layout.location.y,
119 layout.size.width,
120 layout.size.height,
121 )
122 }
123 
124 /// Zero-size rectangle at origin.
125 ///
126 /// # Returns
127 ///
128 /// A valid rectangle with x=0, y=0, width=0, height=0.
129 #[inline]
130 pub fn zero() -> Self {
131 // SAFETY: All values are 0.0, which is finite and non-negative
132 Self {
133 x: 0.0,
134 y: 0.0,
135 width: 0.0,
136 height: 0.0,
137 }
138 }
139 
140 /// Get X position.
141 #[inline]
142 pub fn x(&self) -> f32 {
143 self.x
144 }
145 
146 /// Get Y position.
147 #[inline]
148 pub fn y(&self) -> f32 {
149 self.y
150 }
151 
152 /// Get width.
153 #[inline]
154 pub fn width(&self) -> f32 {
155 self.width
156 }
157 
158 /// Get height.
159 #[inline]
160 pub fn height(&self) -> f32 {
161 self.height
162 }
163 
164 /// Get right edge (x + width).
165 #[inline]
166 pub fn right(&self) -> f32 {
167 self.x + self.width
168 }
169 
170 /// Get bottom edge (y + height).
171 #[inline]
172 pub fn bottom(&self) -> f32 {
173 self.y + self.height
174 }
175 
176 /// Check if a point is inside this rectangle.
177 ///
178 /// # Arguments
179 ///
180 /// * `px` - Point X coordinate
181 /// * `py` - Point Y coordinate
182 ///
183 /// # Returns
184 ///
185 /// `true` if point is inside or on the boundary.
186 #[inline]
187 pub fn contains(&self, px: f32, py: f32) -> bool {
188 px >= self.x && px <= self.right() && py >= self.y && py <= self.bottom()
189 }
190 
191 /// Convert to tuple (x, y, width, height).
192 #[inline]
193 pub fn to_tuple(&self) -> (f32, f32, f32, f32) {
194 (self.x, self.y, self.width, self.height)
195 }
196 
197 /// Convert to array [x, y, width, height].
198 #[inline]
199 pub fn to_array(&self) -> [f32; 4] {
200 [self.x, self.y, self.width, self.height]
201 }
202}
203 
204impl Default for ValidatedRect {
205 fn default() -> Self {
206 Self::zero()
207 }
208}
209 
210#[cfg(test)]
211mod tests {
212 use super::*;
213 
214 #[test]
215 fn test_validated_rect_valid_creation() {
216 let rect = ValidatedRect::new(10.0, 20.0, 100.0, 50.0);
217 assert!(rect.is_ok());
218 let rect = rect.unwrap();
219 assert_eq!(rect.x(), 10.0);
220 assert_eq!(rect.y(), 20.0);
221 assert_eq!(rect.width(), 100.0);
222 assert_eq!(rect.height(), 50.0);
223 }
224 
225 #[test]
226 fn test_validated_rect_rejects_nan() {
227 assert!(ValidatedRect::new(f32::NAN, 0.0, 10.0, 10.0).is_err());
228 assert!(ValidatedRect::new(0.0, f32::NAN, 10.0, 10.0).is_err());
229 assert!(ValidatedRect::new(0.0, 0.0, f32::NAN, 10.0).is_err());
230 assert!(ValidatedRect::new(0.0, 0.0, 10.0, f32::NAN).is_err());
231 }
232 
233 #[test]
234 fn test_validated_rect_rejects_infinity() {
235 assert!(ValidatedRect::new(f32::INFINITY, 0.0, 10.0, 10.0).is_err());
236 assert!(ValidatedRect::new(f32::NEG_INFINITY, 0.0, 10.0, 10.0).is_err());
237 assert!(ValidatedRect::new(0.0, 0.0, f32::INFINITY, 10.0).is_err());
238 }
239 
240 #[test]
241 fn test_validated_rect_rejects_negative_dimensions() {
242 let result = ValidatedRect::new(0.0, 0.0, -10.0, 10.0);
243 assert!(matches!(
244 result,
245 Err(TaffyValidationError::NegativeDimension { .. })
246 ));
247 
248 let result = ValidatedRect::new(0.0, 0.0, 10.0, -5.0);
249 assert!(matches!(
250 result,
251 Err(TaffyValidationError::NegativeDimension { .. })
252 ));
253 }
254 
255 #[test]
256 fn test_validated_rect_allows_negative_position() {
257 // Negative x/y is valid (off-screen rendering)
258 let rect = ValidatedRect::new(-50.0, -30.0, 100.0, 50.0);
259 assert!(rect.is_ok());
260 let rect = rect.unwrap();
261 assert_eq!(rect.x(), -50.0);
262 assert_eq!(rect.y(), -30.0);
263 }
264 
265 #[test]
266 fn test_validated_rect_clamps_extreme_values() {
267 let rect = ValidatedRect::new(2_000_000.0, -2_000_000.0, 10.0, 10.0).unwrap();
268 assert!(rect.x() <= MAX_COORD);
269 assert!(rect.y() >= -MAX_COORD);
270 }
271 
272 #[test]
273 fn test_validated_rect_zero() {
274 let rect = ValidatedRect::zero();
275 assert_eq!(rect.x(), 0.0);
276 assert_eq!(rect.y(), 0.0);
277 assert_eq!(rect.width(), 0.0);
278 assert_eq!(rect.height(), 0.0);
279 }
280 
281 #[test]
282 fn test_validated_rect_contains() {
283 let rect = ValidatedRect::new(10.0, 10.0, 20.0, 20.0).unwrap();
284 
285 // Inside
286 assert!(rect.contains(15.0, 15.0));
287 assert!(rect.contains(20.0, 20.0));
288 
289 // On boundary
290 assert!(rect.contains(10.0, 10.0));
291 assert!(rect.contains(30.0, 30.0));
292 
293 // Outside
294 assert!(!rect.contains(5.0, 15.0));
295 assert!(!rect.contains(15.0, 5.0));
296 assert!(!rect.contains(35.0, 15.0));
297 }
298 
299 #[test]
300 fn test_validated_rect_edges() {
301 let rect = ValidatedRect::new(10.0, 20.0, 100.0, 50.0).unwrap();
302 assert_eq!(rect.right(), 110.0);
303 assert_eq!(rect.bottom(), 70.0);
304 }
305}
306