Skip to main content

presentar_core/
geometry.rs

1//! Geometric primitives: Point, Size, Rect, `CornerRadius`.
2//!
3//! This module provides the fundamental geometric types used throughout Presentar
4//! for layout calculations and rendering.
5//!
6//! # Examples
7//!
8//! ```
9//! use presentar_core::{Point, Size, Rect};
10//!
11//! // Create points and calculate distances
12//! let p1 = Point::new(0.0, 0.0);
13//! let p2 = Point::new(3.0, 4.0);
14//! assert_eq!(p1.distance(&p2), 5.0);
15//!
16//! // Create sizes
17//! let size = Size::new(100.0, 50.0);
18//! assert_eq!(size.area(), 5000.0);
19//!
20//! // Create rectangles
21//! let rect = Rect::new(10.0, 20.0, 100.0, 50.0);
22//! assert!(rect.contains_point(&Point::new(50.0, 40.0)));
23//!
24//! // Rectangle intersection
25//! let r1 = Rect::new(0.0, 0.0, 100.0, 100.0);
26//! let r2 = Rect::new(50.0, 50.0, 100.0, 100.0);
27//! let inter = r1.intersection(&r2).expect("should intersect");
28//! assert_eq!(inter.width, 50.0);
29//! ```
30
31use provable_contracts_macros::contract;
32use serde::{Deserialize, Serialize};
33use std::ops::{Add, Sub};
34
35/// A 2D point with x and y coordinates.
36#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
37pub struct Point {
38    /// X coordinate
39    pub x: f32,
40    /// Y coordinate
41    pub y: f32,
42}
43
44impl Point {
45    /// Origin point (0, 0)
46    pub const ORIGIN: Self = Self { x: 0.0, y: 0.0 };
47
48    /// Create a new point.
49    #[must_use]
50    pub const fn new(x: f32, y: f32) -> Self {
51        Self { x, y }
52    }
53
54    /// Calculate Euclidean distance to another point.
55    #[must_use]
56    pub fn distance(&self, other: &Self) -> f32 {
57        let dx = self.x - other.x;
58        let dy = self.y - other.y;
59        dx.hypot(dy)
60    }
61
62    /// Linear interpolation between two points.
63    #[must_use]
64    pub fn lerp(&self, other: &Self, t: f32) -> Self {
65        Self::new(
66            (other.x - self.x).mul_add(t, self.x),
67            (other.y - self.y).mul_add(t, self.y),
68        )
69    }
70}
71
72impl Default for Point {
73    fn default() -> Self {
74        Self::ORIGIN
75    }
76}
77
78impl Add for Point {
79    type Output = Self;
80
81    fn add(self, rhs: Self) -> Self::Output {
82        Self::new(self.x + rhs.x, self.y + rhs.y)
83    }
84}
85
86impl Sub for Point {
87    type Output = Self;
88
89    fn sub(self, rhs: Self) -> Self::Output {
90        Self::new(self.x - rhs.x, self.y - rhs.y)
91    }
92}
93
94/// A 2D size with width and height.
95#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
96pub struct Size {
97    /// Width
98    pub width: f32,
99    /// Height
100    pub height: f32,
101}
102
103impl Size {
104    /// Zero size
105    pub const ZERO: Self = Self {
106        width: 0.0,
107        height: 0.0,
108    };
109
110    /// Create a new size.
111    #[must_use]
112    pub const fn new(width: f32, height: f32) -> Self {
113        Self { width, height }
114    }
115
116    /// Calculate area.
117    #[must_use]
118    pub fn area(&self) -> f32 {
119        self.width * self.height
120    }
121
122    /// Calculate aspect ratio (width / height).
123    #[must_use]
124    pub fn aspect_ratio(&self) -> f32 {
125        if self.height == 0.0 {
126            0.0
127        } else {
128            self.width / self.height
129        }
130    }
131
132    /// Check if this size can contain another size.
133    #[must_use]
134    pub fn contains(&self, other: &Self) -> bool {
135        self.width >= other.width && self.height >= other.height
136    }
137
138    /// Scale size by a factor.
139    #[must_use]
140    pub fn scale(&self, factor: f32) -> Self {
141        Self::new(self.width * factor, self.height * factor)
142    }
143}
144
145impl Default for Size {
146    fn default() -> Self {
147        Self::ZERO
148    }
149}
150
151/// A rectangle defined by position and size.
152#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
153pub struct Rect {
154    /// X position of top-left corner
155    pub x: f32,
156    /// Y position of top-left corner
157    pub y: f32,
158    /// Width
159    pub width: f32,
160    /// Height
161    pub height: f32,
162}
163
164impl Rect {
165    /// Create a new rectangle.
166    #[must_use]
167    pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
168        Self {
169            x,
170            y,
171            width,
172            height,
173        }
174    }
175
176    /// Create from two corner points.
177    #[must_use]
178    pub fn from_points(top_left: Point, bottom_right: Point) -> Self {
179        Self::new(
180            top_left.x,
181            top_left.y,
182            bottom_right.x - top_left.x,
183            bottom_right.y - top_left.y,
184        )
185    }
186
187    /// Create from size at origin.
188    #[must_use]
189    pub const fn from_size(size: Size) -> Self {
190        Self::new(0.0, 0.0, size.width, size.height)
191    }
192
193    /// Get the origin (top-left) point.
194    #[must_use]
195    pub const fn origin(&self) -> Point {
196        Point::new(self.x, self.y)
197    }
198
199    /// Get the size.
200    #[must_use]
201    pub const fn size(&self) -> Size {
202        Size::new(self.width, self.height)
203    }
204
205    /// Get the area.
206    #[must_use]
207    pub fn area(&self) -> f32 {
208        self.width * self.height
209    }
210
211    /// Get top-left corner.
212    #[must_use]
213    pub const fn top_left(&self) -> Point {
214        Point::new(self.x, self.y)
215    }
216
217    /// Get top-right corner.
218    #[must_use]
219    pub fn top_right(&self) -> Point {
220        Point::new(self.x + self.width, self.y)
221    }
222
223    /// Get bottom-left corner.
224    #[must_use]
225    pub fn bottom_left(&self) -> Point {
226        Point::new(self.x, self.y + self.height)
227    }
228
229    /// Get bottom-right corner.
230    #[must_use]
231    pub fn bottom_right(&self) -> Point {
232        Point::new(self.x + self.width, self.y + self.height)
233    }
234
235    /// Get center point.
236    #[must_use]
237    pub fn center(&self) -> Point {
238        Point::new(self.x + self.width / 2.0, self.y + self.height / 2.0)
239    }
240
241    /// Check if a point is inside the rectangle (inclusive).
242    #[must_use]
243    pub fn contains_point(&self, point: &Point) -> bool {
244        point.x >= self.x
245            && point.x <= self.x + self.width
246            && point.y >= self.y
247            && point.y <= self.y + self.height
248    }
249
250    /// Check if this rectangle intersects another.
251    #[must_use]
252    pub fn intersects(&self, other: &Self) -> bool {
253        self.x < other.x + other.width
254            && self.x + self.width > other.x
255            && self.y < other.y + other.height
256            && self.y + self.height > other.y
257    }
258
259    /// Calculate intersection with another rectangle.
260    #[must_use]
261    #[contract("rect-geometry-v1", equation = "intersection")]
262    pub fn intersection(&self, other: &Self) -> Option<Self> {
263        let x = self.x.max(other.x);
264        let y = self.y.max(other.y);
265        let right = (self.x + self.width).min(other.x + other.width);
266        let bottom = (self.y + self.height).min(other.y + other.height);
267
268        if right > x && bottom > y {
269            Some(Self::new(x, y, right - x, bottom - y))
270        } else {
271            None
272        }
273    }
274
275    /// Calculate union with another rectangle.
276    #[must_use]
277    pub fn union(&self, other: &Self) -> Self {
278        let x = self.x.min(other.x);
279        let y = self.y.min(other.y);
280        let right = (self.x + self.width).max(other.x + other.width);
281        let bottom = (self.y + self.height).max(other.y + other.height);
282
283        Self::new(x, y, right - x, bottom - y)
284    }
285
286    /// Create a new rectangle inset by the given amount on all sides.
287    #[must_use]
288    #[provable_contracts_macros::contract("rect-geometry-v1", equation = "inset")]
289    pub fn inset(&self, amount: f32) -> Self {
290        Self::new(
291            self.x + amount,
292            self.y + amount,
293            2.0f32.mul_add(-amount, self.width).max(0.0),
294            2.0f32.mul_add(-amount, self.height).max(0.0),
295        )
296    }
297
298    /// Create a new rectangle with the given position.
299    #[must_use]
300    pub const fn with_origin(&self, origin: Point) -> Self {
301        Self::new(origin.x, origin.y, self.width, self.height)
302    }
303
304    /// Create a new rectangle with the given size.
305    #[must_use]
306    pub const fn with_size(&self, size: Size) -> Self {
307        Self::new(self.x, self.y, size.width, size.height)
308    }
309}
310
311impl Default for Rect {
312    fn default() -> Self {
313        Self::new(0.0, 0.0, 0.0, 0.0)
314    }
315}
316
317/// Corner radii for rounded rectangles.
318#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
319pub struct CornerRadius {
320    /// Top-left radius
321    pub top_left: f32,
322    /// Top-right radius
323    pub top_right: f32,
324    /// Bottom-right radius
325    pub bottom_right: f32,
326    /// Bottom-left radius
327    pub bottom_left: f32,
328}
329
330impl CornerRadius {
331    /// Zero radius
332    pub const ZERO: Self = Self {
333        top_left: 0.0,
334        top_right: 0.0,
335        bottom_right: 0.0,
336        bottom_left: 0.0,
337    };
338
339    /// Create corner radii with individual values.
340    #[must_use]
341    pub const fn new(top_left: f32, top_right: f32, bottom_right: f32, bottom_left: f32) -> Self {
342        Self {
343            top_left,
344            top_right,
345            bottom_right,
346            bottom_left,
347        }
348    }
349
350    /// Create uniform corner radius.
351    #[must_use]
352    pub const fn uniform(radius: f32) -> Self {
353        Self::new(radius, radius, radius, radius)
354    }
355
356    /// Check if all corners have zero radius.
357    #[must_use]
358    pub fn is_zero(&self) -> bool {
359        self.top_left == 0.0
360            && self.top_right == 0.0
361            && self.bottom_right == 0.0
362            && self.bottom_left == 0.0
363    }
364
365    /// Check if all corners have the same radius.
366    #[must_use]
367    pub fn is_uniform(&self) -> bool {
368        self.top_left == self.top_right
369            && self.top_right == self.bottom_right
370            && self.bottom_right == self.bottom_left
371    }
372}
373
374impl Default for CornerRadius {
375    fn default() -> Self {
376        Self::ZERO
377    }
378}
379
380#[cfg(test)]
381#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
382mod tests {
383    use super::*;
384
385    #[test]
386    fn test_point_default() {
387        assert_eq!(Point::default(), Point::ORIGIN);
388    }
389
390    #[test]
391    fn test_point_lerp() {
392        let p1 = Point::new(0.0, 0.0);
393        let p2 = Point::new(10.0, 10.0);
394        let mid = p1.lerp(&p2, 0.5);
395        assert_eq!(mid, Point::new(5.0, 5.0));
396    }
397
398    #[test]
399    fn test_size_default() {
400        assert_eq!(Size::default(), Size::ZERO);
401    }
402
403    #[test]
404    fn test_size_scale() {
405        let s = Size::new(10.0, 20.0);
406        assert_eq!(s.scale(2.0), Size::new(20.0, 40.0));
407    }
408
409    #[test]
410    fn test_rect_default() {
411        let r = Rect::default();
412        assert_eq!(r.x, 0.0);
413        assert_eq!(r.area(), 0.0);
414    }
415
416    #[test]
417    fn test_corner_radius_is_uniform() {
418        assert!(CornerRadius::uniform(10.0).is_uniform());
419        assert!(!CornerRadius::new(1.0, 2.0, 3.0, 4.0).is_uniform());
420    }
421
422    #[test]
423    fn test_corner_radius_is_zero() {
424        assert!(CornerRadius::ZERO.is_zero());
425        assert!(!CornerRadius::uniform(1.0).is_zero());
426    }
427
428    // ===== Point Tests =====
429
430    #[test]
431    fn test_point_new() {
432        let p = Point::new(10.0, 20.0);
433        assert_eq!(p.x, 10.0);
434        assert_eq!(p.y, 20.0);
435    }
436
437    #[test]
438    fn test_point_distance() {
439        let p1 = Point::new(0.0, 0.0);
440        let p2 = Point::new(3.0, 4.0);
441        assert_eq!(p1.distance(&p2), 5.0);
442    }
443
444    #[test]
445    fn test_point_add() {
446        let p1 = Point::new(1.0, 2.0);
447        let p2 = Point::new(3.0, 4.0);
448        assert_eq!(p1 + p2, Point::new(4.0, 6.0));
449    }
450
451    #[test]
452    fn test_point_sub() {
453        let p1 = Point::new(5.0, 7.0);
454        let p2 = Point::new(2.0, 3.0);
455        assert_eq!(p1 - p2, Point::new(3.0, 4.0));
456    }
457
458    // ===== Size Tests =====
459
460    #[test]
461    fn test_size_new() {
462        let s = Size::new(100.0, 50.0);
463        assert_eq!(s.width, 100.0);
464        assert_eq!(s.height, 50.0);
465    }
466
467    #[test]
468    fn test_size_area() {
469        let s = Size::new(10.0, 20.0);
470        assert_eq!(s.area(), 200.0);
471    }
472
473    #[test]
474    fn test_size_aspect_ratio() {
475        let s = Size::new(16.0, 9.0);
476        assert!((s.aspect_ratio() - 1.777).abs() < 0.01);
477    }
478
479    #[test]
480    fn test_size_aspect_ratio_zero_height() {
481        let s = Size::new(10.0, 0.0);
482        assert_eq!(s.aspect_ratio(), 0.0);
483    }
484
485    #[test]
486    fn test_size_contains() {
487        let big = Size::new(100.0, 100.0);
488        let small = Size::new(50.0, 50.0);
489        assert!(big.contains(&small));
490        assert!(!small.contains(&big));
491    }
492
493    // ===== Rect Tests =====
494
495    #[test]
496    fn test_rect_from_points() {
497        let r = Rect::from_points(Point::new(10.0, 20.0), Point::new(50.0, 70.0));
498        assert_eq!(r.x, 10.0);
499        assert_eq!(r.y, 20.0);
500        assert_eq!(r.width, 40.0);
501        assert_eq!(r.height, 50.0);
502    }
503
504    #[test]
505    fn test_rect_from_size() {
506        let r = Rect::from_size(Size::new(100.0, 50.0));
507        assert_eq!(r.x, 0.0);
508        assert_eq!(r.y, 0.0);
509        assert_eq!(r.width, 100.0);
510    }
511
512    #[test]
513    fn test_rect_corners() {
514        let r = Rect::new(10.0, 20.0, 100.0, 50.0);
515        assert_eq!(r.top_left(), Point::new(10.0, 20.0));
516        assert_eq!(r.top_right(), Point::new(110.0, 20.0));
517        assert_eq!(r.bottom_left(), Point::new(10.0, 70.0));
518        assert_eq!(r.bottom_right(), Point::new(110.0, 70.0));
519    }
520
521    #[test]
522    fn test_rect_center() {
523        let r = Rect::new(0.0, 0.0, 100.0, 50.0);
524        assert_eq!(r.center(), Point::new(50.0, 25.0));
525    }
526
527    #[test]
528    fn test_rect_contains_point() {
529        let r = Rect::new(10.0, 10.0, 100.0, 100.0);
530        assert!(r.contains_point(&Point::new(50.0, 50.0)));
531        assert!(r.contains_point(&Point::new(10.0, 10.0))); // Edge
532        assert!(!r.contains_point(&Point::new(5.0, 50.0)));
533    }
534
535    #[test]
536    fn test_rect_intersects() {
537        let r1 = Rect::new(0.0, 0.0, 100.0, 100.0);
538        let r2 = Rect::new(50.0, 50.0, 100.0, 100.0);
539        let r3 = Rect::new(200.0, 200.0, 50.0, 50.0);
540        assert!(r1.intersects(&r2));
541        assert!(!r1.intersects(&r3));
542    }
543
544    #[test]
545    fn test_rect_intersection() {
546        let r1 = Rect::new(0.0, 0.0, 100.0, 100.0);
547        let r2 = Rect::new(50.0, 50.0, 100.0, 100.0);
548        let inter = r1.intersection(&r2).unwrap();
549        assert_eq!(inter, Rect::new(50.0, 50.0, 50.0, 50.0));
550    }
551
552    #[test]
553    fn test_rect_intersection_none() {
554        let r1 = Rect::new(0.0, 0.0, 50.0, 50.0);
555        let r2 = Rect::new(100.0, 100.0, 50.0, 50.0);
556        assert!(r1.intersection(&r2).is_none());
557    }
558
559    #[test]
560    fn test_rect_union() {
561        let r1 = Rect::new(0.0, 0.0, 50.0, 50.0);
562        let r2 = Rect::new(25.0, 25.0, 50.0, 50.0);
563        let u = r1.union(&r2);
564        assert_eq!(u, Rect::new(0.0, 0.0, 75.0, 75.0));
565    }
566
567    #[test]
568    fn test_rect_inset() {
569        let r = Rect::new(0.0, 0.0, 100.0, 100.0);
570        let inset = r.inset(10.0);
571        assert_eq!(inset, Rect::new(10.0, 10.0, 80.0, 80.0));
572    }
573
574    #[test]
575    fn test_rect_inset_clamps() {
576        let r = Rect::new(0.0, 0.0, 20.0, 20.0);
577        let inset = r.inset(15.0);
578        assert_eq!(inset.width, 0.0);
579        assert_eq!(inset.height, 0.0);
580    }
581
582    #[test]
583    fn test_rect_with_origin() {
584        let r = Rect::new(0.0, 0.0, 100.0, 50.0);
585        let moved = r.with_origin(Point::new(20.0, 30.0));
586        assert_eq!(moved.x, 20.0);
587        assert_eq!(moved.y, 30.0);
588        assert_eq!(moved.width, 100.0);
589    }
590
591    #[test]
592    fn test_rect_with_size() {
593        let r = Rect::new(10.0, 20.0, 100.0, 50.0);
594        let resized = r.with_size(Size::new(200.0, 100.0));
595        assert_eq!(resized.x, 10.0);
596        assert_eq!(resized.width, 200.0);
597    }
598}