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