Skip to main content

xberg_native_pdf/geometry/
mod.rs

1//! Geometric primitives for layout analysis.
2//!
3//! This module provides basic geometric types and operations used throughout
4//! the layout analysis algorithms.
5
6#![forbid(unsafe_code)]
7
8/// A 2D point in document space.
9#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, Default)]
10pub struct Point {
11    /// X coordinate
12    pub x: f32,
13    /// Y coordinate
14    pub y: f32,
15}
16
17impl Point {
18    /// Create a new point.
19    ///
20    /// # Examples
21    ///
22    /// ```
23    /// use xberg_native_pdf::geometry::Point;
24    ///
25    /// let point = Point::new(10.0, 20.0);
26    /// assert_eq!(point.x, 10.0);
27    /// assert_eq!(point.y, 20.0);
28    /// ```
29    pub fn new(x: f32, y: f32) -> Self {
30        Self { x, y }
31    }
32}
33
34/// A rectangle in document space.
35#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, Default)]
36pub struct Rect {
37    /// X coordinate of top-left corner
38    pub x: f32,
39    /// Y coordinate of top-left corner
40    pub y: f32,
41    /// Width of rectangle
42    pub width: f32,
43    /// Height of rectangle
44    pub height: f32,
45}
46
47impl Rect {
48    /// Create a new rectangle from position and dimensions.
49    /// Normalizes dimensions so width and height are always non-negative.
50    ///
51    /// # Examples
52    ///
53    /// ```
54    /// use xberg_native_pdf::geometry::Rect;
55    ///
56    /// let rect = Rect::new(0.0, 50.0, 100.0, -50.0);
57    /// assert_eq!(rect.y, 0.0);
58    /// assert_eq!(rect.height, 50.0);
59    /// ```
60    pub fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
61        let (nx, nw) = if width < 0.0 { (x + width, -width) } else { (x, width) };
62        let (ny, nh) = if height < 0.0 {
63            (y + height, -height)
64        } else {
65            (y, height)
66        };
67        Self {
68            x: nx,
69            y: ny,
70            width: nw,
71            height: nh,
72        }
73    }
74
75    /// Ensure width and height are non-negative.
76    pub fn normalize(self) -> Self {
77        Self::new(self.x, self.y, self.width, self.height)
78    }
79
80    /// Create a rectangle from two corner points.
81    ///
82    /// # Examples
83    ///
84    /// ```
85    /// use xberg_native_pdf::geometry::Rect;
86    ///
87    /// let rect = Rect::from_points(10.0, 20.0, 110.0, 70.0);
88    /// assert_eq!(rect.x, 10.0);
89    /// assert_eq!(rect.y, 20.0);
90    /// assert_eq!(rect.width, 100.0);
91    /// assert_eq!(rect.height, 50.0);
92    /// ```
93    pub fn from_points(x0: f32, y0: f32, x1: f32, y1: f32) -> Self {
94        Self {
95            x: x0,
96            y: y0,
97            width: x1 - x0,
98            height: y1 - y0,
99        }
100    }
101
102    /// Get the left edge x-coordinate.
103    pub fn left(&self) -> f32 {
104        self.x
105    }
106
107    /// Get the right edge x-coordinate.
108    pub fn right(&self) -> f32 {
109        self.x + self.width
110    }
111
112    /// Get the top edge y-coordinate.
113    pub fn top(&self) -> f32 {
114        self.y
115    }
116
117    /// Get the bottom edge y-coordinate.
118    pub fn bottom(&self) -> f32 {
119        self.y + self.height
120    }
121
122    /// Get the center point of the rectangle.
123    ///
124    /// # Examples
125    ///
126    /// ```
127    /// use xberg_native_pdf::geometry::Rect;
128    ///
129    /// let rect = Rect::new(0.0, 0.0, 100.0, 50.0);
130    /// let center = rect.center();
131    /// assert_eq!(center.x, 50.0);
132    /// assert_eq!(center.y, 25.0);
133    /// ```
134    pub fn center(&self) -> Point {
135        Point {
136            x: self.x + self.width / 2.0,
137            y: self.y + self.height / 2.0,
138        }
139    }
140
141    /// Check if this rectangle intersects with another.
142    ///
143    /// # Examples
144    ///
145    /// ```
146    /// use xberg_native_pdf::geometry::Rect;
147    ///
148    /// let r1 = Rect::new(0.0, 0.0, 100.0, 100.0);
149    /// let r2 = Rect::new(50.0, 50.0, 100.0, 100.0);
150    /// let r3 = Rect::new(200.0, 200.0, 100.0, 100.0);
151    ///
152    /// assert!(r1.intersects(&r2));
153    /// assert!(!r1.intersects(&r3));
154    /// ```
155    pub fn intersects(&self, other: &Rect) -> bool {
156        self.left() < other.right()
157            && self.right() > other.left()
158            && self.top() < other.bottom()
159            && self.bottom() > other.top()
160    }
161
162    /// Compute the intersection of this rectangle with another.
163    ///
164    /// Returns the overlapping region as a new rectangle, or None if they don't overlap.
165    ///
166    /// # Examples
167    ///
168    /// ```
169    /// use xberg_native_pdf::geometry::Rect;
170    ///
171    /// let r1 = Rect::new(0.0, 0.0, 100.0, 100.0);
172    /// let r2 = Rect::new(50.0, 50.0, 100.0, 100.0);
173    /// let r3 = Rect::new(200.0, 200.0, 100.0, 100.0);
174    ///
175    /// let inter = r1.intersection(&r2).unwrap();
176    /// assert_eq!(inter.x, 50.0);
177    /// assert_eq!(inter.y, 50.0);
178    /// assert_eq!(inter.width, 50.0);
179    /// assert_eq!(inter.height, 50.0);
180    ///
181    /// assert!(r1.intersection(&r3).is_none());
182    /// ```
183    pub fn intersection(&self, other: &Rect) -> Option<Rect> {
184        if !self.intersects(other) {
185            return None;
186        }
187
188        let x = self.left().max(other.left());
189        let y = self.top().max(other.top());
190        let right = self.right().min(other.right());
191        let bottom = self.bottom().min(other.bottom());
192
193        Some(Rect::new(x, y, right - x, bottom - y))
194    }
195
196    /// Check if this rectangle contains a point.
197    ///
198    /// # Examples
199    ///
200    /// ```
201    /// use xberg_native_pdf::geometry::{Rect, Point};
202    ///
203    /// let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
204    /// let p1 = Point::new(50.0, 50.0);
205    /// let p2 = Point::new(150.0, 150.0);
206    ///
207    /// assert!(rect.contains_point(&p1));
208    /// assert!(!rect.contains_point(&p2));
209    /// ```
210    pub fn contains_point(&self, p: &Point) -> bool {
211        p.x >= self.left() && p.x <= self.right() && p.y >= self.top() && p.y <= self.bottom()
212    }
213
214    /// Check if this rectangle fully contains another rectangle.
215    ///
216    /// # Examples
217    ///
218    /// ```
219    /// use xberg_native_pdf::geometry::Rect;
220    ///
221    /// let r1 = Rect::new(0.0, 0.0, 100.0, 100.0);
222    /// let r2 = Rect::new(10.0, 10.0, 50.0, 50.0);
223    /// let r3 = Rect::new(50.0, 50.0, 100.0, 100.0);
224    ///
225    /// assert!(r1.contains_rect(&r2));
226    /// assert!(!r1.contains_rect(&r3));
227    /// ```
228    pub fn contains_rect(&self, other: &Rect) -> bool {
229        other.left() >= self.left()
230            && other.right() <= self.right()
231            && other.top() >= self.top()
232            && other.bottom() <= self.bottom()
233    }
234
235    /// Compute the union of this rectangle with another.
236    ///
237    /// Returns the smallest rectangle that contains both rectangles.
238    ///
239    /// # Examples
240    ///
241    /// ```
242    /// use xberg_native_pdf::geometry::Rect;
243    ///
244    /// let r1 = Rect::new(0.0, 0.0, 50.0, 50.0);
245    /// let r2 = Rect::new(25.0, 25.0, 50.0, 50.0);
246    /// let union = r1.union(&r2);
247    ///
248    /// assert_eq!(union.x, 0.0);
249    /// assert_eq!(union.y, 0.0);
250    /// assert_eq!(union.right(), 75.0);
251    /// assert_eq!(union.bottom(), 75.0);
252    /// ```
253    pub fn union(&self, other: &Rect) -> Rect {
254        let x0 = self.left().min(other.left());
255        let y0 = self.top().min(other.top());
256        let x1 = self.right().max(other.right());
257        let y1 = self.bottom().max(other.bottom());
258        Rect::from_points(x0, y0, x1, y1)
259    }
260
261    /// Compute the area of the rectangle.
262    ///
263    /// # Examples
264    ///
265    /// ```
266    /// use xberg_native_pdf::geometry::Rect;
267    ///
268    /// let rect = Rect::new(0.0, 0.0, 100.0, 50.0);
269    /// assert_eq!(rect.area(), 5000.0);
270    /// ```
271    pub fn area(&self) -> f32 {
272        self.width * self.height
273    }
274}
275
276/// Compute the Euclidean distance between two points.
277///
278/// # Examples
279///
280/// ```
281/// use xberg_native_pdf::geometry::{Point, euclidean_distance};
282///
283/// let p1 = Point::new(0.0, 0.0);
284/// let p2 = Point::new(3.0, 4.0);
285///
286/// assert_eq!(euclidean_distance(&p1, &p2), 5.0);
287/// ```
288pub fn euclidean_distance(p1: &Point, p2: &Point) -> f32 {
289    ((p2.x - p1.x).powi(2) + (p2.y - p1.y).powi(2)).sqrt()
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295
296    #[test]
297    fn test_point_creation() {
298        let p = Point::new(10.0, 20.0);
299        assert_eq!(p.x, 10.0);
300        assert_eq!(p.y, 20.0);
301    }
302
303    #[test]
304    fn test_rect_creation() {
305        let r = Rect::new(5.0, 10.0, 100.0, 50.0);
306        assert_eq!(r.x, 5.0);
307        assert_eq!(r.y, 10.0);
308        assert_eq!(r.width, 100.0);
309        assert_eq!(r.height, 50.0);
310    }
311
312    #[test]
313    fn test_rect_from_points() {
314        let r = Rect::from_points(10.0, 20.0, 110.0, 70.0);
315        assert_eq!(r.x, 10.0);
316        assert_eq!(r.y, 20.0);
317        assert_eq!(r.width, 100.0);
318        assert_eq!(r.height, 50.0);
319    }
320
321    #[test]
322    fn test_rect_edges() {
323        let r = Rect::new(10.0, 20.0, 100.0, 50.0);
324        assert_eq!(r.left(), 10.0);
325        assert_eq!(r.right(), 110.0);
326        assert_eq!(r.top(), 20.0);
327        assert_eq!(r.bottom(), 70.0);
328    }
329
330    #[test]
331    fn test_rect_center() {
332        let r = Rect::new(0.0, 0.0, 100.0, 50.0);
333        let center = r.center();
334        assert_eq!(center.x, 50.0);
335        assert_eq!(center.y, 25.0);
336    }
337
338    #[test]
339    fn test_rect_intersects() {
340        let r1 = Rect::new(0.0, 0.0, 100.0, 100.0);
341        let r2 = Rect::new(50.0, 50.0, 100.0, 100.0);
342        let r3 = Rect::new(200.0, 200.0, 100.0, 100.0);
343
344        assert!(r1.intersects(&r2));
345        assert!(r2.intersects(&r1));
346        assert!(!r1.intersects(&r3));
347        assert!(!r3.intersects(&r1));
348    }
349
350    #[test]
351    fn test_rect_contains_point() {
352        let r = Rect::new(0.0, 0.0, 100.0, 100.0);
353        let p1 = Point::new(50.0, 50.0);
354        let p2 = Point::new(150.0, 150.0);
355        let p3 = Point::new(0.0, 0.0);
356        let p4 = Point::new(100.0, 100.0);
357
358        assert!(r.contains_point(&p1));
359        assert!(!r.contains_point(&p2));
360        assert!(r.contains_point(&p3));
361        assert!(r.contains_point(&p4));
362    }
363
364    #[test]
365    fn test_rect_union() {
366        let r1 = Rect::new(0.0, 0.0, 50.0, 50.0);
367        let r2 = Rect::new(25.0, 25.0, 50.0, 50.0);
368        let union = r1.union(&r2);
369
370        assert_eq!(union.x, 0.0);
371        assert_eq!(union.y, 0.0);
372        assert_eq!(union.right(), 75.0);
373        assert_eq!(union.bottom(), 75.0);
374    }
375
376    #[test]
377    fn test_rect_area() {
378        let r = Rect::new(0.0, 0.0, 100.0, 50.0);
379        assert_eq!(r.area(), 5000.0);
380    }
381
382    #[test]
383    fn test_euclidean_distance() {
384        let p1 = Point::new(0.0, 0.0);
385        let p2 = Point::new(3.0, 4.0);
386        assert_eq!(euclidean_distance(&p1, &p2), 5.0);
387
388        let p3 = Point::new(1.0, 1.0);
389        let p4 = Point::new(1.0, 1.0);
390        assert_eq!(euclidean_distance(&p3, &p4), 0.0);
391    }
392}